Skip to main content
Claude Code’s UI and state are built entirely with React patterns — just targeting the terminal instead of a browser. State flows from a single mutable AppState object through React context into ~140 components rendered by Ink.

State management

AppState

AppState is the global mutable state object, defined in src/state/AppStateStore.ts. It is typed as DeepImmutable (except for function-typed fields like tasks) and covers everything the application needs at runtime:
// src/state/AppStateStore.ts
export type AppState = DeepImmutable<{
  settings: SettingsJson
  verbose: boolean
  mainLoopModel: ModelSetting
  mainLoopModelForSession: ModelSetting
  statusLineText: string | undefined
  toolPermissionContext: ToolPermissionContext
  expandedView: 'none' | 'tasks' | 'teammates'
  // IDE bridge state
  replBridgeEnabled: boolean
  replBridgeConnected: boolean
  replBridgeSessionActive: boolean
  replBridgeSessionUrl: string | undefined
  // Remote session state
  remoteConnectionStatus: 'connecting' | 'connected' | 'reconnecting' | 'disconnected'
  remoteBackgroundTaskCount: number
  // MCP state
  mcp: {
    clients: MCPServerConnection[]
    tools: Tool[]
    commands: Command[]
    resources: Record<string, ServerResource[]>
  }
  // ... and many more fields
}> & {
  tasks: { [taskId: string]: TaskState }
  agentNameRegistry: Map<string, AgentId>
}
The AppState object is passed into tool contexts, giving tools access to conversation history, settings, and runtime state without prop-drilling.

State components

ComponentLocationPurpose
AppState typesrc/state/AppStateStore.tsGlobal mutable state shape
AppState (module)src/state/AppState.tsxStore implementation + React context
storesrc/state/store.tscreateStore() — lightweight observable store
Selectorssrc/state/selectors.tsPure derived-state functions
Change observerssrc/state/onChangeAppState.tsSide-effects triggered by state changes
Teammate helperssrc/state/teammateViewHelpers.tsTeammate/agent view utilities

The store

createStore() in src/state/store.ts creates a lightweight observable store. State updates flow as setAppState(f: (prev: AppState) => AppState) — an immutable update function passed to both the REPL and the Query Engine:
// Consumers update state via the setter
setAppState(prev => ({
  ...prev,
  verbose: true,
}))

Selectors

Selectors in src/state/selectors.ts are pure functions that derive computed values from AppState:
// src/state/selectors.ts
export function getViewedTeammateTask(
  appState: Pick<AppState, 'viewingAgentTaskId' | 'tasks'>,
): InProcessTeammateTaskState | undefined

export type ActiveAgentForInput =
  | { type: 'leader' }
  | { type: 'viewed'; task: InProcessTeammateTaskState }
  | { type: 'named_agent'; task: LocalAgentTaskState }

export function getActiveAgentForInput(appState: AppState): ActiveAgentForInput

Change observers

src/state/onChangeAppState.ts registers side-effects that fire when specific AppState fields change — for example, persisting settings to disk, triggering analytics events, or updating external services when the model changes.

React context

src/context/ provides React context for cross-cutting concerns that are distinct from the core AppState:
ContextLocationPurpose
Notificationssrc/context/notifications.tsIn-app notification queue
Statssrc/context/stats.tsFPS metrics and render stats

Speculation state

AppState also carries a SpeculationState for pre-emptive response generation:
// src/state/AppStateStore.ts
export type SpeculationState =
  | { status: 'idle' }
  | {
      status: 'active'
      id: string
      abort: () => void
      startTime: number
      messagesRef: { current: Message[] }
      writtenPathsRef: { current: Set<string> }
      boundary: CompletionBoundary | null
      suggestionLength: number
      toolUseCount: number
      isPipelined: boolean
    }

UI layer

React + Ink

The entire terminal UI is built with React + Ink. Ink provides terminal-native primitives (Box, Text, useInput()) that map to React components, so all standard React patterns — hooks, context, concurrent rendering — work as expected.
  • Components are styled with Chalk for terminal colors
  • React Compiler is enabled for optimized re-renders
  • src/components/design-system/ contains shared primitives

Main screens

src/screens/ contains three full-screen UI modes:
ScreenFilePurpose
REPLsrc/screens/REPL.tsxMain interactive REPL — the default screen
Doctorsrc/screens/Doctor.tsxEnvironment diagnostics (/doctor command)
Resume conversationsrc/screens/ResumeConversation.tsxSession restore (/resume command)

Design system (src/components/design-system/)

Shared primitive components used across the UI:
ComponentFilePurpose
ThemedTextThemedText.tsxTheme-aware text rendering
ThemedBoxThemedBox.tsxTheme-aware box layout
ThemeProviderThemeProvider.tsxRoot theme context
DialogDialog.tsxModal dialog frame
PanePane.tsxPanel/pane layout
TabsTabs.tsxTab navigation
FuzzyPickerFuzzyPicker.tsxFuzzy-search selection list
ProgressBarProgressBar.tsxTerminal progress bar
LoadingStateLoadingState.tsxLoading indicator
StatusIconStatusIcon.tsxStatus indicator icon
DividerDivider.tsxVisual separator
KeyboardShortcutHintKeyboardShortcutHint.tsxKeybinding hint display
RatchetRatchet.tsxRatcheting scroll/animation primitive
BylineByline.tsxAttribution/byline text
ListItemListItem.tsxList item layout
colorcolor.tsDesign system color tokens

