@golba98/codexa 1.0.2 → 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 (150) hide show
  1. package/README.md +391 -94
  2. package/package.json +12 -7
  3. package/src/app.tsx +149 -117
  4. package/src/commands/handler.ts +4 -4
  5. package/src/config/buildInfo.ts +2 -2
  6. package/src/config/persistence.ts +10 -0
  7. package/src/config/settings.ts +7 -15
  8. package/src/config/updateCheckCache.ts +19 -1
  9. package/src/core/channel.ts +23 -0
  10. package/src/core/executables/codexExecutable.ts +1 -0
  11. package/src/core/executables/executableResolver.ts +61 -43
  12. package/src/core/launchContext.ts +33 -33
  13. package/src/core/process/processValidation.ts +9 -5
  14. package/src/core/providerLauncher/launcher.ts +59 -42
  15. package/src/core/updateCheck.ts +111 -95
  16. package/src/session/types.ts +2 -1
  17. package/src/ui/ActionRequiredBlock.tsx +5 -5
  18. package/src/ui/ActivityBars.tsx +3 -3
  19. package/src/ui/ActivityIndicator.tsx +6 -6
  20. package/src/ui/AgentBlock.tsx +6 -6
  21. package/src/ui/AnimatedStatusText.tsx +1 -1
  22. package/src/ui/AppShell.tsx +9 -24
  23. package/src/ui/AttachmentImportPanel.tsx +8 -8
  24. package/src/ui/AuthPanel.tsx +20 -20
  25. package/src/ui/BottomComposer.tsx +117 -78
  26. package/src/ui/DashCard.tsx +3 -3
  27. package/src/ui/Markdown.tsx +17 -17
  28. package/src/ui/ModelPickerScreen.tsx +10 -10
  29. package/src/ui/ModelReasoningPicker.tsx +14 -14
  30. package/src/ui/Panel.tsx +3 -3
  31. package/src/ui/PlanActionPicker.tsx +6 -6
  32. package/src/ui/PlanReviewPanel.tsx +8 -8
  33. package/src/ui/ProviderPicker.tsx +18 -18
  34. package/src/ui/RunFooter.tsx +3 -3
  35. package/src/ui/SelectionPanel.tsx +4 -4
  36. package/src/ui/SettingsPanel.tsx +8 -8
  37. package/src/ui/Spinner.tsx +1 -1
  38. package/src/ui/TextEntryPanel.tsx +10 -10
  39. package/src/ui/ThinkingBlock.tsx +8 -8
  40. package/src/ui/Timeline.tsx +21 -17
  41. package/src/ui/TopHeader.tsx +421 -293
  42. package/src/ui/TurnGroup.tsx +31 -31
  43. package/src/ui/UpdateAvailableCard.tsx +41 -0
  44. package/src/ui/UpdatePromptPanel.tsx +203 -0
  45. package/src/ui/focus.ts +3 -0
  46. package/src/ui/layout.ts +1 -1
  47. package/src/ui/logoVariants.ts +107 -0
  48. package/src/ui/modeDisplay.ts +12 -12
  49. package/src/ui/runtimeDisplay.ts +100 -0
  50. package/src/ui/theme.tsx +274 -395
  51. package/src/ui/timelineMeasure.ts +29 -19
  52. package/scripts/audit-codexa-capabilities.mjs +0 -466
  53. package/scripts/gen-build-info.mjs +0 -33
  54. package/scripts/smoke-terminal-bench.mjs +0 -35
  55. package/src/appRenderStability.test.ts +0 -131
  56. package/src/commands/handler.test.ts +0 -655
  57. package/src/config/launchArgs.test.ts +0 -189
  58. package/src/config/layeredConfig.test.ts +0 -143
  59. package/src/config/persistence.test.ts +0 -114
  60. package/src/config/runtimeConfig.test.ts +0 -218
  61. package/src/config/settings.test.ts +0 -155
  62. package/src/config/trustStore.test.ts +0 -29
  63. package/src/core/attachments.test.ts +0 -155
  64. package/src/core/auth/codexAuth.test.ts +0 -68
  65. package/src/core/cleanupFastFail.test.ts +0 -76
  66. package/src/core/codexExecArgs.test.ts +0 -195
  67. package/src/core/codexLaunch.test.ts +0 -205
  68. package/src/core/codexPrompt.test.ts +0 -252
  69. package/src/core/executables/codexExecutable.test.ts +0 -212
  70. package/src/core/executables/executableResolver.test.ts +0 -129
  71. package/src/core/executables/geminiExecutable.test.ts +0 -116
  72. package/src/core/executables/pathSanityScan.test.ts +0 -47
  73. package/src/core/githubDiagnostics.test.ts +0 -92
  74. package/src/core/hollowResponseFormat.test.ts +0 -58
  75. package/src/core/launchContext.test.ts +0 -157
  76. package/src/core/models/codexCapabilities.test.ts +0 -45
  77. package/src/core/models/codexModelCapabilities.test.ts +0 -246
  78. package/src/core/models/modelSpecs.test.ts +0 -283
  79. package/src/core/perf/renderDebug.test.ts +0 -230
  80. package/src/core/planStorage.test.ts +0 -143
  81. package/src/core/process/CommandRunner.test.ts +0 -105
  82. package/src/core/projectInstructions.test.ts +0 -50
  83. package/src/core/providerLauncher/launcher.test.ts +0 -238
  84. package/src/core/providerLauncher/registry.test.ts +0 -324
  85. package/src/core/providerLauncher/workspaceConfig.test.ts +0 -638
  86. package/src/core/providerRuntime/anthropic.test.ts +0 -1120
  87. package/src/core/providerRuntime/capabilityProfile.test.ts +0 -311
  88. package/src/core/providerRuntime/contextMetadata.test.ts +0 -468
  89. package/src/core/providerRuntime/gemini.test.ts +0 -437
  90. package/src/core/providerRuntime/lmstudio.test.ts +0 -168
  91. package/src/core/providerRuntime/local.test.ts +0 -787
  92. package/src/core/providerRuntime/registry.test.ts +0 -233
  93. package/src/core/providers/codexJsonStream.test.ts +0 -148
  94. package/src/core/providers/codexSubprocess.test.ts +0 -68
  95. package/src/core/providers/codexTranscript.test.ts +0 -284
  96. package/src/core/terminal/startupClear.test.ts +0 -55
  97. package/src/core/terminal/terminalCapabilities.test.ts +0 -93
  98. package/src/core/terminal/terminalControl.test.ts +0 -75
  99. package/src/core/terminal/terminalSanitize.test.ts +0 -22
  100. package/src/core/terminal/terminalSelection.test.ts +0 -42
  101. package/src/core/terminal/terminalTitle.test.ts +0 -328
  102. package/src/core/updateCheck.test.ts +0 -194
  103. package/src/core/workspaceActivity.test.ts +0 -163
  104. package/src/core/workspaceGuard.test.ts +0 -151
  105. package/src/core/workspaceRoot.test.ts +0 -23
  106. package/src/exec.test.ts +0 -13
  107. package/src/headless/execArgs.test.ts +0 -147
  108. package/src/headless/execRunner.test.ts +0 -436
  109. package/src/index.test.tsx +0 -620
  110. package/src/session/appSession.test.ts +0 -897
  111. package/src/session/chatLifecycle.test.ts +0 -64
  112. package/src/session/liveRenderScheduler.test.ts +0 -201
  113. package/src/session/planFlow.test.ts +0 -103
  114. package/src/session/planTranscript.test.ts +0 -65
  115. package/src/session/promptRunSchedule.test.ts +0 -36
  116. package/src/ui/ActivityIndicator.test.tsx +0 -58
  117. package/src/ui/AgentBlock.test.ts +0 -6
  118. package/src/ui/AnimatedStatusText.test.ts +0 -16
  119. package/src/ui/AppShell.test.tsx +0 -1776
  120. package/src/ui/AttachmentImportPanel.test.tsx +0 -204
  121. package/src/ui/BottomComposer.test.ts +0 -674
  122. package/src/ui/CodexLogo.tsx +0 -55
  123. package/src/ui/Markdown.test.ts +0 -157
  124. package/src/ui/ModelPickerProviderScope.test.tsx +0 -411
  125. package/src/ui/ModelPickerScreen.test.tsx +0 -99
  126. package/src/ui/ModelPickerState.test.tsx +0 -151
  127. package/src/ui/ModelReasoningPicker.test.tsx +0 -447
  128. package/src/ui/PlanReviewPanel.test.tsx +0 -267
  129. package/src/ui/PromptCardBorder.test.tsx +0 -161
  130. package/src/ui/ProviderPicker.test.tsx +0 -289
  131. package/src/ui/ProviderShortcut.test.tsx +0 -143
  132. package/src/ui/SettingsPanel.test.tsx +0 -233
  133. package/src/ui/Timeline.test.ts +0 -2067
  134. package/src/ui/TimelineNavigation.test.tsx +0 -201
  135. package/src/ui/TopHeader.test.tsx +0 -254
  136. package/src/ui/TurnGroup.test.tsx +0 -365
  137. package/src/ui/busyStatusAnimation.test.ts +0 -30
  138. package/src/ui/commandNormalize.test.ts +0 -142
  139. package/src/ui/diffRenderer.test.ts +0 -102
  140. package/src/ui/focusFlow.test.tsx +0 -1098
  141. package/src/ui/inputBuffer.test.ts +0 -151
  142. package/src/ui/layout.test.ts +0 -146
  143. package/src/ui/modeDisplay.test.ts +0 -42
  144. package/src/ui/runActivityView.test.ts +0 -89
  145. package/src/ui/runLifecycleView.test.tsx +0 -237
  146. package/src/ui/statusRenderIsolation.test.tsx +0 -654
  147. package/src/ui/terminalAnswerFormat.test.ts +0 -19
  148. package/src/ui/textLayout.test.ts +0 -18
  149. package/src/ui/themeFlow.test.ts +0 -53
  150. 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,7 +227,8 @@ 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";
