Chapter 08: UI Upgrades
Improve the terminal interface with model info on startup, status display, collapsible tool blocks, and better input handling.
TUTOR WITH THEFOCUS.AI
Copy this prompt into Claude, ChatGPT, or any external AI assistant. It points the assistant to the course instructions and links it to your student profile to track your progress and customize observations.
You are not enrolled yet. Enroll to generate a Student ID to track lesson completions and store learning notes.
Chapter 08: UI Upgrades
What We’re Building
The agent’s current interface is functional but bare. We’ll upgrade it with:
- Splash screen: Model info, pricing, recent sessions on startup
- Status bar: Current model, session cost, message count
- Tool call display: Collapsible tool blocks with inline output
- Better input: Keyboard shortcuts, paste handling, tab/arrow navigation
- Agent labels: Clear indication of which agent is responding
Step 1: Clean Up Tool Display
Paste this prompt:
update the search tool in src so that it doesn’t look through node_modules by default, only if specifically asked. make sure that everything is properly tested. make sure that typechecking and linting are run all of the time. verify that we are showing reasoning specs, and verify that our usage totals are over the whole conversation
This ensures:
search_filesskipsnode_modulesunlessincludeNodeModules: true- Reasoning details from Gemini are displayed
- Usage totals accumulate across the full session (not per-turn)
Step 2: Improve Welcome Screen
Paste this prompt:
Improve the welcome screen — show model name, pricing, recent session logs, and a more helpful startup message. Show the selected prompt name and description on startup.
The updated startup in src/index.ts:
// Fetch and display model stats
console.log("Fetching model stats for", MODEL, "...");
const stats = await fetchModelStats();
if (stats) {
console.log(`Model: ${stats.name}`);
console.log(
`Cost: $${stats.cost.prompt * 1000000} per 1M input tokens, ` +
`$${stats.cost.completion * 1000000} per 1M output tokens`,
);
}
// Show selected prompt
const selectedPrompt = getPrompt(promptName);
console.log(
`Using prompt: ${selectedPrompt.name} - ${selectedPrompt.description}`,
);
console.log("Agent started. Type 'exit' to quit.");
Step 3: Agent Labels for Subagents
When subagents run, we need to know who’s talking. Paste:
update the prompt so we know what agent the message is going to. Like [poem]> or something like that. What would happen if multiple poem agents were running at the same time?
The agent will add a scrolling label in the tool display:
--- [Subagent Start] code-map ---
Task: Map the entire codebase
[Tool: list_files] {"path":"src/"}
src/
├── index.ts
├── agent.ts
├── lib/
├── tools/
└── prompts/
[Tool: read_file] {"path":"src/agent.ts"}
[content displayed...]
--- [Subagent End] code-map ---
The agentName parameter flows through the system from runTurn to the ask_user tool, which prefixes prompts with the agent name:
export async function askUser(
question: string,
agentName = "system",
): Promise<string> {
const answer = await askQuestion(`\n[${agentName}] ${question}\n> `);
return answer;
}
Step 4: Tool Call Display
The agent already shows tool calls nicely. Here’s the current approach in src/agent.ts:
// For bash commands, show the command directly:
if (toolCall.function.name === "bash" && args.command) {
console.log(`$ ${args.command}`);
} else {
const argStr = JSON.stringify(args);
const displayArgs =
argStr.length > 100 ? `${argStr.substring(0, 97)}...` : argStr;
console.log(`[Tool: ${toolCall.function.name}] ${displayArgs}`);
}
// Truncate results if too long:
const displayResult =
result.length > 1000 ? `${result.substring(0, 1000)}... [truncated]` : result;
console.log(displayResult);
Step 5: Reasoning Display
When the model provides reasoning (Gemini’s thinking), it’s displayed:
if (m.reasoning_details) {
if (Array.isArray(m.reasoning_details) && m.reasoning_details.length > 0) {
console.log("\n[Reasoning]:", m.reasoning_details[0].text);
} else if (typeof m.reasoning_details === "string") {
console.log("\n[Reasoning]:", m.reasoning_details);
}
}
This shows the model’s internal thinking before it takes action — great for understanding why the agent made a particular decision.
Step 6: Fix Subagent Readline Issues
Subagents that use ask_user can interfere with the main input loop:
when a subagent starts up and finishes, it quits out of the agent. I think it has to do with the readline stuff
The fix ensures that getInterface() returns the same readline instance across the whole process:
let rl: readline.Interface | null = null;
export function getInterface(): readline.Interface {
if (!rl) {
rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
prompt: "> ",
});
}
return rl;
}
This singleton pattern prevents multiple readline instances from fighting over stdin/stdout.
The Display Flow
Here’s what a typical interaction looks like after all UI upgrades:
Fetching model stats for google/gemini-3-pro-preview ...
Model: Google Gemini 3 Pro Preview
Cost: $1.25 per 1M input tokens, $10.00 per 1M output tokens
Using prompt: default - The default full-featured agent.
Agent started. Type 'exit' to quit.
> search for all TypeScript interfaces in the codebase
[Tool: search_files] {"query":"interface ","path":"src/"}
src/lib/types.ts:1:export interface Message {
src/lib/types.ts:11:export interface CompletionResponse {
src/lib/api.ts:7:export interface ModelStats {
src/lib/logger.ts:4:export interface Logger {
src/tools/tavily.ts:1:interface TavilyResult {
src/tools/tavily.ts:8:interface TavilyResponse {
src/tools/types.ts:1:export type CommandExecutor = ...
src/tools/types.ts:3:export type AgentRunner = ...
src/prompts/index.ts:6:export interface Prompt {
Assistant: The codebase has these TypeScript interfaces and types:
[lists and explains each...]
[Turn Usage] Input: 850 | Output: 320 | Cost: $0.004262
[Session Total] Input: 12800 | Output: 4500 | Cost: $0.061000