Welcome back! In the previous chapter, Agent Hierarchy (The Tree), we built a visual structure to organize our AI agents. We can see who reports to whom, similar to a file explorer.
However, currently, our agents are just names in a list. We know they are "working," but we don't know what they are doing.
Imagine walking past a meeting room with frosted glass walls. You can see silhouettes of your teammates inside, so you know they are present, but you can't hear what they are saying or see what they are writing on the whiteboard.
In Spinner, the "Raw Logs" of an AI agent are messy. They contain:
We don't want to dump all that text onto the screen. We want a Previewโa way to "wipe a small clear spot" on the frosted glass to see the last 3 sentences they wrote.
To build this, we need to implement three specific logical steps:
In the previous chapter, we used TeammateSpinnerLine to render the agent's name. Now, we simply enable the showPreview prop.
When showPreview is true, the component calculates the summary lines and renders them below the name.
<TeammateSpinnerLine
teammate={activeTeammate}
isLast={true}
// The magic switch to enable the "Peek"
showPreview={true}
/>
The Visual Output:
Without preview:
โโ @Researcher: working...
With preview:
โโ @Researcher: working...
โ Reading file "data.txt"
โ Analyzing contents...
โ Summarizing results
How does the code turn messy data into clean lines? Let's look at the flow of data.
The core logic lives in a helper function called getMessagePreview. Let's break down how it handles different types of AI "thoughts".
When an agent uses a tool, it generates a strict JSON object. We want to convert that into a readable string.
We look inside the input block for fields like query, command, or description.
// Inside getMessagePreview function
if (block.type === 'tool_use') {
const input = block.input;
let toolLine = `Using ${block.name}โฆ`;
// If the tool has a description/query, use that instead
if (input && input.query) {
toolLine = input.query; // e.g. "python error fix"
}
allLines.push(truncateToWidth(toolLine, 80));
}
Sometimes the agent is just "talking" (thinking to itself). This comes as a text block. We simply split the text by new lines and take the most recent ones.
if (block.type === 'text') {
// Split the paragraph into individual lines
const textLines = block.text.split('\n');
// Loop backwards to get the most recent thoughts first
for (let j = textLines.length - 1; j >= 0; j--) {
const line = textLines[j];
// Add to our preview list
allLines.push(truncateToWidth(line, 80));
}
}
Once we have the text strings (e.g., ["Doing A", "Doing B"]), we need to render them underneath the agent's name.
The visual challenge here is maintaining the tree structure.
โ.// Inside TeammateSpinnerLine return statement
const previewTreeChar = isLast ? ' ' : 'โ ';
return (
<Box flexDirection="column">
{/* ... Render Agent Name (covered in Ch 1) ... */}
{/* Render the Preview Lines */}
{previewLines.map((line, idx) => (
<Box key={idx} paddingLeft={3}>
<Text dimColor> </Text>
<Text dimColor>{previewTreeChar} </Text>
<Text dimColor>{line}</Text>
</Box>
))}
</Box>
);
Beginner Tip:
paddingLeft={3}ensures the preview lines align perfectly with the agent's icon above it. Alignment is key to making the tree look connected!
You have effectively "wiped the frost off the glass." By filtering raw logs and extracting the description or text, users can now watch the AI's thought process in real-time without being overwhelmed by code and brackets.
Our UI is functional, but it's a bit monochromatic. To make it truly professional, we need to style our lines, add colors, and handle icons better.
In the next chapter, we will learn how to make our UI beautiful and consistent.
Next Chapter: Theme & Glyph Utilities
Generated by Code IQ