πŸ“ commands/memory/ Β· 03_interactive_cli_component.md

Chapter 3: Interactive CLI Component

πŸ“„ commands/memory/03_interactive_cli_component.md

Chapter 3: Interactive CLI Component

In the previous chapter, Async Command Lifecycle, we learned how to prepare our data before showing anything to the user. We ended with the call function returning a line of code that looked like this:

return <MemoryCommand onDone={onDone} />;

Now, we need to answer the big question: What exactly is <MemoryCommand />?

The Concept: React in the Terminal

Usually, when you think of React, you think of websites with HTML, CSS, and buttons. But in this project, we are using a library called Ink. Ink allows us to use React components to build user interfaces inside the black-and-white text box of your terminal.

The Use Case: We want to present the user with a list of "memory files." The user should be able to:

  1. Use arrow keys to highlight a file.
  2. Press Enter to edit it.
  3. Press Esc to cancel.

Instead of typing complex commands like memory edit --file=project-alpha, the user just sees a menu. The MemoryCommand component is the "brain" that manages this entire interaction.


Step-by-Step Implementation

The MemoryCommand is a React Functional Component. It acts as a Controller: it listens for events (like a user selecting a file) and decides what to do next.

1. The Component Structure

First, let's look at the basic shape of the component. It receives one important tool: onDone.

// We accept 'onDone' as a prop (a tool passed down from the parent)
function MemoryCommand({ onDone }: { 
  onDone: (result?: string, options?: any) => void 
}): React.ReactNode {
  
  // Logic and Event Handlers will go here...

  // The UI (What the user sees) goes here...
  return <Dialog title="Memory" ...> ... </Dialog>;
}

Explanation:

2. The View (Rendering the UI)

Inside the return statement, we define what the user sees. We use a Dialog (a frame) and a MemoryFileSelector (the list of files).

  return (
    <Dialog title="Memory" onCancel={handleCancel} color="remember">
      <Box flexDirection="column">
        <React.Suspense fallback={null}>
          <MemoryFileSelector 
            onSelect={handleSelectMemoryFile} 
            onCancel={handleCancel} 
          />
        </React.Suspense>
        {/* Footer Link code omitted for brevity */}
      </Box>
    </Dialog>
  );

Explanation:

3. Handling Cancellation

What happens if the user changes their mind? We need a simple function to exit gracefully.

  const handleCancel = () => {
    // Call the "Eject Button" with a message
    onDone('Cancelled memory editing', {
      display: 'system' // formatting style
    });
  };

Explanation:

4. Handling Selection (The Core Logic)

This is the most complex part. When a user picks a file, we need to ensure the file exists and then open it.

Part A: Ensuring the File Exists Before we open a file, we must make sure the computer actually has it on the disk.

  const handleSelectMemoryFile = async (memoryPath: string) => {
    try {
      // 1. Create the folder if it's missing
      if (memoryPath.includes(getClaudeConfigHomeDir())) {
        await mkdir(getClaudeConfigHomeDir(), { recursive: true });
      }

      // 2. Create an empty file if it's missing
      // (We use a special flag 'wx' to avoid overwriting existing text)
      await writeFile(memoryPath, '', { encoding: 'utf8', flag: 'wx' });

Explanation:

Part B: Opening the Editor Once the file is safe on the disk, we hand control over to the text editor.

      // 3. Pause the CLI and open the text editor (Vim, Nano, VS Code, etc.)
      await editFileInEditor(memoryPath);

      // 4. When the editor closes, we are done!
      onDone(`Opened memory file at ${getRelativeMemoryPath(memoryPath)}`, {
        display: 'system'
      });
      
    } catch (error) {
      // If anything exploded, log it and exit.
      onDone(`Error opening memory file: ${error}`);
    }
  };

Explanation:


Under the Hood: The Flow of Interaction

How does a React component control a terminal window? Let's visualize the flow when a user selects a file.

sequenceDiagram participant User participant Comp as MemoryCommand (React) participant FS as File System participant Editor as External Editor participant CLI as Main App User->>Comp: Presses "Enter" on a file Note over Comp: handleSelectMemoryFile() starts Comp->>FS: Ensure directory & file exist FS-->>Comp: File is ready Comp->>Editor: Launch Editor (pauses CLI) User->>Editor: Edits text and Saves User->>Editor: Closes Editor Editor-->>Comp: Process exits Comp->>CLI: calls onDone("Opened memory file...") CLI->>User: Exits and prints message

Internal Implementation Details

  1. Ink Rendering: When we write <Box> or <Text>, Ink translates these into ANSI escape codes. These are special invisible characters that tell your terminal "make this text red" or "move the cursor to line 5."
  2. Suspense: You might have noticed <React.Suspense> in the code. Because reading the file list from the hard drive takes a few milliseconds, Suspense allows React to wait gracefully. However, thanks to our work in the Async Command Lifecycle, the data is usually pre-loaded, so the user sees the list instantly.
  3. Error Handling: We wrap the file operations in a try/catch block. File systems are messyβ€”permissions might be denied, or disks might be full. This ensures that if the file system fails, the application doesn't crash; it just informs the user via onDone.

Summary

In this chapter, we built the Interactive CLI Component.

Right now, our component uses a sub-component called MemoryFileSelector to list the files. But how does that selector know which files to show? And where do these files actually live?

Next Chapter: Memory File Provisioning


Generated by Code IQ