Hooks catalog

src/hooks/ contains ~85 hooks organized by category:

Permission hooks

HookFilePurpose
useCanUseTooluseCanUseTool.tsxCheck if a tool is allowed under current permissions
Permission contexttoolPermission/PermissionContext.tsReact context for tool permission state
Permission loggingtoolPermission/permissionLogging.tsTelemetry for permission decisions
Permission handlerstoolPermission/handlers/Per-mode permission decision handlers

IDE integration hooks

HookPurpose
useIDEIntegrationManages IDE connection lifecycle
useIdeConnectionStatusReactive IDE connection status
useDiffInIDEOpens diffs in the connected IDE
useIdeAtMentionedHandles @-mention suggestions from IDE
useIdeSelectionIDE text selection events
useIdeLoggingIDE diagnostics logging

Input handling hooks

HookPurpose
useTextInputCore text input with cursor management
useVimInputVim-mode keybindings overlay
usePasteHandlerClipboard paste with image support
useInputBufferInput buffering and early capture
useSearchInputSearch box input handling
useTypeaheadTypeahead/autocomplete suggestions
useArrowKeyHistoryArrow-key history navigation
useCommandKeybindingsCommand-palette keybindings
useGlobalKeybindingsApplication-wide keyboard shortcuts
useVirtualScrollVirtualised list scrolling

Session management hooks

HookPurpose
useSessionBackgroundingBackground/foreground session lifecycle
useRemoteSessionRemote session WebSocket management
useAssistantHistoryConversation history access
useHistorySearchSearch across session history
useDirectConnectDirect-connect session setup
useSSHSessionSSH tunnel session management
useTeleportResumeTeleport session resume flow

Plugin and skill hooks

HookPurpose
useManagePluginsPlugin install/remove lifecycle
useSkillsChangeReacts to skill directory changes
useSkillImprovementSurveyPost-skill survey trigger
usePluginRecommendationBaseBase hook for plugin recommendations
useLspPluginRecommendationLSP-specific plugin recommendation
useClaudeCodeHintRecommendationHint-based plugin recommendations
useOfficialMarketplaceNotificationMarketplace update notifications

Notification hooks (src/hooks/notifs/)

HookPurpose
useRateLimitWarningNotificationSurfaces rate-limit warnings
useDeprecationWarningNotificationModel deprecation banners
useModelMigrationNotificationsModel migration prompts
useMcpConnectivityStatusMCP server connectivity status
useIDEStatusIndicatorIDE connection status badge
useAutoModeUnavailableNotificationAuto-mode unavailability warning
useFastModeNotificationFast mode status notification
useNpmDeprecationNotificationnpm package deprecation warning
useInstallMessagesPlugin install status messages
usePluginAutoupdateNotificationPlugin auto-update progress
usePluginInstallationStatusPlugin install/remove status
useSettingsErrorsSettings validation error display
useStartupNotificationFirst-run/startup notices
useTeammateShutdownNotificationTeammate agent shutdown notice
useLspInitializationNotificationLSP server init status

Config schemas and migrations

Config schemas (src/schemas/)

All configuration is validated with Zod v4 schemas. Schema categories:
  • User settings
  • Project-level settings
  • Organization/enterprise policies
  • Permission rules

Migrations (src/migrations/)

Config format changes between versions are handled by migration modules. Each migration reads the old format and transforms it to the current schema. Examples from src/main.tsx:
import { migrateAutoUpdatesToSettings } from './migrations/migrateAutoUpdatesToSettings.js'
import { migrateBypassPermissionsAcceptedToSettings } from './migrations/migrateBypassPermissionsAcceptedToSettings.js'
import { migrateFennecToOpus } from './migrations/migrateFennecToOpus.js'
import { migrateLegacyOpusToCurrent } from './migrations/migrateLegacyOpusToCurrent.js'
import { migrateSonnet45ToSonnet46 } from './migrations/migrateSonnet45ToSonnet46.js'
Migrations run at startup before the REPL mounts, ensuring the config is always in the current schema before any component reads it.

React Compiler

React Compiler is enabled for automatic memoization of components and hooks. This eliminates most manual useMemo / useCallback calls and ensures the terminal UI re-renders only the subtrees affected by a state change — important when rendering large conversation histories with 100+ messages.
Because React Compiler handles memoization automatically, components in src/components/ generally avoid manual memo(), useMemo, and useCallback wrappers unless there is a specific reason.

See also

Architecture Overview

The full pipeline from CLI entrypoint to terminal UI.

Query Engine

How QueryEngine.ts manages streaming, tool calls, and context.

Build docs developers (and LLMs) love