source dump of claude code
at main 150 lines 5.3 kB view raw
1export type SessionState = 'idle' | 'running' | 'requires_action' 2 3/** 4 * Context carried with requires_action transitions so downstream 5 * surfaces (CCR sidebar, push notifications) can show what the 6 * session is blocked on, not just that it's blocked. 7 * 8 * Two delivery paths: 9 * - tool_name + action_description → RequiresActionDetails proto 10 * (webhook payload, typed, logged in Datadog) 11 * - full object → external_metadata.pending_action (queryable JSON 12 * on the Session, lets the frontend iterate on shape without 13 * proto round-trips) 14 */ 15export type RequiresActionDetails = { 16 tool_name: string 17 /** Human-readable summary, e.g. "Editing src/foo.ts", "Running npm test" */ 18 action_description: string 19 tool_use_id: string 20 request_id: string 21 /** Raw tool input — the frontend reads from external_metadata.pending_action.input 22 * to parse question options / plan content without scanning the event stream. */ 23 input?: Record<string, unknown> 24} 25 26import { isEnvTruthy } from './envUtils.js' 27import type { PermissionMode } from './permissions/PermissionMode.js' 28import { enqueueSdkEvent } from './sdkEventQueue.js' 29 30// CCR external_metadata keys — push in onChangeAppState, restore in 31// externalMetadataToAppState. 32export type SessionExternalMetadata = { 33 permission_mode?: string | null 34 is_ultraplan_mode?: boolean | null 35 model?: string | null 36 pending_action?: RequiresActionDetails | null 37 // Opaque — typed at the emit site. Importing PostTurnSummaryOutput here 38 // would leak the import path string into sdk.d.ts via agentSdkBridge's 39 // re-export of SessionState. 40 post_turn_summary?: unknown 41 // Mid-turn progress line from the forked-agent summarizer — fires every 42 // ~5 steps / 2min so long-running turns still surface "what's happening 43 // right now" before post_turn_summary arrives. 44 task_summary?: string | null 45} 46 47type SessionStateChangedListener = ( 48 state: SessionState, 49 details?: RequiresActionDetails, 50) => void 51type SessionMetadataChangedListener = ( 52 metadata: SessionExternalMetadata, 53) => void 54type PermissionModeChangedListener = (mode: PermissionMode) => void 55 56let stateListener: SessionStateChangedListener | null = null 57let metadataListener: SessionMetadataChangedListener | null = null 58let permissionModeListener: PermissionModeChangedListener | null = null 59 60export function setSessionStateChangedListener( 61 cb: SessionStateChangedListener | null, 62): void { 63 stateListener = cb 64} 65 66export function setSessionMetadataChangedListener( 67 cb: SessionMetadataChangedListener | null, 68): void { 69 metadataListener = cb 70} 71 72/** 73 * Register a listener for permission-mode changes from onChangeAppState. 74 * Wired by print.ts to emit an SDK system:status message so CCR/IDE clients 75 * see mode transitions in real time — regardless of which code path mutated 76 * toolPermissionContext.mode (Shift+Tab, ExitPlanMode dialog, slash command, 77 * bridge set_permission_mode, etc.). 78 */ 79export function setPermissionModeChangedListener( 80 cb: PermissionModeChangedListener | null, 81): void { 82 permissionModeListener = cb 83} 84 85let hasPendingAction = false 86let currentState: SessionState = 'idle' 87 88export function getSessionState(): SessionState { 89 return currentState 90} 91 92export function notifySessionStateChanged( 93 state: SessionState, 94 details?: RequiresActionDetails, 95): void { 96 currentState = state 97 stateListener?.(state, details) 98 99 // Mirror details into external_metadata so GetSession carries the 100 // pending-action context without proto changes. Cleared via RFC 7396 101 // null on the next non-blocked transition. 102 if (state === 'requires_action' && details) { 103 hasPendingAction = true 104 metadataListener?.({ 105 pending_action: details, 106 }) 107 } else if (hasPendingAction) { 108 hasPendingAction = false 109 metadataListener?.({ pending_action: null }) 110 } 111 112 // task_summary is written mid-turn by the forked summarizer; clear it at 113 // idle so the next turn doesn't briefly show the previous turn's progress. 114 if (state === 'idle') { 115 metadataListener?.({ task_summary: null }) 116 } 117 118 // Mirror to the SDK event stream so non-CCR consumers (scmuxd, VS Code) 119 // see the same authoritative idle/running signal the CCR bridge does. 120 // 'idle' fires after heldBackResult flushes — lets scmuxd flip IDLE and 121 // show the bg-task dot instead of a stuck generating spinner. 122 // 123 // Opt-in until CCR web + mobile clients learn to ignore this subtype in 124 // their isWorking() last-message heuristics — the trailing idle event 125 // currently pins them at "Running...". 126 // https://anthropic.slack.com/archives/C093BJBD1CP/p1774152406752229 127 if (isEnvTruthy(process.env.CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS)) { 128 enqueueSdkEvent({ 129 type: 'system', 130 subtype: 'session_state_changed', 131 state, 132 }) 133 } 134} 135 136export function notifySessionMetadataChanged( 137 metadata: SessionExternalMetadata, 138): void { 139 metadataListener?.(metadata) 140} 141 142/** 143 * Fired by onChangeAppState when toolPermissionContext.mode changes. 144 * Downstream listeners (CCR external_metadata PUT, SDK status stream) are 145 * both wired through this single choke point so no mode-mutation path can 146 * silently bypass them. 147 */ 148export function notifyPermissionModeChanged(mode: PermissionMode): void { 149 permissionModeListener?.(mode) 150}