State and rendering

The UI runs as a React and Ink terminal app. AppState is stored in a small external store, React reads it through useSyncExternalStore, and query events update transcript, permissions, MCP state, notifications, and render state as the session runs.

Rendering and state flow

renderAndRun()
  render(<App><REPL /></App>)
  start deferred prefetch work
  waitUntilExit()

ink render()
  ThemeProvider wraps tree
  Ink.createRoot creates terminal renderer
  Ink.render updates reconciler container

AppStateProvider
  createStore(initialState)
  provide store through React context

useAppState(selector)
  useSyncExternalStore(store.subscribe, snapshot)
  re-render when selected value changes
renderAndRun owns the terminal lifecycle

renderAndRun renders the root element, starts deferred prefetch work, waits until the UI exits, and then performs graceful shutdown. This keeps startup work and terminal cleanup in one lifecycle boundary.

The Ink wrapper sets up the React terminal renderer

The local Ink wrapper adds the theme provider and creates a root. The root creates an Ink instance, exposes render, unmount, clear, and waitUntilExit methods, and ultimately renders through a reconciler container wired to terminal streams.

External reference: Ink terminal UI.

AppState is broad but explicit

AppState includes tool permission context, bridge state, MCP data, plugin data, notifications, elicitation state, thinking state, prompt suggestions, hooks, and more. The default state constructor initializes these pieces so the REPL can assume the shape exists.

The external store keeps UI subscription simple

The store implementation is intentionally small: getState, setState, and subscribe. AppStateProvider creates the store once, then useAppState uses useSyncExternalStore to subscribe to selected values.

store = createStore(initialState)
setState(fn)
  state = fn(state)
  listeners.forEach(listener)

useAppState(selector)
  useSyncExternalStore(
    store.subscribe,
    () => selector(store.getState())
  )

External reference: React useSyncExternalStore.

The App wrapper provides runtime context

The top-level App component wraps children with frame-rate, stats, and AppState providers. Downstream components can then update shared state as model events arrive, MCP connections change, permissions are requested, and prompt suggestions are calculated.

Code reference: App provider wrapper

Fresh state reads prevent stale tool context

The REPL constructs ToolUseContext from current store values when a query begins. This is important for long-running terminal sessions because MCP clients, permission context, resources, and app callbacks can change after the REPL component first rendered.

Code reference: fresh ToolUseContext from current state