@golba98/codexa 1.0.2 → 1.0.4
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 +396 -100
- package/bin/codexa.js +62 -144
- package/package.json +14 -8
- package/src/app.tsx +596 -306
- package/src/commands/handler.ts +6 -6
- package/src/config/buildInfo.ts +2 -2
- package/src/config/layeredConfig.ts +1 -1
- package/src/config/persistence.ts +10 -0
- package/src/config/runtimeConfig.ts +1 -1
- package/src/config/settings.ts +8 -16
- package/src/config/trustStore.ts +1 -1
- package/src/config/updateCheckCache.ts +19 -1
- package/src/core/README.md +52 -0
- package/src/core/agent/loop.ts +282 -0
- package/src/core/agent/protocol.ts +211 -0
- package/src/core/agent/tools.ts +414 -0
- package/src/core/{codexExecArgs.ts → codex/codexExecArgs.ts} +2 -2
- package/src/core/{codexLaunch.ts → codex/codexLaunch.ts} +3 -3
- package/src/core/{codexPrompt.ts → codex/codexPrompt.ts} +3 -3
- package/src/core/debug/modelStateDebug.ts +34 -0
- package/src/core/executables/antigravityExecutable.ts +48 -0
- package/src/core/executables/codexExecutable.ts +1 -0
- package/src/core/executables/executableResolver.ts +65 -43
- package/src/core/perf/renderDebug.ts +10 -6
- package/src/core/process/processValidation.ts +9 -5
- package/src/core/providerLauncher/launcher.ts +59 -42
- package/src/core/providerLauncher/registry.ts +30 -14
- package/src/core/providerLauncher/types.ts +11 -9
- package/src/core/providerLauncher/workspaceConfig.ts +41 -26
- package/src/core/providerRuntime/anthropic.ts +7 -1
- package/src/core/providerRuntime/antigravity.ts +305 -0
- package/src/core/providerRuntime/claudeCodeDiscovery.ts +268 -22
- package/src/core/providerRuntime/claudeCodeDiscoveryDebug.ts +55 -0
- package/src/core/providerRuntime/contextMetadata.ts +12 -24
- package/src/core/providerRuntime/local.ts +129 -51
- package/src/core/providerRuntime/models.ts +22 -11
- package/src/core/providerRuntime/registry.ts +58 -31
- package/src/core/providerRuntime/types.ts +19 -14
- package/src/core/providers/codexSubprocess.ts +2 -2
- package/src/core/providers/types.ts +1 -1
- package/src/core/{attachments.ts → shared/attachments.ts} +27 -4
- package/src/core/{cleanupFastFail.ts → shared/cleanupFastFail.ts} +1 -1
- package/src/core/{hollowResponseFormat.ts → shared/hollowResponseFormat.ts} +1 -1
- package/src/core/terminal/clearFrameBoundary.ts +814 -0
- package/src/core/terminal/frameLock.ts +109 -0
- package/src/core/terminal/inkRenderReset.ts +123 -0
- package/src/core/terminal/terminalControl.ts +22 -0
- package/src/core/terminal/terminalTitle.ts +16 -102
- package/src/core/version/channel.ts +23 -0
- package/src/core/version/updateCheck.ts +193 -0
- package/src/core/{launchContext.ts → workspace/launchContext.ts} +32 -39
- package/src/core/{planStorage.ts → workspace/planStorage.ts} +2 -2
- package/src/core/{workspaceGuard.ts → workspace/workspaceGuard.ts} +97 -13
- package/src/headless/execRunner.ts +2 -2
- package/src/index.tsx +43 -89
- package/src/session/appSession.ts +10 -7
- package/src/session/chatLifecycle.ts +1 -1
- package/src/session/liveRenderScheduler.ts +1 -1
- package/src/session/types.ts +3 -2
- package/src/ui/ActionRequiredBlock.tsx +5 -5
- package/src/ui/ActivityBars.tsx +3 -3
- package/src/ui/ActivityIndicator.tsx +6 -6
- package/src/ui/AgentBlock.tsx +6 -6
- package/src/ui/AnimatedStatusText.tsx +1 -1
- package/src/ui/AppShell.tsx +670 -719
- package/src/ui/AttachmentImportPanel.tsx +8 -8
- package/src/ui/AuthPanel.tsx +20 -20
- package/src/ui/BottomComposer.tsx +158 -118
- package/src/ui/DashCard.tsx +3 -3
- package/src/ui/Markdown.tsx +17 -17
- package/src/ui/ModelPickerScreen.tsx +222 -42
- package/src/ui/ModelReasoningPicker.tsx +15 -15
- package/src/ui/Panel.tsx +3 -3
- package/src/ui/PlanActionPicker.tsx +6 -6
- package/src/ui/PlanReviewPanel.tsx +9 -9
- package/src/ui/ProviderPicker.tsx +735 -321
- package/src/ui/RunFooter.tsx +3 -3
- package/src/ui/RuntimeStatusBar.tsx +108 -0
- package/src/ui/SelectionPanel.tsx +8 -4
- package/src/ui/SettingsPanel.tsx +9 -9
- package/src/ui/Spinner.tsx +1 -1
- package/src/ui/TextEntryPanel.tsx +11 -11
- package/src/ui/ThinkingBlock.tsx +8 -8
- package/src/ui/Timeline.tsx +1625 -1472
- package/src/ui/TopHeader.tsx +437 -293
- package/src/ui/TranscriptShell.tsx +322 -0
- package/src/ui/TurnGroup.tsx +33 -33
- package/src/ui/UpdateAvailableCard.tsx +41 -0
- package/src/ui/UpdatePromptPanel.tsx +197 -0
- package/src/ui/focus.ts +3 -0
- package/src/ui/layout.ts +299 -25
- package/src/ui/layoutListWindow.ts +145 -0
- package/src/ui/logoVariants.ts +103 -0
- package/src/ui/modeDisplay.ts +12 -12
- package/src/ui/runtimeDisplay.ts +112 -0
- package/src/ui/textLayout.ts +15 -4
- package/src/ui/theme.tsx +274 -395
- package/src/ui/timelineMeasure.ts +218 -136
- package/scripts/audit-codexa-capabilities.mjs +0 -466
- package/scripts/gen-build-info.mjs +0 -33
- package/scripts/smoke-terminal-bench.mjs +0 -35
- package/src/appRenderStability.test.ts +0 -131
- package/src/commands/handler.test.ts +0 -655
- package/src/config/launchArgs.test.ts +0 -189
- package/src/config/layeredConfig.test.ts +0 -143
- package/src/config/persistence.test.ts +0 -114
- package/src/config/runtimeConfig.test.ts +0 -218
- package/src/config/settings.test.ts +0 -155
- package/src/config/trustStore.test.ts +0 -29
- package/src/core/attachments.test.ts +0 -155
- package/src/core/auth/codexAuth.test.ts +0 -68
- package/src/core/cleanupFastFail.test.ts +0 -76
- package/src/core/codex.ts +0 -124
- package/src/core/codexExecArgs.test.ts +0 -195
- package/src/core/codexLaunch.test.ts +0 -205
- package/src/core/codexPrompt.test.ts +0 -252
- package/src/core/executables/codexExecutable.test.ts +0 -212
- package/src/core/executables/executableResolver.test.ts +0 -129
- package/src/core/executables/geminiExecutable.test.ts +0 -116
- package/src/core/executables/pathSanityScan.test.ts +0 -47
- package/src/core/githubDiagnostics.test.ts +0 -92
- package/src/core/hollowResponseFormat.test.ts +0 -58
- package/src/core/launchContext.test.ts +0 -157
- package/src/core/models/codexCapabilities.test.ts +0 -45
- package/src/core/models/codexModelCapabilities.test.ts +0 -246
- package/src/core/models/modelSpecs.test.ts +0 -283
- package/src/core/perf/renderDebug.test.ts +0 -230
- package/src/core/planStorage.test.ts +0 -143
- package/src/core/process/CommandRunner.test.ts +0 -105
- package/src/core/projectInstructions.test.ts +0 -50
- package/src/core/providerLauncher/launcher.test.ts +0 -238
- package/src/core/providerLauncher/registry.test.ts +0 -324
- package/src/core/providerLauncher/workspaceConfig.test.ts +0 -638
- package/src/core/providerRuntime/anthropic.test.ts +0 -1120
- package/src/core/providerRuntime/capabilityProfile.test.ts +0 -311
- package/src/core/providerRuntime/contextMetadata.test.ts +0 -468
- package/src/core/providerRuntime/gemini.test.ts +0 -437
- package/src/core/providerRuntime/lmstudio.test.ts +0 -168
- package/src/core/providerRuntime/local.test.ts +0 -787
- package/src/core/providerRuntime/registry.test.ts +0 -233
- package/src/core/providers/codexJsonStream.test.ts +0 -148
- package/src/core/providers/codexSubprocess.test.ts +0 -68
- package/src/core/providers/codexTranscript.test.ts +0 -284
- package/src/core/terminal/startupClear.test.ts +0 -55
- package/src/core/terminal/terminalCapabilities.test.ts +0 -93
- package/src/core/terminal/terminalControl.test.ts +0 -75
- package/src/core/terminal/terminalSanitize.test.ts +0 -22
- package/src/core/terminal/terminalSelection.test.ts +0 -42
- package/src/core/terminal/terminalTitle.test.ts +0 -328
- package/src/core/updateCheck.test.ts +0 -194
- package/src/core/updateCheck.ts +0 -172
- package/src/core/workspaceActivity.test.ts +0 -163
- package/src/core/workspaceGuard.test.ts +0 -151
- package/src/core/workspaceRoot.test.ts +0 -23
- package/src/exec.test.ts +0 -13
- package/src/headless/execArgs.test.ts +0 -147
- package/src/headless/execRunner.test.ts +0 -436
- package/src/index.test.tsx +0 -620
- package/src/session/appSession.test.ts +0 -897
- package/src/session/chatLifecycle.test.ts +0 -64
- package/src/session/liveRenderScheduler.test.ts +0 -201
- package/src/session/planFlow.test.ts +0 -103
- package/src/session/planTranscript.test.ts +0 -65
- package/src/session/promptRunSchedule.test.ts +0 -36
- package/src/ui/ActivityIndicator.test.tsx +0 -58
- package/src/ui/AgentBlock.test.ts +0 -6
- package/src/ui/AnimatedStatusText.test.ts +0 -16
- package/src/ui/AppShell.test.tsx +0 -1776
- package/src/ui/AttachmentImportPanel.test.tsx +0 -204
- package/src/ui/BottomComposer.test.ts +0 -674
- package/src/ui/CodexLogo.tsx +0 -55
- package/src/ui/Markdown.test.ts +0 -157
- package/src/ui/ModelPickerProviderScope.test.tsx +0 -411
- package/src/ui/ModelPickerScreen.test.tsx +0 -99
- package/src/ui/ModelPickerState.test.tsx +0 -151
- package/src/ui/ModelReasoningPicker.test.tsx +0 -447
- package/src/ui/PlanReviewPanel.test.tsx +0 -267
- package/src/ui/PromptCardBorder.test.tsx +0 -161
- package/src/ui/ProviderPicker.test.tsx +0 -289
- package/src/ui/ProviderShortcut.test.tsx +0 -143
- package/src/ui/SettingsPanel.test.tsx +0 -233
- package/src/ui/StaticTranscriptItem.tsx +0 -56
- package/src/ui/Timeline.test.ts +0 -2067
- package/src/ui/TimelineNavigation.test.tsx +0 -201
- package/src/ui/TopHeader.test.tsx +0 -254
- package/src/ui/TurnGroup.test.tsx +0 -365
- package/src/ui/busyStatusAnimation.test.ts +0 -30
- package/src/ui/commandNormalize.test.ts +0 -142
- package/src/ui/diffRenderer.test.ts +0 -102
- package/src/ui/focusFlow.test.tsx +0 -1098
- package/src/ui/inputBuffer.test.ts +0 -151
- package/src/ui/layout.test.ts +0 -146
- package/src/ui/modeDisplay.test.ts +0 -42
- package/src/ui/runActivityView.test.ts +0 -89
- package/src/ui/runLifecycleView.test.tsx +0 -237
- package/src/ui/statusRenderIsolation.test.tsx +0 -654
- package/src/ui/terminalAnswerFormat.test.ts +0 -19
- package/src/ui/textLayout.test.ts +0 -18
- package/src/ui/themeFlow.test.ts +0 -53
- package/src/ui/timelineMeasureCache.test.ts +0 -986
- /package/src/core/{inputDebug.ts → debug/inputDebug.ts} +0 -0
- /package/src/core/{clipboard.ts → shared/clipboard.ts} +0 -0
- /package/src/core/{githubDiagnostics.ts → shared/githubDiagnostics.ts} +0 -0
- /package/src/core/{projectInstructions.ts → workspace/projectInstructions.ts} +0 -0
- /package/src/core/{workspaceActivity.ts → workspace/workspaceActivity.ts} +0 -0
- /package/src/core/{workspaceRoot.ts → workspace/workspaceRoot.ts} +0 -0
package/src/app.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { startTransition, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
1
|
+
import React, { startTransition, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import { spawn } from "child_process";
|
|
3
3
|
import { existsSync } from "fs";
|
|
4
4
|
import path from "node:path";
|
|
@@ -7,6 +7,38 @@ import path from "node:path";
|
|
|
7
7
|
function appDiagLog(msg: string): void {
|
|
8
8
|
void msg;
|
|
9
9
|
}
|
|
10
|
+
|
|
11
|
+
function normalizeRuntimeAvailability(value: unknown): RuntimeAvailability {
|
|
12
|
+
if (value === "checking" || value === "reconnecting") return value;
|
|
13
|
+
if (value === "available") return "available";
|
|
14
|
+
if (value === "unavailable" || value === "no-models") return "unavailable";
|
|
15
|
+
return "unknown";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function formatRuntimeProviderLabel(providerId: ProviderId): string {
|
|
19
|
+
if (providerId === "local") return "Local";
|
|
20
|
+
if (providerId === "google") return "Google";
|
|
21
|
+
if (providerId === "anthropic") return "Anthropic";
|
|
22
|
+
if (providerId === "antigravity") return "Antigravity";
|
|
23
|
+
return "OpenAI";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function readDiagnosticString(
|
|
27
|
+
diagnostics: Record<string, string | number | boolean | null> | undefined,
|
|
28
|
+
keys: string[],
|
|
29
|
+
): string | null {
|
|
30
|
+
if (!diagnostics) return null;
|
|
31
|
+
for (const key of keys) {
|
|
32
|
+
const value = diagnostics[key];
|
|
33
|
+
if (typeof value === "string" && value.trim()) {
|
|
34
|
+
return value.trim();
|
|
35
|
+
}
|
|
36
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
37
|
+
return String(value);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
10
42
|
import { Box, Text, useApp, useFocusManager, useInput, useStdin, useStdout } from "ink";
|
|
11
43
|
import { handleCommand } from "./commands/handler.js";
|
|
12
44
|
import {
|
|
@@ -17,6 +49,7 @@ import {
|
|
|
17
49
|
import type { LaunchArgs } from "./config/launchArgs.js";
|
|
18
50
|
import { loadSettings, saveSettings } from "./config/persistence.js";
|
|
19
51
|
import {
|
|
52
|
+
APP_VERSION,
|
|
20
53
|
type AuthPreference,
|
|
21
54
|
type AvailableBackend,
|
|
22
55
|
type AvailableMode,
|
|
@@ -76,9 +109,9 @@ import {
|
|
|
76
109
|
probeCodexAuthStatus,
|
|
77
110
|
} from "./core/auth/codexAuth.js";
|
|
78
111
|
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";
|
|
112
|
+
import { copyToClipboard } from "./core/shared/clipboard.js";
|
|
113
|
+
import { normalizePlanReviewMarkdown, savePlan, readPlan } from "./core/workspace/planStorage.js";
|
|
114
|
+
import { getBlockedCleanupFailure } from "./core/shared/cleanupFastFail.js";
|
|
82
115
|
import { runShellCommand, summarizeCommandResult } from "./core/process/CommandRunner.js";
|
|
83
116
|
import {
|
|
84
117
|
buildPlanExecutionPrompt,
|
|
@@ -86,8 +119,8 @@ import {
|
|
|
86
119
|
detectHollowResponse,
|
|
87
120
|
isClearlySafeGeneratedCleanupRequest,
|
|
88
121
|
resolveExecutionMode,
|
|
89
|
-
} from "./core/codexPrompt.js";
|
|
90
|
-
import { formatHollowResponse } from "./core/hollowResponseFormat.js";
|
|
122
|
+
} from "./core/codex/codexPrompt.js";
|
|
123
|
+
import { formatHollowResponse } from "./core/shared/hollowResponseFormat.js";
|
|
91
124
|
import {
|
|
92
125
|
createFallbackModelCapabilities,
|
|
93
126
|
findModelCapability,
|
|
@@ -103,31 +136,29 @@ import {
|
|
|
103
136
|
buildWorkspaceCommandContext,
|
|
104
137
|
createWorkspaceRelaunchPlan,
|
|
105
138
|
guardWorkspaceRelaunch,
|
|
139
|
+
type LaunchContext,
|
|
106
140
|
resolveLaunchContext,
|
|
107
|
-
} from "./core/launchContext.js";
|
|
141
|
+
} from "./core/workspace/launchContext.js";
|
|
108
142
|
import {
|
|
109
143
|
findOutsideWorkspacePaths,
|
|
144
|
+
formatSkippedDependencyPath,
|
|
110
145
|
getPromptWorkspaceGuardMessage,
|
|
111
146
|
getShellWorkspaceGuardMessage,
|
|
112
|
-
} from "./core/workspaceGuard.js";
|
|
113
|
-
import {
|
|
114
|
-
type ModelSpec,
|
|
115
|
-
} from "./core/models/modelSpecs.js";
|
|
147
|
+
} from "./core/workspace/workspaceGuard.js";
|
|
116
148
|
import {
|
|
117
|
-
contextMetadataToModelSpec,
|
|
118
149
|
formatContextCompact,
|
|
119
150
|
formatContextLength,
|
|
120
151
|
resolveModelContextLength,
|
|
121
152
|
type ModelContextMetadata,
|
|
122
153
|
} from "./core/providerRuntime/contextMetadata.js";
|
|
123
|
-
import { captureWorkspaceSnapshot, createWorkspaceActivityTracker, diffWorkspaceSnapshots } from "./core/workspaceActivity.js";
|
|
124
|
-
import { resolveWorkspaceRoot } from "./core/workspaceRoot.js";
|
|
154
|
+
import { captureWorkspaceSnapshot, createWorkspaceActivityTracker, diffWorkspaceSnapshots } from "./core/workspace/workspaceActivity.js";
|
|
155
|
+
import { resolveWorkspaceRoot } from "./core/workspace/workspaceRoot.js";
|
|
125
156
|
import {
|
|
126
157
|
importExternalFile,
|
|
127
158
|
isImageFile,
|
|
128
159
|
rewritePromptWithImportedPaths,
|
|
129
|
-
} from "./core/attachments.js";
|
|
130
|
-
import { loadProjectInstructions } from "./core/projectInstructions.js";
|
|
160
|
+
} from "./core/shared/attachments.js";
|
|
161
|
+
import { loadProjectInstructions } from "./core/workspace/projectInstructions.js";
|
|
131
162
|
import { isNoiseLine } from "./core/providers/codexTranscript.js";
|
|
132
163
|
import { getBackendProvider } from "./core/providers/registry.js";
|
|
133
164
|
import type { BackendProgressUpdate, BackendProvider } from "./core/providers/types.js";
|
|
@@ -157,16 +188,13 @@ import {
|
|
|
157
188
|
} from "./core/providerLauncher/workspaceConfig.js";
|
|
158
189
|
import { sanitizeTerminalInput, sanitizeTerminalLines, sanitizeTerminalOutput } from "./core/terminal/terminalSanitize.js";
|
|
159
190
|
import { createTerminalModeController, setTerminalControlUIState } from "./core/terminal/terminalControl.js";
|
|
191
|
+
import { resolveInkRenderInstance, resetInkOutputForFreshFrame } from "./core/terminal/inkRenderReset.js";
|
|
192
|
+
import { createClearFrameBoundaryController } from "./core/terminal/clearFrameBoundary.js";
|
|
160
193
|
import {
|
|
161
|
-
acquireTerminalTitleGuard,
|
|
162
|
-
beginColdStartSequence,
|
|
163
|
-
deriveTerminalTitle,
|
|
164
|
-
reassertIntendedTerminalTitle,
|
|
165
|
-
refreshTerminalTitle,
|
|
166
194
|
setTerminalTitleLifecycleState,
|
|
167
|
-
setIntendedTerminalTitle,
|
|
168
195
|
} from "./core/terminal/terminalTitle.js";
|
|
169
|
-
import { getStdinDebugState, traceInputDebug } from "./core/inputDebug.js";
|
|
196
|
+
import { getStdinDebugState, traceInputDebug } from "./core/debug/inputDebug.js";
|
|
197
|
+
import { traceModelStateDebug } from "./core/debug/modelStateDebug.js";
|
|
170
198
|
import * as perf from "./core/perf/profiler.js";
|
|
171
199
|
import * as renderDebug from "./core/perf/renderDebug.js";
|
|
172
200
|
import {
|
|
@@ -177,7 +205,7 @@ import {
|
|
|
177
205
|
getLocalGitRemoteUrl,
|
|
178
206
|
parseRepoIdentity,
|
|
179
207
|
type DiagnosticResult,
|
|
180
|
-
} from "./core/githubDiagnostics.js";
|
|
208
|
+
} from "./core/shared/githubDiagnostics.js";
|
|
181
209
|
import type { RunEvent, Screen, ShellEvent, TimelineEvent, UIState, UserPromptEvent } from "./session/types.js";
|
|
182
210
|
import {
|
|
183
211
|
buildFollowUpPrompt,
|
|
@@ -204,7 +232,7 @@ import {
|
|
|
204
232
|
import { AuthPanel } from "./ui/AuthPanel.js";
|
|
205
233
|
import { BackendPicker } from "./ui/BackendPicker.js";
|
|
206
234
|
import { measureBottomComposerRows, MemoizedBottomComposer } from "./ui/BottomComposer.js";
|
|
207
|
-
import { useTerminalViewport } from "./ui/layout.js";
|
|
235
|
+
import { resolveStartupHeaderMode, useTerminalViewport } from "./ui/layout.js";
|
|
208
236
|
import { ModelPickerScreen } from "./ui/ModelPickerScreen.js";
|
|
209
237
|
import { ModePicker } from "./ui/ModePicker.js";
|
|
210
238
|
import { PlanActionPicker, type PlanActionValue, measurePlanActionPickerRows } from "./ui/PlanActionPicker.js";
|
|
@@ -214,10 +242,12 @@ import { ReasoningPicker } from "./ui/ReasoningPicker.js";
|
|
|
214
242
|
import { AttachmentImportPanel, type PendingImportFile } from "./ui/AttachmentImportPanel.js";
|
|
215
243
|
import { SelectionPanel } from "./ui/SelectionPanel.js";
|
|
216
244
|
import { SettingsPanel } from "./ui/SettingsPanel.js";
|
|
245
|
+
import { UpdatePromptPanel } from "./ui/UpdatePromptPanel.js";
|
|
217
246
|
import { measureTextEntryPanelRows, TextEntryPanel } from "./ui/TextEntryPanel.js";
|
|
218
247
|
import { ThemePicker } from "./ui/ThemePicker.js";
|
|
219
248
|
import { getFocusTargetForScreen, FOCUS_IDS } from "./ui/focus.js";
|
|
220
249
|
import { ThemeProvider, THEMES } from "./ui/theme.js";
|
|
250
|
+
import { buildActiveRuntimeDisplay, runtimeDisplayToSummary } from "./ui/runtimeDisplay.js";
|
|
221
251
|
import {
|
|
222
252
|
cancelThemeSelection,
|
|
223
253
|
commitThemeSelection,
|
|
@@ -228,7 +258,10 @@ import {
|
|
|
228
258
|
} from "./ui/themeFlow.js";
|
|
229
259
|
import { isBusy as isUiBusy } from "./session/types.js";
|
|
230
260
|
import { AppShell } from "./ui/AppShell.js";
|
|
231
|
-
import {
|
|
261
|
+
import { TranscriptShell } from "./ui/TranscriptShell.js";
|
|
262
|
+
import type { RuntimeAvailability } from "./ui/RuntimeStatusBar.js";
|
|
263
|
+
import { checkForUpdates, formatLocalDevUpdateStatus, formatUpdateInstructions, shouldRunStartupUpdateCheck, type UpdateCheckResult } from "./core/version/updateCheck.js";
|
|
264
|
+
import { isLocalDevChannel } from "./core/version/channel.js";
|
|
232
265
|
import {
|
|
233
266
|
isCacheValid,
|
|
234
267
|
loadUpdateCheckCache,
|
|
@@ -260,6 +293,51 @@ function createTurnId(): number {
|
|
|
260
293
|
return nextTurnId++;
|
|
261
294
|
}
|
|
262
295
|
|
|
296
|
+
function createLaunchModeEvent(launchContext: LaunchContext): TimelineEvent | null {
|
|
297
|
+
const devLaunchNotice = buildDevLaunchNotice(launchContext);
|
|
298
|
+
if (!devLaunchNotice) return null;
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
id: createEventId(),
|
|
302
|
+
type: "system",
|
|
303
|
+
createdAt: Date.now(),
|
|
304
|
+
title: sanitizeTerminalOutput("Launch mode"),
|
|
305
|
+
content: sanitizeTerminalOutput(devLaunchNotice, { preserveTabs: false, tabSize: 2 }),
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function createProviderMigrationNoticeEvent(
|
|
310
|
+
notice: ProviderWorkspaceConfig["migrationNotice"] | undefined,
|
|
311
|
+
providerLabel: string | null = null,
|
|
312
|
+
): TimelineEvent | null {
|
|
313
|
+
if (!notice) return null;
|
|
314
|
+
|
|
315
|
+
const resolvedProviderLabel = providerLabel ?? formatRuntimeProviderLabel(notice.revertedProviderId);
|
|
316
|
+
return {
|
|
317
|
+
id: createEventId(),
|
|
318
|
+
type: "system",
|
|
319
|
+
createdAt: Date.now(),
|
|
320
|
+
title: sanitizeTerminalOutput("Provider migrated"),
|
|
321
|
+
content: sanitizeTerminalOutput(
|
|
322
|
+
`${formatRuntimeProviderLabel(notice.deprecatedProviderId as ProviderId)} provider is no longer supported. Reverted to ${resolvedProviderLabel}.`,
|
|
323
|
+
{ preserveTabs: false, tabSize: 2 },
|
|
324
|
+
),
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function createStartupStaticEvents({
|
|
329
|
+
launchContext,
|
|
330
|
+
providerWorkspaceConfig,
|
|
331
|
+
}: {
|
|
332
|
+
launchContext: LaunchContext;
|
|
333
|
+
providerWorkspaceConfig: ProviderWorkspaceConfig;
|
|
334
|
+
}): TimelineEvent[] {
|
|
335
|
+
return [
|
|
336
|
+
createLaunchModeEvent(launchContext),
|
|
337
|
+
createProviderMigrationNoticeEvent(providerWorkspaceConfig.migrationNotice),
|
|
338
|
+
].filter((event): event is TimelineEvent => event !== null);
|
|
339
|
+
}
|
|
340
|
+
|
|
263
341
|
function createInitialAuthStatus(): CodexAuthProbeResult {
|
|
264
342
|
return {
|
|
265
343
|
state: "checking",
|
|
@@ -307,6 +385,9 @@ export function App({ launchArgs }: AppProps) {
|
|
|
307
385
|
? projectInstructionsLoad.instructions
|
|
308
386
|
: null;
|
|
309
387
|
const initialSettings = useRef(loadSettings());
|
|
388
|
+
const skippedUpdateVersionRef = useRef<string | null>(
|
|
389
|
+
initialSettings.current.updateCheck.skippedUpdateVersion ?? null,
|
|
390
|
+
);
|
|
310
391
|
const initialProviderWorkspaceConfig = useRef<ProviderWorkspaceConfig>(loadProviderWorkspaceConfig(workspaceRoot));
|
|
311
392
|
const initialLayeredConfig = useRef<LayeredConfigResult | null>(null);
|
|
312
393
|
if (initialLayeredConfig.current === null) {
|
|
@@ -321,6 +402,12 @@ export function App({ launchArgs }: AppProps) {
|
|
|
321
402
|
[launchContext],
|
|
322
403
|
);
|
|
323
404
|
const terminalLayout = useTerminalViewport();
|
|
405
|
+
// Assigned during render (like screenRef) so the clear-frame boundary can
|
|
406
|
+
// tell, at frame-write time, whether the committing tree was laid out
|
|
407
|
+
// against the current terminal width (the viewport hook commits dimensions
|
|
408
|
+
// on a trailing settle, so frames can lag stdout.columns).
|
|
409
|
+
const terminalLayoutColsRef = useRef<number | undefined>(terminalLayout.rawCols ?? terminalLayout.cols);
|
|
410
|
+
terminalLayoutColsRef.current = terminalLayout.rawCols ?? terminalLayout.cols;
|
|
324
411
|
|
|
325
412
|
// ─── State & Refs ────────────────────────────────────────────────────────────
|
|
326
413
|
|
|
@@ -372,7 +459,26 @@ export function App({ launchArgs }: AppProps) {
|
|
|
372
459
|
const screenRef = useRef<Screen>("main");
|
|
373
460
|
screenRef.current = screen;
|
|
374
461
|
const [composerInstanceKey, setComposerInstanceKey] = useState(0);
|
|
375
|
-
|
|
462
|
+
// Bumped purely to force one extra React commit when the /clear boundary needs
|
|
463
|
+
// the authoritative post-clear frame flushed (see the syncRenderState effect).
|
|
464
|
+
const [, bumpPostClearRepaint] = useState(0);
|
|
465
|
+
// Bumped whenever a width-changing resize forces a physical terminal clear
|
|
466
|
+
// (see clearFrameBoundaryController below). TranscriptShell folds this into
|
|
467
|
+
// its <Static> key so already-flushed content (logo, past turns) reprints
|
|
468
|
+
// at the new width instead of staying erased — Ink's <Static> never
|
|
469
|
+
// re-emits items on its own once flushed.
|
|
470
|
+
const [staticRepaintGeneration, bumpStaticRepaintGeneration] = useState(0);
|
|
471
|
+
// Assigned during render (like screenRef) so the clear-frame boundary can
|
|
472
|
+
// tell, at frame-write time, whether the committing tree already contains
|
|
473
|
+
// the re-flushed <Static> content for a pending width repaint.
|
|
474
|
+
const staticRepaintGenerationRef = useRef(0);
|
|
475
|
+
staticRepaintGenerationRef.current = staticRepaintGeneration;
|
|
476
|
+
const { state: sessionState, dispatch: dispatchSession } = useAppSessionState(() => {
|
|
477
|
+
return createStartupStaticEvents({
|
|
478
|
+
launchContext,
|
|
479
|
+
providerWorkspaceConfig: initialProviderWorkspaceConfig.current,
|
|
480
|
+
});
|
|
481
|
+
});
|
|
376
482
|
const [authStatus, setAuthStatus] = useState<CodexAuthProbeResult>(createInitialAuthStatus());
|
|
377
483
|
const [authStatusBusy, setAuthStatusBusy] = useState(false);
|
|
378
484
|
// Running character total across the conversation — used to estimate token usage
|
|
@@ -383,6 +489,24 @@ export function App({ launchArgs }: AppProps) {
|
|
|
383
489
|
const { stdout } = useStdout();
|
|
384
490
|
const { stdin } = useStdin();
|
|
385
491
|
const terminalControl = useMemo(() => createTerminalModeController((chunk) => stdout.write(chunk)), [stdout]);
|
|
492
|
+
// Live Ink instance behind this stdout, used to reset Ink's frame caches on
|
|
493
|
+
// the /clear boundary so the next frame is authoritative (see handleClear).
|
|
494
|
+
const inkInstance = useMemo(() => resolveInkRenderInstance(stdout), [stdout]);
|
|
495
|
+
const clearFrameBoundaryController = useMemo(
|
|
496
|
+
() => createClearFrameBoundaryController({
|
|
497
|
+
instance: inkInstance,
|
|
498
|
+
terminalControl,
|
|
499
|
+
stdout,
|
|
500
|
+
source: "src/app.tsx:clearBoundary",
|
|
501
|
+
// Read at frame-write time; screenRef is assigned during render, so it
|
|
502
|
+
// always reflects the render that produced the frame being written.
|
|
503
|
+
isOverlayActive: () => screenRef.current !== "main",
|
|
504
|
+
onWidthResizeRefresh: () => bumpStaticRepaintGeneration((tick) => tick + 1),
|
|
505
|
+
getRenderedRepaintGeneration: () => staticRepaintGenerationRef.current,
|
|
506
|
+
getRenderedLayoutCols: () => terminalLayoutColsRef.current,
|
|
507
|
+
}),
|
|
508
|
+
[inkInstance, stdout, terminalControl],
|
|
509
|
+
);
|
|
386
510
|
const [mouseOverride, setMouseOverride] = useState<boolean | null>(null);
|
|
387
511
|
const [isMouseIdle, setIsMouseIdle] = useState(false);
|
|
388
512
|
const mouseIdleTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
@@ -392,9 +516,14 @@ export function App({ launchArgs }: AppProps) {
|
|
|
392
516
|
if (mouseIdleTimerRef.current) {
|
|
393
517
|
clearTimeout(mouseIdleTimerRef.current);
|
|
394
518
|
}
|
|
519
|
+
// We disable the idle timeout to ensure reliable trackpad/mouse-wheel scrolling
|
|
520
|
+
// in the pop-out window. Otherwise, after 1.5 seconds of inactivity, mouse capture
|
|
521
|
+
// turns off, and scrolling stops working entirely.
|
|
522
|
+
/*
|
|
395
523
|
mouseIdleTimerRef.current = setTimeout(() => {
|
|
396
524
|
setIsMouseIdle(true);
|
|
397
525
|
}, 1500);
|
|
526
|
+
*/
|
|
398
527
|
}, []);
|
|
399
528
|
|
|
400
529
|
useInput(() => {
|
|
@@ -406,26 +535,71 @@ export function App({ launchArgs }: AppProps) {
|
|
|
406
535
|
const [planFlow, setPlanFlow] = useState<PlanFlowState>(createInitialPlanFlowState);
|
|
407
536
|
const [initialRevisionText, setInitialRevisionText] = useState("");
|
|
408
537
|
const [updateCheckResult, setUpdateCheckResult] = useState<UpdateCheckResult | null>(null);
|
|
409
|
-
//
|
|
410
|
-
//
|
|
411
|
-
// wheel events; native drag-select then requires Shift in Windows Terminal.
|
|
412
|
-
// "selection" keeps the terminal in control of mouse events.
|
|
413
|
-
// /mouse toggles the in-session override without changing the saved setting.
|
|
414
|
-
//
|
|
415
|
-
// We apply an idle-timeout: if no wheel events or keypresses occur for 1.5s,
|
|
416
|
-
// we disable mouse reporting so native drag-selection works unmodified.
|
|
538
|
+
// Transcript mode leaves mouse reporting off so wheel/trackpad input scrolls
|
|
539
|
+
// the terminal emulator's native scrollback instead of an in-app viewport.
|
|
417
540
|
const mouseCapture = (mouseOverride ?? (terminalMouseMode === "wheel")) && !isMouseIdle;
|
|
541
|
+
const effectiveMouseCapture = false;
|
|
542
|
+
const overlayMode = screen !== "main";
|
|
418
543
|
|
|
419
544
|
// ─── Effects & Handlers ──────────────────────────────────────────────────────
|
|
420
545
|
|
|
421
546
|
useEffect(() => {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
547
|
+
return () => {
|
|
548
|
+
clearFrameBoundaryController?.dispose();
|
|
549
|
+
};
|
|
550
|
+
}, [clearFrameBoundaryController]);
|
|
551
|
+
|
|
552
|
+
useEffect(() => {
|
|
553
|
+
if (!clearFrameBoundaryController) return;
|
|
554
|
+
const postClearRepaintPending = clearFrameBoundaryController.syncRenderState({
|
|
555
|
+
generation: sessionState.clearEpoch,
|
|
556
|
+
staticEventsLength: sessionState.staticEvents.length,
|
|
557
|
+
activeEventsLength: sessionState.activeEvents.length,
|
|
558
|
+
transcriptCleared: sessionState.staticEvents.length === 0 && sessionState.activeEvents.length === 0,
|
|
559
|
+
clearGenerationReady: sessionState.clearEpoch > 0 && sessionState.uiState.kind === "IDLE" && sessionState.activeEvents.length === 0,
|
|
560
|
+
uiStateKind: sessionState.uiState.kind,
|
|
561
|
+
});
|
|
562
|
+
if (postClearRepaintPending) {
|
|
563
|
+
// Ink already wrote (and suppressed) the cleared frame during the commit
|
|
564
|
+
// that preceded this passive effect, so the boundary's gate only became
|
|
565
|
+
// satisfiable just now. Force one more commit to deterministically flush
|
|
566
|
+
// the authoritative post-clear frame instead of waiting on an incidental
|
|
567
|
+
// later render. `bumpPostClearRepaint` is not an effect dependency, so this
|
|
568
|
+
// re-render does not re-run the effect (no loop).
|
|
569
|
+
bumpPostClearRepaint((tick) => tick + 1);
|
|
570
|
+
}
|
|
571
|
+
}, [clearFrameBoundaryController, sessionState]);
|
|
572
|
+
|
|
573
|
+
useEffect(() => {
|
|
574
|
+
// Main transcript mode keeps native terminal scrollback and selection in
|
|
575
|
+
// control. Keep writing the disable sequence defensively in case a previous
|
|
576
|
+
// version or overlay left mouse reporting enabled.
|
|
577
|
+
terminalControl.setMouseReporting(effectiveMouseCapture, effectiveMouseCapture ? "src/app.tsx:mouseCapture.enable" : "src/app.tsx:mouseCapture.disable");
|
|
425
578
|
return () => {
|
|
426
579
|
terminalControl.setMouseReporting(false, "src/app.tsx:mouseCapture.cleanup");
|
|
427
580
|
};
|
|
428
|
-
}, [
|
|
581
|
+
}, [effectiveMouseCapture, terminalControl]);
|
|
582
|
+
|
|
583
|
+
useLayoutEffect(() => {
|
|
584
|
+
// The clear-frame boundary owns alternate-screen switching so the buffer
|
|
585
|
+
// flip happens atomically with the first frame of the new screen (see
|
|
586
|
+
// clearFrameBoundary.ts). Toggling from an effect would run after Ink has
|
|
587
|
+
// already written that frame into the wrong buffer — the overlay would
|
|
588
|
+
// land in the normal buffer's scrollback and the alt screen would open
|
|
589
|
+
// blank. This effect is only a fallback for environments where no live
|
|
590
|
+
// Ink instance could be resolved (tests, exotic Ink versions).
|
|
591
|
+
if (clearFrameBoundaryController) return;
|
|
592
|
+
terminalControl.setAlternateScreen(
|
|
593
|
+
overlayMode,
|
|
594
|
+
overlayMode ? "src/app.tsx:overlay.enterAlternateScreen" : "src/app.tsx:overlay.exitAlternateScreen",
|
|
595
|
+
);
|
|
596
|
+
}, [clearFrameBoundaryController, overlayMode, terminalControl]);
|
|
597
|
+
|
|
598
|
+
useEffect(() => {
|
|
599
|
+
return () => {
|
|
600
|
+
terminalControl.setAlternateScreen(false, "src/app.tsx:overlay.cleanupAlternateScreen");
|
|
601
|
+
};
|
|
602
|
+
}, [terminalControl]);
|
|
429
603
|
|
|
430
604
|
const cleanupRef = useRef<(() => void) | null>(null);
|
|
431
605
|
const activeRunLifecycleRef = useRef<PromptRunLifecycle | null>(null);
|
|
@@ -444,7 +618,7 @@ export function App({ launchArgs }: AppProps) {
|
|
|
444
618
|
const modelSelectionInFlightRef = useRef(false);
|
|
445
619
|
const providerRouteErrorsRef = useRef<Record<string, string>>({});
|
|
446
620
|
const providerDiagnosticsRef = useRef<Record<string, Record<string, string | number | boolean | null>>>({});
|
|
447
|
-
const providerMigrationNoticeShownRef = useRef(
|
|
621
|
+
const providerMigrationNoticeShownRef = useRef(Boolean(initialProviderWorkspaceConfig.current.migrationNotice));
|
|
448
622
|
const initialPromptSubmittedRef = useRef(false);
|
|
449
623
|
const activeThemeName = getDisplayedThemeName(themeSelection);
|
|
450
624
|
const activeTheme =
|
|
@@ -500,6 +674,31 @@ export function App({ launchArgs }: AppProps) {
|
|
|
500
674
|
[providerRegistry],
|
|
501
675
|
);
|
|
502
676
|
const activeRouteProviderId = activeProviderRoute.providerId;
|
|
677
|
+
const markProviderAvailability = useCallback((
|
|
678
|
+
providerId: ProviderId,
|
|
679
|
+
availability: RuntimeAvailability,
|
|
680
|
+
reason: string,
|
|
681
|
+
) => {
|
|
682
|
+
const previous = providerDiagnosticsRef.current[providerId] ?? {};
|
|
683
|
+
const selectedModel = typeof previous.selectedModel === "string" && previous.selectedModel.trim()
|
|
684
|
+
? previous.selectedModel.trim()
|
|
685
|
+
: providerId === activeProviderRoute.providerId
|
|
686
|
+
? activeProviderRoute.modelId
|
|
687
|
+
: null;
|
|
688
|
+
providerDiagnosticsRef.current[providerId] = {
|
|
689
|
+
...previous,
|
|
690
|
+
selectedModel,
|
|
691
|
+
availabilityStatus: availability,
|
|
692
|
+
endpointCheckResult: availability,
|
|
693
|
+
};
|
|
694
|
+
traceModelStateDebug("provider_availability_marked", {
|
|
695
|
+
providerId,
|
|
696
|
+
selectedModel,
|
|
697
|
+
availability,
|
|
698
|
+
reason,
|
|
699
|
+
});
|
|
700
|
+
setRegistryNonce((current) => current + 1);
|
|
701
|
+
}, [activeProviderRoute.modelId, activeProviderRoute.providerId]);
|
|
503
702
|
|
|
504
703
|
// Reset provider readiness when the user switches to a different provider.
|
|
505
704
|
useEffect(() => {
|
|
@@ -733,17 +932,6 @@ export function App({ launchArgs }: AppProps) {
|
|
|
733
932
|
() => formatWorkspaceDisplayPath(workspaceRoot, workspaceDisplayMode),
|
|
734
933
|
[workspaceDisplayMode, workspaceRoot],
|
|
735
934
|
);
|
|
736
|
-
const terminalTitleLabel = useMemo(
|
|
737
|
-
() => deriveTerminalTitle(workspaceRoot, terminalTitleMode),
|
|
738
|
-
[terminalTitleMode, workspaceRoot],
|
|
739
|
-
);
|
|
740
|
-
const latestTerminalTitleRef = useRef(terminalTitleLabel);
|
|
741
|
-
latestTerminalTitleRef.current = terminalTitleLabel;
|
|
742
|
-
|
|
743
|
-
// Cold-start: write title immediately on mount, then retry to outlast Windows Terminal shell integration.
|
|
744
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
745
|
-
useEffect(() => beginColdStartSequence(latestTerminalTitleRef.current), []);
|
|
746
|
-
|
|
747
935
|
const { staticEvents, activeEvents, uiState, inputValue, cursor } = sessionState;
|
|
748
936
|
|
|
749
937
|
const currentUserSettings = useMemo<UserSettingValues>(() => ({
|
|
@@ -765,16 +953,73 @@ export function App({ launchArgs }: AppProps) {
|
|
|
765
953
|
[planFlow],
|
|
766
954
|
);
|
|
767
955
|
|
|
768
|
-
const
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
956
|
+
const activeRuntimeDisplay = useMemo(() => buildActiveRuntimeDisplay({
|
|
957
|
+
route: activeProviderRoute,
|
|
958
|
+
reasoningLevel,
|
|
959
|
+
mode,
|
|
960
|
+
tokensUsed: estimateTokens(conversationChars),
|
|
961
|
+
modelCapability: currentModelCapability,
|
|
962
|
+
contextMetadata: activeContextMetadata,
|
|
963
|
+
}), [
|
|
964
|
+
activeContextMetadata,
|
|
965
|
+
activeProviderRoute,
|
|
966
|
+
conversationChars,
|
|
967
|
+
currentModelCapability,
|
|
968
|
+
mode,
|
|
969
|
+
reasoningLevel,
|
|
970
|
+
]);
|
|
971
|
+
const currentModelSpec = activeRuntimeDisplay.modelSpec;
|
|
972
|
+
const activeRuntimeAvailability = useMemo<RuntimeAvailability>(() => {
|
|
973
|
+
if (activeProviderRoute.providerId !== "local") {
|
|
974
|
+
return "available";
|
|
975
|
+
}
|
|
976
|
+
const diagnostics = providerDiagnosticsRef.current.local;
|
|
977
|
+
return normalizeRuntimeAvailability(diagnostics?.availabilityStatus ?? diagnostics?.endpointCheckResult);
|
|
978
|
+
// registryNonce intentionally re-reads providerDiagnosticsRef.
|
|
979
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
980
|
+
}, [activeProviderRoute.providerId, registryNonce]);
|
|
981
|
+
const visibleRuntimeModelState = useMemo(() => {
|
|
982
|
+
const diagnostics = providerDiagnosticsRef.current[activeProviderRoute.providerId];
|
|
983
|
+
const providerLabel = activeRouteProvider?.displayName
|
|
984
|
+
?? formatRuntimeProviderLabel(activeProviderRoute.providerId);
|
|
985
|
+
const diagnosticModel = readDiagnosticString(diagnostics, [
|
|
986
|
+
"selectedModel",
|
|
987
|
+
"modelId",
|
|
988
|
+
"currentModel",
|
|
989
|
+
"defaultModel",
|
|
990
|
+
]);
|
|
991
|
+
const routeModel = activeProviderRoute.modelId?.trim();
|
|
992
|
+
const modelLabel = routeModel
|
|
993
|
+
|| diagnosticModel
|
|
994
|
+
|| (activeRuntimeAvailability === "checking" || activeRuntimeAvailability === "reconnecting"
|
|
995
|
+
? "Detecting..."
|
|
996
|
+
: "Unknown");
|
|
997
|
+
const modelDisplay = activeRuntimeDisplay.footerModelDisplay?.trim()
|
|
998
|
+
|| `${providerLabel} / ${modelLabel}`;
|
|
999
|
+
const diagnosticContext = readDiagnosticString(diagnostics, ["contextDisplay", "contextLength"]);
|
|
1000
|
+
const contextDisplay = activeRuntimeDisplay.contextDisplay?.trim()
|
|
1001
|
+
|| diagnosticContext
|
|
1002
|
+
|| "Unknown";
|
|
1003
|
+
const nextState = {
|
|
1004
|
+
selectedProvider: activeProviderRoute.providerId,
|
|
1005
|
+
selectedModel: modelLabel,
|
|
1006
|
+
modelDisplay,
|
|
1007
|
+
contextDisplay,
|
|
1008
|
+
availability: activeRuntimeAvailability,
|
|
1009
|
+
};
|
|
1010
|
+
traceModelStateDebug("runtime_model_display_derived", nextState);
|
|
1011
|
+
return nextState;
|
|
1012
|
+
// registryNonce intentionally re-reads providerDiagnosticsRef.
|
|
1013
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1014
|
+
}, [
|
|
1015
|
+
activeProviderRoute.modelId,
|
|
1016
|
+
activeProviderRoute.providerId,
|
|
1017
|
+
activeRuntimeAvailability,
|
|
1018
|
+
activeRuntimeDisplay.contextDisplay,
|
|
1019
|
+
activeRuntimeDisplay.footerModelDisplay,
|
|
1020
|
+
activeRouteProvider?.displayName,
|
|
1021
|
+
registryNonce,
|
|
1022
|
+
]);
|
|
778
1023
|
|
|
779
1024
|
const hasUserPrompt = useMemo(
|
|
780
1025
|
() => staticEvents.some((e) => e.type === "user") || activeEvents.some((e) => e.type === "user"),
|
|
@@ -799,63 +1044,15 @@ export function App({ launchArgs }: AppProps) {
|
|
|
799
1044
|
const busyRef = useRef(busy);
|
|
800
1045
|
busyRef.current = busy;
|
|
801
1046
|
|
|
802
|
-
const
|
|
803
|
-
refreshTerminalTitle({
|
|
804
|
-
terminalTitleMode,
|
|
805
|
-
workspaceName: deriveTerminalTitle(workspaceRoot, "dir"),
|
|
806
|
-
force: true,
|
|
807
|
-
debugEventName,
|
|
808
|
-
busyState,
|
|
809
|
-
});
|
|
810
|
-
}, [terminalTitleMode, workspaceRoot]);
|
|
811
|
-
|
|
812
|
-
const writeCurrentTerminalTitleBeforeStateChange = useCallback((reason: string) => {
|
|
813
|
-
setIntendedTerminalTitle(deriveTerminalTitle(workspaceRoot, terminalTitleMode), {
|
|
814
|
-
force: true,
|
|
815
|
-
reason,
|
|
816
|
-
});
|
|
817
|
-
}, [terminalTitleMode, workspaceRoot]);
|
|
818
|
-
|
|
819
|
-
// Re-assert terminal title whenever the app transitions between busy states,
|
|
820
|
-
// starts/stops streaming, or executes tools to recover from external overwrites.
|
|
1047
|
+
const prevBusyRef = useRef(busy);
|
|
821
1048
|
useEffect(() => {
|
|
822
|
-
if (!busy) {
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
workspaceName: deriveTerminalTitle(workspaceRoot, "dir"),
|
|
826
|
-
force: true,
|
|
827
|
-
debugEventName: "busy-end",
|
|
828
|
-
busyState: false,
|
|
829
|
-
});
|
|
830
|
-
return;
|
|
1049
|
+
if (!busy && prevBusyRef.current && screen === "main") {
|
|
1050
|
+
intendedFocusTargetRef.current = FOCUS_IDS.composer;
|
|
1051
|
+
focusManager.focus(FOCUS_IDS.composer);
|
|
831
1052
|
}
|
|
1053
|
+
prevBusyRef.current = busy;
|
|
1054
|
+
}, [busy, screen, focusManager]);
|
|
832
1055
|
|
|
833
|
-
refreshTerminalTitle({
|
|
834
|
-
terminalTitleMode,
|
|
835
|
-
workspaceName: deriveTerminalTitle(workspaceRoot, "dir"),
|
|
836
|
-
force: true,
|
|
837
|
-
debugEventName: "busy-start",
|
|
838
|
-
busyState: true,
|
|
839
|
-
});
|
|
840
|
-
|
|
841
|
-
const timer = setInterval(() => {
|
|
842
|
-
refreshTerminalTitle({
|
|
843
|
-
terminalTitleMode,
|
|
844
|
-
workspaceName: deriveTerminalTitle(workspaceRoot, "dir"),
|
|
845
|
-
force: true,
|
|
846
|
-
debugEventName: "busy-guard",
|
|
847
|
-
busyState: true,
|
|
848
|
-
});
|
|
849
|
-
}, 250);
|
|
850
|
-
|
|
851
|
-
return () => {
|
|
852
|
-
clearInterval(timer);
|
|
853
|
-
};
|
|
854
|
-
}, [uiState.kind, busy, terminalTitleMode, workspaceRoot]);
|
|
855
|
-
|
|
856
|
-
useEffect(() => {
|
|
857
|
-
setIntendedTerminalTitle(terminalTitleLabel, { force: true, reason: "terminal-title-label-change" });
|
|
858
|
-
}, [terminalTitleLabel]);
|
|
859
1056
|
const modelCapabilitiesBusyRef = useRef(modelCapabilitiesBusy);
|
|
860
1057
|
modelCapabilitiesBusyRef.current = modelCapabilitiesBusy;
|
|
861
1058
|
const composerRows = useMemo(() => {
|
|
@@ -889,9 +1086,59 @@ export function App({ launchArgs }: AppProps) {
|
|
|
889
1086
|
terminalLayout,
|
|
890
1087
|
uiState,
|
|
891
1088
|
]);
|
|
1089
|
+
const activeRootComponent = screen === "main" ? "TranscriptShell" : "AppShell";
|
|
1090
|
+
const startupHeaderMode = useMemo(
|
|
1091
|
+
() => resolveStartupHeaderMode({
|
|
1092
|
+
cols: terminalLayout.cols,
|
|
1093
|
+
rows: terminalLayout.rows,
|
|
1094
|
+
introRows: 8,
|
|
1095
|
+
composerRows,
|
|
1096
|
+
}),
|
|
1097
|
+
[composerRows, terminalLayout.cols, terminalLayout.rows],
|
|
1098
|
+
);
|
|
1099
|
+
const previousStartupTraceKeyRef = useRef<string | null>(null);
|
|
1100
|
+
|
|
1101
|
+
useEffect(() => {
|
|
1102
|
+
const nextKey = [
|
|
1103
|
+
terminalLayout.cols,
|
|
1104
|
+
terminalLayout.rows,
|
|
1105
|
+
terminalLayout.mode,
|
|
1106
|
+
activeRootComponent,
|
|
1107
|
+
screen,
|
|
1108
|
+
startupHeaderMode,
|
|
1109
|
+
staticEvents.length,
|
|
1110
|
+
activeEvents.length,
|
|
1111
|
+
uiState.kind,
|
|
1112
|
+
].join("|");
|
|
1113
|
+
if (previousStartupTraceKeyRef.current === nextKey) return;
|
|
1114
|
+
previousStartupTraceKeyRef.current = nextKey;
|
|
1115
|
+
renderDebug.traceEvent("startup", "state", {
|
|
1116
|
+
cols: terminalLayout.cols,
|
|
1117
|
+
rows: terminalLayout.rows,
|
|
1118
|
+
layoutMode: terminalLayout.mode,
|
|
1119
|
+
activeRoot: activeRootComponent,
|
|
1120
|
+
screen,
|
|
1121
|
+
startupHeaderMode,
|
|
1122
|
+
logoBranchSelected: startupHeaderMode === "large",
|
|
1123
|
+
staticEventsLength: staticEvents.length,
|
|
1124
|
+
activeEventsLength: activeEvents.length,
|
|
1125
|
+
uiStateKind: uiState.kind,
|
|
1126
|
+
});
|
|
1127
|
+
}, [
|
|
1128
|
+
activeEvents.length,
|
|
1129
|
+
activeRootComponent,
|
|
1130
|
+
screen,
|
|
1131
|
+
startupHeaderMode,
|
|
1132
|
+
staticEvents.length,
|
|
1133
|
+
terminalLayout.cols,
|
|
1134
|
+
terminalLayout.mode,
|
|
1135
|
+
terminalLayout.rows,
|
|
1136
|
+
uiState.kind,
|
|
1137
|
+
]);
|
|
892
1138
|
|
|
893
1139
|
renderDebug.useRenderDebug("Root", {
|
|
894
1140
|
screen,
|
|
1141
|
+
activeRoot: activeRootComponent,
|
|
895
1142
|
uiStateKind: uiState.kind,
|
|
896
1143
|
staticEvents,
|
|
897
1144
|
activeEvents,
|
|
@@ -900,8 +1147,11 @@ export function App({ launchArgs }: AppProps) {
|
|
|
900
1147
|
cursor,
|
|
901
1148
|
busy,
|
|
902
1149
|
composerRows,
|
|
1150
|
+
startupHeaderMode,
|
|
1151
|
+
logoBranchSelected: startupHeaderMode === "large",
|
|
903
1152
|
cols: terminalLayout.cols,
|
|
904
1153
|
rows: terminalLayout.rows,
|
|
1154
|
+
layoutMode: terminalLayout.mode,
|
|
905
1155
|
layoutEpoch: terminalLayout.layoutEpoch,
|
|
906
1156
|
planFlowKind: planFlow.kind,
|
|
907
1157
|
mode,
|
|
@@ -1180,13 +1430,12 @@ export function App({ launchArgs }: AppProps) {
|
|
|
1180
1430
|
useEffect(() => {
|
|
1181
1431
|
const notice = providerWorkspaceConfig.migrationNotice;
|
|
1182
1432
|
if (!notice || providerMigrationNoticeShownRef.current) return;
|
|
1183
|
-
providerMigrationNoticeShownRef.current = true;
|
|
1184
1433
|
const providerLabel = findProvider(providerRegistry, notice.revertedProviderId)?.displayName ?? "OpenAI";
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
);
|
|
1189
|
-
}, [
|
|
1434
|
+
const event = createProviderMigrationNoticeEvent(notice, providerLabel);
|
|
1435
|
+
if (!event) return;
|
|
1436
|
+
providerMigrationNoticeShownRef.current = true;
|
|
1437
|
+
appendStaticEvent(event);
|
|
1438
|
+
}, [appendStaticEvent, providerRegistry, providerWorkspaceConfig.migrationNotice]);
|
|
1190
1439
|
|
|
1191
1440
|
useEffect(() => {
|
|
1192
1441
|
if (projectInstructionsLoad.status === "loaded") {
|
|
@@ -1334,6 +1583,7 @@ export function App({ launchArgs }: AppProps) {
|
|
|
1334
1583
|
useEffect(() => {
|
|
1335
1584
|
void (async () => {
|
|
1336
1585
|
try {
|
|
1586
|
+
markProviderAvailability("local", "checking", "startup-probe");
|
|
1337
1587
|
const result = await checkLocalProvider({ override: providerWorkspaceConfig.providers?.local });
|
|
1338
1588
|
if (result.diagnostics) {
|
|
1339
1589
|
providerDiagnosticsRef.current["local"] = result.diagnostics as Record<string, string | number | boolean | null>;
|
|
@@ -1353,43 +1603,44 @@ export function App({ launchArgs }: AppProps) {
|
|
|
1353
1603
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1354
1604
|
}, [workspaceRoot]);
|
|
1355
1605
|
|
|
1356
|
-
useEffect(() => {
|
|
1357
|
-
const devLaunchNotice = buildDevLaunchNotice(launchContext);
|
|
1358
|
-
if (!devLaunchNotice) return;
|
|
1359
|
-
|
|
1360
|
-
appendSystemEvent("Launch mode", devLaunchNotice);
|
|
1361
|
-
}, [appendSystemEvent, launchContext]);
|
|
1362
|
-
|
|
1363
1606
|
// Non-blocking background update check — runs once at startup.
|
|
1364
1607
|
useEffect(() => {
|
|
1365
1608
|
const ucSettings = initialSettings.current.updateCheck ?? DEFAULT_UPDATE_CHECK_SETTINGS;
|
|
1366
|
-
if (!ucSettings.enabled) return;
|
|
1609
|
+
if (!shouldRunStartupUpdateCheck(process.env, ucSettings.enabled)) return;
|
|
1367
1610
|
|
|
1368
1611
|
const timer = setTimeout(() => {
|
|
1369
1612
|
void (async () => {
|
|
1370
1613
|
try {
|
|
1371
1614
|
const cache = loadUpdateCheckCache();
|
|
1372
|
-
if (cache && isCacheValid(cache, ucSettings.intervalHours)) {
|
|
1373
|
-
if (cache.updateAvailable) {
|
|
1374
|
-
setUpdateCheckResult({
|
|
1375
|
-
status: "update-available",
|
|
1376
|
-
currentVersion: cache.currentVersion,
|
|
1377
|
-
latestVersion: cache.latestVersion,
|
|
1378
|
-
checkedAt: cache.lastChecked,
|
|
1379
|
-
});
|
|
1380
|
-
|
|
1615
|
+
if (cache && isCacheValid(cache, ucSettings.intervalHours, APP_VERSION)) {
|
|
1616
|
+
if (cache.updateAvailable && cache.latestVersion) {
|
|
1617
|
+
setUpdateCheckResult({
|
|
1618
|
+
status: "update-available",
|
|
1619
|
+
currentVersion: cache.currentVersion,
|
|
1620
|
+
latestVersion: cache.latestVersion,
|
|
1621
|
+
checkedAt: cache.lastChecked,
|
|
1622
|
+
});
|
|
1623
|
+
if (cache.latestVersion !== skippedUpdateVersionRef.current) {
|
|
1624
|
+
setScreen("update-prompt");
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1381
1627
|
return;
|
|
1382
1628
|
}
|
|
1383
|
-
const result = await checkForUpdates({ enabled: true });
|
|
1384
|
-
if (result.status !== "error") {
|
|
1385
|
-
setUpdateCheckResult(result);
|
|
1386
|
-
saveUpdateCheckCache({
|
|
1387
|
-
lastChecked: result.checkedAt,
|
|
1388
|
-
currentVersion: result.currentVersion,
|
|
1389
|
-
latestVersion: result.latestVersion,
|
|
1390
|
-
updateAvailable: result.status === "update-available",
|
|
1391
|
-
});
|
|
1392
|
-
|
|
1629
|
+
const result = await checkForUpdates({ enabled: true });
|
|
1630
|
+
if (result.status !== "error") {
|
|
1631
|
+
setUpdateCheckResult(result);
|
|
1632
|
+
saveUpdateCheckCache({
|
|
1633
|
+
lastChecked: result.checkedAt,
|
|
1634
|
+
currentVersion: result.currentVersion,
|
|
1635
|
+
latestVersion: result.latestVersion,
|
|
1636
|
+
updateAvailable: result.status === "update-available",
|
|
1637
|
+
});
|
|
1638
|
+
if (result.status === "update-available" && result.latestVersion) {
|
|
1639
|
+
if (result.latestVersion !== skippedUpdateVersionRef.current) {
|
|
1640
|
+
setScreen("update-prompt");
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1393
1644
|
} catch {
|
|
1394
1645
|
// Never crash the TUI on a failed update check.
|
|
1395
1646
|
}
|
|
@@ -1602,11 +1853,6 @@ export function App({ launchArgs }: AppProps) {
|
|
|
1602
1853
|
}
|
|
1603
1854
|
setScreen("main");
|
|
1604
1855
|
appendSystemEvent("Reasoning updated", `Reasoning level is now ${formatReasoningLabel(nextReasoningLevel)}.`);
|
|
1605
|
-
refreshTerminalTitle({
|
|
1606
|
-
terminalTitleMode,
|
|
1607
|
-
workspaceName: deriveTerminalTitle(workspaceRoot, "dir"),
|
|
1608
|
-
force: true,
|
|
1609
|
-
});
|
|
1610
1856
|
}, [
|
|
1611
1857
|
activeProviderRoute.backendKind,
|
|
1612
1858
|
activeProviderRoute.modelId,
|
|
@@ -1621,9 +1867,7 @@ export function App({ launchArgs }: AppProps) {
|
|
|
1621
1867
|
persistActiveRoute,
|
|
1622
1868
|
persistProviderDefaultModelAndReasoning,
|
|
1623
1869
|
providerWorkspaceConfig.activeRoute,
|
|
1624
|
-
terminalTitleMode,
|
|
1625
1870
|
updateRuntimeConfig,
|
|
1626
|
-
workspaceRoot,
|
|
1627
1871
|
]);
|
|
1628
1872
|
|
|
1629
1873
|
const setPlanModeWithNotice = useCallback((nextEnabled: boolean) => {
|
|
@@ -1701,11 +1945,6 @@ export function App({ launchArgs }: AppProps) {
|
|
|
1701
1945
|
"Model updated",
|
|
1702
1946
|
`Active model is now ${nextModel}. Reasoning set to ${formatReasoningLabel(normalizedReasoning)}.`,
|
|
1703
1947
|
);
|
|
1704
|
-
refreshTerminalTitle({
|
|
1705
|
-
terminalTitleMode,
|
|
1706
|
-
workspaceName: deriveTerminalTitle(workspaceRoot, "dir"),
|
|
1707
|
-
force: true,
|
|
1708
|
-
});
|
|
1709
1948
|
} catch (error) {
|
|
1710
1949
|
const message = error instanceof Error ? error.message : String(error);
|
|
1711
1950
|
traceInputDebug("model_selection_app_failure", getInputDebugSnapshot({
|
|
@@ -1762,11 +2001,11 @@ export function App({ launchArgs }: AppProps) {
|
|
|
1762
2001
|
|
|
1763
2002
|
try {
|
|
1764
2003
|
const normalizedReasoning = normalizeReasoningForModelCapabilities(nextModel, nextReasoning, routeCapabilities);
|
|
1765
|
-
const releaseTitleGuard = acquireTerminalTitleGuard(500, () => {
|
|
1766
|
-
forceRefreshCurrentTerminalTitle("provider_validation_active", true);
|
|
1767
|
-
});
|
|
1768
2004
|
let validation;
|
|
1769
2005
|
try {
|
|
2006
|
+
if (providerId === "local") {
|
|
2007
|
+
markProviderAvailability("local", "checking", "provider-validation");
|
|
2008
|
+
}
|
|
1770
2009
|
validation = await validateProviderRouteActivation({
|
|
1771
2010
|
route: {
|
|
1772
2011
|
providerId,
|
|
@@ -1785,8 +2024,7 @@ export function App({ launchArgs }: AppProps) {
|
|
|
1785
2024
|
}
|
|
1786
2025
|
setRegistryNonce((n) => n + 1);
|
|
1787
2026
|
} finally {
|
|
1788
|
-
|
|
1789
|
-
forceRefreshCurrentTerminalTitle("after_provider_validation", false);
|
|
2027
|
+
// Runtime status is rendered from provider diagnostics; no title guard is active here.
|
|
1790
2028
|
}
|
|
1791
2029
|
if (validation.status !== "ready") {
|
|
1792
2030
|
traceInputDebug("model_selection_app_failure", getInputDebugSnapshot({
|
|
@@ -1833,11 +2071,6 @@ export function App({ launchArgs }: AppProps) {
|
|
|
1833
2071
|
}));
|
|
1834
2072
|
|
|
1835
2073
|
// Route changes are reflected reactively in the BottomComposer metadata row.
|
|
1836
|
-
refreshTerminalTitle({
|
|
1837
|
-
terminalTitleMode,
|
|
1838
|
-
workspaceName: deriveTerminalTitle(workspaceRoot, "dir"),
|
|
1839
|
-
force: true,
|
|
1840
|
-
});
|
|
1841
2074
|
} catch (error) {
|
|
1842
2075
|
const message = error instanceof Error ? error.message : String(error);
|
|
1843
2076
|
traceInputDebug("model_selection_app_failure", getInputDebugSnapshot({
|
|
@@ -1847,12 +2080,11 @@ export function App({ launchArgs }: AppProps) {
|
|
|
1847
2080
|
error: message,
|
|
1848
2081
|
}));
|
|
1849
2082
|
appendErrorEvent("Model selection failed", message);
|
|
1850
|
-
forceRefreshCurrentTerminalTitle("model_selection_failed", false);
|
|
1851
2083
|
} finally {
|
|
1852
2084
|
modelSelectionInFlightRef.current = false;
|
|
1853
2085
|
returnToChatMode("selection");
|
|
1854
2086
|
}
|
|
1855
|
-
}, [activeProviderRoute.modelId, activeProviderRoute.providerId, activeRouteProvider, appendErrorEvent, appendSystemEvent, busy, getInputDebugSnapshot, modelCapabilities, persistActiveRoute, providerWorkspaceConfig.providers, returnToChatMode, runtimeConfig.geminiCommandPath, updateRuntimeConfig, workspaceRoot]);
|
|
2087
|
+
}, [activeProviderRoute.modelId, activeProviderRoute.providerId, activeRouteProvider, appendErrorEvent, appendSystemEvent, busy, getInputDebugSnapshot, markProviderAvailability, modelCapabilities, persistActiveRoute, providerWorkspaceConfig.providers, returnToChatMode, runtimeConfig.geminiCommandPath, updateRuntimeConfig, workspaceRoot]);
|
|
1856
2088
|
|
|
1857
2089
|
const setAuthPreferenceWithNotice = useCallback((nextPreference: AuthPreference) => {
|
|
1858
2090
|
setAuthPreference(nextPreference);
|
|
@@ -1881,11 +2113,6 @@ export function App({ launchArgs }: AppProps) {
|
|
|
1881
2113
|
}
|
|
1882
2114
|
if (nextSettings.terminalTitleMode !== terminalTitleMode) {
|
|
1883
2115
|
setTerminalTitleMode(nextSettings.terminalTitleMode);
|
|
1884
|
-
refreshTerminalTitle({
|
|
1885
|
-
terminalTitleMode: nextSettings.terminalTitleMode,
|
|
1886
|
-
workspaceName: deriveTerminalTitle(workspaceRoot, "dir"),
|
|
1887
|
-
force: true,
|
|
1888
|
-
});
|
|
1889
2116
|
}
|
|
1890
2117
|
const nextShowBusyLoader = parseBusyLoaderSettingValue(nextSettings.showBusyLoader);
|
|
1891
2118
|
if (nextShowBusyLoader !== showBusyLoader) {
|
|
@@ -1898,6 +2125,33 @@ export function App({ launchArgs }: AppProps) {
|
|
|
1898
2125
|
setScreen("main");
|
|
1899
2126
|
}, [applyWorkspaceDisplayMode, showBusyLoader, terminalMouseMode, terminalTitleMode, workspaceDisplayMode]);
|
|
1900
2127
|
|
|
2128
|
+
const handleSkipUpdateForSession = useCallback(() => {
|
|
2129
|
+
setScreen("main");
|
|
2130
|
+
}, []);
|
|
2131
|
+
|
|
2132
|
+
const handleSkipUpdateVersion = useCallback((version: string) => {
|
|
2133
|
+
initialSettings.current.updateCheck = {
|
|
2134
|
+
...initialSettings.current.updateCheck,
|
|
2135
|
+
skippedUpdateVersion: version,
|
|
2136
|
+
};
|
|
2137
|
+
saveSettings({
|
|
2138
|
+
ui: {
|
|
2139
|
+
layoutStyle: initialSettings.current.ui.layoutStyle,
|
|
2140
|
+
theme: themeSelection.committedTheme,
|
|
2141
|
+
workspaceDisplayMode,
|
|
2142
|
+
terminalTitleMode,
|
|
2143
|
+
showBusyLoader,
|
|
2144
|
+
terminalMouseMode,
|
|
2145
|
+
customTheme,
|
|
2146
|
+
},
|
|
2147
|
+
auth: { preference: authPreference },
|
|
2148
|
+
header: headerConfig,
|
|
2149
|
+
updateCheck: initialSettings.current.updateCheck,
|
|
2150
|
+
});
|
|
2151
|
+
skippedUpdateVersionRef.current = version;
|
|
2152
|
+
setScreen("main");
|
|
2153
|
+
}, [authPreference, customTheme, headerConfig, showBusyLoader, terminalMouseMode, terminalTitleMode, themeSelection.committedTheme, workspaceDisplayMode]);
|
|
2154
|
+
|
|
1901
2155
|
const setApprovalPolicyWithNotice = useCallback((nextValue: RuntimeApprovalPolicy) => {
|
|
1902
2156
|
const gate = guardConfigMutation("mode", busy);
|
|
1903
2157
|
if (!gate.allowed) {
|
|
@@ -2023,6 +2277,7 @@ export function App({ launchArgs }: AppProps) {
|
|
|
2023
2277
|
}
|
|
2024
2278
|
|
|
2025
2279
|
setScreen("provider-picker");
|
|
2280
|
+
markProviderAvailability("local", "checking", "provider-picker-open");
|
|
2026
2281
|
void checkLocalProvider({ override: providerWorkspaceConfig.providers?.local }).then((result) => {
|
|
2027
2282
|
if (!isMountedRef.current) return;
|
|
2028
2283
|
if (result.diagnostics) {
|
|
@@ -2035,7 +2290,7 @@ export function App({ launchArgs }: AppProps) {
|
|
|
2035
2290
|
}
|
|
2036
2291
|
setRegistryNonce((n) => n + 1);
|
|
2037
2292
|
}).catch(() => undefined);
|
|
2038
|
-
}, [appendSystemEvent, busy, providerWorkspaceConfig.providers]);
|
|
2293
|
+
}, [appendSystemEvent, busy, markProviderAvailability, providerWorkspaceConfig.providers]);
|
|
2039
2294
|
|
|
2040
2295
|
const setWorkspaceDefaultProviderWithNotice = useCallback((providerId: ProviderId) => {
|
|
2041
2296
|
const provider = findProvider(providerRegistry, providerId);
|
|
@@ -2094,6 +2349,7 @@ export function App({ launchArgs }: AppProps) {
|
|
|
2094
2349
|
|
|
2095
2350
|
if (providerId === "local") {
|
|
2096
2351
|
appendSystemEvent("Model discovery", "Refreshing LM Studio metadata...");
|
|
2352
|
+
markProviderAvailability("local", "checking", "use-in-codexa");
|
|
2097
2353
|
void checkLocalProvider({ override: providerWorkspaceConfig.providers?.local }).then((validation) => {
|
|
2098
2354
|
if (!isMountedRef.current) return;
|
|
2099
2355
|
if (validation.diagnostics) {
|
|
@@ -2203,6 +2459,9 @@ export function App({ launchArgs }: AppProps) {
|
|
|
2203
2459
|
appendSystemEvent("Model discovery", providerId === "anthropic"
|
|
2204
2460
|
? "Refreshing Claude capabilities..."
|
|
2205
2461
|
: `Refreshing models for ${provider.displayName}...`);
|
|
2462
|
+
if (providerId === "local") {
|
|
2463
|
+
markProviderAvailability("local", "checking", "refresh-models");
|
|
2464
|
+
}
|
|
2206
2465
|
void runtime.refreshModels({
|
|
2207
2466
|
cwd: workspaceRoot,
|
|
2208
2467
|
localConfig: providerId === "local" ? providerWorkspaceConfig.providers?.local : undefined,
|
|
@@ -2309,8 +2568,7 @@ export function App({ launchArgs }: AppProps) {
|
|
|
2309
2568
|
stdout.write("\n");
|
|
2310
2569
|
},
|
|
2311
2570
|
afterLaunch: () => {
|
|
2312
|
-
terminalControl.setMouseReporting(
|
|
2313
|
-
forceRefreshCurrentTerminalTitle("provider_launch_return", false);
|
|
2571
|
+
terminalControl.setMouseReporting(effectiveMouseCapture, "src/app.tsx:providerLaunch.restoreMouse");
|
|
2314
2572
|
},
|
|
2315
2573
|
}).then((result) => {
|
|
2316
2574
|
if (!isMountedRef.current) return;
|
|
@@ -2324,10 +2582,10 @@ export function App({ launchArgs }: AppProps) {
|
|
|
2324
2582
|
activeProviderRoute,
|
|
2325
2583
|
appendErrorEvent,
|
|
2326
2584
|
appendSystemEvent,
|
|
2327
|
-
|
|
2328
|
-
mouseCapture,
|
|
2585
|
+
effectiveMouseCapture,
|
|
2329
2586
|
providerRegistry,
|
|
2330
2587
|
providerWorkspaceConfig.providers,
|
|
2588
|
+
markProviderAvailability,
|
|
2331
2589
|
modelCapabilities,
|
|
2332
2590
|
reasoningLevel,
|
|
2333
2591
|
refreshModelCapabilities,
|
|
@@ -2529,6 +2787,18 @@ export function App({ launchArgs }: AppProps) {
|
|
|
2529
2787
|
dispatchSession({ type: "RESET_INPUT" });
|
|
2530
2788
|
}, [dispatchSession]);
|
|
2531
2789
|
|
|
2790
|
+
const resetToHomeScreen = useCallback((seedEvents: TimelineEvent[] = []) => {
|
|
2791
|
+
dispatchSession({
|
|
2792
|
+
type: "CLEAR_TRANSCRIPT",
|
|
2793
|
+
seedEvents,
|
|
2794
|
+
});
|
|
2795
|
+
setConversationChars(0);
|
|
2796
|
+
setScreen("main");
|
|
2797
|
+
resetComposer();
|
|
2798
|
+
intendedFocusTargetRef.current = FOCUS_IDS.composer;
|
|
2799
|
+
focusManager.focus(FOCUS_IDS.composer);
|
|
2800
|
+
}, [dispatchSession, focusManager, resetComposer]);
|
|
2801
|
+
|
|
2532
2802
|
const finalizePromptRun = useCallback((
|
|
2533
2803
|
runId: number,
|
|
2534
2804
|
turnId: number,
|
|
@@ -2840,23 +3110,36 @@ export function App({ launchArgs }: AppProps) {
|
|
|
2840
3110
|
}, [dispatchSession]);
|
|
2841
3111
|
|
|
2842
3112
|
const handleClear = useCallback(() => {
|
|
2843
|
-
|
|
3113
|
+
const clearGeneration = sessionState.clearEpoch + 1;
|
|
3114
|
+
const clearBoundaryArmed = clearFrameBoundaryController?.beginClearGeneration(clearGeneration) ?? false;
|
|
3115
|
+
renderDebug.traceEvent("terminal", "clearCommandReceived", {
|
|
3116
|
+
clearGeneration,
|
|
3117
|
+
clearPending: clearBoundaryArmed,
|
|
3118
|
+
liveInkInstanceResolved: Boolean(inkInstance),
|
|
3119
|
+
});
|
|
2844
3120
|
cancelActiveRun(false);
|
|
2845
3121
|
activeTurnIdRef.current = null;
|
|
2846
3122
|
activeRunLifecycleRef.current = null;
|
|
2847
3123
|
activeRunTimingRef.current = null;
|
|
2848
3124
|
setPlanFlow(resetPlanFlow());
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
resetComposer();
|
|
2853
|
-
terminalControl.clearTranscript("src/app.tsx:handleClear");
|
|
2854
|
-
refreshTerminalTitle({
|
|
2855
|
-
terminalTitleMode,
|
|
2856
|
-
workspaceName: deriveTerminalTitle(workspaceRoot, "dir"),
|
|
2857
|
-
force: true,
|
|
3125
|
+
renderDebug.traceEvent("terminal", "clearReactStateRequested", {
|
|
3126
|
+
clearGeneration,
|
|
3127
|
+
clearPending: clearBoundaryArmed,
|
|
2858
3128
|
});
|
|
2859
|
-
|
|
3129
|
+
resetToHomeScreen(createStartupStaticEvents({
|
|
3130
|
+
launchContext,
|
|
3131
|
+
providerWorkspaceConfig,
|
|
3132
|
+
}));
|
|
3133
|
+
if (!clearBoundaryArmed) {
|
|
3134
|
+
// Fallback path (unexpected Ink mismatch): preserve /clear semantics.
|
|
3135
|
+
terminalControl.clearTranscript("src/app.tsx:handleClear:fallback");
|
|
3136
|
+
resetInkOutputForFreshFrame({ instance: inkInstance, columns: stdout.columns });
|
|
3137
|
+
renderDebug.traceEvent("terminal", "clearBoundaryFallback", {
|
|
3138
|
+
clearGeneration,
|
|
3139
|
+
liveInkInstanceResolved: Boolean(inkInstance),
|
|
3140
|
+
});
|
|
3141
|
+
}
|
|
3142
|
+
}, [cancelActiveRun, clearFrameBoundaryController, inkInstance, launchContext, providerWorkspaceConfig, resetToHomeScreen, sessionState.clearEpoch, stdout.columns, terminalControl]);
|
|
2860
3143
|
|
|
2861
3144
|
const handleShellExecute = useCallback((command: string) => {
|
|
2862
3145
|
const safeCommand = sanitizeTerminalInput(command).trim();
|
|
@@ -2868,7 +3151,6 @@ export function App({ launchArgs }: AppProps) {
|
|
|
2868
3151
|
|
|
2869
3152
|
const shellId = createEventId();
|
|
2870
3153
|
const startTime = Date.now();
|
|
2871
|
-
writeCurrentTerminalTitleBeforeStateChange("before-shell-start");
|
|
2872
3154
|
|
|
2873
3155
|
const initialEvent: ShellEvent = {
|
|
2874
3156
|
id: shellId,
|
|
@@ -2931,9 +3213,6 @@ export function App({ launchArgs }: AppProps) {
|
|
|
2931
3213
|
safeCommand,
|
|
2932
3214
|
{ cwd: workspaceRoot },
|
|
2933
3215
|
{
|
|
2934
|
-
onProcessLifecycle: (event) => {
|
|
2935
|
-
reassertIntendedTerminalTitle({ reason: `shell-process-${event}` });
|
|
2936
|
-
},
|
|
2937
3216
|
onStdout: (text) => {
|
|
2938
3217
|
const lines = sanitizeTerminalLines(text.split(/\r?\n/));
|
|
2939
3218
|
if (lines.length > 0) {
|
|
@@ -2977,9 +3256,8 @@ export function App({ launchArgs }: AppProps) {
|
|
|
2977
3256
|
};
|
|
2978
3257
|
|
|
2979
3258
|
dispatchSession({ type: "FINALIZE_SHELL", shellId, finalEvent });
|
|
2980
|
-
forceRefreshCurrentTerminalTitle("shell_output_complete", false);
|
|
2981
3259
|
});
|
|
2982
|
-
}, [allowedWritableRoots, appendErrorEvent, dispatchSession, focusManager,
|
|
3260
|
+
}, [allowedWritableRoots, appendErrorEvent, dispatchSession, focusManager, workspaceRoot]);
|
|
2983
3261
|
|
|
2984
3262
|
const handleWorkspaceRelaunch = useCallback((targetPath: string) => {
|
|
2985
3263
|
const gate = guardWorkspaceRelaunch(busy);
|
|
@@ -3112,7 +3390,6 @@ export function App({ launchArgs }: AppProps) {
|
|
|
3112
3390
|
setConversationChars((count) => count + safeProviderPrompt.length);
|
|
3113
3391
|
|
|
3114
3392
|
const runId = createEventId();
|
|
3115
|
-
writeCurrentTerminalTitleBeforeStateChange("before-prompt-run-start");
|
|
3116
3393
|
perf.startSession(String(runId));
|
|
3117
3394
|
perf.mark("dispatch_start");
|
|
3118
3395
|
perf.setMeta("fast_cleanup", fastCleanupRun);
|
|
@@ -3239,9 +3516,6 @@ export function App({ launchArgs }: AppProps) {
|
|
|
3239
3516
|
safeProviderPrompt,
|
|
3240
3517
|
{ runtime: runtimeForTurn, workspaceRoot, projectInstructions },
|
|
3241
3518
|
{
|
|
3242
|
-
onProcessLifecycle: (event) => {
|
|
3243
|
-
reassertIntendedTerminalTitle({ reason: `codex-process-${event}` });
|
|
3244
|
-
},
|
|
3245
3519
|
onAssistantDelta: (chunk) => {
|
|
3246
3520
|
const geminiBoundary = activeProviderRoute.providerId === "google";
|
|
3247
3521
|
appDiagLog(`onAssistantDelta: provider=${activeProviderRoute.providerId} chunk.length=${chunk?.length ?? 0} isEmpty=${!chunk}`);
|
|
@@ -3304,7 +3578,6 @@ export function App({ launchArgs }: AppProps) {
|
|
|
3304
3578
|
if (activity.status === "running") {
|
|
3305
3579
|
return;
|
|
3306
3580
|
}
|
|
3307
|
-
forceRefreshCurrentTerminalTitle("tool_activity_complete", true);
|
|
3308
3581
|
if (fastCleanupRun && !blockedCleanupFailureSurfaced) {
|
|
3309
3582
|
const blockedCleanupFailure = getBlockedCleanupFailure(activity);
|
|
3310
3583
|
if (blockedCleanupFailure) {
|
|
@@ -3364,7 +3637,6 @@ export function App({ launchArgs }: AppProps) {
|
|
|
3364
3637
|
const formatted = formatHollowResponse(hollow, safeResponse);
|
|
3365
3638
|
traceLiveRunDiagnostics("completed");
|
|
3366
3639
|
void finalizePromptRun(runId, turnId, "completed", undefined, formatted);
|
|
3367
|
-
forceRefreshCurrentTerminalTitle("prompt_run_completed", false);
|
|
3368
3640
|
return;
|
|
3369
3641
|
}
|
|
3370
3642
|
}
|
|
@@ -3402,7 +3674,6 @@ export function App({ launchArgs }: AppProps) {
|
|
|
3402
3674
|
}
|
|
3403
3675
|
traceLiveRunDiagnostics("completed");
|
|
3404
3676
|
void finalizePromptRun(runId, turnId, "completed", undefined, finalResponse);
|
|
3405
|
-
forceRefreshCurrentTerminalTitle("prompt_run_completed", false);
|
|
3406
3677
|
};
|
|
3407
3678
|
|
|
3408
3679
|
if (flushedLiveUpdates) {
|
|
@@ -3435,7 +3706,6 @@ export function App({ launchArgs }: AppProps) {
|
|
|
3435
3706
|
|
|
3436
3707
|
traceLiveRunDiagnostics("failed");
|
|
3437
3708
|
void finalizePromptRun(runId, turnId, "failed", errorMessage);
|
|
3438
|
-
forceRefreshCurrentTerminalTitle("prompt_run_failed", false);
|
|
3439
3709
|
};
|
|
3440
3710
|
|
|
3441
3711
|
if (flushedLiveUpdates) {
|
|
@@ -3491,8 +3761,6 @@ export function App({ launchArgs }: AppProps) {
|
|
|
3491
3761
|
appendSystemEvent,
|
|
3492
3762
|
authStatus.state,
|
|
3493
3763
|
finalizePromptRun,
|
|
3494
|
-
forceRefreshCurrentTerminalTitle,
|
|
3495
|
-
writeCurrentTerminalTitleBeforeStateChange,
|
|
3496
3764
|
mode,
|
|
3497
3765
|
provider,
|
|
3498
3766
|
projectInstructions,
|
|
@@ -3506,15 +3774,21 @@ export function App({ launchArgs }: AppProps) {
|
|
|
3506
3774
|
if (!pendingImport) return;
|
|
3507
3775
|
const replacements: Array<{ rawPath: string; workspaceRelativePath: string }> = [];
|
|
3508
3776
|
for (const file of pendingImport.files) {
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3777
|
+
try {
|
|
3778
|
+
const destPath = await importExternalFile(file.srcPath, pendingImport.attachmentsDir);
|
|
3779
|
+
if (destPath) {
|
|
3780
|
+
const relPath = path.relative(workspaceRoot, destPath).replace(/\\/g, "/");
|
|
3781
|
+
replacements.push({ rawPath: file.rawPath, workspaceRelativePath: relPath });
|
|
3782
|
+
}
|
|
3783
|
+
} catch (err: any) {
|
|
3784
|
+
appendErrorEvent("Import failed", `Could not import ${path.basename(file.srcPath)}: ${err.message}`);
|
|
3785
|
+
}
|
|
3512
3786
|
}
|
|
3513
3787
|
const rewrittenPrompt = rewritePromptWithImportedPaths(pendingImport.prompt, replacements);
|
|
3514
3788
|
setPendingImport(null);
|
|
3515
3789
|
setScreen("main");
|
|
3516
3790
|
startPromptRun(rewrittenPrompt, rewrittenPrompt, { submitTiming: createPromptRunTiming(), commitPrompt: true });
|
|
3517
|
-
}, [pendingImport, workspaceRoot, startPromptRun]);
|
|
3791
|
+
}, [pendingImport, workspaceRoot, startPromptRun, appendErrorEvent]);
|
|
3518
3792
|
|
|
3519
3793
|
const handleImportCancel = useCallback(() => {
|
|
3520
3794
|
if (!pendingImport) return;
|
|
@@ -3900,8 +4174,13 @@ export function App({ launchArgs }: AppProps) {
|
|
|
3900
4174
|
case "diagnose_providers": {
|
|
3901
4175
|
const lines: string[] = ["Provider CLI diagnostics:"];
|
|
3902
4176
|
const diags = providerDiagnosticsRef.current;
|
|
3903
|
-
const providerIds = ["openai", "anthropic", "
|
|
3904
|
-
const labels: Record<string, string> = {
|
|
4177
|
+
const providerIds = ["openai", "anthropic", "local", "antigravity"] as const;
|
|
4178
|
+
const labels: Record<string, string> = {
|
|
4179
|
+
openai: "OpenAI/Codex",
|
|
4180
|
+
anthropic: "Anthropic/Claude",
|
|
4181
|
+
local: "Local OpenAI-compatible",
|
|
4182
|
+
antigravity: "Antigravity CLI",
|
|
4183
|
+
};
|
|
3905
4184
|
for (const id of providerIds) {
|
|
3906
4185
|
const diag = diags[id];
|
|
3907
4186
|
lines.push(`\n ${labels[id] ?? id}:`);
|
|
@@ -4008,8 +4287,8 @@ export function App({ launchArgs }: AppProps) {
|
|
|
4008
4287
|
appendSystemEvent(
|
|
4009
4288
|
"Mouse mode updated",
|
|
4010
4289
|
nextMouse
|
|
4011
|
-
? "
|
|
4012
|
-
: "
|
|
4290
|
+
? "Mouse preference set to wheel mode. Main chat still uses native terminal scrollback; SGR capture is not used for transcript scrolling."
|
|
4291
|
+
: "Mouse preference set to selection mode. Main chat uses native terminal scrollback and native drag-select.",
|
|
4013
4292
|
);
|
|
4014
4293
|
return;
|
|
4015
4294
|
}
|
|
@@ -4051,6 +4330,10 @@ export function App({ launchArgs }: AppProps) {
|
|
|
4051
4330
|
return;
|
|
4052
4331
|
case "update": {
|
|
4053
4332
|
const arg = commandResult.value ?? "status";
|
|
4333
|
+
if (isLocalDevChannel() && arg !== "check") {
|
|
4334
|
+
appendSystemEvent("Update", formatLocalDevUpdateStatus());
|
|
4335
|
+
return;
|
|
4336
|
+
}
|
|
4054
4337
|
void (async () => {
|
|
4055
4338
|
let freshResult = updateCheckResult;
|
|
4056
4339
|
if (arg === "check" || freshResult === null) {
|
|
@@ -4058,18 +4341,22 @@ export function App({ launchArgs }: AppProps) {
|
|
|
4058
4341
|
freshResult = await checkForUpdates({ enabled: true });
|
|
4059
4342
|
setUpdateCheckResult(freshResult);
|
|
4060
4343
|
if (freshResult.status !== "error") {
|
|
4061
|
-
saveUpdateCheckCache({
|
|
4062
|
-
lastChecked: freshResult.checkedAt,
|
|
4063
|
-
currentVersion: freshResult.currentVersion,
|
|
4064
|
-
latestVersion: freshResult.latestVersion,
|
|
4065
|
-
updateAvailable: freshResult.status === "update-available",
|
|
4066
|
-
});
|
|
4067
|
-
}
|
|
4344
|
+
saveUpdateCheckCache({
|
|
4345
|
+
lastChecked: freshResult.checkedAt,
|
|
4346
|
+
currentVersion: freshResult.currentVersion,
|
|
4347
|
+
latestVersion: freshResult.latestVersion,
|
|
4348
|
+
updateAvailable: freshResult.status === "update-available",
|
|
4349
|
+
});
|
|
4350
|
+
}
|
|
4068
4351
|
} catch {
|
|
4069
4352
|
freshResult = null;
|
|
4070
4353
|
}
|
|
4071
4354
|
}
|
|
4072
|
-
|
|
4355
|
+
if (freshResult?.status === "update-available" && freshResult.latestVersion) {
|
|
4356
|
+
setScreen("update-prompt");
|
|
4357
|
+
} else {
|
|
4358
|
+
appendSystemEvent("Update", formatUpdateInstructions(freshResult));
|
|
4359
|
+
}
|
|
4073
4360
|
})();
|
|
4074
4361
|
return;
|
|
4075
4362
|
}
|
|
@@ -4112,7 +4399,14 @@ export function App({ launchArgs }: AppProps) {
|
|
|
4112
4399
|
}
|
|
4113
4400
|
|
|
4114
4401
|
// Validate workspace access for normal prompts
|
|
4115
|
-
const outsideViolations = findOutsideWorkspacePaths(value, workspaceRoot, allowedWritableRoots);
|
|
4402
|
+
const { violations: outsideViolations, skippedExternalPaths } = findOutsideWorkspacePaths(value, workspaceRoot, allowedWritableRoots);
|
|
4403
|
+
|
|
4404
|
+
if (skippedExternalPaths.length > 0) {
|
|
4405
|
+
for (const skipped of skippedExternalPaths) {
|
|
4406
|
+
appendSystemEvent("Dependency skipped", `Skipped external dependency source: ${formatSkippedDependencyPath(skipped)}`);
|
|
4407
|
+
}
|
|
4408
|
+
}
|
|
4409
|
+
|
|
4116
4410
|
if (outsideViolations.length > 0) {
|
|
4117
4411
|
if (runtimeConfig.policy.allowExternalFileImport) {
|
|
4118
4412
|
const attachmentsDir = path.isAbsolute(runtimeConfig.policy.attachmentDir)
|
|
@@ -4202,51 +4496,12 @@ export function App({ launchArgs }: AppProps) {
|
|
|
4202
4496
|
workspaceRoot,
|
|
4203
4497
|
]);
|
|
4204
4498
|
|
|
4205
|
-
const modelDisplayName =
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
if (activeProviderRoute.modelSelection.kind === "auto") {
|
|
4212
|
-
return `auto ${activeProviderRoute.modelSelection.family === "gemini-3" ? "Gemini 3" : "Gemini 2.5"}`;
|
|
4213
|
-
}
|
|
4214
|
-
}
|
|
4215
|
-
if (activeProviderRoute.providerId === "local") {
|
|
4216
|
-
return activeProviderRoute.modelId;
|
|
4217
|
-
}
|
|
4218
|
-
return model;
|
|
4219
|
-
}, [
|
|
4220
|
-
activeProviderRoute.modelId,
|
|
4221
|
-
activeProviderRoute.modelSelection,
|
|
4222
|
-
activeProviderRoute.providerId,
|
|
4223
|
-
activeProviderRoute.reasoning,
|
|
4224
|
-
currentModelCapability?.label,
|
|
4225
|
-
model,
|
|
4226
|
-
reasoningLevel,
|
|
4227
|
-
]);
|
|
4228
|
-
const composerReasoningLevel = activeProviderRoute.providerId === "anthropic"
|
|
4229
|
-
? ""
|
|
4230
|
-
: activeProviderRoute.reasoning ?? reasoningLevel;
|
|
4231
|
-
const headerRuntimeSummary = useMemo(() => {
|
|
4232
|
-
const contextLabel = activeContextMetadata?.contextLength != null
|
|
4233
|
-
? `${formatContextCompact(estimateTokens(conversationChars))} / ${activeContextMetadata.confidence === "estimated" ? "~" : ""}${formatContextCompact(activeContextMetadata.contextLength)}`
|
|
4234
|
-
: "Unknown";
|
|
4235
|
-
|
|
4236
|
-
return {
|
|
4237
|
-
...runtimeSummary,
|
|
4238
|
-
providerLabel: activeRouteProvider?.displayName ?? runtimeSummary.providerLabel,
|
|
4239
|
-
modelLabel: modelDisplayName,
|
|
4240
|
-
contextLabel,
|
|
4241
|
-
};
|
|
4242
|
-
}, [
|
|
4243
|
-
activeContextMetadata?.confidence,
|
|
4244
|
-
activeContextMetadata?.contextLength,
|
|
4245
|
-
activeRouteProvider?.displayName,
|
|
4246
|
-
conversationChars,
|
|
4247
|
-
modelDisplayName,
|
|
4248
|
-
runtimeSummary,
|
|
4249
|
-
]);
|
|
4499
|
+
const modelDisplayName = activeRuntimeDisplay.modelDisplay;
|
|
4500
|
+
const composerReasoningLevel = "";
|
|
4501
|
+
const headerRuntimeSummary = useMemo(
|
|
4502
|
+
() => runtimeDisplayToSummary(activeRuntimeDisplay, runtimeSummary),
|
|
4503
|
+
[activeRuntimeDisplay, runtimeSummary],
|
|
4504
|
+
);
|
|
4250
4505
|
const effectiveHeaderConfig = useMemo<HeaderConfig>(() => ({
|
|
4251
4506
|
...headerConfig,
|
|
4252
4507
|
showProvider: true,
|
|
@@ -4263,7 +4518,7 @@ export function App({ launchArgs }: AppProps) {
|
|
|
4263
4518
|
if (planFlow.kind === "awaiting_action") {
|
|
4264
4519
|
if (!hasVisibleTranscriptPlan) {
|
|
4265
4520
|
return (
|
|
4266
|
-
<Text color={activeTheme.
|
|
4521
|
+
<Text color={activeTheme.textMuted}>
|
|
4267
4522
|
Plan could not be displayed. Please ask Codexa to regenerate the plan.
|
|
4268
4523
|
</Text>
|
|
4269
4524
|
);
|
|
@@ -4286,7 +4541,7 @@ export function App({ launchArgs }: AppProps) {
|
|
|
4286
4541
|
placeholder={planFlow.mode === "revise"
|
|
4287
4542
|
? "e.g. keep it to one file and add tests"
|
|
4288
4543
|
: "e.g. keep it minimal and avoid touching other files"}
|
|
4289
|
-
footerHint="Enter
|
|
4544
|
+
footerHint="Esc to close · Enter to confirm"
|
|
4290
4545
|
initialValue={initialRevisionText}
|
|
4291
4546
|
onSubmit={(value) => {
|
|
4292
4547
|
setInitialRevisionText("");
|
|
@@ -4303,8 +4558,10 @@ export function App({ launchArgs }: AppProps) {
|
|
|
4303
4558
|
uiState={uiState}
|
|
4304
4559
|
mode={mode}
|
|
4305
4560
|
model={modelDisplayName}
|
|
4561
|
+
footerModelDisplay={activeRuntimeDisplay.footerModelDisplay}
|
|
4306
4562
|
themeName={activeThemeName}
|
|
4307
4563
|
reasoningLevel={composerReasoningLevel}
|
|
4564
|
+
contextDisplay={activeRuntimeDisplay.contextDisplay}
|
|
4308
4565
|
planMode={planMode}
|
|
4309
4566
|
showBusyLoader={showBusyLoader}
|
|
4310
4567
|
tokensUsed={estimateTokens(conversationChars)}
|
|
@@ -4339,12 +4596,14 @@ export function App({ launchArgs }: AppProps) {
|
|
|
4339
4596
|
handleCancel,
|
|
4340
4597
|
handlePlanFeedbackSubmit,
|
|
4341
4598
|
hasVisibleTranscriptPlan,
|
|
4342
|
-
activeTheme.
|
|
4599
|
+
activeTheme.textMuted,
|
|
4343
4600
|
composerInstanceKey,
|
|
4344
4601
|
terminalLayout,
|
|
4345
4602
|
uiState,
|
|
4346
4603
|
mode,
|
|
4347
4604
|
modelDisplayName,
|
|
4605
|
+
activeRuntimeDisplay.footerModelDisplay,
|
|
4606
|
+
activeRuntimeDisplay.contextDisplay,
|
|
4348
4607
|
activeThemeName,
|
|
4349
4608
|
composerReasoningLevel,
|
|
4350
4609
|
planMode,
|
|
@@ -4370,18 +4629,15 @@ export function App({ launchArgs }: AppProps) {
|
|
|
4370
4629
|
cycleModeWithNotice,
|
|
4371
4630
|
handleQuit,
|
|
4372
4631
|
activeProviderRoute.providerId,
|
|
4632
|
+
sessionState.externalCliStatus,
|
|
4373
4633
|
]);
|
|
4374
4634
|
|
|
4375
4635
|
// ─── Render ──────────────────────────────────────────────────────────────────
|
|
4376
4636
|
|
|
4377
|
-
// Plan review is shown inline in the Timeline, not as a separate overlay.
|
|
4378
|
-
const mainPanelElement = null;
|
|
4379
|
-
|
|
4380
4637
|
return (
|
|
4381
4638
|
<ThemeProvider theme={activeThemeName} customTheme={customTheme}>
|
|
4382
|
-
<
|
|
4639
|
+
<TranscriptShell
|
|
4383
4640
|
layout={terminalLayout}
|
|
4384
|
-
screen={screen}
|
|
4385
4641
|
authState={authStatus.state}
|
|
4386
4642
|
workspaceLabel={workspaceLabel}
|
|
4387
4643
|
workspaceRoot={workspaceRoot}
|
|
@@ -4390,21 +4646,44 @@ export function App({ launchArgs }: AppProps) {
|
|
|
4390
4646
|
activeEvents={activeEvents}
|
|
4391
4647
|
uiState={uiState}
|
|
4392
4648
|
verboseMode={verboseMode}
|
|
4393
|
-
mouseCapture={mouseCapture}
|
|
4394
|
-
onMouseActivity={resetMouseIdle}
|
|
4395
|
-
selectionProfile={selectionProfile}
|
|
4396
4649
|
clearCount={sessionState.clearCount}
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4650
|
+
repaintGeneration={staticRepaintGeneration}
|
|
4651
|
+
composer={composerElement}
|
|
4652
|
+
composerRows={composerRows}
|
|
4653
|
+
visible={screen === "main"}
|
|
4654
|
+
/>
|
|
4655
|
+
|
|
4656
|
+
{screen !== "main" && (
|
|
4657
|
+
<AppShell
|
|
4658
|
+
layout={terminalLayout}
|
|
4659
|
+
screen={screen}
|
|
4660
|
+
authState={authStatus.state}
|
|
4661
|
+
workspaceLabel={workspaceLabel}
|
|
4662
|
+
workspaceRoot={workspaceRoot}
|
|
4663
|
+
runtimeSummary={headerRuntimeSummary}
|
|
4664
|
+
staticEvents={staticEvents}
|
|
4665
|
+
activeEvents={activeEvents}
|
|
4666
|
+
uiState={uiState}
|
|
4667
|
+
verboseMode={verboseMode}
|
|
4668
|
+
mouseCapture={effectiveMouseCapture}
|
|
4669
|
+
onMouseActivity={resetMouseIdle}
|
|
4670
|
+
selectionProfile={selectionProfile}
|
|
4671
|
+
clearCount={sessionState.clearCount}
|
|
4672
|
+
headerConfig={effectiveHeaderConfig}
|
|
4673
|
+
updateAvailable={
|
|
4674
|
+
updateCheckResult?.status === "update-available" && updateCheckResult.latestVersion
|
|
4675
|
+
? { latestVersion: updateCheckResult.latestVersion, currentVersion: updateCheckResult.currentVersion }
|
|
4676
|
+
: null
|
|
4677
|
+
}
|
|
4678
|
+
panel={
|
|
4679
|
+
<>
|
|
4680
|
+
{screen === "backend-picker" && (
|
|
4681
|
+
<BackendPicker
|
|
4682
|
+
currentBackend={backend}
|
|
4683
|
+
onSelect={(value) => setBackendWithNotice(value as AvailableBackend)}
|
|
4684
|
+
onCancel={() => setScreen("main")}
|
|
4685
|
+
/>
|
|
4686
|
+
)}
|
|
4408
4687
|
|
|
4409
4688
|
{screen === "provider-picker" && (
|
|
4410
4689
|
<ProviderPicker
|
|
@@ -4557,7 +4836,7 @@ export function App({ launchArgs }: AppProps) {
|
|
|
4557
4836
|
subtitle="Enter an absolute path or a path relative to the locked workspace."
|
|
4558
4837
|
placeholder="relative\\or\\absolute\\path"
|
|
4559
4838
|
inputLabel="Path"
|
|
4560
|
-
footerHint="Enter
|
|
4839
|
+
footerHint="Esc to close · Enter to confirm"
|
|
4561
4840
|
onSubmit={(value) => {
|
|
4562
4841
|
if (!value.trim()) {
|
|
4563
4842
|
appendSystemEvent("Runtime policy", "Writable root path cannot be empty.");
|
|
@@ -4644,18 +4923,29 @@ export function App({ launchArgs }: AppProps) {
|
|
|
4644
4923
|
onCancel={handleImportCancel}
|
|
4645
4924
|
/>
|
|
4646
4925
|
)}
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4926
|
+
|
|
4927
|
+
{screen === "update-prompt" && updateCheckResult?.status === "update-available" && updateCheckResult.latestVersion && (
|
|
4928
|
+
<UpdatePromptPanel
|
|
4929
|
+
focusId={FOCUS_IDS.updatePrompt}
|
|
4930
|
+
currentVersion={updateCheckResult.currentVersion}
|
|
4931
|
+
latestVersion={updateCheckResult.latestVersion}
|
|
4932
|
+
onSkip={handleSkipUpdateForSession}
|
|
4933
|
+
onSkipUntilNextVersion={handleSkipUpdateVersion}
|
|
4934
|
+
/>
|
|
4935
|
+
)}
|
|
4936
|
+
</>
|
|
4937
|
+
}
|
|
4938
|
+
mainPanel={null}
|
|
4939
|
+
mainPanelMode="viewport"
|
|
4940
|
+
composer={composerElement}
|
|
4941
|
+
composerRows={composerRows}
|
|
4942
|
+
panelHint={screen !== "model-picker" ? (
|
|
4943
|
+
<Box marginTop={1} paddingX={1}>
|
|
4944
|
+
<Text color={activeTheme.textDim}>Close the active panel with Esc to return to the composer.</Text>
|
|
4945
|
+
</Box>
|
|
4946
|
+
) : null}
|
|
4947
|
+
/>
|
|
4948
|
+
)}
|
|
4659
4949
|
</ThemeProvider>
|
|
4660
4950
|
);
|
|
4661
4951
|
}
|