AI Assistant

Claude Code uses a dual-state architecture: an immutable React-facing AppState for UI rendering and a mutable bootstrap/state.ts singleton for session-level bookkeeping. This separation keeps the React reconciler efficient (shallow comparisons on immutable snapshots) while giving hot-path code (telemetry, cost tracking, token budgets) zero-overhead mutable access.

Store Pattern

The store implementation in src/state/store.ts is a minimal Zustand-style reactive container. It provides three operations (getState, setState, and subscribe) with an optional onChange callback for side-effect wiring.

export type Store<T> = {
  getState: () => T
  setState: (updater: (prev: T) => T) => void
  subscribe: (listener: Listener) => () => void
}

export function createStore<T>(
  initialState: T,
  onChange?: OnChange<T>,
): Store<T> {
  let state = initialState
  const listeners = new Set<Listener>()

  return {
    getState: () => state,
    setState: (updater) => {
      const prev = state
      const next = updater(prev)
      if (Object.is(next, prev)) return
      state = next
      onChange?.({ newState: next, oldState: prev })
      for (const listener of listeners) listener()
    },
    subscribe: (listener) => {
      listeners.add(listener)
      return () => listeners.delete(listener)
    },
  }
}

Key design choices:

  • Referential equality bail-out: Object.is(next, prev) prevents unnecessary re-renders when the updater returns the same reference.
  • Synchronous notification: listeners fire immediately after state changes, ensuring the UI stays consistent.
  • onChange hook: the AppStateStore passes an onChange callback that propagates diffs to external systems (bridge, telemetry, CCR metadata).

AppState

AppState is the primary UI state tree, defined in src/state/AppStateStore.ts. It is wrapped in DeepImmutable<> to enforce read-only access at the type level.

Top-Level Shape

settingsSettingsJson
User and project settings merged from all config sources.
verboseboolean
Whether verbose/debug output is enabled.
mainLoopModelModelSetting
Current model alias or full name (null for default).
mainLoopModelForSessionModelSetting
Session-scoped model override.
toolPermissionContextToolPermissionContext
Current permission mode, grants, and session-scoped rules.
tasksRecord<string, TaskState>
Unified task state: background agents, teammates, local agent tasks. Excluded from DeepImmutable because TaskState contains function types.
mcpobject
MCP server connections, tools, commands, and resources.
pluginsobject
Enabled/disabled plugins, commands, errors, and installation status.
agentNameRegistryMap<string, AgentId>
Name-to-ID registry populated by the Agent tool when a name parameter is provided.
thinkingEnabledboolean | undefined
Whether extended thinking is active for the session.
speculationSpeculationState
Speculative execution state (idle or active with abort handle, messages ref, etc.).
promptSuggestionobject
Current prompt suggestion text, timing, and generation metadata.
fileHistoryFileHistoryState
Tracked file snapshots for undo/rewind.
attributionAttributionState
Git commit attribution tracking.
todosRecord<string, TodoList>
Per-agent TODO lists.
teamContextobject | undefined
Swarm team membership, teammates, and leader identity.
inboxobject
Inter-agent messages for swarm communication.

UI State Fields

The AppState also carries fields that control the terminal UI layout:

  • expandedView: which panel is expanded ('none' | 'tasks' | 'teammates')
  • footerSelection: which footer pill is focused ('tasks' | 'tmux' | 'bagel' | 'teams' | 'bridge' | 'companion' | null)
  • isBriefOnly: whether the assistant is in brief/compact output mode
  • activeOverlays: set of active overlay IDs for Escape key coordination

Bridge/Remote State

A significant portion of AppState tracks the always-on bridge connection for remote control:

replBridgeEnabled: boolean
replBridgeConnected: boolean
replBridgeSessionActive: boolean
replBridgeReconnecting: boolean
replBridgeConnectUrl: string | undefined
replBridgeSessionUrl: string | undefined

These fields allow the UI to show connection status, session URLs, and error states for the bridge subsystem.

Bootstrap State

The bootstrap state in src/bootstrap/state.ts is a plain mutable singleton. It stores session-scoped values that do not drive React rendering but are read on hot paths.

sessionIdSessionId
Unique identifier for the current session.
parentSessionIdSessionId | undefined
Parent session ID for lineage tracking (plan mode to implementation).
originalCwdstring
The working directory at process start.
projectRootstring
Stable project root, set once at startup. Used for project identity (history, skills, sessions) rather than file operations.
totalCostUSDnumber
Accumulated API cost for the session.
totalAPIDurationnumber
Total time spent on API calls.
totalLinesAddednumber
Lines of code added during the session.
totalLinesRemovednumber
Lines of code removed during the session.
modelUsageRecord<string, ModelUsage>
Per-model token usage breakdown.
isInteractiveboolean
Whether the session is interactive (REPL) vs headless (SDK/pipe).
cwdstring
Current working directory (may change during session via EnterWorktreeTool).
lastInteractionTimenumber
Timestamp of the last user interaction.

Telemetry Counters

Bootstrap state holds OpenTelemetry instrumentation:

meter: Meter | null
sessionCounter: AttributedCounter | null
locCounter: AttributedCounter | null
costCounter: AttributedCounter | null
tokenCounter: AttributedCounter | null
activeTimeCounter: AttributedCounter | null

These counters are null until telemetry is initialized, allowing the bootstrap module to remain import-order agnostic.

Session Tracking

Several fields track session lifecycle for features like plan mode, skill invocation, and cron scheduling:

  • hasExitedPlanMode / needsPlanModeExitAttachment: plan mode lifecycle
  • invokedSkills: map of skill invocations, keyed by agentId:skillName, preserved across compaction
  • sessionCronTasks: ephemeral cron tasks that die with the process
  • registeredHooks: SDK callbacks and plugin native hooks

Selectors

Selectors in src/state/selectors.ts derive computed state from AppState. They are pure functions with no side effects.

getViewedTeammateTask

Returns the currently viewed in-process teammate task, or undefined if no teammate is being viewed or the task is not an in-process teammate type.

function getViewedTeammateTask(
  appState: Pick<AppState, 'viewingAgentTaskId' | 'tasks'>,
): InProcessTeammateTaskState | undefined

getActiveAgentForInput

Determines where user input should be routed. Returns a discriminated union:

type ActiveAgentForInput =
  | { type: 'leader' }                                    // Input goes to the leader
  | { type: 'viewed'; task: InProcessTeammateTaskState }   // Input goes to viewed agent
  | { type: 'named_agent'; task: LocalAgentTaskState }     // Input goes to named agent

Used by input routing logic to direct user messages to the correct agent in multi-agent scenarios.

Dual-State Design Rationale

The split between AppState (immutable, store-based) and bootstrap state (mutable singleton) is intentional. AppState drives React rendering and must follow immutable update patterns for correct reconciliation. Bootstrap state holds values read on hot paths (cost tracking, token counting, telemetry) where the overhead of store updates and listener notification would be wasteful. The two layers are bridged by the onChangeAppState callback, which syncs relevant diffs to external systems.

This design avoids two common pitfalls:

  1. Over-rendering: putting telemetry counters in AppState would trigger React re-renders on every API response.
  2. Stale reads: using the store for hot-path reads would require getState() calls that add overhead in tight loops.