state — Zustand State Management

Zustand State Management

Redux-like state store powered by Zustand. The single source of truth for all REPL UI state — conversation history, agent status, input buffer, permission prompts, and tool execution state. Provides a React Context provider, typed selectors, change notifiers, and agent state utilities.

6
Files
1
Store
15+
Selectors
TS
Language

🔵 Zustand Store 🟢 React Context 🟡 Selectors 🔴 Change Notifiers

💡 What is this?

The state module is the centralized state management layer for Claude Code's React-based terminal UI. It uses Zustand (a lightweight alternative to Redux) to create a single store that holds all application state, from conversation messages to the current permission prompt.

🧩 Analogy: Think of it like a flight control tower

Just as a flight control tower is the single source of truth for all aircraft positions, runway status, and weather conditions — and every controller queries the same shared state — the Zustand store is the single source of truth for all UI state. Components don't talk to each other directly; they read from and write to the store, and the store notifies them of changes.

Architecture

flowchart LR
  subgraph Store["Zustand Store"]
    SHAPE["AppStateStore.ts
State Shape"] CREATE["store.ts
Store Creation"] end subgraph React["React Layer"] CTX["AppState.tsx
Context Provider"] HOOK["useAppState
Hook"] end subgraph Utilities["Utilities"] SEL["selectors.ts
State Queries"] NOTIFY["onChangeAppState.ts
Change Notifier"] TEAM["teammateViewHelpers.ts
Agent Utils"] end SHAPE --> CREATE CREATE --> CTX CTX --> HOOK HOOK --> SEL CREATE --> NOTIFY CREATE --> TEAM SEL --> |reads| HOOK NOTIFY --> |subscribes| CREATE

Data Flow

flowchart TD
  A["User Action
(keypress, submit)"] --> B["Component calls
store.dispatch()"] B --> C["Zustand updates
immutable state"] C --> D["Change notifier fires
onChangeAppState.ts"] D --> E["Subscribed selectors
recompute"] E --> F["React components
re-render"] C --> G["Side effects
(API calls, file writes)"] G --> H["Results dispatched
back to store"] H --> C

🚶 Step-by-Step Narrated Walkthrough

Follow a user message submission through the state management pipeline:

  1. User Input — The user types a message and presses Enter in the REPL input component.
  2. Dispatch — The component calls store.getState().addMessage(userMessage) to add it to state.
  3. Immutable Update — Zustand produces a new state object with the message appended to messages[].
  4. Change Notification — onChangeAppState fires registered listeners with the old and new state.
  5. Selector Recompute — Selectors like selectLastMessage and selectIsWaiting recompute their derived values.
  6. React Re-render — Components subscribed via useAppState(selectMessages) re-render with the new message list.
  7. API Side Effect — A listener triggers the API call, sending the new message to the Anthropic API.
  8. Response Dispatch — When the API responds, the assistant message is dispatched back to the store.
  9. Cycle Repeats — The store updates again, selectors recompute, and the UI shows the assistant reply.

Key Classes & Functions

NameFileDescription
AppStateStoreAppStateStore.tsTypeScript interface defining the full state shape (messages, agents, input, permissions)
createStorestore.tsZustand create() call that instantiates the store with initial state and actions
AppStateProviderAppState.tsxReact Context provider that wraps the app tree, passing the store instance
useAppStateAppState.tsxReact hook that subscribes a component to a selector, re-renders on change
onChangeAppStateonChangeAppState.tsSubscription API: register callbacks that fire on every state transition
selectMessagesselectors.tsSelector returning the current conversation message array
selectIsWaitingselectors.tsSelector returning whether the UI is waiting for an API response
selectCurrentAgentselectors.tsSelector returning the currently active agent or sub-agent
selectPermissionPromptselectors.tsSelector returning the pending permission prompt, if any
getTeammateViewteammateViewHelpers.tsDerives a simplified view of agent state for teammate/sub-agent display
isAgentActiveteammateViewHelpers.tsUtility checking whether a given agent ID is currently active
selectInputBufferselectors.tsSelector returning the current input text buffer

State Shape Reference

PropertyTypeDescription
messagesMessage[]Full conversation history for the current session
isWaitingbooleanWhether an API request is in flight
inputBufferstringCurrent text in the input field
permissionPromptPermissionPrompt | nullPending permission request from a tool
agentsMap<AgentId, AgentState>State of all active agents and sub-agents
currentAgentIdAgentIdID of the currently focused agent
sessionIdSessionIdBranded ID for the current REPL session
errorAppError | nullCurrent error state for display in the UI

Directory Map

Source tree for state
state/
├── index.ts                   ← Public API barrel export
├── AppStateStore.ts           ← State shape interface definition
├── store.ts                   ← Zustand store creation & actions
├── AppState.tsx               ← React Context provider & useAppState hook
├── onChangeAppState.ts        ← Change subscription / notifier
├── selectors.ts               ← Derived state query functions
└── teammateViewHelpers.ts     ← Agent state display utilities

Code Example

import { AppStateProvider, useAppState } from "./state/AppState";
import { selectMessages, selectIsWaiting } from "./state/selectors";
import { onChangeAppState } from "./state/onChangeAppState";

// Wrap the app with the state provider
function App() {
  return (
    <AppStateProvider>
      <ReplView />
    </AppStateProvider>
  );
}

// Subscribe to state slices in a component
function ReplView() {
  const messages = useAppState(selectMessages);
  const isWaiting = useAppState(selectIsWaiting);

  return (
    <div>
      {messages.map((msg) => (
        <MessageBubble key={msg.id} message={msg} />
      ))}
      {isWaiting && <Spinner />}
    </div>
  );
}

// Listen for state changes outside React
onChangeAppState((prev, next) => {
  if (prev.messages.length !== next.messages.length) {
    persistConversation(next.messages);
  }
});