source dump of claude code
at main 569 lines 22 kB view raw
1import type { Notification } from 'src/context/notifications.js' 2import type { TodoList } from 'src/utils/todo/types.js' 3import type { BridgePermissionCallbacks } from '../bridge/bridgePermissionCallbacks.js' 4import type { Command } from '../commands.js' 5import type { ChannelPermissionCallbacks } from '../services/mcp/channelPermissions.js' 6import type { ElicitationRequestEvent } from '../services/mcp/elicitationHandler.js' 7import type { 8 MCPServerConnection, 9 ServerResource, 10} from '../services/mcp/types.js' 11import { shouldEnablePromptSuggestion } from '../services/PromptSuggestion/promptSuggestion.js' 12import { 13 getEmptyToolPermissionContext, 14 type Tool, 15 type ToolPermissionContext, 16} from '../Tool.js' 17import type { TaskState } from '../tasks/types.js' 18import type { AgentColorName } from '../tools/AgentTool/agentColorManager.js' 19import type { AgentDefinitionsResult } from '../tools/AgentTool/loadAgentsDir.js' 20import type { AllowedPrompt } from '../tools/ExitPlanModeTool/ExitPlanModeV2Tool.js' 21import type { AgentId } from '../types/ids.js' 22import type { Message, UserMessage } from '../types/message.js' 23import type { LoadedPlugin, PluginError } from '../types/plugin.js' 24import type { DeepImmutable } from '../types/utils.js' 25import { 26 type AttributionState, 27 createEmptyAttributionState, 28} from '../utils/commitAttribution.js' 29import type { EffortValue } from '../utils/effort.js' 30import type { FileHistoryState } from '../utils/fileHistory.js' 31import type { REPLHookContext } from '../utils/hooks/postSamplingHooks.js' 32import type { SessionHooksState } from '../utils/hooks/sessionHooks.js' 33import type { ModelSetting } from '../utils/model/model.js' 34import type { DenialTrackingState } from '../utils/permissions/denialTracking.js' 35import type { PermissionMode } from '../utils/permissions/PermissionMode.js' 36import { getInitialSettings } from '../utils/settings/settings.js' 37import type { SettingsJson } from '../utils/settings/types.js' 38import { shouldEnableThinkingByDefault } from '../utils/thinking.js' 39import type { Store } from './store.js' 40 41export type CompletionBoundary = 42 | { type: 'complete'; completedAt: number; outputTokens: number } 43 | { type: 'bash'; command: string; completedAt: number } 44 | { type: 'edit'; toolName: string; filePath: string; completedAt: number } 45 | { 46 type: 'denied_tool' 47 toolName: string 48 detail: string 49 completedAt: number 50 } 51 52export type SpeculationResult = { 53 messages: Message[] 54 boundary: CompletionBoundary | null 55 timeSavedMs: number 56} 57 58export type SpeculationState = 59 | { status: 'idle' } 60 | { 61 status: 'active' 62 id: string 63 abort: () => void 64 startTime: number 65 messagesRef: { current: Message[] } // Mutable ref - avoids array spreading per message 66 writtenPathsRef: { current: Set<string> } // Mutable ref - relative paths written to overlay 67 boundary: CompletionBoundary | null 68 suggestionLength: number 69 toolUseCount: number 70 isPipelined: boolean 71 contextRef: { current: REPLHookContext } 72 pipelinedSuggestion?: { 73 text: string 74 promptId: 'user_intent' | 'stated_intent' 75 generationRequestId: string | null 76 } | null 77 } 78 79export const IDLE_SPECULATION_STATE: SpeculationState = { status: 'idle' } 80 81export type FooterItem = 82 | 'tasks' 83 | 'tmux' 84 | 'bagel' 85 | 'teams' 86 | 'bridge' 87 | 'companion' 88 89export type AppState = DeepImmutable<{ 90 settings: SettingsJson 91 verbose: boolean 92 mainLoopModel: ModelSetting 93 mainLoopModelForSession: ModelSetting 94 statusLineText: string | undefined 95 expandedView: 'none' | 'tasks' | 'teammates' 96 isBriefOnly: boolean 97 // Optional - only present when ENABLE_AGENT_SWARMS is true (for dead code elimination) 98 showTeammateMessagePreview?: boolean 99 selectedIPAgentIndex: number 100 // CoordinatorTaskPanel selection: -1 = pill, 0 = main, 1..N = agent rows. 101 // AppState (not local) so the panel can read it directly without prop-drilling 102 // through PromptInput → PromptInputFooter. 103 coordinatorTaskIndex: number 104 viewSelectionMode: 'none' | 'selecting-agent' | 'viewing-agent' 105 // Which footer pill is focused (arrow-key navigation below the prompt). 106 // Lives in AppState so pill components rendered outside PromptInput 107 // (CompanionSprite in REPL.tsx) can read their own focused state. 108 footerSelection: FooterItem | null 109 toolPermissionContext: ToolPermissionContext 110 spinnerTip?: string 111 // Agent name from --agent CLI flag or settings (for logo display) 112 agent: string | undefined 113 // Assistant mode fully enabled (settings + GrowthBook gate + trust). 114 // Single source of truth - computed once in main.tsx before option 115 // mutation, consumers read this instead of re-calling isAssistantMode(). 116 kairosEnabled: boolean 117 // Remote session URL for --remote mode (shown in footer indicator) 118 remoteSessionUrl: string | undefined 119 // Remote session WS state (`claude assistant` viewer). 'connected' means the 120 // live event stream is open; 'reconnecting' = transient WS drop, backoff 121 // in progress; 'disconnected' = permanent close or reconnects exhausted. 122 remoteConnectionStatus: 123 | 'connecting' 124 | 'connected' 125 | 'reconnecting' 126 | 'disconnected' 127 // `claude assistant`: count of background tasks (Agent calls, teammates, 128 // workflows) running inside the REMOTE daemon child. Event-sourced from 129 // system/task_started and system/task_notification on the WS. The local 130 // AppState.tasks is always empty in viewer mode — the tasks live in a 131 // different process. 132 remoteBackgroundTaskCount: number 133 // Always-on bridge: desired state (controlled by /config or footer toggle) 134 replBridgeEnabled: boolean 135 // Always-on bridge: true when activated via /remote-control command, false when config-driven 136 replBridgeExplicit: boolean 137 // Outbound-only mode: forward events to CCR but reject inbound prompts/control 138 replBridgeOutboundOnly: boolean 139 // Always-on bridge: env registered + session created (= "Ready") 140 replBridgeConnected: boolean 141 // Always-on bridge: ingress WebSocket is open (= "Connected" - user on claude.ai) 142 replBridgeSessionActive: boolean 143 // Always-on bridge: poll loop is in error backoff (= "Reconnecting") 144 replBridgeReconnecting: boolean 145 // Always-on bridge: connect URL for Ready state (?bridge=envId) 146 replBridgeConnectUrl: string | undefined 147 // Always-on bridge: session URL on claude.ai (set when connected) 148 replBridgeSessionUrl: string | undefined 149 // Always-on bridge: IDs for debugging (shown in dialog when --verbose) 150 replBridgeEnvironmentId: string | undefined 151 replBridgeSessionId: string | undefined 152 // Always-on bridge: error message when connection fails (shown in BridgeDialog) 153 replBridgeError: string | undefined 154 // Always-on bridge: session name set via `/remote-control <name>` (used as session title) 155 replBridgeInitialName: string | undefined 156 // Always-on bridge: first-time remote dialog pending (set by /remote-control command) 157 showRemoteCallout: boolean 158}> & { 159 // Unified task state - excluded from DeepImmutable because TaskState contains function types 160 tasks: { [taskId: string]: TaskState } 161 // Name → AgentId registry populated by Agent tool when `name` is provided. 162 // Latest-wins on collision. Used by SendMessage to route by name. 163 agentNameRegistry: Map<string, AgentId> 164 // Task ID that has been foregrounded - its messages are shown in main view 165 foregroundedTaskId?: string 166 // Task ID of in-process teammate whose transcript is being viewed (undefined = leader's view) 167 viewingAgentTaskId?: string 168 // Latest companion reaction from the friend observer (src/buddy/observer.ts) 169 companionReaction?: string 170 // Timestamp of last /buddy pet — CompanionSprite renders hearts while recent 171 companionPetAt?: number 172 // TODO (ashwin): see if we can use utility-types DeepReadonly for this 173 mcp: { 174 clients: MCPServerConnection[] 175 tools: Tool[] 176 commands: Command[] 177 resources: Record<string, ServerResource[]> 178 /** 179 * Incremented by /reload-plugins to trigger MCP effects to re-run 180 * and pick up newly-enabled plugin MCP servers. Effects read this 181 * as a dependency; the value itself is not consumed. 182 */ 183 pluginReconnectKey: number 184 } 185 plugins: { 186 enabled: LoadedPlugin[] 187 disabled: LoadedPlugin[] 188 commands: Command[] 189 /** 190 * Plugin system errors collected during loading and initialization. 191 * See {@link PluginError} type documentation for complete details on error 192 * structure, context fields, and display format. 193 */ 194 errors: PluginError[] 195 // Installation status for background plugin/marketplace installation 196 installationStatus: { 197 marketplaces: Array<{ 198 name: string 199 status: 'pending' | 'installing' | 'installed' | 'failed' 200 error?: string 201 }> 202 plugins: Array<{ 203 id: string 204 name: string 205 status: 'pending' | 'installing' | 'installed' | 'failed' 206 error?: string 207 }> 208 } 209 /** 210 * Set to true when plugin state on disk has changed (background reconcile, 211 * /plugin menu install, external settings edit) and active components are 212 * stale. In interactive mode, user runs /reload-plugins to consume. In 213 * headless mode, refreshPluginState() auto-consumes via refreshActivePlugins(). 214 */ 215 needsRefresh: boolean 216 } 217 agentDefinitions: AgentDefinitionsResult 218 fileHistory: FileHistoryState 219 attribution: AttributionState 220 todos: { [agentId: string]: TodoList } 221 remoteAgentTaskSuggestions: { summary: string; task: string }[] 222 notifications: { 223 current: Notification | null 224 queue: Notification[] 225 } 226 elicitation: { 227 queue: ElicitationRequestEvent[] 228 } 229 thinkingEnabled: boolean | undefined 230 promptSuggestionEnabled: boolean 231 sessionHooks: SessionHooksState 232 tungstenActiveSession?: { 233 sessionName: string 234 socketName: string 235 target: string // The tmux target (e.g., "session:window.pane") 236 } 237 tungstenLastCapturedTime?: number // Timestamp when frame was captured for model 238 tungstenLastCommand?: { 239 command: string // The command string to display (e.g., "Enter", "echo hello") 240 timestamp: number // When the command was sent 241 } 242 // Sticky tmux panel visibility — mirrors globalConfig.tungstenPanelVisible for reactivity. 243 tungstenPanelVisible?: boolean 244 // Transient auto-hide at turn end — separate from tungstenPanelVisible so the 245 // pill stays in the footer (user can reopen) but the panel content doesn't take 246 // screen space when idle. Cleared on next Tmux tool use or user toggle. NOT persisted. 247 tungstenPanelAutoHidden?: boolean 248 // WebBrowser tool (codename bagel): pill visible in footer 249 bagelActive?: boolean 250 // WebBrowser tool: current page URL shown in pill label 251 bagelUrl?: string 252 // WebBrowser tool: sticky panel visibility toggle 253 bagelPanelVisible?: boolean 254 // chicago MCP session state. Types inlined (not imported from 255 // @ant/computer-use-mcp/types) so external typecheck passes without the 256 // ant-scoped dep resolved. Shapes match `AppGrant`/`CuGrantFlags` 257 // structurally — wrapper.tsx assigns via structural compatibility. Only 258 // populated when feature('CHICAGO_MCP') is active. 259 computerUseMcpState?: { 260 // Session-scoped app allowlist. NOT persisted across resume. 261 allowedApps?: readonly { 262 bundleId: string 263 displayName: string 264 grantedAt: number 265 }[] 266 // Clipboard/system-key grant flags (orthogonal to allowlist). 267 grantFlags?: { 268 clipboardRead: boolean 269 clipboardWrite: boolean 270 systemKeyCombos: boolean 271 } 272 // Dims-only (NOT the blob) for scaleCoord after compaction. The full 273 // `ScreenshotResult` including base64 is process-local in wrapper.tsx. 274 lastScreenshotDims?: { 275 width: number 276 height: number 277 displayWidth: number 278 displayHeight: number 279 displayId?: number 280 originX?: number 281 originY?: number 282 } 283 // Accumulated by onAppsHidden, cleared + unhidden at turn end. 284 hiddenDuringTurn?: ReadonlySet<string> 285 // Which display CU targets. Written back by the package's 286 // `autoTargetDisplay` resolver via `onResolvedDisplayUpdated`. Persisted 287 // across resume so clicks stay on the display the model last saw. 288 selectedDisplayId?: number 289 // True when the model explicitly picked a display via `switch_display`. 290 // Makes `handleScreenshot` skip the resolver chase chain and honor 291 // `selectedDisplayId` directly. Cleared on resolver writeback (pinned 292 // display unplugged → Swift fell back to main) and on 293 // `switch_display("auto")`. 294 displayPinnedByModel?: boolean 295 // Sorted comma-joined bundle-ID set the display was last auto-resolved 296 // for. `handleScreenshot` only re-resolves when the allowed set has 297 // changed since — keeps the resolver from yanking on every screenshot. 298 displayResolvedForApps?: string 299 } 300 // REPL tool VM context - persists across REPL calls for state sharing 301 replContext?: { 302 vmContext: import('vm').Context 303 registeredTools: Map< 304 string, 305 { 306 name: string 307 description: string 308 schema: Record<string, unknown> 309 handler: (args: Record<string, unknown>) => Promise<unknown> 310 } 311 > 312 console: { 313 log: (...args: unknown[]) => void 314 error: (...args: unknown[]) => void 315 warn: (...args: unknown[]) => void 316 info: (...args: unknown[]) => void 317 debug: (...args: unknown[]) => void 318 getStdout: () => string 319 getStderr: () => string 320 clear: () => void 321 } 322 } 323 teamContext?: { 324 teamName: string 325 teamFilePath: string 326 leadAgentId: string 327 // Self-identity for swarm members (separate processes in tmux panes) 328 // Note: This is different from toolUseContext.agentId which is for in-process subagents 329 selfAgentId?: string // Swarm member's own ID (same as leadAgentId for leaders) 330 selfAgentName?: string // Swarm member's name ('team-lead' for leaders) 331 isLeader?: boolean // True if this swarm member is the team leader 332 selfAgentColor?: string // Assigned color for UI (used by dynamically joined sessions) 333 teammates: { 334 [teammateId: string]: { 335 name: string 336 agentType?: string 337 color?: string 338 tmuxSessionName: string 339 tmuxPaneId: string 340 cwd: string 341 worktreePath?: string 342 spawnedAt: number 343 } 344 } 345 } 346 // Standalone agent context for non-swarm sessions with custom name/color 347 standaloneAgentContext?: { 348 name: string 349 color?: AgentColorName 350 } 351 inbox: { 352 messages: Array<{ 353 id: string 354 from: string 355 text: string 356 timestamp: string 357 status: 'pending' | 'processing' | 'processed' 358 color?: string 359 summary?: string 360 }> 361 } 362 // Worker sandbox permission requests (leader side) - for network access approval 363 workerSandboxPermissions: { 364 queue: Array<{ 365 requestId: string 366 workerId: string 367 workerName: string 368 workerColor?: string 369 host: string 370 createdAt: number 371 }> 372 selectedIndex: number 373 } 374 // Pending permission request on worker side (shown while waiting for leader approval) 375 pendingWorkerRequest: { 376 toolName: string 377 toolUseId: string 378 description: string 379 } | null 380 // Pending sandbox permission request on worker side 381 pendingSandboxRequest: { 382 requestId: string 383 host: string 384 } | null 385 promptSuggestion: { 386 text: string | null 387 promptId: 'user_intent' | 'stated_intent' | null 388 shownAt: number 389 acceptedAt: number 390 generationRequestId: string | null 391 } 392 speculation: SpeculationState 393 speculationSessionTimeSavedMs: number 394 skillImprovement: { 395 suggestion: { 396 skillName: string 397 updates: { section: string; change: string; reason: string }[] 398 } | null 399 } 400 // Auth version - incremented on login/logout to trigger re-fetching of auth-dependent data 401 authVersion: number 402 // Initial message to process (from CLI args or plan mode exit) 403 // When set, REPL will process the message and trigger a query 404 initialMessage: { 405 message: UserMessage 406 clearContext?: boolean 407 mode?: PermissionMode 408 // Session-scoped permission rules from plan mode (e.g., "run tests", "install dependencies") 409 allowedPrompts?: AllowedPrompt[] 410 } | null 411 // Pending plan verification state (set when exiting plan mode) 412 // Used by VerifyPlanExecution tool to trigger background verification 413 pendingPlanVerification?: { 414 plan: string 415 verificationStarted: boolean 416 verificationCompleted: boolean 417 } 418 // Denial tracking for classifier modes (YOLO, headless, etc.) - falls back to prompting when limits exceeded 419 denialTracking?: DenialTrackingState 420 // Active overlays (Select dialogs, etc.) for Escape key coordination 421 activeOverlays: ReadonlySet<string> 422 // Fast mode 423 fastMode?: boolean 424 // Advisor model for server-side advisor tool (undefined = disabled). 425 advisorModel?: string 426 // Effort value 427 effortValue?: EffortValue 428 // Set synchronously in launchUltraplan before the detached flow starts. 429 // Prevents duplicate launches during the ~5s window before 430 // ultraplanSessionUrl is set by teleportToRemote. Cleared by launchDetached 431 // once the URL is set or on failure. 432 ultraplanLaunching?: boolean 433 // Active ultraplan CCR session URL. Set while the RemoteAgentTask runs; 434 // truthy disables the keyword trigger + rainbow. Cleared when the poll 435 // reaches terminal state. 436 ultraplanSessionUrl?: string 437 // Approved ultraplan awaiting user choice (implement here vs fresh session). 438 // Set by RemoteAgentTask poll on approval; cleared by UltraplanChoiceDialog. 439 ultraplanPendingChoice?: { plan: string; sessionId: string; taskId: string } 440 // Pre-launch permission dialog. Set by /ultraplan (slash or keyword); 441 // cleared by UltraplanLaunchDialog on choice. 442 ultraplanLaunchPending?: { blurb: string } 443 // Remote-harness side: set via set_permission_mode control_request, 444 // pushed to CCR external_metadata.is_ultraplan_mode by onChangeAppState. 445 isUltraplanMode?: boolean 446 // Always-on bridge: permission callbacks for bidirectional permission checks 447 replBridgePermissionCallbacks?: BridgePermissionCallbacks 448 // Channel permission callbacks — permission prompts over Telegram/iMessage/etc. 449 // Races against local UI + bridge + hooks + classifier via claim() in 450 // interactiveHandler.ts. Constructed once in useManageMCPConnections. 451 channelPermissionCallbacks?: ChannelPermissionCallbacks 452} 453 454export type AppStateStore = Store<AppState> 455 456export function getDefaultAppState(): AppState { 457 // Determine initial permission mode for teammates spawned with plan_mode_required 458 // Use lazy require to avoid circular dependency with teammate.ts 459 /* eslint-disable @typescript-eslint/no-require-imports */ 460 const teammateUtils = 461 require('../utils/teammate.js') as typeof import('../utils/teammate.js') 462 /* eslint-enable @typescript-eslint/no-require-imports */ 463 const initialMode: PermissionMode = 464 teammateUtils.isTeammate() && teammateUtils.isPlanModeRequired() 465 ? 'plan' 466 : 'default' 467 468 return { 469 settings: getInitialSettings(), 470 tasks: {}, 471 agentNameRegistry: new Map(), 472 verbose: false, 473 mainLoopModel: null, // alias, full name (as with --model or env var), or null (default) 474 mainLoopModelForSession: null, 475 statusLineText: undefined, 476 expandedView: 'none', 477 isBriefOnly: false, 478 showTeammateMessagePreview: false, 479 selectedIPAgentIndex: -1, 480 coordinatorTaskIndex: -1, 481 viewSelectionMode: 'none', 482 footerSelection: null, 483 kairosEnabled: false, 484 remoteSessionUrl: undefined, 485 remoteConnectionStatus: 'connecting', 486 remoteBackgroundTaskCount: 0, 487 replBridgeEnabled: false, 488 replBridgeExplicit: false, 489 replBridgeOutboundOnly: false, 490 replBridgeConnected: false, 491 replBridgeSessionActive: false, 492 replBridgeReconnecting: false, 493 replBridgeConnectUrl: undefined, 494 replBridgeSessionUrl: undefined, 495 replBridgeEnvironmentId: undefined, 496 replBridgeSessionId: undefined, 497 replBridgeError: undefined, 498 replBridgeInitialName: undefined, 499 showRemoteCallout: false, 500 toolPermissionContext: { 501 ...getEmptyToolPermissionContext(), 502 mode: initialMode, 503 }, 504 agent: undefined, 505 agentDefinitions: { activeAgents: [], allAgents: [] }, 506 fileHistory: { 507 snapshots: [], 508 trackedFiles: new Set(), 509 snapshotSequence: 0, 510 }, 511 attribution: createEmptyAttributionState(), 512 mcp: { 513 clients: [], 514 tools: [], 515 commands: [], 516 resources: {}, 517 pluginReconnectKey: 0, 518 }, 519 plugins: { 520 enabled: [], 521 disabled: [], 522 commands: [], 523 errors: [], 524 installationStatus: { 525 marketplaces: [], 526 plugins: [], 527 }, 528 needsRefresh: false, 529 }, 530 todos: {}, 531 remoteAgentTaskSuggestions: [], 532 notifications: { 533 current: null, 534 queue: [], 535 }, 536 elicitation: { 537 queue: [], 538 }, 539 thinkingEnabled: shouldEnableThinkingByDefault(), 540 promptSuggestionEnabled: shouldEnablePromptSuggestion(), 541 sessionHooks: new Map(), 542 inbox: { 543 messages: [], 544 }, 545 workerSandboxPermissions: { 546 queue: [], 547 selectedIndex: 0, 548 }, 549 pendingWorkerRequest: null, 550 pendingSandboxRequest: null, 551 promptSuggestion: { 552 text: null, 553 promptId: null, 554 shownAt: 0, 555 acceptedAt: 0, 556 generationRequestId: null, 557 }, 558 speculation: IDLE_SPECULATION_STATE, 559 speculationSessionTimeSavedMs: 0, 560 skillImprovement: { 561 suggestion: null, 562 }, 563 authVersion: 0, 564 initialMessage: null, 565 effortValue: undefined, 566 activeOverlays: new Set<string>(), 567 fastMode: false, 568 } 569}