chore: initialize recovered claude workspace
This commit is contained in:
171
src/state/onChangeAppState.ts
Normal file
171
src/state/onChangeAppState.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { setMainLoopModelOverride } from '../bootstrap/state.js'
|
||||
import {
|
||||
clearApiKeyHelperCache,
|
||||
clearAwsCredentialsCache,
|
||||
clearGcpCredentialsCache,
|
||||
} from '../utils/auth.js'
|
||||
import { getGlobalConfig, saveGlobalConfig } from '../utils/config.js'
|
||||
import { toError } from '../utils/errors.js'
|
||||
import { logError } from '../utils/log.js'
|
||||
import { applyConfigEnvironmentVariables } from '../utils/managedEnv.js'
|
||||
import {
|
||||
permissionModeFromString,
|
||||
toExternalPermissionMode,
|
||||
} from '../utils/permissions/PermissionMode.js'
|
||||
import {
|
||||
notifyPermissionModeChanged,
|
||||
notifySessionMetadataChanged,
|
||||
type SessionExternalMetadata,
|
||||
} from '../utils/sessionState.js'
|
||||
import { updateSettingsForSource } from '../utils/settings/settings.js'
|
||||
import type { AppState } from './AppStateStore.js'
|
||||
|
||||
// Inverse of the push below — restore on worker restart.
|
||||
export function externalMetadataToAppState(
|
||||
metadata: SessionExternalMetadata,
|
||||
): (prev: AppState) => AppState {
|
||||
return prev => ({
|
||||
...prev,
|
||||
...(typeof metadata.permission_mode === 'string'
|
||||
? {
|
||||
toolPermissionContext: {
|
||||
...prev.toolPermissionContext,
|
||||
mode: permissionModeFromString(metadata.permission_mode),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(typeof metadata.is_ultraplan_mode === 'boolean'
|
||||
? { isUltraplanMode: metadata.is_ultraplan_mode }
|
||||
: {}),
|
||||
})
|
||||
}
|
||||
|
||||
export function onChangeAppState({
|
||||
newState,
|
||||
oldState,
|
||||
}: {
|
||||
newState: AppState
|
||||
oldState: AppState
|
||||
}) {
|
||||
// toolPermissionContext.mode — single choke point for CCR/SDK mode sync.
|
||||
//
|
||||
// Prior to this block, mode changes were relayed to CCR by only 2 of 8+
|
||||
// mutation paths: a bespoke setAppState wrapper in print.ts (headless/SDK
|
||||
// mode only) and a manual notify in the set_permission_mode handler.
|
||||
// Every other path — Shift+Tab cycling, ExitPlanModePermissionRequest
|
||||
// dialog options, the /plan slash command, rewind, the REPL bridge's
|
||||
// onSetPermissionMode — mutated AppState without telling
|
||||
// CCR, leaving external_metadata.permission_mode stale and the web UI out
|
||||
// of sync with the CLI's actual mode.
|
||||
//
|
||||
// Hooking the diff here means ANY setAppState call that changes the mode
|
||||
// notifies CCR (via notifySessionMetadataChanged → ccrClient.reportMetadata)
|
||||
// and the SDK status stream (via notifyPermissionModeChanged → registered
|
||||
// in print.ts). The scattered callsites above need zero changes.
|
||||
const prevMode = oldState.toolPermissionContext.mode
|
||||
const newMode = newState.toolPermissionContext.mode
|
||||
if (prevMode !== newMode) {
|
||||
// CCR external_metadata must not receive internal-only mode names
|
||||
// (bubble, ungated auto). Externalize first — and skip
|
||||
// the CCR notify if the EXTERNAL mode didn't change (e.g.,
|
||||
// default→bubble→default is noise from CCR's POV since both
|
||||
// externalize to 'default'). The SDK channel (notifyPermissionModeChanged)
|
||||
// passes raw mode; its listener in print.ts applies its own filter.
|
||||
const prevExternal = toExternalPermissionMode(prevMode)
|
||||
const newExternal = toExternalPermissionMode(newMode)
|
||||
if (prevExternal !== newExternal) {
|
||||
// Ultraplan = first plan cycle only. The initial control_request
|
||||
// sets mode and isUltraplanMode atomically, so the flag's
|
||||
// transition gates it. null per RFC 7396 (removes the key).
|
||||
const isUltraplan =
|
||||
newExternal === 'plan' &&
|
||||
newState.isUltraplanMode &&
|
||||
!oldState.isUltraplanMode
|
||||
? true
|
||||
: null
|
||||
notifySessionMetadataChanged({
|
||||
permission_mode: newExternal,
|
||||
is_ultraplan_mode: isUltraplan,
|
||||
})
|
||||
}
|
||||
notifyPermissionModeChanged(newMode)
|
||||
}
|
||||
|
||||
// mainLoopModel: remove it from settings?
|
||||
if (
|
||||
newState.mainLoopModel !== oldState.mainLoopModel &&
|
||||
newState.mainLoopModel === null
|
||||
) {
|
||||
// Remove from settings
|
||||
updateSettingsForSource('userSettings', { model: undefined })
|
||||
setMainLoopModelOverride(null)
|
||||
}
|
||||
|
||||
// mainLoopModel: add it to settings?
|
||||
if (
|
||||
newState.mainLoopModel !== oldState.mainLoopModel &&
|
||||
newState.mainLoopModel !== null
|
||||
) {
|
||||
// Save to settings
|
||||
updateSettingsForSource('userSettings', { model: newState.mainLoopModel })
|
||||
setMainLoopModelOverride(newState.mainLoopModel)
|
||||
}
|
||||
|
||||
// expandedView → persist as showExpandedTodos + showSpinnerTree for backwards compat
|
||||
if (newState.expandedView !== oldState.expandedView) {
|
||||
const showExpandedTodos = newState.expandedView === 'tasks'
|
||||
const showSpinnerTree = newState.expandedView === 'teammates'
|
||||
if (
|
||||
getGlobalConfig().showExpandedTodos !== showExpandedTodos ||
|
||||
getGlobalConfig().showSpinnerTree !== showSpinnerTree
|
||||
) {
|
||||
saveGlobalConfig(current => ({
|
||||
...current,
|
||||
showExpandedTodos,
|
||||
showSpinnerTree,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
// verbose
|
||||
if (
|
||||
newState.verbose !== oldState.verbose &&
|
||||
getGlobalConfig().verbose !== newState.verbose
|
||||
) {
|
||||
const verbose = newState.verbose
|
||||
saveGlobalConfig(current => ({
|
||||
...current,
|
||||
verbose,
|
||||
}))
|
||||
}
|
||||
|
||||
// tungstenPanelVisible (ant-only tmux panel sticky toggle)
|
||||
if (process.env.USER_TYPE === 'ant') {
|
||||
if (
|
||||
newState.tungstenPanelVisible !== oldState.tungstenPanelVisible &&
|
||||
newState.tungstenPanelVisible !== undefined &&
|
||||
getGlobalConfig().tungstenPanelVisible !== newState.tungstenPanelVisible
|
||||
) {
|
||||
const tungstenPanelVisible = newState.tungstenPanelVisible
|
||||
saveGlobalConfig(current => ({ ...current, tungstenPanelVisible }))
|
||||
}
|
||||
}
|
||||
|
||||
// settings: clear auth-related caches when settings change
|
||||
// This ensures apiKeyHelper and AWS/GCP credential changes take effect immediately
|
||||
if (newState.settings !== oldState.settings) {
|
||||
try {
|
||||
clearApiKeyHelperCache()
|
||||
clearAwsCredentialsCache()
|
||||
clearGcpCredentialsCache()
|
||||
|
||||
// Re-apply environment variables when settings.env changes
|
||||
// This is additive-only: new vars are added, existing may be overwritten, nothing is deleted
|
||||
if (newState.settings.env !== oldState.settings.env) {
|
||||
applyConfigEnvironmentVariables()
|
||||
}
|
||||
} catch (error) {
|
||||
logError(toError(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user