source dump of claude code
at main 171 lines 6.2 kB view raw
1import { setMainLoopModelOverride } from '../bootstrap/state.js' 2import { 3 clearApiKeyHelperCache, 4 clearAwsCredentialsCache, 5 clearGcpCredentialsCache, 6} from '../utils/auth.js' 7import { getGlobalConfig, saveGlobalConfig } from '../utils/config.js' 8import { toError } from '../utils/errors.js' 9import { logError } from '../utils/log.js' 10import { applyConfigEnvironmentVariables } from '../utils/managedEnv.js' 11import { 12 permissionModeFromString, 13 toExternalPermissionMode, 14} from '../utils/permissions/PermissionMode.js' 15import { 16 notifyPermissionModeChanged, 17 notifySessionMetadataChanged, 18 type SessionExternalMetadata, 19} from '../utils/sessionState.js' 20import { updateSettingsForSource } from '../utils/settings/settings.js' 21import type { AppState } from './AppStateStore.js' 22 23// Inverse of the push below — restore on worker restart. 24export function externalMetadataToAppState( 25 metadata: SessionExternalMetadata, 26): (prev: AppState) => AppState { 27 return prev => ({ 28 ...prev, 29 ...(typeof metadata.permission_mode === 'string' 30 ? { 31 toolPermissionContext: { 32 ...prev.toolPermissionContext, 33 mode: permissionModeFromString(metadata.permission_mode), 34 }, 35 } 36 : {}), 37 ...(typeof metadata.is_ultraplan_mode === 'boolean' 38 ? { isUltraplanMode: metadata.is_ultraplan_mode } 39 : {}), 40 }) 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 }) 101 setMainLoopModelOverride(null) 102 } 103 104 // mainLoopModel: add it to settings? 105 if ( 106 newState.mainLoopModel !== oldState.mainLoopModel && 107 newState.mainLoopModel !== null 108 ) { 109 // Save to settings 110 updateSettingsForSource('userSettings', { model: newState.mainLoopModel }) 111 setMainLoopModelOverride(newState.mainLoopModel) 112 } 113 114 // expandedView → persist as showExpandedTodos + showSpinnerTree for backwards compat 115 if (newState.expandedView !== oldState.expandedView) { 116 const showExpandedTodos = newState.expandedView === 'tasks' 117 const showSpinnerTree = newState.expandedView === 'teammates' 118 if ( 119 getGlobalConfig().showExpandedTodos !== showExpandedTodos || 120 getGlobalConfig().showSpinnerTree !== showSpinnerTree 121 ) { 122 saveGlobalConfig(current => ({ 123 ...current, 124 showExpandedTodos, 125 showSpinnerTree, 126 })) 127 } 128 } 129 130 // verbose 131 if ( 132 newState.verbose !== oldState.verbose && 133 getGlobalConfig().verbose !== newState.verbose 134 ) { 135 const verbose = newState.verbose 136 saveGlobalConfig(current => ({ 137 ...current, 138 verbose, 139 })) 140 } 141 142 // tungstenPanelVisible (ant-only tmux panel sticky toggle) 143 if (process.env.USER_TYPE === 'ant') { 144 if ( 145 newState.tungstenPanelVisible !== oldState.tungstenPanelVisible && 146 newState.tungstenPanelVisible !== undefined && 147 getGlobalConfig().tungstenPanelVisible !== newState.tungstenPanelVisible 148 ) { 149 const tungstenPanelVisible = newState.tungstenPanelVisible 150 saveGlobalConfig(current => ({ ...current, tungstenPanelVisible })) 151 } 152 } 153 154 // settings: clear auth-related caches when settings change 155 // This ensures apiKeyHelper and AWS/GCP credential changes take effect immediately 156 if (newState.settings !== oldState.settings) { 157 try { 158 clearApiKeyHelperCache() 159 clearAwsCredentialsCache() 160 clearGcpCredentialsCache() 161 162 // Re-apply environment variables when settings.env changes 163 // This is additive-only: new vars are added, existing may be overwritten, nothing is deleted 164 if (newState.settings.env !== oldState.settings.env) { 165 applyConfigEnvironmentVariables() 166 } 167 } catch (error) { 168 logError(toError(error)) 169 } 170 } 171}