@golba98/codexa 1.0.1 → 1.0.3

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 (234) hide show
  1. package/README.md +391 -94
  2. package/package.json +14 -8
  3. package/src/app.tsx +196 -63
  4. package/src/commands/handler.ts +9 -4
  5. package/src/config/buildInfo.ts +3 -0
  6. package/src/config/launchArgs.ts +10 -0
  7. package/src/config/layeredConfig.ts +0 -0
  8. package/src/config/persistence.ts +51 -0
  9. package/src/config/runtimeConfig.ts +4 -0
  10. package/src/config/settings.ts +6 -13
  11. package/src/config/toml-serialize.ts +0 -0
  12. package/src/config/trustStore.ts +0 -0
  13. package/src/config/updateCheckCache.ts +63 -0
  14. package/src/core/attachments.ts +0 -0
  15. package/src/core/auth/codexAuth.ts +0 -0
  16. package/src/core/channel.ts +23 -0
  17. package/src/core/cleanupFastFail.ts +0 -0
  18. package/src/core/clipboard.ts +0 -0
  19. package/src/core/codex.ts +0 -0
  20. package/src/core/codexExecArgs.ts +0 -0
  21. package/src/core/codexLaunch.ts +0 -0
  22. package/src/core/codexPrompt.ts +0 -0
  23. package/src/core/executables/claudeExecutable.ts +0 -0
  24. package/src/core/executables/codexExecutable.ts +1 -0
  25. package/src/core/executables/executableResolver.ts +61 -43
  26. package/src/core/executables/geminiExecutable.ts +0 -0
  27. package/src/core/githubDiagnostics.ts +0 -0
  28. package/src/core/hollowResponseFormat.ts +0 -0
  29. package/src/core/inputDebug.ts +0 -0
  30. package/src/core/launchContext.ts +33 -33
  31. package/src/core/models/codexCapabilities.ts +0 -0
  32. package/src/core/models/codexModelCapabilities.ts +0 -0
  33. package/src/core/models/modelSpecs.ts +0 -0
  34. package/src/core/perf/profiler.ts +0 -0
  35. package/src/core/perf/renderDebug.ts +0 -0
  36. package/src/core/planStorage.ts +0 -0
  37. package/src/core/process/CommandRunner.ts +0 -0
  38. package/src/core/process/processValidation.ts +9 -5
  39. package/src/core/projectInstructions.ts +0 -0
  40. package/src/core/providerLauncher/launcher.ts +59 -42
  41. package/src/core/providerLauncher/registry.ts +0 -0
  42. package/src/core/providerLauncher/types.ts +0 -0
  43. package/src/core/providerLauncher/workspaceConfig.ts +0 -0
  44. package/src/core/providerRuntime/anthropic.ts +0 -0
  45. package/src/core/providerRuntime/capabilityProfile.ts +0 -0
  46. package/src/core/providerRuntime/claudeCodeDiscovery.ts +0 -0
  47. package/src/core/providerRuntime/contextMetadata.ts +0 -0
  48. package/src/core/providerRuntime/gemini.ts +0 -0
  49. package/src/core/providerRuntime/lmstudio.ts +0 -0
  50. package/src/core/providerRuntime/local.ts +0 -0
  51. package/src/core/providerRuntime/models.ts +0 -0
  52. package/src/core/providerRuntime/reasoning.ts +0 -0
  53. package/src/core/providerRuntime/registry.ts +0 -0
  54. package/src/core/providerRuntime/types.ts +0 -0
  55. package/src/core/providers/codexJsonStream.ts +0 -0
  56. package/src/core/providers/codexSubprocess.ts +0 -0
  57. package/src/core/providers/codexTranscript.ts +0 -0
  58. package/src/core/providers/openaiNative.ts +0 -0
  59. package/src/core/providers/registry.ts +0 -0
  60. package/src/core/providers/types.ts +0 -0
  61. package/src/core/terminal/startupClear.ts +20 -0
  62. package/src/core/terminal/terminalCapabilities.ts +0 -0
  63. package/src/core/terminal/terminalControl.ts +0 -0
  64. package/src/core/terminal/terminalSanitize.ts +0 -0
  65. package/src/core/terminal/terminalSelection.ts +0 -0
  66. package/src/core/terminal/terminalTitle.ts +0 -0
  67. package/src/core/updateCheck.ts +188 -0
  68. package/src/core/workspaceActivity.ts +0 -0
  69. package/src/core/workspaceGuard.ts +0 -0
  70. package/src/core/workspaceRoot.ts +0 -0
  71. package/src/exec.ts +0 -0
  72. package/src/headless/execArgs.ts +1 -0
  73. package/src/headless/execRunner.ts +0 -0
  74. package/src/index.tsx +13 -9
  75. package/src/session/appSession.ts +0 -0
  76. package/src/session/chatLifecycle.ts +0 -0
  77. package/src/session/liveRenderScheduler.ts +0 -0
  78. package/src/session/planFlow.ts +0 -0
  79. package/src/session/planTranscript.ts +0 -0
  80. package/src/session/promptRunSchedule.ts +0 -0
  81. package/src/session/types.ts +2 -1
  82. package/src/test/runtimeTestUtils.ts +0 -0
  83. package/src/types/react-dom.d.ts +0 -0
  84. package/src/ui/ActionRequiredBlock.tsx +5 -5
  85. package/src/ui/ActivityBars.tsx +3 -3
  86. package/src/ui/ActivityIndicator.tsx +6 -6
  87. package/src/ui/AgentBlock.tsx +6 -6
  88. package/src/ui/AnimatedStatusText.tsx +1 -1
  89. package/src/ui/AppShell.tsx +13 -5
  90. package/src/ui/AttachmentImportPanel.tsx +8 -8
  91. package/src/ui/AuthPanel.tsx +20 -20
  92. package/src/ui/BackendPicker.tsx +0 -0
  93. package/src/ui/BottomComposer.tsx +117 -78
  94. package/src/ui/DashCard.tsx +3 -3
  95. package/src/ui/Markdown.tsx +17 -17
  96. package/src/ui/ModePicker.tsx +0 -0
  97. package/src/ui/ModelPicker.tsx +0 -0
  98. package/src/ui/ModelPickerScreen.tsx +10 -10
  99. package/src/ui/ModelReasoningPicker.tsx +14 -14
  100. package/src/ui/Panel.tsx +3 -3
  101. package/src/ui/PermissionsPanel.tsx +0 -0
  102. package/src/ui/PlanActionPicker.tsx +6 -6
  103. package/src/ui/PlanReviewPanel.tsx +8 -8
  104. package/src/ui/ProviderPicker.tsx +18 -18
  105. package/src/ui/ReasoningPicker.tsx +0 -0
  106. package/src/ui/RunFooter.tsx +3 -3
  107. package/src/ui/SelectionPanel.tsx +4 -4
  108. package/src/ui/SettingsPanel.tsx +8 -8
  109. package/src/ui/Spinner.tsx +1 -1
  110. package/src/ui/StaticIntroItem.tsx +0 -0
  111. package/src/ui/StaticTranscriptItem.tsx +0 -0
  112. package/src/ui/TextEntryPanel.tsx +10 -10
  113. package/src/ui/ThemePicker.tsx +0 -0
  114. package/src/ui/ThinkingBlock.tsx +8 -8
  115. package/src/ui/Timeline.tsx +21 -17
  116. package/src/ui/TopHeader.tsx +421 -257
  117. package/src/ui/TurnGroup.tsx +31 -31
  118. package/src/ui/UpdateAvailableCard.tsx +41 -0
  119. package/src/ui/UpdatePromptPanel.tsx +203 -0
  120. package/src/ui/busyStatusAnimation.ts +0 -0
  121. package/src/ui/commandNormalize.ts +0 -0
  122. package/src/ui/diffRenderer.ts +0 -0
  123. package/src/ui/focus.ts +3 -0
  124. package/src/ui/inputBuffer.ts +0 -0
  125. package/src/ui/layout.ts +1 -1
  126. package/src/ui/logoVariants.ts +107 -0
  127. package/src/ui/modeDisplay.ts +12 -12
  128. package/src/ui/outputPipeline.ts +0 -0
  129. package/src/ui/progressEntries.ts +0 -0
  130. package/src/ui/runActivityView.ts +0 -0
  131. package/src/ui/runtimeDisplay.ts +100 -0
  132. package/src/ui/slashCommands.ts +0 -0
  133. package/src/ui/terminalAnswerFormat.ts +0 -0
  134. package/src/ui/textLayout.ts +0 -0
  135. package/src/ui/theme.tsx +274 -395
  136. package/src/ui/themeFlow.ts +0 -0
  137. package/src/ui/timelineMeasure.ts +29 -19
  138. package/src/ui/useThrottledValue.ts +0 -0
  139. package/scripts/audit-codexa-capabilities.mjs +0 -466
  140. package/scripts/smoke-terminal-bench.mjs +0 -35
  141. package/src/appRenderStability.test.ts +0 -131
  142. package/src/commands/handler.test.ts +0 -643
  143. package/src/config/launchArgs.test.ts +0 -158
  144. package/src/config/layeredConfig.test.ts +0 -143
  145. package/src/config/persistence.test.ts +0 -110
  146. package/src/config/runtimeConfig.test.ts +0 -218
  147. package/src/config/settings.test.ts +0 -155
  148. package/src/config/trustStore.test.ts +0 -29
  149. package/src/core/attachments.test.ts +0 -155
  150. package/src/core/auth/codexAuth.test.ts +0 -68
  151. package/src/core/cleanupFastFail.test.ts +0 -76
  152. package/src/core/codexExecArgs.test.ts +0 -195
  153. package/src/core/codexLaunch.test.ts +0 -205
  154. package/src/core/codexPrompt.test.ts +0 -252
  155. package/src/core/executables/codexExecutable.test.ts +0 -212
  156. package/src/core/executables/executableResolver.test.ts +0 -129
  157. package/src/core/executables/geminiExecutable.test.ts +0 -116
  158. package/src/core/executables/pathSanityScan.test.ts +0 -47
  159. package/src/core/githubDiagnostics.test.ts +0 -92
  160. package/src/core/hollowResponseFormat.test.ts +0 -58
  161. package/src/core/launchContext.test.ts +0 -157
  162. package/src/core/models/codexCapabilities.test.ts +0 -45
  163. package/src/core/models/codexModelCapabilities.test.ts +0 -246
  164. package/src/core/models/modelSpecs.test.ts +0 -283
  165. package/src/core/perf/renderDebug.test.ts +0 -230
  166. package/src/core/planStorage.test.ts +0 -143
  167. package/src/core/process/CommandRunner.test.ts +0 -105
  168. package/src/core/projectInstructions.test.ts +0 -50
  169. package/src/core/providerLauncher/launcher.test.ts +0 -238
  170. package/src/core/providerLauncher/registry.test.ts +0 -324
  171. package/src/core/providerLauncher/workspaceConfig.test.ts +0 -638
  172. package/src/core/providerRuntime/anthropic.test.ts +0 -1120
  173. package/src/core/providerRuntime/capabilityProfile.test.ts +0 -311
  174. package/src/core/providerRuntime/contextMetadata.test.ts +0 -468
  175. package/src/core/providerRuntime/gemini.test.ts +0 -437
  176. package/src/core/providerRuntime/lmstudio.test.ts +0 -168
  177. package/src/core/providerRuntime/local.test.ts +0 -787
  178. package/src/core/providerRuntime/registry.test.ts +0 -233
  179. package/src/core/providers/codexJsonStream.test.ts +0 -148
  180. package/src/core/providers/codexSubprocess.test.ts +0 -68
  181. package/src/core/providers/codexTranscript.test.ts +0 -284
  182. package/src/core/terminal/terminalCapabilities.test.ts +0 -93
  183. package/src/core/terminal/terminalControl.test.ts +0 -75
  184. package/src/core/terminal/terminalSanitize.test.ts +0 -22
  185. package/src/core/terminal/terminalSelection.test.ts +0 -42
  186. package/src/core/terminal/terminalTitle.test.ts +0 -328
  187. package/src/core/workspaceActivity.test.ts +0 -163
  188. package/src/core/workspaceGuard.test.ts +0 -151
  189. package/src/core/workspaceRoot.test.ts +0 -23
  190. package/src/exec.test.ts +0 -13
  191. package/src/headless/execArgs.test.ts +0 -147
  192. package/src/headless/execRunner.test.ts +0 -434
  193. package/src/index.test.tsx +0 -618
  194. package/src/session/appSession.test.ts +0 -897
  195. package/src/session/chatLifecycle.test.ts +0 -64
  196. package/src/session/liveRenderScheduler.test.ts +0 -201
  197. package/src/session/planFlow.test.ts +0 -103
  198. package/src/session/planTranscript.test.ts +0 -65
  199. package/src/session/promptRunSchedule.test.ts +0 -36
  200. package/src/ui/ActivityIndicator.test.tsx +0 -58
  201. package/src/ui/AgentBlock.test.ts +0 -6
  202. package/src/ui/AnimatedStatusText.test.ts +0 -16
  203. package/src/ui/AppShell.test.tsx +0 -1739
  204. package/src/ui/AttachmentImportPanel.test.tsx +0 -204
  205. package/src/ui/BottomComposer.test.ts +0 -674
  206. package/src/ui/CodexLogo.tsx +0 -55
  207. package/src/ui/Markdown.test.ts +0 -157
  208. package/src/ui/ModelPickerProviderScope.test.tsx +0 -411
  209. package/src/ui/ModelPickerScreen.test.tsx +0 -99
  210. package/src/ui/ModelPickerState.test.tsx +0 -151
  211. package/src/ui/ModelReasoningPicker.test.tsx +0 -447
  212. package/src/ui/PlanReviewPanel.test.tsx +0 -267
  213. package/src/ui/PromptCardBorder.test.tsx +0 -161
  214. package/src/ui/ProviderPicker.test.tsx +0 -289
  215. package/src/ui/ProviderShortcut.test.tsx +0 -143
  216. package/src/ui/SettingsPanel.test.tsx +0 -233
  217. package/src/ui/Timeline.test.ts +0 -2067
  218. package/src/ui/TimelineNavigation.test.tsx +0 -201
  219. package/src/ui/TopHeader.test.tsx +0 -239
  220. package/src/ui/TurnGroup.test.tsx +0 -365
  221. package/src/ui/busyStatusAnimation.test.ts +0 -30
  222. package/src/ui/commandNormalize.test.ts +0 -142
  223. package/src/ui/diffRenderer.test.ts +0 -102
  224. package/src/ui/focusFlow.test.tsx +0 -1098
  225. package/src/ui/inputBuffer.test.ts +0 -151
  226. package/src/ui/layout.test.ts +0 -145
  227. package/src/ui/modeDisplay.test.ts +0 -42
  228. package/src/ui/runActivityView.test.ts +0 -89
  229. package/src/ui/runLifecycleView.test.tsx +0 -237
  230. package/src/ui/statusRenderIsolation.test.tsx +0 -654
  231. package/src/ui/terminalAnswerFormat.test.ts +0 -19
  232. package/src/ui/textLayout.test.ts +0 -18
  233. package/src/ui/themeFlow.test.ts +0 -53
  234. package/src/ui/timelineMeasureCache.test.ts +0 -986
