sessionState.ts
utils/sessionState.ts
151
Lines
5309
Bytes
10
Exports
3
Imports
10
Keywords
What this is
This page documents one file from the repository and includes its full source so you can read it without leaving the docs site.
Beginner explanation
This file is one piece of the larger system. Its name, directory, imports, and exports show where it fits. Start by reading the exports and related files first.
How it is used
Start from the exports list and related files. Those are the easiest clues for where this file fits into the system.
Expert explanation
Architecturally, this file intersects with session-engine. It contains 151 lines, 3 detected imports, and 10 detected exports.
Important relationships
Detected exports
SessionStateRequiresActionDetailsSessionExternalMetadatasetSessionStateChangedListenersetSessionMetadataChangedListenersetPermissionModeChangedListenergetSessionStatenotifySessionStateChangednotifySessionMetadataChangednotifyPermissionModeChanged
Keywords
voididlesessionstatedetailsmodemetadatalistenerrequiresactiondetailsexternal_metadatapending_actionrunning
Detected imports
./envUtils.js./permissions/PermissionMode.js./sdkEventQueue.js
Source notes
This page embeds the full file contents. Small or leaf files are still indexed honestly instead of being over-explained.
Full source
export type SessionState = 'idle' | 'running' | 'requires_action'
/**
* Context carried with requires_action transitions so downstream
* surfaces (CCR sidebar, push notifications) can show what the
* session is blocked on, not just that it's blocked.
*
* Two delivery paths:
* - tool_name + action_description → RequiresActionDetails proto
* (webhook payload, typed, logged in Datadog)
* - full object → external_metadata.pending_action (queryable JSON
* on the Session, lets the frontend iterate on shape without
* proto round-trips)
*/
export type RequiresActionDetails = {
tool_name: string
/** Human-readable summary, e.g. "Editing src/foo.ts", "Running npm test" */
action_description: string
tool_use_id: string
request_id: string
/** Raw tool input — the frontend reads from external_metadata.pending_action.input
* to parse question options / plan content without scanning the event stream. */
input?: Record<string, unknown>
}
import { isEnvTruthy } from './envUtils.js'
import type { PermissionMode } from './permissions/PermissionMode.js'
import { enqueueSdkEvent } from './sdkEventQueue.js'
// CCR external_metadata keys — push in onChangeAppState, restore in
// externalMetadataToAppState.
export type SessionExternalMetadata = {
permission_mode?: string | null
is_ultraplan_mode?: boolean | null
model?: string | null
pending_action?: RequiresActionDetails | null
// Opaque — typed at the emit site. Importing PostTurnSummaryOutput here
// would leak the import path string into sdk.d.ts via agentSdkBridge's
// re-export of SessionState.
post_turn_summary?: unknown
// Mid-turn progress line from the forked-agent summarizer — fires every
// ~5 steps / 2min so long-running turns still surface "what's happening
// right now" before post_turn_summary arrives.
task_summary?: string | null
}
type SessionStateChangedListener = (
state: SessionState,
details?: RequiresActionDetails,
) => void
type SessionMetadataChangedListener = (
metadata: SessionExternalMetadata,
) => void
type PermissionModeChangedListener = (mode: PermissionMode) => void
let stateListener: SessionStateChangedListener | null = null
let metadataListener: SessionMetadataChangedListener | null = null
let permissionModeListener: PermissionModeChangedListener | null = null
export function setSessionStateChangedListener(
cb: SessionStateChangedListener | null,
): void {
stateListener = cb
}
export function setSessionMetadataChangedListener(
cb: SessionMetadataChangedListener | null,
): void {
metadataListener = cb
}
/**
* Register a listener for permission-mode changes from onChangeAppState.
* Wired by print.ts to emit an SDK system:status message so CCR/IDE clients
* see mode transitions in real time — regardless of which code path mutated
* toolPermissionContext.mode (Shift+Tab, ExitPlanMode dialog, slash command,
* bridge set_permission_mode, etc.).
*/
export function setPermissionModeChangedListener(
cb: PermissionModeChangedListener | null,
): void {
permissionModeListener = cb
}
let hasPendingAction = false
let currentState: SessionState = 'idle'
export function getSessionState(): SessionState {
return currentState
}
export function notifySessionStateChanged(
state: SessionState,
details?: RequiresActionDetails,
): void {
currentState = state
stateListener?.(state, details)
// Mirror details into external_metadata so GetSession carries the
// pending-action context without proto changes. Cleared via RFC 7396
// null on the next non-blocked transition.
if (state === 'requires_action' && details) {
hasPendingAction = true
metadataListener?.({
pending_action: details,
})
} else if (hasPendingAction) {
hasPendingAction = false
metadataListener?.({ pending_action: null })
}
// task_summary is written mid-turn by the forked summarizer; clear it at
// idle so the next turn doesn't briefly show the previous turn's progress.
if (state === 'idle') {
metadataListener?.({ task_summary: null })
}
// Mirror to the SDK event stream so non-CCR consumers (scmuxd, VS Code)
// see the same authoritative idle/running signal the CCR bridge does.
// 'idle' fires after heldBackResult flushes — lets scmuxd flip IDLE and
// show the bg-task dot instead of a stuck generating spinner.
//
// Opt-in until CCR web + mobile clients learn to ignore this subtype in
// their isWorking() last-message heuristics — the trailing idle event
// currently pins them at "Running...".
// https://anthropic.slack.com/archives/C093BJBD1CP/p1774152406752229
if (isEnvTruthy(process.env.CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS)) {
enqueueSdkEvent({
type: 'system',
subtype: 'session_state_changed',
state,
})
}
}
export function notifySessionMetadataChanged(
metadata: SessionExternalMetadata,
): void {
metadataListener?.(metadata)
}
/**
* Fired by onChangeAppState when toolPermissionContext.mode changes.
* Downstream listeners (CCR external_metadata PUT, SDK status stream) are
* both wired through this single choke point so no mode-mutation path can
* silently bypass them.
*/
export function notifyPermissionModeChanged(mode: PermissionMode): void {
permissionModeListener?.(mode)
}