Welcome back! In Chapter 1: Lazy Command Entry Point, we learned how to create a "Menu Item" that lazily loads our code.
Now that the user has clicked that menu item (by typing think-back), the application needs to wake up and show something.
In this chapter, we explore Ink-based UI Orchestration. We will learn how to build a rich, interactive "Dashboard" inside the terminal using React.
Think of a complex machine, like a nuclear reactor or a high-speed train.
If the dashboard is broken, it doesn't matter how good the engine isβthe operator can't use it.
The Problem: Standard terminal scripts usually just print text line-by-line. They are static. The Solution: We use a library called Ink. It allows us to use React to build terminal dashboards. We can have loading spinners that vanish when done, and menus you can navigate with arrow keys.
To orchestrate our UI, we need three layers:
call): The function that clears a space in the terminal to draw our UI.ThinkbackFlow): A parent component that decides what to show (Is it loading? Is it broken? Is it ready?).ThinkbackMenu): The actual buttons and options the user interacts with.
Let's walk through how we construct this in thinkback.tsx.
call)This is the bridge between the raw terminal and React. When the lazy loader imports our file, it calls this function.
// The entry point for the UI
export async function call(onDone) {
// Renders the React component into the terminal
return <ThinkbackFlow onDone={onDone} />;
}
This simply tells the computer: "Start the React engine and mount the ThinkbackFlow component."
ThinkbackFlow)This component manages the State of our application. It acts like a traffic controller.
It tracks two main things:
function ThinkbackFlow({ onDone }) {
// State: Are we done installing dependencies?
const [installComplete, setInstallComplete] = useState(false);
// State: Do we have a generated video file?
const [hasGenerated, setHasGenerated] = useState(null);
// ... logic to check these states ...
Based on these states, ThinkbackFlow decides what to render.
Scenario A: Still Installing If the installation isn't done, show the Installer (which has a loading spinner).
if (!installComplete) {
return <ThinkbackInstaller
onReady={handleReady}
onError={handleError}
/>;
}
Scenario B: Loading Data If we are installed but haven't checked for files yet, show a spinner.
if (hasGenerated === null) {
return (
<Box>
<Spinner />
<Text>Loading thinkback skillβ¦</Text>
</Box>
);
}
Scenario C: Ready for Action If everything is ready, show the interactive menu.
return (
<ThinkbackMenu
onDone={onDone}
onAction={handleAction}
hasGenerated={hasGenerated}
/>
);
}
ThinkbackMenu)
This is what the user actually sees and uses. It uses a Select component (a list you can scroll through).
The menu options change dynamically. If you have never used the tool, it says "Let's go!". If you have, it offers "Play", "Edit", etc.
function ThinkbackMenu({ onAction, hasGenerated }) {
// Define options based on history
const options = hasGenerated
? [ { label: 'Play animation', value: 'play' }, ... ]
: [ { label: "Let's go!", value: 'regenerate' } ];
// Render the interactive list
return <Select options={options} onChange={handleSelect} />;
}
How does the terminal update without printing a million new lines? Ink takes over the standard output and repaints only the parts of the screen that changed, just like a web browser.
Here is the flow of our UI Orchestration:
Let's look at a specific logic piece in thinkback.tsx regarding how the Menu handles user choices.
The ThinkbackMenu component doesn't actually do the work (like regenerating video). It just tells the parent what the user wants. This is called Generative Action Dispatch.
// Inside ThinkbackMenu...
function handleSelect(value) {
if (value === "play") {
// If Play, run the animation immediately
playAnimation(skillDir);
} else {
// Otherwise, tell the parent (The Brain) to run an action
onAction(value);
}
}
The "Brain" (ThinkbackFlow) then translates that simple string (e.g., 'edit') into a complex prompt for the AI.
// Inside ThinkbackFlow...
function handleAction(action) {
const prompts = {
edit: 'Use the Skill tool to... mode=edit...',
fix: 'Use the Skill tool to... mode=fix...',
};
// Send the specific AI prompt back to the system
onDone(prompts[action], { display: "user" });
}
In this chapter, we learned about Ink-based UI Orchestration.
ThinkbackFlow) to manage the state of the terminal UI.The user has now selected "Play" from our beautiful menu. But how do we actually show a movie inside a text-based terminal?
In the next chapter, we will build the engine that plays the movie.
Next Chapter: Animation Runtime Engine
Generated by Code IQ