231
- import { checkForUpdates, formatUpdateInstructions, type UpdateCheckResult } from "./core/updateCheck.js";
230
+ import { checkForUpdates, formatLocalDevUpdateStatus, formatUpdateInstructions, shouldRunStartupUpdateCheck, type UpdateCheckResult } from "./core/updateCheck.js";
231
+ import { isLocalDevChannel } from "./core/channel.js";
232
232
  import {
233
233
  isCacheValid,
234
234
  loadUpdateCheckCache,
@@ -307,6 +307,9 @@ export function App({ launchArgs }: AppProps) {
307
307
  ? projectInstructionsLoad.instructions
308
308
  : null;
309
309
  const initialSettings = useRef(loadSettings());
310
+ const skippedUpdateVersionRef = useRef<string | null>(
311
+ initialSettings.current.updateCheck.skippedUpdateVersion ?? null,
312
+ );
310
313
  const initialProviderWorkspaceConfig = useRef<ProviderWorkspaceConfig>(loadProviderWorkspaceConfig(workspaceRoot));
311
314
  const initialLayeredConfig = useRef<LayeredConfigResult | null>(null);
312
315
  if (initialLayeredConfig.current === null) {
@@ -765,16 +768,22 @@ export function App({ launchArgs }: AppProps) {
765
768
  [planFlow],
766
769
  );
767
770
 
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]);
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;
778
787
 
779
788
  const hasUserPrompt = useMemo(
780
789
  () => staticEvents.some((e) => e.type === "user") || activeEvents.some((e) => e.type === "user"),
@@ -1363,33 +1372,41 @@ export function App({ launchArgs }: AppProps) {
1363
1372
  // Non-blocking background update check — runs once at startup.
1364
1373
  useEffect(() => {
1365
1374
  const ucSettings = initialSettings.current.updateCheck ?? DEFAULT_UPDATE_CHECK_SETTINGS;
1366
- if (!ucSettings.enabled) return;
1375
+ if (!shouldRunStartupUpdateCheck(process.env, ucSettings.enabled)) return;
1367
1376
 
1368
1377
  const timer = setTimeout(() => {
1369
1378
  void (async () => {
1370
1379
  try {
1371
1380
  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
- }
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
+ }
1381
1393
  return;
1382
1394
  }
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
- }
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
+ }
1393
1410
  } catch {
1394
1411
  // Never crash the TUI on a failed update check.
1395
1412
  }
@@ -1898,6 +1915,33 @@ export function App({ launchArgs }: AppProps) {
1898
1915
  setScreen("main");
1899
1916
  }, [applyWorkspaceDisplayMode, showBusyLoader, terminalMouseMode, terminalTitleMode, workspaceDisplayMode]);
1900
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
+
1901
1945
  const setApprovalPolicyWithNotice = useCallback((nextValue: RuntimeApprovalPolicy) => {
1902
1946
  const gate = guardConfigMutation("mode", busy);
1903
1947
  if (!gate.allowed) {
@@ -4051,6 +4095,10 @@ export function App({ launchArgs }: AppProps) {
4051
4095
  return;
4052
4096
  case "update": {
4053
4097
  const arg = commandResult.value ?? "status";
4098
+ if (isLocalDevChannel() && arg !== "check") {
4099
+ appendSystemEvent("Update", formatLocalDevUpdateStatus());
4100
+ return;
4101
+ }
4054
4102
  void (async () => {
4055
4103
  let freshResult = updateCheckResult;
4056
4104
  if (arg === "check" || freshResult === null) {
@@ -4058,18 +4106,22 @@ export function App({ launchArgs }: AppProps) {
4058
4106
  freshResult = await checkForUpdates({ enabled: true });
4059
4107
  setUpdateCheckResult(freshResult);
4060
4108
  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
- }
4109
+ saveUpdateCheckCache({
4110
+ lastChecked: freshResult.checkedAt,
4111
+ currentVersion: freshResult.currentVersion,
4112
+ latestVersion: freshResult.latestVersion,
4113
+ updateAvailable: freshResult.status === "update-available",
4114
+ });
4115
+ }
4068
4116
  } catch {
4069
4117
  freshResult = null;
4070
4118
  }
4071
4119
  }
4072
- appendSystemEvent("Update", formatUpdateInstructions(freshResult));
4120
+ if (freshResult?.status === "update-available" && freshResult.latestVersion) {
4121
+ setScreen("update-prompt");
4122
+ } else {
4123
+ appendSystemEvent("Update", formatUpdateInstructions(freshResult));
4124
+ }
4073
4125
  })();
4074
4126
  return;
4075
4127
  }
@@ -4202,58 +4254,19 @@ export function App({ launchArgs }: AppProps) {
4202
4254
  workspaceRoot,
4203
4255
  ]);
4204
4256
 
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
- ]);
4250
- const effectiveHeaderConfig = useMemo<HeaderConfig>(() => ({
4251
- ...headerConfig,
4252
- showProvider: true,
4253
- showModel: false,
4254
- showReasoning: false,
4255
- showContext: false,
4256
- }), [headerConfig]);
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]);
4257
4270
 
