@golba98/codexa 1.0.3 → 1.0.5
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 +17 -18
- package/bin/codexa.js +62 -144
- package/package.json +4 -3
- package/src/app.tsx +642 -303
- package/src/commands/handler.ts +7 -18
- package/src/config/buildInfo.ts +2 -2
- package/src/config/layeredConfig.ts +1 -1
- package/src/config/runtimeConfig.ts +1 -1
- package/src/config/settings.ts +1 -1
- package/src/config/trustStore.ts +1 -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/executableResolver.ts +5 -1
- package/src/core/models/codexModelCapabilities.ts +57 -4
- package/src/core/models/codexModelsCacheSeed.ts +153 -0
- package/src/core/models/providerModelCache.ts +106 -0
- package/src/core/perf/renderDebug.ts +10 -6
- package/src/core/process/CommandRunner.ts +12 -1
- package/src/core/providerLauncher/registry.ts +64 -18
- package/src/core/providerLauncher/types.ts +12 -9
- package/src/core/providerLauncher/workspaceConfig.ts +41 -26
- package/src/core/providerRuntime/anthropic.ts +10 -6
- package/src/core/providerRuntime/antigravity.ts +461 -0
- package/src/core/providerRuntime/claudeCodeDiscovery.ts +724 -446
- 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/mistralVibe.ts +663 -0
- package/src/core/providerRuntime/models.ts +40 -15
- package/src/core/providerRuntime/reasoning.ts +9 -6
- package/src/core/providerRuntime/registry.ts +76 -4
- package/src/core/providerRuntime/types.ts +20 -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/{channel.ts → version/channel.ts} +1 -1
- package/src/core/{updateCheck.ts → version/updateCheck.ts} +10 -5
- package/src/core/{launchContext.ts → workspace/launchContext.ts} +9 -16
- 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 +66 -98
- 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 +1 -1
- package/src/test/runtimeTestUtils.ts +90 -0
- package/src/ui/{ActivityBars.tsx → chrome/ActivityBars.tsx} +1 -1
- package/src/ui/{ActivityIndicator.tsx → chrome/ActivityIndicator.tsx} +3 -3
- package/src/ui/{AnimatedStatusText.tsx → chrome/AnimatedStatusText.tsx} +3 -3
- package/src/ui/chrome/AppShell.tsx +672 -0
- package/src/ui/{BottomComposer.tsx → chrome/BottomComposer.tsx} +159 -158
- package/src/ui/{DashCard.tsx → chrome/DashCard.tsx} +2 -2
- package/src/ui/{RunFooter.tsx → chrome/RunFooter.tsx} +3 -3
- package/src/ui/chrome/RuntimeStatusBar.tsx +108 -0
- package/src/ui/{Spinner.tsx → chrome/Spinner.tsx} +1 -1
- package/src/ui/{TopHeader.tsx → chrome/TopHeader.tsx} +54 -38
- package/src/ui/{UpdateAvailableCard.tsx → chrome/UpdateAvailableCard.tsx} +5 -5
- package/src/ui/{commandNormalize.ts → input/commandNormalize.ts} +1 -1
- package/src/ui/{focus.ts → input/focus.ts} +1 -1
- package/src/ui/{inputBuffer.ts → input/inputBuffer.ts} +3 -3
- package/src/ui/layout.ts +298 -24
- package/src/ui/{AttachmentImportPanel.tsx → panels/AttachmentImportPanel.tsx} +3 -3
- package/src/ui/{AuthPanel.tsx → panels/AuthPanel.tsx} +5 -5
- package/src/ui/{BackendPicker.tsx → panels/BackendPicker.tsx} +2 -2
- package/src/ui/{ModePicker.tsx → panels/ModePicker.tsx} +2 -2
- package/src/ui/{ModelPicker.tsx → panels/ModelPicker.tsx} +2 -2
- package/src/ui/{ModelPickerScreen.tsx → panels/ModelPickerScreen.tsx} +224 -42
- package/src/ui/{ModelReasoningPicker.tsx → panels/ModelReasoningPicker.tsx} +5 -5
- package/src/ui/{Panel.tsx → panels/Panel.tsx} +1 -1
- package/src/ui/{PermissionsPanel.tsx → panels/PermissionsPanel.tsx} +3 -3
- package/src/ui/{PlanActionPicker.tsx → panels/PlanActionPicker.tsx} +2 -2
- package/src/ui/{PlanReviewPanel.tsx → panels/PlanReviewPanel.tsx} +5 -5
- package/src/ui/panels/ProviderPicker.tsx +737 -0
- package/src/ui/{ReasoningPicker.tsx → panels/ReasoningPicker.tsx} +3 -3
- package/src/ui/{SelectionPanel.tsx → panels/SelectionPanel.tsx} +5 -1
- package/src/ui/{SettingsPanel.tsx → panels/SettingsPanel.tsx} +4 -4
- package/src/ui/{TextEntryPanel.tsx → panels/TextEntryPanel.tsx} +4 -4
- package/src/ui/{ThemePicker.tsx → panels/ThemePicker.tsx} +2 -2
- package/src/ui/{UpdatePromptPanel.tsx → panels/UpdatePromptPanel.tsx} +17 -23
- package/src/ui/{Markdown.tsx → render/Markdown.tsx} +2 -2
- package/src/ui/{diffRenderer.ts → render/diffRenderer.ts} +1 -1
- package/src/ui/{logoVariants.ts → render/logoVariants.ts} +4 -8
- package/src/ui/{modeDisplay.ts → render/modeDisplay.ts} +2 -2
- package/src/ui/{outputPipeline.ts → render/outputPipeline.ts} +2 -2
- package/src/ui/{runtimeDisplay.ts → render/runtimeDisplay.ts} +23 -10
- package/src/ui/{textLayout.ts → render/textLayout.ts} +15 -4
- package/src/ui/{ActionRequiredBlock.tsx → timeline/ActionRequiredBlock.tsx} +3 -3
- package/src/ui/{AgentBlock.tsx → timeline/AgentBlock.tsx} +10 -10
- package/src/ui/{StaticIntroItem.tsx → timeline/StaticIntroItem.tsx} +2 -2
- package/src/ui/{ThinkingBlock.tsx → timeline/ThinkingBlock.tsx} +4 -4
- package/src/ui/{Timeline.tsx → timeline/Timeline.tsx} +1620 -1471
- package/src/ui/timeline/TranscriptShell.tsx +322 -0
- package/src/ui/{TurnGroup.tsx → timeline/TurnGroup.tsx} +15 -15
- package/src/ui/timeline/layoutListWindow.ts +145 -0
- package/src/ui/{progressEntries.ts → timeline/progressEntries.ts} +3 -3
- package/src/ui/{runActivityView.ts → timeline/runActivityView.ts} +1 -1
- package/src/ui/{timelineMeasure.ts → timeline/timelineMeasure.ts} +206 -134
- package/src/core/codex.ts +0 -124
- package/src/ui/AppShell.tsx +0 -706
- package/src/ui/ProviderPicker.tsx +0 -321
- package/src/ui/StaticTranscriptItem.tsx +0 -56
- /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/ui/{busyStatusAnimation.ts → chrome/busyStatusAnimation.ts} +0 -0
- /package/src/ui/{slashCommands.ts → input/slashCommands.ts} +0 -0
- /package/src/ui/{terminalAnswerFormat.ts → render/terminalAnswerFormat.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,39 @@ 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 === "mistral") return "Mistral Vibe CLI";
|
|
23
|
+
if (providerId === "antigravity") return "Antigravity";
|
|
24
|
+
return "OpenAI";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readDiagnosticString(
|
|
28
|
+
diagnostics: Record<string, string | number | boolean | null> | undefined,
|
|
29
|
+
keys: string[],
|
|
30
|
+
): string | null {
|
|
31
|
+
if (!diagnostics) return null;
|
|
32
|
+
for (const key of keys) {
|
|
33
|
+
const value = diagnostics[key];
|
|
34
|
+
if (typeof value === "string" && value.trim()) {
|
|
35
|
+
return value.trim();
|
|
36
|
+
}
|
|
37
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
38
|
+
return String(value);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
10
43
|
import { Box, Text, useApp, useFocusManager, useInput, useStdin, useStdout } from "ink";
|
|
11
44
|
import { handleCommand } from "./commands/handler.js";
|
|
12
45
|
import {
|
|
@@ -77,9 +110,9 @@ import {
|
|
|
77
110
|
probeCodexAuthStatus,
|
|
78
111
|
} from "./core/auth/codexAuth.js";
|
|
79
112
|
import { getTerminalSelectionProfile } from "./core/terminal/terminalSelection.js";
|
|
80
|
-
import { copyToClipboard } from "./core/clipboard.js";
|
|
81
|
-
import { normalizePlanReviewMarkdown, savePlan, readPlan } from "./core/planStorage.js";
|
|
82
|
-
import { getBlockedCleanupFailure } from "./core/cleanupFastFail.js";
|
|
113
|
+
import { copyToClipboard } from "./core/shared/clipboard.js";
|
|
114
|
+
import { normalizePlanReviewMarkdown, savePlan, readPlan } from "./core/workspace/planStorage.js";
|
|
115
|
+
import { getBlockedCleanupFailure } from "./core/shared/cleanupFastFail.js";
|
|
83
116
|
import { runShellCommand, summarizeCommandResult } from "./core/process/CommandRunner.js";
|
|
84
117
|
import {
|
|
85
118
|
buildPlanExecutionPrompt,
|
|
@@ -87,8 +120,8 @@ import {
|
|
|
87
120
|
detectHollowResponse,
|
|
88
121
|
isClearlySafeGeneratedCleanupRequest,
|
|
89
122
|
resolveExecutionMode,
|
|
90
|
-
} from "./core/codexPrompt.js";
|
|
91
|
-
import { formatHollowResponse } from "./core/hollowResponseFormat.js";
|
|
123
|
+
} from "./core/codex/codexPrompt.js";
|
|
124
|
+
import { formatHollowResponse } from "./core/shared/hollowResponseFormat.js";
|
|
92
125
|
import {
|
|
93
126
|
createFallbackModelCapabilities,
|
|
94
127
|
findModelCapability,
|
|
@@ -99,32 +132,35 @@ import {
|
|
|
99
132
|
normalizeReasoningForModelCapabilities,
|
|
100
133
|
type CodexModelCapabilities,
|
|
101
134
|
} from "./core/models/codexModelCapabilities.js";
|
|
135
|
+
import { loadSeededCodexCapabilities } from "./core/models/codexModelsCacheSeed.js";
|
|
102
136
|
import {
|
|
103
137
|
buildDevLaunchNotice,
|
|
104
138
|
buildWorkspaceCommandContext,
|
|
105
139
|
createWorkspaceRelaunchPlan,
|
|
106
140
|
guardWorkspaceRelaunch,
|
|
141
|
+
type LaunchContext,
|
|
107
142
|
resolveLaunchContext,
|
|
108
|
-
} from "./core/launchContext.js";
|
|
143
|
+
} from "./core/workspace/launchContext.js";
|
|
109
144
|
import {
|
|
110
145
|
findOutsideWorkspacePaths,
|
|
146
|
+
formatSkippedDependencyPath,
|
|
111
147
|
getPromptWorkspaceGuardMessage,
|
|
112
148
|
getShellWorkspaceGuardMessage,
|
|
113
|
-
} from "./core/workspaceGuard.js";
|
|
114
|
-
import {
|
|
115
|
-
formatContextCompact,
|
|
116
|
-
formatContextLength,
|
|
117
|
-
resolveModelContextLength,
|
|
118
|
-
type ModelContextMetadata,
|
|
149
|
+
} from "./core/workspace/workspaceGuard.js";
|
|
150
|
+
import {
|
|
151
|
+
formatContextCompact,
|
|
152
|
+
formatContextLength,
|
|
153
|
+
resolveModelContextLength,
|
|
154
|
+
type ModelContextMetadata,
|
|
119
155
|
} from "./core/providerRuntime/contextMetadata.js";
|
|
120
|
-
import { captureWorkspaceSnapshot, createWorkspaceActivityTracker, diffWorkspaceSnapshots } from "./core/workspaceActivity.js";
|
|
121
|
-
import { resolveWorkspaceRoot } from "./core/workspaceRoot.js";
|
|
156
|
+
import { captureWorkspaceSnapshot, createWorkspaceActivityTracker, diffWorkspaceSnapshots } from "./core/workspace/workspaceActivity.js";
|
|
157
|
+
import { resolveWorkspaceRoot } from "./core/workspace/workspaceRoot.js";
|
|
122
158
|
import {
|
|
123
159
|
importExternalFile,
|
|
124
160
|
isImageFile,
|
|
125
161
|
rewritePromptWithImportedPaths,
|
|
126
|
-
} from "./core/attachments.js";
|
|
127
|
-
import { loadProjectInstructions } from "./core/projectInstructions.js";
|
|
162
|
+
} from "./core/shared/attachments.js";
|
|
163
|
+
import { loadProjectInstructions } from "./core/workspace/projectInstructions.js";
|
|
128
164
|
import { isNoiseLine } from "./core/providers/codexTranscript.js";
|
|
129
165
|
import { getBackendProvider } from "./core/providers/registry.js";
|
|
130
166
|
import type { BackendProgressUpdate, BackendProvider } from "./core/providers/types.js";
|
|
@@ -137,11 +173,18 @@ import {
|
|
|
137
173
|
getProviderRuntime,
|
|
138
174
|
isProviderRouteConfigured,
|
|
139
175
|
isProviderRoutableInCodexa,
|
|
176
|
+
persistProviderDiscovery,
|
|
140
177
|
resolveActiveProviderRoute,
|
|
141
178
|
validateProviderRouteActivation,
|
|
142
179
|
} from "./core/providerRuntime/registry.js";
|
|
143
180
|
import { hasGeminiApiKey, runGeminiDiagnostics } from "./core/providerRuntime/gemini.js";
|
|
144
181
|
import { checkLocalProvider, runLocalDiagnostics, setLocalProviderConfig } from "./core/providerRuntime/local.js";
|
|
182
|
+
import {
|
|
183
|
+
detectVibeActiveModel,
|
|
184
|
+
launchMistralVibeCli,
|
|
185
|
+
resetMistralVibeSession,
|
|
186
|
+
resolveVibeExecutable,
|
|
187
|
+
} from "./core/providerRuntime/mistralVibe.js";
|
|
145
188
|
import { validateAnthropicRoute, ANTHROPIC_ROUTE_SETUP_MESSAGE } from "./core/providerRuntime/anthropic.js";
|
|
146
189
|
import { providerModelsToCodexCapabilities } from "./core/providerRuntime/models.js";
|
|
147
190
|
import {
|
|
@@ -154,16 +197,13 @@ import {
|
|
|
154
197
|
} from "./core/providerLauncher/workspaceConfig.js";
|
|
155
198
|
import { sanitizeTerminalInput, sanitizeTerminalLines, sanitizeTerminalOutput } from "./core/terminal/terminalSanitize.js";
|
|
156
199
|
import { createTerminalModeController, setTerminalControlUIState } from "./core/terminal/terminalControl.js";
|
|
200
|
+
import { resolveInkRenderInstance, resetInkOutputForFreshFrame } from "./core/terminal/inkRenderReset.js";
|
|
201
|
+
import { createClearFrameBoundaryController } from "./core/terminal/clearFrameBoundary.js";
|
|
157
202
|
import {
|
|
158
|
-
acquireTerminalTitleGuard,
|
|
159
|
-
beginColdStartSequence,
|
|
160
|
-
deriveTerminalTitle,
|
|
161
|
-
reassertIntendedTerminalTitle,
|
|
162
|
-
refreshTerminalTitle,
|
|
163
203
|
setTerminalTitleLifecycleState,
|
|
164
|
-
setIntendedTerminalTitle,
|
|
165
204
|
} from "./core/terminal/terminalTitle.js";
|
|
166
|
-
import { getStdinDebugState, traceInputDebug } from "./core/inputDebug.js";
|
|
205
|
+
import { getStdinDebugState, traceInputDebug } from "./core/debug/inputDebug.js";
|
|
206
|
+
import { traceModelStateDebug } from "./core/debug/modelStateDebug.js";
|
|
167
207
|
import * as perf from "./core/perf/profiler.js";
|
|
168
208
|
import * as renderDebug from "./core/perf/renderDebug.js";
|
|
169
209
|
import {
|
|
@@ -174,7 +214,7 @@ import {
|
|
|
174
214
|
getLocalGitRemoteUrl,
|
|
175
215
|
parseRepoIdentity,
|
|
176
216
|
type DiagnosticResult,
|
|
177
|
-
} from "./core/githubDiagnostics.js";
|
|
217
|
+
} from "./core/shared/githubDiagnostics.js";
|
|
178
218
|
import type { RunEvent, Screen, ShellEvent, TimelineEvent, UIState, UserPromptEvent } from "./session/types.js";
|
|
179
219
|
import {
|
|
180
220
|
buildFollowUpPrompt,
|
|
@@ -198,25 +238,25 @@ import {
|
|
|
198
238
|
submitPlanFeedback,
|
|
199
239
|
type PlanFlowState,
|
|
200
240
|
} from "./session/planFlow.js";
|
|
201
|
-
import { AuthPanel } from "./ui/AuthPanel.js";
|
|
202
|
-
import { BackendPicker } from "./ui/BackendPicker.js";
|
|
203
|
-
import { measureBottomComposerRows, MemoizedBottomComposer } from "./ui/BottomComposer.js";
|
|
204
|
-
import { useTerminalViewport } from "./ui/layout.js";
|
|
205
|
-
import { ModelPickerScreen } from "./ui/ModelPickerScreen.js";
|
|
206
|
-
import { ModePicker } from "./ui/ModePicker.js";
|
|
207
|
-
import { PlanActionPicker, type PlanActionValue, measurePlanActionPickerRows } from "./ui/PlanActionPicker.js";
|
|
208
|
-
import { PermissionsPanel, type PermissionsPanelAction } from "./ui/PermissionsPanel.js";
|
|
209
|
-
import { ProviderPicker } from "./ui/ProviderPicker.js";
|
|
210
|
-
import { ReasoningPicker } from "./ui/ReasoningPicker.js";
|
|
211
|
-
import { AttachmentImportPanel, type PendingImportFile } from "./ui/AttachmentImportPanel.js";
|
|
212
|
-
import { SelectionPanel } from "./ui/SelectionPanel.js";
|
|
213
|
-
import { SettingsPanel } from "./ui/SettingsPanel.js";
|
|
214
|
-
import { UpdatePromptPanel } from "./ui/UpdatePromptPanel.js";
|
|
215
|
-
import { measureTextEntryPanelRows, TextEntryPanel } from "./ui/TextEntryPanel.js";
|
|
216
|
-
import { ThemePicker } from "./ui/ThemePicker.js";
|
|
217
|
-
import { getFocusTargetForScreen, FOCUS_IDS } from "./ui/focus.js";
|
|
218
|
-
import { ThemeProvider, THEMES } from "./ui/theme.js";
|
|
219
|
-
import { buildActiveRuntimeDisplay, runtimeDisplayToSummary } from "./ui/runtimeDisplay.js";
|
|
241
|
+
import { AuthPanel } from "./ui/panels/AuthPanel.js";
|
|
242
|
+
import { BackendPicker } from "./ui/panels/BackendPicker.js";
|
|
243
|
+
import { measureBottomComposerRows, MemoizedBottomComposer } from "./ui/chrome/BottomComposer.js";
|
|
244
|
+
import { resolveStartupHeaderMode, useTerminalViewport } from "./ui/layout.js";
|
|
245
|
+
import { ModelPickerScreen } from "./ui/panels/ModelPickerScreen.js";
|
|
246
|
+
import { ModePicker } from "./ui/panels/ModePicker.js";
|
|
247
|
+
import { PlanActionPicker, type PlanActionValue, measurePlanActionPickerRows } from "./ui/panels/PlanActionPicker.js";
|
|
248
|
+
import { PermissionsPanel, type PermissionsPanelAction } from "./ui/panels/PermissionsPanel.js";
|
|
249
|
+
import { ProviderPicker } from "./ui/panels/ProviderPicker.js";
|
|
250
|
+
import { ReasoningPicker } from "./ui/panels/ReasoningPicker.js";
|
|
251
|
+
import { AttachmentImportPanel, type PendingImportFile } from "./ui/panels/AttachmentImportPanel.js";
|
|
252
|
+
import { SelectionPanel } from "./ui/panels/SelectionPanel.js";
|
|
253
|
+
import { SettingsPanel } from "./ui/panels/SettingsPanel.js";
|
|
254
|
+
import { UpdatePromptPanel } from "./ui/panels/UpdatePromptPanel.js";
|
|
255
|
+
import { measureTextEntryPanelRows, TextEntryPanel } from "./ui/panels/TextEntryPanel.js";
|
|
256
|
+
import { ThemePicker } from "./ui/panels/ThemePicker.js";
|
|
257
|
+
import { getFocusTargetForScreen, FOCUS_IDS } from "./ui/input/focus.js";
|
|
258
|
+
import { ThemeProvider, THEMES } from "./ui/theme.js";
|
|
259
|
+
import { buildActiveRuntimeDisplay, runtimeDisplayToSummary } from "./ui/render/runtimeDisplay.js";
|
|
220
260
|
import {
|
|
221
261
|
cancelThemeSelection,
|
|
222
262
|
commitThemeSelection,
|
|
@@ -226,9 +266,11 @@ import {
|
|
|
226
266
|
type ThemeSelectionState,
|
|
227
267
|
} from "./ui/themeFlow.js";
|
|
228
268
|
import { isBusy as isUiBusy } from "./session/types.js";
|
|
229
|
-
import { AppShell } from "./ui/AppShell.js";
|
|
230
|
-
import {
|
|
231
|
-
import {
|
|
269
|
+
import { AppShell } from "./ui/chrome/AppShell.js";
|
|
270
|
+
import { TranscriptShell } from "./ui/timeline/TranscriptShell.js";
|
|
271
|
+
import type { RuntimeAvailability } from "./ui/chrome/RuntimeStatusBar.js";
|
|
272
|
+
import { checkForUpdates, formatLocalDevUpdateStatus, formatUpdateInstructions, shouldRunStartupUpdateCheck, type UpdateCheckResult } from "./core/version/updateCheck.js";
|
|
273
|
+
import { isLocalDevChannel } from "./core/version/channel.js";
|
|
232
274
|
import {
|
|
233
275
|
isCacheValid,
|
|
234
276
|
loadUpdateCheckCache,
|
|
@@ -260,6 +302,51 @@ function createTurnId(): number {
|
|
|
260
302
|
return nextTurnId++;
|
|
261
303
|
}
|
|
262
304
|
|
|
305
|
+
function createLaunchModeEvent(launchContext: LaunchContext): TimelineEvent | null {
|
|
306
|
+
const devLaunchNotice = buildDevLaunchNotice(launchContext);
|
|
307
|
+
if (!devLaunchNotice) return null;
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
id: createEventId(),
|
|
311
|
+
type: "system",
|
|
312
|
+
createdAt: Date.now(),
|
|
313
|
+
title: sanitizeTerminalOutput("Launch mode"),
|
|
314
|
+
content: sanitizeTerminalOutput(devLaunchNotice, { preserveTabs: false, tabSize: 2 }),
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function createProviderMigrationNoticeEvent(
|
|
319
|
+
notice: ProviderWorkspaceConfig["migrationNotice"] | undefined,
|
|
320
|
+
providerLabel: string | null = null,
|
|
321
|
+
): TimelineEvent | null {
|
|
322
|
+
if (!notice) return null;
|
|
323
|
+
|
|
324
|
+
const resolvedProviderLabel = providerLabel ?? formatRuntimeProviderLabel(notice.revertedProviderId);
|
|
325
|
+
return {
|
|
326
|
+
id: createEventId(),
|
|
327
|
+
type: "system",
|
|
328
|
+
createdAt: Date.now(),
|
|
329
|
+
title: sanitizeTerminalOutput("Provider migrated"),
|
|
330
|
+
content: sanitizeTerminalOutput(
|
|
331
|
+
`${formatRuntimeProviderLabel(notice.deprecatedProviderId as ProviderId)} provider is no longer supported. Reverted to ${resolvedProviderLabel}.`,
|
|
332
|
+
{ preserveTabs: false, tabSize: 2 },
|
|
333
|
+
),
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function createStartupStaticEvents({
|
|
338
|
+
launchContext,
|
|
339
|
+
providerWorkspaceConfig,
|
|
340
|
+
}: {
|
|
341
|
+
launchContext: LaunchContext;
|
|
342
|
+
providerWorkspaceConfig: ProviderWorkspaceConfig;
|
|
343
|
+
}): TimelineEvent[] {
|
|
344
|
+
return [
|
|
345
|
+
createLaunchModeEvent(launchContext),
|
|
346
|
+
createProviderMigrationNoticeEvent(providerWorkspaceConfig.migrationNotice),
|
|
347
|
+
].filter((event): event is TimelineEvent => event !== null);
|
|
348
|
+
}
|
|
349
|
+
|
|
263
350
|
function createInitialAuthStatus(): CodexAuthProbeResult {
|
|
264
351
|
return {
|
|
265
352
|
state: "checking",
|
|
@@ -324,6 +411,12 @@ export function App({ launchArgs }: AppProps) {
|
|
|
324
411
|
[launchContext],
|
|
325
412
|
);
|
|
326
413
|
const terminalLayout = useTerminalViewport();
|
|
414
|
+
// Assigned during render (like screenRef) so the clear-frame boundary can
|
|
415
|
+
// tell, at frame-write time, whether the committing tree was laid out
|
|
416
|
+
// against the current terminal width (the viewport hook commits dimensions
|
|
417
|
+
// on a trailing settle, so frames can lag stdout.columns).
|
|
418
|
+
const terminalLayoutColsRef = useRef<number | undefined>(terminalLayout.rawCols ?? terminalLayout.cols);
|
|
419
|
+
terminalLayoutColsRef.current = terminalLayout.rawCols ?? terminalLayout.cols;
|
|
327
420
|
|
|
328
421
|
// ─── State & Refs ────────────────────────────────────────────────────────────
|
|
329
422
|
|
|
@@ -375,17 +468,60 @@ export function App({ launchArgs }: AppProps) {
|
|
|
375
468
|
const screenRef = useRef<Screen>("main");
|
|
376
469
|
screenRef.current = screen;
|
|
377
470
|
const [composerInstanceKey, setComposerInstanceKey] = useState(0);
|
|
378
|
-
|
|
471
|
+
// Bumped purely to force one extra React commit when the /clear boundary needs
|
|
472
|
+
// the authoritative post-clear frame flushed (see the syncRenderState effect).
|
|
473
|
+
const [, bumpPostClearRepaint] = useState(0);
|
|
474
|
+
// Bumped whenever a width-changing resize forces a physical terminal clear
|
|
475
|
+
// (see clearFrameBoundaryController below). TranscriptShell folds this into
|
|
476
|
+
// its <Static> key so already-flushed content (logo, past turns) reprints
|
|
477
|
+
// at the new width instead of staying erased — Ink's <Static> never
|
|
478
|
+
// re-emits items on its own once flushed.
|
|
479
|
+
const [staticRepaintGeneration, bumpStaticRepaintGeneration] = useState(0);
|
|
480
|
+
// Assigned during render (like screenRef) so the clear-frame boundary can
|
|
481
|
+
// tell, at frame-write time, whether the committing tree already contains
|
|
482
|
+
// the re-flushed <Static> content for a pending width repaint.
|
|
483
|
+
const staticRepaintGenerationRef = useRef(0);
|
|
484
|
+
staticRepaintGenerationRef.current = staticRepaintGeneration;
|
|
485
|
+
const { state: sessionState, dispatch: dispatchSession } = useAppSessionState(() => {
|
|
486
|
+
return createStartupStaticEvents({
|
|
487
|
+
launchContext,
|
|
488
|
+
providerWorkspaceConfig: initialProviderWorkspaceConfig.current,
|
|
489
|
+
});
|
|
490
|
+
});
|
|
379
491
|
const [authStatus, setAuthStatus] = useState<CodexAuthProbeResult>(createInitialAuthStatus());
|
|
380
492
|
const [authStatusBusy, setAuthStatusBusy] = useState(false);
|
|
381
493
|
// Running character total across the conversation — used to estimate token usage
|
|
382
494
|
const [conversationChars, setConversationChars] = useState(0);
|
|
383
|
-
|
|
495
|
+
// Seeded synchronously from local caches (codex's models_cache.json or the
|
|
496
|
+
// persisted last-good discovery) so the model picker opens instantly with
|
|
497
|
+
// real models; live discovery replaces this in the background.
|
|
498
|
+
const [modelCapabilities, setModelCapabilities] = useState<CodexModelCapabilities | null>(() => loadSeededCodexCapabilities());
|
|
384
499
|
const [modelCapabilitiesBusy, setModelCapabilitiesBusy] = useState(false);
|
|
500
|
+
// True while a provider route switch is validating (subprocess probes);
|
|
501
|
+
// drives the model picker's loading state for non-openai providers.
|
|
502
|
+
const [routeSwitchBusy, setRouteSwitchBusy] = useState(false);
|
|
385
503
|
const [activeContextMetadata, setActiveContextMetadata] = useState<ModelContextMetadata | null>(null);
|
|
386
504
|
const { stdout } = useStdout();
|
|
387
505
|
const { stdin } = useStdin();
|
|
388
506
|
const terminalControl = useMemo(() => createTerminalModeController((chunk) => stdout.write(chunk)), [stdout]);
|
|
507
|
+
// Live Ink instance behind this stdout, used to reset Ink's frame caches on
|
|
508
|
+
// the /clear boundary so the next frame is authoritative (see handleClear).
|
|
509
|
+
const inkInstance = useMemo(() => resolveInkRenderInstance(stdout), [stdout]);
|
|
510
|
+
const clearFrameBoundaryController = useMemo(
|
|
511
|
+
() => createClearFrameBoundaryController({
|
|
512
|
+
instance: inkInstance,
|
|
513
|
+
terminalControl,
|
|
514
|
+
stdout,
|
|
515
|
+
source: "src/app.tsx:clearBoundary",
|
|
516
|
+
// Read at frame-write time; screenRef is assigned during render, so it
|
|
517
|
+
// always reflects the render that produced the frame being written.
|
|
518
|
+
isOverlayActive: () => screenRef.current !== "main",
|
|
519
|
+
onWidthResizeRefresh: () => bumpStaticRepaintGeneration((tick) => tick + 1),
|
|
520
|
+
getRenderedRepaintGeneration: () => staticRepaintGenerationRef.current,
|
|
521
|
+
getRenderedLayoutCols: () => terminalLayoutColsRef.current,
|
|
522
|
+
}),
|
|
523
|
+
[inkInstance, stdout, terminalControl],
|
|
524
|
+
);
|
|
389
525
|
const [mouseOverride, setMouseOverride] = useState<boolean | null>(null);
|
|
390
526
|
const [isMouseIdle, setIsMouseIdle] = useState(false);
|
|
391
527
|
const mouseIdleTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
@@ -395,9 +531,14 @@ export function App({ launchArgs }: AppProps) {
|
|
|
395
531
|
if (mouseIdleTimerRef.current) {
|
|
396
532
|
clearTimeout(mouseIdleTimerRef.current);
|
|
397
533
|
}
|
|
534
|
+
// We disable the idle timeout to ensure reliable trackpad/mouse-wheel scrolling
|
|
535
|
+
// in the pop-out window. Otherwise, after 1.5 seconds of inactivity, mouse capture
|
|
536
|
+
// turns off, and scrolling stops working entirely.
|
|
537
|
+
/*
|
|
398
538
|
mouseIdleTimerRef.current = setTimeout(() => {
|
|
399
539
|
setIsMouseIdle(true);
|
|
400
540
|
}, 1500);
|
|
541
|
+
*/
|
|
401
542
|
}, []);
|
|
402
543
|
|
|
403
544
|
useInput(() => {
|
|
@@ -409,26 +550,71 @@ export function App({ launchArgs }: AppProps) {
|
|
|
409
550
|
const [planFlow, setPlanFlow] = useState<PlanFlowState>(createInitialPlanFlowState);
|
|
410
551
|
const [initialRevisionText, setInitialRevisionText] = useState("");
|
|
411
552
|
const [updateCheckResult, setUpdateCheckResult] = useState<UpdateCheckResult | null>(null);
|
|
412
|
-
//
|
|
413
|
-
//
|
|
414
|
-
// wheel events; native drag-select then requires Shift in Windows Terminal.
|
|
415
|
-
// "selection" keeps the terminal in control of mouse events.
|
|
416
|
-
// /mouse toggles the in-session override without changing the saved setting.
|
|
417
|
-
//
|
|
418
|
-
// We apply an idle-timeout: if no wheel events or keypresses occur for 1.5s,
|
|
419
|
-
// we disable mouse reporting so native drag-selection works unmodified.
|
|
553
|
+
// Transcript mode leaves mouse reporting off so wheel/trackpad input scrolls
|
|
554
|
+
// the terminal emulator's native scrollback instead of an in-app viewport.
|
|
420
555
|
const mouseCapture = (mouseOverride ?? (terminalMouseMode === "wheel")) && !isMouseIdle;
|
|
556
|
+
const effectiveMouseCapture = false;
|
|
557
|
+
const overlayMode = screen !== "main";
|
|
421
558
|
|
|
422
559
|
// ─── Effects & Handlers ──────────────────────────────────────────────────────
|
|
423
560
|
|
|
424
561
|
useEffect(() => {
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
562
|
+
return () => {
|
|
563
|
+
clearFrameBoundaryController?.dispose();
|
|
564
|
+
};
|
|
565
|
+
}, [clearFrameBoundaryController]);
|
|
566
|
+
|
|
567
|
+
useEffect(() => {
|
|
568
|
+
if (!clearFrameBoundaryController) return;
|
|
569
|
+
const postClearRepaintPending = clearFrameBoundaryController.syncRenderState({
|
|
570
|
+
generation: sessionState.clearEpoch,
|
|
571
|
+
staticEventsLength: sessionState.staticEvents.length,
|
|
572
|
+
activeEventsLength: sessionState.activeEvents.length,
|
|
573
|
+
transcriptCleared: sessionState.staticEvents.length === 0 && sessionState.activeEvents.length === 0,
|
|
574
|
+
clearGenerationReady: sessionState.clearEpoch > 0 && sessionState.uiState.kind === "IDLE" && sessionState.activeEvents.length === 0,
|
|
575
|
+
uiStateKind: sessionState.uiState.kind,
|
|
576
|
+
});
|
|
577
|
+
if (postClearRepaintPending) {
|
|
578
|
+
// Ink already wrote (and suppressed) the cleared frame during the commit
|
|
579
|
+
// that preceded this passive effect, so the boundary's gate only became
|
|
580
|
+
// satisfiable just now. Force one more commit to deterministically flush
|
|
581
|
+
// the authoritative post-clear frame instead of waiting on an incidental
|
|
582
|
+
// later render. `bumpPostClearRepaint` is not an effect dependency, so this
|
|
583
|
+
// re-render does not re-run the effect (no loop).
|
|
584
|
+
bumpPostClearRepaint((tick) => tick + 1);
|
|
585
|
+
}
|
|
586
|
+
}, [clearFrameBoundaryController, sessionState]);
|
|
587
|
+
|
|
588
|
+
useEffect(() => {
|
|
589
|
+
// Main transcript mode keeps native terminal scrollback and selection in
|
|
590
|
+
// control. Keep writing the disable sequence defensively in case a previous
|
|
591
|
+
// version or overlay left mouse reporting enabled.
|
|
592
|
+
terminalControl.setMouseReporting(effectiveMouseCapture, effectiveMouseCapture ? "src/app.tsx:mouseCapture.enable" : "src/app.tsx:mouseCapture.disable");
|
|
428
593
|
return () => {
|
|
429
594
|
terminalControl.setMouseReporting(false, "src/app.tsx:mouseCapture.cleanup");
|
|
430
595
|
};
|
|
431
|
-
}, [
|
|
596
|
+
}, [effectiveMouseCapture, terminalControl]);
|
|
597
|
+
|
|
598
|
+
useLayoutEffect(() => {
|
|
599
|
+
// The clear-frame boundary owns alternate-screen switching so the buffer
|
|
600
|
+
// flip happens atomically with the first frame of the new screen (see
|
|
601
|
+
// clearFrameBoundary.ts). Toggling from an effect would run after Ink has
|
|
602
|
+
// already written that frame into the wrong buffer — the overlay would
|
|
603
|
+
// land in the normal buffer's scrollback and the alt screen would open
|
|
604
|
+
// blank. This effect is only a fallback for environments where no live
|
|
605
|
+
// Ink instance could be resolved (tests, exotic Ink versions).
|
|
606
|
+
if (clearFrameBoundaryController) return;
|
|
607
|
+
terminalControl.setAlternateScreen(
|
|
608
|
+
overlayMode,
|
|
609
|
+
overlayMode ? "src/app.tsx:overlay.enterAlternateScreen" : "src/app.tsx:overlay.exitAlternateScreen",
|
|
610
|
+
);
|
|
611
|
+
}, [clearFrameBoundaryController, overlayMode, terminalControl]);
|
|
612
|
+
|
|
613
|
+
useEffect(() => {
|
|
614
|
+
return () => {
|
|
615
|
+
terminalControl.setAlternateScreen(false, "src/app.tsx:overlay.cleanupAlternateScreen");
|
|
616
|
+
};
|
|
617
|
+
}, [terminalControl]);
|
|
432
618
|
|
|
433
619
|
const cleanupRef = useRef<(() => void) | null>(null);
|
|
434
620
|
const activeRunLifecycleRef = useRef<PromptRunLifecycle | null>(null);
|
|
@@ -447,7 +633,7 @@ export function App({ launchArgs }: AppProps) {
|
|
|
447
633
|
const modelSelectionInFlightRef = useRef(false);
|
|
448
634
|
const providerRouteErrorsRef = useRef<Record<string, string>>({});
|
|
449
635
|
const providerDiagnosticsRef = useRef<Record<string, Record<string, string | number | boolean | null>>>({});
|
|
450
|
-
const providerMigrationNoticeShownRef = useRef(
|
|
636
|
+
const providerMigrationNoticeShownRef = useRef(Boolean(initialProviderWorkspaceConfig.current.migrationNotice));
|
|
451
637
|
const initialPromptSubmittedRef = useRef(false);
|
|
452
638
|
const activeThemeName = getDisplayedThemeName(themeSelection);
|
|
453
639
|
const activeTheme =
|
|
@@ -487,6 +673,7 @@ export function App({ launchArgs }: AppProps) {
|
|
|
487
673
|
setLocalProviderConfig(providerWorkspaceConfig.providers?.local);
|
|
488
674
|
return buildProviderRegistry({
|
|
489
675
|
activeModel: model,
|
|
676
|
+
workspaceRoot,
|
|
490
677
|
workspaceConfig: providerWorkspaceConfig,
|
|
491
678
|
diagnostics: providerDiagnosticsRef.current,
|
|
492
679
|
routeErrors: providerRouteErrorsRef.current,
|
|
@@ -496,13 +683,38 @@ export function App({ launchArgs }: AppProps) {
|
|
|
496
683
|
// updates mutate providerDiagnosticsRef/providerRouteErrorsRef (refs, not state)
|
|
497
684
|
// and then increment the nonce to trigger a re-read of those refs.
|
|
498
685
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
499
|
-
[model, providerWorkspaceConfig, registryNonce],
|
|
686
|
+
[model, providerWorkspaceConfig, registryNonce, workspaceRoot],
|
|
500
687
|
);
|
|
501
688
|
const workspaceDefaultProvider = useMemo(
|
|
502
689
|
() => providerRegistry.find((provider) => provider.isDefault) ?? providerRegistry[0] ?? null,
|
|
503
690
|
[providerRegistry],
|
|
504
691
|
);
|
|
505
692
|
const activeRouteProviderId = activeProviderRoute.providerId;
|
|
693
|
+
const markProviderAvailability = useCallback((
|
|
694
|
+
providerId: ProviderId,
|
|
695
|
+
availability: RuntimeAvailability,
|
|
696
|
+
reason: string,
|
|
697
|
+
) => {
|
|
698
|
+
const previous = providerDiagnosticsRef.current[providerId] ?? {};
|
|
699
|
+
const selectedModel = typeof previous.selectedModel === "string" && previous.selectedModel.trim()
|
|
700
|
+
? previous.selectedModel.trim()
|
|
701
|
+
: providerId === activeProviderRoute.providerId
|
|
702
|
+
? activeProviderRoute.modelId
|
|
703
|
+
: null;
|
|
704
|
+
providerDiagnosticsRef.current[providerId] = {
|
|
705
|
+
...previous,
|
|
706
|
+
selectedModel,
|
|
707
|
+
availabilityStatus: availability,
|
|
708
|
+
endpointCheckResult: availability,
|
|
709
|
+
};
|
|
710
|
+
traceModelStateDebug("provider_availability_marked", {
|
|
711
|
+
providerId,
|
|
712
|
+
selectedModel,
|
|
713
|
+
availability,
|
|
714
|
+
reason,
|
|
715
|
+
});
|
|
716
|
+
setRegistryNonce((current) => current + 1);
|
|
717
|
+
}, [activeProviderRoute.modelId, activeProviderRoute.providerId]);
|
|
506
718
|
|
|
507
719
|
// Reset provider readiness when the user switches to a different provider.
|
|
508
720
|
useEffect(() => {
|
|
@@ -521,7 +733,10 @@ export function App({ launchArgs }: AppProps) {
|
|
|
521
733
|
const modelPickerDiscovery = useMemo(() => {
|
|
522
734
|
if (modelPickerProviderId === "openai") return null;
|
|
523
735
|
return discoverProviderModels(modelPickerProviderId);
|
|
524
|
-
|
|
736
|
+
// registryNonce is intentionally included: route validation discovers models
|
|
737
|
+
// as a side effect and bumps the nonce so an open picker re-reads them.
|
|
738
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
739
|
+
}, [modelPickerProviderId, registryNonce]);
|
|
525
740
|
const providerModelCapabilities = useMemo(() => {
|
|
526
741
|
if (!modelPickerDiscovery) return null;
|
|
527
742
|
return providerModelsToCodexCapabilities(modelPickerDiscovery.models, activeProviderRoute.modelId);
|
|
@@ -736,17 +951,6 @@ export function App({ launchArgs }: AppProps) {
|
|
|
736
951
|
() => formatWorkspaceDisplayPath(workspaceRoot, workspaceDisplayMode),
|
|
737
952
|
[workspaceDisplayMode, workspaceRoot],
|
|
738
953
|
);
|
|
739
|
-
const terminalTitleLabel = useMemo(
|
|
740
|
-
() => deriveTerminalTitle(workspaceRoot, terminalTitleMode),
|
|
741
|
-
[terminalTitleMode, workspaceRoot],
|
|
742
|
-
);
|
|
743
|
-
const latestTerminalTitleRef = useRef(terminalTitleLabel);
|
|
744
|
-
latestTerminalTitleRef.current = terminalTitleLabel;
|
|
745
|
-
|
|
746
|
-
// Cold-start: write title immediately on mount, then retry to outlast Windows Terminal shell integration.
|
|
747
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
748
|
-
useEffect(() => beginColdStartSequence(latestTerminalTitleRef.current), []);
|
|
749
|
-
|
|
750
954
|
const { staticEvents, activeEvents, uiState, inputValue, cursor } = sessionState;
|
|
751
955
|
|
|
752
956
|
const currentUserSettings = useMemo<UserSettingValues>(() => ({
|
|
@@ -768,22 +972,73 @@ export function App({ launchArgs }: AppProps) {
|
|
|
768
972
|
[planFlow],
|
|
769
973
|
);
|
|
770
974
|
|
|
771
|
-
const activeRuntimeDisplay = useMemo(() => buildActiveRuntimeDisplay({
|
|
772
|
-
route: activeProviderRoute,
|
|
773
|
-
reasoningLevel,
|
|
774
|
-
mode,
|
|
775
|
-
tokensUsed: estimateTokens(conversationChars),
|
|
776
|
-
modelCapability: currentModelCapability,
|
|
777
|
-
contextMetadata: activeContextMetadata,
|
|
778
|
-
}), [
|
|
779
|
-
activeContextMetadata,
|
|
780
|
-
activeProviderRoute,
|
|
781
|
-
conversationChars,
|
|
782
|
-
currentModelCapability,
|
|
783
|
-
mode,
|
|
784
|
-
reasoningLevel,
|
|
785
|
-
]);
|
|
786
|
-
const currentModelSpec = activeRuntimeDisplay.modelSpec;
|
|
975
|
+
const activeRuntimeDisplay = useMemo(() => buildActiveRuntimeDisplay({
|
|
976
|
+
route: activeProviderRoute,
|
|
977
|
+
reasoningLevel,
|
|
978
|
+
mode,
|
|
979
|
+
tokensUsed: estimateTokens(conversationChars),
|
|
980
|
+
modelCapability: currentModelCapability,
|
|
981
|
+
contextMetadata: activeContextMetadata,
|
|
982
|
+
}), [
|
|
983
|
+
activeContextMetadata,
|
|
984
|
+
activeProviderRoute,
|
|
985
|
+
conversationChars,
|
|
986
|
+
currentModelCapability,
|
|
987
|
+
mode,
|
|
988
|
+
reasoningLevel,
|
|
989
|
+
]);
|
|
990
|
+
const currentModelSpec = activeRuntimeDisplay.modelSpec;
|
|
991
|
+
const activeRuntimeAvailability = useMemo<RuntimeAvailability>(() => {
|
|
992
|
+
if (activeProviderRoute.providerId !== "local") {
|
|
993
|
+
return "available";
|
|
994
|
+
}
|
|
995
|
+
const diagnostics = providerDiagnosticsRef.current.local;
|
|
996
|
+
return normalizeRuntimeAvailability(diagnostics?.availabilityStatus ?? diagnostics?.endpointCheckResult);
|
|
997
|
+
// registryNonce intentionally re-reads providerDiagnosticsRef.
|
|
998
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
999
|
+
}, [activeProviderRoute.providerId, registryNonce]);
|
|
1000
|
+
const visibleRuntimeModelState = useMemo(() => {
|
|
1001
|
+
const diagnostics = providerDiagnosticsRef.current[activeProviderRoute.providerId];
|
|
1002
|
+
const providerLabel = activeRouteProvider?.displayName
|
|
1003
|
+
?? formatRuntimeProviderLabel(activeProviderRoute.providerId);
|
|
1004
|
+
const diagnosticModel = readDiagnosticString(diagnostics, [
|
|
1005
|
+
"selectedModel",
|
|
1006
|
+
"modelId",
|
|
1007
|
+
"currentModel",
|
|
1008
|
+
"defaultModel",
|
|
1009
|
+
]);
|
|
1010
|
+
const routeModel = activeProviderRoute.modelId?.trim();
|
|
1011
|
+
const modelLabel = routeModel
|
|
1012
|
+
|| diagnosticModel
|
|
1013
|
+
|| (activeRuntimeAvailability === "checking" || activeRuntimeAvailability === "reconnecting"
|
|
1014
|
+
? "Detecting..."
|
|
1015
|
+
: "Unknown");
|
|
1016
|
+
const modelDisplay = activeRuntimeDisplay.footerModelDisplay?.trim()
|
|
1017
|
+
|| `${providerLabel} / ${modelLabel}`;
|
|
1018
|
+
const diagnosticContext = readDiagnosticString(diagnostics, ["contextDisplay", "contextLength"]);
|
|
1019
|
+
const contextDisplay = activeRuntimeDisplay.contextDisplay?.trim()
|
|
1020
|
+
|| diagnosticContext
|
|
1021
|
+
|| "Unknown";
|
|
1022
|
+
const nextState = {
|
|
1023
|
+
selectedProvider: activeProviderRoute.providerId,
|
|
1024
|
+
selectedModel: modelLabel,
|
|
1025
|
+
modelDisplay,
|
|
1026
|
+
contextDisplay,
|
|
1027
|
+
availability: activeRuntimeAvailability,
|
|
1028
|
+
};
|
|
1029
|
+
traceModelStateDebug("runtime_model_display_derived", nextState);
|
|
1030
|
+
return nextState;
|
|
1031
|
+
// registryNonce intentionally re-reads providerDiagnosticsRef.
|
|
1032
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1033
|
+
}, [
|
|
1034
|
+
activeProviderRoute.modelId,
|
|
1035
|
+
activeProviderRoute.providerId,
|
|
1036
|
+
activeRuntimeAvailability,
|
|
1037
|
+
activeRuntimeDisplay.contextDisplay,
|
|
1038
|
+
activeRuntimeDisplay.footerModelDisplay,
|
|
1039
|
+
activeRouteProvider?.displayName,
|
|
1040
|
+
registryNonce,
|
|
1041
|
+
]);
|
|
787
1042
|
|
|
788
1043
|
const hasUserPrompt = useMemo(
|
|
789
1044
|
() => staticEvents.some((e) => e.type === "user") || activeEvents.some((e) => e.type === "user"),
|
|
@@ -808,63 +1063,15 @@ export function App({ launchArgs }: AppProps) {
|
|
|
808
1063
|
const busyRef = useRef(busy);
|
|
809
1064
|
busyRef.current = busy;
|
|
810
1065
|
|
|
811
|
-
const
|
|
812
|
-
refreshTerminalTitle({
|
|
813
|
-
terminalTitleMode,
|
|
814
|
-
workspaceName: deriveTerminalTitle(workspaceRoot, "dir"),
|
|
815
|
-
force: true,
|
|
816
|
-
debugEventName,
|
|
817
|
-
busyState,
|
|
818
|
-
});
|
|
819
|
-
}, [terminalTitleMode, workspaceRoot]);
|
|
820
|
-
|
|
821
|
-
const writeCurrentTerminalTitleBeforeStateChange = useCallback((reason: string) => {
|
|
822
|
-
setIntendedTerminalTitle(deriveTerminalTitle(workspaceRoot, terminalTitleMode), {
|
|
823
|
-
force: true,
|
|
824
|
-
reason,
|
|
825
|
-
});
|
|
826
|
-
}, [terminalTitleMode, workspaceRoot]);
|
|
827
|
-
|
|
828
|
-
// Re-assert terminal title whenever the app transitions between busy states,
|
|
829
|
-
// starts/stops streaming, or executes tools to recover from external overwrites.
|
|
1066
|
+
const prevBusyRef = useRef(busy);
|
|
830
1067
|
useEffect(() => {
|
|
831
|
-
if (!busy) {
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
workspaceName: deriveTerminalTitle(workspaceRoot, "dir"),
|
|
835
|
-
force: true,
|
|
836
|
-
debugEventName: "busy-end",
|
|
837
|
-
busyState: false,
|
|
838
|
-
});
|
|
839
|
-
return;
|
|
1068
|
+
if (!busy && prevBusyRef.current && screen === "main") {
|
|
1069
|
+
intendedFocusTargetRef.current = FOCUS_IDS.composer;
|
|
1070
|
+
focusManager.focus(FOCUS_IDS.composer);
|
|
840
1071
|
}
|
|
1072
|
+
prevBusyRef.current = busy;
|
|
1073
|
+
}, [busy, screen, focusManager]);
|
|
841
1074
|
|
|
842
|
-
refreshTerminalTitle({
|
|
843
|
-
terminalTitleMode,
|
|
844
|
-
workspaceName: deriveTerminalTitle(workspaceRoot, "dir"),
|
|
845
|
-
force: true,
|
|
846
|
-
debugEventName: "busy-start",
|
|
847
|
-
busyState: true,
|
|
848
|
-
});
|
|
849
|
-
|
|
850
|
-
const timer = setInterval(() => {
|
|
851
|
-
refreshTerminalTitle({
|
|
852
|
-
terminalTitleMode,
|
|
853
|
-
workspaceName: deriveTerminalTitle(workspaceRoot, "dir"),
|
|
854
|
-
force: true,
|
|
855
|
-
debugEventName: "busy-guard",
|
|
856
|
-
busyState: true,
|
|
857
|
-
});
|
|
858
|
-
}, 250);
|
|
859
|
-
|
|
860
|
-
return () => {
|
|
861
|
-
clearInterval(timer);
|
|
862
|
-
};
|
|
863
|
-
}, [uiState.kind, busy, terminalTitleMode, workspaceRoot]);
|
|
864
|
-
|
|
865
|
-
useEffect(() => {
|
|
866
|
-
setIntendedTerminalTitle(terminalTitleLabel, { force: true, reason: "terminal-title-label-change" });
|
|
867
|
-
}, [terminalTitleLabel]);
|
|
868
1075
|
const modelCapabilitiesBusyRef = useRef(modelCapabilitiesBusy);
|
|
869
1076
|
modelCapabilitiesBusyRef.current = modelCapabilitiesBusy;
|
|
870
1077
|
const composerRows = useMemo(() => {
|
|
@@ -898,9 +1105,59 @@ export function App({ launchArgs }: AppProps) {
|
|
|
898
1105
|
terminalLayout,
|
|
899
1106
|
uiState,
|
|
900
1107
|
]);
|
|
1108
|
+
const activeRootComponent = screen === "main" ? "TranscriptShell" : "AppShell";
|
|
1109
|
+
const startupHeaderMode = useMemo(
|
|
1110
|
+
() => resolveStartupHeaderMode({
|
|
1111
|
+
cols: terminalLayout.cols,
|
|
1112
|
+
rows: terminalLayout.rows,
|
|
1113
|
+
introRows: 8,
|
|
1114
|
+
composerRows,
|
|
1115
|
+
}),
|
|
1116
|
+
[composerRows, terminalLayout.cols, terminalLayout.rows],
|
|
1117
|
+
);
|
|
1118
|
+
const previousStartupTraceKeyRef = useRef<string | null>(null);
|
|
1119
|
+
|
|
1120
|
+
useEffect(() => {
|
|
1121
|
+
const nextKey = [
|
|
1122
|
+
terminalLayout.cols,
|
|
1123
|
+
terminalLayout.rows,
|
|
1124
|
+
terminalLayout.mode,
|
|
1125
|
+
activeRootComponent,
|
|
1126
|
+
screen,
|
|
1127
|
+
startupHeaderMode,
|
|
1128
|
+
staticEvents.length,
|
|
1129
|
+
activeEvents.length,
|
|
1130
|
+
uiState.kind,
|
|
1131
|
+
].join("|");
|
|
1132
|
+
if (previousStartupTraceKeyRef.current === nextKey) return;
|
|
1133
|
+
previousStartupTraceKeyRef.current = nextKey;
|
|
1134
|
+
renderDebug.traceEvent("startup", "state", {
|
|
1135
|
+
cols: terminalLayout.cols,
|
|
1136
|
+
rows: terminalLayout.rows,
|
|
1137
|
+
layoutMode: terminalLayout.mode,
|
|
1138
|
+
activeRoot: activeRootComponent,
|
|
1139
|
+
screen,
|
|
1140
|
+
startupHeaderMode,
|
|
1141
|
+
logoBranchSelected: startupHeaderMode === "large",
|
|
1142
|
+
staticEventsLength: staticEvents.length,
|
|
1143
|
+
activeEventsLength: activeEvents.length,
|
|
1144
|
+
uiStateKind: uiState.kind,
|
|
1145
|
+
});
|
|
1146
|
+
}, [
|
|
1147
|
+
activeEvents.length,
|
|
1148
|
+
activeRootComponent,
|
|
1149
|
+
screen,
|
|
1150
|
+
startupHeaderMode,
|
|
1151
|
+
staticEvents.length,
|
|
1152
|
+
terminalLayout.cols,
|
|
1153
|
+
terminalLayout.mode,
|
|
1154
|
+
terminalLayout.rows,
|
|
1155
|
+
uiState.kind,
|
|
1156
|
+
]);
|
|
901
1157
|
|
|
902
1158
|
renderDebug.useRenderDebug("Root", {
|
|
903
1159
|
screen,
|
|
1160
|
+
activeRoot: activeRootComponent,
|
|
904
1161
|
uiStateKind: uiState.kind,
|
|
905
1162
|
staticEvents,
|
|
906
1163
|
activeEvents,
|
|
@@ -909,8 +1166,11 @@ export function App({ launchArgs }: AppProps) {
|
|
|
909
1166
|
cursor,
|
|
910
1167
|
busy,
|
|
911
1168
|
composerRows,
|
|
1169
|
+
startupHeaderMode,
|
|
1170
|
+
logoBranchSelected: startupHeaderMode === "large",
|
|
912
1171
|
cols: terminalLayout.cols,
|
|
913
1172
|
rows: terminalLayout.rows,
|
|
1173
|
+
layoutMode: terminalLayout.mode,
|
|
914
1174
|
layoutEpoch: terminalLayout.layoutEpoch,
|
|
915
1175
|
planFlowKind: planFlow.kind,
|
|
916
1176
|
mode,
|
|
@@ -1189,13 +1449,12 @@ export function App({ launchArgs }: AppProps) {
|
|
|
1189
1449
|
useEffect(() => {
|
|
1190
1450
|
const notice = providerWorkspaceConfig.migrationNotice;
|
|
1191
1451
|
if (!notice || providerMigrationNoticeShownRef.current) return;
|
|
1192
|
-
providerMigrationNoticeShownRef.current = true;
|
|
1193
1452
|
const providerLabel = findProvider(providerRegistry, notice.revertedProviderId)?.displayName ?? "OpenAI";
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
);
|
|
1198
|
-
}, [
|
|
1453
|
+
const event = createProviderMigrationNoticeEvent(notice, providerLabel);
|
|
1454
|
+
if (!event) return;
|
|
1455
|
+
providerMigrationNoticeShownRef.current = true;
|
|
1456
|
+
appendStaticEvent(event);
|
|
1457
|
+
}, [appendStaticEvent, providerRegistry, providerWorkspaceConfig.migrationNotice]);
|
|
1199
1458
|
|
|
1200
1459
|
useEffect(() => {
|
|
1201
1460
|
if (projectInstructionsLoad.status === "loaded") {
|
|
@@ -1328,6 +1587,9 @@ export function App({ launchArgs }: AppProps) {
|
|
|
1328
1587
|
providerRouteErrorsRef.current["anthropic"] = result.message ?? ANTHROPIC_ROUTE_SETUP_MESSAGE;
|
|
1329
1588
|
} else {
|
|
1330
1589
|
delete providerRouteErrorsRef.current["anthropic"];
|
|
1590
|
+
// Persist the freshly discovered catalog so newly released Claude
|
|
1591
|
+
// models replace the stale on-disk cache without a manual refresh.
|
|
1592
|
+
persistProviderDiscovery(discoverProviderModels("anthropic"));
|
|
1331
1593
|
}
|
|
1332
1594
|
} catch {
|
|
1333
1595
|
// Best-effort probe — failures are surfaced only when the user activates the route.
|
|
@@ -1343,6 +1605,7 @@ export function App({ launchArgs }: AppProps) {
|
|
|
1343
1605
|
useEffect(() => {
|
|
1344
1606
|
void (async () => {
|
|
1345
1607
|
try {
|
|
1608
|
+
markProviderAvailability("local", "checking", "startup-probe");
|
|
1346
1609
|
const result = await checkLocalProvider({ override: providerWorkspaceConfig.providers?.local });
|
|
1347
1610
|
if (result.diagnostics) {
|
|
1348
1611
|
providerDiagnosticsRef.current["local"] = result.diagnostics as Record<string, string | number | boolean | null>;
|
|
@@ -1362,13 +1625,6 @@ export function App({ launchArgs }: AppProps) {
|
|
|
1362
1625
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1363
1626
|
}, [workspaceRoot]);
|
|
1364
1627
|
|
|
1365
|
-
useEffect(() => {
|
|
1366
|
-
const devLaunchNotice = buildDevLaunchNotice(launchContext);
|
|
1367
|
-
if (!devLaunchNotice) return;
|
|
1368
|
-
|
|
1369
|
-
appendSystemEvent("Launch mode", devLaunchNotice);
|
|
1370
|
-
}, [appendSystemEvent, launchContext]);
|
|
1371
|
-
|
|
1372
1628
|
// Non-blocking background update check — runs once at startup.
|
|
1373
1629
|
useEffect(() => {
|
|
1374
1630
|
const ucSettings = initialSettings.current.updateCheck ?? DEFAULT_UPDATE_CHECK_SETTINGS;
|
|
@@ -1619,11 +1875,6 @@ export function App({ launchArgs }: AppProps) {
|
|
|
1619
1875
|
}
|
|
1620
1876
|
setScreen("main");
|
|
1621
1877
|
appendSystemEvent("Reasoning updated", `Reasoning level is now ${formatReasoningLabel(nextReasoningLevel)}.`);
|
|
1622
|
-
refreshTerminalTitle({
|
|
1623
|
-
terminalTitleMode,
|
|
1624
|
-
workspaceName: deriveTerminalTitle(workspaceRoot, "dir"),
|
|
1625
|
-
force: true,
|
|
1626
|
-
});
|
|
1627
1878
|
}, [
|
|
1628
1879
|
activeProviderRoute.backendKind,
|
|
1629
1880
|
activeProviderRoute.modelId,
|
|
@@ -1638,9 +1889,7 @@ export function App({ launchArgs }: AppProps) {
|
|
|
1638
1889
|
persistActiveRoute,
|
|
1639
1890
|
persistProviderDefaultModelAndReasoning,
|
|
1640
1891
|
providerWorkspaceConfig.activeRoute,
|
|
1641
|
-
terminalTitleMode,
|
|
1642
1892
|
updateRuntimeConfig,
|
|
1643
|
-
workspaceRoot,
|
|
1644
1893
|
]);
|
|
1645
1894
|
|
|
1646
1895
|
const setPlanModeWithNotice = useCallback((nextEnabled: boolean) => {
|
|
@@ -1718,11 +1967,6 @@ export function App({ launchArgs }: AppProps) {
|
|
|
1718
1967
|
"Model updated",
|
|
1719
1968
|
`Active model is now ${nextModel}. Reasoning set to ${formatReasoningLabel(normalizedReasoning)}.`,
|
|
1720
1969
|
);
|
|
1721
|
-
refreshTerminalTitle({
|
|
1722
|
-
terminalTitleMode,
|
|
1723
|
-
workspaceName: deriveTerminalTitle(workspaceRoot, "dir"),
|
|
1724
|
-
force: true,
|
|
1725
|
-
});
|
|
1726
1970
|
} catch (error) {
|
|
1727
1971
|
const message = error instanceof Error ? error.message : String(error);
|
|
1728
1972
|
traceInputDebug("model_selection_app_failure", getInputDebugSnapshot({
|
|
@@ -1771,6 +2015,12 @@ export function App({ launchArgs }: AppProps) {
|
|
|
1771
2015
|
}
|
|
1772
2016
|
|
|
1773
2017
|
modelSelectionInFlightRef.current = true;
|
|
2018
|
+
// Reflect the target provider in the model picker immediately: route
|
|
2019
|
+
// validation below can take seconds (subprocess probes), and until it
|
|
2020
|
+
// persists the new active route, modelPickerProviderId would otherwise
|
|
2021
|
+
// keep resolving to the stale route.
|
|
2022
|
+
setPendingRouteProviderId(providerId);
|
|
2023
|
+
setRouteSwitchBusy(true);
|
|
1774
2024
|
traceInputDebug("model_selection_app_start", getInputDebugSnapshot({
|
|
1775
2025
|
handler: "setModelAndReasoningWithNotice",
|
|
1776
2026
|
model: nextModel,
|
|
@@ -1779,11 +2029,11 @@ export function App({ launchArgs }: AppProps) {
|
|
|
1779
2029
|
|
|
1780
2030
|
try {
|
|
1781
2031
|
const normalizedReasoning = normalizeReasoningForModelCapabilities(nextModel, nextReasoning, routeCapabilities);
|
|
1782
|
-
const releaseTitleGuard = acquireTerminalTitleGuard(500, () => {
|
|
1783
|
-
forceRefreshCurrentTerminalTitle("provider_validation_active", true);
|
|
1784
|
-
});
|
|
1785
2032
|
let validation;
|
|
1786
2033
|
try {
|
|
2034
|
+
if (providerId === "local") {
|
|
2035
|
+
markProviderAvailability("local", "checking", "provider-validation");
|
|
2036
|
+
}
|
|
1787
2037
|
validation = await validateProviderRouteActivation({
|
|
1788
2038
|
route: {
|
|
1789
2039
|
providerId,
|
|
@@ -1802,8 +2052,7 @@ export function App({ launchArgs }: AppProps) {
|
|
|
1802
2052
|
}
|
|
1803
2053
|
setRegistryNonce((n) => n + 1);
|
|
1804
2054
|
} finally {
|
|
1805
|
-
|
|
1806
|
-
forceRefreshCurrentTerminalTitle("after_provider_validation", false);
|
|
2055
|
+
// Runtime status is rendered from provider diagnostics; no title guard is active here.
|
|
1807
2056
|
}
|
|
1808
2057
|
if (validation.status !== "ready") {
|
|
1809
2058
|
traceInputDebug("model_selection_app_failure", getInputDebugSnapshot({
|
|
@@ -1841,6 +2090,10 @@ export function App({ launchArgs }: AppProps) {
|
|
|
1841
2090
|
model: nextModel,
|
|
1842
2091
|
reasoningLevel: normalizedReasoning,
|
|
1843
2092
|
}));
|
|
2093
|
+
if (providerId === "mistral" && activeProviderRoute.providerId !== "mistral") {
|
|
2094
|
+
// Switching to Vibe starts a fresh CLI conversation instead of resuming a stale one.
|
|
2095
|
+
resetMistralVibeSession(workspaceRoot);
|
|
2096
|
+
}
|
|
1844
2097
|
persistActiveRoute(providerId, nextModel, normalizedReasoning, validation.backendKind, geminiSelection);
|
|
1845
2098
|
setPendingRouteProviderId(null);
|
|
1846
2099
|
traceInputDebug("model_selection_app_success", getInputDebugSnapshot({
|
|
@@ -1850,11 +2103,6 @@ export function App({ launchArgs }: AppProps) {
|
|
|
1850
2103
|
}));
|
|
1851
2104
|
|
|
1852
2105
|
// Route changes are reflected reactively in the BottomComposer metadata row.
|
|
1853
|
-
refreshTerminalTitle({
|
|
1854
|
-
terminalTitleMode,
|
|
1855
|
-
workspaceName: deriveTerminalTitle(workspaceRoot, "dir"),
|
|
1856
|
-
force: true,
|
|
1857
|
-
});
|
|
1858
2106
|
} catch (error) {
|
|
1859
2107
|
const message = error instanceof Error ? error.message : String(error);
|
|
1860
2108
|
traceInputDebug("model_selection_app_failure", getInputDebugSnapshot({
|
|
@@ -1863,13 +2111,14 @@ export function App({ launchArgs }: AppProps) {
|
|
|
1863
2111
|
reasoning: nextReasoning,
|
|
1864
2112
|
error: message,
|
|
1865
2113
|
}));
|
|
2114
|
+
setPendingRouteProviderId(null);
|
|
1866
2115
|
appendErrorEvent("Model selection failed", message);
|
|
1867
|
-
forceRefreshCurrentTerminalTitle("model_selection_failed", false);
|
|
1868
2116
|
} finally {
|
|
2117
|
+
setRouteSwitchBusy(false);
|
|
1869
2118
|
modelSelectionInFlightRef.current = false;
|
|
1870
2119
|
returnToChatMode("selection");
|
|
1871
2120
|
}
|
|
1872
|
-
}, [activeProviderRoute.modelId, activeProviderRoute.providerId, activeRouteProvider, appendErrorEvent, appendSystemEvent, busy, getInputDebugSnapshot, modelCapabilities, persistActiveRoute, providerWorkspaceConfig.providers, returnToChatMode, runtimeConfig.geminiCommandPath, updateRuntimeConfig, workspaceRoot]);
|
|
2121
|
+
}, [activeProviderRoute.modelId, activeProviderRoute.providerId, activeRouteProvider, appendErrorEvent, appendSystemEvent, busy, getInputDebugSnapshot, markProviderAvailability, modelCapabilities, persistActiveRoute, providerWorkspaceConfig.providers, returnToChatMode, runtimeConfig.geminiCommandPath, updateRuntimeConfig, workspaceRoot]);
|
|
1873
2122
|
|
|
1874
2123
|
const setAuthPreferenceWithNotice = useCallback((nextPreference: AuthPreference) => {
|
|
1875
2124
|
setAuthPreference(nextPreference);
|
|
@@ -1898,11 +2147,6 @@ export function App({ launchArgs }: AppProps) {
|
|
|
1898
2147
|
}
|
|
1899
2148
|
if (nextSettings.terminalTitleMode !== terminalTitleMode) {
|
|
1900
2149
|
setTerminalTitleMode(nextSettings.terminalTitleMode);
|
|
1901
|
-
refreshTerminalTitle({
|
|
1902
|
-
terminalTitleMode: nextSettings.terminalTitleMode,
|
|
1903
|
-
workspaceName: deriveTerminalTitle(workspaceRoot, "dir"),
|
|
1904
|
-
force: true,
|
|
1905
|
-
});
|
|
1906
2150
|
}
|
|
1907
2151
|
const nextShowBusyLoader = parseBusyLoaderSettingValue(nextSettings.showBusyLoader);
|
|
1908
2152
|
if (nextShowBusyLoader !== showBusyLoader) {
|
|
@@ -2059,7 +2303,11 @@ export function App({ launchArgs }: AppProps) {
|
|
|
2059
2303
|
}, [appendSystemEvent, busy]);
|
|
2060
2304
|
|
|
2061
2305
|
const openProviderPicker = useCallback(() => {
|
|
2062
|
-
|
|
2306
|
+
// Mid route switch, keep the pending id so initialProviderId highlights
|
|
2307
|
+
// the provider being activated instead of the stale route.
|
|
2308
|
+
if (!modelSelectionInFlightRef.current) {
|
|
2309
|
+
setPendingRouteProviderId(null);
|
|
2310
|
+
}
|
|
2063
2311
|
const gate = guardConfigMutation("backend", busy);
|
|
2064
2312
|
if (!gate.allowed) {
|
|
2065
2313
|
appendSystemEvent("Busy", gate.message ?? "Finish the current run before opening providers.");
|
|
@@ -2067,6 +2315,32 @@ export function App({ launchArgs }: AppProps) {
|
|
|
2067
2315
|
}
|
|
2068
2316
|
|
|
2069
2317
|
setScreen("provider-picker");
|
|
2318
|
+
providerDiagnosticsRef.current.mistral = {
|
|
2319
|
+
...providerDiagnosticsRef.current.mistral,
|
|
2320
|
+
selectedModel: detectVibeActiveModel({ cwd: workspaceRoot }).modelId,
|
|
2321
|
+
availabilityStatus: "checking",
|
|
2322
|
+
};
|
|
2323
|
+
void resolveVibeExecutable({ cwd: workspaceRoot }).then((resolvedCommand) => {
|
|
2324
|
+
if (!isMountedRef.current) return;
|
|
2325
|
+
const modelDetection = detectVibeActiveModel({ cwd: workspaceRoot });
|
|
2326
|
+
providerDiagnosticsRef.current.mistral = {
|
|
2327
|
+
resolvedCommand,
|
|
2328
|
+
selectedModel: modelDetection.modelId,
|
|
2329
|
+
modelSource: modelDetection.source,
|
|
2330
|
+
configPath: modelDetection.configPath,
|
|
2331
|
+
availabilityStatus: resolvedCommand ? "available" : "unavailable",
|
|
2332
|
+
};
|
|
2333
|
+
setRegistryNonce((n) => n + 1);
|
|
2334
|
+
}).catch(() => {
|
|
2335
|
+
if (!isMountedRef.current) return;
|
|
2336
|
+
providerDiagnosticsRef.current.mistral = {
|
|
2337
|
+
selectedModel: detectVibeActiveModel({ cwd: workspaceRoot }).modelId,
|
|
2338
|
+
resolvedCommand: null,
|
|
2339
|
+
availabilityStatus: "unavailable",
|
|
2340
|
+
};
|
|
2341
|
+
setRegistryNonce((n) => n + 1);
|
|
2342
|
+
});
|
|
2343
|
+
markProviderAvailability("local", "checking", "provider-picker-open");
|
|
2070
2344
|
void checkLocalProvider({ override: providerWorkspaceConfig.providers?.local }).then((result) => {
|
|
2071
2345
|
if (!isMountedRef.current) return;
|
|
2072
2346
|
if (result.diagnostics) {
|
|
@@ -2079,7 +2353,7 @@ export function App({ launchArgs }: AppProps) {
|
|
|
2079
2353
|
}
|
|
2080
2354
|
setRegistryNonce((n) => n + 1);
|
|
2081
2355
|
}).catch(() => undefined);
|
|
2082
|
-
}, [appendSystemEvent, busy, providerWorkspaceConfig.providers]);
|
|
2356
|
+
}, [appendSystemEvent, busy, markProviderAvailability, providerWorkspaceConfig.providers, workspaceRoot]);
|
|
2083
2357
|
|
|
2084
2358
|
const setWorkspaceDefaultProviderWithNotice = useCallback((providerId: ProviderId) => {
|
|
2085
2359
|
const provider = findProvider(providerRegistry, providerId);
|
|
@@ -2096,7 +2370,9 @@ export function App({ launchArgs }: AppProps) {
|
|
|
2096
2370
|
const routeConfigured = isProviderRouteConfigured(providerId);
|
|
2097
2371
|
appendSystemEvent(
|
|
2098
2372
|
"Provider default updated",
|
|
2099
|
-
provider.routeMode === "
|
|
2373
|
+
provider.routeMode === "launch-only"
|
|
2374
|
+
? `${provider.displayName} is now the workspace default external CLI. Active chat route remains ${activeRouteProvider?.displayName ?? "OpenAI"} / ${model}.`
|
|
2375
|
+
: provider.routeMode === "in-codexa" && routeConfigured
|
|
2100
2376
|
? `${provider.displayName} is now the workspace default provider. Active chat route remains ${activeRouteProvider?.displayName ?? "OpenAI"} / ${model}.`
|
|
2101
2377
|
: `${provider.displayName} is set as the workspace default, but in-Codexa routing is not configured yet. Active chat route remains ${activeRouteProvider?.displayName ?? "OpenAI"} / ${model}.`,
|
|
2102
2378
|
);
|
|
@@ -2138,6 +2414,7 @@ export function App({ launchArgs }: AppProps) {
|
|
|
2138
2414
|
|
|
2139
2415
|
if (providerId === "local") {
|
|
2140
2416
|
appendSystemEvent("Model discovery", "Refreshing LM Studio metadata...");
|
|
2417
|
+
markProviderAvailability("local", "checking", "use-in-codexa");
|
|
2141
2418
|
void checkLocalProvider({ override: providerWorkspaceConfig.providers?.local }).then((validation) => {
|
|
2142
2419
|
if (!isMountedRef.current) return;
|
|
2143
2420
|
if (validation.diagnostics) {
|
|
@@ -2247,10 +2524,14 @@ export function App({ launchArgs }: AppProps) {
|
|
|
2247
2524
|
appendSystemEvent("Model discovery", providerId === "anthropic"
|
|
2248
2525
|
? "Refreshing Claude capabilities..."
|
|
2249
2526
|
: `Refreshing models for ${provider.displayName}...`);
|
|
2527
|
+
if (providerId === "local") {
|
|
2528
|
+
markProviderAvailability("local", "checking", "refresh-models");
|
|
2529
|
+
}
|
|
2250
2530
|
void runtime.refreshModels({
|
|
2251
2531
|
cwd: workspaceRoot,
|
|
2252
2532
|
localConfig: providerId === "local" ? providerWorkspaceConfig.providers?.local : undefined,
|
|
2253
2533
|
}).then((discovery) => {
|
|
2534
|
+
persistProviderDiscovery(discovery);
|
|
2254
2535
|
if (discovery.diagnostics) {
|
|
2255
2536
|
providerDiagnosticsRef.current[providerId] = discovery.diagnostics as Record<string, string | number | boolean | null>;
|
|
2256
2537
|
}
|
|
@@ -2342,10 +2623,10 @@ export function App({ launchArgs }: AppProps) {
|
|
|
2342
2623
|
setScreen("main");
|
|
2343
2624
|
appendSystemEvent(
|
|
2344
2625
|
"Provider launch",
|
|
2345
|
-
`Suspending Codexa and launching ${provider.displayName}. Codexa will resume when the external CLI exits.`,
|
|
2626
|
+
`Suspending Codexa and launching ${provider.displayName}${providerId === "mistral" ? ` / ${provider.currentModel}` : ""}. Codexa will resume when the external CLI exits.`,
|
|
2346
2627
|
);
|
|
2347
2628
|
|
|
2348
|
-
|
|
2629
|
+
const launchOptions = {
|
|
2349
2630
|
cwd: workspaceRoot,
|
|
2350
2631
|
stdin,
|
|
2351
2632
|
beforeLaunch: () => {
|
|
@@ -2353,12 +2634,20 @@ export function App({ launchArgs }: AppProps) {
|
|
|
2353
2634
|
stdout.write("\n");
|
|
2354
2635
|
},
|
|
2355
2636
|
afterLaunch: () => {
|
|
2356
|
-
terminalControl.setMouseReporting(
|
|
2357
|
-
forceRefreshCurrentTerminalTitle("provider_launch_return", false);
|
|
2637
|
+
terminalControl.setMouseReporting(effectiveMouseCapture, "src/app.tsx:providerLaunch.restoreMouse");
|
|
2358
2638
|
},
|
|
2359
|
-
}
|
|
2639
|
+
};
|
|
2640
|
+
const launchPromise = providerId === "mistral"
|
|
2641
|
+
? launchMistralVibeCli(provider, launchOptions)
|
|
2642
|
+
: launchProviderCli(provider, launchOptions);
|
|
2643
|
+
|
|
2644
|
+
void launchPromise.then((result) => {
|
|
2360
2645
|
if (!isMountedRef.current) return;
|
|
2361
|
-
|
|
2646
|
+
if (providerId === "mistral" && (result.status === "missing-command" || result.status === "spawn-error")) {
|
|
2647
|
+
appendErrorEvent("Mistral Vibe launch failed", result.message);
|
|
2648
|
+
} else {
|
|
2649
|
+
appendSystemEvent("Provider launch", result.message);
|
|
2650
|
+
}
|
|
2362
2651
|
}).catch((error) => {
|
|
2363
2652
|
if (!isMountedRef.current) return;
|
|
2364
2653
|
const message = error instanceof Error ? error.message : "Provider launch failed.";
|
|
@@ -2368,10 +2657,10 @@ export function App({ launchArgs }: AppProps) {
|
|
|
2368
2657
|
activeProviderRoute,
|
|
2369
2658
|
appendErrorEvent,
|
|
2370
2659
|
appendSystemEvent,
|
|
2371
|
-
|
|
2372
|
-
mouseCapture,
|
|
2660
|
+
effectiveMouseCapture,
|
|
2373
2661
|
providerRegistry,
|
|
2374
2662
|
providerWorkspaceConfig.providers,
|
|
2663
|
+
markProviderAvailability,
|
|
2375
2664
|
modelCapabilities,
|
|
2376
2665
|
reasoningLevel,
|
|
2377
2666
|
refreshModelCapabilities,
|
|
@@ -2385,7 +2674,11 @@ export function App({ launchArgs }: AppProps) {
|
|
|
2385
2674
|
]);
|
|
2386
2675
|
|
|
2387
2676
|
const openModelPicker = useCallback(() => {
|
|
2388
|
-
|
|
2677
|
+
// While a route switch is validating, keep the pending id so the picker
|
|
2678
|
+
// shows the target provider instead of snapping back to the stale route.
|
|
2679
|
+
if (!modelSelectionInFlightRef.current) {
|
|
2680
|
+
setPendingRouteProviderId(null);
|
|
2681
|
+
}
|
|
2389
2682
|
traceInputDebug("model_picker_open_request", getInputDebugSnapshot({
|
|
2390
2683
|
handler: "openModelPicker",
|
|
2391
2684
|
currentScreen: screen,
|
|
@@ -2573,6 +2866,18 @@ export function App({ launchArgs }: AppProps) {
|
|
|
2573
2866
|
dispatchSession({ type: "RESET_INPUT" });
|
|
2574
2867
|
}, [dispatchSession]);
|
|
2575
2868
|
|
|
2869
|
+
const resetToHomeScreen = useCallback((seedEvents: TimelineEvent[] = []) => {
|
|
2870
|
+
dispatchSession({
|
|
2871
|
+
type: "CLEAR_TRANSCRIPT",
|
|
2872
|
+
seedEvents,
|
|
2873
|
+
});
|
|
2874
|
+
setConversationChars(0);
|
|
2875
|
+
setScreen("main");
|
|
2876
|
+
resetComposer();
|
|
2877
|
+
intendedFocusTargetRef.current = FOCUS_IDS.composer;
|
|
2878
|
+
focusManager.focus(FOCUS_IDS.composer);
|
|
2879
|
+
}, [dispatchSession, focusManager, resetComposer]);
|
|
2880
|
+
|
|
2576
2881
|
const finalizePromptRun = useCallback((
|
|
2577
2882
|
runId: number,
|
|
2578
2883
|
turnId: number,
|
|
@@ -2884,23 +3189,37 @@ export function App({ launchArgs }: AppProps) {
|
|
|
2884
3189
|
}, [dispatchSession]);
|
|
2885
3190
|
|
|
2886
3191
|
const handleClear = useCallback(() => {
|
|
2887
|
-
|
|
3192
|
+
const clearGeneration = sessionState.clearEpoch + 1;
|
|
3193
|
+
const clearBoundaryArmed = clearFrameBoundaryController?.beginClearGeneration(clearGeneration) ?? false;
|
|
3194
|
+
renderDebug.traceEvent("terminal", "clearCommandReceived", {
|
|
3195
|
+
clearGeneration,
|
|
3196
|
+
clearPending: clearBoundaryArmed,
|
|
3197
|
+
liveInkInstanceResolved: Boolean(inkInstance),
|
|
3198
|
+
});
|
|
2888
3199
|
cancelActiveRun(false);
|
|
2889
3200
|
activeTurnIdRef.current = null;
|
|
2890
3201
|
activeRunLifecycleRef.current = null;
|
|
2891
3202
|
activeRunTimingRef.current = null;
|
|
3203
|
+
resetMistralVibeSession(workspaceRoot);
|
|
2892
3204
|
setPlanFlow(resetPlanFlow());
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
resetComposer();
|
|
2897
|
-
terminalControl.clearTranscript("src/app.tsx:handleClear");
|
|
2898
|
-
refreshTerminalTitle({
|
|
2899
|
-
terminalTitleMode,
|
|
2900
|
-
workspaceName: deriveTerminalTitle(workspaceRoot, "dir"),
|
|
2901
|
-
force: true,
|
|
3205
|
+
renderDebug.traceEvent("terminal", "clearReactStateRequested", {
|
|
3206
|
+
clearGeneration,
|
|
3207
|
+
clearPending: clearBoundaryArmed,
|
|
2902
3208
|
});
|
|
2903
|
-
|
|
3209
|
+
resetToHomeScreen(createStartupStaticEvents({
|
|
3210
|
+
launchContext,
|
|
3211
|
+
providerWorkspaceConfig,
|
|
3212
|
+
}));
|
|
3213
|
+
if (!clearBoundaryArmed) {
|
|
3214
|
+
// Fallback path (unexpected Ink mismatch): preserve /clear semantics.
|
|
3215
|
+
terminalControl.clearTranscript("src/app.tsx:handleClear:fallback");
|
|
3216
|
+
resetInkOutputForFreshFrame({ instance: inkInstance, columns: stdout.columns });
|
|
3217
|
+
renderDebug.traceEvent("terminal", "clearBoundaryFallback", {
|
|
3218
|
+
clearGeneration,
|
|
3219
|
+
liveInkInstanceResolved: Boolean(inkInstance),
|
|
3220
|
+
});
|
|
3221
|
+
}
|
|
3222
|
+
}, [cancelActiveRun, clearFrameBoundaryController, inkInstance, launchContext, providerWorkspaceConfig, resetToHomeScreen, sessionState.clearEpoch, stdout.columns, terminalControl, workspaceRoot]);
|
|
2904
3223
|
|
|
2905
3224
|
const handleShellExecute = useCallback((command: string) => {
|
|
2906
3225
|
const safeCommand = sanitizeTerminalInput(command).trim();
|
|
@@ -2912,7 +3231,6 @@ export function App({ launchArgs }: AppProps) {
|
|
|
2912
3231
|
|
|
2913
3232
|
const shellId = createEventId();
|
|
2914
3233
|
const startTime = Date.now();
|
|
2915
|
-
writeCurrentTerminalTitleBeforeStateChange("before-shell-start");
|
|
2916
3234
|
|
|
2917
3235
|
const initialEvent: ShellEvent = {
|
|
2918
3236
|
id: shellId,
|
|
@@ -2975,9 +3293,6 @@ export function App({ launchArgs }: AppProps) {
|
|
|
2975
3293
|
safeCommand,
|
|
2976
3294
|
{ cwd: workspaceRoot },
|
|
2977
3295
|
{
|
|
2978
|
-
onProcessLifecycle: (event) => {
|
|
2979
|
-
reassertIntendedTerminalTitle({ reason: `shell-process-${event}` });
|
|
2980
|
-
},
|
|
2981
3296
|
onStdout: (text) => {
|
|
2982
3297
|
const lines = sanitizeTerminalLines(text.split(/\r?\n/));
|
|
2983
3298
|
if (lines.length > 0) {
|
|
@@ -3021,9 +3336,8 @@ export function App({ launchArgs }: AppProps) {
|
|
|
3021
3336
|
};
|
|
3022
3337
|
|
|
3023
3338
|
dispatchSession({ type: "FINALIZE_SHELL", shellId, finalEvent });
|
|
3024
|
-
forceRefreshCurrentTerminalTitle("shell_output_complete", false);
|
|
3025
3339
|
});
|
|
3026
|
-
}, [allowedWritableRoots, appendErrorEvent, dispatchSession, focusManager,
|
|
3340
|
+
}, [allowedWritableRoots, appendErrorEvent, dispatchSession, focusManager, workspaceRoot]);
|
|
3027
3341
|
|
|
3028
3342
|
const handleWorkspaceRelaunch = useCallback((targetPath: string) => {
|
|
3029
3343
|
const gate = guardWorkspaceRelaunch(busy);
|
|
@@ -3156,7 +3470,6 @@ export function App({ launchArgs }: AppProps) {
|
|
|
3156
3470
|
setConversationChars((count) => count + safeProviderPrompt.length);
|
|
3157
3471
|
|
|
3158
3472
|
const runId = createEventId();
|
|
3159
|
-
writeCurrentTerminalTitleBeforeStateChange("before-prompt-run-start");
|
|
3160
3473
|
perf.startSession(String(runId));
|
|
3161
3474
|
perf.mark("dispatch_start");
|
|
3162
3475
|
perf.setMeta("fast_cleanup", fastCleanupRun);
|
|
@@ -3283,9 +3596,6 @@ export function App({ launchArgs }: AppProps) {
|
|
|
3283
3596
|
safeProviderPrompt,
|
|
3284
3597
|
{ runtime: runtimeForTurn, workspaceRoot, projectInstructions },
|
|
3285
3598
|
{
|
|
3286
|
-
onProcessLifecycle: (event) => {
|
|
3287
|
-
reassertIntendedTerminalTitle({ reason: `codex-process-${event}` });
|
|
3288
|
-
},
|
|
3289
3599
|
onAssistantDelta: (chunk) => {
|
|
3290
3600
|
const geminiBoundary = activeProviderRoute.providerId === "google";
|
|
3291
3601
|
appDiagLog(`onAssistantDelta: provider=${activeProviderRoute.providerId} chunk.length=${chunk?.length ?? 0} isEmpty=${!chunk}`);
|
|
@@ -3348,7 +3658,6 @@ export function App({ launchArgs }: AppProps) {
|
|
|
3348
3658
|
if (activity.status === "running") {
|
|
3349
3659
|
return;
|
|
3350
3660
|
}
|
|
3351
|
-
forceRefreshCurrentTerminalTitle("tool_activity_complete", true);
|
|
3352
3661
|
if (fastCleanupRun && !blockedCleanupFailureSurfaced) {
|
|
3353
3662
|
const blockedCleanupFailure = getBlockedCleanupFailure(activity);
|
|
3354
3663
|
if (blockedCleanupFailure) {
|
|
@@ -3408,7 +3717,6 @@ export function App({ launchArgs }: AppProps) {
|
|
|
3408
3717
|
const formatted = formatHollowResponse(hollow, safeResponse);
|
|
3409
3718
|
traceLiveRunDiagnostics("completed");
|
|
3410
3719
|
void finalizePromptRun(runId, turnId, "completed", undefined, formatted);
|
|
3411
|
-
forceRefreshCurrentTerminalTitle("prompt_run_completed", false);
|
|
3412
3720
|
return;
|
|
3413
3721
|
}
|
|
3414
3722
|
}
|
|
@@ -3446,7 +3754,6 @@ export function App({ launchArgs }: AppProps) {
|
|
|
3446
3754
|
}
|
|
3447
3755
|
traceLiveRunDiagnostics("completed");
|
|
3448
3756
|
void finalizePromptRun(runId, turnId, "completed", undefined, finalResponse);
|
|
3449
|
-
forceRefreshCurrentTerminalTitle("prompt_run_completed", false);
|
|
3450
3757
|
};
|
|
3451
3758
|
|
|
3452
3759
|
if (flushedLiveUpdates) {
|
|
@@ -3479,7 +3786,6 @@ export function App({ launchArgs }: AppProps) {
|
|
|
3479
3786
|
|
|
3480
3787
|
traceLiveRunDiagnostics("failed");
|
|
3481
3788
|
void finalizePromptRun(runId, turnId, "failed", errorMessage);
|
|
3482
|
-
forceRefreshCurrentTerminalTitle("prompt_run_failed", false);
|
|
3483
3789
|
};
|
|
3484
3790
|
|
|
3485
3791
|
if (flushedLiveUpdates) {
|
|
@@ -3535,8 +3841,6 @@ export function App({ launchArgs }: AppProps) {
|
|
|
3535
3841
|
appendSystemEvent,
|
|
3536
3842
|
authStatus.state,
|
|
3537
3843
|
finalizePromptRun,
|
|
3538
|
-
forceRefreshCurrentTerminalTitle,
|
|
3539
|
-
writeCurrentTerminalTitleBeforeStateChange,
|
|
3540
3844
|
mode,
|
|
3541
3845
|
provider,
|
|
3542
3846
|
projectInstructions,
|
|
@@ -3550,15 +3854,21 @@ export function App({ launchArgs }: AppProps) {
|
|
|
3550
3854
|
if (!pendingImport) return;
|
|
3551
3855
|
const replacements: Array<{ rawPath: string; workspaceRelativePath: string }> = [];
|
|
3552
3856
|
for (const file of pendingImport.files) {
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3857
|
+
try {
|
|
3858
|
+
const destPath = await importExternalFile(file.srcPath, pendingImport.attachmentsDir);
|
|
3859
|
+
if (destPath) {
|
|
3860
|
+
const relPath = path.relative(workspaceRoot, destPath).replace(/\\/g, "/");
|
|
3861
|
+
replacements.push({ rawPath: file.rawPath, workspaceRelativePath: relPath });
|
|
3862
|
+
}
|
|
3863
|
+
} catch (err: any) {
|
|
3864
|
+
appendErrorEvent("Import failed", `Could not import ${path.basename(file.srcPath)}: ${err.message}`);
|
|
3865
|
+
}
|
|
3556
3866
|
}
|
|
3557
3867
|
const rewrittenPrompt = rewritePromptWithImportedPaths(pendingImport.prompt, replacements);
|
|
3558
3868
|
setPendingImport(null);
|
|
3559
3869
|
setScreen("main");
|
|
3560
3870
|
startPromptRun(rewrittenPrompt, rewrittenPrompt, { submitTiming: createPromptRunTiming(), commitPrompt: true });
|
|
3561
|
-
}, [pendingImport, workspaceRoot, startPromptRun]);
|
|
3871
|
+
}, [pendingImport, workspaceRoot, startPromptRun, appendErrorEvent]);
|
|
3562
3872
|
|
|
3563
3873
|
const handleImportCancel = useCallback(() => {
|
|
3564
3874
|
if (!pendingImport) return;
|
|
@@ -3944,8 +4254,13 @@ export function App({ launchArgs }: AppProps) {
|
|
|
3944
4254
|
case "diagnose_providers": {
|
|
3945
4255
|
const lines: string[] = ["Provider CLI diagnostics:"];
|
|
3946
4256
|
const diags = providerDiagnosticsRef.current;
|
|
3947
|
-
const providerIds = ["openai", "anthropic", "
|
|
3948
|
-
const labels: Record<string, string> = {
|
|
4257
|
+
const providerIds = ["openai", "anthropic", "local", "antigravity"] as const;
|
|
4258
|
+
const labels: Record<string, string> = {
|
|
4259
|
+
openai: "OpenAI/Codex",
|
|
4260
|
+
anthropic: "Anthropic/Claude",
|
|
4261
|
+
local: "Local OpenAI-compatible",
|
|
4262
|
+
antigravity: "Antigravity CLI",
|
|
4263
|
+
};
|
|
3949
4264
|
for (const id of providerIds) {
|
|
3950
4265
|
const diag = diags[id];
|
|
3951
4266
|
lines.push(`\n ${labels[id] ?? id}:`);
|
|
@@ -4052,8 +4367,8 @@ export function App({ launchArgs }: AppProps) {
|
|
|
4052
4367
|
appendSystemEvent(
|
|
4053
4368
|
"Mouse mode updated",
|
|
4054
4369
|
nextMouse
|
|
4055
|
-
? "
|
|
4056
|
-
: "
|
|
4370
|
+
? "Mouse preference set to wheel mode. Main chat still uses native terminal scrollback; SGR capture is not used for transcript scrolling."
|
|
4371
|
+
: "Mouse preference set to selection mode. Main chat uses native terminal scrollback and native drag-select.",
|
|
4057
4372
|
);
|
|
4058
4373
|
return;
|
|
4059
4374
|
}
|
|
@@ -4164,7 +4479,14 @@ export function App({ launchArgs }: AppProps) {
|
|
|
4164
4479
|
}
|
|
4165
4480
|
|
|
4166
4481
|
// Validate workspace access for normal prompts
|
|
4167
|
-
const outsideViolations = findOutsideWorkspacePaths(value, workspaceRoot, allowedWritableRoots);
|
|
4482
|
+
const { violations: outsideViolations, skippedExternalPaths } = findOutsideWorkspacePaths(value, workspaceRoot, allowedWritableRoots);
|
|
4483
|
+
|
|
4484
|
+
if (skippedExternalPaths.length > 0) {
|
|
4485
|
+
for (const skipped of skippedExternalPaths) {
|
|
4486
|
+
appendSystemEvent("Dependency skipped", `Skipped external dependency source: ${formatSkippedDependencyPath(skipped)}`);
|
|
4487
|
+
}
|
|
4488
|
+
}
|
|
4489
|
+
|
|
4168
4490
|
if (outsideViolations.length > 0) {
|
|
4169
4491
|
if (runtimeConfig.policy.allowExternalFileImport) {
|
|
4170
4492
|
const attachmentsDir = path.isAbsolute(runtimeConfig.policy.attachmentDir)
|
|
@@ -4254,19 +4576,19 @@ export function App({ launchArgs }: AppProps) {
|
|
|
4254
4576
|
workspaceRoot,
|
|
4255
4577
|
]);
|
|
4256
4578
|
|
|
4257
|
-
const modelDisplayName = activeRuntimeDisplay.modelDisplay;
|
|
4258
|
-
const composerReasoningLevel = "";
|
|
4259
|
-
const headerRuntimeSummary = useMemo(
|
|
4260
|
-
() => runtimeDisplayToSummary(activeRuntimeDisplay, runtimeSummary),
|
|
4261
|
-
[activeRuntimeDisplay, runtimeSummary],
|
|
4262
|
-
);
|
|
4263
|
-
const effectiveHeaderConfig = useMemo<HeaderConfig>(() => ({
|
|
4264
|
-
...headerConfig,
|
|
4265
|
-
showProvider: true,
|
|
4266
|
-
showModel: false,
|
|
4267
|
-
showReasoning: false,
|
|
4268
|
-
showContext: false,
|
|
4269
|
-
}), [headerConfig]);
|
|
4579
|
+
const modelDisplayName = activeRuntimeDisplay.modelDisplay;
|
|
4580
|
+
const composerReasoningLevel = "";
|
|
4581
|
+
const headerRuntimeSummary = useMemo(
|
|
4582
|
+
() => runtimeDisplayToSummary(activeRuntimeDisplay, runtimeSummary),
|
|
4583
|
+
[activeRuntimeDisplay, runtimeSummary],
|
|
4584
|
+
);
|
|
4585
|
+
const effectiveHeaderConfig = useMemo<HeaderConfig>(() => ({
|
|
4586
|
+
...headerConfig,
|
|
4587
|
+
showProvider: true,
|
|
4588
|
+
showModel: false,
|
|
4589
|
+
showReasoning: false,
|
|
4590
|
+
showContext: false,
|
|
4591
|
+
}), [headerConfig]);
|
|
4270
4592
|
|
|
4271
4593
|
// Memoize the composer element so AppShell's memo check (prev.composer ===
|
|
4272
4594
|
// next.composer) passes during streaming. Without this, a new JSX element is
|
|
@@ -4299,7 +4621,7 @@ export function App({ launchArgs }: AppProps) {
|
|
|
4299
4621
|
placeholder={planFlow.mode === "revise"
|
|
4300
4622
|
? "e.g. keep it to one file and add tests"
|
|
4301
4623
|
: "e.g. keep it minimal and avoid touching other files"}
|
|
4302
|
-
footerHint="Enter
|
|
4624
|
+
footerHint="Esc to close · Enter to confirm"
|
|
4303
4625
|
initialValue={initialRevisionText}
|
|
4304
4626
|
onSubmit={(value) => {
|
|
4305
4627
|
setInitialRevisionText("");
|
|
@@ -4313,14 +4635,14 @@ export function App({ launchArgs }: AppProps) {
|
|
|
4313
4635
|
<MemoizedBottomComposer
|
|
4314
4636
|
key={composerInstanceKey}
|
|
4315
4637
|
layout={terminalLayout}
|
|
4316
|
-
uiState={uiState}
|
|
4317
|
-
mode={mode}
|
|
4318
|
-
model={modelDisplayName}
|
|
4319
|
-
footerModelDisplay={activeRuntimeDisplay.footerModelDisplay}
|
|
4320
|
-
themeName={activeThemeName}
|
|
4321
|
-
reasoningLevel={composerReasoningLevel}
|
|
4322
|
-
contextDisplay={activeRuntimeDisplay.contextDisplay}
|
|
4323
|
-
planMode={planMode}
|
|
4638
|
+
uiState={uiState}
|
|
4639
|
+
mode={mode}
|
|
4640
|
+
model={modelDisplayName}
|
|
4641
|
+
footerModelDisplay={activeRuntimeDisplay.footerModelDisplay}
|
|
4642
|
+
themeName={activeThemeName}
|
|
4643
|
+
reasoningLevel={composerReasoningLevel}
|
|
4644
|
+
contextDisplay={activeRuntimeDisplay.contextDisplay}
|
|
4645
|
+
planMode={planMode}
|
|
4324
4646
|
showBusyLoader={showBusyLoader}
|
|
4325
4647
|
tokensUsed={estimateTokens(conversationChars)}
|
|
4326
4648
|
modelSpec={currentModelSpec}
|
|
@@ -4358,11 +4680,11 @@ export function App({ launchArgs }: AppProps) {
|
|
|
4358
4680
|
composerInstanceKey,
|
|
4359
4681
|
terminalLayout,
|
|
4360
4682
|
uiState,
|
|
4361
|
-
mode,
|
|
4362
|
-
modelDisplayName,
|
|
4363
|
-
activeRuntimeDisplay.footerModelDisplay,
|
|
4364
|
-
activeRuntimeDisplay.contextDisplay,
|
|
4365
|
-
activeThemeName,
|
|
4683
|
+
mode,
|
|
4684
|
+
modelDisplayName,
|
|
4685
|
+
activeRuntimeDisplay.footerModelDisplay,
|
|
4686
|
+
activeRuntimeDisplay.contextDisplay,
|
|
4687
|
+
activeThemeName,
|
|
4366
4688
|
composerReasoningLevel,
|
|
4367
4689
|
planMode,
|
|
4368
4690
|
showBusyLoader,
|
|
@@ -4385,21 +4707,17 @@ export function App({ launchArgs }: AppProps) {
|
|
|
4385
4707
|
togglePlanModeWithNotice,
|
|
4386
4708
|
handleClear,
|
|
4387
4709
|
cycleModeWithNotice,
|
|
4388
|
-
handleQuit,
|
|
4389
|
-
activeProviderRoute.providerId,
|
|
4390
|
-
sessionState.externalCliStatus,
|
|
4391
|
-
]);
|
|
4710
|
+
handleQuit,
|
|
4711
|
+
activeProviderRoute.providerId,
|
|
4712
|
+
sessionState.externalCliStatus,
|
|
4713
|
+
]);
|
|
4392
4714
|
|
|
4393
4715
|
// ─── Render ──────────────────────────────────────────────────────────────────
|
|
4394
4716
|
|
|
4395
|
-
// Plan review is shown inline in the Timeline, not as a separate overlay.
|
|
4396
|
-
const mainPanelElement = null;
|
|
4397
|
-
|
|
4398
4717
|
return (
|
|
4399
4718
|
<ThemeProvider theme={activeThemeName} customTheme={customTheme}>
|
|
4400
|
-
<
|
|
4719
|
+
<TranscriptShell
|
|
4401
4720
|
layout={terminalLayout}
|
|
4402
|
-
screen={screen}
|
|
4403
4721
|
authState={authStatus.state}
|
|
4404
4722
|
workspaceLabel={workspaceLabel}
|
|
4405
4723
|
workspaceRoot={workspaceRoot}
|
|
@@ -4408,25 +4726,44 @@ export function App({ launchArgs }: AppProps) {
|
|
|
4408
4726
|
activeEvents={activeEvents}
|
|
4409
4727
|
uiState={uiState}
|
|
4410
4728
|
verboseMode={verboseMode}
|
|
4411
|
-
mouseCapture={mouseCapture}
|
|
4412
|
-
onMouseActivity={resetMouseIdle}
|
|
4413
|
-
selectionProfile={selectionProfile}
|
|
4414
4729
|
clearCount={sessionState.clearCount}
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4730
|
+
repaintGeneration={staticRepaintGeneration}
|
|
4731
|
+
composer={composerElement}
|
|
4732
|
+
composerRows={composerRows}
|
|
4733
|
+
visible={screen === "main"}
|
|
4734
|
+
/>
|
|
4735
|
+
|
|
4736
|
+
{screen !== "main" && (
|
|
4737
|
+
<AppShell
|
|
4738
|
+
layout={terminalLayout}
|
|
4739
|
+
screen={screen}
|
|
4740
|
+
authState={authStatus.state}
|
|
4741
|
+
workspaceLabel={workspaceLabel}
|
|
4742
|
+
workspaceRoot={workspaceRoot}
|
|
4743
|
+
runtimeSummary={headerRuntimeSummary}
|
|
4744
|
+
staticEvents={staticEvents}
|
|
4745
|
+
activeEvents={activeEvents}
|
|
4746
|
+
uiState={uiState}
|
|
4747
|
+
verboseMode={verboseMode}
|
|
4748
|
+
mouseCapture={effectiveMouseCapture}
|
|
4749
|
+
onMouseActivity={resetMouseIdle}
|
|
4750
|
+
selectionProfile={selectionProfile}
|
|
4751
|
+
clearCount={sessionState.clearCount}
|
|
4752
|
+
headerConfig={effectiveHeaderConfig}
|
|
4753
|
+
updateAvailable={
|
|
4754
|
+
updateCheckResult?.status === "update-available" && updateCheckResult.latestVersion
|
|
4755
|
+
? { latestVersion: updateCheckResult.latestVersion, currentVersion: updateCheckResult.currentVersion }
|
|
4756
|
+
: null
|
|
4757
|
+
}
|
|
4758
|
+
panel={
|
|
4759
|
+
<>
|
|
4760
|
+
{screen === "backend-picker" && (
|
|
4761
|
+
<BackendPicker
|
|
4762
|
+
currentBackend={backend}
|
|
4763
|
+
onSelect={(value) => setBackendWithNotice(value as AvailableBackend)}
|
|
4764
|
+
onCancel={() => setScreen("main")}
|
|
4765
|
+
/>
|
|
4766
|
+
)}
|
|
4430
4767
|
|
|
4431
4768
|
{screen === "provider-picker" && (
|
|
4432
4769
|
<ProviderPicker
|
|
@@ -4448,7 +4785,8 @@ export function App({ launchArgs }: AppProps) {
|
|
|
4448
4785
|
currentModel={modelPickerCurrentModel}
|
|
4449
4786
|
currentReasoning={modelPickerCurrentReasoning}
|
|
4450
4787
|
activeProviderLabel={modelPickerProviderLabel}
|
|
4451
|
-
isLoading={
|
|
4788
|
+
isLoading={modelPickerModels.length === 0
|
|
4789
|
+
&& ((modelPickerProviderId === "openai" && modelCapabilitiesBusy) || routeSwitchBusy)}
|
|
4452
4790
|
emptyMessage={modelPickerEmptyMessage}
|
|
4453
4791
|
onSelect={(m, r, geminiSelection) => {
|
|
4454
4792
|
if (pendingRouteProviderId && pendingRouteProviderId !== activeProviderRoute.providerId) {
|
|
@@ -4579,7 +4917,7 @@ export function App({ launchArgs }: AppProps) {
|
|
|
4579
4917
|
subtitle="Enter an absolute path or a path relative to the locked workspace."
|
|
4580
4918
|
placeholder="relative\\or\\absolute\\path"
|
|
4581
4919
|
inputLabel="Path"
|
|
4582
|
-
footerHint="Enter
|
|
4920
|
+
footerHint="Esc to close · Enter to confirm"
|
|
4583
4921
|
onSubmit={(value) => {
|
|
4584
4922
|
if (!value.trim()) {
|
|
4585
4923
|
appendSystemEvent("Runtime policy", "Writable root path cannot be empty.");
|
|
@@ -4676,18 +5014,19 @@ export function App({ launchArgs }: AppProps) {
|
|
|
4676
5014
|
onSkipUntilNextVersion={handleSkipUpdateVersion}
|
|
4677
5015
|
/>
|
|
4678
5016
|
)}
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
5017
|
+
</>
|
|
5018
|
+
}
|
|
5019
|
+
mainPanel={null}
|
|
5020
|
+
mainPanelMode="viewport"
|
|
5021
|
+
composer={composerElement}
|
|
5022
|
+
composerRows={composerRows}
|
|
5023
|
+
panelHint={screen !== "model-picker" ? (
|
|
5024
|
+
<Box marginTop={1} paddingX={1}>
|
|
5025
|
+
<Text color={activeTheme.textDim}>Close the active panel with Esc to return to the composer.</Text>
|
|
5026
|
+
</Box>
|
|
5027
|
+
) : null}
|
|
5028
|
+
/>
|
|
5029
|
+
)}
|
|
4691
5030
|
</ThemeProvider>
|
|
4692
5031
|
);
|
|
4693
5032
|
}
|