@golba98/codexa 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +320 -0
- package/bin/codexa.js +445 -0
- package/package.json +45 -0
- package/scripts/audit-codexa-capabilities.mjs +466 -0
- package/scripts/smoke-terminal-bench.mjs +35 -0
- package/src/app.tsx +4561 -0
- package/src/appRenderStability.test.ts +131 -0
- package/src/commands/handler.test.ts +643 -0
- package/src/commands/handler.ts +875 -0
- package/src/config/launchArgs.test.ts +158 -0
- package/src/config/launchArgs.ts +186 -0
- package/src/config/layeredConfig.test.ts +143 -0
- package/src/config/layeredConfig.ts +836 -0
- package/src/config/persistence.test.ts +110 -0
- package/src/config/persistence.ts +311 -0
- package/src/config/runtimeConfig.test.ts +218 -0
- package/src/config/runtimeConfig.ts +554 -0
- package/src/config/settings.test.ts +155 -0
- package/src/config/settings.ts +401 -0
- package/src/config/toml-serialize.ts +98 -0
- package/src/config/trustStore.test.ts +29 -0
- package/src/config/trustStore.ts +68 -0
- package/src/core/attachments.test.ts +155 -0
- package/src/core/attachments.ts +71 -0
- package/src/core/auth/codexAuth.test.ts +68 -0
- package/src/core/auth/codexAuth.ts +359 -0
- package/src/core/cleanupFastFail.test.ts +76 -0
- package/src/core/cleanupFastFail.ts +67 -0
- package/src/core/clipboard.ts +24 -0
- package/src/core/codex.ts +124 -0
- package/src/core/codexExecArgs.test.ts +195 -0
- package/src/core/codexExecArgs.ts +152 -0
- package/src/core/codexLaunch.test.ts +205 -0
- package/src/core/codexLaunch.ts +162 -0
- package/src/core/codexPrompt.test.ts +252 -0
- package/src/core/codexPrompt.ts +428 -0
- package/src/core/executables/claudeExecutable.ts +63 -0
- package/src/core/executables/codexExecutable.test.ts +212 -0
- package/src/core/executables/codexExecutable.ts +159 -0
- package/src/core/executables/executableResolver.test.ts +129 -0
- package/src/core/executables/executableResolver.ts +138 -0
- package/src/core/executables/geminiExecutable.test.ts +116 -0
- package/src/core/executables/geminiExecutable.ts +78 -0
- package/src/core/executables/pathSanityScan.test.ts +47 -0
- package/src/core/githubDiagnostics.test.ts +92 -0
- package/src/core/githubDiagnostics.ts +222 -0
- package/src/core/hollowResponseFormat.test.ts +58 -0
- package/src/core/hollowResponseFormat.ts +39 -0
- package/src/core/inputDebug.ts +51 -0
- package/src/core/launchContext.test.ts +157 -0
- package/src/core/launchContext.ts +266 -0
- package/src/core/models/codexCapabilities.test.ts +45 -0
- package/src/core/models/codexCapabilities.ts +95 -0
- package/src/core/models/codexModelCapabilities.test.ts +246 -0
- package/src/core/models/codexModelCapabilities.ts +571 -0
- package/src/core/models/modelSpecs.test.ts +283 -0
- package/src/core/models/modelSpecs.ts +300 -0
- package/src/core/perf/profiler.ts +125 -0
- package/src/core/perf/renderDebug.test.ts +230 -0
- package/src/core/perf/renderDebug.ts +373 -0
- package/src/core/planStorage.test.ts +143 -0
- package/src/core/planStorage.ts +141 -0
- package/src/core/process/CommandRunner.test.ts +105 -0
- package/src/core/process/CommandRunner.ts +269 -0
- package/src/core/process/processValidation.ts +101 -0
- package/src/core/projectInstructions.test.ts +50 -0
- package/src/core/projectInstructions.ts +54 -0
- package/src/core/providerLauncher/launcher.test.ts +238 -0
- package/src/core/providerLauncher/launcher.ts +203 -0
- package/src/core/providerLauncher/registry.test.ts +324 -0
- package/src/core/providerLauncher/registry.ts +253 -0
- package/src/core/providerLauncher/types.ts +84 -0
- package/src/core/providerLauncher/workspaceConfig.test.ts +638 -0
- package/src/core/providerLauncher/workspaceConfig.ts +407 -0
- package/src/core/providerRuntime/anthropic.test.ts +1120 -0
- package/src/core/providerRuntime/anthropic.ts +576 -0
- package/src/core/providerRuntime/capabilityProfile.test.ts +311 -0
- package/src/core/providerRuntime/capabilityProfile.ts +288 -0
- package/src/core/providerRuntime/claudeCodeDiscovery.ts +446 -0
- package/src/core/providerRuntime/contextMetadata.test.ts +468 -0
- package/src/core/providerRuntime/contextMetadata.ts +409 -0
- package/src/core/providerRuntime/gemini.test.ts +437 -0
- package/src/core/providerRuntime/gemini.ts +784 -0
- package/src/core/providerRuntime/lmstudio.test.ts +168 -0
- package/src/core/providerRuntime/lmstudio.ts +118 -0
- package/src/core/providerRuntime/local.test.ts +787 -0
- package/src/core/providerRuntime/local.ts +754 -0
- package/src/core/providerRuntime/models.ts +150 -0
- package/src/core/providerRuntime/reasoning.ts +17 -0
- package/src/core/providerRuntime/registry.test.ts +233 -0
- package/src/core/providerRuntime/registry.ts +203 -0
- package/src/core/providerRuntime/types.ts +103 -0
- package/src/core/providers/codexJsonStream.test.ts +148 -0
- package/src/core/providers/codexJsonStream.ts +305 -0
- package/src/core/providers/codexSubprocess.test.ts +68 -0
- package/src/core/providers/codexSubprocess.ts +372 -0
- package/src/core/providers/codexTranscript.test.ts +284 -0
- package/src/core/providers/codexTranscript.ts +695 -0
- package/src/core/providers/openaiNative.ts +13 -0
- package/src/core/providers/registry.ts +21 -0
- package/src/core/providers/types.ts +59 -0
- package/src/core/terminal/terminalCapabilities.test.ts +93 -0
- package/src/core/terminal/terminalCapabilities.ts +100 -0
- package/src/core/terminal/terminalControl.test.ts +75 -0
- package/src/core/terminal/terminalControl.ts +147 -0
- package/src/core/terminal/terminalSanitize.test.ts +22 -0
- package/src/core/terminal/terminalSanitize.ts +147 -0
- package/src/core/terminal/terminalSelection.test.ts +42 -0
- package/src/core/terminal/terminalSelection.ts +66 -0
- package/src/core/terminal/terminalTitle.test.ts +328 -0
- package/src/core/terminal/terminalTitle.ts +483 -0
- package/src/core/workspaceActivity.test.ts +163 -0
- package/src/core/workspaceActivity.ts +380 -0
- package/src/core/workspaceGuard.test.ts +151 -0
- package/src/core/workspaceGuard.ts +288 -0
- package/src/core/workspaceRoot.test.ts +23 -0
- package/src/core/workspaceRoot.ts +47 -0
- package/src/exec.test.ts +13 -0
- package/src/exec.ts +72 -0
- package/src/headless/execArgs.test.ts +147 -0
- package/src/headless/execArgs.ts +294 -0
- package/src/headless/execRunner.test.ts +434 -0
- package/src/headless/execRunner.ts +304 -0
- package/src/index.test.tsx +618 -0
- package/src/index.tsx +296 -0
- package/src/session/appSession.test.ts +897 -0
- package/src/session/appSession.ts +761 -0
- package/src/session/chatLifecycle.test.ts +64 -0
- package/src/session/chatLifecycle.ts +951 -0
- package/src/session/liveRenderScheduler.test.ts +201 -0
- package/src/session/liveRenderScheduler.ts +214 -0
- package/src/session/planFlow.test.ts +103 -0
- package/src/session/planFlow.ts +149 -0
- package/src/session/planTranscript.test.ts +65 -0
- package/src/session/planTranscript.ts +15 -0
- package/src/session/promptRunSchedule.test.ts +36 -0
- package/src/session/promptRunSchedule.ts +26 -0
- package/src/session/types.ts +228 -0
- package/src/test/runtimeTestUtils.ts +14 -0
- package/src/types/react-dom.d.ts +3 -0
- package/src/ui/ActionRequiredBlock.tsx +38 -0
- package/src/ui/ActivityBars.tsx +68 -0
- package/src/ui/ActivityIndicator.test.tsx +58 -0
- package/src/ui/ActivityIndicator.tsx +58 -0
- package/src/ui/AgentBlock.test.ts +6 -0
- package/src/ui/AgentBlock.tsx +130 -0
- package/src/ui/AnimatedStatusText.test.ts +16 -0
- package/src/ui/AnimatedStatusText.tsx +69 -0
- package/src/ui/AppShell.test.tsx +1739 -0
- package/src/ui/AppShell.tsx +698 -0
- package/src/ui/AttachmentImportPanel.test.tsx +204 -0
- package/src/ui/AttachmentImportPanel.tsx +98 -0
- package/src/ui/AuthPanel.tsx +113 -0
- package/src/ui/BackendPicker.tsx +28 -0
- package/src/ui/BottomComposer.test.ts +674 -0
- package/src/ui/BottomComposer.tsx +1028 -0
- package/src/ui/CodexLogo.tsx +55 -0
- package/src/ui/DashCard.tsx +82 -0
- package/src/ui/Markdown.test.ts +157 -0
- package/src/ui/Markdown.tsx +310 -0
- package/src/ui/ModePicker.tsx +27 -0
- package/src/ui/ModelPicker.tsx +31 -0
- package/src/ui/ModelPickerProviderScope.test.tsx +411 -0
- package/src/ui/ModelPickerScreen.test.tsx +99 -0
- package/src/ui/ModelPickerScreen.tsx +416 -0
- package/src/ui/ModelPickerState.test.tsx +151 -0
- package/src/ui/ModelReasoningPicker.test.tsx +447 -0
- package/src/ui/ModelReasoningPicker.tsx +458 -0
- package/src/ui/Panel.tsx +51 -0
- package/src/ui/PermissionsPanel.tsx +78 -0
- package/src/ui/PlanActionPicker.tsx +119 -0
- package/src/ui/PlanReviewPanel.test.tsx +267 -0
- package/src/ui/PlanReviewPanel.tsx +212 -0
- package/src/ui/PromptCardBorder.test.tsx +161 -0
- package/src/ui/ProviderPicker.test.tsx +289 -0
- package/src/ui/ProviderPicker.tsx +321 -0
- package/src/ui/ProviderShortcut.test.tsx +143 -0
- package/src/ui/ReasoningPicker.tsx +46 -0
- package/src/ui/RunFooter.tsx +65 -0
- package/src/ui/SelectionPanel.tsx +67 -0
- package/src/ui/SettingsPanel.test.tsx +233 -0
- package/src/ui/SettingsPanel.tsx +156 -0
- package/src/ui/Spinner.tsx +25 -0
- package/src/ui/StaticIntroItem.tsx +54 -0
- package/src/ui/StaticTranscriptItem.tsx +56 -0
- package/src/ui/TextEntryPanel.tsx +139 -0
- package/src/ui/ThemePicker.tsx +31 -0
- package/src/ui/ThinkingBlock.tsx +100 -0
- package/src/ui/Timeline.test.ts +2067 -0
- package/src/ui/Timeline.tsx +1472 -0
- package/src/ui/TimelineNavigation.test.tsx +201 -0
- package/src/ui/TopHeader.test.tsx +239 -0
- package/src/ui/TopHeader.tsx +257 -0
- package/src/ui/TurnGroup.test.tsx +365 -0
- package/src/ui/TurnGroup.tsx +657 -0
- package/src/ui/busyStatusAnimation.test.ts +30 -0
- package/src/ui/busyStatusAnimation.ts +11 -0
- package/src/ui/commandNormalize.test.ts +142 -0
- package/src/ui/commandNormalize.ts +66 -0
- package/src/ui/diffRenderer.test.ts +102 -0
- package/src/ui/diffRenderer.ts +116 -0
- package/src/ui/focus.ts +61 -0
- package/src/ui/focusFlow.test.tsx +1098 -0
- package/src/ui/inputBuffer.test.ts +151 -0
- package/src/ui/inputBuffer.ts +203 -0
- package/src/ui/layout.test.ts +145 -0
- package/src/ui/layout.ts +287 -0
- package/src/ui/modeDisplay.test.ts +42 -0
- package/src/ui/modeDisplay.ts +52 -0
- package/src/ui/outputPipeline.ts +64 -0
- package/src/ui/progressEntries.ts +156 -0
- package/src/ui/runActivityView.test.ts +89 -0
- package/src/ui/runActivityView.ts +37 -0
- package/src/ui/runLifecycleView.test.tsx +237 -0
- package/src/ui/slashCommands.ts +41 -0
- package/src/ui/statusRenderIsolation.test.tsx +654 -0
- package/src/ui/terminalAnswerFormat.test.ts +19 -0
- package/src/ui/terminalAnswerFormat.ts +128 -0
- package/src/ui/textLayout.test.ts +18 -0
- package/src/ui/textLayout.ts +338 -0
- package/src/ui/theme.tsx +395 -0
- package/src/ui/themeFlow.test.ts +53 -0
- package/src/ui/themeFlow.ts +41 -0
- package/src/ui/timelineMeasure.ts +3088 -0
- package/src/ui/timelineMeasureCache.test.ts +986 -0
- package/src/ui/useThrottledValue.ts +31 -0
package/src/app.tsx
ADDED
|
@@ -0,0 +1,4561 @@
|
|
|
1
|
+
import React, { startTransition, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
|
+
import { existsSync } from "fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
// Diagnostic tracing hook — no-op by default; wire to a real logger when debugging.
|
|
7
|
+
function appDiagLog(msg: string): void {
|
|
8
|
+
void msg;
|
|
9
|
+
}
|
|
10
|
+
import { Box, Text, useApp, useFocusManager, useInput, useStdin, useStdout } from "ink";
|
|
11
|
+
import { handleCommand } from "./commands/handler.js";
|
|
12
|
+
import {
|
|
13
|
+
applyLayeredRuntimeOverride,
|
|
14
|
+
resolveLayeredConfig,
|
|
15
|
+
type LayeredConfigResult,
|
|
16
|
+
} from "./config/layeredConfig.js";
|
|
17
|
+
import type { LaunchArgs } from "./config/launchArgs.js";
|
|
18
|
+
import { loadSettings, saveSettings } from "./config/persistence.js";
|
|
19
|
+
import {
|
|
20
|
+
type AuthPreference,
|
|
21
|
+
type AvailableBackend,
|
|
22
|
+
type AvailableMode,
|
|
23
|
+
type AvailableModel,
|
|
24
|
+
parseBusyLoaderSettingValue,
|
|
25
|
+
type ReasoningLevel,
|
|
26
|
+
type TerminalMouseMode,
|
|
27
|
+
USER_SETTING_DEFINITIONS,
|
|
28
|
+
type WorkspaceDisplayMode,
|
|
29
|
+
type UserSettingValues,
|
|
30
|
+
estimateTokens,
|
|
31
|
+
formatBusyLoaderSettingValue,
|
|
32
|
+
formatAuthPreferenceLabel,
|
|
33
|
+
formatBackendLabel,
|
|
34
|
+
formatWorkspaceDisplayModeLabel,
|
|
35
|
+
formatModeLabel,
|
|
36
|
+
formatReasoningLabel,
|
|
37
|
+
formatThemeLabel,
|
|
38
|
+
formatWorkspaceDisplayPath,
|
|
39
|
+
getNextMode,
|
|
40
|
+
type HeaderConfig,
|
|
41
|
+
type TerminalTitleMode,
|
|
42
|
+
} from "./config/settings.js";
|
|
43
|
+
import {
|
|
44
|
+
AVAILABLE_APPROVAL_POLICIES,
|
|
45
|
+
AVAILABLE_NETWORK_ACCESS_VALUES,
|
|
46
|
+
AVAILABLE_SANDBOX_MODES,
|
|
47
|
+
addWritableRoot,
|
|
48
|
+
buildRuntimeSummary,
|
|
49
|
+
clearWritableRoots,
|
|
50
|
+
diffRuntimeConfig,
|
|
51
|
+
formatApprovalPolicyLabel,
|
|
52
|
+
formatNetworkAccessLabel,
|
|
53
|
+
formatPersonalityLabel,
|
|
54
|
+
formatSandboxModeLabel,
|
|
55
|
+
formatServiceTierLabel,
|
|
56
|
+
mergeRuntimeConfig,
|
|
57
|
+
removeWritableRoot,
|
|
58
|
+
resolveRuntimeConfig,
|
|
59
|
+
resolveWritableRootCommandPath,
|
|
60
|
+
type PartialRuntimeConfig,
|
|
61
|
+
type RuntimeApprovalPolicy,
|
|
62
|
+
type RuntimeConfig,
|
|
63
|
+
type RuntimeNetworkAccess,
|
|
64
|
+
type RuntimePersonality,
|
|
65
|
+
type RuntimeSandboxMode,
|
|
66
|
+
type RuntimeServiceTier,
|
|
67
|
+
} from "./config/runtimeConfig.js";
|
|
68
|
+
import { setProjectTrust } from "./config/trustStore.js";
|
|
69
|
+
import {
|
|
70
|
+
type CodexAuthProbeResult,
|
|
71
|
+
getAuthStatusMessage,
|
|
72
|
+
getLoginGuidance,
|
|
73
|
+
getLogoutGuidance,
|
|
74
|
+
getRunGateDecision,
|
|
75
|
+
isLikelyAuthFailure,
|
|
76
|
+
probeCodexAuthStatus,
|
|
77
|
+
} from "./core/auth/codexAuth.js";
|
|
78
|
+
import { getTerminalSelectionProfile } from "./core/terminal/terminalSelection.js";
|
|
79
|
+
import { copyToClipboard } from "./core/clipboard.js";
|
|
80
|
+
import { normalizePlanReviewMarkdown, savePlan, readPlan } from "./core/planStorage.js";
|
|
81
|
+
import { getBlockedCleanupFailure } from "./core/cleanupFastFail.js";
|
|
82
|
+
import { runShellCommand, summarizeCommandResult } from "./core/process/CommandRunner.js";
|
|
83
|
+
import {
|
|
84
|
+
buildPlanExecutionPrompt,
|
|
85
|
+
buildPlanningPrompt,
|
|
86
|
+
detectHollowResponse,
|
|
87
|
+
isClearlySafeGeneratedCleanupRequest,
|
|
88
|
+
resolveExecutionMode,
|
|
89
|
+
} from "./core/codexPrompt.js";
|
|
90
|
+
import { formatHollowResponse } from "./core/hollowResponseFormat.js";
|
|
91
|
+
import {
|
|
92
|
+
createFallbackModelCapabilities,
|
|
93
|
+
findModelCapability,
|
|
94
|
+
formatModelCapabilitiesList,
|
|
95
|
+
getCodexModelCapabilities,
|
|
96
|
+
getPreferredModelFromCapabilities,
|
|
97
|
+
getSelectableModelCapabilities,
|
|
98
|
+
normalizeReasoningForModelCapabilities,
|
|
99
|
+
type CodexModelCapabilities,
|
|
100
|
+
} from "./core/models/codexModelCapabilities.js";
|
|
101
|
+
import {
|
|
102
|
+
buildDevLaunchNotice,
|
|
103
|
+
buildWorkspaceCommandContext,
|
|
104
|
+
createWorkspaceRelaunchPlan,
|
|
105
|
+
guardWorkspaceRelaunch,
|
|
106
|
+
resolveLaunchContext,
|
|
107
|
+
} from "./core/launchContext.js";
|
|
108
|
+
import {
|
|
109
|
+
findOutsideWorkspacePaths,
|
|
110
|
+
getPromptWorkspaceGuardMessage,
|
|
111
|
+
getShellWorkspaceGuardMessage,
|
|
112
|
+
} from "./core/workspaceGuard.js";
|
|
113
|
+
import {
|
|
114
|
+
type ModelSpec,
|
|
115
|
+
} from "./core/models/modelSpecs.js";
|
|
116
|
+
import {
|
|
117
|
+
contextMetadataToModelSpec,
|
|
118
|
+
formatContextCompact,
|
|
119
|
+
formatContextLength,
|
|
120
|
+
resolveModelContextLength,
|
|
121
|
+
type ModelContextMetadata,
|
|
122
|
+
} from "./core/providerRuntime/contextMetadata.js";
|
|
123
|
+
import { captureWorkspaceSnapshot, createWorkspaceActivityTracker, diffWorkspaceSnapshots } from "./core/workspaceActivity.js";
|
|
124
|
+
import { resolveWorkspaceRoot } from "./core/workspaceRoot.js";
|
|
125
|
+
import {
|
|
126
|
+
importExternalFile,
|
|
127
|
+
isImageFile,
|
|
128
|
+
rewritePromptWithImportedPaths,
|
|
129
|
+
} from "./core/attachments.js";
|
|
130
|
+
import { loadProjectInstructions } from "./core/projectInstructions.js";
|
|
131
|
+
import { isNoiseLine } from "./core/providers/codexTranscript.js";
|
|
132
|
+
import { getBackendProvider } from "./core/providers/registry.js";
|
|
133
|
+
import type { BackendProgressUpdate, BackendProvider } from "./core/providers/types.js";
|
|
134
|
+
import { launchProviderCli } from "./core/providerLauncher/launcher.js";
|
|
135
|
+
import { buildProviderRegistry, findProvider, getActiveRouteProviderId } from "./core/providerLauncher/registry.js";
|
|
136
|
+
import type { ProviderId, ProviderPickerAction, ProviderWorkspaceConfig } from "./core/providerLauncher/types.js";
|
|
137
|
+
import {
|
|
138
|
+
discoverProviderModels,
|
|
139
|
+
getProviderRouteSetupMessage,
|
|
140
|
+
getProviderRuntime,
|
|
141
|
+
isProviderRouteConfigured,
|
|
142
|
+
isProviderRoutableInCodexa,
|
|
143
|
+
resolveActiveProviderRoute,
|
|
144
|
+
validateProviderRouteActivation,
|
|
145
|
+
} from "./core/providerRuntime/registry.js";
|
|
146
|
+
import { hasGeminiApiKey, runGeminiDiagnostics } from "./core/providerRuntime/gemini.js";
|
|
147
|
+
import { checkLocalProvider, runLocalDiagnostics, setLocalProviderConfig } from "./core/providerRuntime/local.js";
|
|
148
|
+
import { validateAnthropicRoute, ANTHROPIC_ROUTE_SETUP_MESSAGE } from "./core/providerRuntime/anthropic.js";
|
|
149
|
+
import { providerModelsToCodexCapabilities } from "./core/providerRuntime/models.js";
|
|
150
|
+
import {
|
|
151
|
+
loadProviderWorkspaceConfig,
|
|
152
|
+
saveProviderWorkspaceConfig,
|
|
153
|
+
setProviderActiveRoute,
|
|
154
|
+
setProviderDefaultReasoning,
|
|
155
|
+
setProviderDefaultModel,
|
|
156
|
+
setProviderWorkspaceDefault,
|
|
157
|
+
} from "./core/providerLauncher/workspaceConfig.js";
|
|
158
|
+
import { sanitizeTerminalInput, sanitizeTerminalLines, sanitizeTerminalOutput } from "./core/terminal/terminalSanitize.js";
|
|
159
|
+
import { createTerminalModeController, setTerminalControlUIState } from "./core/terminal/terminalControl.js";
|
|
160
|
+
import {
|
|
161
|
+
acquireTerminalTitleGuard,
|
|
162
|
+
beginColdStartSequence,
|
|
163
|
+
deriveTerminalTitle,
|
|
164
|
+
reassertIntendedTerminalTitle,
|
|
165
|
+
refreshTerminalTitle,
|
|
166
|
+
setTerminalTitleLifecycleState,
|
|
167
|
+
setIntendedTerminalTitle,
|
|
168
|
+
} from "./core/terminal/terminalTitle.js";
|
|
169
|
+
import { getStdinDebugState, traceInputDebug } from "./core/inputDebug.js";
|
|
170
|
+
import * as perf from "./core/perf/profiler.js";
|
|
171
|
+
import * as renderDebug from "./core/perf/renderDebug.js";
|
|
172
|
+
import {
|
|
173
|
+
checkGhCli,
|
|
174
|
+
checkLocalGitRemote,
|
|
175
|
+
checkLocalGitWrite,
|
|
176
|
+
classifyDiagnostics,
|
|
177
|
+
getLocalGitRemoteUrl,
|
|
178
|
+
parseRepoIdentity,
|
|
179
|
+
type DiagnosticResult,
|
|
180
|
+
} from "./core/githubDiagnostics.js";
|
|
181
|
+
import type { RunEvent, Screen, ShellEvent, TimelineEvent, UIState, UserPromptEvent } from "./session/types.js";
|
|
182
|
+
import {
|
|
183
|
+
buildFollowUpPrompt,
|
|
184
|
+
createRunEvent,
|
|
185
|
+
extractAssistantActionRequired,
|
|
186
|
+
guardConfigMutation,
|
|
187
|
+
isCurrentRun,
|
|
188
|
+
} from "./session/chatLifecycle.js";
|
|
189
|
+
import { findUserPrompt, useAppSessionState } from "./session/appSession.js";
|
|
190
|
+
import { createLiveRenderScheduler, type LiveRenderUpdate } from "./session/liveRenderScheduler.js";
|
|
191
|
+
import { hasFinalizedTranscriptPlan } from "./session/planTranscript.js";
|
|
192
|
+
import { schedulePromptRunStartAfterVisibleCommit } from "./session/promptRunSchedule.js";
|
|
193
|
+
import {
|
|
194
|
+
approvePlanExecution,
|
|
195
|
+
beginPlanFeedback,
|
|
196
|
+
cancelPlanFeedback,
|
|
197
|
+
createInitialPlanFlowState,
|
|
198
|
+
finishPlanGeneration,
|
|
199
|
+
resetPlanFlow,
|
|
200
|
+
startPlanGeneration,
|
|
201
|
+
submitPlanFeedback,
|
|
202
|
+
type PlanFlowState,
|
|
203
|
+
} from "./session/planFlow.js";
|
|
204
|
+
import { AuthPanel } from "./ui/AuthPanel.js";
|
|
205
|
+
import { BackendPicker } from "./ui/BackendPicker.js";
|
|
206
|
+
import { measureBottomComposerRows, MemoizedBottomComposer } from "./ui/BottomComposer.js";
|
|
207
|
+
import { useTerminalViewport } from "./ui/layout.js";
|
|
208
|
+
import { ModelPickerScreen } from "./ui/ModelPickerScreen.js";
|
|
209
|
+
import { ModePicker } from "./ui/ModePicker.js";
|
|
210
|
+
import { PlanActionPicker, type PlanActionValue, measurePlanActionPickerRows } from "./ui/PlanActionPicker.js";
|
|
211
|
+
import { PermissionsPanel, type PermissionsPanelAction } from "./ui/PermissionsPanel.js";
|
|
212
|
+
import { ProviderPicker } from "./ui/ProviderPicker.js";
|
|
213
|
+
import { ReasoningPicker } from "./ui/ReasoningPicker.js";
|
|
214
|
+
import { AttachmentImportPanel, type PendingImportFile } from "./ui/AttachmentImportPanel.js";
|
|
215
|
+
import { SelectionPanel } from "./ui/SelectionPanel.js";
|
|
216
|
+
import { SettingsPanel } from "./ui/SettingsPanel.js";
|
|
217
|
+
import { measureTextEntryPanelRows, TextEntryPanel } from "./ui/TextEntryPanel.js";
|
|
218
|
+
import { ThemePicker } from "./ui/ThemePicker.js";
|
|
219
|
+
import { getFocusTargetForScreen, FOCUS_IDS } from "./ui/focus.js";
|
|
220
|
+
import { ThemeProvider, THEMES } from "./ui/theme.js";
|
|
221
|
+
import {
|
|
222
|
+
cancelThemeSelection,
|
|
223
|
+
commitThemeSelection,
|
|
224
|
+
getDisplayedThemeName,
|
|
225
|
+
previewThemeSelection,
|
|
226
|
+
shouldBumpComposerInstance,
|
|
227
|
+
type ThemeSelectionState,
|
|
228
|
+
} from "./ui/themeFlow.js";
|
|
229
|
+
import { isBusy as isUiBusy } from "./session/types.js";
|
|
230
|
+
import { AppShell } from "./ui/AppShell.js";
|
|
231
|
+
|
|
232
|
+
// ─── Module Constants & Helpers ────────────────────────────────────────────────
|
|
233
|
+
|
|
234
|
+
let nextEventId = 0;
|
|
235
|
+
let nextTurnId = 0;
|
|
236
|
+
// 50ms keeps assistant text live while avoiding frame-wide terminal repaint
|
|
237
|
+
// churn during streaming/action updates.
|
|
238
|
+
const LIVE_UPDATE_FLUSH_MS = 50;
|
|
239
|
+
const PROGRESS_ONLY_FLUSH_MS = 175;
|
|
240
|
+
|
|
241
|
+
function formatWritableRootsMessage(roots: readonly string[]): string {
|
|
242
|
+
return roots.length > 0
|
|
243
|
+
? roots.map((root) => ` - ${root}`).join("\n")
|
|
244
|
+
: " - none";
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function createEventId(): number {
|
|
248
|
+
return nextEventId++;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function createTurnId(): number {
|
|
252
|
+
return nextTurnId++;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function createInitialAuthStatus(): CodexAuthProbeResult {
|
|
256
|
+
return {
|
|
257
|
+
state: "checking",
|
|
258
|
+
checkedAt: 0,
|
|
259
|
+
rawSummary: "Auth check pending.",
|
|
260
|
+
recommendedAction: "Run /auth status to check sign-in state.",
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
interface AppProps {
|
|
265
|
+
launchArgs: LaunchArgs;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
interface PromptRunTiming {
|
|
269
|
+
submitEpochMs: number;
|
|
270
|
+
submitMonotonicMs: number;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
interface PromptRunLifecycle {
|
|
274
|
+
parseActionRequired?: boolean;
|
|
275
|
+
disableModeAutoUpgrade?: boolean;
|
|
276
|
+
runtimeOverride?: PartialRuntimeConfig;
|
|
277
|
+
responsePresentation?: "assistant" | "plan";
|
|
278
|
+
approvedPlan?: string;
|
|
279
|
+
submitTiming?: PromptRunTiming;
|
|
280
|
+
commitPrompt?: boolean;
|
|
281
|
+
onCompleted?: (result: { response: string; turnId: number; runId: number }) => void;
|
|
282
|
+
onFailed?: (result: { message: string; turnId: number; runId: number }) => void;
|
|
283
|
+
onCanceled?: (result: { turnId: number; runId: number }) => void;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function createPromptRunTiming(): PromptRunTiming {
|
|
287
|
+
return {
|
|
288
|
+
submitEpochMs: Date.now(),
|
|
289
|
+
submitMonotonicMs: performance.now(),
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function App({ launchArgs }: AppProps) {
|
|
294
|
+
const { exit } = useApp();
|
|
295
|
+
const focusManager = useFocusManager();
|
|
296
|
+
const workspaceRoot = useMemo(() => resolveWorkspaceRoot(), []);
|
|
297
|
+
const projectInstructionsLoad = useMemo(() => loadProjectInstructions(workspaceRoot), [workspaceRoot]);
|
|
298
|
+
const projectInstructions = projectInstructionsLoad.status === "loaded"
|
|
299
|
+
? projectInstructionsLoad.instructions
|
|
300
|
+
: null;
|
|
301
|
+
const initialSettings = useRef(loadSettings());
|
|
302
|
+
const initialProviderWorkspaceConfig = useRef<ProviderWorkspaceConfig>(loadProviderWorkspaceConfig(workspaceRoot));
|
|
303
|
+
const initialLayeredConfig = useRef<LayeredConfigResult | null>(null);
|
|
304
|
+
if (initialLayeredConfig.current === null) {
|
|
305
|
+
initialLayeredConfig.current = resolveLayeredConfig({ workspaceRoot, launchArgs });
|
|
306
|
+
}
|
|
307
|
+
const launchContext = useMemo(
|
|
308
|
+
() => resolveLaunchContext({ workspaceRoot, forwardArgs: launchArgs.passthroughArgs }),
|
|
309
|
+
[launchArgs.passthroughArgs, workspaceRoot],
|
|
310
|
+
);
|
|
311
|
+
const workspaceCommandContext = useMemo(
|
|
312
|
+
() => buildWorkspaceCommandContext(launchContext),
|
|
313
|
+
[launchContext],
|
|
314
|
+
);
|
|
315
|
+
const terminalLayout = useTerminalViewport();
|
|
316
|
+
|
|
317
|
+
// ─── State & Refs ────────────────────────────────────────────────────────────
|
|
318
|
+
|
|
319
|
+
const [baseLayeredConfig, setBaseLayeredConfig] = useState<LayeredConfigResult>(initialLayeredConfig.current);
|
|
320
|
+
const [sessionRuntimeOverride, setSessionRuntimeOverride] = useState<PartialRuntimeConfig>(() => {
|
|
321
|
+
const initialRoute = initialProviderWorkspaceConfig.current.activeRoute;
|
|
322
|
+
if (!initialRoute || !isProviderRoutableInCodexa(initialRoute.providerId)) {
|
|
323
|
+
return {};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// When --model was given on the CLI, that arg must win over the persisted activeRoute
|
|
327
|
+
// model so that explicit test/benchmark model flags are actually honoured for the session.
|
|
328
|
+
const cliModel = launchArgs.modelOverride;
|
|
329
|
+
return {
|
|
330
|
+
model: cliModel ?? initialRoute.modelId,
|
|
331
|
+
...(initialRoute.reasoning ? { reasoningLevel: initialRoute.reasoning } : {}),
|
|
332
|
+
};
|
|
333
|
+
});
|
|
334
|
+
const [authPreference, setAuthPreference] = useState<AuthPreference>(initialSettings.current.auth.preference);
|
|
335
|
+
const [workspaceDisplayMode, setWorkspaceDisplayMode] = useState<WorkspaceDisplayMode>(
|
|
336
|
+
initialSettings.current.ui.workspaceDisplayMode,
|
|
337
|
+
);
|
|
338
|
+
const [terminalTitleMode, setTerminalTitleMode] = useState<TerminalTitleMode>(
|
|
339
|
+
initialSettings.current.ui.terminalTitleMode,
|
|
340
|
+
);
|
|
341
|
+
const [showBusyLoader, setShowBusyLoader] = useState(
|
|
342
|
+
initialSettings.current.ui.showBusyLoader,
|
|
343
|
+
);
|
|
344
|
+
const [terminalMouseMode, setTerminalMouseMode] = useState<TerminalMouseMode>(
|
|
345
|
+
initialSettings.current.ui.terminalMouseMode,
|
|
346
|
+
);
|
|
347
|
+
const [providerWorkspaceConfig, setProviderWorkspaceConfig] = useState<ProviderWorkspaceConfig>(
|
|
348
|
+
initialProviderWorkspaceConfig.current,
|
|
349
|
+
);
|
|
350
|
+
const [pendingRouteProviderId, setPendingRouteProviderId] = useState<ProviderId | null>(null);
|
|
351
|
+
const [themeSelection, setThemeSelection] = useState<ThemeSelectionState>({
|
|
352
|
+
committedTheme: initialSettings.current.ui.theme,
|
|
353
|
+
previewTheme: null,
|
|
354
|
+
});
|
|
355
|
+
const [customTheme, setCustomTheme] = useState(initialSettings.current.ui.customTheme);
|
|
356
|
+
const [headerConfig] = useState(initialSettings.current.header);
|
|
357
|
+
const [screen, setScreen] = useState<Screen>("main");
|
|
358
|
+
const [pendingImport, setPendingImport] = useState<{
|
|
359
|
+
prompt: string;
|
|
360
|
+
files: PendingImportFile[];
|
|
361
|
+
attachmentsDir: string;
|
|
362
|
+
} | null>(null);
|
|
363
|
+
const [registryNonce, setRegistryNonce] = useState(0);
|
|
364
|
+
const screenRef = useRef<Screen>("main");
|
|
365
|
+
screenRef.current = screen;
|
|
366
|
+
const [composerInstanceKey, setComposerInstanceKey] = useState(0);
|
|
367
|
+
const { state: sessionState, dispatch: dispatchSession } = useAppSessionState();
|
|
368
|
+
const [authStatus, setAuthStatus] = useState<CodexAuthProbeResult>(createInitialAuthStatus());
|
|
369
|
+
const [authStatusBusy, setAuthStatusBusy] = useState(false);
|
|
370
|
+
// Running character total across the conversation — used to estimate token usage
|
|
371
|
+
const [conversationChars, setConversationChars] = useState(0);
|
|
372
|
+
const [modelCapabilities, setModelCapabilities] = useState<CodexModelCapabilities | null>(null);
|
|
373
|
+
const [modelCapabilitiesBusy, setModelCapabilitiesBusy] = useState(false);
|
|
374
|
+
const [activeContextMetadata, setActiveContextMetadata] = useState<ModelContextMetadata | null>(null);
|
|
375
|
+
const { stdout } = useStdout();
|
|
376
|
+
const { stdin } = useStdin();
|
|
377
|
+
const terminalControl = useMemo(() => createTerminalModeController((chunk) => stdout.write(chunk)), [stdout]);
|
|
378
|
+
const [mouseOverride, setMouseOverride] = useState<boolean | null>(null);
|
|
379
|
+
const [isMouseIdle, setIsMouseIdle] = useState(false);
|
|
380
|
+
const mouseIdleTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
381
|
+
|
|
382
|
+
const resetMouseIdle = useCallback(() => {
|
|
383
|
+
setIsMouseIdle(false);
|
|
384
|
+
if (mouseIdleTimerRef.current) {
|
|
385
|
+
clearTimeout(mouseIdleTimerRef.current);
|
|
386
|
+
}
|
|
387
|
+
mouseIdleTimerRef.current = setTimeout(() => {
|
|
388
|
+
setIsMouseIdle(true);
|
|
389
|
+
}, 1500);
|
|
390
|
+
}, []);
|
|
391
|
+
|
|
392
|
+
useInput(() => {
|
|
393
|
+
// Any keyboard activity re-enables mouse tracking if it was idle.
|
|
394
|
+
resetMouseIdle();
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
const [verboseMode, setVerboseMode] = useState(false);
|
|
398
|
+
const [planFlow, setPlanFlow] = useState<PlanFlowState>(createInitialPlanFlowState);
|
|
399
|
+
const [initialRevisionText, setInitialRevisionText] = useState("");
|
|
400
|
+
// Mouse reporting defaults to the persisted terminalMouseMode setting.
|
|
401
|
+
// "wheel" (default) enables SGR mouse reporting so the timeline can receive
|
|
402
|
+
// wheel events; native drag-select then requires Shift in Windows Terminal.
|
|
403
|
+
// "selection" keeps the terminal in control of mouse events.
|
|
404
|
+
// /mouse toggles the in-session override without changing the saved setting.
|
|
405
|
+
//
|
|
406
|
+
// We apply an idle-timeout: if no wheel events or keypresses occur for 1.5s,
|
|
407
|
+
// we disable mouse reporting so native drag-selection works unmodified.
|
|
408
|
+
const mouseCapture = (mouseOverride ?? (terminalMouseMode === "wheel")) && !isMouseIdle;
|
|
409
|
+
|
|
410
|
+
// ─── Effects & Handlers ──────────────────────────────────────────────────────
|
|
411
|
+
|
|
412
|
+
useEffect(() => {
|
|
413
|
+
// Default path writes the disable sequences defensively, preserving native
|
|
414
|
+
// terminal selection unless the user explicitly opts into /mouse capture.
|
|
415
|
+
terminalControl.setMouseReporting(mouseCapture, mouseCapture ? "src/app.tsx:mouseCapture.enable" : "src/app.tsx:mouseCapture.disable");
|
|
416
|
+
return () => {
|
|
417
|
+
terminalControl.setMouseReporting(false, "src/app.tsx:mouseCapture.cleanup");
|
|
418
|
+
};
|
|
419
|
+
}, [mouseCapture, terminalControl]);
|
|
420
|
+
|
|
421
|
+
const cleanupRef = useRef<(() => void) | null>(null);
|
|
422
|
+
const activeRunLifecycleRef = useRef<PromptRunLifecycle | null>(null);
|
|
423
|
+
const activeRunTimingRef = useRef<(PromptRunTiming & { runId: number; turnId: number }) | null>(null);
|
|
424
|
+
const isMountedRef = useRef(true);
|
|
425
|
+
const activeRunIdRef = useRef<number | null>(null);
|
|
426
|
+
const activeTurnIdRef = useRef<number | null>(null);
|
|
427
|
+
const clearEpochRef = useRef<number>(0); // Incremented on /clear to suppress stale command events
|
|
428
|
+
const externalCliStatusRef = useRef(sessionState.externalCliStatus);
|
|
429
|
+
const previousScreenRef = useRef<Screen>("main");
|
|
430
|
+
const themePreviewTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
431
|
+
const modelDiscoveryInFlightRef = useRef<Promise<CodexModelCapabilities> | null>(null);
|
|
432
|
+
const modelDiscoveryAnnounceRef = useRef(false);
|
|
433
|
+
const intendedInputModeRef = useRef<"chat/input" | "model-picker">("chat/input");
|
|
434
|
+
const intendedFocusTargetRef = useRef<string>(FOCUS_IDS.composer);
|
|
435
|
+
const modelSelectionInFlightRef = useRef(false);
|
|
436
|
+
const providerRouteErrorsRef = useRef<Record<string, string>>({});
|
|
437
|
+
const providerDiagnosticsRef = useRef<Record<string, Record<string, string | number | boolean | null>>>({});
|
|
438
|
+
const providerMigrationNoticeShownRef = useRef(false);
|
|
439
|
+
const initialPromptSubmittedRef = useRef(false);
|
|
440
|
+
const activeThemeName = getDisplayedThemeName(themeSelection);
|
|
441
|
+
const activeTheme =
|
|
442
|
+
activeThemeName === "custom"
|
|
443
|
+
? { ...THEMES.purple, ...customTheme }
|
|
444
|
+
: (THEMES[activeThemeName] ?? THEMES.purple);
|
|
445
|
+
const baseRuntimeConfigRef = useRef(baseLayeredConfig.runtime);
|
|
446
|
+
const layeredRuntimeConfig = useMemo(
|
|
447
|
+
() => applyLayeredRuntimeOverride(baseLayeredConfig, sessionRuntimeOverride, "In-session overrides"),
|
|
448
|
+
[baseLayeredConfig, sessionRuntimeOverride],
|
|
449
|
+
);
|
|
450
|
+
const runtimeConfig = layeredRuntimeConfig.runtime;
|
|
451
|
+
const { provider: backend, model, mode, reasoningLevel, planMode } = runtimeConfig;
|
|
452
|
+
const resolvedRuntimeConfig = useMemo(() => resolveRuntimeConfig(runtimeConfig), [runtimeConfig]);
|
|
453
|
+
const runtimeSummary = useMemo(() => buildRuntimeSummary(resolvedRuntimeConfig), [resolvedRuntimeConfig]);
|
|
454
|
+
const activeProviderRoute = useMemo(() => {
|
|
455
|
+
// When --model was given on the CLI, override the stored activeRoute's modelId so
|
|
456
|
+
// the actual run uses the CLI model instead of whatever is persisted in providers.json.
|
|
457
|
+
// The providers.json entry is left unchanged so it survives this session.
|
|
458
|
+
const cliModel = launchArgs.modelOverride;
|
|
459
|
+
const configuredRoute = providerWorkspaceConfig.activeRoute;
|
|
460
|
+
const effectiveRoute = cliModel && configuredRoute
|
|
461
|
+
? { ...configuredRoute, modelId: cliModel }
|
|
462
|
+
: configuredRoute;
|
|
463
|
+
return resolveActiveProviderRoute({
|
|
464
|
+
workspaceConfigActiveRoute: effectiveRoute,
|
|
465
|
+
currentModel: model,
|
|
466
|
+
currentReasoning: reasoningLevel,
|
|
467
|
+
});
|
|
468
|
+
}, [launchArgs.modelOverride, model, providerWorkspaceConfig.activeRoute, reasoningLevel, registryNonce]);
|
|
469
|
+
const activeProviderRuntime = useMemo(
|
|
470
|
+
() => getProviderRuntime(activeProviderRoute.providerId),
|
|
471
|
+
[activeProviderRoute.providerId],
|
|
472
|
+
);
|
|
473
|
+
const providerRegistry = useMemo(
|
|
474
|
+
() => {
|
|
475
|
+
setLocalProviderConfig(providerWorkspaceConfig.providers?.local);
|
|
476
|
+
return buildProviderRegistry({
|
|
477
|
+
activeModel: model,
|
|
478
|
+
workspaceConfig: providerWorkspaceConfig,
|
|
479
|
+
diagnostics: providerDiagnosticsRef.current,
|
|
480
|
+
routeErrors: providerRouteErrorsRef.current,
|
|
481
|
+
});
|
|
482
|
+
},
|
|
483
|
+
// registryNonce is intentionally included: startup probes and post-validation
|
|
484
|
+
// updates mutate providerDiagnosticsRef/providerRouteErrorsRef (refs, not state)
|
|
485
|
+
// and then increment the nonce to trigger a re-read of those refs.
|
|
486
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
487
|
+
[model, providerWorkspaceConfig, registryNonce],
|
|
488
|
+
);
|
|
489
|
+
const workspaceDefaultProvider = useMemo(
|
|
490
|
+
() => providerRegistry.find((provider) => provider.isDefault) ?? providerRegistry[0] ?? null,
|
|
491
|
+
[providerRegistry],
|
|
492
|
+
);
|
|
493
|
+
const activeRouteProviderId = activeProviderRoute.providerId;
|
|
494
|
+
|
|
495
|
+
// Reset provider readiness when the user switches to a different provider.
|
|
496
|
+
useEffect(() => {
|
|
497
|
+
dispatchSession({ type: "SET_EXTERNAL_CLI_STATUS", status: "idle" });
|
|
498
|
+
}, [activeRouteProviderId]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
499
|
+
|
|
500
|
+
const activeRouteProvider = useMemo(
|
|
501
|
+
() => findProvider(providerRegistry, activeRouteProviderId) ?? providerRegistry[0] ?? null,
|
|
502
|
+
[activeRouteProviderId, providerRegistry],
|
|
503
|
+
);
|
|
504
|
+
const modelPickerProviderId = pendingRouteProviderId ?? activeProviderRoute.providerId;
|
|
505
|
+
const modelPickerRuntime = useMemo(
|
|
506
|
+
() => getProviderRuntime(modelPickerProviderId),
|
|
507
|
+
[modelPickerProviderId],
|
|
508
|
+
);
|
|
509
|
+
const modelPickerDiscovery = useMemo(() => {
|
|
510
|
+
if (modelPickerProviderId === "openai") return null;
|
|
511
|
+
return discoverProviderModels(modelPickerProviderId);
|
|
512
|
+
}, [modelPickerProviderId]);
|
|
513
|
+
const providerModelCapabilities = useMemo(() => {
|
|
514
|
+
if (!modelPickerDiscovery) return null;
|
|
515
|
+
return providerModelsToCodexCapabilities(modelPickerDiscovery.models, activeProviderRoute.modelId);
|
|
516
|
+
}, [activeProviderRoute.modelId, modelPickerDiscovery]);
|
|
517
|
+
const activeRouteModelCapabilities = useMemo(() => {
|
|
518
|
+
if (activeProviderRoute.providerId === "openai") return modelCapabilities;
|
|
519
|
+
const discovery = discoverProviderModels(activeProviderRoute.providerId);
|
|
520
|
+
return providerModelsToCodexCapabilities(discovery.models, activeProviderRoute.modelId);
|
|
521
|
+
}, [activeProviderRoute.modelId, activeProviderRoute.providerId, modelCapabilities]);
|
|
522
|
+
const modelPickerModels = useMemo(
|
|
523
|
+
() => {
|
|
524
|
+
if (providerModelCapabilities) {
|
|
525
|
+
return getSelectableModelCapabilities(providerModelCapabilities);
|
|
526
|
+
}
|
|
527
|
+
if (modelPickerProviderId === "openai") {
|
|
528
|
+
return getSelectableModelCapabilities(modelCapabilities ?? createFallbackModelCapabilities(null));
|
|
529
|
+
}
|
|
530
|
+
return [];
|
|
531
|
+
},
|
|
532
|
+
[modelCapabilities, modelPickerProviderId, providerModelCapabilities],
|
|
533
|
+
);
|
|
534
|
+
const modelPickerCurrentModel = useMemo(() => {
|
|
535
|
+
if (!pendingRouteProviderId) return activeProviderRoute.modelId;
|
|
536
|
+
if (pendingRouteProviderId === activeProviderRoute.providerId) return activeProviderRoute.modelId;
|
|
537
|
+
// Non-active provider: use stored default, fall back to first selectable model
|
|
538
|
+
const storedDefault = providerWorkspaceConfig.providers?.[pendingRouteProviderId]?.currentModel;
|
|
539
|
+
const selectable = getSelectableModelCapabilities(
|
|
540
|
+
providerModelCapabilities ?? createFallbackModelCapabilities(null),
|
|
541
|
+
);
|
|
542
|
+
return storedDefault ?? selectable[0]?.model ?? model;
|
|
543
|
+
}, [activeProviderRoute, model, pendingRouteProviderId, providerModelCapabilities, providerWorkspaceConfig]);
|
|
544
|
+
const modelPickerCurrentReasoning = useMemo(() => {
|
|
545
|
+
if (!pendingRouteProviderId) {
|
|
546
|
+
return activeProviderRoute.reasoning ?? reasoningLevel;
|
|
547
|
+
}
|
|
548
|
+
if (pendingRouteProviderId === activeProviderRoute.providerId) {
|
|
549
|
+
return activeProviderRoute.reasoning ?? reasoningLevel;
|
|
550
|
+
}
|
|
551
|
+
const storedReasoning = providerWorkspaceConfig.providers?.[pendingRouteProviderId]?.currentReasoning;
|
|
552
|
+
if (storedReasoning) return storedReasoning;
|
|
553
|
+
const pickerCapabilities = pendingRouteProviderId === "openai" ? modelCapabilities : providerModelCapabilities;
|
|
554
|
+
const capability = findModelCapability(pickerCapabilities, modelPickerCurrentModel);
|
|
555
|
+
return capability?.defaultReasoningLevel
|
|
556
|
+
?? capability?.supportedReasoningLevels?.[0]?.id
|
|
557
|
+
?? reasoningLevel;
|
|
558
|
+
}, [
|
|
559
|
+
activeProviderRoute.providerId,
|
|
560
|
+
activeProviderRoute.reasoning,
|
|
561
|
+
modelPickerCurrentModel,
|
|
562
|
+
modelCapabilities,
|
|
563
|
+
pendingRouteProviderId,
|
|
564
|
+
providerModelCapabilities,
|
|
565
|
+
providerWorkspaceConfig.providers,
|
|
566
|
+
reasoningLevel,
|
|
567
|
+
]);
|
|
568
|
+
const modelPickerProviderLabel = useMemo(
|
|
569
|
+
() => modelPickerRuntime.modelPickerLabel
|
|
570
|
+
?? findProvider(providerRegistry, modelPickerProviderId)?.displayName
|
|
571
|
+
?? modelPickerRuntime.label,
|
|
572
|
+
[modelPickerProviderId, modelPickerRuntime, providerRegistry],
|
|
573
|
+
);
|
|
574
|
+
const modelPickerEmptyMessage = useMemo(() => {
|
|
575
|
+
if (modelPickerProviderId === "openai") {
|
|
576
|
+
return modelCapabilities?.error ? `No models available: ${modelCapabilities.error}` : "No models available.";
|
|
577
|
+
}
|
|
578
|
+
if (modelPickerDiscovery?.status === "ready" && modelPickerDiscovery.models.length === 0) {
|
|
579
|
+
const setup = getProviderRouteSetupMessage(modelPickerProviderId);
|
|
580
|
+
return `No ${modelPickerProviderLabel} models available because the route is not configured. ${setup}`;
|
|
581
|
+
}
|
|
582
|
+
return modelPickerDiscovery?.message ?? "No models available.";
|
|
583
|
+
}, [modelCapabilities, modelPickerDiscovery, modelPickerProviderId, modelPickerProviderLabel]);
|
|
584
|
+
const routeStatusMessage = useMemo(() => {
|
|
585
|
+
const providerLines = providerRegistry.map((provider) => {
|
|
586
|
+
const runtime = getProviderRuntime(provider.id);
|
|
587
|
+
const discovery = runtime.discoverModels();
|
|
588
|
+
const routingStatus = runtime.routeAvailable
|
|
589
|
+
? isProviderRouteConfigured(provider.id) ? "configured" : "not configured"
|
|
590
|
+
: "unavailable";
|
|
591
|
+
|
|
592
|
+
let line = ` ${provider.displayName} routing: ${routingStatus} (${discovery.backendKind})`;
|
|
593
|
+
|
|
594
|
+
const diagnostics = providerDiagnosticsRef.current[provider.id];
|
|
595
|
+
if (diagnostics && provider.id === "google") {
|
|
596
|
+
const lines = [line];
|
|
597
|
+
if (diagnostics.resolvedCommand ?? diagnostics.executablePath) lines.push(` Resolved command: ${diagnostics.resolvedCommand ?? diagnostics.executablePath}`);
|
|
598
|
+
if (diagnostics.version) lines.push(` Version: ${diagnostics.version}`);
|
|
599
|
+
if (diagnostics.headlessPromptMode) lines.push(` Headless prompt mode: ${diagnostics.headlessPromptMode}`);
|
|
600
|
+
lines.push(` Status: ${diagnostics.probeStatus ?? (diagnostics.status === "completed" && diagnostics.exitCode === 0 && diagnostics.probeMatch ? "Ready" : "failed")}`);
|
|
601
|
+
if (diagnostics.lastProbeCommandArgs) lines.push(` Last probe command args: ${diagnostics.lastProbeCommandArgs}`);
|
|
602
|
+
if (diagnostics.status !== "completed" || diagnostics.exitCode !== 0 || !diagnostics.probeMatch) {
|
|
603
|
+
const reason = diagnostics.failureReason ?? (diagnostics.timeout ? "timeout" : "unknown");
|
|
604
|
+
lines.push(` Reason: ${reason}`);
|
|
605
|
+
if (diagnostics.firstUsefulOutputLine) lines.push(` First output: ${diagnostics.firstUsefulOutputLine}`);
|
|
606
|
+
}
|
|
607
|
+
lines.push(` API fallback: ${hasGeminiApiKey() ? "available" : "unavailable"}`);
|
|
608
|
+
line = lines.join("\n");
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (diagnostics && provider.id === "local") {
|
|
612
|
+
const lines = [line];
|
|
613
|
+
lines.push(` Base URL: ${diagnostics.baseUrl ?? "unknown"}`);
|
|
614
|
+
lines.push(` Selected model: ${diagnostics.selectedModel ?? "none"}`);
|
|
615
|
+
lines.push(` Models: ${diagnostics.discoveredModels || "none"}`);
|
|
616
|
+
lines.push(` Endpoint check: ${diagnostics.endpointCheckResult ?? "unknown"}`);
|
|
617
|
+
if (diagnostics.errorMessage) lines.push(` Error: ${diagnostics.errorMessage}`);
|
|
618
|
+
line = lines.join("\n");
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (diagnostics?.contextSource || diagnostics?.contextError) {
|
|
622
|
+
const contextLength = typeof diagnostics.contextLength === "number"
|
|
623
|
+
? diagnostics.contextLength
|
|
624
|
+
: null;
|
|
625
|
+
const lines = line.split("\n");
|
|
626
|
+
lines.push(` Context: ${formatContextLength(contextLength)}`);
|
|
627
|
+
lines.push(` Context source: ${diagnostics.contextSource ?? "unknown"}`);
|
|
628
|
+
lines.push(` Context confidence: ${diagnostics.contextConfidence ?? "unknown"}`);
|
|
629
|
+
if (diagnostics.contextRawField) lines.push(` Context field: ${diagnostics.contextRawField}`);
|
|
630
|
+
if (diagnostics.contextError) lines.push(` Context reason: ${diagnostics.contextError}`);
|
|
631
|
+
line = lines.join("\n");
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
return line;
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
const activeModelInfo = activeProviderRoute.providerId === "google" && activeProviderRoute.modelSelection
|
|
638
|
+
? (activeProviderRoute.modelSelection.kind === "auto"
|
|
639
|
+
? `Auto (${activeProviderRoute.modelSelection.family === "gemini-3" ? "Gemini 3" : "Gemini 2.5"}) -> ${activeProviderRoute.modelId}`
|
|
640
|
+
: activeProviderRoute.modelId)
|
|
641
|
+
: activeProviderRoute.modelId;
|
|
642
|
+
|
|
643
|
+
const ctxValue = activeContextMetadata?.contextLength != null
|
|
644
|
+
? `${activeContextMetadata.confidence === "estimated" ? "~" : ""}${formatContextCompact(activeContextMetadata.contextLength)}`
|
|
645
|
+
: "Unknown";
|
|
646
|
+
const ctxSource = activeContextMetadata?.source && activeContextMetadata.source !== "unknown"
|
|
647
|
+
? ` (${activeContextMetadata.source})`
|
|
648
|
+
: "";
|
|
649
|
+
|
|
650
|
+
return [
|
|
651
|
+
"Route status:",
|
|
652
|
+
` Workspace default provider: ${workspaceDefaultProvider?.displayName ?? "OpenAI"}`,
|
|
653
|
+
` Active chat route: ${activeRouteProvider?.displayName ?? "OpenAI"} / ${activeModelInfo}`,
|
|
654
|
+
` Context: ${ctxValue}${ctxSource}`,
|
|
655
|
+
` Backend kind: ${activeProviderRoute.backendKind}`,
|
|
656
|
+
` In-Codexa routing: ${activeProviderRuntime.routeAvailable ? isProviderRouteConfigured(activeProviderRoute.providerId) ? "configured" : "not configured" : "unavailable"}`,
|
|
657
|
+
` External launch: ${activeRouteProvider?.launchCommand ? "Available" : "Unavailable"}`,
|
|
658
|
+
...(providerLines.length > 0 ? providerLines : []),
|
|
659
|
+
].join("\n");
|
|
660
|
+
}, [activeContextMetadata, activeProviderRoute.backendKind, activeProviderRoute.modelId, activeProviderRoute.modelSelection, activeProviderRoute.providerId, activeProviderRoute.reasoning, activeProviderRuntime.routeAvailable, activeRouteModelCapabilities, activeRouteProvider, providerRegistry, reasoningLevel, workspaceDefaultProvider]);
|
|
661
|
+
const selectionProfile = useMemo(
|
|
662
|
+
() => getTerminalSelectionProfile(process.env),
|
|
663
|
+
[],
|
|
664
|
+
);
|
|
665
|
+
const selectableModelCapabilities = useMemo(
|
|
666
|
+
() => activeRouteModelCapabilities ? getSelectableModelCapabilities(activeRouteModelCapabilities) : [],
|
|
667
|
+
[activeRouteModelCapabilities],
|
|
668
|
+
);
|
|
669
|
+
const currentModelCapability = useMemo(
|
|
670
|
+
() => findModelCapability(activeRouteModelCapabilities, activeProviderRoute.modelId),
|
|
671
|
+
[activeProviderRoute.modelId, activeRouteModelCapabilities],
|
|
672
|
+
);
|
|
673
|
+
const currentModelRawMetadataKey = useMemo(
|
|
674
|
+
() => currentModelCapability?.raw === undefined ? "" : JSON.stringify(currentModelCapability.raw),
|
|
675
|
+
[currentModelCapability?.raw],
|
|
676
|
+
);
|
|
677
|
+
const currentReasoningCapabilities = currentModelCapability?.supportedReasoningLevels ?? [];
|
|
678
|
+
const currentReasoningSourceLabel = useMemo(() => {
|
|
679
|
+
if (activeProviderRoute.providerId !== "anthropic") return null;
|
|
680
|
+
const raw = currentModelCapability?.raw as { source?: string; effortVerified?: boolean } | null | undefined;
|
|
681
|
+
if (raw?.source === "claude-code" || raw?.source === "discovered") return "Discovered from Claude Code";
|
|
682
|
+
if (raw?.source === "settings" || raw?.source === "config") return "From Claude settings";
|
|
683
|
+
return raw?.effortVerified === false ? "Fallback defaults; unverified" : "Fallback defaults";
|
|
684
|
+
}, [activeProviderRoute.providerId, currentModelCapability]);
|
|
685
|
+
|
|
686
|
+
useEffect(() => {
|
|
687
|
+
let cancelled = false;
|
|
688
|
+
const providerId = activeProviderRoute.providerId;
|
|
689
|
+
const modelId = activeProviderRoute.modelId;
|
|
690
|
+
const rawMetadata = currentModelCapability?.raw;
|
|
691
|
+
|
|
692
|
+
void resolveModelContextLength({
|
|
693
|
+
providerId,
|
|
694
|
+
modelId,
|
|
695
|
+
providerConfig: providerWorkspaceConfig.providers?.[providerId],
|
|
696
|
+
rawMetadata,
|
|
697
|
+
}).then((metadata) => {
|
|
698
|
+
if (cancelled || !isMountedRef.current) return;
|
|
699
|
+
setActiveContextMetadata(metadata);
|
|
700
|
+
|
|
701
|
+
const contextSource = metadata.source === "known-registry" ? "registry" : metadata.source;
|
|
702
|
+
providerDiagnosticsRef.current[providerId] = {
|
|
703
|
+
...(providerDiagnosticsRef.current[providerId] ?? {}),
|
|
704
|
+
contextLength: metadata.contextLength,
|
|
705
|
+
contextSource,
|
|
706
|
+
contextConfidence: metadata.confidence,
|
|
707
|
+
contextRawField: metadata.rawField ?? null,
|
|
708
|
+
contextError: metadata.error ?? null,
|
|
709
|
+
};
|
|
710
|
+
setRegistryNonce((current) => current + 1);
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
return () => {
|
|
714
|
+
cancelled = true;
|
|
715
|
+
};
|
|
716
|
+
}, [
|
|
717
|
+
activeProviderRoute.modelId,
|
|
718
|
+
activeProviderRoute.providerId,
|
|
719
|
+
currentModelRawMetadataKey,
|
|
720
|
+
providerWorkspaceConfig.providers,
|
|
721
|
+
]);
|
|
722
|
+
|
|
723
|
+
const workspaceLabel = useMemo(
|
|
724
|
+
() => formatWorkspaceDisplayPath(workspaceRoot, workspaceDisplayMode),
|
|
725
|
+
[workspaceDisplayMode, workspaceRoot],
|
|
726
|
+
);
|
|
727
|
+
const terminalTitleLabel = useMemo(
|
|
728
|
+
() => deriveTerminalTitle(workspaceRoot, terminalTitleMode),
|
|
729
|
+
[terminalTitleMode, workspaceRoot],
|
|
730
|
+
);
|
|
731
|
+
const latestTerminalTitleRef = useRef(terminalTitleLabel);
|
|
732
|
+
latestTerminalTitleRef.current = terminalTitleLabel;
|
|
733
|
+
|
|
734
|
+
// Cold-start: write title immediately on mount, then retry to outlast Windows Terminal shell integration.
|
|
735
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
736
|
+
useEffect(() => beginColdStartSequence(latestTerminalTitleRef.current), []);
|
|
737
|
+
|
|
738
|
+
const { staticEvents, activeEvents, uiState, inputValue, cursor } = sessionState;
|
|
739
|
+
|
|
740
|
+
const currentUserSettings = useMemo<UserSettingValues>(() => ({
|
|
741
|
+
workspaceDisplayMode,
|
|
742
|
+
terminalTitleMode,
|
|
743
|
+
showBusyLoader: formatBusyLoaderSettingValue(showBusyLoader),
|
|
744
|
+
terminalMouseMode,
|
|
745
|
+
}), [showBusyLoader, terminalMouseMode, terminalTitleMode, workspaceDisplayMode]);
|
|
746
|
+
|
|
747
|
+
const allowedWritableRoots = useMemo(
|
|
748
|
+
() => resolvedRuntimeConfig.policy.writableRoots,
|
|
749
|
+
[resolvedRuntimeConfig],
|
|
750
|
+
);
|
|
751
|
+
|
|
752
|
+
const hasPlanFileAvailable = useMemo(
|
|
753
|
+
() => planFlow.kind !== "idle"
|
|
754
|
+
&& planFlow.planFilePath !== null
|
|
755
|
+
&& existsSync(planFlow.planFilePath),
|
|
756
|
+
[planFlow],
|
|
757
|
+
);
|
|
758
|
+
|
|
759
|
+
const currentModelSpec = useMemo<ModelSpec>(() => {
|
|
760
|
+
return contextMetadataToModelSpec(activeContextMetadata ?? {
|
|
761
|
+
providerId: activeProviderRoute.providerId,
|
|
762
|
+
modelId: activeProviderRoute.modelId,
|
|
763
|
+
contextLength: null,
|
|
764
|
+
source: "unknown",
|
|
765
|
+
confidence: "unknown",
|
|
766
|
+
error: "Context length unavailable for this model.",
|
|
767
|
+
});
|
|
768
|
+
}, [activeContextMetadata, activeProviderRoute.modelId, activeProviderRoute.providerId]);
|
|
769
|
+
|
|
770
|
+
const hasUserPrompt = useMemo(
|
|
771
|
+
() => staticEvents.some((e) => e.type === "user") || activeEvents.some((e) => e.type === "user"),
|
|
772
|
+
[staticEvents, activeEvents],
|
|
773
|
+
);
|
|
774
|
+
|
|
775
|
+
const hasVisibleTranscriptPlan = useMemo(
|
|
776
|
+
() => planFlow.kind === "awaiting_action"
|
|
777
|
+
&& hasFinalizedTranscriptPlan(staticEvents, planFlow.currentPlan),
|
|
778
|
+
[planFlow, staticEvents],
|
|
779
|
+
);
|
|
780
|
+
|
|
781
|
+
// Refs for mutable state values — used by stable callbacks below so they
|
|
782
|
+
// always read the latest value without being listed as deps (which would
|
|
783
|
+
// recreate the callbacks on every keystroke and defeat memoisation).
|
|
784
|
+
const inputValueRef = useRef(inputValue);
|
|
785
|
+
inputValueRef.current = inputValue;
|
|
786
|
+
const cursorRef = useRef(cursor);
|
|
787
|
+
cursorRef.current = cursor;
|
|
788
|
+
|
|
789
|
+
const busy = isUiBusy(uiState);
|
|
790
|
+
const busyRef = useRef(busy);
|
|
791
|
+
busyRef.current = busy;
|
|
792
|
+
|
|
793
|
+
const forceRefreshCurrentTerminalTitle = useCallback((debugEventName: string, busyState = busyRef.current) => {
|
|
794
|
+
refreshTerminalTitle({
|
|
795
|
+
terminalTitleMode,
|
|
796
|
+
workspaceName: deriveTerminalTitle(workspaceRoot, "dir"),
|
|
797
|
+
force: true,
|
|
798
|
+
debugEventName,
|
|
799
|
+
busyState,
|
|
800
|
+
});
|
|
801
|
+
}, [terminalTitleMode, workspaceRoot]);
|
|
802
|
+
|
|
803
|
+
const writeCurrentTerminalTitleBeforeStateChange = useCallback((reason: string) => {
|
|
804
|
+
setIntendedTerminalTitle(deriveTerminalTitle(workspaceRoot, terminalTitleMode), {
|
|
805
|
+
force: true,
|
|
806
|
+
reason,
|
|
807
|
+
});
|
|
808
|
+
}, [terminalTitleMode, workspaceRoot]);
|
|
809
|
+
|
|
810
|
+
// Re-assert terminal title whenever the app transitions between busy states,
|
|
811
|
+
// starts/stops streaming, or executes tools to recover from external overwrites.
|
|
812
|
+
useEffect(() => {
|
|
813
|
+
if (!busy) {
|
|
814
|
+
refreshTerminalTitle({
|
|
815
|
+
terminalTitleMode,
|
|
816
|
+
workspaceName: deriveTerminalTitle(workspaceRoot, "dir"),
|
|
817
|
+
force: true,
|
|
818
|
+
debugEventName: "busy-end",
|
|
819
|
+
busyState: false,
|
|
820
|
+
});
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
refreshTerminalTitle({
|
|
825
|
+
terminalTitleMode,
|
|
826
|
+
workspaceName: deriveTerminalTitle(workspaceRoot, "dir"),
|
|
827
|
+
force: true,
|
|
828
|
+
debugEventName: "busy-start",
|
|
829
|
+
busyState: true,
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
const timer = setInterval(() => {
|
|
833
|
+
refreshTerminalTitle({
|
|
834
|
+
terminalTitleMode,
|
|
835
|
+
workspaceName: deriveTerminalTitle(workspaceRoot, "dir"),
|
|
836
|
+
force: true,
|
|
837
|
+
debugEventName: "busy-guard",
|
|
838
|
+
busyState: true,
|
|
839
|
+
});
|
|
840
|
+
}, 250);
|
|
841
|
+
|
|
842
|
+
return () => {
|
|
843
|
+
clearInterval(timer);
|
|
844
|
+
};
|
|
845
|
+
}, [uiState.kind, busy, terminalTitleMode, workspaceRoot]);
|
|
846
|
+
|
|
847
|
+
useEffect(() => {
|
|
848
|
+
setIntendedTerminalTitle(terminalTitleLabel, { force: true, reason: "terminal-title-label-change" });
|
|
849
|
+
}, [terminalTitleLabel]);
|
|
850
|
+
const modelCapabilitiesBusyRef = useRef(modelCapabilitiesBusy);
|
|
851
|
+
modelCapabilitiesBusyRef.current = modelCapabilitiesBusy;
|
|
852
|
+
const composerRows = useMemo(() => {
|
|
853
|
+
if (planFlow.kind === "awaiting_action") {
|
|
854
|
+
return hasVisibleTranscriptPlan ? measurePlanActionPickerRows(terminalLayout.cols) : 1;
|
|
855
|
+
}
|
|
856
|
+
if (planFlow.kind === "collecting_feedback") {
|
|
857
|
+
return measureTextEntryPanelRows();
|
|
858
|
+
}
|
|
859
|
+
return measureBottomComposerRows({
|
|
860
|
+
layout: terminalLayout,
|
|
861
|
+
uiState,
|
|
862
|
+
mode,
|
|
863
|
+
model,
|
|
864
|
+
reasoningLevel,
|
|
865
|
+
tokensUsed: estimateTokens(conversationChars),
|
|
866
|
+
modelSpec: currentModelSpec,
|
|
867
|
+
value: inputValue,
|
|
868
|
+
cursor,
|
|
869
|
+
});
|
|
870
|
+
}, [
|
|
871
|
+
conversationChars,
|
|
872
|
+
currentModelSpec,
|
|
873
|
+
cursor,
|
|
874
|
+
inputValue,
|
|
875
|
+
mode,
|
|
876
|
+
model,
|
|
877
|
+
planFlow.kind,
|
|
878
|
+
hasVisibleTranscriptPlan,
|
|
879
|
+
reasoningLevel,
|
|
880
|
+
terminalLayout,
|
|
881
|
+
uiState,
|
|
882
|
+
]);
|
|
883
|
+
|
|
884
|
+
renderDebug.useRenderDebug("Root", {
|
|
885
|
+
screen,
|
|
886
|
+
uiStateKind: uiState.kind,
|
|
887
|
+
staticEvents,
|
|
888
|
+
activeEvents,
|
|
889
|
+
activeEventsLength: activeEvents.length,
|
|
890
|
+
inputValue,
|
|
891
|
+
cursor,
|
|
892
|
+
busy,
|
|
893
|
+
composerRows,
|
|
894
|
+
cols: terminalLayout.cols,
|
|
895
|
+
rows: terminalLayout.rows,
|
|
896
|
+
layoutEpoch: terminalLayout.layoutEpoch,
|
|
897
|
+
planFlowKind: planFlow.kind,
|
|
898
|
+
mode,
|
|
899
|
+
model,
|
|
900
|
+
reasoningLevel,
|
|
901
|
+
});
|
|
902
|
+
renderDebug.useLifecycleDebug("App", {
|
|
903
|
+
screen,
|
|
904
|
+
uiStateKind: uiState.kind,
|
|
905
|
+
});
|
|
906
|
+
renderDebug.traceLayoutValidity("Root", {
|
|
907
|
+
cols: terminalLayout.cols,
|
|
908
|
+
rows: terminalLayout.rows,
|
|
909
|
+
rawCols: terminalLayout.rawCols,
|
|
910
|
+
rawRows: terminalLayout.rawRows,
|
|
911
|
+
composerRows,
|
|
912
|
+
});
|
|
913
|
+
const previousUiStateKindRef = useRef(uiState.kind);
|
|
914
|
+
useEffect(() => {
|
|
915
|
+
setTerminalControlUIState(uiState.kind);
|
|
916
|
+
setTerminalTitleLifecycleState(`${uiState.kind}${busy ? ":busy" : ":idle"}`);
|
|
917
|
+
}, [busy, uiState.kind]);
|
|
918
|
+
|
|
919
|
+
useEffect(() => {
|
|
920
|
+
const previousKind = previousUiStateKindRef.current;
|
|
921
|
+
if (previousKind !== uiState.kind) {
|
|
922
|
+
renderDebug.traceStateTransition({
|
|
923
|
+
component: "App",
|
|
924
|
+
prevKind: previousKind,
|
|
925
|
+
nextKind: uiState.kind,
|
|
926
|
+
activeEventsLength: activeEvents.length,
|
|
927
|
+
staticEventsLength: staticEvents.length,
|
|
928
|
+
screen,
|
|
929
|
+
});
|
|
930
|
+
previousUiStateKindRef.current = uiState.kind;
|
|
931
|
+
}
|
|
932
|
+
}, [activeEvents.length, screen, staticEvents.length, uiState.kind]);
|
|
933
|
+
const previousEventCountRef = useRef(staticEvents.length + activeEvents.length);
|
|
934
|
+
useEffect(() => {
|
|
935
|
+
const previousCount = previousEventCountRef.current;
|
|
936
|
+
const nextCount = staticEvents.length + activeEvents.length;
|
|
937
|
+
if (previousCount > 0 && nextCount === 0) {
|
|
938
|
+
renderDebug.traceBlankFrame("Root", {
|
|
939
|
+
reason: "event-count-dropped-to-zero",
|
|
940
|
+
previousCount,
|
|
941
|
+
staticEventsLength: staticEvents.length,
|
|
942
|
+
activeEventsLength: activeEvents.length,
|
|
943
|
+
uiStateKind: uiState.kind,
|
|
944
|
+
screen,
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
previousEventCountRef.current = nextCount;
|
|
948
|
+
}, [activeEvents.length, screen, staticEvents.length, uiState.kind]);
|
|
949
|
+
const previousRootMeasurements = useRef<{
|
|
950
|
+
composerRows: number;
|
|
951
|
+
cols: number;
|
|
952
|
+
rows: number;
|
|
953
|
+
layoutEpoch: number;
|
|
954
|
+
} | null>(null);
|
|
955
|
+
useEffect(() => {
|
|
956
|
+
const previous = previousRootMeasurements.current;
|
|
957
|
+
const changed: string[] = [];
|
|
958
|
+
if (!previous) {
|
|
959
|
+
changed.push("mount");
|
|
960
|
+
} else {
|
|
961
|
+
if (previous.composerRows !== composerRows) changed.push("composerRows");
|
|
962
|
+
if (previous.cols !== terminalLayout.cols) changed.push("width");
|
|
963
|
+
if (previous.rows !== terminalLayout.rows) changed.push("height");
|
|
964
|
+
if (previous.layoutEpoch !== terminalLayout.layoutEpoch) changed.push("layoutEpoch");
|
|
965
|
+
}
|
|
966
|
+
if (changed.length > 0) {
|
|
967
|
+
renderDebug.traceEvent("layout", "rootMeasurementUpdate", {
|
|
968
|
+
reason: changed.join(","),
|
|
969
|
+
composerRows,
|
|
970
|
+
cols: terminalLayout.cols,
|
|
971
|
+
rows: terminalLayout.rows,
|
|
972
|
+
layoutEpoch: terminalLayout.layoutEpoch,
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
previousRootMeasurements.current = {
|
|
976
|
+
composerRows,
|
|
977
|
+
cols: terminalLayout.cols,
|
|
978
|
+
rows: terminalLayout.rows,
|
|
979
|
+
layoutEpoch: terminalLayout.layoutEpoch,
|
|
980
|
+
};
|
|
981
|
+
}, [composerRows, terminalLayout.cols, terminalLayout.layoutEpoch, terminalLayout.rows]);
|
|
982
|
+
|
|
983
|
+
const backendProvider: BackendProvider = useMemo(() => getBackendProvider(backend), [backend]);
|
|
984
|
+
const provider: BackendProvider = useMemo(() => {
|
|
985
|
+
if (activeProviderRoute.providerId === "openai") {
|
|
986
|
+
return backendProvider;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
const routeRuntime = getProviderRuntime(activeProviderRoute.providerId);
|
|
990
|
+
return {
|
|
991
|
+
id: backend,
|
|
992
|
+
label: routeRuntime.label,
|
|
993
|
+
description: routeRuntime.routeStatus,
|
|
994
|
+
authState: routeRuntime.routeAvailable ? "delegated" : "coming-soon",
|
|
995
|
+
authLabel: routeRuntime.routeAvailable ? "Configured" : "Not configured",
|
|
996
|
+
statusMessage: routeRuntime.routeStatus,
|
|
997
|
+
supportsModels: (candidateModel) => candidateModel === activeProviderRoute.modelId,
|
|
998
|
+
run: routeRuntime.run
|
|
999
|
+
? (prompt, options, handlers) => {
|
|
1000
|
+
const geminiCommandPath = activeProviderRoute.providerId === "google"
|
|
1001
|
+
? providerWorkspaceConfig.providers?.google?.geminiCommandPath ?? options.runtime.geminiCommandPath
|
|
1002
|
+
: options.runtime.geminiCommandPath;
|
|
1003
|
+
return routeRuntime.run?.({
|
|
1004
|
+
prompt,
|
|
1005
|
+
route: activeProviderRoute,
|
|
1006
|
+
runtime: geminiCommandPath ? { ...options.runtime, geminiCommandPath } : options.runtime,
|
|
1007
|
+
workspaceRoot: options.workspaceRoot,
|
|
1008
|
+
projectInstructions: options.projectInstructions,
|
|
1009
|
+
localConfig: activeProviderRoute.providerId === "local"
|
|
1010
|
+
? providerWorkspaceConfig.providers?.local
|
|
1011
|
+
: undefined,
|
|
1012
|
+
}, handlers) ?? (() => undefined);
|
|
1013
|
+
}
|
|
1014
|
+
: undefined,
|
|
1015
|
+
};
|
|
1016
|
+
}, [activeProviderRoute, backend, backendProvider, providerWorkspaceConfig.providers]);
|
|
1017
|
+
|
|
1018
|
+
const getInputDebugSnapshot = useCallback((extra: Record<string, unknown> = {}) => {
|
|
1019
|
+
const currentScreen = screenRef.current;
|
|
1020
|
+
const currentBusy = busyRef.current;
|
|
1021
|
+
const currentModelLoading = modelCapabilitiesBusyRef.current;
|
|
1022
|
+
|
|
1023
|
+
return {
|
|
1024
|
+
screen: currentScreen,
|
|
1025
|
+
mode: intendedInputModeRef.current,
|
|
1026
|
+
modelPickerOpen: currentScreen === "model-picker",
|
|
1027
|
+
composerEnabled: currentScreen === "main" && !currentBusy,
|
|
1028
|
+
inputLocked: currentBusy,
|
|
1029
|
+
busy: currentBusy,
|
|
1030
|
+
modelLoading: currentModelLoading,
|
|
1031
|
+
modelSelection: modelSelectionInFlightRef.current,
|
|
1032
|
+
focusTarget: intendedFocusTargetRef.current,
|
|
1033
|
+
stdin: getStdinDebugState(stdin),
|
|
1034
|
+
...extra,
|
|
1035
|
+
};
|
|
1036
|
+
}, [stdin]);
|
|
1037
|
+
|
|
1038
|
+
useEffect(() => {
|
|
1039
|
+
baseRuntimeConfigRef.current = baseLayeredConfig.runtime;
|
|
1040
|
+
}, [baseLayeredConfig.runtime]);
|
|
1041
|
+
|
|
1042
|
+
useEffect(() => {
|
|
1043
|
+
saveSettings({
|
|
1044
|
+
ui: {
|
|
1045
|
+
layoutStyle: initialSettings.current.ui.layoutStyle,
|
|
1046
|
+
theme: themeSelection.committedTheme,
|
|
1047
|
+
workspaceDisplayMode,
|
|
1048
|
+
terminalTitleMode,
|
|
1049
|
+
showBusyLoader,
|
|
1050
|
+
terminalMouseMode,
|
|
1051
|
+
customTheme,
|
|
1052
|
+
},
|
|
1053
|
+
auth: {
|
|
1054
|
+
preference: authPreference,
|
|
1055
|
+
},
|
|
1056
|
+
header: headerConfig,
|
|
1057
|
+
});
|
|
1058
|
+
}, [authPreference, customTheme, showBusyLoader, terminalMouseMode, terminalTitleMode, themeSelection.committedTheme, workspaceDisplayMode]);
|
|
1059
|
+
|
|
1060
|
+
useEffect(() => {
|
|
1061
|
+
return () => {
|
|
1062
|
+
isMountedRef.current = false;
|
|
1063
|
+
cleanupRef.current?.();
|
|
1064
|
+
if (themePreviewTimerRef.current) {
|
|
1065
|
+
clearTimeout(themePreviewTimerRef.current);
|
|
1066
|
+
themePreviewTimerRef.current = null;
|
|
1067
|
+
}
|
|
1068
|
+
};
|
|
1069
|
+
}, []);
|
|
1070
|
+
|
|
1071
|
+
useEffect(() => {
|
|
1072
|
+
if (screen === "theme-picker") {
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
if (themePreviewTimerRef.current) {
|
|
1077
|
+
clearTimeout(themePreviewTimerRef.current);
|
|
1078
|
+
themePreviewTimerRef.current = null;
|
|
1079
|
+
}
|
|
1080
|
+
}, [screen]);
|
|
1081
|
+
|
|
1082
|
+
useEffect(() => {
|
|
1083
|
+
const previousScreen = previousScreenRef.current;
|
|
1084
|
+
if (shouldBumpComposerInstance(previousScreen, screen)) {
|
|
1085
|
+
setComposerInstanceKey((currentKey) => currentKey + 1);
|
|
1086
|
+
}
|
|
1087
|
+
previousScreenRef.current = screen;
|
|
1088
|
+
}, [screen]);
|
|
1089
|
+
|
|
1090
|
+
useEffect(() => {
|
|
1091
|
+
const focusTarget = getFocusTargetForScreen(screen);
|
|
1092
|
+
intendedFocusTargetRef.current = focusTarget;
|
|
1093
|
+
traceInputDebug("focus_route", getInputDebugSnapshot({ focusTarget }));
|
|
1094
|
+
focusManager.focus(focusTarget);
|
|
1095
|
+
}, [composerInstanceKey, focusManager, getInputDebugSnapshot, screen]);
|
|
1096
|
+
|
|
1097
|
+
useEffect(() => {
|
|
1098
|
+
if (screen !== "main") return;
|
|
1099
|
+
|
|
1100
|
+
if (planFlow.kind === "awaiting_action") {
|
|
1101
|
+
intendedFocusTargetRef.current = FOCUS_IDS.composer;
|
|
1102
|
+
focusManager.focus(FOCUS_IDS.composer);
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
if (planFlow.kind === "collecting_feedback") {
|
|
1107
|
+
intendedFocusTargetRef.current = FOCUS_IDS.composer;
|
|
1108
|
+
focusManager.focus(FOCUS_IDS.composer);
|
|
1109
|
+
}
|
|
1110
|
+
}, [focusManager, planFlow.kind, screen]);
|
|
1111
|
+
|
|
1112
|
+
useEffect(() => {
|
|
1113
|
+
if (screen === "model-picker" || intendedInputModeRef.current !== "model-picker") {
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
traceInputDebug("safety_recovery", getInputDebugSnapshot({
|
|
1118
|
+
reason: "model-picker-closed-with-stale-input-mode",
|
|
1119
|
+
restoredMode: "chat/input",
|
|
1120
|
+
restoredFocusTarget: FOCUS_IDS.composer,
|
|
1121
|
+
}));
|
|
1122
|
+
intendedInputModeRef.current = "chat/input";
|
|
1123
|
+
intendedFocusTargetRef.current = FOCUS_IDS.composer;
|
|
1124
|
+
focusManager.focus(FOCUS_IDS.composer);
|
|
1125
|
+
}, [focusManager, getInputDebugSnapshot, screen]);
|
|
1126
|
+
|
|
1127
|
+
const returnToChatMode = useCallback((reason = "unknown") => {
|
|
1128
|
+
intendedInputModeRef.current = "chat/input";
|
|
1129
|
+
intendedFocusTargetRef.current = FOCUS_IDS.composer;
|
|
1130
|
+
traceInputDebug("model_picker_close", getInputDebugSnapshot({
|
|
1131
|
+
reason,
|
|
1132
|
+
restoredMode: "chat/input",
|
|
1133
|
+
restoredModelPickerOpen: false,
|
|
1134
|
+
restoredComposerEnabled: true,
|
|
1135
|
+
restoredInputLocked: false,
|
|
1136
|
+
restoredFocusTarget: FOCUS_IDS.composer,
|
|
1137
|
+
}));
|
|
1138
|
+
setScreen("main");
|
|
1139
|
+
focusManager.focus(FOCUS_IDS.composer);
|
|
1140
|
+
}, [focusManager, getInputDebugSnapshot]);
|
|
1141
|
+
|
|
1142
|
+
const appendStaticEvent = useCallback((event: TimelineEvent) => {
|
|
1143
|
+
dispatchSession({ type: "APPEND_STATIC_EVENT", event });
|
|
1144
|
+
}, [dispatchSession]);
|
|
1145
|
+
|
|
1146
|
+
const appendSystemEvent = useCallback((title: string, content: string) => {
|
|
1147
|
+
const safeTitle = sanitizeTerminalOutput(title);
|
|
1148
|
+
const safeContent = sanitizeTerminalOutput(content, { preserveTabs: false, tabSize: 2 });
|
|
1149
|
+
appendStaticEvent({
|
|
1150
|
+
id: createEventId(),
|
|
1151
|
+
type: "system",
|
|
1152
|
+
createdAt: Date.now(),
|
|
1153
|
+
title: safeTitle,
|
|
1154
|
+
content: safeContent,
|
|
1155
|
+
});
|
|
1156
|
+
}, [appendStaticEvent]);
|
|
1157
|
+
|
|
1158
|
+
const appendErrorEvent = useCallback((title: string, content: string) => {
|
|
1159
|
+
const safeTitle = sanitizeTerminalOutput(title);
|
|
1160
|
+
const safeContent = sanitizeTerminalOutput(content, { preserveTabs: false, tabSize: 2 });
|
|
1161
|
+
appendStaticEvent({
|
|
1162
|
+
id: createEventId(),
|
|
1163
|
+
type: "error",
|
|
1164
|
+
createdAt: Date.now(),
|
|
1165
|
+
title: safeTitle,
|
|
1166
|
+
content: safeContent,
|
|
1167
|
+
});
|
|
1168
|
+
}, [appendStaticEvent]);
|
|
1169
|
+
|
|
1170
|
+
useEffect(() => {
|
|
1171
|
+
const notice = providerWorkspaceConfig.migrationNotice;
|
|
1172
|
+
if (!notice || providerMigrationNoticeShownRef.current) return;
|
|
1173
|
+
providerMigrationNoticeShownRef.current = true;
|
|
1174
|
+
const providerLabel = findProvider(providerRegistry, notice.revertedProviderId)?.displayName ?? "OpenAI";
|
|
1175
|
+
appendSystemEvent(
|
|
1176
|
+
"Provider migrated",
|
|
1177
|
+
`Antigravity provider is no longer supported. Reverted to ${providerLabel}.`,
|
|
1178
|
+
);
|
|
1179
|
+
}, [appendSystemEvent, providerRegistry, providerWorkspaceConfig.migrationNotice]);
|
|
1180
|
+
|
|
1181
|
+
useEffect(() => {
|
|
1182
|
+
if (projectInstructionsLoad.status === "loaded") {
|
|
1183
|
+
traceInputDebug("project_instructions_loaded", {
|
|
1184
|
+
path: projectInstructionsLoad.instructions.path,
|
|
1185
|
+
content: projectInstructionsLoad.instructions.content,
|
|
1186
|
+
});
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
if (projectInstructionsLoad.status === "error") {
|
|
1191
|
+
appendErrorEvent(
|
|
1192
|
+
"Project instructions",
|
|
1193
|
+
`Could not read ${projectInstructionsLoad.path}: ${projectInstructionsLoad.message}`,
|
|
1194
|
+
);
|
|
1195
|
+
}
|
|
1196
|
+
}, [appendErrorEvent, projectInstructionsLoad]);
|
|
1197
|
+
|
|
1198
|
+
const refreshModelCapabilities = useCallback((forceRefresh = false, announce = false): Promise<CodexModelCapabilities> => {
|
|
1199
|
+
// Single-flight: concurrent requests share the same in-flight discovery
|
|
1200
|
+
// promise so we never spawn a duplicate discovery job or emit duplicate
|
|
1201
|
+
// transcript messages.
|
|
1202
|
+
if (modelDiscoveryInFlightRef.current && !forceRefresh) {
|
|
1203
|
+
traceInputDebug("model_loading_inflight", getInputDebugSnapshot({ forceRefresh, announce }));
|
|
1204
|
+
if (announce) {
|
|
1205
|
+
modelDiscoveryAnnounceRef.current = true;
|
|
1206
|
+
}
|
|
1207
|
+
return modelDiscoveryInFlightRef.current;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
if (announce) {
|
|
1211
|
+
modelDiscoveryAnnounceRef.current = true;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
setModelCapabilitiesBusy(true);
|
|
1215
|
+
traceInputDebug("model_loading_start", getInputDebugSnapshot({ forceRefresh, announce }));
|
|
1216
|
+
const promise = (async () => {
|
|
1217
|
+
try {
|
|
1218
|
+
const capabilities = await getCodexModelCapabilities({ forceRefresh });
|
|
1219
|
+
setModelCapabilities(capabilities);
|
|
1220
|
+
traceInputDebug("model_loading_success", getInputDebugSnapshot({
|
|
1221
|
+
status: capabilities.status,
|
|
1222
|
+
source: capabilities.source,
|
|
1223
|
+
modelCount: getSelectableModelCapabilities(capabilities).length,
|
|
1224
|
+
}));
|
|
1225
|
+
if (capabilities.status === "fallback") {
|
|
1226
|
+
traceInputDebug("model_loading_failure", getInputDebugSnapshot({
|
|
1227
|
+
status: capabilities.status,
|
|
1228
|
+
error: capabilities.error,
|
|
1229
|
+
}));
|
|
1230
|
+
}
|
|
1231
|
+
if (modelDiscoveryAnnounceRef.current) {
|
|
1232
|
+
const modelCount = getSelectableModelCapabilities(capabilities).length;
|
|
1233
|
+
const source = capabilities.status === "ready" ? "Codex runtime" : "fallback compatibility list";
|
|
1234
|
+
appendSystemEvent("Model discovery", `Loaded ${modelCount} models from ${source}.`);
|
|
1235
|
+
}
|
|
1236
|
+
return capabilities;
|
|
1237
|
+
} catch (error) {
|
|
1238
|
+
const fallback = createFallbackModelCapabilities(error);
|
|
1239
|
+
setModelCapabilities(fallback);
|
|
1240
|
+
traceInputDebug("model_loading_failure", getInputDebugSnapshot({
|
|
1241
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1242
|
+
}));
|
|
1243
|
+
if (modelDiscoveryAnnounceRef.current) {
|
|
1244
|
+
appendErrorEvent("Model discovery failed", fallback.error ?? "Unable to discover Codex models.");
|
|
1245
|
+
}
|
|
1246
|
+
return fallback;
|
|
1247
|
+
} finally {
|
|
1248
|
+
setModelCapabilitiesBusy(false);
|
|
1249
|
+
modelDiscoveryInFlightRef.current = null;
|
|
1250
|
+
modelDiscoveryAnnounceRef.current = false;
|
|
1251
|
+
traceInputDebug("model_loading_finished", getInputDebugSnapshot({ forceRefresh, announce }));
|
|
1252
|
+
}
|
|
1253
|
+
})();
|
|
1254
|
+
|
|
1255
|
+
modelDiscoveryInFlightRef.current = promise;
|
|
1256
|
+
return promise;
|
|
1257
|
+
}, [appendErrorEvent, appendSystemEvent, getInputDebugSnapshot]);
|
|
1258
|
+
|
|
1259
|
+
const setRuntimeUnauthenticated = useCallback((summary: string) => {
|
|
1260
|
+
setAuthStatus({
|
|
1261
|
+
state: "unauthenticated",
|
|
1262
|
+
checkedAt: Date.now(),
|
|
1263
|
+
rawSummary: summary,
|
|
1264
|
+
recommendedAction: "Run `codex login` and retry.",
|
|
1265
|
+
});
|
|
1266
|
+
}, []);
|
|
1267
|
+
|
|
1268
|
+
const refreshAuthStatus = useCallback(async (announce: boolean) => {
|
|
1269
|
+
setAuthStatusBusy(true);
|
|
1270
|
+
setAuthStatus((prev) => ({ ...prev, state: "checking" }));
|
|
1271
|
+
|
|
1272
|
+
try {
|
|
1273
|
+
const result = await probeCodexAuthStatus();
|
|
1274
|
+
setAuthStatus(result);
|
|
1275
|
+
if (announce) {
|
|
1276
|
+
appendSystemEvent("Auth status", getAuthStatusMessage(result));
|
|
1277
|
+
}
|
|
1278
|
+
} catch (error) {
|
|
1279
|
+
const message = error instanceof Error ? error.message : "Unknown auth probe failure";
|
|
1280
|
+
const fallback: CodexAuthProbeResult = {
|
|
1281
|
+
state: "unknown",
|
|
1282
|
+
checkedAt: Date.now(),
|
|
1283
|
+
rawSummary: message,
|
|
1284
|
+
recommendedAction: "Run `codex login` manually, then retry /auth status.",
|
|
1285
|
+
};
|
|
1286
|
+
setAuthStatus(fallback);
|
|
1287
|
+
if (announce) {
|
|
1288
|
+
appendErrorEvent("Auth status probe failed", message);
|
|
1289
|
+
}
|
|
1290
|
+
} finally {
|
|
1291
|
+
setAuthStatusBusy(false);
|
|
1292
|
+
}
|
|
1293
|
+
}, [appendErrorEvent, appendSystemEvent]);
|
|
1294
|
+
|
|
1295
|
+
useEffect(() => {
|
|
1296
|
+
void refreshAuthStatus(false);
|
|
1297
|
+
}, []);
|
|
1298
|
+
|
|
1299
|
+
// Probe Anthropic/Claude Code CLI auth at startup so the provider picker
|
|
1300
|
+
// shows the correct route availability without requiring manual activation.
|
|
1301
|
+
useEffect(() => {
|
|
1302
|
+
void (async () => {
|
|
1303
|
+
try {
|
|
1304
|
+
const result = await validateAnthropicRoute({ cwd: workspaceRoot, configuredPath: providerWorkspaceConfig.providers?.anthropic?.claudeCommandPath });
|
|
1305
|
+
if (result.diagnostics) {
|
|
1306
|
+
providerDiagnosticsRef.current["anthropic"] = result.diagnostics as Record<string, string | number | boolean | null>;
|
|
1307
|
+
}
|
|
1308
|
+
if (result.status !== "ready") {
|
|
1309
|
+
providerRouteErrorsRef.current["anthropic"] = result.message ?? ANTHROPIC_ROUTE_SETUP_MESSAGE;
|
|
1310
|
+
} else {
|
|
1311
|
+
delete providerRouteErrorsRef.current["anthropic"];
|
|
1312
|
+
}
|
|
1313
|
+
} catch {
|
|
1314
|
+
// Best-effort probe — failures are surfaced only when the user activates the route.
|
|
1315
|
+
}
|
|
1316
|
+
setRegistryNonce((n) => n + 1);
|
|
1317
|
+
})();
|
|
1318
|
+
// workspaceRoot is stable for the session lifetime; this runs exactly once.
|
|
1319
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1320
|
+
}, [workspaceRoot]);
|
|
1321
|
+
|
|
1322
|
+
// Probe local OpenAI-compatible servers such as LM Studio at startup so the
|
|
1323
|
+
// provider picker can show actual availability and loaded model IDs.
|
|
1324
|
+
useEffect(() => {
|
|
1325
|
+
void (async () => {
|
|
1326
|
+
try {
|
|
1327
|
+
const result = await checkLocalProvider({ override: providerWorkspaceConfig.providers?.local });
|
|
1328
|
+
if (result.diagnostics) {
|
|
1329
|
+
providerDiagnosticsRef.current["local"] = result.diagnostics as Record<string, string | number | boolean | null>;
|
|
1330
|
+
}
|
|
1331
|
+
if (result.status !== "ready") {
|
|
1332
|
+
providerRouteErrorsRef.current["local"] = result.message ?? "Local provider unavailable.";
|
|
1333
|
+
} else {
|
|
1334
|
+
delete providerRouteErrorsRef.current["local"];
|
|
1335
|
+
}
|
|
1336
|
+
} catch {
|
|
1337
|
+
// Best-effort probe — failures are surfaced only when the user activates the route.
|
|
1338
|
+
}
|
|
1339
|
+
setRegistryNonce((n) => n + 1);
|
|
1340
|
+
})();
|
|
1341
|
+
// workspaceRoot is stable for the session lifetime; local provider config is
|
|
1342
|
+
// loaded before this first startup probe.
|
|
1343
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1344
|
+
}, [workspaceRoot]);
|
|
1345
|
+
|
|
1346
|
+
useEffect(() => {
|
|
1347
|
+
const devLaunchNotice = buildDevLaunchNotice(launchContext);
|
|
1348
|
+
if (!devLaunchNotice) return;
|
|
1349
|
+
|
|
1350
|
+
appendSystemEvent("Launch mode", devLaunchNotice);
|
|
1351
|
+
}, [appendSystemEvent, launchContext]);
|
|
1352
|
+
|
|
1353
|
+
// Track clear epoch to suppress stale command result events
|
|
1354
|
+
useEffect(() => {
|
|
1355
|
+
clearEpochRef.current = sessionState.clearEpoch;
|
|
1356
|
+
}, [sessionState.clearEpoch]);
|
|
1357
|
+
|
|
1358
|
+
useEffect(() => {
|
|
1359
|
+
externalCliStatusRef.current = sessionState.externalCliStatus;
|
|
1360
|
+
}, [sessionState.externalCliStatus]);
|
|
1361
|
+
|
|
1362
|
+
const reloadBaseLayeredConfig = useCallback(() => {
|
|
1363
|
+
const nextConfig = resolveLayeredConfig({ workspaceRoot, launchArgs });
|
|
1364
|
+
baseRuntimeConfigRef.current = nextConfig.runtime;
|
|
1365
|
+
setBaseLayeredConfig(nextConfig);
|
|
1366
|
+
return nextConfig;
|
|
1367
|
+
}, [launchArgs, workspaceRoot]);
|
|
1368
|
+
|
|
1369
|
+
const updateRuntimeConfig = useCallback((updater: (current: RuntimeConfig) => RuntimeConfig) => {
|
|
1370
|
+
setSessionRuntimeOverride((currentPatch) => {
|
|
1371
|
+
const baseRuntime = baseRuntimeConfigRef.current;
|
|
1372
|
+
const currentRuntime = mergeRuntimeConfig(baseRuntime, currentPatch);
|
|
1373
|
+
const nextRuntime = updater(currentRuntime);
|
|
1374
|
+
return diffRuntimeConfig(baseRuntime, nextRuntime);
|
|
1375
|
+
});
|
|
1376
|
+
}, []);
|
|
1377
|
+
|
|
1378
|
+
const updateRuntimePolicy = useCallback((updater: (current: RuntimeConfig["policy"]) => RuntimeConfig["policy"]) => {
|
|
1379
|
+
updateRuntimeConfig((current) => ({
|
|
1380
|
+
...current,
|
|
1381
|
+
policy: updater(current.policy),
|
|
1382
|
+
}));
|
|
1383
|
+
}, [updateRuntimeConfig]);
|
|
1384
|
+
|
|
1385
|
+
const persistActiveRoute = useCallback((
|
|
1386
|
+
providerId: ProviderId,
|
|
1387
|
+
nextModel: string,
|
|
1388
|
+
nextReasoning: string,
|
|
1389
|
+
backendKindOverride?: ReturnType<typeof getProviderRuntime>["backendKind"],
|
|
1390
|
+
modelSelection?: import("./core/providerRuntime/types.js").GeminiModelSelection,
|
|
1391
|
+
) => {
|
|
1392
|
+
try {
|
|
1393
|
+
const runtime = getProviderRuntime(providerId);
|
|
1394
|
+
let nextConfig = setProviderActiveRoute(providerWorkspaceConfig, {
|
|
1395
|
+
providerId,
|
|
1396
|
+
modelId: nextModel,
|
|
1397
|
+
backendKind: backendKindOverride ?? runtime.backendKind,
|
|
1398
|
+
reasoning: nextReasoning,
|
|
1399
|
+
modelSelection,
|
|
1400
|
+
});
|
|
1401
|
+
nextConfig = setProviderDefaultReasoning(
|
|
1402
|
+
setProviderDefaultModel(nextConfig, providerId, nextModel),
|
|
1403
|
+
providerId,
|
|
1404
|
+
nextReasoning,
|
|
1405
|
+
);
|
|
1406
|
+
saveProviderWorkspaceConfig(workspaceRoot, nextConfig);
|
|
1407
|
+
setProviderWorkspaceConfig(nextConfig);
|
|
1408
|
+
} catch (error) {
|
|
1409
|
+
const message = error instanceof Error ? error.message : "Unable to save active route.";
|
|
1410
|
+
appendErrorEvent("Route save failed", message);
|
|
1411
|
+
}
|
|
1412
|
+
}, [appendErrorEvent, providerWorkspaceConfig, workspaceRoot]);
|
|
1413
|
+
|
|
1414
|
+
const persistProviderDefaultModelAndReasoning = useCallback((
|
|
1415
|
+
providerId: ProviderId,
|
|
1416
|
+
modelId: string,
|
|
1417
|
+
nextReasoning: string,
|
|
1418
|
+
) => {
|
|
1419
|
+
try {
|
|
1420
|
+
const withModel = setProviderDefaultModel(providerWorkspaceConfig, providerId, modelId);
|
|
1421
|
+
const nextConfig = setProviderDefaultReasoning(withModel, providerId, nextReasoning);
|
|
1422
|
+
saveProviderWorkspaceConfig(workspaceRoot, nextConfig);
|
|
1423
|
+
setProviderWorkspaceConfig(nextConfig);
|
|
1424
|
+
} catch (error) {
|
|
1425
|
+
const message = error instanceof Error ? error.message : "Unable to save provider defaults.";
|
|
1426
|
+
appendErrorEvent("Provider defaults save failed", message);
|
|
1427
|
+
}
|
|
1428
|
+
}, [appendErrorEvent, providerWorkspaceConfig, workspaceRoot]);
|
|
1429
|
+
|
|
1430
|
+
// Auto-correct the runtime model when capabilities load and the configured model is
|
|
1431
|
+
// unavailable. Placed after persistActiveRoute / persistProviderDefaultModelAndReasoning
|
|
1432
|
+
// declarations because the effect calls persistActiveRoute (TDZ-safe from here).
|
|
1433
|
+
useEffect(() => {
|
|
1434
|
+
if (activeProviderRoute.providerId !== "openai") {
|
|
1435
|
+
return;
|
|
1436
|
+
}
|
|
1437
|
+
if (modelCapabilities?.status !== "ready") {
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
const nextModel = getPreferredModelFromCapabilities(modelCapabilities, model);
|
|
1442
|
+
const nextReasoning = normalizeReasoningForModelCapabilities(
|
|
1443
|
+
nextModel,
|
|
1444
|
+
reasoningLevel,
|
|
1445
|
+
modelCapabilities,
|
|
1446
|
+
);
|
|
1447
|
+
|
|
1448
|
+
if (nextModel === model && nextReasoning === reasoningLevel) {
|
|
1449
|
+
return;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
updateRuntimeConfig((current) => ({
|
|
1453
|
+
...current,
|
|
1454
|
+
model: nextModel,
|
|
1455
|
+
reasoningLevel: nextReasoning,
|
|
1456
|
+
}));
|
|
1457
|
+
|
|
1458
|
+
// Persist the corrected model so providers.json stays in sync and the same
|
|
1459
|
+
// correction does not silently re-fire on every restart. Skip persistence
|
|
1460
|
+
// when --model was given on the CLI: that session is intentionally temporary.
|
|
1461
|
+
if (nextModel !== model && !launchArgs.modelOverride) {
|
|
1462
|
+
persistActiveRoute(activeProviderRoute.providerId, nextModel, nextReasoning);
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
if (nextModel !== model) {
|
|
1466
|
+
appendSystemEvent(
|
|
1467
|
+
"Model updated",
|
|
1468
|
+
`Configured model ${model} is unavailable in the detected Codex runtime. Active model is now ${nextModel}.`,
|
|
1469
|
+
);
|
|
1470
|
+
} else if (nextReasoning !== reasoningLevel) {
|
|
1471
|
+
appendSystemEvent(
|
|
1472
|
+
"Reasoning updated",
|
|
1473
|
+
`Reasoning level is now ${formatReasoningLabel(nextReasoning)} for ${nextModel}.`,
|
|
1474
|
+
);
|
|
1475
|
+
}
|
|
1476
|
+
}, [activeProviderRoute.providerId, appendSystemEvent, launchArgs.modelOverride, model, modelCapabilities, persistActiveRoute, reasoningLevel, updateRuntimeConfig]);
|
|
1477
|
+
|
|
1478
|
+
const setBackendWithNotice = useCallback((nextBackend: AvailableBackend) => {
|
|
1479
|
+
const gate = guardConfigMutation("backend", busy);
|
|
1480
|
+
if (!gate.allowed) {
|
|
1481
|
+
appendSystemEvent("Busy", gate.message ?? "Finish the current run before changing the backend.");
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
updateRuntimeConfig((current) => ({
|
|
1486
|
+
...current,
|
|
1487
|
+
provider: nextBackend,
|
|
1488
|
+
}));
|
|
1489
|
+
setScreen("main");
|
|
1490
|
+
appendSystemEvent("Backend updated", `Active backend is now ${formatBackendLabel(nextBackend)}.`);
|
|
1491
|
+
if (nextBackend === "codex-subprocess") {
|
|
1492
|
+
void refreshAuthStatus(false);
|
|
1493
|
+
}
|
|
1494
|
+
}, [appendSystemEvent, busy, refreshAuthStatus, updateRuntimeConfig]);
|
|
1495
|
+
|
|
1496
|
+
const setModeWithNotice = useCallback((nextMode: AvailableMode) => {
|
|
1497
|
+
const gate = guardConfigMutation("mode", busy);
|
|
1498
|
+
if (!gate.allowed) {
|
|
1499
|
+
appendSystemEvent("Busy", gate.message ?? "Finish the current run before changing the mode.");
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
updateRuntimeConfig((current) => ({
|
|
1504
|
+
...current,
|
|
1505
|
+
mode: nextMode,
|
|
1506
|
+
}));
|
|
1507
|
+
setScreen("main");
|
|
1508
|
+
appendSystemEvent("Mode updated", `Execution mode switched to ${formatModeLabel(nextMode)}.`);
|
|
1509
|
+
}, [appendSystemEvent, busy, updateRuntimeConfig]);
|
|
1510
|
+
|
|
1511
|
+
const cycleModeWithNotice = useCallback(() => {
|
|
1512
|
+
setModeWithNotice(getNextMode(mode));
|
|
1513
|
+
}, [mode, setModeWithNotice]);
|
|
1514
|
+
|
|
1515
|
+
const setReasoningWithNotice = useCallback((nextReasoningLevel: ReasoningLevel) => {
|
|
1516
|
+
const gate = guardConfigMutation("reasoning", busy);
|
|
1517
|
+
if (!gate.allowed) {
|
|
1518
|
+
appendSystemEvent("Busy", gate.message ?? "Finish the current run before changing the reasoning level.");
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
const supported = currentModelCapability?.supportedReasoningLevels;
|
|
1523
|
+
if (supported && !supported.some((item) => item.id === nextReasoningLevel)) {
|
|
1524
|
+
appendErrorEvent(
|
|
1525
|
+
"Reasoning unavailable",
|
|
1526
|
+
`${model} does not advertise ${formatReasoningLabel(nextReasoningLevel)} reasoning in the detected Codex runtime.`,
|
|
1527
|
+
);
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
updateRuntimeConfig((current) => ({
|
|
1532
|
+
...current,
|
|
1533
|
+
reasoningLevel: nextReasoningLevel,
|
|
1534
|
+
}));
|
|
1535
|
+
// When --model was given on the CLI the active route's modelId reflects that CLI arg,
|
|
1536
|
+
// not what is stored in providers.json. Persist the stored model so the CLI-only
|
|
1537
|
+
// override is never written permanently; fall back to activeProviderRoute.modelId when
|
|
1538
|
+
// no CLI model is active (normal interactive case).
|
|
1539
|
+
const modelToPersist = launchArgs.modelOverride
|
|
1540
|
+
? (providerWorkspaceConfig.activeRoute?.modelId ?? activeProviderRoute.modelId)
|
|
1541
|
+
: activeProviderRoute.modelId;
|
|
1542
|
+
if (activeProviderRoute.providerId === "openai") {
|
|
1543
|
+
persistProviderDefaultModelAndReasoning("openai", modelToPersist, nextReasoningLevel);
|
|
1544
|
+
} else {
|
|
1545
|
+
persistActiveRoute(
|
|
1546
|
+
activeProviderRoute.providerId,
|
|
1547
|
+
modelToPersist,
|
|
1548
|
+
nextReasoningLevel,
|
|
1549
|
+
activeProviderRoute.backendKind,
|
|
1550
|
+
activeProviderRoute.modelSelection,
|
|
1551
|
+
);
|
|
1552
|
+
}
|
|
1553
|
+
setScreen("main");
|
|
1554
|
+
appendSystemEvent("Reasoning updated", `Reasoning level is now ${formatReasoningLabel(nextReasoningLevel)}.`);
|
|
1555
|
+
refreshTerminalTitle({
|
|
1556
|
+
terminalTitleMode,
|
|
1557
|
+
workspaceName: deriveTerminalTitle(workspaceRoot, "dir"),
|
|
1558
|
+
force: true,
|
|
1559
|
+
});
|
|
1560
|
+
}, [
|
|
1561
|
+
activeProviderRoute.backendKind,
|
|
1562
|
+
activeProviderRoute.modelId,
|
|
1563
|
+
activeProviderRoute.modelSelection,
|
|
1564
|
+
activeProviderRoute.providerId,
|
|
1565
|
+
appendErrorEvent,
|
|
1566
|
+
appendSystemEvent,
|
|
1567
|
+
busy,
|
|
1568
|
+
currentModelCapability,
|
|
1569
|
+
launchArgs.modelOverride,
|
|
1570
|
+
model,
|
|
1571
|
+
persistActiveRoute,
|
|
1572
|
+
persistProviderDefaultModelAndReasoning,
|
|
1573
|
+
providerWorkspaceConfig.activeRoute,
|
|
1574
|
+
terminalTitleMode,
|
|
1575
|
+
updateRuntimeConfig,
|
|
1576
|
+
workspaceRoot,
|
|
1577
|
+
]);
|
|
1578
|
+
|
|
1579
|
+
const setPlanModeWithNotice = useCallback((nextEnabled: boolean) => {
|
|
1580
|
+
const gate = guardConfigMutation("mode", busy);
|
|
1581
|
+
if (!gate.allowed) {
|
|
1582
|
+
appendSystemEvent("Busy", gate.message ?? "Finish the current run before changing plan mode.");
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
updateRuntimeConfig((current) => ({
|
|
1587
|
+
...current,
|
|
1588
|
+
planMode: nextEnabled,
|
|
1589
|
+
}));
|
|
1590
|
+
if (!nextEnabled) {
|
|
1591
|
+
setPlanFlow(resetPlanFlow());
|
|
1592
|
+
}
|
|
1593
|
+
appendSystemEvent("Plan mode", `Plan mode ${nextEnabled ? "enabled" : "disabled"}.`);
|
|
1594
|
+
}, [appendSystemEvent, busy, updateRuntimeConfig]);
|
|
1595
|
+
|
|
1596
|
+
const togglePlanModeWithNotice = useCallback(() => {
|
|
1597
|
+
setPlanModeWithNotice(!planMode);
|
|
1598
|
+
}, [planMode, setPlanModeWithNotice]);
|
|
1599
|
+
|
|
1600
|
+
const setModelWithNotice = useCallback(async (nextModel: AvailableModel) => {
|
|
1601
|
+
const gate = guardConfigMutation("model", busy);
|
|
1602
|
+
if (!gate.allowed) {
|
|
1603
|
+
traceInputDebug("model_selection_blocked", getInputDebugSnapshot({
|
|
1604
|
+
handler: "setModelWithNotice",
|
|
1605
|
+
model: nextModel,
|
|
1606
|
+
reason: "busy",
|
|
1607
|
+
}));
|
|
1608
|
+
appendSystemEvent("Busy", gate.message ?? "Finish the current run before changing the model.");
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
modelSelectionInFlightRef.current = true;
|
|
1613
|
+
traceInputDebug("model_selection_app_start", getInputDebugSnapshot({
|
|
1614
|
+
handler: "setModelWithNotice",
|
|
1615
|
+
model: nextModel,
|
|
1616
|
+
}));
|
|
1617
|
+
|
|
1618
|
+
try {
|
|
1619
|
+
const routeProviderId = activeProviderRoute.providerId;
|
|
1620
|
+
const normalizedReasoning = normalizeReasoningForModelCapabilities(nextModel, reasoningLevel, activeRouteModelCapabilities);
|
|
1621
|
+
const validation = await validateProviderRouteActivation({
|
|
1622
|
+
route: {
|
|
1623
|
+
providerId: routeProviderId,
|
|
1624
|
+
modelId: nextModel,
|
|
1625
|
+
backendKind: getProviderRuntime(routeProviderId).backendKind,
|
|
1626
|
+
reasoning: normalizedReasoning,
|
|
1627
|
+
},
|
|
1628
|
+
workspaceRoot,
|
|
1629
|
+
geminiCommandPath: providerWorkspaceConfig.providers?.google?.geminiCommandPath ?? runtimeConfig.geminiCommandPath,
|
|
1630
|
+
claudeCommandPath: providerWorkspaceConfig.providers?.anthropic?.claudeCommandPath,
|
|
1631
|
+
localConfig: providerWorkspaceConfig.providers?.local,
|
|
1632
|
+
});
|
|
1633
|
+
if (validation.status !== "ready") {
|
|
1634
|
+
appendSystemEvent(
|
|
1635
|
+
"Provider route unavailable",
|
|
1636
|
+
`${validation.message ?? getProviderRouteSetupMessage(routeProviderId)} Previous active route remains ${activeRouteProvider?.displayName ?? "OpenAI"} / ${activeProviderRoute.modelId}.`,
|
|
1637
|
+
);
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1640
|
+
updateRuntimeConfig((current) => ({
|
|
1641
|
+
...current,
|
|
1642
|
+
model: nextModel,
|
|
1643
|
+
reasoningLevel: normalizeReasoningForModelCapabilities(nextModel, current.reasoningLevel, activeRouteModelCapabilities),
|
|
1644
|
+
}));
|
|
1645
|
+
persistActiveRoute(routeProviderId, nextModel, normalizedReasoning, validation.backendKind);
|
|
1646
|
+
traceInputDebug("model_selection_app_success", getInputDebugSnapshot({
|
|
1647
|
+
handler: "setModelWithNotice",
|
|
1648
|
+
model: nextModel,
|
|
1649
|
+
}));
|
|
1650
|
+
appendSystemEvent(
|
|
1651
|
+
"Model updated",
|
|
1652
|
+
`Active model is now ${nextModel}. Reasoning set to ${formatReasoningLabel(normalizedReasoning)}.`,
|
|
1653
|
+
);
|
|
1654
|
+
refreshTerminalTitle({
|
|
1655
|
+
terminalTitleMode,
|
|
1656
|
+
workspaceName: deriveTerminalTitle(workspaceRoot, "dir"),
|
|
1657
|
+
force: true,
|
|
1658
|
+
});
|
|
1659
|
+
} catch (error) {
|
|
1660
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1661
|
+
traceInputDebug("model_selection_app_failure", getInputDebugSnapshot({
|
|
1662
|
+
handler: "setModelWithNotice",
|
|
1663
|
+
model: nextModel,
|
|
1664
|
+
error: message,
|
|
1665
|
+
}));
|
|
1666
|
+
appendErrorEvent("Model selection failed", message);
|
|
1667
|
+
} finally {
|
|
1668
|
+
modelSelectionInFlightRef.current = false;
|
|
1669
|
+
returnToChatMode("selection");
|
|
1670
|
+
}
|
|
1671
|
+
}, [activeProviderRoute.modelId, activeProviderRoute.providerId, activeRouteModelCapabilities, activeRouteProvider, appendErrorEvent, appendSystemEvent, busy, getInputDebugSnapshot, persistActiveRoute, providerWorkspaceConfig.providers, reasoningLevel, returnToChatMode, runtimeConfig.geminiCommandPath, updateRuntimeConfig, workspaceRoot]);
|
|
1672
|
+
|
|
1673
|
+
const setModelAndReasoningWithNotice = useCallback(async (
|
|
1674
|
+
nextModel: AvailableModel,
|
|
1675
|
+
nextReasoning: ReasoningLevel,
|
|
1676
|
+
providerId: ProviderId = activeProviderRoute.providerId,
|
|
1677
|
+
geminiSelection?: import("./core/providerRuntime/types.js").GeminiModelSelection,
|
|
1678
|
+
) => {
|
|
1679
|
+
const gate = guardConfigMutation("model", busy);
|
|
1680
|
+
if (!gate.allowed) {
|
|
1681
|
+
traceInputDebug("model_selection_blocked", getInputDebugSnapshot({
|
|
1682
|
+
handler: "setModelAndReasoningWithNotice",
|
|
1683
|
+
model: nextModel,
|
|
1684
|
+
reasoning: nextReasoning,
|
|
1685
|
+
reason: "busy",
|
|
1686
|
+
}));
|
|
1687
|
+
appendSystemEvent("Busy", gate.message ?? "Finish the current run before changing the model.");
|
|
1688
|
+
returnToChatMode("selection-blocked");
|
|
1689
|
+
return;
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
const routeCapabilities = providerId === "openai"
|
|
1693
|
+
? modelCapabilities
|
|
1694
|
+
: providerModelsToCodexCapabilities(discoverProviderModels(providerId).models, nextModel);
|
|
1695
|
+
const selectedCapability = findModelCapability(routeCapabilities, nextModel);
|
|
1696
|
+
const supported = selectedCapability?.supportedReasoningLevels;
|
|
1697
|
+
if (supported && supported.length > 0 && !supported.some((item) => item.id === nextReasoning)) {
|
|
1698
|
+
appendErrorEvent(
|
|
1699
|
+
"Reasoning unavailable",
|
|
1700
|
+
`${nextModel} does not advertise ${formatReasoningLabel(nextReasoning)} reasoning in the detected Codex runtime.`,
|
|
1701
|
+
);
|
|
1702
|
+
returnToChatMode("selection-invalid");
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
modelSelectionInFlightRef.current = true;
|
|
1707
|
+
traceInputDebug("model_selection_app_start", getInputDebugSnapshot({
|
|
1708
|
+
handler: "setModelAndReasoningWithNotice",
|
|
1709
|
+
model: nextModel,
|
|
1710
|
+
reasoning: nextReasoning,
|
|
1711
|
+
}));
|
|
1712
|
+
|
|
1713
|
+
try {
|
|
1714
|
+
const normalizedReasoning = normalizeReasoningForModelCapabilities(nextModel, nextReasoning, routeCapabilities);
|
|
1715
|
+
const releaseTitleGuard = acquireTerminalTitleGuard(500, () => {
|
|
1716
|
+
forceRefreshCurrentTerminalTitle("provider_validation_active", true);
|
|
1717
|
+
});
|
|
1718
|
+
let validation;
|
|
1719
|
+
try {
|
|
1720
|
+
validation = await validateProviderRouteActivation({
|
|
1721
|
+
route: {
|
|
1722
|
+
providerId,
|
|
1723
|
+
modelId: nextModel,
|
|
1724
|
+
backendKind: getProviderRuntime(providerId).backendKind,
|
|
1725
|
+
reasoning: normalizedReasoning,
|
|
1726
|
+
modelSelection: geminiSelection,
|
|
1727
|
+
},
|
|
1728
|
+
workspaceRoot,
|
|
1729
|
+
geminiCommandPath: providerWorkspaceConfig.providers?.google?.geminiCommandPath ?? runtimeConfig.geminiCommandPath,
|
|
1730
|
+
claudeCommandPath: providerWorkspaceConfig.providers?.anthropic?.claudeCommandPath,
|
|
1731
|
+
localConfig: providerWorkspaceConfig.providers?.local,
|
|
1732
|
+
});
|
|
1733
|
+
if (validation.diagnostics) {
|
|
1734
|
+
providerDiagnosticsRef.current[providerId] = validation.diagnostics as Record<string, string | number | boolean | null>;
|
|
1735
|
+
}
|
|
1736
|
+
setRegistryNonce((n) => n + 1);
|
|
1737
|
+
} finally {
|
|
1738
|
+
releaseTitleGuard();
|
|
1739
|
+
forceRefreshCurrentTerminalTitle("after_provider_validation", false);
|
|
1740
|
+
}
|
|
1741
|
+
if (validation.status !== "ready") {
|
|
1742
|
+
traceInputDebug("model_selection_app_failure", getInputDebugSnapshot({
|
|
1743
|
+
handler: "setModelAndReasoningWithNotice",
|
|
1744
|
+
model: nextModel,
|
|
1745
|
+
reasoning: nextReasoning,
|
|
1746
|
+
error: validation.message ?? getProviderRouteSetupMessage(providerId),
|
|
1747
|
+
}));
|
|
1748
|
+
const errorMessage = validation.message ?? getProviderRouteSetupMessage(providerId);
|
|
1749
|
+
if (providerRouteErrorsRef.current[providerId] !== errorMessage) {
|
|
1750
|
+
appendSystemEvent(
|
|
1751
|
+
"Provider route unavailable",
|
|
1752
|
+
`${errorMessage} Previous active route remains ${activeRouteProvider?.displayName ?? "OpenAI"} / ${activeProviderRoute.modelId}.`,
|
|
1753
|
+
);
|
|
1754
|
+
providerRouteErrorsRef.current[providerId] = errorMessage;
|
|
1755
|
+
}
|
|
1756
|
+
setPendingRouteProviderId(null);
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
if (providerRouteErrorsRef.current[providerId]) {
|
|
1761
|
+
appendSystemEvent(
|
|
1762
|
+
"Provider route available",
|
|
1763
|
+
validation.message ?? (providerId === "google"
|
|
1764
|
+
? "Google/Gemini is available via Gemini CLI."
|
|
1765
|
+
: providerId === "anthropic"
|
|
1766
|
+
? "Anthropic/Claude is available via Claude Code."
|
|
1767
|
+
: `${getProviderRuntime(providerId).label} is available via ${validation.backendKind}.`),
|
|
1768
|
+
);
|
|
1769
|
+
delete providerRouteErrorsRef.current[providerId];
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
updateRuntimeConfig((current) => ({
|
|
1773
|
+
...current,
|
|
1774
|
+
model: nextModel,
|
|
1775
|
+
reasoningLevel: normalizedReasoning,
|
|
1776
|
+
}));
|
|
1777
|
+
persistActiveRoute(providerId, nextModel, normalizedReasoning, validation.backendKind, geminiSelection);
|
|
1778
|
+
setPendingRouteProviderId(null);
|
|
1779
|
+
traceInputDebug("model_selection_app_success", getInputDebugSnapshot({
|
|
1780
|
+
handler: "setModelAndReasoningWithNotice",
|
|
1781
|
+
model: nextModel,
|
|
1782
|
+
reasoning: normalizedReasoning,
|
|
1783
|
+
}));
|
|
1784
|
+
|
|
1785
|
+
// Route changes are reflected reactively in the BottomComposer metadata row.
|
|
1786
|
+
refreshTerminalTitle({
|
|
1787
|
+
terminalTitleMode,
|
|
1788
|
+
workspaceName: deriveTerminalTitle(workspaceRoot, "dir"),
|
|
1789
|
+
force: true,
|
|
1790
|
+
});
|
|
1791
|
+
} catch (error) {
|
|
1792
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1793
|
+
traceInputDebug("model_selection_app_failure", getInputDebugSnapshot({
|
|
1794
|
+
handler: "setModelAndReasoningWithNotice",
|
|
1795
|
+
model: nextModel,
|
|
1796
|
+
reasoning: nextReasoning,
|
|
1797
|
+
error: message,
|
|
1798
|
+
}));
|
|
1799
|
+
appendErrorEvent("Model selection failed", message);
|
|
1800
|
+
forceRefreshCurrentTerminalTitle("model_selection_failed", false);
|
|
1801
|
+
} finally {
|
|
1802
|
+
modelSelectionInFlightRef.current = false;
|
|
1803
|
+
returnToChatMode("selection");
|
|
1804
|
+
}
|
|
1805
|
+
}, [activeProviderRoute.modelId, activeProviderRoute.providerId, activeRouteProvider, appendErrorEvent, appendSystemEvent, busy, getInputDebugSnapshot, modelCapabilities, persistActiveRoute, providerWorkspaceConfig.providers, returnToChatMode, runtimeConfig.geminiCommandPath, updateRuntimeConfig, workspaceRoot]);
|
|
1806
|
+
|
|
1807
|
+
const setAuthPreferenceWithNotice = useCallback((nextPreference: AuthPreference) => {
|
|
1808
|
+
setAuthPreference(nextPreference);
|
|
1809
|
+
appendSystemEvent("Auth preference updated", `Preference set to ${formatAuthPreferenceLabel(nextPreference)}.`);
|
|
1810
|
+
}, [appendSystemEvent]);
|
|
1811
|
+
|
|
1812
|
+
const applyWorkspaceDisplayMode = useCallback((nextMode: WorkspaceDisplayMode) => {
|
|
1813
|
+
setWorkspaceDisplayMode(nextMode);
|
|
1814
|
+
}, []);
|
|
1815
|
+
|
|
1816
|
+
const setWorkspaceDisplayModeWithNotice = useCallback((nextMode: WorkspaceDisplayMode) => {
|
|
1817
|
+
applyWorkspaceDisplayMode(nextMode);
|
|
1818
|
+
}, [applyWorkspaceDisplayMode]);
|
|
1819
|
+
|
|
1820
|
+
const setTerminalTitleModeWithNotice = useCallback((nextMode: TerminalTitleMode) => {
|
|
1821
|
+
setTerminalTitleMode(nextMode);
|
|
1822
|
+
appendSystemEvent(
|
|
1823
|
+
"Settings",
|
|
1824
|
+
`Terminal title set to ${formatWorkspaceDisplayModeLabel(nextMode)} (${nextMode}).`,
|
|
1825
|
+
);
|
|
1826
|
+
}, [appendSystemEvent]);
|
|
1827
|
+
|
|
1828
|
+
const saveSettingsFromPanel = useCallback((nextSettings: UserSettingValues) => {
|
|
1829
|
+
if (nextSettings.workspaceDisplayMode !== workspaceDisplayMode) {
|
|
1830
|
+
applyWorkspaceDisplayMode(nextSettings.workspaceDisplayMode);
|
|
1831
|
+
}
|
|
1832
|
+
if (nextSettings.terminalTitleMode !== terminalTitleMode) {
|
|
1833
|
+
setTerminalTitleMode(nextSettings.terminalTitleMode);
|
|
1834
|
+
refreshTerminalTitle({
|
|
1835
|
+
terminalTitleMode: nextSettings.terminalTitleMode,
|
|
1836
|
+
workspaceName: deriveTerminalTitle(workspaceRoot, "dir"),
|
|
1837
|
+
force: true,
|
|
1838
|
+
});
|
|
1839
|
+
}
|
|
1840
|
+
const nextShowBusyLoader = parseBusyLoaderSettingValue(nextSettings.showBusyLoader);
|
|
1841
|
+
if (nextShowBusyLoader !== showBusyLoader) {
|
|
1842
|
+
setShowBusyLoader(nextShowBusyLoader);
|
|
1843
|
+
}
|
|
1844
|
+
if (nextSettings.terminalMouseMode !== terminalMouseMode) {
|
|
1845
|
+
setTerminalMouseMode(nextSettings.terminalMouseMode);
|
|
1846
|
+
setMouseOverride(null); // let the newly persisted mode drive mouseCapture
|
|
1847
|
+
}
|
|
1848
|
+
setScreen("main");
|
|
1849
|
+
}, [applyWorkspaceDisplayMode, showBusyLoader, terminalMouseMode, terminalTitleMode, workspaceDisplayMode]);
|
|
1850
|
+
|
|
1851
|
+
const setApprovalPolicyWithNotice = useCallback((nextValue: RuntimeApprovalPolicy) => {
|
|
1852
|
+
const gate = guardConfigMutation("mode", busy);
|
|
1853
|
+
if (!gate.allowed) {
|
|
1854
|
+
appendSystemEvent("Busy", gate.message ?? "Finish the current run before changing runtime policy.");
|
|
1855
|
+
return;
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
updateRuntimePolicy((current) => ({ ...current, approvalPolicy: nextValue }));
|
|
1859
|
+
appendSystemEvent("Runtime policy", `Approval policy set to ${formatApprovalPolicyLabel(nextValue)}.`);
|
|
1860
|
+
}, [appendSystemEvent, busy, updateRuntimePolicy]);
|
|
1861
|
+
|
|
1862
|
+
const setSandboxModeWithNotice = useCallback((nextValue: RuntimeSandboxMode) => {
|
|
1863
|
+
const gate = guardConfigMutation("mode", busy);
|
|
1864
|
+
if (!gate.allowed) {
|
|
1865
|
+
appendSystemEvent("Busy", gate.message ?? "Finish the current run before changing runtime policy.");
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
updateRuntimePolicy((current) => ({ ...current, sandboxMode: nextValue }));
|
|
1870
|
+
appendSystemEvent("Runtime policy", `Sandbox mode set to ${formatSandboxModeLabel(nextValue)}.`);
|
|
1871
|
+
}, [appendSystemEvent, busy, updateRuntimePolicy]);
|
|
1872
|
+
|
|
1873
|
+
const setNetworkAccessWithNotice = useCallback((nextValue: RuntimeNetworkAccess) => {
|
|
1874
|
+
const gate = guardConfigMutation("mode", busy);
|
|
1875
|
+
if (!gate.allowed) {
|
|
1876
|
+
appendSystemEvent("Busy", gate.message ?? "Finish the current run before changing runtime policy.");
|
|
1877
|
+
return;
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
updateRuntimePolicy((current) => ({ ...current, networkAccess: nextValue }));
|
|
1881
|
+
appendSystemEvent("Runtime policy", `Network access set to ${formatNetworkAccessLabel(nextValue)}.`);
|
|
1882
|
+
}, [appendSystemEvent, busy, updateRuntimePolicy]);
|
|
1883
|
+
|
|
1884
|
+
const addWritableRootWithNotice = useCallback((pathValue: string) => {
|
|
1885
|
+
const gate = guardConfigMutation("mode", busy);
|
|
1886
|
+
if (!gate.allowed) {
|
|
1887
|
+
appendSystemEvent("Busy", gate.message ?? "Finish the current run before changing runtime policy.");
|
|
1888
|
+
return;
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
const resolvedPath = resolveWritableRootCommandPath(pathValue, workspaceRoot);
|
|
1892
|
+
updateRuntimeConfig((current) => addWritableRoot(current, resolvedPath));
|
|
1893
|
+
appendSystemEvent("Runtime policy", `Writable root added: ${resolvedPath}.`);
|
|
1894
|
+
}, [appendSystemEvent, busy, updateRuntimeConfig, workspaceRoot]);
|
|
1895
|
+
|
|
1896
|
+
const removeWritableRootWithNotice = useCallback((pathValue: string) => {
|
|
1897
|
+
const gate = guardConfigMutation("mode", busy);
|
|
1898
|
+
if (!gate.allowed) {
|
|
1899
|
+
appendSystemEvent("Busy", gate.message ?? "Finish the current run before changing runtime policy.");
|
|
1900
|
+
return;
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
const resolvedPath = resolveWritableRootCommandPath(pathValue, workspaceRoot);
|
|
1904
|
+
updateRuntimeConfig((current) => removeWritableRoot(current, resolvedPath));
|
|
1905
|
+
appendSystemEvent("Runtime policy", `Writable root removed: ${resolvedPath}.`);
|
|
1906
|
+
}, [appendSystemEvent, busy, updateRuntimeConfig, workspaceRoot]);
|
|
1907
|
+
|
|
1908
|
+
const clearWritableRootsWithNotice = useCallback(() => {
|
|
1909
|
+
const gate = guardConfigMutation("mode", busy);
|
|
1910
|
+
if (!gate.allowed) {
|
|
1911
|
+
appendSystemEvent("Busy", gate.message ?? "Finish the current run before changing runtime policy.");
|
|
1912
|
+
return;
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
updateRuntimeConfig((current) => clearWritableRoots(current));
|
|
1916
|
+
appendSystemEvent("Runtime policy", "Writable roots cleared.");
|
|
1917
|
+
}, [appendSystemEvent, busy, updateRuntimeConfig]);
|
|
1918
|
+
|
|
1919
|
+
const setServiceTierWithNotice = useCallback((nextValue: RuntimeServiceTier) => {
|
|
1920
|
+
const gate = guardConfigMutation("mode", busy);
|
|
1921
|
+
if (!gate.allowed) {
|
|
1922
|
+
appendSystemEvent("Busy", gate.message ?? "Finish the current run before changing runtime policy.");
|
|
1923
|
+
return;
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
updateRuntimePolicy((current) => ({ ...current, serviceTier: nextValue }));
|
|
1927
|
+
appendSystemEvent("Runtime policy", `Service tier set to ${formatServiceTierLabel(nextValue)}.`);
|
|
1928
|
+
}, [appendSystemEvent, busy, updateRuntimePolicy]);
|
|
1929
|
+
|
|
1930
|
+
const setPersonalityWithNotice = useCallback((nextValue: RuntimePersonality) => {
|
|
1931
|
+
const gate = guardConfigMutation("mode", busy);
|
|
1932
|
+
if (!gate.allowed) {
|
|
1933
|
+
appendSystemEvent("Busy", gate.message ?? "Finish the current run before changing runtime policy.");
|
|
1934
|
+
return;
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
updateRuntimePolicy((current) => ({ ...current, personality: nextValue }));
|
|
1938
|
+
appendSystemEvent("Runtime policy", `Personality set to ${formatPersonalityLabel(nextValue)}.`);
|
|
1939
|
+
}, [appendSystemEvent, busy, updateRuntimePolicy]);
|
|
1940
|
+
|
|
1941
|
+
const setProjectTrustWithNotice = useCallback((trusted: boolean) => {
|
|
1942
|
+
const gate = guardConfigMutation("mode", busy);
|
|
1943
|
+
if (!gate.allowed) {
|
|
1944
|
+
appendSystemEvent("Busy", gate.message ?? "Finish the current run before changing project trust.");
|
|
1945
|
+
return;
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
const projectRoot = baseLayeredConfig.diagnostics.projectRoot;
|
|
1949
|
+
setProjectTrust(projectRoot, trusted);
|
|
1950
|
+
reloadBaseLayeredConfig();
|
|
1951
|
+
appendSystemEvent(
|
|
1952
|
+
"Config trust",
|
|
1953
|
+
`${trusted ? "Trusted" : "Untrusted"} project root: ${projectRoot}.`,
|
|
1954
|
+
);
|
|
1955
|
+
}, [appendSystemEvent, baseLayeredConfig.diagnostics.projectRoot, busy, reloadBaseLayeredConfig]);
|
|
1956
|
+
|
|
1957
|
+
const openBackendPicker = useCallback(() => {
|
|
1958
|
+
const gate = guardConfigMutation("backend", busy);
|
|
1959
|
+
if (!gate.allowed) {
|
|
1960
|
+
appendSystemEvent("Busy", gate.message ?? "Finish the current run before changing the backend.");
|
|
1961
|
+
return;
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
setScreen("backend-picker");
|
|
1965
|
+
}, [appendSystemEvent, busy]);
|
|
1966
|
+
|
|
1967
|
+
const openProviderPicker = useCallback(() => {
|
|
1968
|
+
setPendingRouteProviderId(null);
|
|
1969
|
+
const gate = guardConfigMutation("backend", busy);
|
|
1970
|
+
if (!gate.allowed) {
|
|
1971
|
+
appendSystemEvent("Busy", gate.message ?? "Finish the current run before opening providers.");
|
|
1972
|
+
return;
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
setScreen("provider-picker");
|
|
1976
|
+
void checkLocalProvider({ override: providerWorkspaceConfig.providers?.local }).then((result) => {
|
|
1977
|
+
if (!isMountedRef.current) return;
|
|
1978
|
+
if (result.diagnostics) {
|
|
1979
|
+
providerDiagnosticsRef.current["local"] = result.diagnostics as Record<string, string | number | boolean | null>;
|
|
1980
|
+
}
|
|
1981
|
+
if (result.status === "ready") {
|
|
1982
|
+
delete providerRouteErrorsRef.current["local"];
|
|
1983
|
+
} else {
|
|
1984
|
+
providerRouteErrorsRef.current["local"] = result.message ?? "Local provider unavailable.";
|
|
1985
|
+
}
|
|
1986
|
+
setRegistryNonce((n) => n + 1);
|
|
1987
|
+
}).catch(() => undefined);
|
|
1988
|
+
}, [appendSystemEvent, busy, providerWorkspaceConfig.providers]);
|
|
1989
|
+
|
|
1990
|
+
const setWorkspaceDefaultProviderWithNotice = useCallback((providerId: ProviderId) => {
|
|
1991
|
+
const provider = findProvider(providerRegistry, providerId);
|
|
1992
|
+
if (!provider) {
|
|
1993
|
+
appendErrorEvent("Provider unavailable", `Unknown provider: ${providerId}`);
|
|
1994
|
+
return;
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
try {
|
|
1998
|
+
const nextConfig = setProviderWorkspaceDefault(providerWorkspaceConfig, providerId);
|
|
1999
|
+
saveProviderWorkspaceConfig(workspaceRoot, nextConfig);
|
|
2000
|
+
setProviderWorkspaceConfig(nextConfig);
|
|
2001
|
+
setScreen("main");
|
|
2002
|
+
const routeConfigured = isProviderRouteConfigured(providerId);
|
|
2003
|
+
appendSystemEvent(
|
|
2004
|
+
"Provider default updated",
|
|
2005
|
+
provider.routeMode === "in-codexa" && routeConfigured
|
|
2006
|
+
? `${provider.displayName} is now the workspace default provider. Active chat route remains ${activeRouteProvider?.displayName ?? "OpenAI"} / ${model}.`
|
|
2007
|
+
: `${provider.displayName} is set as the workspace default, but in-Codexa routing is not configured yet. Active chat route remains ${activeRouteProvider?.displayName ?? "OpenAI"} / ${model}.`,
|
|
2008
|
+
);
|
|
2009
|
+
} catch (error) {
|
|
2010
|
+
const message = error instanceof Error ? error.message : "Unable to save provider workspace config.";
|
|
2011
|
+
appendErrorEvent("Provider default failed", message);
|
|
2012
|
+
}
|
|
2013
|
+
}, [activeRouteProvider, appendErrorEvent, appendSystemEvent, model, providerRegistry, providerWorkspaceConfig, workspaceRoot]);
|
|
2014
|
+
|
|
2015
|
+
const handleProviderAction = useCallback((providerId: ProviderId, action: ProviderPickerAction) => {
|
|
2016
|
+
if (action === "cancel") {
|
|
2017
|
+
setScreen("main");
|
|
2018
|
+
return;
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
if (action === "set-default") {
|
|
2022
|
+
setWorkspaceDefaultProviderWithNotice(providerId);
|
|
2023
|
+
return;
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
const provider = findProvider(providerRegistry, providerId);
|
|
2027
|
+
if (!provider) {
|
|
2028
|
+
setScreen("main");
|
|
2029
|
+
appendErrorEvent("Provider unavailable", `Unknown provider: ${providerId}`);
|
|
2030
|
+
return;
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
if (action === "use-in-codexa") {
|
|
2034
|
+
if (!isProviderRoutableInCodexa(providerId)) {
|
|
2035
|
+
const message = provider.isDefault
|
|
2036
|
+
? `${provider.displayName} is set as the workspace default, but in-Codexa routing is not configured yet.`
|
|
2037
|
+
: `${provider.displayName} in-Codexa routing is not configured yet.`;
|
|
2038
|
+
if (providerRouteErrorsRef.current[providerId] !== message) {
|
|
2039
|
+
appendSystemEvent("Provider route unavailable", message);
|
|
2040
|
+
providerRouteErrorsRef.current[providerId] = message;
|
|
2041
|
+
}
|
|
2042
|
+
return;
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
if (providerId === "local") {
|
|
2046
|
+
appendSystemEvent("Model discovery", "Refreshing LM Studio metadata...");
|
|
2047
|
+
void checkLocalProvider({ override: providerWorkspaceConfig.providers?.local }).then((validation) => {
|
|
2048
|
+
if (!isMountedRef.current) return;
|
|
2049
|
+
if (validation.diagnostics) {
|
|
2050
|
+
providerDiagnosticsRef.current["local"] = validation.diagnostics as Record<string, string | number | boolean | null>;
|
|
2051
|
+
}
|
|
2052
|
+
if (validation.status !== "ready") {
|
|
2053
|
+
const message = validation.message ?? "Local provider unavailable.";
|
|
2054
|
+
providerRouteErrorsRef.current["local"] = message;
|
|
2055
|
+
appendSystemEvent("Provider route unavailable", message);
|
|
2056
|
+
setRegistryNonce((n) => n + 1);
|
|
2057
|
+
return;
|
|
2058
|
+
}
|
|
2059
|
+
delete providerRouteErrorsRef.current["local"];
|
|
2060
|
+
const selectedModel = typeof validation.diagnostics?.selectedModel === "string" && validation.diagnostics.selectedModel.trim()
|
|
2061
|
+
? validation.diagnostics.selectedModel.trim()
|
|
2062
|
+
: provider.currentModel;
|
|
2063
|
+
setRegistryNonce((n) => n + 1);
|
|
2064
|
+
intendedInputModeRef.current = "chat/input";
|
|
2065
|
+
intendedFocusTargetRef.current = FOCUS_IDS.composer;
|
|
2066
|
+
setScreen("main");
|
|
2067
|
+
void setModelAndReasoningWithNotice(
|
|
2068
|
+
selectedModel as AvailableModel,
|
|
2069
|
+
(providerWorkspaceConfig.providers?.local?.currentReasoning ?? activeProviderRoute.reasoning ?? reasoningLevel) as ReasoningLevel,
|
|
2070
|
+
"local",
|
|
2071
|
+
);
|
|
2072
|
+
}).catch((error) => {
|
|
2073
|
+
if (!isMountedRef.current) return;
|
|
2074
|
+
appendErrorEvent("Local refresh failed", error instanceof Error ? error.message : String(error));
|
|
2075
|
+
});
|
|
2076
|
+
return;
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
const workspaceProviderConfig = providerWorkspaceConfig.providers?.[providerId];
|
|
2080
|
+
const activeRoute = providerWorkspaceConfig.activeRoute;
|
|
2081
|
+
const isCurrentActive = activeRoute?.providerId === providerId;
|
|
2082
|
+
|
|
2083
|
+
// A "real" model is one that isn't a generic placeholder label from registry.ts
|
|
2084
|
+
const isRealModel = provider.currentModel &&
|
|
2085
|
+
!provider.currentModel.endsWith("default") &&
|
|
2086
|
+
provider.currentModel !== "Google default";
|
|
2087
|
+
|
|
2088
|
+
const providerReasoning = workspaceProviderConfig?.currentReasoning
|
|
2089
|
+
?? (isCurrentActive ? activeRoute?.reasoning : undefined)
|
|
2090
|
+
?? reasoningLevel;
|
|
2091
|
+
|
|
2092
|
+
if (isRealModel || isCurrentActive) {
|
|
2093
|
+
let geminiSelection: import("./core/providerRuntime/types.js").GeminiModelSelection | undefined;
|
|
2094
|
+
if (providerId === "google") {
|
|
2095
|
+
if (isCurrentActive && activeRoute?.modelSelection) {
|
|
2096
|
+
geminiSelection = activeRoute.modelSelection;
|
|
2097
|
+
} else if (workspaceProviderConfig?.currentModel) {
|
|
2098
|
+
geminiSelection = { kind: "manual", modelId: workspaceProviderConfig.currentModel };
|
|
2099
|
+
} else {
|
|
2100
|
+
geminiSelection = { kind: "auto", family: "gemini-3" };
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
intendedInputModeRef.current = "chat/input";
|
|
2105
|
+
intendedFocusTargetRef.current = FOCUS_IDS.composer;
|
|
2106
|
+
setScreen("main");
|
|
2107
|
+
void setModelAndReasoningWithNotice(
|
|
2108
|
+
(workspaceProviderConfig?.currentModel ?? provider.currentModel) as AvailableModel,
|
|
2109
|
+
providerReasoning as ReasoningLevel,
|
|
2110
|
+
providerId,
|
|
2111
|
+
geminiSelection,
|
|
2112
|
+
);
|
|
2113
|
+
return;
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
// No clear model to use, open the picker.
|
|
2117
|
+
setPendingRouteProviderId(providerId);
|
|
2118
|
+
intendedInputModeRef.current = "model-picker";
|
|
2119
|
+
intendedFocusTargetRef.current = FOCUS_IDS.modelPicker;
|
|
2120
|
+
setScreen("model-picker");
|
|
2121
|
+
return;
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
if (action === "select-model") {
|
|
2125
|
+
if (providerId === "openai" && !modelCapabilities) {
|
|
2126
|
+
void refreshModelCapabilities(false, false);
|
|
2127
|
+
}
|
|
2128
|
+
setPendingRouteProviderId(providerId);
|
|
2129
|
+
intendedInputModeRef.current = "model-picker";
|
|
2130
|
+
intendedFocusTargetRef.current = FOCUS_IDS.modelPicker;
|
|
2131
|
+
setScreen("model-picker");
|
|
2132
|
+
return;
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
if (action === "refresh-models") {
|
|
2136
|
+
if (!isProviderRoutableInCodexa(providerId)) {
|
|
2137
|
+
const message = provider.isDefault
|
|
2138
|
+
? `${provider.displayName} is set as the workspace default, but in-Codexa routing is not configured yet.`
|
|
2139
|
+
: `${provider.displayName} in-Codexa routing is not configured yet.`;
|
|
2140
|
+
if (providerRouteErrorsRef.current[providerId] !== message) {
|
|
2141
|
+
appendSystemEvent("Provider route unavailable", message);
|
|
2142
|
+
providerRouteErrorsRef.current[providerId] = message;
|
|
2143
|
+
}
|
|
2144
|
+
return;
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
if (providerId === "openai") {
|
|
2148
|
+
void refreshModelCapabilities(true, true);
|
|
2149
|
+
appendSystemEvent("Model discovery", `Refreshing models for ${provider.displayName}.`);
|
|
2150
|
+
} else {
|
|
2151
|
+
const runtime = getProviderRuntime(providerId);
|
|
2152
|
+
if (runtime.refreshModels) {
|
|
2153
|
+
appendSystemEvent("Model discovery", providerId === "anthropic"
|
|
2154
|
+
? "Refreshing Claude capabilities..."
|
|
2155
|
+
: `Refreshing models for ${provider.displayName}...`);
|
|
2156
|
+
void runtime.refreshModels({
|
|
2157
|
+
cwd: workspaceRoot,
|
|
2158
|
+
localConfig: providerId === "local" ? providerWorkspaceConfig.providers?.local : undefined,
|
|
2159
|
+
}).then((discovery) => {
|
|
2160
|
+
if (discovery.diagnostics) {
|
|
2161
|
+
providerDiagnosticsRef.current[providerId] = discovery.diagnostics as Record<string, string | number | boolean | null>;
|
|
2162
|
+
}
|
|
2163
|
+
if (discovery.status === "ready") {
|
|
2164
|
+
delete providerRouteErrorsRef.current[providerId];
|
|
2165
|
+
} else if (discovery.message) {
|
|
2166
|
+
providerRouteErrorsRef.current[providerId] = discovery.message;
|
|
2167
|
+
}
|
|
2168
|
+
appendSystemEvent(
|
|
2169
|
+
"Model discovery",
|
|
2170
|
+
discovery.status === "ready"
|
|
2171
|
+
? discovery.message ?? `Loaded ${discovery.models.length} models for ${provider.displayName} (${discovery.models[0]?.source ?? "fallback"}).`
|
|
2172
|
+
: discovery.message ?? `${provider.displayName} model routing is not configured yet.`,
|
|
2173
|
+
);
|
|
2174
|
+
setRegistryNonce((n) => n + 1);
|
|
2175
|
+
});
|
|
2176
|
+
} else {
|
|
2177
|
+
const discovery = discoverProviderModels(providerId);
|
|
2178
|
+
appendSystemEvent(
|
|
2179
|
+
"Model discovery",
|
|
2180
|
+
discovery.status === "ready"
|
|
2181
|
+
? `Loaded ${discovery.models.length} configured models for ${provider.displayName}.`
|
|
2182
|
+
: discovery.message ?? `${provider.displayName} model routing is not configured yet.`,
|
|
2183
|
+
);
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
return;
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
if (action === "run-diagnostics") {
|
|
2190
|
+
if (providerId !== "google" && providerId !== "local") {
|
|
2191
|
+
appendErrorEvent("Provider diagnostics unavailable", `Diagnostics are not implemented for ${provider.displayName}.`);
|
|
2192
|
+
return;
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
setScreen("main");
|
|
2196
|
+
if (providerId === "local") {
|
|
2197
|
+
appendSystemEvent("Local diagnostics", "Running Local provider diagnostics...");
|
|
2198
|
+
void runLocalDiagnostics({
|
|
2199
|
+
localConfig: providerWorkspaceConfig.providers?.local,
|
|
2200
|
+
}).then((message) => {
|
|
2201
|
+
if (!isMountedRef.current) return;
|
|
2202
|
+
const discovery = discoverProviderModels("local");
|
|
2203
|
+
if (discovery.diagnostics) {
|
|
2204
|
+
providerDiagnosticsRef.current["local"] = discovery.diagnostics as Record<string, string | number | boolean | null>;
|
|
2205
|
+
}
|
|
2206
|
+
if (discovery.status === "ready") {
|
|
2207
|
+
delete providerRouteErrorsRef.current["local"];
|
|
2208
|
+
} else if (discovery.message) {
|
|
2209
|
+
providerRouteErrorsRef.current["local"] = discovery.message;
|
|
2210
|
+
}
|
|
2211
|
+
appendSystemEvent("Local diagnostics", message);
|
|
2212
|
+
setRegistryNonce((n) => n + 1);
|
|
2213
|
+
}).catch((error) => {
|
|
2214
|
+
if (!isMountedRef.current) return;
|
|
2215
|
+
appendErrorEvent("Local diagnostics failed", error instanceof Error ? error.message : String(error));
|
|
2216
|
+
});
|
|
2217
|
+
return;
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
appendSystemEvent("Gemini diagnostics", "Running Gemini diagnostics...");
|
|
2221
|
+
const geminiCommandPath = providerWorkspaceConfig.providers?.google?.geminiCommandPath ?? runtimeConfig.geminiCommandPath;
|
|
2222
|
+
void runGeminiDiagnostics({
|
|
2223
|
+
cwd: workspaceRoot,
|
|
2224
|
+
runtime: geminiCommandPath ? { ...resolvedRuntimeConfig, geminiCommandPath } : resolvedRuntimeConfig,
|
|
2225
|
+
configuredPath: geminiCommandPath,
|
|
2226
|
+
selectedModel: activeProviderRoute.providerId === "google"
|
|
2227
|
+
? activeProviderRoute.modelId
|
|
2228
|
+
: providerWorkspaceConfig.providers?.google?.currentModel ?? "gemini-3-flash-preview",
|
|
2229
|
+
selectedReasoning: activeProviderRoute.providerId === "google"
|
|
2230
|
+
? activeProviderRoute.reasoning ?? reasoningLevel
|
|
2231
|
+
: providerWorkspaceConfig.providers?.google?.currentReasoning ?? reasoningLevel,
|
|
2232
|
+
}).then((message) => {
|
|
2233
|
+
if (!isMountedRef.current) return;
|
|
2234
|
+
appendSystemEvent("Gemini diagnostics", message);
|
|
2235
|
+
}).catch((error) => {
|
|
2236
|
+
if (!isMountedRef.current) return;
|
|
2237
|
+
const message = error instanceof Error ? error.message : "Gemini diagnostics failed.";
|
|
2238
|
+
appendErrorEvent("Gemini diagnostics failed", message);
|
|
2239
|
+
});
|
|
2240
|
+
return;
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
if (busyRef.current) {
|
|
2244
|
+
appendSystemEvent("Busy", "Finish the current run before launching a provider CLI.");
|
|
2245
|
+
return;
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
setScreen("main");
|
|
2249
|
+
appendSystemEvent(
|
|
2250
|
+
"Provider launch",
|
|
2251
|
+
`Suspending Codexa and launching ${provider.displayName}. Codexa will resume when the external CLI exits.`,
|
|
2252
|
+
);
|
|
2253
|
+
|
|
2254
|
+
void launchProviderCli(provider, {
|
|
2255
|
+
cwd: workspaceRoot,
|
|
2256
|
+
stdin,
|
|
2257
|
+
beforeLaunch: () => {
|
|
2258
|
+
terminalControl.setMouseReporting(false, "src/app.tsx:providerLaunch.disableMouse");
|
|
2259
|
+
stdout.write("\n");
|
|
2260
|
+
},
|
|
2261
|
+
afterLaunch: () => {
|
|
2262
|
+
terminalControl.setMouseReporting(mouseCapture, "src/app.tsx:providerLaunch.restoreMouse");
|
|
2263
|
+
forceRefreshCurrentTerminalTitle("provider_launch_return", false);
|
|
2264
|
+
},
|
|
2265
|
+
}).then((result) => {
|
|
2266
|
+
if (!isMountedRef.current) return;
|
|
2267
|
+
appendSystemEvent("Provider launch", result.message);
|
|
2268
|
+
}).catch((error) => {
|
|
2269
|
+
if (!isMountedRef.current) return;
|
|
2270
|
+
const message = error instanceof Error ? error.message : "Provider launch failed.";
|
|
2271
|
+
appendErrorEvent("Provider launch failed", message);
|
|
2272
|
+
});
|
|
2273
|
+
}, [
|
|
2274
|
+
activeProviderRoute,
|
|
2275
|
+
appendErrorEvent,
|
|
2276
|
+
appendSystemEvent,
|
|
2277
|
+
forceRefreshCurrentTerminalTitle,
|
|
2278
|
+
mouseCapture,
|
|
2279
|
+
providerRegistry,
|
|
2280
|
+
providerWorkspaceConfig.providers,
|
|
2281
|
+
modelCapabilities,
|
|
2282
|
+
reasoningLevel,
|
|
2283
|
+
refreshModelCapabilities,
|
|
2284
|
+
resolvedRuntimeConfig,
|
|
2285
|
+
runtimeConfig.geminiCommandPath,
|
|
2286
|
+
setWorkspaceDefaultProviderWithNotice,
|
|
2287
|
+
stdin,
|
|
2288
|
+
stdout,
|
|
2289
|
+
terminalControl,
|
|
2290
|
+
workspaceRoot,
|
|
2291
|
+
]);
|
|
2292
|
+
|
|
2293
|
+
const openModelPicker = useCallback(() => {
|
|
2294
|
+
setPendingRouteProviderId(null);
|
|
2295
|
+
traceInputDebug("model_picker_open_request", getInputDebugSnapshot({
|
|
2296
|
+
handler: "openModelPicker",
|
|
2297
|
+
currentScreen: screen,
|
|
2298
|
+
}));
|
|
2299
|
+
|
|
2300
|
+
const gate = guardConfigMutation("model", busy);
|
|
2301
|
+
if (!gate.allowed) {
|
|
2302
|
+
traceInputDebug("model_picker_open_blocked", getInputDebugSnapshot({
|
|
2303
|
+
handler: "openModelPicker",
|
|
2304
|
+
reason: "busy",
|
|
2305
|
+
}));
|
|
2306
|
+
appendSystemEvent("Busy", gate.message ?? "Finish the current run before changing the model.");
|
|
2307
|
+
return;
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
if (screen === "model-picker") {
|
|
2311
|
+
intendedInputModeRef.current = "model-picker";
|
|
2312
|
+
intendedFocusTargetRef.current = FOCUS_IDS.modelPicker;
|
|
2313
|
+
traceInputDebug("model_picker_open_duplicate", getInputDebugSnapshot({
|
|
2314
|
+
handler: "openModelPicker",
|
|
2315
|
+
focusTarget: FOCUS_IDS.modelPicker,
|
|
2316
|
+
}));
|
|
2317
|
+
focusManager.focus(FOCUS_IDS.modelPicker);
|
|
2318
|
+
return;
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
// Kick off discovery if we don't already have capabilities. The helper is
|
|
2322
|
+
// single-flight, so repeated picker opens while discovery is in progress
|
|
2323
|
+
// subscribe to the existing promise instead of spawning duplicate jobs or
|
|
2324
|
+
// log entries. Open the picker immediately; it renders a loading state
|
|
2325
|
+
// until the promise resolves and state updates commit the model list.
|
|
2326
|
+
if (activeProviderRoute.providerId === "openai" && !modelCapabilities) {
|
|
2327
|
+
traceInputDebug("model_picker_loading_trigger", getInputDebugSnapshot({
|
|
2328
|
+
handler: "openModelPicker",
|
|
2329
|
+
}));
|
|
2330
|
+
void refreshModelCapabilities(false, false);
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
intendedInputModeRef.current = "model-picker";
|
|
2334
|
+
intendedFocusTargetRef.current = FOCUS_IDS.modelPicker;
|
|
2335
|
+
setScreen("model-picker");
|
|
2336
|
+
traceInputDebug("model_picker_opened", getInputDebugSnapshot({
|
|
2337
|
+
handler: "openModelPicker",
|
|
2338
|
+
nextScreen: "model-picker",
|
|
2339
|
+
focusTarget: FOCUS_IDS.modelPicker,
|
|
2340
|
+
}));
|
|
2341
|
+
}, [activeProviderRoute.providerId, appendSystemEvent, busy, focusManager, getInputDebugSnapshot, modelCapabilities, refreshModelCapabilities, screen]);
|
|
2342
|
+
|
|
2343
|
+
const openModePicker = useCallback(() => {
|
|
2344
|
+
const gate = guardConfigMutation("mode", busy);
|
|
2345
|
+
if (!gate.allowed) {
|
|
2346
|
+
appendSystemEvent("Busy", gate.message ?? "Finish the current run before changing the mode.");
|
|
2347
|
+
return;
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
setScreen("mode-picker");
|
|
2351
|
+
}, [appendSystemEvent, busy]);
|
|
2352
|
+
|
|
2353
|
+
const openReasoningPicker = useCallback(() => {
|
|
2354
|
+
const gate = guardConfigMutation("reasoning", busy);
|
|
2355
|
+
if (!gate.allowed) {
|
|
2356
|
+
appendSystemEvent("Busy", gate.message ?? "Finish the current run before changing the reasoning level.");
|
|
2357
|
+
return;
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
if (!currentModelCapability?.supportedReasoningLevels?.length) {
|
|
2361
|
+
if (!modelCapabilitiesBusy) {
|
|
2362
|
+
void refreshModelCapabilities(true, true);
|
|
2363
|
+
}
|
|
2364
|
+
appendSystemEvent(
|
|
2365
|
+
"Reasoning unavailable",
|
|
2366
|
+
`Codex has not provided reasoning metadata for ${model}. No guessed reasoning levels will be shown.`,
|
|
2367
|
+
);
|
|
2368
|
+
return;
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
setScreen("reasoning-picker");
|
|
2372
|
+
}, [appendSystemEvent, busy, currentModelCapability, model, modelCapabilitiesBusy, refreshModelCapabilities]);
|
|
2373
|
+
|
|
2374
|
+
const openThemePicker = useCallback(() => {
|
|
2375
|
+
const gate = guardConfigMutation("theme", busy);
|
|
2376
|
+
if (!gate.allowed) {
|
|
2377
|
+
appendSystemEvent("Busy", gate.message ?? "Finish the current run before changing the theme.");
|
|
2378
|
+
return;
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
setScreen("theme-picker");
|
|
2382
|
+
}, [appendSystemEvent, busy]);
|
|
2383
|
+
|
|
2384
|
+
const openSettingsPanel = useCallback(() => {
|
|
2385
|
+
const gate = guardConfigMutation("mode", busy);
|
|
2386
|
+
if (!gate.allowed) {
|
|
2387
|
+
appendSystemEvent("Busy", gate.message ?? "Finish the current run before changing settings.");
|
|
2388
|
+
return;
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
setScreen("settings-panel");
|
|
2392
|
+
}, [appendSystemEvent, busy]);
|
|
2393
|
+
|
|
2394
|
+
const openAuthPanel = useCallback(() => {
|
|
2395
|
+
if (busy) {
|
|
2396
|
+
appendSystemEvent("Busy", "Finish the current run before opening auth guidance.");
|
|
2397
|
+
return;
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
setScreen("auth-panel");
|
|
2401
|
+
}, [appendSystemEvent, busy]);
|
|
2402
|
+
|
|
2403
|
+
const openPermissionsPanel = useCallback(() => {
|
|
2404
|
+
const gate = guardConfigMutation("mode", busy);
|
|
2405
|
+
if (!gate.allowed) {
|
|
2406
|
+
appendSystemEvent("Busy", gate.message ?? "Finish the current run before changing runtime policy.");
|
|
2407
|
+
return;
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2410
|
+
setScreen("permissions-panel");
|
|
2411
|
+
}, [appendSystemEvent, busy]);
|
|
2412
|
+
|
|
2413
|
+
const openPermissionsApprovalPicker = useCallback(() => {
|
|
2414
|
+
setScreen("permissions-approval-picker");
|
|
2415
|
+
}, []);
|
|
2416
|
+
|
|
2417
|
+
const openPermissionsSandboxPicker = useCallback(() => {
|
|
2418
|
+
setScreen("permissions-sandbox-picker");
|
|
2419
|
+
}, []);
|
|
2420
|
+
|
|
2421
|
+
const openPermissionsNetworkPicker = useCallback(() => {
|
|
2422
|
+
setScreen("permissions-network-picker");
|
|
2423
|
+
}, []);
|
|
2424
|
+
|
|
2425
|
+
const openPermissionsAddWritableRoot = useCallback(() => {
|
|
2426
|
+
setScreen("permissions-add-writable-root");
|
|
2427
|
+
}, []);
|
|
2428
|
+
|
|
2429
|
+
const openPermissionsRemoveWritableRoot = useCallback(() => {
|
|
2430
|
+
if (runtimeConfig.policy.writableRoots.length === 0) {
|
|
2431
|
+
appendSystemEvent("Runtime policy", "No writable roots are configured.");
|
|
2432
|
+
return;
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
setScreen("permissions-remove-writable-root");
|
|
2436
|
+
}, [appendSystemEvent, runtimeConfig.policy.writableRoots.length]);
|
|
2437
|
+
|
|
2438
|
+
const handlePermissionsPanelAction = useCallback((action: PermissionsPanelAction) => {
|
|
2439
|
+
switch (action) {
|
|
2440
|
+
case "approval-policy":
|
|
2441
|
+
openPermissionsApprovalPicker();
|
|
2442
|
+
return;
|
|
2443
|
+
case "sandbox":
|
|
2444
|
+
openPermissionsSandboxPicker();
|
|
2445
|
+
return;
|
|
2446
|
+
case "network":
|
|
2447
|
+
openPermissionsNetworkPicker();
|
|
2448
|
+
return;
|
|
2449
|
+
case "writable-roots-summary":
|
|
2450
|
+
appendSystemEvent(
|
|
2451
|
+
"Runtime policy",
|
|
2452
|
+
`Writable roots:\n${formatWritableRootsMessage(runtimeConfig.policy.writableRoots)}`,
|
|
2453
|
+
);
|
|
2454
|
+
return;
|
|
2455
|
+
case "writable-roots-add":
|
|
2456
|
+
openPermissionsAddWritableRoot();
|
|
2457
|
+
return;
|
|
2458
|
+
case "writable-roots-remove":
|
|
2459
|
+
openPermissionsRemoveWritableRoot();
|
|
2460
|
+
return;
|
|
2461
|
+
case "writable-roots-clear":
|
|
2462
|
+
clearWritableRootsWithNotice();
|
|
2463
|
+
return;
|
|
2464
|
+
default:
|
|
2465
|
+
return;
|
|
2466
|
+
}
|
|
2467
|
+
}, [
|
|
2468
|
+
appendSystemEvent,
|
|
2469
|
+
clearWritableRootsWithNotice,
|
|
2470
|
+
openPermissionsAddWritableRoot,
|
|
2471
|
+
openPermissionsApprovalPicker,
|
|
2472
|
+
openPermissionsNetworkPicker,
|
|
2473
|
+
openPermissionsRemoveWritableRoot,
|
|
2474
|
+
openPermissionsSandboxPicker,
|
|
2475
|
+
runtimeConfig.policy.writableRoots,
|
|
2476
|
+
]);
|
|
2477
|
+
|
|
2478
|
+
const resetComposer = useCallback(() => {
|
|
2479
|
+
dispatchSession({ type: "RESET_INPUT" });
|
|
2480
|
+
}, [dispatchSession]);
|
|
2481
|
+
|
|
2482
|
+
const finalizePromptRun = useCallback((
|
|
2483
|
+
runId: number,
|
|
2484
|
+
turnId: number,
|
|
2485
|
+
status: "completed" | "failed" | "canceled",
|
|
2486
|
+
message?: string,
|
|
2487
|
+
response?: string,
|
|
2488
|
+
) => {
|
|
2489
|
+
if (!isCurrentRun(activeRunIdRef.current, runId)) {
|
|
2490
|
+
appDiagLog(`FINALIZE_RUN_BOUNDARY: ignored stale runId=${runId} turnId=${turnId} status=${status} activeRunId=${activeRunIdRef.current}`);
|
|
2491
|
+
return false;
|
|
2492
|
+
}
|
|
2493
|
+
perf.mark("finalize_start");
|
|
2494
|
+
|
|
2495
|
+
const lifecycle = activeRunLifecycleRef.current;
|
|
2496
|
+
const timing = activeRunTimingRef.current?.runId === runId
|
|
2497
|
+
? activeRunTimingRef.current
|
|
2498
|
+
: null;
|
|
2499
|
+
const finalMonotonicMs = performance.now();
|
|
2500
|
+
const durationMs = timing
|
|
2501
|
+
? Math.max(0, Math.round(finalMonotonicMs - timing.submitMonotonicMs))
|
|
2502
|
+
: 0;
|
|
2503
|
+
renderDebug.traceEvent("run", "finalizeTiming", {
|
|
2504
|
+
runId,
|
|
2505
|
+
turnId,
|
|
2506
|
+
status,
|
|
2507
|
+
promptSubmitEpochMs: timing?.submitEpochMs,
|
|
2508
|
+
promptSubmitMonotonicMs: timing?.submitMonotonicMs,
|
|
2509
|
+
finalRenderMonotonicMs: finalMonotonicMs,
|
|
2510
|
+
elapsedWallMs: durationMs,
|
|
2511
|
+
});
|
|
2512
|
+
const cleanup = cleanupRef.current;
|
|
2513
|
+
cleanupRef.current = null;
|
|
2514
|
+
activeRunLifecycleRef.current = null;
|
|
2515
|
+
activeRunTimingRef.current = null;
|
|
2516
|
+
activeRunIdRef.current = null;
|
|
2517
|
+
activeTurnIdRef.current = null;
|
|
2518
|
+
appDiagLog([
|
|
2519
|
+
"FINALIZE_RUN_BOUNDARY:",
|
|
2520
|
+
`provider=${activeProviderRoute.providerId}`,
|
|
2521
|
+
`runId=${runId}`,
|
|
2522
|
+
`turnId=${turnId}`,
|
|
2523
|
+
`status=${status}`,
|
|
2524
|
+
`responseProvided=${response !== undefined}`,
|
|
2525
|
+
`responseLength=${response?.length ?? 0}`,
|
|
2526
|
+
`messagePresent=${Boolean(message?.trim())}`,
|
|
2527
|
+
`composerUnlockReason=finalizePromptRun:${status}`,
|
|
2528
|
+
].join(" "));
|
|
2529
|
+
focusManager.focus(FOCUS_IDS.composer);
|
|
2530
|
+
appDiagLog(`COMPOSER_ACTIVE_AGAIN: reason=finalizePromptRun:${status} activeRunCleared=true focusTarget=${FOCUS_IDS.composer}`);
|
|
2531
|
+
cleanup?.();
|
|
2532
|
+
const safeMessage = message ? sanitizeTerminalOutput(message) : undefined;
|
|
2533
|
+
// When response is undefined, signal the reducer to preserve streamed content as-is.
|
|
2534
|
+
const safeResponse = response != null
|
|
2535
|
+
? sanitizeTerminalOutput(response, { preserveTabs: false, tabSize: 2 })
|
|
2536
|
+
: undefined;
|
|
2537
|
+
const shouldParseActionRequired = lifecycle?.parseActionRequired ?? true;
|
|
2538
|
+
const parsed = status === "completed" && safeResponse?.trim()
|
|
2539
|
+
? shouldParseActionRequired
|
|
2540
|
+
? extractAssistantActionRequired(safeResponse)
|
|
2541
|
+
: { content: safeResponse, question: null as string | null }
|
|
2542
|
+
: { content: safeResponse, question: null as string | null };
|
|
2543
|
+
appDiagLog([
|
|
2544
|
+
"FINALIZE_RUN_PAYLOAD:",
|
|
2545
|
+
`provider=${activeProviderRoute.providerId}`,
|
|
2546
|
+
`runId=${runId}`,
|
|
2547
|
+
`turnId=${turnId}`,
|
|
2548
|
+
`status=${status}`,
|
|
2549
|
+
`safeResponseLength=${safeResponse?.length ?? 0}`,
|
|
2550
|
+
`parsedContentLength=${parsed.content?.length ?? 0}`,
|
|
2551
|
+
`assistantAppendCalledExpected=${Boolean(parsed.content?.trim())}`,
|
|
2552
|
+
`finalRunState=${status}`,
|
|
2553
|
+
].join(" "));
|
|
2554
|
+
dispatchSession({
|
|
2555
|
+
type: "FINALIZE_RUN",
|
|
2556
|
+
runId,
|
|
2557
|
+
turnId,
|
|
2558
|
+
status,
|
|
2559
|
+
message: safeMessage,
|
|
2560
|
+
response: parsed.content,
|
|
2561
|
+
durationMs,
|
|
2562
|
+
responsePresentation: lifecycle?.responsePresentation,
|
|
2563
|
+
question: status === "completed" ? parsed.question : null,
|
|
2564
|
+
assistantFactory: () => ({
|
|
2565
|
+
id: createEventId(),
|
|
2566
|
+
type: "assistant",
|
|
2567
|
+
createdAt: Date.now(),
|
|
2568
|
+
content: parsed.content?.trim() ? parsed.content : "",
|
|
2569
|
+
contentChunks: [],
|
|
2570
|
+
turnId,
|
|
2571
|
+
}),
|
|
2572
|
+
});
|
|
2573
|
+
perf.mark("finalize_done");
|
|
2574
|
+
perf.setMeta("content_length", parsed.content?.length ?? 0);
|
|
2575
|
+
perf.setMeta("status", status);
|
|
2576
|
+
perf.setMeta("elapsed_wall_ms", durationMs);
|
|
2577
|
+
const perfSession = perf.getSession();
|
|
2578
|
+
if (perfSession) perf.persistSession(perfSession);
|
|
2579
|
+
|
|
2580
|
+
if (status === "completed") {
|
|
2581
|
+
lifecycle?.onCompleted?.({
|
|
2582
|
+
response: parsed.content ?? "",
|
|
2583
|
+
turnId,
|
|
2584
|
+
runId,
|
|
2585
|
+
});
|
|
2586
|
+
} else if (status === "failed") {
|
|
2587
|
+
lifecycle?.onFailed?.({
|
|
2588
|
+
message: safeMessage ?? "Run failed",
|
|
2589
|
+
turnId,
|
|
2590
|
+
runId,
|
|
2591
|
+
});
|
|
2592
|
+
} else {
|
|
2593
|
+
lifecycle?.onCanceled?.({
|
|
2594
|
+
turnId,
|
|
2595
|
+
runId,
|
|
2596
|
+
});
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
return true;
|
|
2600
|
+
}, [activeProviderRoute.providerId, dispatchSession, focusManager]);
|
|
2601
|
+
|
|
2602
|
+
const cancelActiveRun = useCallback((retainHistory = true) => {
|
|
2603
|
+
const runId = activeRunIdRef.current;
|
|
2604
|
+
if (runId === null) return false;
|
|
2605
|
+
const promptTurnId = activeTurnIdRef.current;
|
|
2606
|
+
|
|
2607
|
+
if (!isCurrentRun(activeRunIdRef.current, runId)) {
|
|
2608
|
+
return false;
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
const shellEvent = activeEvents.find((event) => event.type === "shell" && event.id === runId) as ShellEvent | undefined;
|
|
2612
|
+
const runEvent = activeEvents.find((event) => event.type === "run" && event.id === runId) as RunEvent | undefined;
|
|
2613
|
+
|
|
2614
|
+
if (retainHistory && runEvent) {
|
|
2615
|
+
return finalizePromptRun(runId, runEvent.turnId, "canceled");
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
const cleanup = cleanupRef.current;
|
|
2619
|
+
const lifecycle = activeRunLifecycleRef.current;
|
|
2620
|
+
cleanupRef.current = null;
|
|
2621
|
+
activeRunLifecycleRef.current = null;
|
|
2622
|
+
activeRunTimingRef.current = null;
|
|
2623
|
+
activeRunIdRef.current = null;
|
|
2624
|
+
activeTurnIdRef.current = null;
|
|
2625
|
+
focusManager.focus(FOCUS_IDS.composer);
|
|
2626
|
+
cleanup?.();
|
|
2627
|
+
|
|
2628
|
+
if (retainHistory) {
|
|
2629
|
+
if (shellEvent) {
|
|
2630
|
+
activeRunLifecycleRef.current = null;
|
|
2631
|
+
activeRunTimingRef.current = null;
|
|
2632
|
+
dispatchSession({
|
|
2633
|
+
type: "FINALIZE_SHELL",
|
|
2634
|
+
shellId: runId,
|
|
2635
|
+
finalEvent: { ...shellEvent, status: "failed", exitCode: -1, durationMs: null },
|
|
2636
|
+
});
|
|
2637
|
+
} else {
|
|
2638
|
+
dispatchSession({ type: "REMOVE_ACTIVE_RUNTIME", runId, turnId: promptTurnId });
|
|
2639
|
+
const runEvent = activeEvents.find((event) => event.type === "run" && event.id === runId) as RunEvent | undefined;
|
|
2640
|
+
if (runEvent) {
|
|
2641
|
+
void finalizePromptRun(runId, runEvent.turnId, "canceled");
|
|
2642
|
+
} else {
|
|
2643
|
+
if (promptTurnId !== null) {
|
|
2644
|
+
lifecycle?.onCanceled?.({ turnId: promptTurnId, runId });
|
|
2645
|
+
}
|
|
2646
|
+
dispatchSession({ type: "REMOVE_ACTIVE_RUNTIME", runId, turnId: promptTurnId });
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
} else {
|
|
2650
|
+
if (promptTurnId !== null) {
|
|
2651
|
+
lifecycle?.onCanceled?.({ turnId: promptTurnId, runId });
|
|
2652
|
+
}
|
|
2653
|
+
if (uiState.kind === "SHELL_RUNNING") {
|
|
2654
|
+
dispatchSession({ type: "UI_ACTION", action: { type: "SHELL_FINISHED", shellId: runId } });
|
|
2655
|
+
} else if (promptTurnId !== null) {
|
|
2656
|
+
dispatchSession({ type: "UI_ACTION", action: { type: "RUN_CANCELED", turnId: promptTurnId } });
|
|
2657
|
+
}
|
|
2658
|
+
dispatchSession({ type: "REMOVE_ACTIVE_RUNTIME", runId, turnId: promptTurnId });
|
|
2659
|
+
return true;
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
if (uiState.kind === "SHELL_RUNNING") {
|
|
2663
|
+
dispatchSession({ type: "UI_ACTION", action: { type: "SHELL_FINISHED", shellId: runId } });
|
|
2664
|
+
} else if (promptTurnId !== null) {
|
|
2665
|
+
dispatchSession({ type: "UI_ACTION", action: { type: "RUN_CANCELED", turnId: promptTurnId } });
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
return true;
|
|
2669
|
+
}, [activeEvents, dispatchSession, finalizePromptRun, focusManager, uiState.kind]);
|
|
2670
|
+
|
|
2671
|
+
const handleCancel = useCallback(() => {
|
|
2672
|
+
if (busy) {
|
|
2673
|
+
cancelActiveRun(true);
|
|
2674
|
+
return;
|
|
2675
|
+
}
|
|
2676
|
+
if (planFlow.kind === "collecting_feedback") {
|
|
2677
|
+
setPlanFlow((current) => cancelPlanFeedback(current));
|
|
2678
|
+
return;
|
|
2679
|
+
}
|
|
2680
|
+
if (planFlow.kind === "awaiting_action") {
|
|
2681
|
+
setPlanFlow(resetPlanFlow());
|
|
2682
|
+
appendSystemEvent("Plan review", "Plan review canceled. No changes were made.");
|
|
2683
|
+
return;
|
|
2684
|
+
}
|
|
2685
|
+
if (uiState.kind === "AWAITING_USER_ACTION" || uiState.kind === "ERROR") {
|
|
2686
|
+
dispatchSession({ type: "UI_ACTION", action: { type: "DISMISS_TRANSIENT" } });
|
|
2687
|
+
resetComposer();
|
|
2688
|
+
}
|
|
2689
|
+
}, [appendSystemEvent, busy, cancelActiveRun, dispatchSession, planFlow.kind, resetComposer, uiState.kind]);
|
|
2690
|
+
|
|
2691
|
+
const handleQuit = useCallback(() => {
|
|
2692
|
+
cancelActiveRun(false);
|
|
2693
|
+
exit();
|
|
2694
|
+
}, [cancelActiveRun, exit]);
|
|
2695
|
+
|
|
2696
|
+
const handleCopy = useCallback(async () => {
|
|
2697
|
+
// Build a full conversation transcript from all user prompts and assistant
|
|
2698
|
+
// responses in staticEvents, paired by turnId and sorted chronologically.
|
|
2699
|
+
type TurnPair = { createdAt: number; prompt: string; response: string | null };
|
|
2700
|
+
const turns = new Map<number, TurnPair>();
|
|
2701
|
+
|
|
2702
|
+
for (const event of staticEvents) {
|
|
2703
|
+
if (event.type === "user") {
|
|
2704
|
+
const existing = turns.get(event.turnId);
|
|
2705
|
+
if (!existing) {
|
|
2706
|
+
turns.set(event.turnId, { createdAt: event.createdAt, prompt: event.prompt, response: null });
|
|
2707
|
+
}
|
|
2708
|
+
} else if (event.type === "assistant") {
|
|
2709
|
+
const existing = turns.get(event.turnId);
|
|
2710
|
+
if (existing) {
|
|
2711
|
+
existing.response = event.content;
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
if (turns.size === 0) {
|
|
2717
|
+
// After /clear, the conversation is empty and that's expected.
|
|
2718
|
+
// Don't show "Copy unavailable" error - maintain clean post-clear state.
|
|
2719
|
+
// Only show this error if the user tries to copy on a fresh session, not post-clear.
|
|
2720
|
+
return;
|
|
2721
|
+
}
|
|
2722
|
+
|
|
2723
|
+
// Sort turns by creation time and format as a readable dialogue.
|
|
2724
|
+
const lines: string[] = [];
|
|
2725
|
+
const sorted = [...turns.values()].sort((a, b) => a.createdAt - b.createdAt);
|
|
2726
|
+
for (const turn of sorted) {
|
|
2727
|
+
lines.push(`You: ${turn.prompt.trim()}`);
|
|
2728
|
+
if (turn.response?.trim()) {
|
|
2729
|
+
lines.push("");
|
|
2730
|
+
lines.push(`Codexa: ${turn.response.trim()}`);
|
|
2731
|
+
}
|
|
2732
|
+
lines.push("");
|
|
2733
|
+
}
|
|
2734
|
+
const transcript = lines.join("\n").trimEnd();
|
|
2735
|
+
|
|
2736
|
+
const ok = await copyToClipboard(transcript);
|
|
2737
|
+
const turnWord = turns.size === 1 ? "1 turn" : `${turns.size} turns`;
|
|
2738
|
+
appendSystemEvent(
|
|
2739
|
+
"Clipboard",
|
|
2740
|
+
ok
|
|
2741
|
+
? `Copied full conversation (${turnWord}) to clipboard.`
|
|
2742
|
+
: "Clipboard unavailable.",
|
|
2743
|
+
);
|
|
2744
|
+
}, [appendSystemEvent, staticEvents]);
|
|
2745
|
+
|
|
2746
|
+
const savePlanFile = useCallback((planContent: string): string | null => {
|
|
2747
|
+
const filePath = savePlan(planContent, workspaceRoot);
|
|
2748
|
+
if (!filePath) {
|
|
2749
|
+
appendErrorEvent(
|
|
2750
|
+
"Plan file unavailable",
|
|
2751
|
+
"The generated plan could not be saved.",
|
|
2752
|
+
);
|
|
2753
|
+
}
|
|
2754
|
+
return filePath;
|
|
2755
|
+
}, [appendErrorEvent, workspaceRoot]);
|
|
2756
|
+
|
|
2757
|
+
const handleViewPlanFile = useCallback((planFilePath: string | null) => {
|
|
2758
|
+
if (!planFilePath) {
|
|
2759
|
+
appendErrorEvent("Plan file unavailable", "There is no saved plan file to view for this review.");
|
|
2760
|
+
return;
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
const contents = readPlan(planFilePath);
|
|
2764
|
+
if (contents === null) {
|
|
2765
|
+
appendErrorEvent("Plan file unavailable", `The saved plan file is no longer available: ${planFilePath}`);
|
|
2766
|
+
return;
|
|
2767
|
+
}
|
|
2768
|
+
|
|
2769
|
+
const sanitized = normalizePlanReviewMarkdown(contents, workspaceRoot);
|
|
2770
|
+
appendSystemEvent("Plan file", [`Path: ${planFilePath}`, "", sanitized].join("\n"));
|
|
2771
|
+
}, [appendErrorEvent, appendSystemEvent, workspaceRoot]);
|
|
2772
|
+
|
|
2773
|
+
// ─── Stable composer-input callbacks ──────────────────────────────────────────
|
|
2774
|
+
// These use refs so the function identity never changes, avoiding
|
|
2775
|
+
// unnecessary downstream work even though the memo comparator on
|
|
2776
|
+
// MemoizedBottomComposer already skips callback checks.
|
|
2777
|
+
const handleChangeInput = useCallback((value: string, nextCursor: number) => {
|
|
2778
|
+
const safeValue = sanitizeTerminalInput(value);
|
|
2779
|
+
dispatchSession({ type: "SET_INPUT", value: safeValue, cursor: Math.min(nextCursor, safeValue.length) });
|
|
2780
|
+
}, [dispatchSession]);
|
|
2781
|
+
|
|
2782
|
+
const handleChangeValue = useCallback((value: string) => {
|
|
2783
|
+
const safeValue = sanitizeTerminalInput(value);
|
|
2784
|
+
dispatchSession({ type: "SET_INPUT", value: safeValue, cursor: Math.min(cursorRef.current, safeValue.length) });
|
|
2785
|
+
}, [dispatchSession]);
|
|
2786
|
+
|
|
2787
|
+
const handleChangeCursor = useCallback((nextCursor: number) => {
|
|
2788
|
+
const safeValue = sanitizeTerminalInput(inputValueRef.current);
|
|
2789
|
+
dispatchSession({ type: "SET_INPUT", value: safeValue, cursor: Math.min(nextCursor, safeValue.length) });
|
|
2790
|
+
}, [dispatchSession]);
|
|
2791
|
+
|
|
2792
|
+
const handleClear = useCallback(() => {
|
|
2793
|
+
writeCurrentTerminalTitleBeforeStateChange("before-clear");
|
|
2794
|
+
cancelActiveRun(false);
|
|
2795
|
+
activeTurnIdRef.current = null;
|
|
2796
|
+
activeRunLifecycleRef.current = null;
|
|
2797
|
+
activeRunTimingRef.current = null;
|
|
2798
|
+
setPlanFlow(resetPlanFlow());
|
|
2799
|
+
dispatchSession({ type: "CLEAR_TRANSCRIPT" });
|
|
2800
|
+
setConversationChars(0);
|
|
2801
|
+
setScreen("main");
|
|
2802
|
+
resetComposer();
|
|
2803
|
+
terminalControl.clearTranscript("src/app.tsx:handleClear");
|
|
2804
|
+
refreshTerminalTitle({
|
|
2805
|
+
terminalTitleMode,
|
|
2806
|
+
workspaceName: deriveTerminalTitle(workspaceRoot, "dir"),
|
|
2807
|
+
force: true,
|
|
2808
|
+
});
|
|
2809
|
+
}, [cancelActiveRun, dispatchSession, resetComposer, terminalControl, terminalTitleMode, workspaceRoot, writeCurrentTerminalTitleBeforeStateChange]);
|
|
2810
|
+
|
|
2811
|
+
const handleShellExecute = useCallback((command: string) => {
|
|
2812
|
+
const safeCommand = sanitizeTerminalInput(command).trim();
|
|
2813
|
+
const guardMessage = getShellWorkspaceGuardMessage(safeCommand, workspaceRoot, allowedWritableRoots);
|
|
2814
|
+
if (guardMessage) {
|
|
2815
|
+
appendErrorEvent("Shell command blocked", guardMessage);
|
|
2816
|
+
return;
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
const shellId = createEventId();
|
|
2820
|
+
const startTime = Date.now();
|
|
2821
|
+
writeCurrentTerminalTitleBeforeStateChange("before-shell-start");
|
|
2822
|
+
|
|
2823
|
+
const initialEvent: ShellEvent = {
|
|
2824
|
+
id: shellId,
|
|
2825
|
+
createdAt: startTime,
|
|
2826
|
+
type: "shell",
|
|
2827
|
+
command: safeCommand,
|
|
2828
|
+
lines: [],
|
|
2829
|
+
stderrLines: [],
|
|
2830
|
+
summary: `Executing shell: ${safeCommand}`,
|
|
2831
|
+
status: "running",
|
|
2832
|
+
exitCode: null,
|
|
2833
|
+
durationMs: null,
|
|
2834
|
+
};
|
|
2835
|
+
|
|
2836
|
+
dispatchSession({ type: "SET_ACTIVE_EVENTS", events: [initialEvent] });
|
|
2837
|
+
activeRunLifecycleRef.current = null;
|
|
2838
|
+
activeRunTimingRef.current = null;
|
|
2839
|
+
activeRunIdRef.current = shellId;
|
|
2840
|
+
activeTurnIdRef.current = null;
|
|
2841
|
+
dispatchSession({ type: "UI_ACTION", action: { type: "SHELL_STARTED", shellId } });
|
|
2842
|
+
|
|
2843
|
+
let pendingStdout: string[] = [];
|
|
2844
|
+
let pendingStderr: string[] = [];
|
|
2845
|
+
let shellFlushTimer: ReturnType<typeof setTimeout> | null = null;
|
|
2846
|
+
|
|
2847
|
+
const flushShellLines = () => {
|
|
2848
|
+
if (shellFlushTimer) {
|
|
2849
|
+
clearTimeout(shellFlushTimer);
|
|
2850
|
+
shellFlushTimer = null;
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
const stdoutLines = pendingStdout;
|
|
2854
|
+
const stderrLines = pendingStderr;
|
|
2855
|
+
pendingStdout = [];
|
|
2856
|
+
pendingStderr = [];
|
|
2857
|
+
|
|
2858
|
+
if (stdoutLines.length === 0 && stderrLines.length === 0) {
|
|
2859
|
+
return;
|
|
2860
|
+
}
|
|
2861
|
+
|
|
2862
|
+
startTransition(() => {
|
|
2863
|
+
if (stdoutLines.length > 0) {
|
|
2864
|
+
dispatchSession({ type: "UPDATE_SHELL_LINES", shellId, stream: "stdout", lines: stdoutLines });
|
|
2865
|
+
}
|
|
2866
|
+
if (stderrLines.length > 0) {
|
|
2867
|
+
dispatchSession({ type: "UPDATE_SHELL_LINES", shellId, stream: "stderr", lines: stderrLines });
|
|
2868
|
+
}
|
|
2869
|
+
});
|
|
2870
|
+
};
|
|
2871
|
+
|
|
2872
|
+
const scheduleShellFlush = () => {
|
|
2873
|
+
if (shellFlushTimer) return;
|
|
2874
|
+
shellFlushTimer = setTimeout(() => {
|
|
2875
|
+
shellFlushTimer = null;
|
|
2876
|
+
flushShellLines();
|
|
2877
|
+
}, LIVE_UPDATE_FLUSH_MS);
|
|
2878
|
+
};
|
|
2879
|
+
|
|
2880
|
+
const runner = runShellCommand(
|
|
2881
|
+
safeCommand,
|
|
2882
|
+
{ cwd: workspaceRoot },
|
|
2883
|
+
{
|
|
2884
|
+
onProcessLifecycle: (event) => {
|
|
2885
|
+
reassertIntendedTerminalTitle({ reason: `shell-process-${event}` });
|
|
2886
|
+
},
|
|
2887
|
+
onStdout: (text) => {
|
|
2888
|
+
const lines = sanitizeTerminalLines(text.split(/\r?\n/));
|
|
2889
|
+
if (lines.length > 0) {
|
|
2890
|
+
pendingStdout.push(...lines);
|
|
2891
|
+
scheduleShellFlush();
|
|
2892
|
+
}
|
|
2893
|
+
},
|
|
2894
|
+
onStderr: (text) => {
|
|
2895
|
+
const lines = sanitizeTerminalLines(text.split(/\r?\n/));
|
|
2896
|
+
if (lines.length > 0) {
|
|
2897
|
+
pendingStderr.push(...lines);
|
|
2898
|
+
scheduleShellFlush();
|
|
2899
|
+
}
|
|
2900
|
+
},
|
|
2901
|
+
},
|
|
2902
|
+
);
|
|
2903
|
+
|
|
2904
|
+
cleanupRef.current = () => {
|
|
2905
|
+
if (shellFlushTimer) {
|
|
2906
|
+
clearTimeout(shellFlushTimer);
|
|
2907
|
+
shellFlushTimer = null;
|
|
2908
|
+
}
|
|
2909
|
+
runner.cancel();
|
|
2910
|
+
};
|
|
2911
|
+
|
|
2912
|
+
void runner.result.then((result) => {
|
|
2913
|
+
if (activeRunIdRef.current !== shellId) return;
|
|
2914
|
+
flushShellLines();
|
|
2915
|
+
activeRunIdRef.current = null;
|
|
2916
|
+
cleanupRef.current = null;
|
|
2917
|
+
focusManager.focus(FOCUS_IDS.composer);
|
|
2918
|
+
|
|
2919
|
+
const finalEvent: ShellEvent = {
|
|
2920
|
+
...initialEvent,
|
|
2921
|
+
lines: sanitizeTerminalLines(result.stdout.split(/\r?\n/)),
|
|
2922
|
+
stderrLines: sanitizeTerminalLines(result.stderr.split(/\r?\n/)),
|
|
2923
|
+
summary: sanitizeTerminalOutput(summarizeCommandResult(safeCommand, result)),
|
|
2924
|
+
status: result.status === "completed" ? "completed" : "failed",
|
|
2925
|
+
exitCode: result.exitCode,
|
|
2926
|
+
durationMs: result.durationMs,
|
|
2927
|
+
};
|
|
2928
|
+
|
|
2929
|
+
dispatchSession({ type: "FINALIZE_SHELL", shellId, finalEvent });
|
|
2930
|
+
forceRefreshCurrentTerminalTitle("shell_output_complete", false);
|
|
2931
|
+
});
|
|
2932
|
+
}, [allowedWritableRoots, appendErrorEvent, dispatchSession, focusManager, forceRefreshCurrentTerminalTitle, workspaceRoot, writeCurrentTerminalTitleBeforeStateChange]);
|
|
2933
|
+
|
|
2934
|
+
const handleWorkspaceRelaunch = useCallback((targetPath: string) => {
|
|
2935
|
+
const gate = guardWorkspaceRelaunch(busy);
|
|
2936
|
+
if (!gate.allowed) {
|
|
2937
|
+
appendSystemEvent("Busy", gate.message ?? "Finish the current run before relaunching into another workspace.");
|
|
2938
|
+
return;
|
|
2939
|
+
}
|
|
2940
|
+
|
|
2941
|
+
const relaunchResult = createWorkspaceRelaunchPlan(targetPath, launchContext);
|
|
2942
|
+
if (!relaunchResult.ok) {
|
|
2943
|
+
appendErrorEvent("Workspace relaunch failed", relaunchResult.message);
|
|
2944
|
+
return;
|
|
2945
|
+
}
|
|
2946
|
+
|
|
2947
|
+
try {
|
|
2948
|
+
const child = spawn(relaunchResult.plan.executable, relaunchResult.plan.args, {
|
|
2949
|
+
cwd: relaunchResult.plan.cwd,
|
|
2950
|
+
env: relaunchResult.plan.env,
|
|
2951
|
+
stdio: "inherit",
|
|
2952
|
+
});
|
|
2953
|
+
|
|
2954
|
+
let launched = false;
|
|
2955
|
+
child.once("error", (error) => {
|
|
2956
|
+
if (launched) return;
|
|
2957
|
+
appendErrorEvent("Workspace relaunch failed", error.message);
|
|
2958
|
+
});
|
|
2959
|
+
child.once("spawn", () => {
|
|
2960
|
+
launched = true;
|
|
2961
|
+
exit();
|
|
2962
|
+
});
|
|
2963
|
+
} catch (error) {
|
|
2964
|
+
const message = error instanceof Error ? error.message : "Unknown relaunch failure";
|
|
2965
|
+
appendErrorEvent("Workspace relaunch failed", message);
|
|
2966
|
+
}
|
|
2967
|
+
}, [appendErrorEvent, appendSystemEvent, busy, exit, launchContext]);
|
|
2968
|
+
|
|
2969
|
+
const handleHistoryUp = useCallback(() => {
|
|
2970
|
+
dispatchSession({ type: "HISTORY_UP" });
|
|
2971
|
+
}, [dispatchSession]);
|
|
2972
|
+
|
|
2973
|
+
const handleHistoryDown = useCallback(() => {
|
|
2974
|
+
dispatchSession({ type: "HISTORY_DOWN" });
|
|
2975
|
+
}, [dispatchSession]);
|
|
2976
|
+
|
|
2977
|
+
const findUserPromptForTurn = useCallback((turnId: number): UserPromptEvent | null => {
|
|
2978
|
+
return findUserPrompt([...staticEvents, ...activeEvents], turnId);
|
|
2979
|
+
}, [activeEvents, staticEvents]);
|
|
2980
|
+
|
|
2981
|
+
const startPromptRun = useCallback((
|
|
2982
|
+
displayPrompt: string,
|
|
2983
|
+
providerPrompt: string,
|
|
2984
|
+
lifecycle: PromptRunLifecycle = {},
|
|
2985
|
+
) => {
|
|
2986
|
+
const submitTiming = lifecycle.submitTiming ?? createPromptRunTiming();
|
|
2987
|
+
const safeDisplayPrompt = sanitizeTerminalInput(displayPrompt).trim();
|
|
2988
|
+
const safeProviderPrompt = sanitizeTerminalInput(providerPrompt).trim();
|
|
2989
|
+
if (!safeDisplayPrompt || !safeProviderPrompt) {
|
|
2990
|
+
appendErrorEvent("Prompt blocked", "The prompt only contained non-printable/control characters after sanitization.");
|
|
2991
|
+
return false;
|
|
2992
|
+
}
|
|
2993
|
+
|
|
2994
|
+
const requestedRuntime = mergeRuntimeConfig(runtimeConfig, lifecycle.runtimeOverride ?? {});
|
|
2995
|
+
const requestedMode = requestedRuntime.mode;
|
|
2996
|
+
const executionModeDecision = lifecycle.disableModeAutoUpgrade
|
|
2997
|
+
? { mode: requestedMode, autoUpgraded: false }
|
|
2998
|
+
: resolveExecutionMode(requestedMode, safeProviderPrompt);
|
|
2999
|
+
const effectiveMode = executionModeDecision.mode;
|
|
3000
|
+
const runtimeConfigForTurn = {
|
|
3001
|
+
...requestedRuntime,
|
|
3002
|
+
mode: effectiveMode,
|
|
3003
|
+
model: activeProviderRoute.modelId,
|
|
3004
|
+
reasoningLevel: activeProviderRoute.reasoning ?? requestedRuntime.reasoningLevel,
|
|
3005
|
+
...(providerWorkspaceConfig.providers?.openai?.codexCommandPath
|
|
3006
|
+
? { codexCommandPath: providerWorkspaceConfig.providers.openai.codexCommandPath }
|
|
3007
|
+
: {}),
|
|
3008
|
+
};
|
|
3009
|
+
let runtimeForTurn = resolveRuntimeConfig(runtimeConfigForTurn);
|
|
3010
|
+
const fastCleanupRun = isClearlySafeGeneratedCleanupRequest(safeProviderPrompt)
|
|
3011
|
+
&& effectiveMode !== "suggest"
|
|
3012
|
+
&& runtimeForTurn.policy.sandboxMode !== "read-only";
|
|
3013
|
+
if (fastCleanupRun && ["medium", "high", "xhigh"].includes(runtimeForTurn.reasoningLevel)) {
|
|
3014
|
+
runtimeForTurn = resolveRuntimeConfig({
|
|
3015
|
+
...runtimeConfigForTurn,
|
|
3016
|
+
reasoningLevel: "low",
|
|
3017
|
+
});
|
|
3018
|
+
}
|
|
3019
|
+
if (executionModeDecision.autoUpgraded) {
|
|
3020
|
+
appendSystemEvent(
|
|
3021
|
+
"Mode auto-upgraded",
|
|
3022
|
+
"This prompt looks like a file-editing request, so the run is using Auto instead of Read-only.",
|
|
3023
|
+
);
|
|
3024
|
+
}
|
|
3025
|
+
if (fastCleanupRun) {
|
|
3026
|
+
appendSystemEvent(
|
|
3027
|
+
"Fast cleanup path",
|
|
3028
|
+
"Using a low-latency cleanup profile: shallow inspection, generated artifacts only, no branch/bootstrap setup.",
|
|
3029
|
+
);
|
|
3030
|
+
}
|
|
3031
|
+
|
|
3032
|
+
if (!provider.run) {
|
|
3033
|
+
appendErrorEvent(
|
|
3034
|
+
"Backend unavailable",
|
|
3035
|
+
`${provider.label} is a planned provider placeholder. Use Codexa Core for runnable execution in v1.`,
|
|
3036
|
+
);
|
|
3037
|
+
return false;
|
|
3038
|
+
}
|
|
3039
|
+
const runProvider = provider.run;
|
|
3040
|
+
|
|
3041
|
+
if (activeProviderRoute.providerId === "openai" && backend === "codex-subprocess") {
|
|
3042
|
+
const decision = getRunGateDecision(authStatus.state, {
|
|
3043
|
+
warnOnUnknown: authStatus.checkedAt > 0,
|
|
3044
|
+
});
|
|
3045
|
+
if (!decision.allowRun) {
|
|
3046
|
+
appendErrorEvent("Authentication required", decision.blockMessage ?? "Please sign in with `codex login`.");
|
|
3047
|
+
return false;
|
|
3048
|
+
}
|
|
3049
|
+
if (decision.warningMessage) {
|
|
3050
|
+
appendSystemEvent("Auth warning", decision.warningMessage);
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
const turnId = createTurnId();
|
|
3055
|
+
const userEvent: UserPromptEvent = {
|
|
3056
|
+
id: createEventId(),
|
|
3057
|
+
type: "user",
|
|
3058
|
+
createdAt: submitTiming.submitEpochMs,
|
|
3059
|
+
prompt: safeDisplayPrompt,
|
|
3060
|
+
turnId,
|
|
3061
|
+
};
|
|
3062
|
+
setConversationChars((count) => count + safeProviderPrompt.length);
|
|
3063
|
+
|
|
3064
|
+
const runId = createEventId();
|
|
3065
|
+
writeCurrentTerminalTitleBeforeStateChange("before-prompt-run-start");
|
|
3066
|
+
perf.startSession(String(runId));
|
|
3067
|
+
perf.mark("dispatch_start");
|
|
3068
|
+
perf.setMeta("fast_cleanup", fastCleanupRun);
|
|
3069
|
+
perf.setMeta("reasoning", runtimeForTurn.reasoningLevel);
|
|
3070
|
+
activeRunIdRef.current = runId;
|
|
3071
|
+
activeTurnIdRef.current = turnId;
|
|
3072
|
+
activeRunLifecycleRef.current = lifecycle;
|
|
3073
|
+
activeRunTimingRef.current = { ...submitTiming, runId, turnId };
|
|
3074
|
+
if (externalCliStatusRef.current === "idle") {
|
|
3075
|
+
dispatchSession({ type: "SET_EXTERNAL_CLI_STATUS", status: "starting" });
|
|
3076
|
+
}
|
|
3077
|
+
dispatchSession({
|
|
3078
|
+
type: "SUBMIT_PROMPT_RUN",
|
|
3079
|
+
historyValue: lifecycle.commitPrompt ? safeDisplayPrompt : undefined,
|
|
3080
|
+
turnId,
|
|
3081
|
+
runId,
|
|
3082
|
+
events: [
|
|
3083
|
+
userEvent,
|
|
3084
|
+
{
|
|
3085
|
+
...createRunEvent({
|
|
3086
|
+
id: runId,
|
|
3087
|
+
backendId: backend,
|
|
3088
|
+
backendLabel: provider.label,
|
|
3089
|
+
runtime: runtimeForTurn,
|
|
3090
|
+
prompt: safeProviderPrompt,
|
|
3091
|
+
turnId,
|
|
3092
|
+
startedAtMs: submitTiming.submitEpochMs,
|
|
3093
|
+
responsePresentation: lifecycle.responsePresentation,
|
|
3094
|
+
approvedPlan: lifecycle.approvedPlan,
|
|
3095
|
+
}),
|
|
3096
|
+
summary: "Codexa is starting...",
|
|
3097
|
+
},
|
|
3098
|
+
],
|
|
3099
|
+
});
|
|
3100
|
+
|
|
3101
|
+
let streamedAssistantContent = "";
|
|
3102
|
+
let legacyProgressSequence = 0;
|
|
3103
|
+
let firstRenderFired = false;
|
|
3104
|
+
let finalAnswerVisibleFired = false;
|
|
3105
|
+
let blockedCleanupFailureSurfaced = false;
|
|
3106
|
+
|
|
3107
|
+
let preRunSnapshot: ReturnType<typeof captureWorkspaceSnapshot> | null = null;
|
|
3108
|
+
let finalWorkspacePollDone = false;
|
|
3109
|
+
let activityTracker: ReturnType<typeof createWorkspaceActivityTracker> | null = null;
|
|
3110
|
+
|
|
3111
|
+
const liveScheduler = createLiveRenderScheduler({
|
|
3112
|
+
assistantFlushMs: LIVE_UPDATE_FLUSH_MS,
|
|
3113
|
+
progressOnlyFlushMs: PROGRESS_ONLY_FLUSH_MS,
|
|
3114
|
+
flush: (updates: LiveRenderUpdate[]) => {
|
|
3115
|
+
if (!isCurrentRun(activeRunIdRef.current, runId)) {
|
|
3116
|
+
return;
|
|
3117
|
+
}
|
|
3118
|
+
|
|
3119
|
+
if (!firstRenderFired && updates.some((update) => update.type === "assistant")) {
|
|
3120
|
+
firstRenderFired = true;
|
|
3121
|
+
perf.mark("first_render");
|
|
3122
|
+
}
|
|
3123
|
+
|
|
3124
|
+
dispatchSession({
|
|
3125
|
+
type: "RUN_APPLY_LIVE_UPDATES",
|
|
3126
|
+
turnId,
|
|
3127
|
+
runId,
|
|
3128
|
+
updates,
|
|
3129
|
+
assistantEventFactory: (chunk) => ({
|
|
3130
|
+
id: createEventId(),
|
|
3131
|
+
type: "assistant",
|
|
3132
|
+
createdAt: Date.now(),
|
|
3133
|
+
content: "",
|
|
3134
|
+
contentChunks: [chunk],
|
|
3135
|
+
turnId,
|
|
3136
|
+
}),
|
|
3137
|
+
});
|
|
3138
|
+
},
|
|
3139
|
+
});
|
|
3140
|
+
|
|
3141
|
+
const flushLiveUpdates = (): boolean => {
|
|
3142
|
+
perf.inc("flushes");
|
|
3143
|
+
return liveScheduler.flushNow();
|
|
3144
|
+
};
|
|
3145
|
+
|
|
3146
|
+
const traceLiveRunDiagnostics = (status: "completed" | "failed" | "canceled") => {
|
|
3147
|
+
const stats = liveScheduler.getStats();
|
|
3148
|
+
const now = performance.now();
|
|
3149
|
+
renderDebug.traceEvent("run", "liveBatchSummary", {
|
|
3150
|
+
runId,
|
|
3151
|
+
turnId,
|
|
3152
|
+
status,
|
|
3153
|
+
promptSubmitEpochMs: submitTiming.submitEpochMs,
|
|
3154
|
+
promptSubmitMonotonicMs: submitTiming.submitMonotonicMs,
|
|
3155
|
+
finalRenderMonotonicMs: now,
|
|
3156
|
+
elapsedWallMs: Math.max(0, Math.round(now - submitTiming.submitMonotonicMs)),
|
|
3157
|
+
providerEventsReceived: stats.providerEvents,
|
|
3158
|
+
uiFlushes: stats.flushes,
|
|
3159
|
+
averageFlushIntervalMs: stats.averageFlushIntervalMs,
|
|
3160
|
+
maxFlushIntervalMs: stats.maxFlushIntervalMs,
|
|
3161
|
+
});
|
|
3162
|
+
};
|
|
3163
|
+
|
|
3164
|
+
let stopProviderRun: (() => void) | undefined;
|
|
3165
|
+
let cancelScheduledProviderStart: (() => void) | null = null;
|
|
3166
|
+
let providerStartCancelled = false;
|
|
3167
|
+
|
|
3168
|
+
const startProviderRun = () => {
|
|
3169
|
+
if (providerStartCancelled || !isCurrentRun(activeRunIdRef.current, runId)) {
|
|
3170
|
+
return;
|
|
3171
|
+
}
|
|
3172
|
+
|
|
3173
|
+
// Capture the workspace state after the visible run has had a chance to
|
|
3174
|
+
// render, so first-prompt filesystem work cannot block initial progress.
|
|
3175
|
+
if (activeProviderRoute.providerId === "openai" && backend === "codex-subprocess") {
|
|
3176
|
+
preRunSnapshot = captureWorkspaceSnapshot(workspaceRoot);
|
|
3177
|
+
activityTracker = createWorkspaceActivityTracker({
|
|
3178
|
+
rootDir: workspaceRoot,
|
|
3179
|
+
initialSnapshot: preRunSnapshot,
|
|
3180
|
+
onActivity: (activity) => {
|
|
3181
|
+
if (!isCurrentRun(activeRunIdRef.current, runId)) return;
|
|
3182
|
+
liveScheduler.enqueue({ type: "activity", activity });
|
|
3183
|
+
},
|
|
3184
|
+
});
|
|
3185
|
+
}
|
|
3186
|
+
|
|
3187
|
+
perf.mark("provider_run_start");
|
|
3188
|
+
stopProviderRun = runProvider(
|
|
3189
|
+
safeProviderPrompt,
|
|
3190
|
+
{ runtime: runtimeForTurn, workspaceRoot, projectInstructions },
|
|
3191
|
+
{
|
|
3192
|
+
onProcessLifecycle: (event) => {
|
|
3193
|
+
reassertIntendedTerminalTitle({ reason: `codex-process-${event}` });
|
|
3194
|
+
},
|
|
3195
|
+
onAssistantDelta: (chunk) => {
|
|
3196
|
+
const geminiBoundary = activeProviderRoute.providerId === "google";
|
|
3197
|
+
appDiagLog(`onAssistantDelta: provider=${activeProviderRoute.providerId} chunk.length=${chunk?.length ?? 0} isEmpty=${!chunk}`);
|
|
3198
|
+
if (geminiBoundary) {
|
|
3199
|
+
appDiagLog(`GEMINI_APP_BOUNDARY: onAssistantDelta received=yes nonEmpty=${Boolean(chunk)} runId=${runId} turnId=${turnId}`);
|
|
3200
|
+
}
|
|
3201
|
+
if (!chunk || !isCurrentRun(activeRunIdRef.current, runId)) {
|
|
3202
|
+
if (geminiBoundary) {
|
|
3203
|
+
appDiagLog(`GEMINI_APP_BOUNDARY: onAssistantDelta assistantAppendCalled=no reason=${!chunk ? "empty-chunk" : "stale-run"} runId=${runId} turnId=${turnId}`);
|
|
3204
|
+
}
|
|
3205
|
+
return;
|
|
3206
|
+
}
|
|
3207
|
+
const t0 = performance.now();
|
|
3208
|
+
const safeChunk = sanitizeTerminalOutput(chunk, { preserveTabs: false, tabSize: 2 });
|
|
3209
|
+
perf.accumulate("sanitize_ms", performance.now() - t0);
|
|
3210
|
+
perf.inc("chunks");
|
|
3211
|
+
if (!safeChunk) {
|
|
3212
|
+
appDiagLog(`onAssistantDelta: safeChunk empty after sanitize → no content queued to liveScheduler`);
|
|
3213
|
+
if (geminiBoundary) {
|
|
3214
|
+
appDiagLog(`GEMINI_APP_BOUNDARY: onAssistantDelta assistantAppendCalled=no reason=empty-after-sanitize runId=${runId} turnId=${turnId}`);
|
|
3215
|
+
}
|
|
3216
|
+
return;
|
|
3217
|
+
}
|
|
3218
|
+
appDiagLog(`onAssistantDelta: ASSISTANT_APPEND_PATH reached — queuing ${safeChunk.length} chars (liveScheduler→RUN_APPLY_LIVE_UPDATES→assistantEvent in activeEvents→FINALIZE_RUN→staticEvents)`);
|
|
3219
|
+
dispatchSession({ type: "SET_EXTERNAL_CLI_STATUS", status: "ready" });
|
|
3220
|
+
liveScheduler.enqueue({
|
|
3221
|
+
type: lifecycle.responsePresentation === "plan" ? "plan" : "assistant",
|
|
3222
|
+
chunk: safeChunk,
|
|
3223
|
+
});
|
|
3224
|
+
streamedAssistantContent += safeChunk;
|
|
3225
|
+
if (geminiBoundary) {
|
|
3226
|
+
appDiagLog(`GEMINI_APP_BOUNDARY: onAssistantDelta assistantAppendCalled=yes queuedLength=${safeChunk.length} totalStreamedLength=${streamedAssistantContent.length} runId=${runId} turnId=${turnId}`);
|
|
3227
|
+
}
|
|
3228
|
+
},
|
|
3229
|
+
onFinalAnswerObserved: (response) => {
|
|
3230
|
+
if (!isCurrentRun(activeRunIdRef.current, runId) || finalAnswerVisibleFired) return;
|
|
3231
|
+
const flushedLiveUpdates = flushLiveUpdates();
|
|
3232
|
+
const markFinalAnswerVisible = () => {
|
|
3233
|
+
if (!isCurrentRun(activeRunIdRef.current, runId) || finalAnswerVisibleFired) return;
|
|
3234
|
+
finalAnswerVisibleFired = true;
|
|
3235
|
+
const safeResponse = sanitizeTerminalOutput(response, { preserveTabs: false, tabSize: 2 });
|
|
3236
|
+
dispatchSession({
|
|
3237
|
+
type: "RUN_MARK_FINAL_ANSWER_OBSERVED",
|
|
3238
|
+
runId,
|
|
3239
|
+
turnId,
|
|
3240
|
+
response: safeResponse.trim() ? safeResponse : undefined,
|
|
3241
|
+
});
|
|
3242
|
+
perf.mark("final_answer_visible");
|
|
3243
|
+
};
|
|
3244
|
+
|
|
3245
|
+
if (flushedLiveUpdates) {
|
|
3246
|
+
setTimeout(markFinalAnswerVisible, 0);
|
|
3247
|
+
} else {
|
|
3248
|
+
markFinalAnswerVisible();
|
|
3249
|
+
}
|
|
3250
|
+
},
|
|
3251
|
+
onToolActivity: (activity) => {
|
|
3252
|
+
if (!isCurrentRun(activeRunIdRef.current, runId)) return;
|
|
3253
|
+
liveScheduler.enqueue({ type: "tool", activity });
|
|
3254
|
+
if (activity.status === "running") {
|
|
3255
|
+
return;
|
|
3256
|
+
}
|
|
3257
|
+
forceRefreshCurrentTerminalTitle("tool_activity_complete", true);
|
|
3258
|
+
if (fastCleanupRun && !blockedCleanupFailureSurfaced) {
|
|
3259
|
+
const blockedCleanupFailure = getBlockedCleanupFailure(activity);
|
|
3260
|
+
if (blockedCleanupFailure) {
|
|
3261
|
+
blockedCleanupFailureSurfaced = true;
|
|
3262
|
+
flushLiveUpdates();
|
|
3263
|
+
traceLiveRunDiagnostics("failed");
|
|
3264
|
+
void finalizePromptRun(runId, turnId, "failed", blockedCleanupFailure);
|
|
3265
|
+
return;
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3268
|
+
},
|
|
3269
|
+
onResponse: (response) => {
|
|
3270
|
+
const geminiBoundary = activeProviderRoute.providerId === "google";
|
|
3271
|
+
appDiagLog(`onResponse: provider=${activeProviderRoute.providerId} response.length=${response?.length ?? 0}`);
|
|
3272
|
+
if (geminiBoundary) {
|
|
3273
|
+
appDiagLog(`GEMINI_APP_BOUNDARY: onResponse received=yes nonEmpty=${Boolean(response?.trim())} runId=${runId} turnId=${turnId}`);
|
|
3274
|
+
}
|
|
3275
|
+
if (!isCurrentRun(activeRunIdRef.current, runId)) {
|
|
3276
|
+
if (geminiBoundary) {
|
|
3277
|
+
appDiagLog(`GEMINI_APP_BOUNDARY: onResponse finalizeCalled=no reason=stale-run runId=${runId} turnId=${turnId}`);
|
|
3278
|
+
}
|
|
3279
|
+
return;
|
|
3280
|
+
}
|
|
3281
|
+
perf.mark("response_cb_start");
|
|
3282
|
+
|
|
3283
|
+
// Force one final synchronous workspace poll before finalizing the run.
|
|
3284
|
+
// This closes the race condition where the activity tracker's interval
|
|
3285
|
+
// hasn't fired yet and late file changes would be missed.
|
|
3286
|
+
if (activityTracker && preRunSnapshot) {
|
|
3287
|
+
try {
|
|
3288
|
+
perf.mark("snapshot_start");
|
|
3289
|
+
const finalSnapshot = captureWorkspaceSnapshot(workspaceRoot);
|
|
3290
|
+
perf.mark("snapshot_end");
|
|
3291
|
+
const lateActivity = diffWorkspaceSnapshots(preRunSnapshot, finalSnapshot);
|
|
3292
|
+
if (lateActivity.length > 0) {
|
|
3293
|
+
liveScheduler.enqueue({ type: "activity", activity: lateActivity });
|
|
3294
|
+
}
|
|
3295
|
+
} catch {
|
|
3296
|
+
// Non-fatal: best-effort final poll
|
|
3297
|
+
} finally {
|
|
3298
|
+
finalWorkspacePollDone = true;
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
3301
|
+
|
|
3302
|
+
const flushedLiveUpdates = flushLiveUpdates();
|
|
3303
|
+
const finalizeResponse = () => {
|
|
3304
|
+
if (!isCurrentRun(activeRunIdRef.current, runId)) return;
|
|
3305
|
+
const safeResponse = sanitizeTerminalOutput(response, { preserveTabs: false, tabSize: 2 });
|
|
3306
|
+
setConversationChars((count) => count + safeResponse.length);
|
|
3307
|
+
|
|
3308
|
+
// Validate response quality for write-intent/destructive prompts:
|
|
3309
|
+
// If the backend returned filler like "Hello." instead of execution
|
|
3310
|
+
// feedback, inject a warning so the user isn't silently misled.
|
|
3311
|
+
if (effectiveMode !== "suggest") {
|
|
3312
|
+
const hollow = detectHollowResponse(safeProviderPrompt, safeResponse);
|
|
3313
|
+
if (hollow.isHollow) {
|
|
3314
|
+
const formatted = formatHollowResponse(hollow, safeResponse);
|
|
3315
|
+
traceLiveRunDiagnostics("completed");
|
|
3316
|
+
void finalizePromptRun(runId, turnId, "completed", undefined, formatted);
|
|
3317
|
+
forceRefreshCurrentTerminalTitle("prompt_run_completed", false);
|
|
3318
|
+
return;
|
|
3319
|
+
}
|
|
3320
|
+
}
|
|
3321
|
+
|
|
3322
|
+
// If the streamed content matches the sanitized response (after
|
|
3323
|
+
// normalizing whitespace), pass undefined so FINALIZE_RUN preserves
|
|
3324
|
+
// the already-rendered streamed content — avoiding a visual flash.
|
|
3325
|
+
const normalizeWs = (s: string) => s.replace(/\s+/g, " ").trim();
|
|
3326
|
+
const streamedNorm = normalizeWs(streamedAssistantContent);
|
|
3327
|
+
const responseNorm = normalizeWs(safeResponse);
|
|
3328
|
+
const finalResponse =
|
|
3329
|
+
lifecycle.responsePresentation !== "plan" && streamedNorm && (
|
|
3330
|
+
streamedNorm === responseNorm ||
|
|
3331
|
+
(responseNorm.startsWith(streamedNorm) && streamedNorm.length / responseNorm.length > 0.8)
|
|
3332
|
+
)
|
|
3333
|
+
? undefined
|
|
3334
|
+
: safeResponse;
|
|
3335
|
+
appDiagLog(`onResponse.finalizeResponse: safeResponse.length=${safeResponse.length} streamedContent.length=${streamedAssistantContent.length} finalResponse=${finalResponse === undefined ? "undefined(use-streamed)" : `${finalResponse.length}chars`}`);
|
|
3336
|
+
if (geminiBoundary) {
|
|
3337
|
+
const extractionStatus = safeResponse.trim() || streamedAssistantContent.trim()
|
|
3338
|
+
? "assistant-text"
|
|
3339
|
+
: "completed-empty-assistant";
|
|
3340
|
+
appDiagLog([
|
|
3341
|
+
"GEMINI_APP_BOUNDARY:",
|
|
3342
|
+
`onResponse finalizeCalled=yes`,
|
|
3343
|
+
`extractionStatus=${extractionStatus}`,
|
|
3344
|
+
`safeResponseLength=${safeResponse.length}`,
|
|
3345
|
+
`streamedAssistantContentLength=${streamedAssistantContent.length}`,
|
|
3346
|
+
`finalResponseProvided=${finalResponse !== undefined}`,
|
|
3347
|
+
`finalRunState=completed`,
|
|
3348
|
+
`reasonComposerBecomesActive=FINALIZE_RUN_COMPLETED`,
|
|
3349
|
+
`runId=${runId}`,
|
|
3350
|
+
`turnId=${turnId}`,
|
|
3351
|
+
].join(" "));
|
|
3352
|
+
}
|
|
3353
|
+
traceLiveRunDiagnostics("completed");
|
|
3354
|
+
void finalizePromptRun(runId, turnId, "completed", undefined, finalResponse);
|
|
3355
|
+
forceRefreshCurrentTerminalTitle("prompt_run_completed", false);
|
|
3356
|
+
};
|
|
3357
|
+
|
|
3358
|
+
if (flushedLiveUpdates) {
|
|
3359
|
+
setTimeout(finalizeResponse, 0);
|
|
3360
|
+
} else {
|
|
3361
|
+
finalizeResponse();
|
|
3362
|
+
}
|
|
3363
|
+
},
|
|
3364
|
+
onError: (message, rawOutput) => {
|
|
3365
|
+
if (!isCurrentRun(activeRunIdRef.current, runId)) return;
|
|
3366
|
+
const flushedLiveUpdates = flushLiveUpdates();
|
|
3367
|
+
const finalizeError = () => {
|
|
3368
|
+
if (!isCurrentRun(activeRunIdRef.current, runId)) return;
|
|
3369
|
+
const safeMessage = sanitizeTerminalOutput(message);
|
|
3370
|
+
const safeRawOutput = sanitizeTerminalOutput(rawOutput ?? "");
|
|
3371
|
+
const combinedOutput = [safeMessage, safeRawOutput].filter(Boolean).join("\n");
|
|
3372
|
+
const errorMessage = isLikelyAuthFailure(combinedOutput)
|
|
3373
|
+
? [
|
|
3374
|
+
"Codexa reported an authentication/session error.",
|
|
3375
|
+
"Recovery:",
|
|
3376
|
+
" codex login",
|
|
3377
|
+
"",
|
|
3378
|
+
`Raw error: ${safeMessage}`,
|
|
3379
|
+
].join("\n")
|
|
3380
|
+
: safeMessage;
|
|
3381
|
+
|
|
3382
|
+
if (isLikelyAuthFailure(combinedOutput)) {
|
|
3383
|
+
setRuntimeUnauthenticated("Auth/session failure detected in neural link.");
|
|
3384
|
+
}
|
|
3385
|
+
|
|
3386
|
+
traceLiveRunDiagnostics("failed");
|
|
3387
|
+
void finalizePromptRun(runId, turnId, "failed", errorMessage);
|
|
3388
|
+
forceRefreshCurrentTerminalTitle("prompt_run_failed", false);
|
|
3389
|
+
};
|
|
3390
|
+
|
|
3391
|
+
if (flushedLiveUpdates) {
|
|
3392
|
+
setTimeout(finalizeError, 0);
|
|
3393
|
+
} else {
|
|
3394
|
+
finalizeError();
|
|
3395
|
+
}
|
|
3396
|
+
},
|
|
3397
|
+
onProgress: (update) => {
|
|
3398
|
+
perf.inc("progress_updates");
|
|
3399
|
+
const safeText = sanitizeTerminalOutput(update.text);
|
|
3400
|
+
if (!safeText) return;
|
|
3401
|
+
if (isNoiseLine(safeText)) return;
|
|
3402
|
+
if (!isCurrentRun(activeRunIdRef.current, runId)) return;
|
|
3403
|
+
const safeUpdate: BackendProgressUpdate = {
|
|
3404
|
+
id: update.id?.trim() ? update.id : `legacy-progress-${++legacyProgressSequence}`,
|
|
3405
|
+
source: update.source,
|
|
3406
|
+
text: safeText,
|
|
3407
|
+
};
|
|
3408
|
+
liveScheduler.enqueue({ type: "progress", update: safeUpdate });
|
|
3409
|
+
},
|
|
3410
|
+
},
|
|
3411
|
+
);
|
|
3412
|
+
};
|
|
3413
|
+
|
|
3414
|
+
cancelScheduledProviderStart = schedulePromptRunStartAfterVisibleCommit(startProviderRun);
|
|
3415
|
+
|
|
3416
|
+
cleanupRef.current = () => {
|
|
3417
|
+
providerStartCancelled = true;
|
|
3418
|
+
cancelScheduledProviderStart?.();
|
|
3419
|
+
flushLiveUpdates();
|
|
3420
|
+
// Do one final sync poll before stopping the tracker to capture
|
|
3421
|
+
// any last-moment file changes that were in-flight.
|
|
3422
|
+
if (activityTracker && preRunSnapshot && !finalWorkspacePollDone) {
|
|
3423
|
+
try {
|
|
3424
|
+
const cleanupSnapshot = captureWorkspaceSnapshot(workspaceRoot);
|
|
3425
|
+
const lastActivity = diffWorkspaceSnapshots(preRunSnapshot, cleanupSnapshot);
|
|
3426
|
+
if (lastActivity.length > 0 && isCurrentRun(activeRunIdRef.current, runId)) {
|
|
3427
|
+
dispatchSession({ type: "RUN_APPEND_ACTIVITY", runId, activity: lastActivity });
|
|
3428
|
+
}
|
|
3429
|
+
} catch {
|
|
3430
|
+
// Non-fatal
|
|
3431
|
+
}
|
|
3432
|
+
}
|
|
3433
|
+
activityTracker?.stop();
|
|
3434
|
+
stopProviderRun?.();
|
|
3435
|
+
liveScheduler.cancel();
|
|
3436
|
+
};
|
|
3437
|
+
|
|
3438
|
+
return true;
|
|
3439
|
+
}, [
|
|
3440
|
+
appendErrorEvent,
|
|
3441
|
+
appendSystemEvent,
|
|
3442
|
+
authStatus.state,
|
|
3443
|
+
finalizePromptRun,
|
|
3444
|
+
forceRefreshCurrentTerminalTitle,
|
|
3445
|
+
writeCurrentTerminalTitleBeforeStateChange,
|
|
3446
|
+
mode,
|
|
3447
|
+
provider,
|
|
3448
|
+
projectInstructions,
|
|
3449
|
+
dispatchSession,
|
|
3450
|
+
setRuntimeUnauthenticated,
|
|
3451
|
+
runtimeConfig,
|
|
3452
|
+
workspaceRoot,
|
|
3453
|
+
]);
|
|
3454
|
+
|
|
3455
|
+
const handleImportConfirm = useCallback(async () => {
|
|
3456
|
+
if (!pendingImport) return;
|
|
3457
|
+
const replacements: Array<{ rawPath: string; workspaceRelativePath: string }> = [];
|
|
3458
|
+
for (const file of pendingImport.files) {
|
|
3459
|
+
const destPath = await importExternalFile(file.srcPath, pendingImport.attachmentsDir);
|
|
3460
|
+
const relPath = path.relative(workspaceRoot, destPath).replace(/\\/g, "/");
|
|
3461
|
+
replacements.push({ rawPath: file.rawPath, workspaceRelativePath: relPath });
|
|
3462
|
+
}
|
|
3463
|
+
const rewrittenPrompt = rewritePromptWithImportedPaths(pendingImport.prompt, replacements);
|
|
3464
|
+
setPendingImport(null);
|
|
3465
|
+
setScreen("main");
|
|
3466
|
+
startPromptRun(rewrittenPrompt, rewrittenPrompt, { submitTiming: createPromptRunTiming(), commitPrompt: true });
|
|
3467
|
+
}, [pendingImport, workspaceRoot, startPromptRun]);
|
|
3468
|
+
|
|
3469
|
+
const handleImportCancel = useCallback(() => {
|
|
3470
|
+
if (!pendingImport) return;
|
|
3471
|
+
dispatchSession({ type: "SET_INPUT", value: pendingImport.prompt, cursor: pendingImport.prompt.length });
|
|
3472
|
+
setPendingImport(null);
|
|
3473
|
+
setScreen("main");
|
|
3474
|
+
}, [pendingImport, dispatchSession]);
|
|
3475
|
+
|
|
3476
|
+
const runPlanGeneration = useCallback((
|
|
3477
|
+
state: Extract<PlanFlowState, { kind: "generating" }>,
|
|
3478
|
+
displayPrompt: string,
|
|
3479
|
+
submitTiming?: PromptRunTiming,
|
|
3480
|
+
commitPrompt = false,
|
|
3481
|
+
) => {
|
|
3482
|
+
const started = startPromptRun(
|
|
3483
|
+
displayPrompt,
|
|
3484
|
+
buildPlanningPrompt({
|
|
3485
|
+
task: state.originalPrompt,
|
|
3486
|
+
constraints: state.constraints,
|
|
3487
|
+
currentPlan: state.currentPlan,
|
|
3488
|
+
pendingFeedback: state.pendingFeedback,
|
|
3489
|
+
}),
|
|
3490
|
+
{
|
|
3491
|
+
runtimeOverride: {
|
|
3492
|
+
mode: "suggest",
|
|
3493
|
+
planMode: false,
|
|
3494
|
+
},
|
|
3495
|
+
disableModeAutoUpgrade: true,
|
|
3496
|
+
parseActionRequired: false,
|
|
3497
|
+
responsePresentation: "plan",
|
|
3498
|
+
submitTiming,
|
|
3499
|
+
commitPrompt,
|
|
3500
|
+
onCompleted: ({ response }) => {
|
|
3501
|
+
const nextPlan = response.trim();
|
|
3502
|
+
if (!nextPlan) {
|
|
3503
|
+
setPlanFlow(resetPlanFlow());
|
|
3504
|
+
appendErrorEvent("Plan generation failed", "Plan mode expected a concrete plan, but the response was empty.");
|
|
3505
|
+
return;
|
|
3506
|
+
}
|
|
3507
|
+
const planFilePath = savePlanFile(nextPlan);
|
|
3508
|
+
setPlanFlow((current) => finishPlanGeneration(current, nextPlan, planFilePath));
|
|
3509
|
+
},
|
|
3510
|
+
onFailed: () => {
|
|
3511
|
+
setPlanFlow(resetPlanFlow());
|
|
3512
|
+
},
|
|
3513
|
+
onCanceled: () => {
|
|
3514
|
+
setPlanFlow(resetPlanFlow());
|
|
3515
|
+
},
|
|
3516
|
+
},
|
|
3517
|
+
);
|
|
3518
|
+
|
|
3519
|
+
if (!started) {
|
|
3520
|
+
setPlanFlow(resetPlanFlow());
|
|
3521
|
+
}
|
|
3522
|
+
|
|
3523
|
+
return started;
|
|
3524
|
+
}, [appendErrorEvent, savePlanFile, startPromptRun]);
|
|
3525
|
+
|
|
3526
|
+
const startApprovedPlanExecution = useCallback((state: Extract<PlanFlowState, { kind: "awaiting_action" }>) => {
|
|
3527
|
+
const submitTiming = createPromptRunTiming();
|
|
3528
|
+
setPlanFlow(approvePlanExecution(state));
|
|
3529
|
+
const started = startPromptRun(
|
|
3530
|
+
state.originalPrompt,
|
|
3531
|
+
buildPlanExecutionPrompt({
|
|
3532
|
+
task: state.originalPrompt,
|
|
3533
|
+
approvedPlan: state.currentPlan,
|
|
3534
|
+
constraints: state.constraints,
|
|
3535
|
+
}),
|
|
3536
|
+
{
|
|
3537
|
+
approvedPlan: state.currentPlan,
|
|
3538
|
+
submitTiming,
|
|
3539
|
+
runtimeOverride: {
|
|
3540
|
+
mode: state.executionMode,
|
|
3541
|
+
planMode: false,
|
|
3542
|
+
},
|
|
3543
|
+
onCompleted: () => {
|
|
3544
|
+
setPlanFlow(resetPlanFlow());
|
|
3545
|
+
},
|
|
3546
|
+
onFailed: () => {
|
|
3547
|
+
setPlanFlow(resetPlanFlow());
|
|
3548
|
+
},
|
|
3549
|
+
onCanceled: () => {
|
|
3550
|
+
setPlanFlow(resetPlanFlow());
|
|
3551
|
+
},
|
|
3552
|
+
},
|
|
3553
|
+
);
|
|
3554
|
+
|
|
3555
|
+
if (!started) {
|
|
3556
|
+
setPlanFlow(state);
|
|
3557
|
+
}
|
|
3558
|
+
}, [startPromptRun]);
|
|
3559
|
+
|
|
3560
|
+
const handlePlanAction = useCallback((action: PlanActionValue) => {
|
|
3561
|
+
if (planFlow.kind !== "awaiting_action") {
|
|
3562
|
+
return;
|
|
3563
|
+
}
|
|
3564
|
+
|
|
3565
|
+
switch (action) {
|
|
3566
|
+
case "implement":
|
|
3567
|
+
startApprovedPlanExecution(planFlow);
|
|
3568
|
+
return;
|
|
3569
|
+
case "revise":
|
|
3570
|
+
setPlanFlow(beginPlanFeedback(planFlow, "revise"));
|
|
3571
|
+
return;
|
|
3572
|
+
case "cancel":
|
|
3573
|
+
setPlanFlow(resetPlanFlow());
|
|
3574
|
+
appendSystemEvent("Plan review", "Plan review canceled. No changes were made.");
|
|
3575
|
+
return;
|
|
3576
|
+
default:
|
|
3577
|
+
return;
|
|
3578
|
+
}
|
|
3579
|
+
}, [appendSystemEvent, planFlow, startApprovedPlanExecution]);
|
|
3580
|
+
|
|
3581
|
+
const handlePlanFeedbackSubmit = useCallback((value: string) => {
|
|
3582
|
+
if (planFlow.kind !== "collecting_feedback") {
|
|
3583
|
+
return;
|
|
3584
|
+
}
|
|
3585
|
+
|
|
3586
|
+
const feedback = sanitizeTerminalInput(value).trim();
|
|
3587
|
+
if (!feedback) {
|
|
3588
|
+
appendSystemEvent("Plan review", "Add a short revision note or constraint before submitting.");
|
|
3589
|
+
return;
|
|
3590
|
+
}
|
|
3591
|
+
|
|
3592
|
+
const nextState = submitPlanFeedback(planFlow, feedback);
|
|
3593
|
+
if (nextState.kind !== "generating") {
|
|
3594
|
+
return;
|
|
3595
|
+
}
|
|
3596
|
+
|
|
3597
|
+
setPlanFlow(nextState);
|
|
3598
|
+
runPlanGeneration(nextState, feedback, createPromptRunTiming());
|
|
3599
|
+
}, [appendSystemEvent, planFlow, runPlanGeneration]);
|
|
3600
|
+
|
|
3601
|
+
useEffect(() => {
|
|
3602
|
+
if (initialPromptSubmittedRef.current || busy) {
|
|
3603
|
+
return;
|
|
3604
|
+
}
|
|
3605
|
+
|
|
3606
|
+
const initialPrompt = sanitizeTerminalInput(launchArgs.initialPrompt ?? "").trim();
|
|
3607
|
+
if (!initialPrompt) {
|
|
3608
|
+
return;
|
|
3609
|
+
}
|
|
3610
|
+
|
|
3611
|
+
initialPromptSubmittedRef.current = true;
|
|
3612
|
+
|
|
3613
|
+
const workspaceGuardMessage = getPromptWorkspaceGuardMessage(initialPrompt, workspaceRoot, allowedWritableRoots);
|
|
3614
|
+
if (workspaceGuardMessage) {
|
|
3615
|
+
appendErrorEvent("Workspace boundary", workspaceGuardMessage);
|
|
3616
|
+
return;
|
|
3617
|
+
}
|
|
3618
|
+
|
|
3619
|
+
if (planMode) {
|
|
3620
|
+
const nextPlanState = startPlanGeneration(initialPrompt, mode);
|
|
3621
|
+
setPlanFlow(nextPlanState);
|
|
3622
|
+
runPlanGeneration(nextPlanState, initialPrompt, createPromptRunTiming(), true);
|
|
3623
|
+
return;
|
|
3624
|
+
}
|
|
3625
|
+
|
|
3626
|
+
startPromptRun(initialPrompt, initialPrompt, { submitTiming: createPromptRunTiming(), commitPrompt: true });
|
|
3627
|
+
}, [
|
|
3628
|
+
allowedWritableRoots,
|
|
3629
|
+
appendErrorEvent,
|
|
3630
|
+
busy,
|
|
3631
|
+
dispatchSession,
|
|
3632
|
+
launchArgs.initialPrompt,
|
|
3633
|
+
mode,
|
|
3634
|
+
planMode,
|
|
3635
|
+
runPlanGeneration,
|
|
3636
|
+
startPromptRun,
|
|
3637
|
+
workspaceRoot,
|
|
3638
|
+
]);
|
|
3639
|
+
|
|
3640
|
+
const handleSubmit = useCallback(() => {
|
|
3641
|
+
const submitTiming = createPromptRunTiming();
|
|
3642
|
+
perf.mark("submit");
|
|
3643
|
+
const value = sanitizeTerminalInput(inputValue).trim();
|
|
3644
|
+
if (!value) return;
|
|
3645
|
+
|
|
3646
|
+
// Special perf debug command (not routed through handleCommand)
|
|
3647
|
+
if (value === "/perf") {
|
|
3648
|
+
const session = perf.getSession();
|
|
3649
|
+
const summary = session
|
|
3650
|
+
? perf.buildSummary(session)
|
|
3651
|
+
: "No perf data recorded yet. Set CODEXA_PERF=1 and send a prompt first.";
|
|
3652
|
+
appendSystemEvent("Perf report", summary);
|
|
3653
|
+
dispatchSession({ type: "PUSH_HISTORY", value });
|
|
3654
|
+
resetComposer();
|
|
3655
|
+
return;
|
|
3656
|
+
}
|
|
3657
|
+
|
|
3658
|
+
// ========== COMMAND ROUTING (before AWAITING_USER_ACTION) ==========
|
|
3659
|
+
// Shell execution: ! prefix routes directly to the terminal
|
|
3660
|
+
if (value.startsWith("!")) {
|
|
3661
|
+
if (busy) return;
|
|
3662
|
+
const shellCmd = value.slice(1).trim();
|
|
3663
|
+
if (!shellCmd) return;
|
|
3664
|
+
dispatchSession({ type: "PUSH_HISTORY", value });
|
|
3665
|
+
resetComposer();
|
|
3666
|
+
handleShellExecute(shellCmd);
|
|
3667
|
+
return;
|
|
3668
|
+
}
|
|
3669
|
+
|
|
3670
|
+
// Parse slash commands (/ prefix) and question-prefix invalid commands (? prefix)
|
|
3671
|
+
const commandResult = handleCommand(value, {
|
|
3672
|
+
config: layeredRuntimeConfig,
|
|
3673
|
+
runtime: runtimeConfig,
|
|
3674
|
+
resolvedRuntime: resolvedRuntimeConfig,
|
|
3675
|
+
settings: {
|
|
3676
|
+
workspaceDisplayMode,
|
|
3677
|
+
terminalTitleMode,
|
|
3678
|
+
showBusyLoader,
|
|
3679
|
+
},
|
|
3680
|
+
workspace: workspaceCommandContext,
|
|
3681
|
+
tokensUsed: estimateTokens(conversationChars),
|
|
3682
|
+
modelCapabilities: activeRouteModelCapabilities,
|
|
3683
|
+
routeStatusMessage,
|
|
3684
|
+
activeRouteProviderLabel: activeRouteProvider?.displayName ?? "OpenAI",
|
|
3685
|
+
projectInstructions: projectInstructionsLoad,
|
|
3686
|
+
});
|
|
3687
|
+
const isCommand = commandResult !== null;
|
|
3688
|
+
|
|
3689
|
+
if (isCommand) {
|
|
3690
|
+
// Internal commands should NOT be added to PUSH_HISTORY or sent to provider
|
|
3691
|
+
resetComposer();
|
|
3692
|
+
|
|
3693
|
+
switch (commandResult.action) {
|
|
3694
|
+
case "exit":
|
|
3695
|
+
handleQuit();
|
|
3696
|
+
return;
|
|
3697
|
+
case "clear":
|
|
3698
|
+
handleClear();
|
|
3699
|
+
return;
|
|
3700
|
+
case "backend":
|
|
3701
|
+
if (commandResult.value) {
|
|
3702
|
+
setBackendWithNotice(commandResult.value as AvailableBackend);
|
|
3703
|
+
}
|
|
3704
|
+
return;
|
|
3705
|
+
case "model":
|
|
3706
|
+
if (commandResult.value) {
|
|
3707
|
+
setModelWithNotice(commandResult.value as AvailableModel);
|
|
3708
|
+
}
|
|
3709
|
+
return;
|
|
3710
|
+
case "mode":
|
|
3711
|
+
if (commandResult.value) {
|
|
3712
|
+
setModeWithNotice(commandResult.value as AvailableMode);
|
|
3713
|
+
}
|
|
3714
|
+
return;
|
|
3715
|
+
case "reasoning":
|
|
3716
|
+
if (commandResult.value) {
|
|
3717
|
+
setReasoningWithNotice(commandResult.value as ReasoningLevel);
|
|
3718
|
+
}
|
|
3719
|
+
return;
|
|
3720
|
+
case "plan_mode":
|
|
3721
|
+
if (commandResult.value) {
|
|
3722
|
+
setPlanModeWithNotice(commandResult.value === "on");
|
|
3723
|
+
} else if (commandResult.message) {
|
|
3724
|
+
appendSystemEvent("Plan mode", commandResult.message);
|
|
3725
|
+
}
|
|
3726
|
+
return;
|
|
3727
|
+
case "status":
|
|
3728
|
+
case "runtime_writable_roots_list":
|
|
3729
|
+
if (commandResult.message) {
|
|
3730
|
+
appendSystemEvent("Runtime status", commandResult.message);
|
|
3731
|
+
}
|
|
3732
|
+
return;
|
|
3733
|
+
case "route_status":
|
|
3734
|
+
if (commandResult.message) {
|
|
3735
|
+
appendSystemEvent("Route status", commandResult.message);
|
|
3736
|
+
}
|
|
3737
|
+
return;
|
|
3738
|
+
case "config_status":
|
|
3739
|
+
if (commandResult.message) {
|
|
3740
|
+
appendSystemEvent("Config", commandResult.message);
|
|
3741
|
+
}
|
|
3742
|
+
return;
|
|
3743
|
+
case "config_trust_status":
|
|
3744
|
+
if (commandResult.message) {
|
|
3745
|
+
appendSystemEvent("Config trust", commandResult.message);
|
|
3746
|
+
}
|
|
3747
|
+
return;
|
|
3748
|
+
case "config_trust_set":
|
|
3749
|
+
if (commandResult.value) {
|
|
3750
|
+
setProjectTrustWithNotice(commandResult.value === "on");
|
|
3751
|
+
}
|
|
3752
|
+
return;
|
|
3753
|
+
case "permissions_status":
|
|
3754
|
+
if (commandResult.message) {
|
|
3755
|
+
appendSystemEvent("Permissions", commandResult.message);
|
|
3756
|
+
}
|
|
3757
|
+
return;
|
|
3758
|
+
case "runtime_approval_policy":
|
|
3759
|
+
if (commandResult.value) {
|
|
3760
|
+
setApprovalPolicyWithNotice(commandResult.value as RuntimeApprovalPolicy);
|
|
3761
|
+
} else if (commandResult.message) {
|
|
3762
|
+
appendSystemEvent("Runtime policy", commandResult.message);
|
|
3763
|
+
}
|
|
3764
|
+
return;
|
|
3765
|
+
case "runtime_sandbox_mode":
|
|
3766
|
+
if (commandResult.value) {
|
|
3767
|
+
setSandboxModeWithNotice(commandResult.value as RuntimeSandboxMode);
|
|
3768
|
+
} else if (commandResult.message) {
|
|
3769
|
+
appendSystemEvent("Runtime policy", commandResult.message);
|
|
3770
|
+
}
|
|
3771
|
+
return;
|
|
3772
|
+
case "runtime_network_access":
|
|
3773
|
+
if (commandResult.value) {
|
|
3774
|
+
setNetworkAccessWithNotice(commandResult.value as RuntimeNetworkAccess);
|
|
3775
|
+
} else if (commandResult.message) {
|
|
3776
|
+
appendSystemEvent("Runtime policy", commandResult.message);
|
|
3777
|
+
}
|
|
3778
|
+
return;
|
|
3779
|
+
case "runtime_writable_roots_add":
|
|
3780
|
+
if (commandResult.value) {
|
|
3781
|
+
addWritableRootWithNotice(commandResult.value);
|
|
3782
|
+
}
|
|
3783
|
+
return;
|
|
3784
|
+
case "runtime_writable_roots_remove":
|
|
3785
|
+
if (commandResult.value) {
|
|
3786
|
+
removeWritableRootWithNotice(commandResult.value);
|
|
3787
|
+
}
|
|
3788
|
+
return;
|
|
3789
|
+
case "runtime_writable_roots_clear":
|
|
3790
|
+
clearWritableRootsWithNotice();
|
|
3791
|
+
return;
|
|
3792
|
+
case "runtime_service_tier":
|
|
3793
|
+
if (commandResult.value) {
|
|
3794
|
+
setServiceTierWithNotice(commandResult.value as RuntimeServiceTier);
|
|
3795
|
+
} else if (commandResult.message) {
|
|
3796
|
+
appendSystemEvent("Runtime policy", commandResult.message);
|
|
3797
|
+
}
|
|
3798
|
+
return;
|
|
3799
|
+
case "diagnose_github": {
|
|
3800
|
+
const remoteUrl = getLocalGitRemoteUrl();
|
|
3801
|
+
const repo = parseRepoIdentity(remoteUrl);
|
|
3802
|
+
const ghCli = checkGhCli();
|
|
3803
|
+
const localGit = checkLocalGitRemote();
|
|
3804
|
+
const localGitWrite = checkLocalGitWrite();
|
|
3805
|
+
|
|
3806
|
+
// MCP connector check: since TUI can't call MCP, we mark it as unknown
|
|
3807
|
+
// or rely on the agent to fill this in if it's the one running the command.
|
|
3808
|
+
const connector: DiagnosticResult = {
|
|
3809
|
+
path: "GitHub connector/MCP",
|
|
3810
|
+
status: "FAIL",
|
|
3811
|
+
evidence: "TUI cannot directly probe MCP",
|
|
3812
|
+
blocker: "Run /diagnose through the agent for a full probe",
|
|
3813
|
+
recommendedUse: false,
|
|
3814
|
+
};
|
|
3815
|
+
|
|
3816
|
+
const recommendedFlow = classifyDiagnostics(repo, ghCli, localGit, localGitWrite, connector);
|
|
3817
|
+
|
|
3818
|
+
// Instead of console.log, we'll format a message for appendSystemEvent
|
|
3819
|
+
const tableLines = [
|
|
3820
|
+
"Path | Status | Evidence | Blocker",
|
|
3821
|
+
"--------------------|---------|-------------------------------|---------------------------",
|
|
3822
|
+
`${ghCli.path.padEnd(20)}| ${ghCli.status.padEnd(8)}| ${(ghCli.evidence || "").substring(0, 30).padEnd(30)}| ${ghCli.blocker || ""}`,
|
|
3823
|
+
`${localGit.path.padEnd(20)}| ${localGit.status.padEnd(8)}| ${(localGit.evidence || "").substring(0, 30).padEnd(30)}| ${localGit.blocker || ""}`,
|
|
3824
|
+
`${localGitWrite.path.padEnd(20)}| ${localGitWrite.status.padEnd(8)}| ${(localGitWrite.evidence || "").substring(0, 30).padEnd(30)}| ${localGitWrite.blocker || ""}`,
|
|
3825
|
+
`${connector.path.padEnd(20)}| ${connector.status.padEnd(8)}| ${(connector.evidence || "").substring(0, 30).padEnd(30)}| ${connector.blocker || ""}`,
|
|
3826
|
+
];
|
|
3827
|
+
|
|
3828
|
+
const summary = [
|
|
3829
|
+
...tableLines,
|
|
3830
|
+
"",
|
|
3831
|
+
`Resolved repo: ${repo ? `${repo.owner}/${repo.repo}` : "Unknown"}`,
|
|
3832
|
+
`Recommended PR flow: ${recommendedFlow}`,
|
|
3833
|
+
].join("\n");
|
|
3834
|
+
|
|
3835
|
+
appendSystemEvent("GitHub Diagnostics", summary);
|
|
3836
|
+
return;
|
|
3837
|
+
}
|
|
3838
|
+
case "runtime_personality":
|
|
3839
|
+
if (commandResult.value) {
|
|
3840
|
+
setPersonalityWithNotice(commandResult.value as RuntimePersonality);
|
|
3841
|
+
} else if (commandResult.message) {
|
|
3842
|
+
appendSystemEvent("Runtime policy", commandResult.message);
|
|
3843
|
+
}
|
|
3844
|
+
return;
|
|
3845
|
+
case "auth":
|
|
3846
|
+
if (commandResult.value) {
|
|
3847
|
+
setAuthPreferenceWithNotice(commandResult.value as AuthPreference);
|
|
3848
|
+
}
|
|
3849
|
+
return;
|
|
3850
|
+
case "diagnose_providers": {
|
|
3851
|
+
const lines: string[] = ["Provider CLI diagnostics:"];
|
|
3852
|
+
const diags = providerDiagnosticsRef.current;
|
|
3853
|
+
const providerIds = ["openai", "anthropic", "google"] as const;
|
|
3854
|
+
const labels: Record<string, string> = { openai: "OpenAI/Codex", anthropic: "Anthropic/Claude", google: "Google/Gemini" };
|
|
3855
|
+
for (const id of providerIds) {
|
|
3856
|
+
const diag = diags[id];
|
|
3857
|
+
lines.push(`\n ${labels[id] ?? id}:`);
|
|
3858
|
+
if (!diag) {
|
|
3859
|
+
lines.push(" No diagnostic data (provider not yet validated).");
|
|
3860
|
+
continue;
|
|
3861
|
+
}
|
|
3862
|
+
const fields: Array<[string, string]> = [
|
|
3863
|
+
["resolvedCommand", "Resolved command"],
|
|
3864
|
+
["executablePath", "Executable path"],
|
|
3865
|
+
["loggedIn", "Logged in"],
|
|
3866
|
+
["authMethod", "Auth method"],
|
|
3867
|
+
["subscriptionType", "Subscription"],
|
|
3868
|
+
["apiProvider", "API provider"],
|
|
3869
|
+
["modelSource", "Model source"],
|
|
3870
|
+
];
|
|
3871
|
+
for (const [key, label] of fields) {
|
|
3872
|
+
if (diag[key] != null) lines.push(` ${label}: ${diag[key]}`);
|
|
3873
|
+
}
|
|
3874
|
+
}
|
|
3875
|
+
appendSystemEvent("Provider diagnostics", lines.join("\n"));
|
|
3876
|
+
return;
|
|
3877
|
+
}
|
|
3878
|
+
case "setting_status":
|
|
3879
|
+
if (commandResult.message) {
|
|
3880
|
+
appendSystemEvent("Settings", commandResult.message);
|
|
3881
|
+
}
|
|
3882
|
+
return;
|
|
3883
|
+
case "setting_workspace_display":
|
|
3884
|
+
if (commandResult.value) {
|
|
3885
|
+
setWorkspaceDisplayModeWithNotice(commandResult.value as WorkspaceDisplayMode);
|
|
3886
|
+
} else if (commandResult.message) {
|
|
3887
|
+
appendSystemEvent("Settings", commandResult.message);
|
|
3888
|
+
}
|
|
3889
|
+
return;
|
|
3890
|
+
case "setting_terminal_title":
|
|
3891
|
+
if (commandResult.value) {
|
|
3892
|
+
setTerminalTitleModeWithNotice(commandResult.value as TerminalTitleMode);
|
|
3893
|
+
} else if (commandResult.message) {
|
|
3894
|
+
appendSystemEvent("Settings", commandResult.message);
|
|
3895
|
+
}
|
|
3896
|
+
return;
|
|
3897
|
+
case "setting_busy_loader":
|
|
3898
|
+
if (commandResult.value) {
|
|
3899
|
+
const nextShowBusyLoader = commandResult.value === "true";
|
|
3900
|
+
setShowBusyLoader(nextShowBusyLoader);
|
|
3901
|
+
appendSystemEvent("Settings", `Busy loader ${nextShowBusyLoader ? "enabled" : "disabled"}.`);
|
|
3902
|
+
} else if (commandResult.message) {
|
|
3903
|
+
appendSystemEvent("Settings", commandResult.message);
|
|
3904
|
+
}
|
|
3905
|
+
return;
|
|
3906
|
+
case "theme":
|
|
3907
|
+
if (commandResult.value) {
|
|
3908
|
+
setThemeSelection((currentTheme) => commitThemeSelection(currentTheme, commandResult.value!));
|
|
3909
|
+
if (commandResult.message) {
|
|
3910
|
+
appendSystemEvent("Theme", commandResult.message);
|
|
3911
|
+
}
|
|
3912
|
+
}
|
|
3913
|
+
return;
|
|
3914
|
+
case "themes":
|
|
3915
|
+
if (commandResult.message) {
|
|
3916
|
+
appendSystemEvent("Themes", commandResult.message);
|
|
3917
|
+
}
|
|
3918
|
+
return;
|
|
3919
|
+
case "login":
|
|
3920
|
+
appendSystemEvent("Login guidance", getLoginGuidance());
|
|
3921
|
+
return;
|
|
3922
|
+
case "logout":
|
|
3923
|
+
appendSystemEvent("Logout guidance", getLogoutGuidance());
|
|
3924
|
+
return;
|
|
3925
|
+
case "auth_status":
|
|
3926
|
+
void refreshAuthStatus(true);
|
|
3927
|
+
return;
|
|
3928
|
+
case "open_backend_picker":
|
|
3929
|
+
openBackendPicker();
|
|
3930
|
+
return;
|
|
3931
|
+
case "open_provider_picker":
|
|
3932
|
+
openProviderPicker();
|
|
3933
|
+
return;
|
|
3934
|
+
case "open_model_picker":
|
|
3935
|
+
openModelPicker();
|
|
3936
|
+
return;
|
|
3937
|
+
case "open_mode_picker":
|
|
3938
|
+
openModePicker();
|
|
3939
|
+
return;
|
|
3940
|
+
case "open_reasoning_picker":
|
|
3941
|
+
openReasoningPicker();
|
|
3942
|
+
return;
|
|
3943
|
+
case "open_settings_panel":
|
|
3944
|
+
openSettingsPanel();
|
|
3945
|
+
return;
|
|
3946
|
+
case "open_theme_picker":
|
|
3947
|
+
openThemePicker();
|
|
3948
|
+
return;
|
|
3949
|
+
case "open_permissions_panel":
|
|
3950
|
+
openPermissionsPanel();
|
|
3951
|
+
return;
|
|
3952
|
+
case "open_auth_panel":
|
|
3953
|
+
openAuthPanel();
|
|
3954
|
+
return;
|
|
3955
|
+
case "mouse_toggle": {
|
|
3956
|
+
const nextMouse = !(mouseOverride ?? (terminalMouseMode === "wheel"));
|
|
3957
|
+
setMouseOverride(nextMouse);
|
|
3958
|
+
appendSystemEvent(
|
|
3959
|
+
"Mouse mode updated",
|
|
3960
|
+
nextMouse
|
|
3961
|
+
? "SGR mouse capture on — in-app wheel scroll active. Native drag-select requires Shift (Windows Terminal). Run /mouse to disable."
|
|
3962
|
+
: "SGR mouse capture off — native drag-select and native wheel scroll restored. Run /mouse to re-enable.",
|
|
3963
|
+
);
|
|
3964
|
+
return;
|
|
3965
|
+
}
|
|
3966
|
+
case "verbose_toggle": {
|
|
3967
|
+
if (commandResult.message) {
|
|
3968
|
+
appendSystemEvent("Debug", commandResult.message);
|
|
3969
|
+
return;
|
|
3970
|
+
}
|
|
3971
|
+
setVerboseMode((current) => !current);
|
|
3972
|
+
appendSystemEvent(
|
|
3973
|
+
"Verbose mode",
|
|
3974
|
+
verboseMode
|
|
3975
|
+
? "Verbose mode disabled — showing concise output."
|
|
3976
|
+
: "Verbose mode enabled — showing detailed processing info.",
|
|
3977
|
+
);
|
|
3978
|
+
return;
|
|
3979
|
+
}
|
|
3980
|
+
case "copy":
|
|
3981
|
+
void handleCopy();
|
|
3982
|
+
return;
|
|
3983
|
+
case "workspace_relaunch":
|
|
3984
|
+
if (commandResult.value) {
|
|
3985
|
+
handleWorkspaceRelaunch(commandResult.value);
|
|
3986
|
+
}
|
|
3987
|
+
return;
|
|
3988
|
+
case "workspace":
|
|
3989
|
+
case "backends":
|
|
3990
|
+
if (commandResult.message) {
|
|
3991
|
+
appendSystemEvent("Command", commandResult.message);
|
|
3992
|
+
}
|
|
3993
|
+
return;
|
|
3994
|
+
case "models":
|
|
3995
|
+
if (!modelCapabilities) {
|
|
3996
|
+
void refreshModelCapabilities(false, true);
|
|
3997
|
+
}
|
|
3998
|
+
if (commandResult.message) {
|
|
3999
|
+
appendSystemEvent("Command", commandResult.message);
|
|
4000
|
+
}
|
|
4001
|
+
return;
|
|
4002
|
+
case "help":
|
|
4003
|
+
case "unknown":
|
|
4004
|
+
if (commandResult.message) {
|
|
4005
|
+
appendSystemEvent("Command", commandResult.message);
|
|
4006
|
+
}
|
|
4007
|
+
return;
|
|
4008
|
+
default:
|
|
4009
|
+
if (commandResult.message) {
|
|
4010
|
+
appendSystemEvent("Command", commandResult.message);
|
|
4011
|
+
}
|
|
4012
|
+
return;
|
|
4013
|
+
}
|
|
4014
|
+
}
|
|
4015
|
+
|
|
4016
|
+
// ========== NORMAL PROMPT SUBMISSION (after command routing) ==========
|
|
4017
|
+
// Check for follow-up answer submission
|
|
4018
|
+
if (uiState.kind === "AWAITING_USER_ACTION") {
|
|
4019
|
+
const originalUserEvent = findUserPromptForTurn(uiState.turnId);
|
|
4020
|
+
if (!originalUserEvent) {
|
|
4021
|
+
appendErrorEvent("Follow-up unavailable", "The original turn could not be found, so the answer could not be resumed.");
|
|
4022
|
+
dispatchSession({ type: "UI_ACTION", action: { type: "DISMISS_TRANSIENT" } });
|
|
4023
|
+
return;
|
|
4024
|
+
}
|
|
4025
|
+
|
|
4026
|
+
if (busy) return;
|
|
4027
|
+
startPromptRun(value, buildFollowUpPrompt({
|
|
4028
|
+
originalPrompt: originalUserEvent.prompt,
|
|
4029
|
+
assistantQuestion: uiState.question,
|
|
4030
|
+
userAnswer: value,
|
|
4031
|
+
}), { submitTiming, commitPrompt: true });
|
|
4032
|
+
return;
|
|
4033
|
+
}
|
|
4034
|
+
|
|
4035
|
+
// Check if app is busy for normal prompts
|
|
4036
|
+
if (!isCommand && busy) {
|
|
4037
|
+
return;
|
|
4038
|
+
}
|
|
4039
|
+
|
|
4040
|
+
// Validate workspace access for normal prompts
|
|
4041
|
+
const outsideViolations = findOutsideWorkspacePaths(value, workspaceRoot, allowedWritableRoots);
|
|
4042
|
+
if (outsideViolations.length > 0) {
|
|
4043
|
+
if (runtimeConfig.policy.allowExternalFileImport) {
|
|
4044
|
+
const attachmentsDir = path.isAbsolute(runtimeConfig.policy.attachmentDir)
|
|
4045
|
+
? runtimeConfig.policy.attachmentDir
|
|
4046
|
+
: path.join(workspaceRoot, runtimeConfig.policy.attachmentDir);
|
|
4047
|
+
const importFiles: PendingImportFile[] = outsideViolations.map((v) => ({
|
|
4048
|
+
srcPath: v.normalizedPath,
|
|
4049
|
+
rawPath: v.rawPath,
|
|
4050
|
+
destFilename: path.basename(v.normalizedPath),
|
|
4051
|
+
isImage: isImageFile(v.normalizedPath),
|
|
4052
|
+
}));
|
|
4053
|
+
setPendingImport({ prompt: value, files: importFiles, attachmentsDir });
|
|
4054
|
+
setScreen("import-confirmation");
|
|
4055
|
+
return;
|
|
4056
|
+
}
|
|
4057
|
+
const workspaceGuardMessage = getPromptWorkspaceGuardMessage(value, workspaceRoot, allowedWritableRoots);
|
|
4058
|
+
if (workspaceGuardMessage) {
|
|
4059
|
+
appendErrorEvent("Workspace boundary", workspaceGuardMessage);
|
|
4060
|
+
return;
|
|
4061
|
+
}
|
|
4062
|
+
}
|
|
4063
|
+
|
|
4064
|
+
// Submit to provider or plan mode
|
|
4065
|
+
if (planMode) {
|
|
4066
|
+
const nextPlanState = startPlanGeneration(value, mode);
|
|
4067
|
+
setPlanFlow(nextPlanState);
|
|
4068
|
+
runPlanGeneration(nextPlanState, value, submitTiming, true);
|
|
4069
|
+
return;
|
|
4070
|
+
}
|
|
4071
|
+
startPromptRun(value, value, { submitTiming, commitPrompt: true });
|
|
4072
|
+
}, [
|
|
4073
|
+
allowedWritableRoots,
|
|
4074
|
+
appendErrorEvent,
|
|
4075
|
+
appendSystemEvent,
|
|
4076
|
+
busy,
|
|
4077
|
+
buildFollowUpPrompt,
|
|
4078
|
+
conversationChars,
|
|
4079
|
+
dispatchSession,
|
|
4080
|
+
findUserPromptForTurn,
|
|
4081
|
+
focusManager,
|
|
4082
|
+
handleCopy,
|
|
4083
|
+
handleClear,
|
|
4084
|
+
handleQuit,
|
|
4085
|
+
handleShellExecute,
|
|
4086
|
+
handlePlanFeedbackSubmit,
|
|
4087
|
+
handleWorkspaceRelaunch,
|
|
4088
|
+
inputValue,
|
|
4089
|
+
layeredRuntimeConfig,
|
|
4090
|
+
modelCapabilities,
|
|
4091
|
+
mode,
|
|
4092
|
+
openAuthPanel,
|
|
4093
|
+
openBackendPicker,
|
|
4094
|
+
openProviderPicker,
|
|
4095
|
+
openModePicker,
|
|
4096
|
+
openModelPicker,
|
|
4097
|
+
openPermissionsPanel,
|
|
4098
|
+
openReasoningPicker,
|
|
4099
|
+
openSettingsPanel,
|
|
4100
|
+
planMode,
|
|
4101
|
+
refreshAuthStatus,
|
|
4102
|
+
resetComposer,
|
|
4103
|
+
resolvedRuntimeConfig,
|
|
4104
|
+
runPlanGeneration,
|
|
4105
|
+
runtimeConfig,
|
|
4106
|
+
addWritableRootWithNotice,
|
|
4107
|
+
clearWritableRootsWithNotice,
|
|
4108
|
+
removeWritableRootWithNotice,
|
|
4109
|
+
setApprovalPolicyWithNotice,
|
|
4110
|
+
setAuthPreferenceWithNotice,
|
|
4111
|
+
setBackendWithNotice,
|
|
4112
|
+
setNetworkAccessWithNotice,
|
|
4113
|
+
setModeWithNotice,
|
|
4114
|
+
setModelWithNotice,
|
|
4115
|
+
setPlanModeWithNotice,
|
|
4116
|
+
togglePlanModeWithNotice,
|
|
4117
|
+
setPersonalityWithNotice,
|
|
4118
|
+
setProjectTrustWithNotice,
|
|
4119
|
+
setReasoningWithNotice,
|
|
4120
|
+
setSandboxModeWithNotice,
|
|
4121
|
+
setServiceTierWithNotice,
|
|
4122
|
+
showBusyLoader,
|
|
4123
|
+
startPromptRun,
|
|
4124
|
+
themeSelection.committedTheme,
|
|
4125
|
+
uiState,
|
|
4126
|
+
workspaceCommandContext,
|
|
4127
|
+
workspaceDisplayMode,
|
|
4128
|
+
workspaceRoot,
|
|
4129
|
+
]);
|
|
4130
|
+
|
|
4131
|
+
const modelDisplayName = useMemo(() => {
|
|
4132
|
+
if (activeProviderRoute.providerId === "anthropic") {
|
|
4133
|
+
const modelLabel = currentModelCapability?.label ?? activeProviderRoute.modelId;
|
|
4134
|
+
return `Claude Code CLI / ${modelLabel} / reasoning: ${formatReasoningLabel(activeProviderRoute.reasoning ?? reasoningLevel)}`;
|
|
4135
|
+
}
|
|
4136
|
+
if (activeProviderRoute.providerId === "google" && activeProviderRoute.modelSelection) {
|
|
4137
|
+
if (activeProviderRoute.modelSelection.kind === "auto") {
|
|
4138
|
+
return `auto ${activeProviderRoute.modelSelection.family === "gemini-3" ? "Gemini 3" : "Gemini 2.5"}`;
|
|
4139
|
+
}
|
|
4140
|
+
}
|
|
4141
|
+
if (activeProviderRoute.providerId === "local") {
|
|
4142
|
+
return activeProviderRoute.modelId;
|
|
4143
|
+
}
|
|
4144
|
+
return model;
|
|
4145
|
+
}, [
|
|
4146
|
+
activeProviderRoute.modelId,
|
|
4147
|
+
activeProviderRoute.modelSelection,
|
|
4148
|
+
activeProviderRoute.providerId,
|
|
4149
|
+
activeProviderRoute.reasoning,
|
|
4150
|
+
currentModelCapability?.label,
|
|
4151
|
+
model,
|
|
4152
|
+
reasoningLevel,
|
|
4153
|
+
]);
|
|
4154
|
+
const composerReasoningLevel = activeProviderRoute.providerId === "anthropic"
|
|
4155
|
+
? ""
|
|
4156
|
+
: activeProviderRoute.reasoning ?? reasoningLevel;
|
|
4157
|
+
|
|
4158
|
+
// Memoize the composer element so AppShell's memo check (prev.composer ===
|
|
4159
|
+
// next.composer) passes during streaming. Without this, a new JSX element is
|
|
4160
|
+
// created on every App render, forcing the entire AppShell tree (header +
|
|
4161
|
+
// timeline + footer) to re-render on every 25ms streaming flush.
|
|
4162
|
+
const composerElement = useMemo(() => {
|
|
4163
|
+
if (planFlow.kind === "awaiting_action") {
|
|
4164
|
+
if (!hasVisibleTranscriptPlan) {
|
|
4165
|
+
return (
|
|
4166
|
+
<Text color={activeTheme.MUTED}>
|
|
4167
|
+
Plan could not be displayed. Please ask Codexa to regenerate the plan.
|
|
4168
|
+
</Text>
|
|
4169
|
+
);
|
|
4170
|
+
}
|
|
4171
|
+
return (
|
|
4172
|
+
<PlanActionPicker
|
|
4173
|
+
cols={terminalLayout.cols}
|
|
4174
|
+
onSelect={handlePlanAction}
|
|
4175
|
+
onCancel={handleCancel}
|
|
4176
|
+
/>
|
|
4177
|
+
);
|
|
4178
|
+
}
|
|
4179
|
+
if (planFlow.kind === "collecting_feedback") {
|
|
4180
|
+
return (
|
|
4181
|
+
<TextEntryPanel
|
|
4182
|
+
focusId={FOCUS_IDS.composer}
|
|
4183
|
+
title="Update plan"
|
|
4184
|
+
subtitle="Describe what should change. Enter regenerates the plan."
|
|
4185
|
+
inputLabel="Update"
|
|
4186
|
+
placeholder={planFlow.mode === "revise"
|
|
4187
|
+
? "e.g. keep it to one file and add tests"
|
|
4188
|
+
: "e.g. keep it minimal and avoid touching other files"}
|
|
4189
|
+
footerHint="Enter regenerate Esc back Backspace delete"
|
|
4190
|
+
initialValue={initialRevisionText}
|
|
4191
|
+
onSubmit={(value) => {
|
|
4192
|
+
setInitialRevisionText("");
|
|
4193
|
+
handlePlanFeedbackSubmit(value);
|
|
4194
|
+
}}
|
|
4195
|
+
onCancel={() => setPlanFlow((current) => cancelPlanFeedback(current))}
|
|
4196
|
+
/>
|
|
4197
|
+
);
|
|
4198
|
+
}
|
|
4199
|
+
return (
|
|
4200
|
+
<MemoizedBottomComposer
|
|
4201
|
+
key={composerInstanceKey}
|
|
4202
|
+
layout={terminalLayout}
|
|
4203
|
+
uiState={uiState}
|
|
4204
|
+
mode={mode}
|
|
4205
|
+
model={modelDisplayName}
|
|
4206
|
+
themeName={activeThemeName}
|
|
4207
|
+
reasoningLevel={composerReasoningLevel}
|
|
4208
|
+
planMode={planMode}
|
|
4209
|
+
showBusyLoader={showBusyLoader}
|
|
4210
|
+
tokensUsed={estimateTokens(conversationChars)}
|
|
4211
|
+
modelSpec={currentModelSpec}
|
|
4212
|
+
value={inputValue}
|
|
4213
|
+
cursor={cursor}
|
|
4214
|
+
onChangeInput={handleChangeInput}
|
|
4215
|
+
onSubmit={handleSubmit}
|
|
4216
|
+
onCancel={handleCancel}
|
|
4217
|
+
onChangeValue={handleChangeValue}
|
|
4218
|
+
onChangeCursor={handleChangeCursor}
|
|
4219
|
+
onHistoryUp={handleHistoryUp}
|
|
4220
|
+
onHistoryDown={handleHistoryDown}
|
|
4221
|
+
onOpenBackendPicker={openBackendPicker}
|
|
4222
|
+
onOpenProviderPicker={openProviderPicker}
|
|
4223
|
+
onOpenModelPicker={openModelPicker}
|
|
4224
|
+
onOpenModePicker={openModePicker}
|
|
4225
|
+
onOpenThemePicker={openThemePicker}
|
|
4226
|
+
onOpenAuthPanel={openAuthPanel}
|
|
4227
|
+
onTogglePlanMode={togglePlanModeWithNotice}
|
|
4228
|
+
onClear={handleClear}
|
|
4229
|
+
onCycleMode={cycleModeWithNotice}
|
|
4230
|
+
onQuit={handleQuit}
|
|
4231
|
+
activeProviderId={activeProviderRoute.providerId}
|
|
4232
|
+
externalCliStatus={sessionState.externalCliStatus}
|
|
4233
|
+
/>
|
|
4234
|
+
);
|
|
4235
|
+
}, [
|
|
4236
|
+
planFlow,
|
|
4237
|
+
initialRevisionText,
|
|
4238
|
+
handlePlanAction,
|
|
4239
|
+
handleCancel,
|
|
4240
|
+
handlePlanFeedbackSubmit,
|
|
4241
|
+
hasVisibleTranscriptPlan,
|
|
4242
|
+
activeTheme.MUTED,
|
|
4243
|
+
composerInstanceKey,
|
|
4244
|
+
terminalLayout,
|
|
4245
|
+
uiState,
|
|
4246
|
+
mode,
|
|
4247
|
+
modelDisplayName,
|
|
4248
|
+
activeThemeName,
|
|
4249
|
+
composerReasoningLevel,
|
|
4250
|
+
planMode,
|
|
4251
|
+
showBusyLoader,
|
|
4252
|
+
conversationChars,
|
|
4253
|
+
currentModelSpec,
|
|
4254
|
+
inputValue,
|
|
4255
|
+
cursor,
|
|
4256
|
+
handleChangeInput,
|
|
4257
|
+
handleSubmit,
|
|
4258
|
+
handleChangeValue,
|
|
4259
|
+
handleChangeCursor,
|
|
4260
|
+
handleHistoryUp,
|
|
4261
|
+
handleHistoryDown,
|
|
4262
|
+
openBackendPicker,
|
|
4263
|
+
openProviderPicker,
|
|
4264
|
+
openModelPicker,
|
|
4265
|
+
openModePicker,
|
|
4266
|
+
openThemePicker,
|
|
4267
|
+
openAuthPanel,
|
|
4268
|
+
togglePlanModeWithNotice,
|
|
4269
|
+
handleClear,
|
|
4270
|
+
cycleModeWithNotice,
|
|
4271
|
+
handleQuit,
|
|
4272
|
+
activeProviderRoute.providerId,
|
|
4273
|
+
]);
|
|
4274
|
+
|
|
4275
|
+
// ─── Render ──────────────────────────────────────────────────────────────────
|
|
4276
|
+
|
|
4277
|
+
// Plan review is shown inline in the Timeline, not as a separate overlay.
|
|
4278
|
+
const mainPanelElement = null;
|
|
4279
|
+
|
|
4280
|
+
return (
|
|
4281
|
+
<ThemeProvider theme={activeThemeName} customTheme={customTheme}>
|
|
4282
|
+
<AppShell
|
|
4283
|
+
layout={terminalLayout}
|
|
4284
|
+
screen={screen}
|
|
4285
|
+
authState={authStatus.state}
|
|
4286
|
+
workspaceLabel={workspaceLabel}
|
|
4287
|
+
workspaceRoot={workspaceRoot}
|
|
4288
|
+
runtimeSummary={runtimeSummary}
|
|
4289
|
+
staticEvents={staticEvents}
|
|
4290
|
+
activeEvents={activeEvents}
|
|
4291
|
+
uiState={uiState}
|
|
4292
|
+
verboseMode={verboseMode}
|
|
4293
|
+
mouseCapture={mouseCapture}
|
|
4294
|
+
onMouseActivity={resetMouseIdle}
|
|
4295
|
+
selectionProfile={selectionProfile}
|
|
4296
|
+
clearCount={sessionState.clearCount}
|
|
4297
|
+
headerConfig={headerConfig}
|
|
4298
|
+
panel={
|
|
4299
|
+
<>
|
|
4300
|
+
{screen === "backend-picker" && (
|
|
4301
|
+
<BackendPicker
|
|
4302
|
+
currentBackend={backend}
|
|
4303
|
+
onSelect={(value) => setBackendWithNotice(value as AvailableBackend)}
|
|
4304
|
+
onCancel={() => setScreen("main")}
|
|
4305
|
+
/>
|
|
4306
|
+
)}
|
|
4307
|
+
|
|
4308
|
+
{screen === "provider-picker" && (
|
|
4309
|
+
<ProviderPicker
|
|
4310
|
+
layout={terminalLayout}
|
|
4311
|
+
providers={providerRegistry}
|
|
4312
|
+
onAction={handleProviderAction}
|
|
4313
|
+
onCancel={() => {
|
|
4314
|
+
setPendingRouteProviderId(null);
|
|
4315
|
+
setScreen("main");
|
|
4316
|
+
}}
|
|
4317
|
+
initialProviderId={pendingRouteProviderId ?? undefined}
|
|
4318
|
+
/>
|
|
4319
|
+
)}
|
|
4320
|
+
|
|
4321
|
+
{screen === "model-picker" && (
|
|
4322
|
+
<ModelPickerScreen
|
|
4323
|
+
layout={terminalLayout}
|
|
4324
|
+
models={modelPickerModels}
|
|
4325
|
+
currentModel={modelPickerCurrentModel}
|
|
4326
|
+
currentReasoning={modelPickerCurrentReasoning}
|
|
4327
|
+
activeProviderLabel={modelPickerProviderLabel}
|
|
4328
|
+
isLoading={modelPickerProviderId === "openai" && modelCapabilitiesBusy && modelPickerModels.length === 0}
|
|
4329
|
+
emptyMessage={modelPickerEmptyMessage}
|
|
4330
|
+
onSelect={(m, r, geminiSelection) => {
|
|
4331
|
+
if (pendingRouteProviderId && pendingRouteProviderId !== activeProviderRoute.providerId) {
|
|
4332
|
+
// Non-active provider: save as provider default without switching the active route.
|
|
4333
|
+
// User must click "Use in Codexa" to validate and activate.
|
|
4334
|
+
persistProviderDefaultModelAndReasoning(pendingRouteProviderId, m, r);
|
|
4335
|
+
appendSystemEvent(
|
|
4336
|
+
"Provider model saved",
|
|
4337
|
+
`${modelPickerProviderLabel} default model set to ${m} with reasoning ${formatReasoningLabel(r)}. Choose "Use in Codexa" to activate this provider.`,
|
|
4338
|
+
);
|
|
4339
|
+
setScreen("provider-picker");
|
|
4340
|
+
} else {
|
|
4341
|
+
void setModelAndReasoningWithNotice(m as AvailableModel, r as ReasoningLevel, modelPickerProviderId, geminiSelection);
|
|
4342
|
+
}
|
|
4343
|
+
}}
|
|
4344
|
+
onCancel={() => {
|
|
4345
|
+
setPendingRouteProviderId(null);
|
|
4346
|
+
returnToChatMode();
|
|
4347
|
+
}}
|
|
4348
|
+
/>
|
|
4349
|
+
)}
|
|
4350
|
+
|
|
4351
|
+
{screen === "mode-picker" && (
|
|
4352
|
+
<ModePicker
|
|
4353
|
+
currentMode={mode}
|
|
4354
|
+
onSelect={(value) => setModeWithNotice(value as AvailableMode)}
|
|
4355
|
+
onCancel={() => setScreen("main")}
|
|
4356
|
+
/>
|
|
4357
|
+
)}
|
|
4358
|
+
|
|
4359
|
+
{screen === "reasoning-picker" && (
|
|
4360
|
+
<ReasoningPicker
|
|
4361
|
+
currentModel={activeProviderRoute.modelId}
|
|
4362
|
+
currentReasoning={activeProviderRoute.reasoning ?? reasoningLevel}
|
|
4363
|
+
reasoningLevels={currentReasoningCapabilities}
|
|
4364
|
+
defaultReasoning={currentModelCapability?.defaultReasoningLevel ?? null}
|
|
4365
|
+
sourceLabel={currentReasoningSourceLabel}
|
|
4366
|
+
onSelect={(value) => setReasoningWithNotice(value as ReasoningLevel)}
|
|
4367
|
+
onCancel={() => setScreen("main")}
|
|
4368
|
+
/>
|
|
4369
|
+
)}
|
|
4370
|
+
|
|
4371
|
+
{screen === "auth-panel" && (
|
|
4372
|
+
<AuthPanel
|
|
4373
|
+
focusId={FOCUS_IDS.authPanel}
|
|
4374
|
+
provider={provider}
|
|
4375
|
+
authPreference={authPreference}
|
|
4376
|
+
authStatus={authStatus}
|
|
4377
|
+
authStatusBusy={authStatusBusy}
|
|
4378
|
+
onSetPreference={(value) => setAuthPreferenceWithNotice(value as AuthPreference)}
|
|
4379
|
+
onRefreshAuthStatus={() => {
|
|
4380
|
+
void refreshAuthStatus(false);
|
|
4381
|
+
}}
|
|
4382
|
+
onClose={() => setScreen("main")}
|
|
4383
|
+
/>
|
|
4384
|
+
)}
|
|
4385
|
+
|
|
4386
|
+
{screen === "permissions-panel" && (
|
|
4387
|
+
<PermissionsPanel
|
|
4388
|
+
runtime={runtimeConfig}
|
|
4389
|
+
resolvedRuntime={resolvedRuntimeConfig}
|
|
4390
|
+
onSelect={handlePermissionsPanelAction}
|
|
4391
|
+
onCancel={() => setScreen("main")}
|
|
4392
|
+
/>
|
|
4393
|
+
)}
|
|
4394
|
+
|
|
4395
|
+
{screen === "permissions-approval-picker" && (
|
|
4396
|
+
<SelectionPanel
|
|
4397
|
+
focusId={FOCUS_IDS.permissionsApprovalPicker}
|
|
4398
|
+
title="Approval Policy"
|
|
4399
|
+
subtitle="Choose how Codexa should handle approval prompts."
|
|
4400
|
+
items={AVAILABLE_APPROVAL_POLICIES.map((item) => ({
|
|
4401
|
+
label: item.id === runtimeConfig.policy.approvalPolicy
|
|
4402
|
+
? `${item.label} ✓`
|
|
4403
|
+
: item.label,
|
|
4404
|
+
value: item.id,
|
|
4405
|
+
}))}
|
|
4406
|
+
onSelect={(value) => {
|
|
4407
|
+
setApprovalPolicyWithNotice(value as RuntimeApprovalPolicy);
|
|
4408
|
+
setScreen("permissions-panel");
|
|
4409
|
+
}}
|
|
4410
|
+
onCancel={() => setScreen("permissions-panel")}
|
|
4411
|
+
/>
|
|
4412
|
+
)}
|
|
4413
|
+
|
|
4414
|
+
{screen === "permissions-sandbox-picker" && (
|
|
4415
|
+
<SelectionPanel
|
|
4416
|
+
focusId={FOCUS_IDS.permissionsSandboxPicker}
|
|
4417
|
+
title="Sandbox Mode"
|
|
4418
|
+
subtitle="Choose the effective filesystem sandbox for future runs."
|
|
4419
|
+
items={AVAILABLE_SANDBOX_MODES.map((item) => ({
|
|
4420
|
+
label: item.id === runtimeConfig.policy.sandboxMode
|
|
4421
|
+
? `${item.label} ✓`
|
|
4422
|
+
: item.label,
|
|
4423
|
+
value: item.id,
|
|
4424
|
+
}))}
|
|
4425
|
+
onSelect={(value) => {
|
|
4426
|
+
setSandboxModeWithNotice(value as RuntimeSandboxMode);
|
|
4427
|
+
setScreen("permissions-panel");
|
|
4428
|
+
}}
|
|
4429
|
+
onCancel={() => setScreen("permissions-panel")}
|
|
4430
|
+
/>
|
|
4431
|
+
)}
|
|
4432
|
+
|
|
4433
|
+
{screen === "permissions-network-picker" && (
|
|
4434
|
+
<SelectionPanel
|
|
4435
|
+
focusId={FOCUS_IDS.permissionsNetworkPicker}
|
|
4436
|
+
title="Network Access"
|
|
4437
|
+
subtitle="Choose whether network access is enabled for future runs."
|
|
4438
|
+
items={AVAILABLE_NETWORK_ACCESS_VALUES.map((item) => ({
|
|
4439
|
+
label: item.id === runtimeConfig.policy.networkAccess
|
|
4440
|
+
? `${item.label} ✓`
|
|
4441
|
+
: item.label,
|
|
4442
|
+
value: item.id,
|
|
4443
|
+
}))}
|
|
4444
|
+
onSelect={(value) => {
|
|
4445
|
+
setNetworkAccessWithNotice(value as RuntimeNetworkAccess);
|
|
4446
|
+
setScreen("permissions-panel");
|
|
4447
|
+
}}
|
|
4448
|
+
onCancel={() => setScreen("permissions-panel")}
|
|
4449
|
+
/>
|
|
4450
|
+
)}
|
|
4451
|
+
|
|
4452
|
+
{screen === "permissions-add-writable-root" && (
|
|
4453
|
+
<TextEntryPanel
|
|
4454
|
+
focusId={FOCUS_IDS.permissionsAddWritableRoot}
|
|
4455
|
+
title="Add Writable Root"
|
|
4456
|
+
subtitle="Enter an absolute path or a path relative to the locked workspace."
|
|
4457
|
+
placeholder="relative\\or\\absolute\\path"
|
|
4458
|
+
inputLabel="Path"
|
|
4459
|
+
footerHint="Enter save Esc cancel Backspace delete"
|
|
4460
|
+
onSubmit={(value) => {
|
|
4461
|
+
if (!value.trim()) {
|
|
4462
|
+
appendSystemEvent("Runtime policy", "Writable root path cannot be empty.");
|
|
4463
|
+
return;
|
|
4464
|
+
}
|
|
4465
|
+
addWritableRootWithNotice(value);
|
|
4466
|
+
setScreen("permissions-panel");
|
|
4467
|
+
}}
|
|
4468
|
+
onCancel={() => setScreen("permissions-panel")}
|
|
4469
|
+
/>
|
|
4470
|
+
)}
|
|
4471
|
+
|
|
4472
|
+
{screen === "permissions-remove-writable-root" && (
|
|
4473
|
+
<SelectionPanel
|
|
4474
|
+
focusId={FOCUS_IDS.permissionsRemoveWritableRoot}
|
|
4475
|
+
title="Remove Writable Root"
|
|
4476
|
+
subtitle="Select a configured writable root to remove."
|
|
4477
|
+
items={runtimeConfig.policy.writableRoots.map((root) => ({
|
|
4478
|
+
label: root,
|
|
4479
|
+
value: root,
|
|
4480
|
+
}))}
|
|
4481
|
+
onSelect={(value) => {
|
|
4482
|
+
removeWritableRootWithNotice(value);
|
|
4483
|
+
setScreen("permissions-panel");
|
|
4484
|
+
}}
|
|
4485
|
+
onCancel={() => setScreen("permissions-panel")}
|
|
4486
|
+
/>
|
|
4487
|
+
)}
|
|
4488
|
+
|
|
4489
|
+
{screen === "theme-picker" && (
|
|
4490
|
+
<ThemePicker
|
|
4491
|
+
currentTheme={themeSelection.committedTheme}
|
|
4492
|
+
onSelect={(value) => {
|
|
4493
|
+
if (themePreviewTimerRef.current) {
|
|
4494
|
+
clearTimeout(themePreviewTimerRef.current);
|
|
4495
|
+
themePreviewTimerRef.current = null;
|
|
4496
|
+
}
|
|
4497
|
+
setThemeSelection((currentTheme) => commitThemeSelection(currentTheme, value));
|
|
4498
|
+
setScreen("main");
|
|
4499
|
+
appendSystemEvent("Theme updated", `Visual theme switched to ${formatThemeLabel(value)}.`);
|
|
4500
|
+
if (value === "custom") {
|
|
4501
|
+
if (!customTheme) {
|
|
4502
|
+
setCustomTheme({ ...THEMES.purple });
|
|
4503
|
+
}
|
|
4504
|
+
appendSystemEvent(
|
|
4505
|
+
"Custom Theme",
|
|
4506
|
+
"Add a \"custom_theme\" object to ~/.codexa-settings.json with any of these keys: BG, PANEL, PANEL_ALT, PANEL_SOFT, BORDER, BORDER_ACTIVE, BORDER_SUBTLE, TEXT, MUTED, DIM, ACCENT, PROMPT, SUCCESS, WARNING, ERROR, INFO, STAR. Unset keys fall back to Midnight Purple defaults.",
|
|
4507
|
+
);
|
|
4508
|
+
}
|
|
4509
|
+
}}
|
|
4510
|
+
onHighlight={(value) => {
|
|
4511
|
+
if (themePreviewTimerRef.current) clearTimeout(themePreviewTimerRef.current);
|
|
4512
|
+
themePreviewTimerRef.current = setTimeout(() => {
|
|
4513
|
+
setThemeSelection((currentTheme) => previewThemeSelection(currentTheme, value));
|
|
4514
|
+
}, 120);
|
|
4515
|
+
}}
|
|
4516
|
+
onCancel={() => {
|
|
4517
|
+
if (themePreviewTimerRef.current) clearTimeout(themePreviewTimerRef.current);
|
|
4518
|
+
themePreviewTimerRef.current = null;
|
|
4519
|
+
setThemeSelection((currentTheme) => cancelThemeSelection(currentTheme));
|
|
4520
|
+
setScreen("main");
|
|
4521
|
+
}}
|
|
4522
|
+
/>
|
|
4523
|
+
)}
|
|
4524
|
+
|
|
4525
|
+
{screen === "settings-panel" && (
|
|
4526
|
+
<SettingsPanel
|
|
4527
|
+
focusId={FOCUS_IDS.settingsPanel}
|
|
4528
|
+
settings={USER_SETTING_DEFINITIONS}
|
|
4529
|
+
values={currentUserSettings}
|
|
4530
|
+
onSave={(values) => saveSettingsFromPanel(values as UserSettingValues)}
|
|
4531
|
+
onCancel={() => setScreen("main")}
|
|
4532
|
+
/>
|
|
4533
|
+
)}
|
|
4534
|
+
|
|
4535
|
+
{screen === "import-confirmation" && pendingImport && (
|
|
4536
|
+
<AttachmentImportPanel
|
|
4537
|
+
focusId={FOCUS_IDS.importConfirmationPanel}
|
|
4538
|
+
files={pendingImport.files}
|
|
4539
|
+
attachmentsDir={pendingImport.attachmentsDir}
|
|
4540
|
+
workspaceRoot={workspaceRoot}
|
|
4541
|
+
modelSupportsVision={activeRouteProvider?.capabilityProfile?.supportsVision ?? null}
|
|
4542
|
+
onConfirm={() => { void handleImportConfirm(); }}
|
|
4543
|
+
onCancel={handleImportCancel}
|
|
4544
|
+
/>
|
|
4545
|
+
)}
|
|
4546
|
+
</>
|
|
4547
|
+
}
|
|
4548
|
+
mainPanel={null}
|
|
4549
|
+
mainPanelMode="viewport"
|
|
4550
|
+
composer={composerElement}
|
|
4551
|
+
composerRows={composerRows}
|
|
4552
|
+
panelHint={screen !== "main" && screen !== "model-picker" ? (
|
|
4553
|
+
<Box marginTop={1} paddingX={1}>
|
|
4554
|
+
<Text color={activeTheme.DIM}>Close the active panel with Esc to return to the composer.</Text>
|
|
4555
|
+
</Box>
|
|
4556
|
+
) : null}
|
|
4557
|
+
/>
|
|
4558
|
+
</ThemeProvider>
|
|
4559
|
+
);
|
|
4560
|
+
}
|
|
4561
|
+
|