In the previous chapter, Command Module Registration, we set up the "Menu" for our application. We told the CLI that a command named effort exists.
Now, we are going to build the kitchen. When the user orders "Effort," what actually happens?
In traditional scripts (like Bash or simple Python scripts), execution is linear:
However, modern CLI tools are interactive. They might need to wait for an API, show a loading spinner, or manage global state (like a "Memory" of settings).
React-based Command Lifecycle brings the power of web development to the terminal. Instead of just running a function, we render a component. This allows us to:
We want to handle this command:
/effort high
The CLI needs to:
To make this work, we use three main concepts:
call): The function that decides which component to show.onDone Callback: The "Exit Button." Since React apps usually run forever, we need a specific way to tell the CLI "We are finished here."
Let's look at effort.tsx. We will build the lifecycle step-by-step.
This is the function that the Lazy Loader (from Chapter 1) imports. Its job is to look at the arguments and decide what to render.
// effort.tsx
import * as React from 'react';
// ... other imports
export async function call(
onDone: LocalJSXCommandOnDone, // The "Exit" switch
_context: unknown,
args?: string // User input (e.g., "high")
): Promise<React.ReactNode> {
onDone. Pass this around! Whoever calls this function finishes the command. It returns a ReactNodeβliterally UI elements.
Inside call, we check what the user wants. Do they want to see the current status, or change it?
// Inside call()...
args = args?.trim() || '';
// Case A: User typed "/effort" or "/effort status"
if (!args || args === 'current' || args === 'status') {
return <ShowCurrentEffort onDone={onDone} />;
}
// Case B: User typed "/effort high"
const result = executeEffort(args);
return <ApplyEffortAndClose result={result} onDone={onDone} />;
}
<ShowCurrentEffort />. If they did, we render <ApplyEffortAndClose />.ApplyEffortAndClose)This component is invisible. It doesn't print text to the screen immediately. Instead, it acts like a logic controller. It runs once, updates settings, and leaves.
function ApplyEffortAndClose({ result, onDone }) {
const setAppState = useSetAppState(); // Hook to access global state
const { effortUpdate, message } = result;
React.useEffect(() => {
// Logic goes here (see next snippet)
}, [effortUpdate, message, onDone, setAppState]);
return null; // We don't render UI, we just send a final message
}
useEffect. In React, this means "Do this after the component loads." We return null because we aren't drawing a UI widget; we are performing an action.
Inside that useEffect, we actually update the application.
// Inside useEffect...
if (effortUpdate) {
// 1. Update the Global State
setAppState(prev => ({
...prev,
effortValue: effortUpdate.value
}));
}
// 2. Tell the CLI we are done and print the result
onDone(message);
1. setAppState: This updates the CLI's working memory.
2. onDone(message): This is crucial. It prints the success message to the terminal and kills the React process for this command, returning control to the user.
What happens "Under the Hood" when you press Enter?
useEffect hook triggers immediately. It calculates necessary updates.onDone. The CLI receives the final string, prints it, and destroys the component instance.You might ask: Why not just update the variable and return a string? Why use React?
Simple commands might not need it, but complex ones do. By using React components, we prepare ourselves for complex interactions, such as:
We have successfully built a command that has a Lifecycle. It starts, thinks, acts, and finishes using standard React patterns.
call() to route the request.onDone to exit gracefully.Now that we have successfully updated the state to "high", what does "high" actually mean to the AI? How does the system interpret that string?
Next Chapter: Effort Level Controller
Generated by Code IQ