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.
🔵 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:
- User Input — The user types a message and presses Enter in the REPL input component.
- Dispatch — The component calls
store.getState().addMessage(userMessage)to add it to state. - Immutable Update — Zustand produces a new state object with the message appended to
messages[]. - Change Notification —
onChangeAppStatefires registered listeners with the old and new state. - Selector Recompute — Selectors like
selectLastMessageandselectIsWaitingrecompute their derived values. - React Re-render — Components subscribed via
useAppState(selectMessages)re-render with the new message list. - API Side Effect — A listener triggers the API call, sending the new message to the Anthropic API.
- Response Dispatch — When the API responds, the assistant message is dispatched back to the store.
- Cycle Repeats — The store updates again, selectors recompute, and the UI shows the assistant reply.
Key Classes & Functions
| Name | File | Description |
|---|---|---|
AppStateStore | AppStateStore.ts | TypeScript interface defining the full state shape (messages, agents, input, permissions) |
createStore | store.ts | Zustand create() call that instantiates the store with initial state and actions |
AppStateProvider | AppState.tsx | React Context provider that wraps the app tree, passing the store instance |
useAppState | AppState.tsx | React hook that subscribes a component to a selector, re-renders on change |
onChangeAppState | onChangeAppState.ts | Subscription API: register callbacks that fire on every state transition |
selectMessages | selectors.ts | Selector returning the current conversation message array |
selectIsWaiting | selectors.ts | Selector returning whether the UI is waiting for an API response |
selectCurrentAgent | selectors.ts | Selector returning the currently active agent or sub-agent |
selectPermissionPrompt | selectors.ts | Selector returning the pending permission prompt, if any |
getTeammateView | teammateViewHelpers.ts | Derives a simplified view of agent state for teammate/sub-agent display |
isAgentActive | teammateViewHelpers.ts | Utility checking whether a given agent ID is currently active |
selectInputBuffer | selectors.ts | Selector returning the current input text buffer |
State Shape Reference
| Property | Type | Description |
|---|---|---|
messages | Message[] | Full conversation history for the current session |
isWaiting | boolean | Whether an API request is in flight |
inputBuffer | string | Current text in the input field |
permissionPrompt | PermissionPrompt | null | Pending permission request from a tool |
agents | Map<AgentId, AgentState> | State of all active agents and sub-agents |
currentAgentId | AgentId | ID of the currently focused agent |
sessionId | SessionId | Branded ID for the current REPL session |
error | AppError | null | Current 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);
}
});