Skip to content

Chapter 1: TUI (Terminal User Interface)

Welcome to the kiro-cli code walkthrough! kiro-cli is a terminal-based AI coding assistant that runs entirely on your machine. When you type kiro-cli, a rich interactive interface appears right inside your terminal — you can chat with an AI agent that reads your code, writes files, runs commands, and calls external plugins. All of this happens locally, with no browser required.

This first chapter focuses on the TUI — the storefront window of kiro-cli. Think of it like a restaurant's dining room: it's where you, the customer, sit down, place your order, and watch the food arrive. The kitchen (the Rust backend) does the heavy lifting, but the dining room is what you see and interact with.

The TUI lives in packages/tui/ and is written in TypeScript with React. Yes — React, in a terminal. Let's explore how that works.


Motivation

Why not just use a plain terminal with stdin/stdout? After all, many CLI tools get by with simple line-by-line I/O.

The answer is that kiro-cli needs to do things a plain terminal can't:

  • Stream markdown with syntax highlighting as the AI responds, token by token
  • Show tool approval dialogs (e.g., "The agent wants to run rm -rf node_modules — allow?")
  • Display multiple subagent sessions running in parallel, each with its own status
  • Handle rich input — multi-line editing, paste detection, command autocomplete, file references
  • Provide theming (dark/light mode) and layout modes (inline vs. expanded)

A plain readline loop can't do any of this. The TUI gives kiro-cli a real application UI — just one that happens to live inside your terminal emulator.


Use Case

Here's what happens when you type kiro-cli and press Enter:

  1. The Rust binary starts and spawns the TUI as a child process (running under Bun)
  2. The TUI clears the screen, shows a welcome message, and presents an input prompt
  3. You type a question like "Explain the main function in src/main.rs"
  4. The TUI sends your message over the Agent Client Protocol (ACP) to the Rust backend
  5. The backend streams back a response — the TUI renders it as formatted markdown in real time
  6. If the agent wants to use a tool (read a file, run a command), a tool approval card appears
  7. You approve or deny, and the conversation continues

The whole experience feels like a chat app — but it's running in your terminal, powered by React components rendered as ANSI escape sequences.


Key Concepts

The TUI is built from five main building blocks. Let's look at each one.

1. React + Twinki (Terminal React Renderer)

The TUI uses React — the same library you'd use for a web app — but instead of rendering to the DOM, it renders to your terminal. The rendering engine is called Twinki, a custom React reconciler that replaces the popular Ink library.

In packages/tui/src/renderer.ts, you can see the proxy that selects the renderer at startup:

// packages/tui/src/renderer.ts
const useTwinki = process.env.KIRO_RENDERER !== 'ink';
const mod = useTwinki ? twinkiMod : inkMod;

export const Box = mod.Box;
export const Text = mod.Text;
export const render = mod.render;

Components like <Box> and <Text> work just like <div> and <span> in a browser — but they produce terminal output with colors, borders, and flexbox layout. We'll dive deep into Twinki in Chapter 2.

2. The Kiro Class (Session Lifecycle Manager)

The Kiro class in packages/tui/src/kiro.ts is the TUI's bridge to the backend. Think of it as the maître d' — it doesn't cook the food, but it manages your reservation (session), takes your order (prompt), and relays it to the kitchen (Rust backend).

// packages/tui/src/kiro.ts (simplified)
export class Kiro {
  private sessionClient?: SessionClient;

  async initialize(agentPath: string): Promise<void> {
    this.sessionClient = new AcpClient(agentPath);
    await this.sessionClient.initialize();
  }

  async streamMessage(content: string, signal: AbortSignal,
    onEvent: (event: AgentStreamEvent) => void): Promise<void> {
    // Sends prompt to backend, calls onEvent for each streamed token
  }
}

Key responsibilities: - Initialize the ACP connection (spawn or connect to the Rust backend) - Create/resume sessions — kiro-cli can pick up where you left off - Stream messages — send your prompt, receive events token-by-token - Cancel in-flight requests (when you press Ctrl+C)

3. Zustand Store (Application State)

All TUI state lives in a single Zustand store defined in packages/tui/src/stores/app-store.ts. Zustand is a lightweight state management library — think of it as a shared whiteboard that every React component can read from and write to.

The store tracks everything:

// What the store manages (conceptual, not literal code):
// - messages[]        — the conversation history
// - isProcessing      — is the agent currently thinking?
// - pendingApproval   — tool waiting for user's yes/no
// - inputBuffer       — what the user is currently typing
// - sessions          — subagent sessions and their statuses
// - slashCommands     — available /commands

Components subscribe to specific slices of state using useAppStore:

const isProcessing = useAppStore((state) => state.isProcessing);
const mode = useAppStore((state) => state.mode);

This keeps re-renders minimal — a component only updates when its slice changes.

4. ACP Client (Backend Communication)

The AcpClient in packages/tui/src/acp-client.ts implements the Agent Client Protocol — a JSON-RPC-over-stdio protocol that carries messages between the TUI and the Rust backend. It's like a waiter's notepad that both the dining room and kitchen understand.

// packages/tui/src/acp-client.ts (simplified)
export class AcpClient implements SessionClient {
  private connection: acp.ClientSideConnection;