4258
4271
  // Memoize the composer element so AppShell's memo check (prev.composer ===
4259
4272
  // next.composer) passes during streaming. Without this, a new JSX element is
@@ -4263,7 +4276,7 @@ export function App({ launchArgs }: AppProps) {
4263
4276
  if (planFlow.kind === "awaiting_action") {
4264
4277
  if (!hasVisibleTranscriptPlan) {
4265
4278
  return (
4266
- <Text color={activeTheme.MUTED}>
4279
+ <Text color={activeTheme.textMuted}>
4267
4280
  Plan could not be displayed. Please ask Codexa to regenerate the plan.
4268
4281
  </Text>
4269
4282
  );
@@ -4300,12 +4313,14 @@ export function App({ launchArgs }: AppProps) {
4300
4313
  <MemoizedBottomComposer
4301
4314
  key={composerInstanceKey}
4302
4315
  layout={terminalLayout}
4303
- uiState={uiState}
4304
- mode={mode}
4305
- model={modelDisplayName}
4306
- themeName={activeThemeName}
4307
- reasoningLevel={composerReasoningLevel}
4308
- 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}
4309
4324
  showBusyLoader={showBusyLoader}
4310
4325
  tokensUsed={estimateTokens(conversationChars)}
4311
4326
  modelSpec={currentModelSpec}
@@ -4339,13 +4354,15 @@ export function App({ launchArgs }: AppProps) {
4339
4354
  handleCancel,
4340
4355
  handlePlanFeedbackSubmit,
4341
4356
  hasVisibleTranscriptPlan,
4342
- activeTheme.MUTED,
4357
+ activeTheme.textMuted,
4343
4358
  composerInstanceKey,
4344
4359
  terminalLayout,
4345
4360
  uiState,
4346
- mode,
4347
- modelDisplayName,
4348
- activeThemeName,
4361
+ mode,
4362
+ modelDisplayName,
4363
+ activeRuntimeDisplay.footerModelDisplay,
4364
+ activeRuntimeDisplay.contextDisplay,
4365
+ activeThemeName,
4349
4366
  composerReasoningLevel,
4350
4367
  planMode,
4351
4368
  showBusyLoader,
@@ -4368,9 +4385,10 @@ export function App({ launchArgs }: AppProps) {
4368
4385
  togglePlanModeWithNotice,
4369
4386
  handleClear,
4370
4387
  cycleModeWithNotice,
4371
- handleQuit,
4372
- activeProviderRoute.providerId,
4373
- ]);
4388
+ handleQuit,
4389
+ activeProviderRoute.providerId,
4390
+ sessionState.externalCliStatus,
4391
+ ]);
4374
4392
 
4375
4393
  // ─── Render ──────────────────────────────────────────────────────────────────
4376
4394
 
@@ -4395,7 +4413,11 @@ export function App({ launchArgs }: AppProps) {
4395
4413
  selectionProfile={selectionProfile}
4396
4414
  clearCount={sessionState.clearCount}
4397
4415
  headerConfig={effectiveHeaderConfig}
4398
- updateCheckResult={updateCheckResult}
4416
+ updateAvailable={
4417
+ updateCheckResult?.status === "update-available" && updateCheckResult.latestVersion
4418
+ ? { latestVersion: updateCheckResult.latestVersion, currentVersion: updateCheckResult.currentVersion }
4419
+ : null
4420
+ }
4399
4421
  panel={
4400
4422
  <>
4401
4423
  {screen === "backend-picker" && (
@@ -4644,6 +4666,16 @@ export function App({ launchArgs }: AppProps) {
4644
4666
  onCancel={handleImportCancel}
4645
4667
  />
4646
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
+ )}
4647
4679
  </>
4648
4680
  }
4649
4681
  mainPanel={null}
@@ -4652,7 +4684,7 @@ export function App({ launchArgs }: AppProps) {
4652
4684
  composerRows={composerRows}
4653
4685
  panelHint={screen !== "main" && screen !== "model-picker" ? (
4654
4686
  <Box marginTop={1} paddingX={1}>
4655
- <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>
4656
4688
  </Box>
4657
4689
  ) : null}
4658
4690
  />
@@ -398,10 +398,10 @@ function buildHelpMessage(context: CommandContext): string {
398
398
  " /copy Copy last response to clipboard",
399
399
  " /update [check] Show update status and instructions for updating Codexa",
400
400
  " /help Show this help",
401
- "",
402
- "Install on Windows:",
403
- " npm link Make the codexa command available",
404
- " 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",
405
405
  "",
406
406
  "Shortcuts:",
407
407
  " Ctrl+B Open backend picker",
@@ -1,3 +1,3 @@
1
1
  // Generated by scripts/gen-build-info.mjs — do not edit manually.
2
- export const BUILD_COMMIT = "6ef3a8a85efef3a525472973403a628a84804c4d" as const;
3
- export const BUILD_VERSION = "1.0.2" as const;
2
+ export const BUILD_COMMIT = "f4940aa066eae20c0b8decb1fb70f02aca0f2dc0" as const;
3
+ export const APP_VERSION = "1.0.3" as const;
@@ -50,6 +50,7 @@ export interface AuthSettings {
50
50
  export interface UpdateCheckSettings {
51
51
  enabled: boolean;
52
52
  intervalHours: number;
53
+ skippedUpdateVersion?: string | null;
53
54
  }
54
55
 
55
56
  export const DEFAULT_UPDATE_CHECK_SETTINGS: UpdateCheckSettings = {
@@ -125,6 +126,7 @@ function normalizeUpdateCheckSettings(
125
126
  intervalHours: typeof input?.intervalHours === "number" && input.intervalHours > 0
126
127
  ? input.intervalHours
127
128
  : DEFAULT_UPDATE_CHECK_SETTINGS.intervalHours,
129
+ skippedUpdateVersion: input?.skippedUpdateVersion ?? null,
128
130
  };
129
131
  }
130
132
 
@@ -269,6 +271,11 @@ export function parseSettingsData(data: unknown): AppSettings {
269
271
  : typeof updateCheckSource.interval_hours === "number"
270
272
  ? updateCheckSource.interval_hours
271
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,
272
279
  }),
273
280
  };
274
281
  }
@@ -299,6 +306,9 @@ export function serializeSettings(settings: AppSettings): Record<string, unknown
299
306
  update_check: {
300
307
  enabled: settings.updateCheck.enabled,
301
308
  interval_hours: settings.updateCheck.intervalHours,
309
+ ...(settings.updateCheck.skippedUpdateVersion != null
310
+ ? { skipped_update_version: settings.updateCheck.skippedUpdateVersion }
311
+ : {}),
302
312
  },
303
313
  };
304
314
  }
@@ -1,6 +1,5 @@
1
- import { homedir } from "os";
2
- import { basename, join, parse, win32 } from "path";
3
- import { BUILD_VERSION } from "./buildInfo.js";
1
+ import { homedir } from "os";
2
+ import { basename, join, parse, win32 } from "path";
4
3
 
5
4
  function isWindowsStylePath(p: string): boolean {
6
5
  return /^[A-Za-z]:[\\/]/.test(p) || /^\\\\/.test(p);
@@ -10,14 +9,14 @@ function smartJoin(base: string, ...parts: string[]): string {
10
9
  return isWindowsStylePath(base) ? win32.join(base, ...parts) : join(base, ...parts);
11
10
  }
12
11
 
13
- export const APP_NAME = "Codexa";
14
- export const APP_VERSION = BUILD_VERSION;
12
+ export { APP_VERSION } from "./buildInfo.js";
13
+ export const APP_NAME = "Codexa";
15
14
  export const DEFAULT_BACKEND = "codex-subprocess";
16
15
  export const DEFAULT_MODEL = "gpt-5.4";
17
16
  export const DEFAULT_MODE = "full-auto";
18
17
  export const DEFAULT_REASONING_LEVEL = "high";
19
18
  export const DEFAULT_LAYOUT_STYLE = "gemini-shell";
20
- export const DEFAULT_THEME = "mono";
19
+ export const DEFAULT_THEME = "dark";
21
20
  export const DEFAULT_WORKSPACE_DISPLAY_MODE = "dir";
22
21
  export const DEFAULT_TERMINAL_TITLE_MODE = "dir";
23
22
  export const DEFAULT_SHOW_BUSY_LOADER = true;
@@ -265,21 +264,14 @@ export function formatReasoningLabel(reasoning: string): string {
265
264
  }
266
265
 
267
266
  export const AVAILABLE_THEMES = [
267
+ { id: "dark", label: "Codexa Dark" },
268
268
  { id: "purple", label: "Midnight Purple" },
269
269
  { id: "mono", label: "Black & White" },
270
- { id: "dark", label: "Modern Dark" },
271
270
  { id: "black", label: "Codex the Black" },
272
- { id: "emerald", label: "Emerald Night" },
273
- { id: "solar", label: "Solar Flare" },
274
- { id: "cyber", label: "Cyberpunk Neon" },
275
- { id: "ocean", label: "Deep Oceanic" },
276
271
  { id: "nordic", label: "Nordic Frost" },
277
- { id: "green", label: "Terminal Green" },
278
- { id: "amber", label: "Terminal Amber" },
279
- { id: "vaporwave", label: "Vaporwave Dream" },
280
272
  { id: "dracula", label: "Dracula Night" },
281
273
  { id: "gruvbox", label: "Gruvbox Hard" },
282
- { id: "synthwave", label: "Synthwave '84" },
274
+ { id: "ocean", label: "Deep Oceanic" },
283
275
  { id: "custom", label: "Customize..." },
284
276
  ] as const;
285
277
 
@@ -39,7 +39,25 @@ export function saveUpdateCheckCache(cache: UpdateCheckCache): void {
39
39
  }
40
40
  }
41
41
 
42
- export function isCacheValid(cache: UpdateCheckCache, intervalHours: number): boolean {
42
+ function stripV(v: string): string {
43
+ return v.startsWith("v") ? v.slice(1) : v;
44
+ }
45
+
46
+ /**
47
+ * Returns true only when the cache is still usable:
48
+ * - `runningVersion` matches the version the cache was written for (version-mismatch = stale)
49
+ * - The cache was written within `intervalHours`
50
+ */
51
+ export function isCacheValid(
52
+ cache: UpdateCheckCache,
53
+ intervalHours: number,
54
+ runningVersion?: string,
55
+ ): boolean {
56
+ if (runningVersion !== undefined) {
57
+ if (stripV(cache.currentVersion) !== stripV(runningVersion)) {
58
+ return false;
59
+ }
60
+ }
43
61
  const maxAgeMs = intervalHours * 60 * 60 * 1000;
44
62
  return Date.now() - cache.lastChecked < maxAgeMs;
45
63
  }
@@ -0,0 +1,23 @@
1
+ import { APP_VERSION } from "../config/settings.js";
2
+
3
+ export const CODEXA_CHANNEL_ENV = "CODEXA_CHANNEL";
4
+ export const LOCAL_DEV_CHANNEL = "local-dev";
5
+
6
+ export function getCodexaChannel(env: NodeJS.ProcessEnv = process.env): string {
7
+ return env[CODEXA_CHANNEL_ENV]?.trim() || "published";
8
+ }
9
+
10
+ export function isLocalDevChannel(env: NodeJS.ProcessEnv = process.env): boolean {
11
+ return getCodexaChannel(env) === LOCAL_DEV_CHANNEL;
12
+ }
13
+
14
+ export function formatCodexaVersionLabel(
15
+ version: string = APP_VERSION,
16
+ env: NodeJS.ProcessEnv = process.env,
17
+ ): string {
18
+ return isLocalDevChannel(env) ? `${version}-dev local` : version;
19
+ }
20
+
21
+ export function formatCodexaBrandLabel(env: NodeJS.ProcessEnv = process.env): string {
22
+ return `Codexa v${formatCodexaVersionLabel(APP_VERSION, env)}`;
23
+ }
@@ -101,6 +101,7 @@ export function spawnCodexProcess(
101
101
  options: SpawnOptions,
102
102
  ): ReturnType<typeof spawn> {
103
103
  const spec = buildSpawnSpec(executable, args);
104
+ if (!spec.executable) throw new Error("Codex executable path is empty.");
104
105
  return spawn(spec.executable, spec.args, { ...options, shell: false });
105
106
  }
106
107