CodeDocs Vault

9. State, UI, and Session Persistence

9.1 State layers

Three different persistence scopes:

Scope Mechanism Representative file
Module-level singletons bootstrap/state.ts getSessionId(), getCwd(), getModelUsage(), getLastAPIRequest()
AppState (React) Singleton store + useSyncExternalStore state/AppStateStore.ts, state/store.ts
Session storage ~/.claude/sessions/<id>/ on disk utils/sessionStorage.ts, utils/conversationRecovery.ts

Bootstrap singletons (bootstrap/state.ts)

The things that need to be callable from anywhere without dependency injection:

AppState (state/AppStateStore.ts)

A deeply-structured immutable state. Notable fields:

Store implementation (state/store.ts)

Tiny:

getState()               // current value
setState(updater)        // immutable updater, ref-equality check, notify listeners
subscribe(listener)      // returns unsubscribe

React components subscribe via useSyncExternalStore(subscribeToAppState, getAppStateSnapshot). The store is wrapped by <AppStateProvider> (state/AppState.tsx) which also handles onChangeAppState callbacks and settings-change reconciliation.

9.2 Ink TUI

Component tree

<App>                                      (components/App.tsx)
├── <FpsMetricsProvider>
├── <StatsProvider>
└── <AppStateProvider onChangeAppState>
    └── <MainScreen>
        ├── <Messages>                     (components/Messages.tsx)
        │   ├── <LogoHeader>
        │   └── <VirtualMessageList>
        │       └── many <MessageRow>      (components/MessageRow.tsx)
        │           └── <Message>          (components/Message.tsx)
        ├── <ContextVisualization>
        ├── <EffortCallout>
        ├── <Footer>                       (with many pills: tasks, tmux, teams, bridge…)
        ├── <StatusLine>
        └── <Input>                        (text input with history, vim mode)

Virtualized rendering

VirtualMessageList renders only the visible window plus a small overscan. MessageRow exports hasContentAfterIndex() which is pre-computed to avoid array-prop pinning in the React fiber cache — a performance trick that keeps memoization stable across scroll updates.

Message collapsing

Tool.isSearchOrReadCommand(input) drives whether a row renders as a compact group summary. renderGroupedToolUse(instances) coalesces parallel calls (three Reads → one collapsed group).

Hook highlights (hooks/)

9.3 Text input

useTextInput handles:

9.4 Session transcript

Every session gets ~/.claude/sessions/<sessionId>/:

transcript.jsonl            # serialized message entries, one per line
metadata.json               # title, cost, duration, lines added/removed, token usage, FPS
logs/                       # hook summaries, context collapse snapshots, file history,
                            # attribution, tool result persistence, content replacement

Recording

recordTranscript(messages) writes appending JSONL. In --bare / CLAUDE_CODE_IS_COWORK modes the write is fire-and-forget; otherwise it awaits (QueryEngine.ts:450-463).

Critical ordering: user messages are persisted before entering the query loop so a kill-mid-request still leaves a resumable transcript. Without this, getLastSessionLog filters out queue-only entries and returns null, and --resume fails with "No conversation found" (QueryEngine.ts:438-449 comment).

For assistant messages, the transcript is written fire-and-forget — claude.ts yields one assistant message per content block, then mutates the last one's message.usage/stop_reason on message_delta; awaiting the write would block the generator and prevent message_delta from being seen before the drain timer elapses (QueryEngine.ts:718-731 comment).

Resume path

--resumeloadTranscriptFromFile()processResumedConversation()loadConversationForResume(). Message state, cost, duration are reconstructed. If a mid-compact crash occurred, the compactMetadata.preservedSegment.tailUuid is used to re-align the transcript up through the tail.

9.5 Worktree state

saveWorktreeState() (from setup.ts) records worktree path, branch, and optional PR number in session storage. On resume into the same session, we restore cwd into the worktree (setup.ts:174-285).

9.6 Bridge / remote session layer

Two orthogonal modes:

Outbound (bridge)

claude remote-control (and legacy aliases remote / sync / bridge) turns this CLI into a long-lived environment server. It:

  1. Registers itself with CCR, providing machine name, git repo, max session count, spawn mode.
  2. Polls for work assignments encrypted as WorkSecret bundles.
  3. For each assignment, spawns an isolated session — single-session (teardown on complete), worktree (persistent server, each session isolated), or same-dir (shared cwd).
  4. Reports status, heartbeats, handles timeouts.

WorkSecret contains session_ingress_token, api_base_url, git info, auth tokens, env vars, MCP config (bridge/types.ts).

Inbound (remote session)

RemoteSessionManager subscribes to a WebSocket (remote/SessionsWebSocket.ts) for messages addressed to a given sessionId. When claude.ai/code/session_… is opened in a browser, the frontend sends user messages over this channel. Callbacks: onMessage, onPermissionRequest, onPermissionCancelled, onConnected, onDisconnected, onReconnecting, onError.

The protocol carries:

Session-ingress tokens authenticate; org UUIDs scope permissions.

9.7 SDK surface

entrypoints/agentSdkTypes.ts is the public re-export. Core types:

Headless / print mode is detected via getIsNonInteractiveSession() (wired from setup.ts:14) and routes to runHeadless() instead of renderAndRun().