@golba98/codexa 1.0.3 → 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 +17 -18
- package/bin/codexa.js +62 -144
- package/package.json +4 -3
- package/src/app.tsx +533 -275
- package/src/commands/handler.ts +2 -2
- 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/perf/renderDebug.ts +10 -6
- 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/{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 +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 +1 -1
- package/src/ui/AppShell.tsx +672 -706
- package/src/ui/AttachmentImportPanel.tsx +2 -2
- package/src/ui/BottomComposer.tsx +145 -144
- package/src/ui/ModelPickerScreen.tsx +216 -36
- package/src/ui/ModelReasoningPicker.tsx +1 -1
- package/src/ui/PlanReviewPanel.tsx +1 -1
- package/src/ui/ProviderPicker.tsx +735 -321
- package/src/ui/RuntimeStatusBar.tsx +108 -0
- package/src/ui/SelectionPanel.tsx +4 -0
- package/src/ui/SettingsPanel.tsx +1 -1
- package/src/ui/TextEntryPanel.tsx +1 -1
- package/src/ui/Timeline.tsx +1619 -1470
- package/src/ui/TopHeader.tsx +46 -30
- package/src/ui/TranscriptShell.tsx +322 -0
- package/src/ui/TurnGroup.tsx +2 -2
- package/src/ui/UpdateAvailableCard.tsx +3 -3
- package/src/ui/UpdatePromptPanel.tsx +16 -22
- package/src/ui/layout.ts +298 -24
- package/src/ui/layoutListWindow.ts +145 -0
- package/src/ui/logoVariants.ts +4 -8
- package/src/ui/runtimeDisplay.ts +15 -3
- package/src/ui/textLayout.ts +15 -4
- package/src/ui/timelineMeasure.ts +194 -122
- package/src/core/codex.ts +0 -124
- 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/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 {
|
|
@@ -77,9 +109,9 @@ import {
|
|
|
77
109
|
probeCodexAuthStatus,
|
|
78
110
|
} from "./core/auth/codexAuth.js";
|
|
79
111
|
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";
|
|
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";
|
|
83
115
|
import { runShellCommand, summarizeCommandResult } from "./core/process/CommandRunner.js";
|
|
84
116
|
import {
|
|
85
117
|
buildPlanExecutionPrompt,
|
|
@@ -87,8 +119,8 @@ import {
|
|
|
87
119
|
detectHollowResponse,
|
|
88
120
|
isClearlySafeGeneratedCleanupRequest,
|
|
89
121
|
resolveExecutionMode,
|
|
90
|
-
} from "./core/codexPrompt.js";
|
|
91
|
-
import { formatHollowResponse } from "./core/hollowResponseFormat.js";
|
|
122
|
+
} from "./core/codex/codexPrompt.js";
|
|
123
|
+
import { formatHollowResponse } from "./core/shared/hollowResponseFormat.js";
|
|
92
124
|
import {
|
|
93
125
|
createFallbackModelCapabilities,
|
|
94
126
|
findModelCapability,
|
|
@@ -104,27 +136,29 @@ import {
|
|
|
104
136
|
buildWorkspaceCommandContext,
|
|
105
137
|
createWorkspaceRelaunchPlan,
|
|
106
138
|
guardWorkspaceRelaunch,
|
|
139
|
+
type LaunchContext,
|
|
107
140
|
resolveLaunchContext,
|
|
108
|
-
} from "./core/launchContext.js";
|
|
141
|
+
} from "./core/workspace/launchContext.js";
|
|
109
142
|
import {
|
|
110
143
|
findOutsideWorkspacePaths,
|
|
144
|
+
formatSkippedDependencyPath,
|
|
111
145
|
getPromptWorkspaceGuardMessage,
|
|
112
146
|
getShellWorkspaceGuardMessage,
|
|
113
|
-
} from "./core/workspaceGuard.js";
|
|
114
|
-
import {
|
|
115
|
-
formatContextCompact,
|
|
116
|
-
formatContextLength,
|
|
117
|
-
resolveModelContextLength,
|
|
118
|
-
type ModelContextMetadata,
|
|
147
|
+
} from "./core/workspace/workspaceGuard.js";
|
|
148
|
+
import {
|
|
149
|
+
formatContextCompact,
|
|
150
|
+
formatContextLength,
|
|
151
|
+
resolveModelContextLength,
|
|
152
|
+
type ModelContextMetadata,
|
|
119
153
|
} from "./core/providerRuntime/contextMetadata.js";
|
|
120
|
-
import { captureWorkspaceSnapshot, createWorkspaceActivityTracker, diffWorkspaceSnapshots } from "./core/workspaceActivity.js";
|
|
121
|
-
import { resolveWorkspaceRoot } from "./core/workspaceRoot.js";
|
|
154
|
+
import { captureWorkspaceSnapshot, createWorkspaceActivityTracker, diffWorkspaceSnapshots } from "./core/workspace/workspaceActivity.js";
|
|
155
|
+
import { resolveWorkspaceRoot } from "./core/workspace/workspaceRoot.js";
|
|
122
156
|
import {
|
|
123
157
|
importExternalFile,
|
|
124
158
|
isImageFile,
|
|
125
159
|
rewritePromptWithImportedPaths,
|
|
126
|
-
} from "./core/attachments.js";
|
|
127
|
-
import { loadProjectInstructions } from "./core/projectInstructions.js";
|
|
160
|
+
} from "./core/shared/attachments.js";
|
|
161
|
+
import { loadProjectInstructions } from "./core/workspace/projectInstructions.js";
|
|
128
162
|
import { isNoiseLine } from "./core/providers/codexTranscript.js";
|
|
129
163
|
import { getBackendProvider } from "./core/providers/registry.js";
|
|
130
164
|
import type { BackendProgressUpdate, BackendProvider } from "./core/providers/types.js";
|
|
@@ -154,16 +188,13 @@ import {
|
|
|
154
188
|
} from "./core/providerLauncher/workspaceConfig.js";
|
|
155
189
|
import { sanitizeTerminalInput, sanitizeTerminalLines, sanitizeTerminalOutput } from "./core/terminal/terminalSanitize.js";
|
|
156
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";
|
|
157
193
|
import {
|
|
158
|
-
acquireTerminalTitleGuard,
|
|
159
|
-
beginColdStartSequence,
|
|
160
|
-
deriveTerminalTitle,
|
|
161
|
-
reassertIntendedTerminalTitle,
|
|
162
|
-
refreshTerminalTitle,
|
|
163
194
|
setTerminalTitleLifecycleState,
|
|
164
|
-
setIntendedTerminalTitle,
|
|
165
195
|
} from "./core/terminal/terminalTitle.js";
|
|
166
|
-
import { getStdinDebugState, traceInputDebug } from "./core/inputDebug.js";
|
|
196
|
+
import { getStdinDebugState, traceInputDebug } from "./core/debug/inputDebug.js";
|
|
197
|
+
import { traceModelStateDebug } from "./core/debug/modelStateDebug.js";
|
|
167
198
|
import * as perf from "./core/perf/profiler.js";
|
|
168
199
|
import * as renderDebug from "./core/perf/renderDebug.js";
|
|
169
200
|
import {
|
|
@@ -174,7 +205,7 @@ import {
|
|
|
174
205
|
getLocalGitRemoteUrl,
|
|
175
206
|
parseRepoIdentity,
|
|
176
207
|
type DiagnosticResult,
|
|
177
|
-
} from "./core/githubDiagnostics.js";
|
|
208
|
+
} from "./core/shared/githubDiagnostics.js";
|
|
178
209
|
import type { RunEvent, Screen, ShellEvent, TimelineEvent, UIState, UserPromptEvent } from "./session/types.js";
|
|
179
210
|
import {
|
|
180
211
|
buildFollowUpPrompt,
|
|
@@ -201,7 +232,7 @@ import {
|
|
|
201
232
|
import { AuthPanel } from "./ui/AuthPanel.js";
|
|
202
233
|
import { BackendPicker } from "./ui/BackendPicker.js";
|
|
203
234
|
import { measureBottomComposerRows, MemoizedBottomComposer } from "./ui/BottomComposer.js";
|
|
204
|
-
import { useTerminalViewport } from "./ui/layout.js";
|
|
235
|
+
import { resolveStartupHeaderMode, useTerminalViewport } from "./ui/layout.js";
|
|
205
236
|
import { ModelPickerScreen } from "./ui/ModelPickerScreen.js";
|
|
206
237
|
import { ModePicker } from "./ui/ModePicker.js";
|
|
207
238
|
import { PlanActionPicker, type PlanActionValue, measurePlanActionPickerRows } from "./ui/PlanActionPicker.js";
|
|
@@ -215,8 +246,8 @@ import { UpdatePromptPanel } from "./ui/UpdatePromptPanel.js";
|
|
|
215
246
|
import { measureTextEntryPanelRows, TextEntryPanel } from "./ui/TextEntryPanel.js";
|
|
216
247
|
import { ThemePicker } from "./ui/ThemePicker.js";
|
|
217
248
|
import { getFocusTargetForScreen, FOCUS_IDS } from "./ui/focus.js";
|
|
218
|
-
import { ThemeProvider, THEMES } from "./ui/theme.js";
|
|
219
|
-
import { buildActiveRuntimeDisplay, runtimeDisplayToSummary } from "./ui/runtimeDisplay.js";
|
|
249
|
+
import { ThemeProvider, THEMES } from "./ui/theme.js";
|
|
250
|
+
import { buildActiveRuntimeDisplay, runtimeDisplayToSummary } from "./ui/runtimeDisplay.js";
|
|
220
251
|
import {
|
|
221
252
|
cancelThemeSelection,
|
|
222
253
|
commitThemeSelection,
|
|
@@ -227,8 +258,10 @@ import {
|
|
|
227
258
|
} from "./ui/themeFlow.js";
|
|
228
259
|
import { isBusy as isUiBusy } from "./session/types.js";
|
|
229
260
|
import { AppShell } from "./ui/AppShell.js";
|
|
230
|
-
import {
|
|
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",
|
|
@@ -324,6 +402,12 @@ export function App({ launchArgs }: AppProps) {
|
|
|
324
402
|
[launchContext],
|
|
325
403
|
);
|
|
326
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;
|
|
327
411
|
|
|
328
412
|
// ─── State & Refs ────────────────────────────────────────────────────────────
|
|
329
413
|
|
|
@@ -375,7 +459,26 @@ export function App({ launchArgs }: AppProps) {
|
|
|
375
459
|
const screenRef = useRef<Screen>("main");
|
|
376
460
|
screenRef.current = screen;
|
|
377
461
|
const [composerInstanceKey, setComposerInstanceKey] = useState(0);
|
|
378
|
-
|
|
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
|
+
});
|
|
379
482
|
const [authStatus, setAuthStatus] = useState<CodexAuthProbeResult>(createInitialAuthStatus());
|
|
380
483
|
const [authStatusBusy, setAuthStatusBusy] = useState(false);
|
|
381
484
|
// Running character total across the conversation — used to estimate token usage
|
|
@@ -386,6 +489,24 @@ export function App({ launchArgs }: AppProps) {
|
|
|
386
489
|
const { stdout } = useStdout();
|
|
387
490
|
const { stdin } = useStdin();
|
|
388
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
|
+
);
|
|
389
510
|
const [mouseOverride, setMouseOverride] = useState<boolean | null>(null);
|
|
390
511
|
const [isMouseIdle, setIsMouseIdle] = useState(false);
|
|
391
512
|
const mouseIdleTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
@@ -395,9 +516,14 @@ export function App({ launchArgs }: AppProps) {
|
|
|
395
516
|
if (mouseIdleTimerRef.current) {
|
|
396
517
|
clearTimeout(mouseIdleTimerRef.current);
|
|
397
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
|
+
/*
|
|
398
523
|
mouseIdleTimerRef.current = setTimeout(() => {
|
|
399
524
|
setIsMouseIdle(true);
|
|
400
525
|
}, 1500);
|
|
526
|
+
*/
|
|
401
527
|
}, []);
|
|
402
528
|
|
|
403
529
|
useInput(() => {
|
|
@@ -409,26 +535,71 @@ export function App({ launchArgs }: AppProps) {
|
|
|
409
535
|
const [planFlow, setPlanFlow] = useState<PlanFlowState>(createInitialPlanFlowState);
|
|
410
536
|
const [initialRevisionText, setInitialRevisionText] = useState("");
|
|
411
537
|
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.
|
|
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.
|
|
420
540
|
const mouseCapture = (mouseOverride ?? (terminalMouseMode === "wheel")) && !isMouseIdle;
|
|
541
|
+
const effectiveMouseCapture = false;
|
|
542
|
+
const overlayMode = screen !== "main";
|
|
421
543
|
|
|
422
544
|
// ─── Effects & Handlers ──────────────────────────────────────────────────────
|
|
423
545
|
|
|
424
546
|
useEffect(() => {
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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");
|
|
428
578
|
return () => {
|
|
429
579
|
terminalControl.setMouseReporting(false, "src/app.tsx:mouseCapture.cleanup");
|
|
430
580
|
};
|
|
431
|
-
}, [
|
|
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]);
|
|
432
603
|
|
|
433
604
|
const cleanupRef = useRef<(() => void) | null>(null);
|
|
434
605
|
const activeRunLifecycleRef = useRef<PromptRunLifecycle | null>(null);
|
|
@@ -447,7 +618,7 @@ export function App({ launchArgs }: AppProps) {
|
|
|
447
618
|
const modelSelectionInFlightRef = useRef(false);
|
|
448
619
|
const providerRouteErrorsRef = useRef<Record<string, string>>({});
|
|
449
620
|
const providerDiagnosticsRef = useRef<Record<string, Record<string, string | number | boolean | null>>>({});
|
|
450
|
-
const providerMigrationNoticeShownRef = useRef(
|
|
621
|
+
const providerMigrationNoticeShownRef = useRef(Boolean(initialProviderWorkspaceConfig.current.migrationNotice));
|
|
451
622
|
const initialPromptSubmittedRef = useRef(false);
|
|
452
623
|
const activeThemeName = getDisplayedThemeName(themeSelection);
|
|
453
624
|
const activeTheme =
|
|
@@ -503,6 +674,31 @@ export function App({ launchArgs }: AppProps) {
|
|
|
503
674
|
[providerRegistry],
|
|
504
675
|
);
|
|
505
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]);
|
|
506
702
|
|
|
507
703
|
// Reset provider readiness when the user switches to a different provider.
|
|
508
704
|
useEffect(() => {
|
|
@@ -736,17 +932,6 @@ export function App({ launchArgs }: AppProps) {
|
|
|
736
932
|
() => formatWorkspaceDisplayPath(workspaceRoot, workspaceDisplayMode),
|
|
737
933
|
[workspaceDisplayMode, workspaceRoot],
|
|
738
934
|
);
|
|
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
935
|
const { staticEvents, activeEvents, uiState, inputValue, cursor } = sessionState;
|
|
751
936
|
|
|
752
937
|
const currentUserSettings = useMemo<UserSettingValues>(() => ({
|
|
@@ -768,22 +953,73 @@ export function App({ launchArgs }: AppProps) {
|
|
|
768
953
|
[planFlow],
|
|
769
954
|
);
|
|
770
955
|
|
|
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;
|
|
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
|
+
]);
|
|
787
1023
|
|
|
788
1024
|
const hasUserPrompt = useMemo(
|
|
789
1025
|
() => staticEvents.some((e) => e.type === "user") || activeEvents.some((e) => e.type === "user"),
|
|
@@ -808,63 +1044,15 @@ export function App({ launchArgs }: AppProps) {
|
|
|
808
1044
|
const busyRef = useRef(busy);
|
|
809
1045
|
busyRef.current = busy;
|
|
810
1046
|
|
|
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.
|
|
1047
|
+
const prevBusyRef = useRef(busy);
|
|
830
1048
|
useEffect(() => {
|
|
831
|
-
if (!busy) {
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
workspaceName: deriveTerminalTitle(workspaceRoot, "dir"),
|
|
835
|
-
force: true,
|
|
836
|
-
debugEventName: "busy-end",
|
|
837
|
-
busyState: false,
|
|
838
|
-
});
|
|
839
|
-
return;
|
|
1049
|
+
if (!busy && prevBusyRef.current && screen === "main") {
|
|
1050
|
+
intendedFocusTargetRef.current = FOCUS_IDS.composer;
|
|
1051
|
+
focusManager.focus(FOCUS_IDS.composer);
|
|
840
1052
|
}
|
|
1053
|
+
prevBusyRef.current = busy;
|
|
1054
|
+
}, [busy, screen, focusManager]);
|
|
841
1055
|
|
|
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
1056
|
const modelCapabilitiesBusyRef = useRef(modelCapabilitiesBusy);
|
|
869
1057
|
modelCapabilitiesBusyRef.current = modelCapabilitiesBusy;
|
|
870
1058
|
const composerRows = useMemo(() => {
|
|
@@ -898,9 +1086,59 @@ export function App({ launchArgs }: AppProps) {
|
|
|
898
1086
|
terminalLayout,
|
|
899
1087
|
uiState,
|
|
900
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
|
+
]);
|
|
901
1138
|
|
|
902
1139
|
renderDebug.useRenderDebug("Root", {
|
|
903
1140
|
screen,
|
|
1141
|
+
activeRoot: activeRootComponent,
|
|
904
1142
|
uiStateKind: uiState.kind,
|
|
905
1143
|
staticEvents,
|
|
906
1144
|
activeEvents,
|
|
@@ -909,8 +1147,11 @@ export function App({ launchArgs }: AppProps) {
|
|
|
909
1147
|
cursor,
|
|
910
1148
|
busy,
|
|
911
1149
|
composerRows,
|
|
1150
|
+
startupHeaderMode,
|
|
1151
|
+
logoBranchSelected: startupHeaderMode === "large",
|
|
912
1152
|
cols: terminalLayout.cols,
|
|
913
1153
|
rows: terminalLayout.rows,
|
|
1154
|
+
layoutMode: terminalLayout.mode,
|
|
914
1155
|
layoutEpoch: terminalLayout.layoutEpoch,
|
|
915
1156
|
planFlowKind: planFlow.kind,
|
|
916
1157
|
mode,
|
|
@@ -1189,13 +1430,12 @@ export function App({ launchArgs }: AppProps) {
|
|
|
1189
1430
|
useEffect(() => {
|
|
1190
1431
|
const notice = providerWorkspaceConfig.migrationNotice;
|
|
1191
1432
|
if (!notice || providerMigrationNoticeShownRef.current) return;
|
|
1192
|
-
providerMigrationNoticeShownRef.current = true;
|
|
1193
1433
|
const providerLabel = findProvider(providerRegistry, notice.revertedProviderId)?.displayName ?? "OpenAI";
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
);
|
|
1198
|
-
}, [
|
|
1434
|
+
const event = createProviderMigrationNoticeEvent(notice, providerLabel);
|
|
1435
|
+
if (!event) return;
|
|
1436
|
+
providerMigrationNoticeShownRef.current = true;
|
|
1437
|
+
appendStaticEvent(event);
|
|
1438
|
+
}, [appendStaticEvent, providerRegistry, providerWorkspaceConfig.migrationNotice]);
|
|
1199
1439
|
|
|
1200
1440
|
useEffect(() => {
|
|
1201
1441
|
if (projectInstructionsLoad.status === "loaded") {
|
|
@@ -1343,6 +1583,7 @@ export function App({ launchArgs }: AppProps) {
|
|
|
1343
1583
|
useEffect(() => {
|
|
1344
1584
|
void (async () => {
|
|
1345
1585
|
try {
|
|
1586
|
+
markProviderAvailability("local", "checking", "startup-probe");
|
|
1346
1587
|
const result = await checkLocalProvider({ override: providerWorkspaceConfig.providers?.local });
|
|
1347
1588
|
if (result.diagnostics) {
|
|
1348
1589
|
providerDiagnosticsRef.current["local"] = result.diagnostics as Record<string, string | number | boolean | null>;
|
|
@@ -1362,13 +1603,6 @@ export function App({ launchArgs }: AppProps) {
|
|
|
1362
1603
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1363
1604
|
}, [workspaceRoot]);
|
|
1364
1605
|
|
|
1365
|
-
useEffect(() => {
|
|
1366
|
-
const devLaunchNotice = buildDevLaunchNotice(launchContext);
|
|
1367
|
-
if (!devLaunchNotice) return;
|
|
1368
|
-
|
|
1369
|
-
appendSystemEvent("Launch mode", devLaunchNotice);
|
|
1370
|
-
}, [appendSystemEvent, launchContext]);
|
|
1371
|
-
|
|
1372
1606
|
// Non-blocking background update check — runs once at startup.
|
|
1373
1607
|
useEffect(() => {
|
|
1374
1608
|
const ucSettings = initialSettings.current.updateCheck ?? DEFAULT_UPDATE_CHECK_SETTINGS;
|
|
@@ -1619,11 +1853,6 @@ export function App({ launchArgs }: AppProps) {
|
|
|
1619
1853
|
}
|
|
1620
1854
|
setScreen("main");
|
|
1621
1855
|
appendSystemEvent("Reasoning updated", `Reasoning level is now ${formatReasoningLabel(nextReasoningLevel)}.`);
|
|
1622
|
-
refreshTerminalTitle({
|
|
1623
|
-
terminalTitleMode,
|
|
1624
|
-
workspaceName: deriveTerminalTitle(workspaceRoot, "dir"),
|
|
1625
|
-
force: true,
|
|
1626
|
-
});
|
|
1627
1856
|
}, [
|
|
1628
1857
|
activeProviderRoute.backendKind,
|
|
1629
1858
|
activeProviderRoute.modelId,
|
|
@@ -1638,9 +1867,7 @@ export function App({ launchArgs }: AppProps) {
|
|
|
1638
1867
|
persistActiveRoute,
|
|
1639
1868
|
persistProviderDefaultModelAndReasoning,
|
|
1640
1869
|
providerWorkspaceConfig.activeRoute,
|
|
1641
|
-
terminalTitleMode,
|
|
1642
1870
|
updateRuntimeConfig,
|
|
1643
|
-
workspaceRoot,
|
|
1644
1871
|
]);
|
|
1645
1872
|
|
|
1646
1873
|
const setPlanModeWithNotice = useCallback((nextEnabled: boolean) => {
|
|
@@ -1718,11 +1945,6 @@ export function App({ launchArgs }: AppProps) {
|
|
|
1718
1945
|
"Model updated",
|
|
1719
1946
|
`Active model is now ${nextModel}. Reasoning set to ${formatReasoningLabel(normalizedReasoning)}.`,
|
|
1720
1947
|
);
|
|
1721
|
-
refreshTerminalTitle({
|
|
1722
|
-
terminalTitleMode,
|
|
1723
|
-
workspaceName: deriveTerminalTitle(workspaceRoot, "dir"),
|
|
1724
|
-
force: true,
|
|
1725
|
-
});
|
|
1726
1948
|
} catch (error) {
|
|
1727
1949
|
const message = error instanceof Error ? error.message : String(error);
|
|
1728
1950
|
traceInputDebug("model_selection_app_failure", getInputDebugSnapshot({
|
|
@@ -1779,11 +2001,11 @@ export function App({ launchArgs }: AppProps) {
|
|
|
1779
2001
|
|
|
1780
2002
|
try {
|
|
1781
2003
|
const normalizedReasoning = normalizeReasoningForModelCapabilities(nextModel, nextReasoning, routeCapabilities);
|
|
1782
|
-
const releaseTitleGuard = acquireTerminalTitleGuard(500, () => {
|
|
1783
|
-
forceRefreshCurrentTerminalTitle("provider_validation_active", true);
|
|
1784
|
-
});
|
|
1785
2004
|
let validation;
|
|
1786
2005
|
try {
|
|
2006
|
+
if (providerId === "local") {
|
|
2007
|
+
markProviderAvailability("local", "checking", "provider-validation");
|
|
2008
|
+
}
|
|
1787
2009
|
validation = await validateProviderRouteActivation({
|
|
1788
2010
|
route: {
|
|
1789
2011
|
providerId,
|
|
@@ -1802,8 +2024,7 @@ export function App({ launchArgs }: AppProps) {
|
|
|
1802
2024
|
}
|
|
1803
2025
|
setRegistryNonce((n) => n + 1);
|
|
1804
2026
|
} finally {
|
|
1805
|
-
|
|
1806
|
-
forceRefreshCurrentTerminalTitle("after_provider_validation", false);
|
|
2027
|
+
// Runtime status is rendered from provider diagnostics; no title guard is active here.
|
|
1807
2028
|
}
|
|
1808
2029
|
if (validation.status !== "ready") {
|
|
1809
2030
|
traceInputDebug("model_selection_app_failure", getInputDebugSnapshot({
|
|
@@ -1850,11 +2071,6 @@ export function App({ launchArgs }: AppProps) {
|
|
|
1850
2071
|
}));
|
|
1851
2072
|
|
|
1852
2073
|
// Route changes are reflected reactively in the BottomComposer metadata row.
|
|
1853
|
-
refreshTerminalTitle({
|
|
1854
|
-
terminalTitleMode,
|
|
1855
|
-
workspaceName: deriveTerminalTitle(workspaceRoot, "dir"),
|
|
1856
|
-
force: true,
|
|
1857
|
-
});
|
|
1858
2074
|
} catch (error) {
|
|
1859
2075
|
const message = error instanceof Error ? error.message : String(error);
|
|
1860
2076
|
traceInputDebug("model_selection_app_failure", getInputDebugSnapshot({
|
|
@@ -1864,12 +2080,11 @@ export function App({ launchArgs }: AppProps) {
|
|
|
1864
2080
|
error: message,
|
|
1865
2081
|
}));
|
|
1866
2082
|
appendErrorEvent("Model selection failed", message);
|
|
1867
|
-
forceRefreshCurrentTerminalTitle("model_selection_failed", false);
|
|
1868
2083
|
} finally {
|
|
1869
2084
|
modelSelectionInFlightRef.current = false;
|
|
1870
2085
|
returnToChatMode("selection");
|
|
1871
2086
|
}
|
|
1872
|
-
}, [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]);
|
|
1873
2088
|
|
|
1874
2089
|
const setAuthPreferenceWithNotice = useCallback((nextPreference: AuthPreference) => {
|
|
1875
2090
|
setAuthPreference(nextPreference);
|
|
@@ -1898,11 +2113,6 @@ export function App({ launchArgs }: AppProps) {
|
|
|
1898
2113
|
}
|
|
1899
2114
|
if (nextSettings.terminalTitleMode !== terminalTitleMode) {
|
|
1900
2115
|
setTerminalTitleMode(nextSettings.terminalTitleMode);
|
|
1901
|
-
refreshTerminalTitle({
|
|
1902
|
-
terminalTitleMode: nextSettings.terminalTitleMode,
|
|
1903
|
-
workspaceName: deriveTerminalTitle(workspaceRoot, "dir"),
|
|
1904
|
-
force: true,
|
|
1905
|
-
});
|
|
1906
2116
|
}
|
|
1907
2117
|
const nextShowBusyLoader = parseBusyLoaderSettingValue(nextSettings.showBusyLoader);
|
|
1908
2118
|
if (nextShowBusyLoader !== showBusyLoader) {
|
|
@@ -2067,6 +2277,7 @@ export function App({ launchArgs }: AppProps) {
|
|
|
2067
2277
|
}
|
|
2068
2278
|
|
|
2069
2279
|
setScreen("provider-picker");
|
|
2280
|
+
markProviderAvailability("local", "checking", "provider-picker-open");
|
|
2070
2281
|
void checkLocalProvider({ override: providerWorkspaceConfig.providers?.local }).then((result) => {
|
|
2071
2282
|
if (!isMountedRef.current) return;
|
|
2072
2283
|
if (result.diagnostics) {
|
|
@@ -2079,7 +2290,7 @@ export function App({ launchArgs }: AppProps) {
|
|
|
2079
2290
|
}
|
|
2080
2291
|
setRegistryNonce((n) => n + 1);
|
|
2081
2292
|
}).catch(() => undefined);
|
|
2082
|
-
}, [appendSystemEvent, busy, providerWorkspaceConfig.providers]);
|
|
2293
|
+
}, [appendSystemEvent, busy, markProviderAvailability, providerWorkspaceConfig.providers]);
|
|
2083
2294
|
|
|
2084
2295
|
const setWorkspaceDefaultProviderWithNotice = useCallback((providerId: ProviderId) => {
|
|
2085
2296
|
const provider = findProvider(providerRegistry, providerId);
|
|
@@ -2138,6 +2349,7 @@ export function App({ launchArgs }: AppProps) {
|
|
|
2138
2349
|
|
|
2139
2350
|
if (providerId === "local") {
|
|
2140
2351
|
appendSystemEvent("Model discovery", "Refreshing LM Studio metadata...");
|
|
2352
|
+
markProviderAvailability("local", "checking", "use-in-codexa");
|
|
2141
2353
|
void checkLocalProvider({ override: providerWorkspaceConfig.providers?.local }).then((validation) => {
|
|
2142
2354
|
if (!isMountedRef.current) return;
|
|
2143
2355
|
if (validation.diagnostics) {
|
|
@@ -2247,6 +2459,9 @@ export function App({ launchArgs }: AppProps) {
|
|
|
2247
2459
|
appendSystemEvent("Model discovery", providerId === "anthropic"
|
|
2248
2460
|
? "Refreshing Claude capabilities..."
|
|
2249
2461
|
: `Refreshing models for ${provider.displayName}...`);
|
|
2462
|
+
if (providerId === "local") {
|
|
2463
|
+
markProviderAvailability("local", "checking", "refresh-models");
|
|
2464
|
+
}
|
|
2250
2465
|
void runtime.refreshModels({
|
|
2251
2466
|
cwd: workspaceRoot,
|
|
2252
2467
|
localConfig: providerId === "local" ? providerWorkspaceConfig.providers?.local : undefined,
|
|
@@ -2353,8 +2568,7 @@ export function App({ launchArgs }: AppProps) {
|
|
|
2353
2568
|
stdout.write("\n");
|
|
2354
2569
|
},
|
|
2355
2570
|
afterLaunch: () => {
|
|
2356
|
-
terminalControl.setMouseReporting(
|
|
2357
|
-
forceRefreshCurrentTerminalTitle("provider_launch_return", false);
|
|
2571
|
+
terminalControl.setMouseReporting(effectiveMouseCapture, "src/app.tsx:providerLaunch.restoreMouse");
|
|
2358
2572
|
},
|
|
2359
2573
|
}).then((result) => {
|
|
2360
2574
|
if (!isMountedRef.current) return;
|
|
@@ -2368,10 +2582,10 @@ export function App({ launchArgs }: AppProps) {
|
|
|
2368
2582
|
activeProviderRoute,
|
|
2369
2583
|
appendErrorEvent,
|
|
2370
2584
|
appendSystemEvent,
|
|
2371
|
-
|
|
2372
|
-
mouseCapture,
|
|
2585
|
+
effectiveMouseCapture,
|
|
2373
2586
|
providerRegistry,
|
|
2374
2587
|
providerWorkspaceConfig.providers,
|
|
2588
|
+
markProviderAvailability,
|
|
2375
2589
|
modelCapabilities,
|
|
2376
2590
|
reasoningLevel,
|
|
2377
2591
|
refreshModelCapabilities,
|
|
@@ -2573,6 +2787,18 @@ export function App({ launchArgs }: AppProps) {
|
|
|
2573
2787
|
dispatchSession({ type: "RESET_INPUT" });
|
|
2574
2788
|
}, [dispatchSession]);
|
|
2575
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
|
+
|
|
2576
2802
|
const finalizePromptRun = useCallback((
|
|
2577
2803
|
runId: number,
|
|
2578
2804
|
turnId: number,
|
|
@@ -2884,23 +3110,36 @@ export function App({ launchArgs }: AppProps) {
|
|
|
2884
3110
|
}, [dispatchSession]);
|
|
2885
3111
|
|
|
2886
3112
|
const handleClear = useCallback(() => {
|
|
2887
|
-
|
|
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
|
+
});
|
|
2888
3120
|
cancelActiveRun(false);
|
|
2889
3121
|
activeTurnIdRef.current = null;
|
|
2890
3122
|
activeRunLifecycleRef.current = null;
|
|
2891
3123
|
activeRunTimingRef.current = null;
|
|
2892
3124
|
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,
|
|
3125
|
+
renderDebug.traceEvent("terminal", "clearReactStateRequested", {
|
|
3126
|
+
clearGeneration,
|
|
3127
|
+
clearPending: clearBoundaryArmed,
|
|
2902
3128
|
});
|
|
2903
|
-
|
|
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]);
|
|
2904
3143
|
|
|
2905
3144
|
const handleShellExecute = useCallback((command: string) => {
|
|
2906
3145
|
const safeCommand = sanitizeTerminalInput(command).trim();
|
|
@@ -2912,7 +3151,6 @@ export function App({ launchArgs }: AppProps) {
|
|
|
2912
3151
|
|
|
2913
3152
|
const shellId = createEventId();
|
|
2914
3153
|
const startTime = Date.now();
|
|
2915
|
-
writeCurrentTerminalTitleBeforeStateChange("before-shell-start");
|
|
2916
3154
|
|
|
2917
3155
|
const initialEvent: ShellEvent = {
|
|
2918
3156
|
id: shellId,
|
|
@@ -2975,9 +3213,6 @@ export function App({ launchArgs }: AppProps) {
|
|
|
2975
3213
|
safeCommand,
|
|
2976
3214
|
{ cwd: workspaceRoot },
|
|
2977
3215
|
{
|
|
2978
|
-
onProcessLifecycle: (event) => {
|
|
2979
|
-
reassertIntendedTerminalTitle({ reason: `shell-process-${event}` });
|
|
2980
|
-
},
|
|
2981
3216
|
onStdout: (text) => {
|
|
2982
3217
|
const lines = sanitizeTerminalLines(text.split(/\r?\n/));
|
|
2983
3218
|
if (lines.length > 0) {
|
|
@@ -3021,9 +3256,8 @@ export function App({ launchArgs }: AppProps) {
|
|
|
3021
3256
|
};
|
|
3022
3257
|
|
|
3023
3258
|
dispatchSession({ type: "FINALIZE_SHELL", shellId, finalEvent });
|
|
3024
|
-
forceRefreshCurrentTerminalTitle("shell_output_complete", false);
|
|
3025
3259
|
});
|
|
3026
|
-
}, [allowedWritableRoots, appendErrorEvent, dispatchSession, focusManager,
|
|
3260
|
+
}, [allowedWritableRoots, appendErrorEvent, dispatchSession, focusManager, workspaceRoot]);
|
|
3027
3261
|
|
|
3028
3262
|
const handleWorkspaceRelaunch = useCallback((targetPath: string) => {
|
|
3029
3263
|
const gate = guardWorkspaceRelaunch(busy);
|
|
@@ -3156,7 +3390,6 @@ export function App({ launchArgs }: AppProps) {
|
|
|
3156
3390
|
setConversationChars((count) => count + safeProviderPrompt.length);
|
|
3157
3391
|
|
|
3158
3392
|
const runId = createEventId();
|
|
3159
|
-
writeCurrentTerminalTitleBeforeStateChange("before-prompt-run-start");
|
|
3160
3393
|
perf.startSession(String(runId));
|
|
3161
3394
|
perf.mark("dispatch_start");
|
|
3162
3395
|
perf.setMeta("fast_cleanup", fastCleanupRun);
|
|
@@ -3283,9 +3516,6 @@ export function App({ launchArgs }: AppProps) {
|
|
|
3283
3516
|
safeProviderPrompt,
|
|
3284
3517
|
{ runtime: runtimeForTurn, workspaceRoot, projectInstructions },
|
|
3285
3518
|
{
|
|
3286
|
-
onProcessLifecycle: (event) => {
|
|
3287
|
-
reassertIntendedTerminalTitle({ reason: `codex-process-${event}` });
|
|
3288
|
-
},
|
|
3289
3519
|
onAssistantDelta: (chunk) => {
|
|
3290
3520
|
const geminiBoundary = activeProviderRoute.providerId === "google";
|
|
3291
3521
|
appDiagLog(`onAssistantDelta: provider=${activeProviderRoute.providerId} chunk.length=${chunk?.length ?? 0} isEmpty=${!chunk}`);
|
|
@@ -3348,7 +3578,6 @@ export function App({ launchArgs }: AppProps) {
|
|
|
3348
3578
|
if (activity.status === "running") {
|
|
3349
3579
|
return;
|
|
3350
3580
|
}
|
|
3351
|
-
forceRefreshCurrentTerminalTitle("tool_activity_complete", true);
|
|
3352
3581
|
if (fastCleanupRun && !blockedCleanupFailureSurfaced) {
|
|
3353
3582
|
const blockedCleanupFailure = getBlockedCleanupFailure(activity);
|
|
3354
3583
|
if (blockedCleanupFailure) {
|
|
@@ -3408,7 +3637,6 @@ export function App({ launchArgs }: AppProps) {
|
|
|
3408
3637
|
const formatted = formatHollowResponse(hollow, safeResponse);
|
|
3409
3638
|
traceLiveRunDiagnostics("completed");
|
|
3410
3639
|
void finalizePromptRun(runId, turnId, "completed", undefined, formatted);
|
|
3411
|
-
forceRefreshCurrentTerminalTitle("prompt_run_completed", false);
|
|
3412
3640
|
return;
|
|
3413
3641
|
}
|
|
3414
3642
|
}
|
|
@@ -3446,7 +3674,6 @@ export function App({ launchArgs }: AppProps) {
|
|
|
3446
3674
|
}
|
|
3447
3675
|
traceLiveRunDiagnostics("completed");
|
|
3448
3676
|
void finalizePromptRun(runId, turnId, "completed", undefined, finalResponse);
|
|
3449
|
-
forceRefreshCurrentTerminalTitle("prompt_run_completed", false);
|
|
3450
3677
|
};
|
|
3451
3678
|
|
|
3452
3679
|
if (flushedLiveUpdates) {
|
|
@@ -3479,7 +3706,6 @@ export function App({ launchArgs }: AppProps) {
|
|
|
3479
3706
|
|
|
3480
3707
|
traceLiveRunDiagnostics("failed");
|
|
3481
3708
|
void finalizePromptRun(runId, turnId, "failed", errorMessage);
|
|
3482
|
-
forceRefreshCurrentTerminalTitle("prompt_run_failed", false);
|
|
3483
3709
|
};
|
|
3484
3710
|
|
|
3485
3711
|
if (flushedLiveUpdates) {
|
|
@@ -3535,8 +3761,6 @@ export function App({ launchArgs }: AppProps) {
|
|
|
3535
3761
|
appendSystemEvent,
|
|
3536
3762
|
authStatus.state,
|
|
3537
3763
|
finalizePromptRun,
|
|
3538
|
-
forceRefreshCurrentTerminalTitle,
|
|
3539
|
-
writeCurrentTerminalTitleBeforeStateChange,
|
|
3540
3764
|
mode,
|
|
3541
3765
|
provider,
|
|
3542
3766
|
projectInstructions,
|
|
@@ -3550,15 +3774,21 @@ export function App({ launchArgs }: AppProps) {
|
|
|
3550
3774
|
if (!pendingImport) return;
|
|
3551
3775
|
const replacements: Array<{ rawPath: string; workspaceRelativePath: string }> = [];
|
|
3552
3776
|
for (const file of pendingImport.files) {
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
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
|
+
}
|
|
3556
3786
|
}
|
|
3557
3787
|
const rewrittenPrompt = rewritePromptWithImportedPaths(pendingImport.prompt, replacements);
|
|
3558
3788
|
setPendingImport(null);
|
|
3559
3789
|
setScreen("main");
|
|
3560
3790
|
startPromptRun(rewrittenPrompt, rewrittenPrompt, { submitTiming: createPromptRunTiming(), commitPrompt: true });
|
|
3561
|
-
}, [pendingImport, workspaceRoot, startPromptRun]);
|
|
3791
|
+
}, [pendingImport, workspaceRoot, startPromptRun, appendErrorEvent]);
|
|
3562
3792
|
|
|
3563
3793
|
const handleImportCancel = useCallback(() => {
|
|
3564
3794
|
if (!pendingImport) return;
|
|
@@ -3944,8 +4174,13 @@ export function App({ launchArgs }: AppProps) {
|
|
|
3944
4174
|
case "diagnose_providers": {
|
|
3945
4175
|
const lines: string[] = ["Provider CLI diagnostics:"];
|
|
3946
4176
|
const diags = providerDiagnosticsRef.current;
|
|
3947
|
-
const providerIds = ["openai", "anthropic", "
|
|
3948
|
-
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
|
+
};
|
|
3949
4184
|
for (const id of providerIds) {
|
|
3950
4185
|
const diag = diags[id];
|
|
3951
4186
|
lines.push(`\n ${labels[id] ?? id}:`);
|
|
@@ -4052,8 +4287,8 @@ export function App({ launchArgs }: AppProps) {
|
|
|
4052
4287
|
appendSystemEvent(
|
|
4053
4288
|
"Mouse mode updated",
|
|
4054
4289
|
nextMouse
|
|
4055
|
-
? "
|
|
4056
|
-
: "
|
|
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.",
|
|
4057
4292
|
);
|
|
4058
4293
|
return;
|
|
4059
4294
|
}
|
|
@@ -4164,7 +4399,14 @@ export function App({ launchArgs }: AppProps) {
|
|
|
4164
4399
|
}
|
|
4165
4400
|
|
|
4166
4401
|
// Validate workspace access for normal prompts
|
|
4167
|
-
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
|
+
|
|
4168
4410
|
if (outsideViolations.length > 0) {
|
|
4169
4411
|
if (runtimeConfig.policy.allowExternalFileImport) {
|
|
4170
4412
|
const attachmentsDir = path.isAbsolute(runtimeConfig.policy.attachmentDir)
|
|
@@ -4254,19 +4496,19 @@ export function App({ launchArgs }: AppProps) {
|
|
|
4254
4496
|
workspaceRoot,
|
|
4255
4497
|
]);
|
|
4256
4498
|
|
|
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]);
|
|
4499
|
+
const modelDisplayName = activeRuntimeDisplay.modelDisplay;
|
|
4500
|
+
const composerReasoningLevel = "";
|
|
4501
|
+
const headerRuntimeSummary = useMemo(
|
|
4502
|
+
() => runtimeDisplayToSummary(activeRuntimeDisplay, runtimeSummary),
|
|
4503
|
+
[activeRuntimeDisplay, runtimeSummary],
|
|
4504
|
+
);
|
|
4505
|
+
const effectiveHeaderConfig = useMemo<HeaderConfig>(() => ({
|
|
4506
|
+
...headerConfig,
|
|
4507
|
+
showProvider: true,
|
|
4508
|
+
showModel: false,
|
|
4509
|
+
showReasoning: false,
|
|
4510
|
+
showContext: false,
|
|
4511
|
+
}), [headerConfig]);
|
|
4270
4512
|
|
|
4271
4513
|
// Memoize the composer element so AppShell's memo check (prev.composer ===
|
|
4272
4514
|
// next.composer) passes during streaming. Without this, a new JSX element is
|
|
@@ -4299,7 +4541,7 @@ export function App({ launchArgs }: AppProps) {
|
|
|
4299
4541
|
placeholder={planFlow.mode === "revise"
|
|
4300
4542
|
? "e.g. keep it to one file and add tests"
|
|
4301
4543
|
: "e.g. keep it minimal and avoid touching other files"}
|
|
4302
|
-
footerHint="Enter
|
|
4544
|
+
footerHint="Esc to close · Enter to confirm"
|
|
4303
4545
|
initialValue={initialRevisionText}
|
|
4304
4546
|
onSubmit={(value) => {
|
|
4305
4547
|
setInitialRevisionText("");
|
|
@@ -4313,14 +4555,14 @@ export function App({ launchArgs }: AppProps) {
|
|
|
4313
4555
|
<MemoizedBottomComposer
|
|
4314
4556
|
key={composerInstanceKey}
|
|
4315
4557
|
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}
|
|
4558
|
+
uiState={uiState}
|
|
4559
|
+
mode={mode}
|
|
4560
|
+
model={modelDisplayName}
|
|
4561
|
+
footerModelDisplay={activeRuntimeDisplay.footerModelDisplay}
|
|
4562
|
+
themeName={activeThemeName}
|
|
4563
|
+
reasoningLevel={composerReasoningLevel}
|
|
4564
|
+
contextDisplay={activeRuntimeDisplay.contextDisplay}
|
|
4565
|
+
planMode={planMode}
|
|
4324
4566
|
showBusyLoader={showBusyLoader}
|
|
4325
4567
|
tokensUsed={estimateTokens(conversationChars)}
|
|
4326
4568
|
modelSpec={currentModelSpec}
|
|
@@ -4358,11 +4600,11 @@ export function App({ launchArgs }: AppProps) {
|
|
|
4358
4600
|
composerInstanceKey,
|
|
4359
4601
|
terminalLayout,
|
|
4360
4602
|
uiState,
|
|
4361
|
-
mode,
|
|
4362
|
-
modelDisplayName,
|
|
4363
|
-
activeRuntimeDisplay.footerModelDisplay,
|
|
4364
|
-
activeRuntimeDisplay.contextDisplay,
|
|
4365
|
-
activeThemeName,
|
|
4603
|
+
mode,
|
|
4604
|
+
modelDisplayName,
|
|
4605
|
+
activeRuntimeDisplay.footerModelDisplay,
|
|
4606
|
+
activeRuntimeDisplay.contextDisplay,
|
|
4607
|
+
activeThemeName,
|
|
4366
4608
|
composerReasoningLevel,
|
|
4367
4609
|
planMode,
|
|
4368
4610
|
showBusyLoader,
|
|
@@ -4385,21 +4627,17 @@ export function App({ launchArgs }: AppProps) {
|
|
|
4385
4627
|
togglePlanModeWithNotice,
|
|
4386
4628
|
handleClear,
|
|
4387
4629
|
cycleModeWithNotice,
|
|
4388
|
-
handleQuit,
|
|
4389
|
-
activeProviderRoute.providerId,
|
|
4390
|
-
sessionState.externalCliStatus,
|
|
4391
|
-
]);
|
|
4630
|
+
handleQuit,
|
|
4631
|
+
activeProviderRoute.providerId,
|
|
4632
|
+
sessionState.externalCliStatus,
|
|
4633
|
+
]);
|
|
4392
4634
|
|
|
4393
4635
|
// ─── Render ──────────────────────────────────────────────────────────────────
|
|
4394
4636
|
|
|
4395
|
-
// Plan review is shown inline in the Timeline, not as a separate overlay.
|
|
4396
|
-
const mainPanelElement = null;
|
|
4397
|
-
|
|
4398
4637
|
return (
|
|
4399
4638
|
<ThemeProvider theme={activeThemeName} customTheme={customTheme}>
|
|
4400
|
-
<
|
|
4639
|
+
<TranscriptShell
|
|
4401
4640
|
layout={terminalLayout}
|
|
4402
|
-
screen={screen}
|
|
4403
4641
|
authState={authStatus.state}
|
|
4404
4642
|
workspaceLabel={workspaceLabel}
|
|
4405
4643
|
workspaceRoot={workspaceRoot}
|
|
@@ -4408,25 +4646,44 @@ export function App({ launchArgs }: AppProps) {
|
|
|
4408
4646
|
activeEvents={activeEvents}
|
|
4409
4647
|
uiState={uiState}
|
|
4410
4648
|
verboseMode={verboseMode}
|
|
4411
|
-
mouseCapture={mouseCapture}
|
|
4412
|
-
onMouseActivity={resetMouseIdle}
|
|
4413
|
-
selectionProfile={selectionProfile}
|
|
4414
4649
|
clearCount={sessionState.clearCount}
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
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
|
+
)}
|
|
4430
4687
|
|
|
4431
4688
|
{screen === "provider-picker" && (
|
|
4432
4689
|
<ProviderPicker
|
|
@@ -4579,7 +4836,7 @@ export function App({ launchArgs }: AppProps) {
|
|
|
4579
4836
|
subtitle="Enter an absolute path or a path relative to the locked workspace."
|
|
4580
4837
|
placeholder="relative\\or\\absolute\\path"
|
|
4581
4838
|
inputLabel="Path"
|
|
4582
|
-
footerHint="Enter
|
|
4839
|
+
footerHint="Esc to close · Enter to confirm"
|
|
4583
4840
|
onSubmit={(value) => {
|
|
4584
4841
|
if (!value.trim()) {
|
|
4585
4842
|
appendSystemEvent("Runtime policy", "Writable root path cannot be empty.");
|
|
@@ -4676,18 +4933,19 @@ export function App({ launchArgs }: AppProps) {
|
|
|
4676
4933
|
onSkipUntilNextVersion={handleSkipUpdateVersion}
|
|
4677
4934
|
/>
|
|
4678
4935
|
)}
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
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
|
+
)}
|
|
4691
4949
|
</ThemeProvider>
|
|
4692
4950
|
);
|
|
4693
4951
|
}
|