A Zustand-inspired reactive store for the terminal UI
import type { Notification } from 'src/context/notifications.js'import type { TodoList } from 'src/utils/todo/types.js'import type { BridgePermissionCallbacks } from '../bridge/bridgePermissionCallbacks.js'import type { Command } from '../commands.js'import type { ChannelPermissionCallbacks } from '../services/mcp/channelPermissions.js'import type { ElicitationRequestEvent } from '../services/mcp/elicitationHandler.js'import type {MCPServerConnection,
ServerResource,
} from '../services/mcp/types.js'
import { shouldEnablePromptSuggestion } from '../services/PromptSuggestion/promptSuggestion.js'import {getEmptyToolPermissionContext,
type Tool,
type ToolPermissionContext,
} from '../Tool.js'
import type { TaskState } from '../tasks/types.js'import type { AgentColorName } from '../tools/AgentTool/agentColorManager.js'import type { AgentDefinitionsResult } from '../tools/AgentTool/loadAgentsDir.js'import type { AllowedPrompt } from '../tools/ExitPlanModeTool/ExitPlanModeV2Tool.js'import type { AgentId } from '../types/ids.js'import type { Message, UserMessage } from '../types/message.js'import type { LoadedPlugin, PluginError } from '../types/plugin.js'import type { DeepImmutable } from '../types/utils.js'import {type AttributionState,
createEmptyAttributionState,
} from '../utils/commitAttribution.js'
import type { EffortValue } from '../utils/effort.js'import type { FileHistoryState } from '../utils/fileHistory.js'import type { REPLHookContext } from '../utils/hooks/postSamplingHooks.js'import type { SessionHooksState } from '../utils/hooks/sessionHooks.js'import type { ModelSetting } from '../utils/model/model.js'import type { DenialTrackingState } from '../utils/permissions/denialTracking.js'import type { PermissionMode } from '../utils/permissions/PermissionMode.js'import { getInitialSettings } from '../utils/settings/settings.js'import type { SettingsJson } from '../utils/settings/types.js'import { shouldEnableThinkingByDefault } from '../utils/thinking.js'import type { Store } from './store.js'export type CompletionBoundary =
| { type: 'complete'; completedAt: number; outputTokens: number } | { type: 'bash'; command: string; completedAt: number } | { type: 'edit'; toolName: string; filePath: string; completedAt: number } | {type: 'denied_tool'
toolName: string
detail: string
completedAt: number
}
export type SpeculationResult = {messages: Message[]
boundary: CompletionBoundary | null
timeSavedMs: number
}
export type SpeculationState =
| { status: 'idle' } | {status: 'active'
id: string
abort: () => void
startTime: number
messagesRef: { current: Message[] } // Mutable ref - avoids array spreading per message writtenPathsRef: { current: Set<string> } // Mutable ref - relative paths written to overlayboundary: CompletionBoundary | null
suggestionLength: number
toolUseCount: number
isPipelined: boolean
contextRef: { current: REPLHookContext } pipelinedSuggestion?: {text: string
promptId: 'user_intent' | 'stated_intent'
generationRequestId: string | null
} | null
}
export const IDLE_SPECULATION_STATE: SpeculationState = { status: 'idle' }AppStateStore is Claude Code's single source of truth. Unlike browser apps that can use React context freely, a terminal app using Ink.js needs a careful state model — unnecessary re-renders mean visible flicker.
State is DeepImmutable — TypeScript's type system prevents direct mutations. The only way to change state is through the store's dispatch function, creating an audit trail of every update.
The message log is append-only. When a message is "deleted" (e.g., on /clear), a TombstoneMessage is appended with the IDs of deleted messages. This makes the history easy to replay and avoids index mutation bugs.
Memoized selectors in `src/state/selectors.ts` compute derived values once and cache them. Components subscribe to selectors, not raw state — they only re-render when their specific slice changes.
Ask anything about Immutable State Management
Powered by Groq · Enter to send, Shift+Enter for newline