In the previous chapter, Tool Definition & Schema, we created the strict forms (Schemas) that the AI uses to communicate with our code.
Now, we need to bridge the gap between the computer and the human.
Imagine driving a car that has no dashboard. You press the gas (Input), and the engine revs (Logic), but you have no idea how fast you are going or if the engine is overheating.
The UI Presentation Layer is your dashboard.
In our GrepTool, we use a library called Ink (which is React for the command line) to build these displays.
When the AI decides to run a command, we want to confirm to the user exactly what is happening. We use a function called renderToolUseMessage.
If the AI sends this JSON input:
{ "pattern": "console.log", "path": "src/app.ts" }
We want to display this friendly text:
pattern: "console.log", path: "src/app.ts"
Here is how we transform the raw input object into a human-readable string.
// From file: UI.tsx
export function renderToolUseMessage(input) {
// 1. Always show the pattern
const parts = [`pattern: "${input.pattern}"`];
// 2. Only show the path if the AI provided one
if (input.path) {
parts.push(`path: "${input.path}"`);
}
// 3. Join them with a comma
return parts.join(', ');
}
Explanation:
parts with the most important info: the pattern.path exists. Remember from Chapter 1, path is optional. If it's there, we add it.Displaying the result is slightly more complex because our tool has three different "Output Modes" (defined in our Schema). The UI must adapt to show the right "gauge" for the right mode.
renderToolResultMessage
This function acts as a switch. It looks at the mode property of the output and decides which React component to render.
// From file: UI.tsx
export function renderToolResultMessage(output, context) {
const { verbose } = context;
// Case A: User wanted to see the actual content lines
if (output.mode === 'content') {
return <SearchResultSummary
count={output.numLines}
content={output.content}
verbose={verbose}
/>;
}
// ... continued below
Explanation:
output: This is the data matching the Output Schema we defined in Chapter 1.verbose: A setting that determines if we show a compact summary or a detailed view.SearchResultSummary: This is a helper component (we'll look at it next) that makes the text look pretty.If the user didn't ask for content, they might just want a count or a list of files.
// Case B: User just wanted a count of matches
if (output.mode === 'count') {
return <SearchResultSummary
count={output.numMatches}
countLabel="matches"
secondaryCount={output.numFiles}
secondaryLabel="files"
/>;
}
// Case C: Default (List of filenames)
return <SearchResultSummary
count={output.numFiles}
countLabel="files"
content={output.filenames.join('\n')}
/>;
}
Explanation:
countLabel="matches" so the UI says "15 matches". We also show how many files those matches were found in.\n).
You noticed we used <SearchResultSummary /> in all three cases. This is a "dumb" component that simply formats text nicely. It handles details like pluralization (Match vs Matches) and bolding numbers.
Here is a simplified look at how it renders the text:
// Simplified from UI.tsx
function SearchResultSummary({ count, countLabel, content }) {
// Logic to handle pluralization (e.g., "1 file" vs "5 files")
const label = (count === 1) ? countLabel.slice(0, -1) : countLabel;
return (
<Box flexDirection="column">
<Text>Found <Text bold>{count}</Text> {label}</Text>
{/* Only show content box if there is content */}
{content && (
<Box marginLeft={2}>
<Text>{content}</Text>
</Box>
)}
</Box>
);
}
Explanation:
count is 1. If so, it removes the 's' from the label (e.g., "files" becomes "file").<Box> and <Text> (Ink components) to create a layout. It bolds the number so it pops out to the user.content is empty (like in a search with 0 results), that part of the UI simply doesn't draw.Sometimes, the AI messes up (e.g., asks for a file that doesn't exist). We need a UI for that too.
export function renderToolUseErrorMessage(result) {
// Did we get a "File not found" error?
if (result.includes("File not found")) {
return (
<MessageResponse>
<Text color="error">File not found</Text>
</MessageResponse>
);
}
// Generic fallback
return <Text color="error">Error searching files</Text>;
}
This ensures that instead of crashing or showing a raw error object, the user sees a clean, red error message indicating what went wrong.
In this chapter, we built the User Interface for our GrepTool.
renderToolUseMessage to show the user exactly what the AI is searching for (Pattern & Path).renderToolResultMessage to switch between different output visuals based on the Output Mode.SearchResultSummary component to keep our formatting consistent (bolding numbers, handling plurals).Now that we have the Form (Schema) and the Dashboard (UI), we need to build the Engine. How do we actually construct the search command to send to the operating system?
Next Chapter: Search Command Builder
Generated by Code IQ