Welcome back!
In the previous chapter, Command Registration, we added our btw command to the application's "menu." The app now knows the command exists and lazily loads the code when the user types it.
Now, we face the next challenge: Visuals.
Standard command-line tools just print text line-by-line. But btw is different. It needs to:
In this chapter, we will build the Side Question UI Component.
Imagine you are working on a physical desk. You are writing an essay (your main terminal task). Suddenly, you need to do a quick calculation.
You don't throw away your essay. You grab a Sticky Note, do the math, and then throw the note away.
The Side Question UI is that Sticky Note. It is a temporary "Mini-App" that renders on top of your terminal history without deleting what was there before.
To build this, we use a library called Ink.
If you have used React for web development, you use HTML tags like <div> and <h1>. Ink uses the exact same React logic, but with components made for text:
<Box>: Like a <div>. It holds layout logic (flexbox, padding, margins).<Text>: Like a <span>. It holds text, colors, and bold styling.
We are building a component called BtwSideQuestion. Let's look at its skeleton.
First, what data does our Sticky Note need to exist?
type BtwComponentProps = {
question: string; // The text the user typed
onDone: () => void; // A function to close the sticky note
// ... context data
};
React components use "State" to remember things. Our component needs to remember three things:
function BtwSideQuestion({ question, onDone }: BtwComponentProps) {
// Is the AI done? If null, we are still thinking.
const [response, setResponse] = useState<string | null>(null);
// Did something break?
const [error, setError] = useState<string | null>(null);
// ...
This is the heart of the UI. We use a ternary operator (an if/else statement inside UI) to decide what to show.
return (
<Box flexDirection="column" paddingLeft={2} marginTop={1}>
{/* 1. Always show the user's question first */}
<Box>
<Text bold color="warning">/btw </Text>
<Text dimColor>{question}</Text>
</Box>
{/* 2. Show the content based on state */}
<Box marginTop={1}>
{error ? (
<Text color="error">{error}</Text>
) : response ? (
<Markdown>{response}</Markdown>
) : (
// Still loading...
<Box>
<SpinnerGlyph />
<Text color="warning"> Answering...</Text>
</Box>
)}
</Box>
</Box>
);
How does the component go from "Thinking" to "Answered"? We use a React hook called useEffect.
This hook runs immediately after the component appears on the screen.
Here is a simplified version of the logic inside our component. We use useEffect to trigger the AI processing.
useEffect(() => {
// Define the async work
async function fetchResponse() {
try {
// 1. Call the heavy logic (We will build this later)
const result = await runSideQuestion({ question });
// 2. Update state, which triggers a re-render
setResponse(result.response);
} catch (err) {
setError("Failed to get response");
}
}
// Execute immediately on mount
fetchResponse();
}, [question]);
Finally, how do we actually tell Ink to render this specific component?
Recall from Chapter 1 that the app calls a load() function. That file exports a call function. This is the bridge between the raw command line string and our React UI.
// btw.tsx
export async function call(onDone, context, args) {
const question = args?.trim();
// Validation: Did the user type a question?
if (!question) {
onDone('Usage: /btw <your question>');
return null;
}
// Render our React Component
return (
<BtwSideQuestion
question={question}
context={context}
onDone={onDone}
/>
);
}
In this chapter, we built the visual shell of our feature:
<Box> and <Text> to structure the terminal output.useEffect to trigger the work automatically when the UI appears.However, if you ran this code right now, you would notice two problems:
A "Modal" isn't useful if you are trapped inside it! In the next chapter, we will make our component interactive.
Next Chapter: Event Handling & Navigation
Generated by Code IQ