@golba98/codexa 1.0.2 → 1.0.4

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