@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.
Files changed (85) hide show
  1. package/README.md +17 -18
  2. package/bin/codexa.js +62 -144
  3. package/package.json +4 -3
  4. package/src/app.tsx +533 -275
  5. package/src/commands/handler.ts +2 -2
  6. package/src/config/buildInfo.ts +2 -2
  7. package/src/config/layeredConfig.ts +1 -1
  8. package/src/config/runtimeConfig.ts +1 -1
  9. package/src/config/settings.ts +1 -1
  10. package/src/config/trustStore.ts +1 -1
  11. package/src/core/README.md +52 -0
  12. package/src/core/agent/loop.ts +282 -0
  13. package/src/core/agent/protocol.ts +211 -0
  14. package/src/core/agent/tools.ts +414 -0
  15. package/src/core/{codexExecArgs.ts → codex/codexExecArgs.ts} +2 -2
  16. package/src/core/{codexLaunch.ts → codex/codexLaunch.ts} +3 -3
  17. package/src/core/{codexPrompt.ts → codex/codexPrompt.ts} +3 -3
  18. package/src/core/debug/modelStateDebug.ts +34 -0
  19. package/src/core/executables/antigravityExecutable.ts +48 -0
  20. package/src/core/executables/executableResolver.ts +5 -1
  21. package/src/core/perf/renderDebug.ts +10 -6
  22. package/src/core/providerLauncher/registry.ts +30 -14
  23. package/src/core/providerLauncher/types.ts +11 -9
  24. package/src/core/providerLauncher/workspaceConfig.ts +41 -26
  25. package/src/core/providerRuntime/anthropic.ts +7 -1
  26. package/src/core/providerRuntime/antigravity.ts +305 -0
  27. package/src/core/providerRuntime/claudeCodeDiscovery.ts +268 -22
  28. package/src/core/providerRuntime/claudeCodeDiscoveryDebug.ts +55 -0
  29. package/src/core/providerRuntime/contextMetadata.ts +12 -24
  30. package/src/core/providerRuntime/local.ts +129 -51
  31. package/src/core/providerRuntime/models.ts +22 -11
  32. package/src/core/providerRuntime/registry.ts +58 -31
  33. package/src/core/providerRuntime/types.ts +19 -14
  34. package/src/core/providers/codexSubprocess.ts +2 -2
  35. package/src/core/providers/types.ts +1 -1
  36. package/src/core/{attachments.ts → shared/attachments.ts} +27 -4
  37. package/src/core/{cleanupFastFail.ts → shared/cleanupFastFail.ts} +1 -1
  38. package/src/core/{hollowResponseFormat.ts → shared/hollowResponseFormat.ts} +1 -1
  39. package/src/core/terminal/clearFrameBoundary.ts +814 -0
  40. package/src/core/terminal/frameLock.ts +109 -0
  41. package/src/core/terminal/inkRenderReset.ts +123 -0
  42. package/src/core/terminal/terminalControl.ts +22 -0
  43. package/src/core/terminal/terminalTitle.ts +16 -102
  44. package/src/core/{channel.ts → version/channel.ts} +1 -1
  45. package/src/core/{updateCheck.ts → version/updateCheck.ts} +10 -5
  46. package/src/core/{launchContext.ts → workspace/launchContext.ts} +9 -16
  47. package/src/core/{planStorage.ts → workspace/planStorage.ts} +2 -2
  48. package/src/core/{workspaceGuard.ts → workspace/workspaceGuard.ts} +97 -13
  49. package/src/headless/execRunner.ts +2 -2
  50. package/src/index.tsx +43 -89
  51. package/src/session/appSession.ts +10 -7
  52. package/src/session/chatLifecycle.ts +1 -1
  53. package/src/session/liveRenderScheduler.ts +1 -1
  54. package/src/session/types.ts +1 -1
  55. package/src/ui/AppShell.tsx +672 -706
  56. package/src/ui/AttachmentImportPanel.tsx +2 -2
  57. package/src/ui/BottomComposer.tsx +145 -144
  58. package/src/ui/ModelPickerScreen.tsx +216 -36
  59. package/src/ui/ModelReasoningPicker.tsx +1 -1
  60. package/src/ui/PlanReviewPanel.tsx +1 -1
  61. package/src/ui/ProviderPicker.tsx +735 -321
  62. package/src/ui/RuntimeStatusBar.tsx +108 -0
  63. package/src/ui/SelectionPanel.tsx +4 -0
  64. package/src/ui/SettingsPanel.tsx +1 -1
  65. package/src/ui/TextEntryPanel.tsx +1 -1
  66. package/src/ui/Timeline.tsx +1619 -1470
  67. package/src/ui/TopHeader.tsx +46 -30
  68. package/src/ui/TranscriptShell.tsx +322 -0
  69. package/src/ui/TurnGroup.tsx +2 -2
  70. package/src/ui/UpdateAvailableCard.tsx +3 -3
  71. package/src/ui/UpdatePromptPanel.tsx +16 -22
  72. package/src/ui/layout.ts +298 -24
  73. package/src/ui/layoutListWindow.ts +145 -0
  74. package/src/ui/logoVariants.ts +4 -8
  75. package/src/ui/runtimeDisplay.ts +15 -3
  76. package/src/ui/textLayout.ts +15 -4
  77. package/src/ui/timelineMeasure.ts +194 -122
  78. package/src/core/codex.ts +0 -124
  79. package/src/ui/StaticTranscriptItem.tsx +0 -56
  80. /package/src/core/{inputDebug.ts → debug/inputDebug.ts} +0 -0
  81. /package/src/core/{clipboard.ts → shared/clipboard.ts} +0 -0
  82. /package/src/core/{githubDiagnostics.ts → shared/githubDiagnostics.ts} +0 -0
  83. /package/src/core/{projectInstructions.ts → workspace/projectInstructions.ts} +0 -0
  84. /package/src/core/{workspaceActivity.ts → workspace/workspaceActivity.ts} +0 -0
  85. /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 { checkForUpdates, formatLocalDevUpdateStatus, formatUpdateInstructions, shouldRunStartupUpdateCheck, type UpdateCheckResult } from "./core/updateCheck.js";
