Welcome to Chapter 2!
In the previous chapter, The SkillTool Interface, we built the "Universal Remote" that allows our AI to trigger commands by name.
But imagine pressing a button on a remote and... nothing happens. No light blinks, no sound plays. You wouldn't know if the TV is broken or if it's just "thinking."
This is where the Skill User Interface (UI) comes in.
The Skill User Interface is the visual feedback layer of our agent. It creates the "digital display" for our tools.
Think of SkillTool like a modern washing machine.
The Skill UI provides this screen so the user knows exactly what the AI is doing without having to read raw logs.
Let's stick with our "Reviewing Code" scenario. The AI has decided to run the review-pr skill. This process might take 30 seconds.
Without UI: The terminal freezes. The user thinks, "Did it crash?"
With UI: The terminal updates in real-time:
Skill: review-prInitializing...Reading src/main.ts...Done
Our project uses a library called Ink to render React components inside the command line. The SkillTool doesn't just run logic; it returns React components to visualize that logic.
The UI handles four specific states:
Before looking at the code, let's trace the flow of data when the UI updates.
All the visualization logic lives in UI.tsx. Let's break down the functions used in the diagram above.
renderToolUseMessage)When the AI first invokes the tool, we want to see what it is trying to do.
// From UI.tsx
export function renderToolUseMessage({ skill }: Partial<Input>) {
if (!skill) {
return null;
}
// Simply return the name of the skill to be displayed
return skill;
}
Explanation: This is the equivalent of the channel number appearing on your TV screen. It confirms the command was received.
renderToolUseProgressMessage)This is the most complex part. Skills might take time, or they might spawn "Sub-Agents" (mini-helpers) to do work. The UI needs to show this activity.
State A: Just starting If there are no progress messages yet, show that we are initializing.
// From UI.tsx
if (!progressMessages.length) {
return (
<MessageResponse height={1}>
<Text dimColor>Initializingβ¦</Text>
</MessageResponse>
);
}
State B: Running If the skill is running, we might get hundreds of log lines. We can't show them all! We only show the last few to keep the display clean.
// From UI.tsx
const MAX_PROGRESS_MESSAGES_TO_SHOW = 3;
// ... inside the function
const displayedMessages = verbose
? progressMessages
: progressMessages.slice(-MAX_PROGRESS_MESSAGES_TO_SHOW);
Explanation: This acts like a "ticker tape." Old messages scroll off the screen, keeping the UI focused on what is happening right now.
If a skill is complex (like "Write a whole app"), it might use a Forked Execution Strategy. This creates a sub-agent. The UI wraps these in a SubAgentProvider to organize them visually.
// From UI.tsx
return (
<MessageResponse>
<Box flexDirection="column">
<SubAgentProvider>
{displayedMessages.map(msg => (
// Render the sub-agent's output here
<MessageComponent message={msg.data.message} ... />
))}
</SubAgentProvider>
</Box>
</MessageResponse>
);
renderToolResultMessage)Finally, the machine goes "Ding!" We show the result.
// From UI.tsx
export function renderToolResultMessage(output: Output): React.ReactNode {
// If it was a complex sub-agent (forked), just say "Done"
if ('status' in output && output.status === 'forked') {
return (
<MessageResponse height={1}>
<Text><Byline>Done</Byline></Text>
</MessageResponse>
);
}
// Otherwise, show details (like "Successfully loaded skill")
const parts: string[] = ['Successfully loaded skill'];
// ... add more details to 'parts' ...
return <Text><Byline>{parts}</Byline></Text>;
}
If the washing machine floods (an error occurs), we need a specific red light.
// From UI.tsx
export function renderToolUseErrorMessage(result, { verbose }) {
return (
<>
{/* Show the history of what happened before the crash */}
{renderToolUseProgressMessage(...)}
{/* Show the actual error message in red */}
<FallbackToolUseErrorMessage result={result} verbose={verbose} />
</>
);
}
The Skill User Interface transforms the SkillTool from a black box into a transparent, interactive experience.
Now that we have a Universal Remote (Chapter 1) and a Screen to see what's happening (Chapter 2), we need to figure out how the AI decides what to type into the remote. How does it know to write a specific code update?
To understand that, we need to look at how we build prompts dynamically.
Next Chapter: Dynamic Prompt Construction & Budgeting
Generated by Code IQ