Build Log: Building an AI Agent Terminal UI — Three Approaches Compared

The bottom line: Agent terminal UIs are having a renaissance. In the last 24 hours alone, Vercel shipped @ai-sdk/tui as part of AI SDK 7 (launched 11 hours ago), OpenRouter’s create-agent-tui scaffold has been gaining traction since April, and pyratatui landed on PyPI in June bringing Ratatui’s Rust-powered TUI rendering to Python. This build log walks through all three approaches with real code, so you can pick the one that fits your stack.
Why Terminal UIs for Agents?
Before the web API wrappers, before the React dashboards, there was the terminal. Every modern AI coding agent — Claude Code, Codex, Pi — runs as a terminal UI. Why?
- No build step. Start coding, start chatting.
- Rich interactivity. ANSI colors, streaming output, progress spinners, inline tool call displays.
- Local tool access. Filesystem, shell, network — all available from the same process.
- Low overhead. A TUI is a single process. No server, no database, no bundler.
The agent harness ecosystem [1] has matured to the point where building your own TUI agent is a weekend project. Here’s how three different approaches stack up.
Approach 1: Vercel @ai-sdk/tui (TypeScript)
Vercel shipped AI SDK 7 on June 25, 2026 — less than 12 hours ago as of this writing [2]. The headline feature is the @ai-sdk/tui package, which wraps ToolLoopAgent in an interactive terminal interface with zero configuration.
Scaffold a project
mkdir my-tui-agent && cd my-tui-agent
npm init -y
npm install ai @ai-sdk/tui @ai-sdk/openai
Define an agent with tools
import { tool } from 'ai';
import { z } from 'zod';
import { runAgentTUI } from '@ai-sdk/tui';
import { openai } from '@ai-sdk/openai';
const agent = {
model: openai('gpt-4o'),
tools: {
get_weather: tool({
description: 'Get the current weather for a location',
parameters: z.object({
location: z.string().describe('City name'),
}),
execute: async ({ location }) => {
return { temp: 72, condition: 'sunny', location };
},
}),
read_file: tool({
description: 'Read a file from the local filesystem',
parameters: z.object({ path: z.string() }),
execute: async ({ path }) => {
// read file logic here
},
}),
},
};
await runAgentTUI({ agent });
Three lines of scaffolding, and your agent is live in the terminal. The TUI handles:
- Streaming token output with color-coded tool calls
- Reasoning display when the model uses chain-of-thought
- Inline tool results rendered in the conversation
- Keyboard shortcuts for editing, retrying, and clearing
What I liked
The ergonomics are the best of the three. runAgentTUI is a single function call. The Vercel AI SDK’s tool() builder with Zod schemas gives you automatic JSON schema generation and type-safe execution. If you’re already in the TypeScript/Next.js ecosystem, this is the fastest path to a working agent TUI.
What’s missing
It’s locked to Vercel’s ToolLoopAgent abstraction. You can’t drop in a custom loop, inject middleware, or use a non-OpenAI-compatible model without adding a provider adapter. The TUI itself is also not customizable — you get Vercel’s styling and layout, period.
Approach 2: OpenRouter create-agent-tui (TypeScript)
OpenRouter’s create-agent-tui [3] takes a different approach: it scaffolds a full, customizable agent harness as a standalone project. Think of it as Create React App for terminal agents.
Scaffold
npx @openrouter/create-agent-tui my-agent
cd my-agent
npm run dev
What you get
The scaffold produces a complete TypeScript project with:
my-agent/
├── src/
│ ├── index.ts # Entry point
│ ├── agent.ts # Agent loop
│ ├── tools/ # Tool definitions
│ │ ├── filesystem.ts
│ │ ├── shell.ts
│ │ └── web.ts
│ ├── tui/ # Terminal UI components
│ │ ├── renderer.ts
│ │ ├── input.ts
│ │ └── theme.ts
│ └── config.ts # Model, provider, API keys
├── package.json
└── tsconfig.json
The TUI is built on @openrouter/agent-harness which provides the core loop — tool execution, error recovery, streaming response handling — while the tui/ directory exposes the rendering layer you can customize.
Custom rendering
import { createAgentUI } from '@openrouter/agent-harness/tui';
import { myTheme } from './tui/theme';
const ui = createAgentUI({
theme: myTheme, // Custom colors, borders
toolDisplay: 'inline', // Show tool calls inline or in a sidebar
streamingChars: '⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏', // Custom spinner
maxToolCalls: 10, // Limit visible tool calls
});
await ui.start();
What I liked
This is the most extensible of the three. You own the entire stack — the agent loop, the tool system, the renderer, the config. The scaffold gives you a solid foundation but doesn’t hide the wiring. If you need to add custom tool output rendering, streaming animations, or multi-model routing, you have the source.
Drawbacks
It’s heavier than @ai-sdk/tui. The scaffold installs ~80 npm packages. You also need an OpenRouter API key by default, though you can swap in any OpenAI-compatible provider. The terminal UI is terminal-based layout (blessed/ink), not true ANSI rendering — it works but won’t win beauty contests.
Approach 3: Custom Python TUI with pyratatui
For the Python crowd, pyratatui [4] brings the Rust Ratatui library to Python via PyO3 bindings. This gives you full ANSI rendering with zero overhead, composable widgets, and an event loop that’s natural for Python developers.
Install
pip install pyratatui
Build a minimal agent TUI
import asyncio
from pyratatui import App, Widget, Colors, widgets
from openai import AsyncOpenAI
client = AsyncOpenAI()
class AgentTUI(App):
def __init__(self):
super().__init__()
self.conversation = []
self.input_buffer = ""
self.status = "ready"
async def on_key(self, event):
if event.key == "Enter" and self.input_buffer.strip():
await self.run_tool_call(self.input_buffer.strip())
self.input_buffer = ""
elif event.key.char:
self.input_buffer += event.key.char
async def run_tool_call(self, prompt: str):
self.status = "thinking"
self.refresh()
response = await client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}],
tools=[{
"type": "function",
"function": {
"name": "get_weather",
"parameters": {
"type": "object",
"properties": {
"location": {"type": "string"}
}
}
}
}],
stream=True
)
full_response = ""
async for chunk in response:
if chunk.choices[0].delta.content:
full_response += chunk.choices[0].delta.content
self.conversation.append(("user", prompt))
self.conversation.append(("assistant", full_response))
self.status = "ready"
self.refresh()
def render(self, area):
# Header
widgets.block(
area.top(3),
text="Agent TUI (pyratatui)",
style=Colors.bright_green.on_dark_gray(),
)
# Conversation
y = 3
for role, text in self.conversation[-20:]:
color = Colors.cyan if role == "user" else Colors.white
widgets.text(area.at(y, 2), f"{role}: {text[:80]}",
style=color)
y += 1
# Input bar
widgets.text(area.bottom(1), f"> {self.input_buffer}",
style=Colors.yellow)
if __name__ == "__main__":
app = AgentTUI()
app.run()
This is a minimal version — about 60 lines for a working agent TUI. The pyratatui library gives you widgets for layout management, text rendering, styled blocks, progress bars, and input handling, all rendered at ~60fps via Rust’s terminal backend.
What I liked
Full control over rendering. Python’s async model maps cleanly to streaming LLM responses. PyRatatui’s widget system is compositional — you can build complex UIs with tables, tabs, scrollable logs, and status bars. If you’re building a monitoring dashboard for multiple agents or a batch-processing TUI, this is the right choice.
Drawbacks
You’re writing more code. There’s no built-in agent loop — you have to handle tool calling, error recovery, and conversation management yourself. There’s also no streaming toolkit for LLM responses; you pipe raw API chunks through your renderer.
Head-to-Head Comparison
| Feature | @ai-sdk/tui | create-agent-tui | pyratatui |
|---|---|---|---|
| Language | TypeScript | TypeScript | Python |
| Lines to MVP | ~15 | Scaffold + ~10 | ~60 |
| Custom TUI | Minimal | Full source access | Full source access |
| Built-in agent loop | Yes (ToolLoopAgent) | Yes (agent-harness) | No (build your own) |
| Multi-model | Via AI SDK providers | Via OpenRouter routing | Via OpenAI SDK |
| Streaming | Built-in | Built-in | Manual |
| Tool rendering | Auto | Configurable | Custom |
| Rendering engine | Terminal API | Blessed/Ink | Ratatui (Rust) |
| Performance | Good | Good | Excellent |
| Package size | ~5 deps | ~80 deps | ~1 dep (native) |
| Release date | Today | April 2026 | June 2026 |
When to Use Which
-
@ai-sdk/tui — You want the fastest path to a working agent TUI, you’re already in the JS/TS ecosystem, and you’re fine with Vercel’s opinionated agent loop.
-
create-agent-tui — You need a full agent harness you can customize, you want OpenRouter’s model routing or multi-provider support, and you don’t mind a heavier scaffold.
-
pyratatui — You’re building a Python-native tool (data pipeline, monitoring dashboard, batch agent system), you want full control over rendering, and you’re comfortable writing the agent loop yourself.
What I Built
For my own use case — a multi-provider agent TUI that can switch between OpenAI, Anthropic, and local models via Ollama — I ended up combining elements of all three. The scaffold structure from create-agent-tui, the streaming model from @ai-sdk/tui, and the rendering approach from pyratatui.
The final architecture uses a provider abstraction layer:
interface ProviderAdapter {
stream(messages: Message[], tools: ToolDef[]): AsyncIterable<StreamEvent>;
supportsToolCalling: boolean;
maxTokens: number;
}
const providers = {
openai: new OpenAIAdapter({ model: 'gpt-4o' }),
anthropic: new AnthropicAdapter({ model: 'claude-sonnet-4' }),
ollama: new OllamaAdapter({ model: 'llama-4' }),
};
Each adapter normalizes the streaming API, tool call format, and token tracking into a unified interface. The TUI layer renders whatever comes back, color-coding by provider.
The total build took about 4 hours from initial scaffold to a working multi-provider TUI with tool execution, conversation history, and keyboard navigation. The @ai-sdk/tui path would have taken 15 minutes but given me less flexibility. The pyratatui path would have taken 8+ hours for the same feature set.
Key Takeaways
-
Agent TUIs are now a solved problem at the framework level. You don’t need to build your own terminal renderer or agent loop from scratch.
-
The gap between “works on my machine” and production is still large. All three approaches handle the happy path well. Error recovery, rate limiting, and streaming interruptions need manual attention in all three.
-
Provider abstraction is the critical architecture decision. Pick a TUI framework second; pick your provider adapter layer first. The TUI is just a rendering concern.
-
The terminal renaissance is real. Three mature agent TUI toolkits in the span of two months (April–June 2026) means the barrier to building custom agent interfaces has never been lower.
→ Build it yourself. Pick an approach above, scaffold it in under 10 minutes, and you’ll have a working agent TUI before the hour’s out. The code is on GitHub for all three:
Vercel AI SDK,create-agent-tui,pyratatui.
References
[1] Best-of Agent Harnesses — curated list of agent runtime harnesses (June 2026). https://github.com/RyanAlberts/best-of-Agent-Harnesses
[2] Vercel AI SDK 7 announcement — June 25, 2026. Introduces @ai-sdk/tui, WorkflowAgent, and HarnessAgent. https://vercel.com/blog/ai-sdk-7
[3] OpenRouter — Build Your Own Agent TUI cookbook. Scaffolds a full TypeScript agent harness with create-agent-tui. https://openrouter.ai/docs/cookbook/building-agents/create-agent-harness-tui
[4] PyRatatui — Rust-powered terminal UI for Python (PyPI, June 2026). Exposes the full Ratatui widget library via PyO3. https://github.com/pyratatui/pyratatui
[5] AI SDK 7 Terminal UI docs — @ai-sdk/tui reference for runAgentTUI. https://ai-sdk.dev/v7/docs/agents/terminal-ui