231
- import { isLocalDevChannel } from "./core/channel.js";
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
- const { state: sessionState, dispatch: dispatchSession } = useAppSessionState();
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
- // Mouse reporting defaults to the persisted terminalMouseMode setting.
413
- // "wheel" (default) enables SGR mouse reporting so the timeline can receive
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
- // Default path writes the disable sequences defensively, preserving native
426
- // terminal selection unless the user explicitly opts into /mouse capture.
427
- terminalControl.setMouseReporting(mouseCapture, mouseCapture ? "src/app.tsx:mouseCapture.enable" : "src/app.tsx:mouseCapture.disable");
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
- }, [mouseCapture, terminalControl]);
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(false);
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 forceRefreshCurrentTerminalTitle = useCallback((debugEventName: string, busyState = busyRef.current) => {
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
- refreshTerminalTitle({
833
- terminalTitleMode,
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
- appendSystemEvent(
1195
- "Provider migrated",
1196
- `Antigravity provider is no longer supported. Reverted to ${providerLabel}.`,
1197
- );
1198
- }, [appendSystemEvent, providerRegistry, providerWorkspaceConfig.migrationNotice]);
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
- releaseTitleGuard();
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(mouseCapture, "src/app.tsx:providerLaunch.restoreMouse");
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
- forceRefreshCurrentTerminalTitle,
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
- writeCurrentTerminalTitleBeforeStateChange("before-clear");
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
- dispatchSession({ type: "CLEAR_TRANSCRIPT" });
2894
- setConversationChars(0);
2895
- setScreen("main");
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
- }, [cancelActiveRun, dispatchSession, resetComposer, terminalControl, terminalTitleMode, workspaceRoot, writeCurrentTerminalTitleBeforeStateChange]);
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, forceRefreshCurrentTerminalTitle, workspaceRoot, writeCurrentTerminalTitleBeforeStateChange]);
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
- const destPath = await importExternalFile(file.srcPath, pendingImport.attachmentsDir);
3554
- const relPath = path.relative(workspaceRoot, destPath).replace(/\\/g, "/");
3555
- replacements.push({ rawPath: file.rawPath, workspaceRelativePath: relPath });
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", "google"] as const;
3948
- const labels: Record<string, string> = { openai: "OpenAI/Codex", anthropic: "Anthropic/Claude", google: "Google/Gemini" };
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
- ? "SGR mouse capture on in-app wheel scroll active. Native drag-select requires Shift (Windows Terminal). Run /mouse to disable."
4056
- : "SGR mouse capture off native drag-select and native wheel scroll restored. Run /mouse to re-enable.",
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 regenerate Esc back Backspace delete"
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
- <AppShell
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
- headerConfig={effectiveHeaderConfig}
4416
- updateAvailable={
4417
- updateCheckResult?.status === "update-available" && updateCheckResult.latestVersion
4418
- ? { latestVersion: updateCheckResult.latestVersion, currentVersion: updateCheckResult.currentVersion }
4419
- : null
4420
- }
4421
- panel={
4422
- <>
4423
- {screen === "backend-picker" && (
4424
- <BackendPicker
4425
- currentBackend={backend}
4426
- onSelect={(value) => setBackendWithNotice(value as AvailableBackend)}
4427
- onCancel={() => setScreen("main")}
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 save Esc cancel Backspace delete"
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
- mainPanel={null}
4682
- mainPanelMode="viewport"
4683
- composer={composerElement}
4684
- composerRows={composerRows}
4685
- panelHint={screen !== "main" && screen !== "model-picker" ? (
4686
- <Box marginTop={1} paddingX={1}>
4687
- <Text color={activeTheme.textDim}>Close the active panel with Esc to return to the composer.</Text>
4688
- </Box>
4689
- ) : null}
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
  }