package/src/app.tsx CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  import type { LaunchArgs } from "./config/launchArgs.js";
18
18
  import { loadSettings, saveSettings } from "./config/persistence.js";
19
19
  import {
20
+ APP_VERSION,
20
21
  type AuthPreference,
21
22
  type AvailableBackend,
22
23
  type AvailableMode,
@@ -110,15 +111,11 @@ import {
110
111
  getPromptWorkspaceGuardMessage,
111
112
  getShellWorkspaceGuardMessage,
112
113
  } from "./core/workspaceGuard.js";
113
- import {
114
- type ModelSpec,
115
- } from "./core/models/modelSpecs.js";
116
- import {
117
- contextMetadataToModelSpec,
118
- formatContextCompact,
119
- formatContextLength,
120
- resolveModelContextLength,
121
- type ModelContextMetadata,
114
+ import {
115
+ formatContextCompact,
116
+ formatContextLength,
117
+ resolveModelContextLength,
118
+ type ModelContextMetadata,
122
119
  } from "./core/providerRuntime/contextMetadata.js";
123
120
  import { captureWorkspaceSnapshot, createWorkspaceActivityTracker, diffWorkspaceSnapshots } from "./core/workspaceActivity.js";
124
121
  import { resolveWorkspaceRoot } from "./core/workspaceRoot.js";
@@ -214,10 +211,12 @@ import { ReasoningPicker } from "./ui/ReasoningPicker.js";
214
211
  import { AttachmentImportPanel, type PendingImportFile } from "./ui/AttachmentImportPanel.js";
215
212
  import { SelectionPanel } from "./ui/SelectionPanel.js";
216
213
  import { SettingsPanel } from "./ui/SettingsPanel.js";
214
+ import { UpdatePromptPanel } from "./ui/UpdatePromptPanel.js";
217
215
  import { measureTextEntryPanelRows, TextEntryPanel } from "./ui/TextEntryPanel.js";
218
216
  import { ThemePicker } from "./ui/ThemePicker.js";
219
217
  import { getFocusTargetForScreen, FOCUS_IDS } from "./ui/focus.js";
220
- import { ThemeProvider, THEMES } from "./ui/theme.js";
218
+ import { ThemeProvider, THEMES } from "./ui/theme.js";
219
+ import { buildActiveRuntimeDisplay, runtimeDisplayToSummary } from "./ui/runtimeDisplay.js";
221
220
  import {
222
221
  cancelThemeSelection,
223
222
  commitThemeSelection,
@@ -228,9 +227,18 @@ import {
228
227
  } from "./ui/themeFlow.js";
229
228
  import { isBusy as isUiBusy } from "./session/types.js";
230
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";
232
+ import {
233
+ isCacheValid,
234
+ loadUpdateCheckCache,
235
+ saveUpdateCheckCache,
236
+ } from "./config/updateCheckCache.js";
237
+ import { DEFAULT_UPDATE_CHECK_SETTINGS } from "./config/persistence.js";
231
238
 
232
239
  // ─── Module Constants & Helpers ────────────────────────────────────────────────
233
240
 
241
+
234
242
  let nextEventId = 0;
235
243
  let nextTurnId = 0;
236
244
  // 50ms keeps assistant text live while avoiding frame-wide terminal repaint
@@ -299,6 +307,9 @@ export function App({ launchArgs }: AppProps) {
299
307
  ? projectInstructionsLoad.instructions
300
308
  : null;
301
309
  const initialSettings = useRef(loadSettings());
310
+ const skippedUpdateVersionRef = useRef<string | null>(
311
+ initialSettings.current.updateCheck.skippedUpdateVersion ?? null,
312
+ );
302
313
  const initialProviderWorkspaceConfig = useRef<ProviderWorkspaceConfig>(loadProviderWorkspaceConfig(workspaceRoot));
303
314
  const initialLayeredConfig = useRef<LayeredConfigResult | null>(null);
304
315
  if (initialLayeredConfig.current === null) {
@@ -397,6 +408,7 @@ export function App({ launchArgs }: AppProps) {
397
408
  const [verboseMode, setVerboseMode] = useState(false);
398
409
  const [planFlow, setPlanFlow] = useState<PlanFlowState>(createInitialPlanFlowState);
399
410
  const [initialRevisionText, setInitialRevisionText] = useState("");
411
+ const [updateCheckResult, setUpdateCheckResult] = useState<UpdateCheckResult | null>(null);
400
412
  // Mouse reporting defaults to the persisted terminalMouseMode setting.
401
413
  // "wheel" (default) enables SGR mouse reporting so the timeline can receive
402
414
  // wheel events; native drag-select then requires Shift in Windows Terminal.
@@ -756,16 +768,22 @@ export function App({ launchArgs }: AppProps) {
756
768
  [planFlow],
757
769
  );
758
770
 
759
- const currentModelSpec = useMemo<ModelSpec>(() => {
760
- return contextMetadataToModelSpec(activeContextMetadata ?? {
761
- providerId: activeProviderRoute.providerId,
762
- modelId: activeProviderRoute.modelId,
763
- contextLength: null,
764
- source: "unknown",
765
- confidence: "unknown",
766
- error: "Context length unavailable for this model.",
767
- });
768
- }, [activeContextMetadata, activeProviderRoute.modelId, activeProviderRoute.providerId]);
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;
769
787
 
770
788
  const hasUserPrompt = useMemo(
771
789
  () => staticEvents.some((e) => e.type === "user") || activeEvents.some((e) => e.type === "user"),
@@ -1054,6 +1072,7 @@ export function App({ launchArgs }: AppProps) {
1054
1072
  preference: authPreference,
1055
1073
  },
1056
1074
  header: headerConfig,
1075
+ updateCheck: initialSettings.current.updateCheck,
1057
1076
  });
1058
1077
  }, [authPreference, customTheme, showBusyLoader, terminalMouseMode, terminalTitleMode, themeSelection.committedTheme, workspaceDisplayMode]);
1059
1078
 
@@ -1350,6 +1369,54 @@ export function App({ launchArgs }: AppProps) {
1350
1369
  appendSystemEvent("Launch mode", devLaunchNotice);
1351
1370
  }, [appendSystemEvent, launchContext]);
1352
1371
 
1372
+ // Non-blocking background update check — runs once at startup.
1373
+ useEffect(() => {
1374
+ const ucSettings = initialSettings.current.updateCheck ?? DEFAULT_UPDATE_CHECK_SETTINGS;
1375
+ if (!shouldRunStartupUpdateCheck(process.env, ucSettings.enabled)) return;
1376
+
1377
+ const timer = setTimeout(() => {
1378
+ void (async () => {
1379
+ try {
1380
+ const cache = loadUpdateCheckCache();
1381
+ if (cache && isCacheValid(cache, ucSettings.intervalHours, APP_VERSION)) {
1382
+ if (cache.updateAvailable && cache.latestVersion) {
1383
+ setUpdateCheckResult({
1384
+ status: "update-available",
1385
+ currentVersion: cache.currentVersion,
1386
+ latestVersion: cache.latestVersion,
1387
+ checkedAt: cache.lastChecked,
1388
+ });
1389
+ if (cache.latestVersion !== skippedUpdateVersionRef.current) {
1390
+ setScreen("update-prompt");
1391
+ }
1392
+ }
1393
+ return;
1394
+ }
1395
+ const result = await checkForUpdates({ enabled: true });
1396
+ if (result.status !== "error") {
1397
+ setUpdateCheckResult(result);
1398
+ saveUpdateCheckCache({
1399
+ lastChecked: result.checkedAt,
1400
+ currentVersion: result.currentVersion,
1401
+ latestVersion: result.latestVersion,
1402
+ updateAvailable: result.status === "update-available",
1403
+ });
1404
+ if (result.status === "update-available" && result.latestVersion) {
1405
+ if (result.latestVersion !== skippedUpdateVersionRef.current) {
1406
+ setScreen("update-prompt");
1407
+ }
1408
+ }
1409
+ }
1410
+ } catch {
1411
+ // Never crash the TUI on a failed update check.
1412
+ }
1413
+ })();
1414
+ }, 2000);
1415
+
1416
+ return () => clearTimeout(timer);
1417
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1418
+ }, []);
1419
+
1353
1420
  // Track clear epoch to suppress stale command result events
1354
1421
  useEffect(() => {
1355
1422
  clearEpochRef.current = sessionState.clearEpoch;
@@ -1848,6 +1915,33 @@ export function App({ launchArgs }: AppProps) {
1848
1915
  setScreen("main");
1849
1916
  }, [applyWorkspaceDisplayMode, showBusyLoader, terminalMouseMode, terminalTitleMode, workspaceDisplayMode]);
1850
1917
 
1918
+ const handleSkipUpdateForSession = useCallback(() => {
1919
+ setScreen("main");
1920
+ }, []);
1921
+
1922
+ const handleSkipUpdateVersion = useCallback((version: string) => {
1923
+ initialSettings.current.updateCheck = {
1924
+ ...initialSettings.current.updateCheck,
1925
+ skippedUpdateVersion: version,
1926
+ };
1927
+ saveSettings({
1928
+ ui: {
1929
+ layoutStyle: initialSettings.current.ui.layoutStyle,
1930
+ theme: themeSelection.committedTheme,
1931
+ workspaceDisplayMode,
1932
+ terminalTitleMode,
1933
+ showBusyLoader,
1934
+ terminalMouseMode,
1935
+ customTheme,
1936
+ },
1937
+ auth: { preference: authPreference },
1938
+ header: headerConfig,
1939
+ updateCheck: initialSettings.current.updateCheck,
1940
+ });
1941
+ skippedUpdateVersionRef.current = version;
1942
+ setScreen("main");
1943
+ }, [authPreference, customTheme, headerConfig, showBusyLoader, terminalMouseMode, terminalTitleMode, themeSelection.committedTheme, workspaceDisplayMode]);
1944
+
1851
1945
  const setApprovalPolicyWithNotice = useCallback((nextValue: RuntimeApprovalPolicy) => {
1852
1946
  const gate = guardConfigMutation("mode", busy);
1853
1947
  if (!gate.allowed) {
@@ -3999,6 +4093,38 @@ export function App({ launchArgs }: AppProps) {
3999
4093
  appendSystemEvent("Command", commandResult.message);
4000
4094
  }
4001
4095
  return;
4096
+ case "update": {
4097
+ const arg = commandResult.value ?? "status";
4098
+ if (isLocalDevChannel() && arg !== "check") {
4099
+ appendSystemEvent("Update", formatLocalDevUpdateStatus());
4100
+ return;
4101
+ }
4102
+ void (async () => {
4103
+ let freshResult = updateCheckResult;
4104
+ if (arg === "check" || freshResult === null) {
4105
+ try {
4106
+ freshResult = await checkForUpdates({ enabled: true });
4107
+ setUpdateCheckResult(freshResult);
4108
+ if (freshResult.status !== "error") {
4109
+ saveUpdateCheckCache({
4110
+ lastChecked: freshResult.checkedAt,
4111
+ currentVersion: freshResult.currentVersion,
4112
+ latestVersion: freshResult.latestVersion,
4113
+ updateAvailable: freshResult.status === "update-available",
4114
+ });
4115
+ }
4116
+ } catch {
4117
+ freshResult = null;
4118
+ }
4119
+ }
4120
+ if (freshResult?.status === "update-available" && freshResult.latestVersion) {
4121
+ setScreen("update-prompt");
4122
+ } else {
4123
+ appendSystemEvent("Update", formatUpdateInstructions(freshResult));
4124
+ }
4125
+ })();
4126
+ return;
4127
+ }
4002
4128
  case "help":
4003
4129
  case "unknown":
4004
4130
  if (commandResult.message) {
@@ -4128,32 +4254,19 @@ export function App({ launchArgs }: AppProps) {
4128
4254
  workspaceRoot,
4129
4255
  ]);
4130
4256
 
4131
- const modelDisplayName = useMemo(() => {
4132
- if (activeProviderRoute.providerId === "anthropic") {
4133
- const modelLabel = currentModelCapability?.label ?? activeProviderRoute.modelId;
4134
- return `Claude Code CLI / ${modelLabel} / reasoning: ${formatReasoningLabel(activeProviderRoute.reasoning ?? reasoningLevel)}`;
4135
- }
4136
- if (activeProviderRoute.providerId === "google" && activeProviderRoute.modelSelection) {
4137
- if (activeProviderRoute.modelSelection.kind === "auto") {
4138
- return `auto ${activeProviderRoute.modelSelection.family === "gemini-3" ? "Gemini 3" : "Gemini 2.5"}`;
4139
- }
4140
- }
4141
- if (activeProviderRoute.providerId === "local") {
4142
- return activeProviderRoute.modelId;
4143
- }
4144
- return model;
4145
- }, [
4146
- activeProviderRoute.modelId,
4147
- activeProviderRoute.modelSelection,
4148
- activeProviderRoute.providerId,
4149
- activeProviderRoute.reasoning,
4150
- currentModelCapability?.label,
4151
- model,
4152
- reasoningLevel,
4153
- ]);
4154
- const composerReasoningLevel = activeProviderRoute.providerId === "anthropic"
4155
- ? ""
4156
- : activeProviderRoute.reasoning ?? reasoningLevel;
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]);
4157
4270
 
4158
4271
  // Memoize the composer element so AppShell's memo check (prev.composer ===
4159
4272
  // next.composer) passes during streaming. Without this, a new JSX element is
@@ -4163,7 +4276,7 @@ export function App({ launchArgs }: AppProps) {
4163
4276
  if (planFlow.kind === "awaiting_action") {
4164
4277
  if (!hasVisibleTranscriptPlan) {
4165
4278
  return (
4166
- <Text color={activeTheme.MUTED}>
4279
+ <Text color={activeTheme.textMuted}>
4167
4280
  Plan could not be displayed. Please ask Codexa to regenerate the plan.
4168
4281
  </Text>
4169
4282
  );
@@ -4200,12 +4313,14 @@ export function App({ launchArgs }: AppProps) {
4200
4313
  <MemoizedBottomComposer
4201
4314
  key={composerInstanceKey}
4202
4315
  layout={terminalLayout}
4203
- uiState={uiState}
4204
- mode={mode}
4205
- model={modelDisplayName}
4206
- themeName={activeThemeName}
4207
- reasoningLevel={composerReasoningLevel}
4208
- planMode={planMode}
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}
4209
4324
  showBusyLoader={showBusyLoader}
4210
4325
  tokensUsed={estimateTokens(conversationChars)}
4211
4326
  modelSpec={currentModelSpec}
@@ -4239,13 +4354,15 @@ export function App({ launchArgs }: AppProps) {
4239
4354
  handleCancel,
4240
4355
  handlePlanFeedbackSubmit,
4241
4356
  hasVisibleTranscriptPlan,
4242
- activeTheme.MUTED,
4357
+ activeTheme.textMuted,
4243
4358
  composerInstanceKey,
4244
4359
  terminalLayout,
4245
4360
  uiState,
4246
- mode,
4247
- modelDisplayName,
4248
- activeThemeName,
4361
+ mode,
4362
+ modelDisplayName,
4363
+ activeRuntimeDisplay.footerModelDisplay,
4364
+ activeRuntimeDisplay.contextDisplay,
4365
+ activeThemeName,
4249
4366
  composerReasoningLevel,
4250
4367
  planMode,
4251
4368
  showBusyLoader,
@@ -4268,9 +4385,10 @@ export function App({ launchArgs }: AppProps) {
4268
4385
  togglePlanModeWithNotice,
4269
4386
  handleClear,
4270
4387
  cycleModeWithNotice,
4271
- handleQuit,
4272
- activeProviderRoute.providerId,
4273
- ]);
4388
+ handleQuit,
4389
+ activeProviderRoute.providerId,
4390
+ sessionState.externalCliStatus,
4391
+ ]);
4274
4392
 
4275
4393
  // ─── Render ──────────────────────────────────────────────────────────────────
4276
4394
 
@@ -4285,7 +4403,7 @@ export function App({ launchArgs }: AppProps) {
4285
4403
  authState={authStatus.state}
4286
4404
  workspaceLabel={workspaceLabel}
4287
4405
  workspaceRoot={workspaceRoot}
4288
- runtimeSummary={runtimeSummary}
4406
+ runtimeSummary={headerRuntimeSummary}
4289
4407
  staticEvents={staticEvents}
4290
4408
  activeEvents={activeEvents}
4291
4409
  uiState={uiState}
@@ -4294,7 +4412,12 @@ export function App({ launchArgs }: AppProps) {
4294
4412
  onMouseActivity={resetMouseIdle}
4295
4413
  selectionProfile={selectionProfile}
4296
4414
  clearCount={sessionState.clearCount}
4297
- headerConfig={headerConfig}
4415
+ headerConfig={effectiveHeaderConfig}
4416
+ updateAvailable={
4417
+ updateCheckResult?.status === "update-available" && updateCheckResult.latestVersion
4418
+ ? { latestVersion: updateCheckResult.latestVersion, currentVersion: updateCheckResult.currentVersion }
4419
+ : null
4420
+ }
4298
4421
  panel={
4299
4422
  <>
4300
4423
  {screen === "backend-picker" && (
@@ -4543,6 +4666,16 @@ export function App({ launchArgs }: AppProps) {
4543
4666
  onCancel={handleImportCancel}
4544
4667
  />
4545
4668
  )}
4669
+
4670
+ {screen === "update-prompt" && updateCheckResult?.status === "update-available" && updateCheckResult.latestVersion && (
4671
+ <UpdatePromptPanel
4672
+ focusId={FOCUS_IDS.updatePrompt}
4673
+ currentVersion={updateCheckResult.currentVersion}
4674
+ latestVersion={updateCheckResult.latestVersion}
4675
+ onSkip={handleSkipUpdateForSession}
4676
+ onSkipUntilNextVersion={handleSkipUpdateVersion}
4677
+ />
4678
+ )}
4546
4679
  </>
4547
4680
  }
4548
4681
  mainPanel={null}
@@ -4551,7 +4684,7 @@ export function App({ launchArgs }: AppProps) {
4551
4684
  composerRows={composerRows}
4552
4685
  panelHint={screen !== "main" && screen !== "model-picker" ? (
4553
4686
  <Box marginTop={1} paddingX={1}>
4554
- <Text color={activeTheme.DIM}>Close the active panel with Esc to return to the composer.</Text>
4687
+ <Text color={activeTheme.textDim}>Close the active panel with Esc to return to the composer.</Text>
4555
4688
  </Box>
4556
4689
  ) : null}
4557
4690
  />
@@ -97,6 +97,7 @@ export type CommandAction =
97
97
  | "runtime_personality"
98
98
  | "diagnose_github"
99
99
  | "diagnose_providers"
100
+ | "update"
100
101
  | "unknown";
101
102
 
102
103
  export interface CommandResult {
@@ -395,11 +396,12 @@ function buildHelpMessage(context: CommandContext): string {
395
396
  ` Current reasoning: ${formatReasoningLabel(context.runtime.reasoningLevel)}`,
396
397
  ` Current plan mode: ${context.runtime.planMode ? "Enabled" : "Disabled"}`,
397
398
  " /copy Copy last response to clipboard",
399
+ " /update [check] Show update status and instructions for updating Codexa",
398
400
  " /help Show this help",
399
- "",
400
- "Install on Windows:",
401
- " npm link Make the codexa command available",
402
- " where codexa Verify the command resolves",
401
+ "",
402
+ "Local development:",
403
+ " npm run install:dev-bin Install the codexa-dev command",
404
+ " codexa-dev Run this repo without replacing codexa",
403
405
  "",
404
406
  "Shortcuts:",
405
407
  " Ctrl+B Open backend picker",
@@ -854,6 +856,9 @@ export function handleCommand(text: string, context: CommandContext): CommandRes
854
856
  case "help":
855
857
  return { action: "help", message: buildHelpMessage(context) };
856
858
 
859
+ case "update":
860
+ return { action: "update", value: normalizedArg || "status" };
861
+
857
862
  default:
858
863
  return {
859
864
  action: "unknown",
@@ -0,0 +1,3 @@
1
+ // Generated by scripts/gen-build-info.mjs — do not edit manually.
2
+ export const BUILD_COMMIT = "f4940aa066eae20c0b8decb1fb70f02aca0f2dc0" as const;
3
+ export const APP_VERSION = "1.0.3" as const;
@@ -7,6 +7,7 @@ const FLAG_CONFIG = "--config";
7
7
  const FLAG_CONFIG_SHORT = "-c";
8
8
  const FLAG_MODEL = "--model";
9
9
  const FLAG_MODEL_SHORT = "-m";
10
+ const FLAG_NO_CLEAR = "--no-clear";
10
11
 
11
12
  export interface LaunchArgs {
12
13
  help: boolean;
@@ -17,6 +18,8 @@ export interface LaunchArgs {
17
18
  passthroughArgs: string[];
18
19
  /** Explicitly set when --model / -m was passed on the command line. Null when no model flag was given. */
19
20
  modelOverride: string | null;
21
+ /** True when --no-clear was passed. Suppresses the startup terminal clear. */
22
+ noClear: boolean;
20
23
  }
21
24
 
22
25
  export type LaunchArgsParseResult =
@@ -53,6 +56,7 @@ export function parseLaunchArgs(argv: readonly string[]): LaunchArgsParseResult
53
56
  const promptArgs: string[] = [];
54
57
  let help = false;
55
58
  let version = false;
59
+ let noClear = false;
56
60
  let profile: string | null = null;
57
61
  let modelOverride: string | null = null;
58
62
 
@@ -77,6 +81,11 @@ export function parseLaunchArgs(argv: readonly string[]): LaunchArgsParseResult
77
81
  continue;
78
82
  }
79
83
 
84
+ if (arg === FLAG_NO_CLEAR) {
85
+ noClear = true;
86
+ continue;
87
+ }
88
+
80
89
  // --profile <value> and --profile=<value> both supported.
81
90
  if (arg === FLAG_PROFILE) {
82
91
  const value = normalizeProfileValue(argv[index + 1]);
@@ -181,6 +190,7 @@ export function parseLaunchArgs(argv: readonly string[]): LaunchArgsParseResult
181
190
  configOverrides,
182
191
  passthroughArgs,
183
192
  modelOverride,
193
+ noClear,
184
194
  },
185
195
  };
186
196
  }
File without changes
@@ -47,10 +47,22 @@ export interface AuthSettings {
47
47
  preference: AuthPreference;
48
48
  }
49
49
 
50
+ export interface UpdateCheckSettings {
51
+ enabled: boolean;
52
+ intervalHours: number;
53
+ skippedUpdateVersion?: string | null;
54
+ }
55
+
56
+ export const DEFAULT_UPDATE_CHECK_SETTINGS: UpdateCheckSettings = {
57
+ enabled: true,
58
+ intervalHours: 6,
59
+ };
60
+
50
61
  export interface AppSettings {
51
62
  ui: UiSettings;
52
63
  auth: AuthSettings;
53
64
  header: HeaderConfig;
65
+ updateCheck: UpdateCheckSettings;
54
66
  }
55
67
 
56
68
  // Pick the first string value found under the camelCase or snake_case key.
@@ -106,6 +118,18 @@ function normalizeHeaderConfig(input: Partial<HeaderConfig> | null | undefined):
106
118
  };
107
119
  }
108
120
 
121
+ function normalizeUpdateCheckSettings(
122
+ input: Partial<UpdateCheckSettings> | null | undefined,
123
+ ): UpdateCheckSettings {
124
+ return {
125
+ enabled: typeof input?.enabled === "boolean" ? input.enabled : DEFAULT_UPDATE_CHECK_SETTINGS.enabled,
126
+ intervalHours: typeof input?.intervalHours === "number" && input.intervalHours > 0
127
+ ? input.intervalHours
128
+ : DEFAULT_UPDATE_CHECK_SETTINGS.intervalHours,
129
+ skippedUpdateVersion: input?.skippedUpdateVersion ?? null,
130
+ };
131
+ }
132
+
109
133
  export function getDefaultSettings(): AppSettings {
110
134
  return {
111
135
  ui: normalizeUiSettings(null),
@@ -113,6 +137,7 @@ export function getDefaultSettings(): AppSettings {
113
137
  preference: DEFAULT_AUTH_PREFERENCE,
114
138
  },
115
139
  header: normalizeHeaderConfig(null),
140
+ updateCheck: normalizeUpdateCheckSettings(null),
116
141
  };
117
142
  }
118
143
 
@@ -211,6 +236,12 @@ export function parseSettingsData(data: unknown): AppSettings {
211
236
  ? record.header as Record<string, unknown>
212
237
  : {};
213
238
 
239
+ const updateCheckSource = typeof record.updateCheck === "object" && record.updateCheck !== null
240
+ ? record.updateCheck as Record<string, unknown>
241
+ : typeof record.update_check === "object" && record.update_check !== null
242
+ ? record.update_check as Record<string, unknown>
243
+ : {};
244
+
214
245
  return {
215
246
  ui: normalizeUiSettings({
216
247
  layoutStyle: pickStr(uiSource, "layoutStyle", "layout_style") ?? defaults.ui.layoutStyle,
@@ -233,6 +264,19 @@ export function parseSettingsData(data: unknown): AppSettings {
233
264
  showContext: pickBool(headerSource, "showContext", "show_context"),
234
265
  showAuthStatus: pickBool(headerSource, "showAuthStatus", "show_auth_status"),
235
266
  }),
267
+ updateCheck: normalizeUpdateCheckSettings({
268
+ enabled: pickBool(updateCheckSource, "enabled", "enabled"),
269
+ intervalHours: typeof updateCheckSource.intervalHours === "number"
270
+ ? updateCheckSource.intervalHours
271
+ : typeof updateCheckSource.interval_hours === "number"
272
+ ? updateCheckSource.interval_hours
273
+ : undefined,
274
+ skippedUpdateVersion: typeof updateCheckSource.skippedUpdateVersion === "string"
275
+ ? updateCheckSource.skippedUpdateVersion
276
+ : typeof updateCheckSource.skipped_update_version === "string"
277
+ ? updateCheckSource.skipped_update_version
278
+ : null,
279
+ }),
236
280
  };
237
281
  }
238
282
 
@@ -259,6 +303,13 @@ export function serializeSettings(settings: AppSettings): Record<string, unknown
259
303
  show_context: settings.header.showContext,
260
304
  show_auth_status: settings.header.showAuthStatus,
261
305
  },
306
+ update_check: {
307
+ enabled: settings.updateCheck.enabled,
308
+ interval_hours: settings.updateCheck.intervalHours,
309
+ ...(settings.updateCheck.skippedUpdateVersion != null
310
+ ? { skipped_update_version: settings.updateCheck.skippedUpdateVersion }
311
+ : {}),
312
+ },
262
313
  };
263
314
  }
264
315
 
@@ -110,8 +110,11 @@ export interface RuntimeStatusContext {
110
110
  }
111
111
 
112
112
  export interface RuntimeSummary {
113
+ providerLabel: string;
113
114
  model: AvailableModel;
115
+ modelLabel?: string;
114
116
  reasoningLabel: string;
117
+ contextLabel?: string;
115
118
  modeLabel: string;
116
119
  sandboxLabel: string;
117
120
  approvalLabel: string;
@@ -439,6 +442,7 @@ export function formatPersonalityLabel(value: RuntimePersonality): string {
439
442
 
440
443
  export function buildRuntimeSummary(runtime: ResolvedRuntimeConfig): RuntimeSummary {
441
444
  return {
445
+ providerLabel: formatBackendLabel(runtime.provider),
442
446
  model: runtime.model,
443
447
  reasoningLabel: formatReasoningLabel(runtime.reasoningLevel),
444
448
  modeLabel: formatModeLabel(runtime.mode),
@@ -9,14 +9,14 @@ function smartJoin(base: string, ...parts: string[]): string {
9
9
  return isWindowsStylePath(base) ? win32.join(base, ...parts) : join(base, ...parts);
10
10
  }
11
11
 
12
+ export { APP_VERSION } from "./buildInfo.js";
12
13
  export const APP_NAME = "Codexa";
13
- export const APP_VERSION = "1.0.1";
14
14
  export const DEFAULT_BACKEND = "codex-subprocess";
15
15
  export const DEFAULT_MODEL = "gpt-5.4";
16
16
  export const DEFAULT_MODE = "full-auto";
17
17
  export const DEFAULT_REASONING_LEVEL = "high";
18
18
  export const DEFAULT_LAYOUT_STYLE = "gemini-shell";
19
- export const DEFAULT_THEME = "mono";
19
+ export const DEFAULT_THEME = "dark";
20
20
  export const DEFAULT_WORKSPACE_DISPLAY_MODE = "dir";
21
21
  export const DEFAULT_TERMINAL_TITLE_MODE = "dir";
22
22
  export const DEFAULT_SHOW_BUSY_LOADER = true;
@@ -264,21 +264,14 @@ export function formatReasoningLabel(reasoning: string): string {
264
264
  }
265
265
 
266
266
  export const AVAILABLE_THEMES = [
267
+ { id: "dark", label: "Codexa Dark" },
267
268
  { id: "purple", label: "Midnight Purple" },
268
269
  { id: "mono", label: "Black & White" },
269
- { id: "dark", label: "Modern Dark" },
270
270
  { id: "black", label: "Codex the Black" },
271
- { id: "emerald", label: "Emerald Night" },
272
- { id: "solar", label: "Solar Flare" },
273
- { id: "cyber", label: "Cyberpunk Neon" },
274
- { id: "ocean", label: "Deep Oceanic" },
275
271
  { id: "nordic", label: "Nordic Frost" },
276
- { id: "green", label: "Terminal Green" },
277
- { id: "amber", label: "Terminal Amber" },
278
- { id: "vaporwave", label: "Vaporwave Dream" },
279
272
  { id: "dracula", label: "Dracula Night" },
280
273
  { id: "gruvbox", label: "Gruvbox Hard" },
281
- { id: "synthwave", label: "Synthwave '84" },
274
+ { id: "ocean", label: "Deep Oceanic" },
282
275
  { id: "custom", label: "Customize..." },
283
276
  ] as const;
284
277
 
@@ -377,8 +370,8 @@ export interface HeaderConfig {
377
370
  export const HEADER_CONFIG_DEFAULTS: HeaderConfig = {
378
371
  showBrand: true,
379
372
  showWorkspace: true,
380
- showProvider: false,
381
- showModel: false,
373
+ showProvider: true,
374
+ showModel: true,
382
375
  showReasoning: false,
383
376
  showContext: false,
384
377
  showAuthStatus: false,
File without changes
File without changes