Welcome back! In Chapter 1: Command Definition, we wrote the "Menu" entry for our command. The CLI knows add-dir exists.
Now, we need to decide what happens when the customer actually orders that item.
If Chapter 1 was the Menu, Chapter 2 is the Waiter.
When a user runs a command, two things can happen:
add-dir ./src. The waiter takes the order and runs to the kitchen immediately.add-dir. The waiter has to ask: "Which directory would you like to add?"The Interactive Command UI is the code responsible for having this conversation. It acts as a Controller: it looks at the user's input, decides which screen to show, and handles the flow until the job is done.
We want to support two specific user behaviors with a single file:
my-cli add-dir
# Output: A visual form asks "Enter directory path:"
my-cli add-dir ./src
# Output: Validates ./src and adds it immediately.
call Function
In our architecture, every command exports a specific function named call. This is the entry point. It receives the user's input and returns what should be displayed on the screen.
Since we are using React (Ink), we don't just print text strings; we return UI Components.
Let's look at the basic structure of our controller file (add-dir.tsx).
// --- File: add-dir.tsx ---
export async function call(
onDone: LocalJSXCommandOnDone, // Function to call when we finish
context: LocalJSXCommandContext, // The app's memory (state)
args?: string // What the user typed (e.g., "./src")
): Promise<React.ReactNode> {
// 1. Clean up the input
const directoryPath = (args ?? '').trim();
// ... logic continues below
}
Explanation:
add-dir ./src, then args is "./src".The most important job of the UI Controller is to direct traffic.
// Inside the call() function...
// Scenario 1: The user did NOT provide a path
if (!directoryPath) {
// Return a Visual Component (The Form)
return <AddWorkspaceDirectory
permissionContext={appState.toolPermissionContext}
onAddDirectory={handleAddDirectory}
onCancel={() => onDone('Cancelled')}
/>;
}
// Scenario 2: The user DID provide a path
// ... (Validation logic goes here)
Explanation:
directoryPath is empty, we return a React component called <AddWorkspaceDirectory />.onAddDirectory) so the form knows what to do when the user hits Enter.If the user did provide a path (Scenario 2), we shouldn't show a form. We should try to add it immediately. But what if they made a mistake?
// Scenario 2 continued...
// Validate the path (We cover this in Chapter 3)
const result = await validateDirectoryForWorkspace(directoryPath, ...);
// If there is an error, Render the Error UI
if (result.resultType !== 'success') {
return <AddDirError
message={result.message}
args={args ?? ''}
onDone={() => onDone(result.message)}
/>;
}
// If success, we add the directory (Logic hidden for brevity)
Explanation:
console.error, we return a custom component <AddDirError />.How does the CLI switch between these modes? Let's visualize the decision process.
Let's look closely at how the logic inside add-dir.tsx connects the visuals to the logic.
We need a helper function to actually perform the work. This function is shared by both the "Interactive Form" and the "Direct Argument" flows.
// The shared logic for adding a directory
const handleAddDirectory = async (path: string, remember = false) => {
// 1. Create the update object
const permissionUpdate = {
type: 'addDirectories',
directories: [path],
destination: remember ? 'localSettings' : 'session'
};
// 2. Update the Application State
// (We will cover state specifically in Chapter 4)
applyPermissionUpdate(..., permissionUpdate);
// 3. Tell the CLI we are finished
onDone(`Added ${path} as a working directory.`);
};
Explanation:
onDone to print the success message and exit.Why do we need a special component just for errors? In React (Ink), if you unmount a component too fast, sometimes the text disappears.
function AddDirError({ message, onDone }) {
// Use a timeout to ensure the error renders before we exit
useEffect(() => {
const timer = setTimeout(onDone, 0);
return () => clearTimeout(timer);
}, [onDone]);
return (
<Box flexDirection="column">
<Text color="red">Error: {message}</Text>
</Box>
);
}
Explanation:
In this chapter, we built the Interactive Command UI. We learned:
call function acts as a Controller for the command.<AddWorkspaceDirectory> or <AddDirError>) to render the interface.However, simply asking for a directory isn't enough. What if the user types a path that doesn't exist? What if they try to add a system folder they shouldn't access?
We need to check the input before we accept it.
Next Chapter: Directory Validation
Generated by Code IQ