MCPcopy
hub / github.com/codeaashu/claude-code / onChangeAppState

Function onChangeAppState

src/state/onChangeAppState.ts:43–171  ·  view source on GitHub ↗
({
  newState,
  oldState,
}: {
  newState: AppState
  oldState: AppState
})

Source from the content-addressed store, hash-verified

41}
42
43export function onChangeAppState({
44 newState,
45 oldState,
46}: {
47 newState: AppState
48 oldState: AppState
49}) {
50 // toolPermissionContext.mode — single choke point for CCR/SDK mode sync.
51 //
52 // Prior to this block, mode changes were relayed to CCR by only 2 of 8+
53 // mutation paths: a bespoke setAppState wrapper in print.ts (headless/SDK
54 // mode only) and a manual notify in the set_permission_mode handler.
55 // Every other path — Shift+Tab cycling, ExitPlanModePermissionRequest
56 // dialog options, the /plan slash command, rewind, the REPL bridge's
57 // onSetPermissionMode — mutated AppState without telling
58 // CCR, leaving external_metadata.permission_mode stale and the web UI out
59 // of sync with the CLI's actual mode.
60 //
61 // Hooking the diff here means ANY setAppState call that changes the mode
62 // notifies CCR (via notifySessionMetadataChanged → ccrClient.reportMetadata)
63 // and the SDK status stream (via notifyPermissionModeChanged → registered
64 // in print.ts). The scattered callsites above need zero changes.
65 const prevMode = oldState.toolPermissionContext.mode
66 const newMode = newState.toolPermissionContext.mode
67 if (prevMode !== newMode) {
68 // CCR external_metadata must not receive internal-only mode names
69 // (bubble, ungated auto). Externalize first — and skip
70 // the CCR notify if the EXTERNAL mode didn't change (e.g.,
71 // default→bubble→default is noise from CCR's POV since both
72 // externalize to 'default'). The SDK channel (notifyPermissionModeChanged)
73 // passes raw mode; its listener in print.ts applies its own filter.
74 const prevExternal = toExternalPermissionMode(prevMode)
75 const newExternal = toExternalPermissionMode(newMode)
76 if (prevExternal !== newExternal) {
77 // Ultraplan = first plan cycle only. The initial control_request
78 // sets mode and isUltraplanMode atomically, so the flag's
79 // transition gates it. null per RFC 7396 (removes the key).
80 const isUltraplan =
81 newExternal === 'plan' &&
82 newState.isUltraplanMode &&
83 !oldState.isUltraplanMode
84 ? true
85 : null
86 notifySessionMetadataChanged({
87 permission_mode: newExternal,
88 is_ultraplan_mode: isUltraplan,
89 })
90 }
91 notifyPermissionModeChanged(newMode)
92 }
93
94 // mainLoopModel: remove it from settings?
95 if (
96 newState.mainLoopModel !== oldState.mainLoopModel &&
97 newState.mainLoopModel === null
98 ) {
99 // Remove from settings
100 updateSettingsForSource('userSettings', { model: undefined })

Callers

nothing calls this directly

Calls 13

toExternalPermissionModeFunction · 0.85
updateSettingsForSourceFunction · 0.85
setMainLoopModelOverrideFunction · 0.85
getGlobalConfigFunction · 0.85
saveGlobalConfigFunction · 0.85
clearApiKeyHelperCacheFunction · 0.85
clearAwsCredentialsCacheFunction · 0.85
clearGcpCredentialsCacheFunction · 0.85
toErrorFunction · 0.85

Tested by

no test coverage detected