  constructor(agentPath: string) {
    // Either spawn the Rust binary as a child process (normal mode)
    // or connect to an existing one via Unix socket (Kanvas mode)
    const socketPath = process.env.KIRO_ACP_SOCKET;
    if (socketPath) {
      const socket = net.connect(socketPath);
      // ... wire up socket streams
    } else {
      this.agentProcess = spawn(agentPath, ['acp', ...args]);
    }
  }
}

The ACP client handles: - Spawning the Rust backend process (or connecting to an existing one) - Sending JSON-RPC requests (session/new, session/prompt, session/cancel) - Receiving streamed notifications (content tokens, tool calls, status updates) - Extension methods like slash commands, MCP server events, and agent switching

5. Input Handling (Keypress → Action)

Terminal input is surprisingly complex. The TUI uses a custom useKeypress hook (in packages/tui/src/hooks/useKeypress.ts) that handles raw terminal bytes — including bracketed paste detection, modifier keys, and special sequences.

// packages/tui/src/hooks/useKeypress.ts (simplified)
const PASTE_START = '\x1b[200~';
const PASTE_END = '\x1b[201~';

export const useKeypress = (handler: KeyHandler) => {
  // Captures raw stdin, detects paste sequences,
  // normalizes key events, and calls the handler
};

The AppContainer component (in packages/tui/src/components/layout/AppContainer.tsx) wires up top-level key bindings:

  • Ctrl+C — cancel the current agent turn
  • Ctrl+Z — suspend the process (like any Unix program)
  • Ctrl+Y — copy OAuth URLs or trigger alert actions
  • Enter — submit the current input to the agent

When you press Enter, the input flows through the Zustand store's sendMessage action, which expands file references, creates an abort controller, and calls kiro.streamMessage().


How It All Fits Together

Here's the full journey of a single keystroke, from your fingers to the AI's response:

sequenceDiagram
    participant User
    participant TUI as TUI (React)
    participant Store as Zustand Store
    participant ACP as ACP Client
    participant Backend as Rust Backend

    User->>TUI: Types "explain main.rs" + Enter
    TUI->>Store: sendMessage("explain main.rs")
    Store->>ACP: kiro.streamMessage(content, signal, onEvent)
    ACP->>Backend: JSON-RPC session/prompt
    Backend-->>ACP: Streamed content events (tokens)
    ACP-->>Store: onEvent(AgentStreamEvent)
    Store-->>TUI: React re-render with new message content

Each streamed token triggers a store update, which triggers a React re-render, which Twinki efficiently paints to the terminal — updating only the lines that changed.


Internal Implementation

Let's walk through the actual code to see how these pieces connect.

Entry Point: packages/tui/src/index.tsx

This is where everything starts. The file is ~300 lines and does three things before React even mounts:

  1. Creates the Kiro instance and Zustand store outside of React (lines 72–79), so initialization begins immediately:
// packages/tui/src/index.tsx
const kiro = new Kiro();
const appStore = createAppStore({ kiro, ... });
  1. Wires up event handlers — the wireUpHandlers() function (around line 83) connects kiro.onCommandsUpdate, kiro.onModelUpdate, kiro.onAgentUpdate, and many more callbacks to the store. This is the plumbing that makes backend events flow into React state.

  2. Renders the React app using Twinki (around line 290):

const instance = render(<App />, {
  exitOnCtrlC: false,
  patchConsole: false,
  kittyKeyboard: { mode: 'auto', flags: ['disambiguateEscapeCodes'] },
});

Notice exitOnCtrlC: false — the TUI handles Ctrl+C itself (to cancel the agent, not exit the app).

The App Component and Layout

The <App> component wraps everything in providers — ThemeProvider, AppStoreContext, ErrorBoundary — and renders <AppContainer>.

Inside AppContainer (packages/tui/src/components/layout/AppContainer.tsx, around line 42), the TUI decides which layout to show based on the current mode:

  • InlineLayout — compact, chat-style view (default)
  • ExpandedLayout — full-screen with more detail
  • CrewMonitorScreen — shows all subagent sessions at once
  • SessionViewScreen — focused view of a single subagent

The layout selection is driven by the store's mode field, which the user can toggle with keyboard shortcuts.

The Message Flow: Store → Kiro → ACP

When you press Enter, the store's sendMessage action (in packages/tui/src/stores/app-store.ts, around line 926) kicks off:

  1. Checks if initialized and not already processing — if busy, queues the message
  2. Expands @file: references and attached files into the prompt content
  3. Creates an AbortController (so Ctrl+C can cancel mid-stream)
  4. Calls kiro.streamMessage(content, signal, onEvent)
  5. The onEvent callback feeds each AgentStreamEvent into the store, which updates the message list

The Kiro.streamMessage method (in packages/tui/src/kiro.ts, around line 260) subscribes to the ACP client's update stream, sets a 3-minute timeout for the first response, and resolves when the backend finishes its turn.


What's Next

You've seen how the TUI captures your input, manages state with Zustand, and communicates with the Rust backend over ACP. But we glossed over something important: how does React actually paint pixels (well, characters) to your terminal?

That's the job of Twinki — a custom React reconciler that replaces Ink with differential line-based rendering. It's the reason the TUI feels snappy even when streaming hundreds of tokens per second.

Continue to Chapter 2: Twinki to see how the rendering engine works under the hood.