@golba98/codexa 1.0.3 → 1.0.5

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