@golba98/codexa 1.0.4 → 1.0.6

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 (78) hide show
  1. package/README.md +6 -10
  2. package/package.json +1 -1
  3. package/src/app.tsx +188 -121
  4. package/src/commands/handler.ts +11 -20
  5. package/src/config/appVersion.ts +65 -0
  6. package/src/config/buildInfo.ts +2 -2
  7. package/src/config/settings.ts +5 -1
  8. package/src/config/updateCheckCache.ts +18 -8
  9. package/src/core/models/codexModelCapabilities.ts +57 -4
  10. package/src/core/models/codexModelsCacheSeed.ts +153 -0
  11. package/src/core/models/providerModelCache.ts +90 -0
  12. package/src/core/process/CommandRunner.ts +12 -1
  13. package/src/core/providerLauncher/registry.ts +37 -7
  14. package/src/core/providerLauncher/types.ts +2 -1
  15. package/src/core/providerRuntime/anthropic.ts +3 -5
  16. package/src/core/providerRuntime/antigravity.ts +257 -90
  17. package/src/core/providerRuntime/claudeCodeDiscovery.ts +724 -692
  18. package/src/core/providerRuntime/mistralVibe.ts +663 -0
  19. package/src/core/providerRuntime/models.ts +18 -4
  20. package/src/core/providerRuntime/reasoning.ts +9 -6
  21. package/src/core/providerRuntime/registry.ts +93 -44
  22. package/src/core/providerRuntime/types.ts +1 -0
  23. package/src/core/version/packageManager.ts +119 -0
  24. package/src/core/version/updateCheck.ts +15 -9
  25. package/src/index.tsx +23 -9
  26. package/src/ui/{ActivityBars.tsx → chrome/ActivityBars.tsx} +1 -1
  27. package/src/ui/{ActivityIndicator.tsx → chrome/ActivityIndicator.tsx} +3 -3
  28. package/src/ui/{AnimatedStatusText.tsx → chrome/AnimatedStatusText.tsx} +3 -3
  29. package/src/ui/{AppShell.tsx → chrome/AppShell.tsx} +11 -11
  30. package/src/ui/{BottomComposer.tsx → chrome/BottomComposer.tsx} +15 -15
  31. package/src/ui/{DashCard.tsx → chrome/DashCard.tsx} +2 -2
  32. package/src/ui/{RunFooter.tsx → chrome/RunFooter.tsx} +3 -3
  33. package/src/ui/{RuntimeStatusBar.tsx → chrome/RuntimeStatusBar.tsx} +3 -3
  34. package/src/ui/{Spinner.tsx → chrome/Spinner.tsx} +1 -1
  35. package/src/ui/{TopHeader.tsx → chrome/TopHeader.tsx} +15 -13
  36. package/src/ui/{UpdateAvailableCard.tsx → chrome/UpdateAvailableCard.tsx} +6 -5
  37. package/src/ui/{commandNormalize.ts → input/commandNormalize.ts} +1 -1
  38. package/src/ui/{focus.ts → input/focus.ts} +1 -1
  39. package/src/ui/{inputBuffer.ts → input/inputBuffer.ts} +3 -3
  40. package/src/ui/{slashCommands.ts → input/slashCommands.ts} +1 -0
  41. package/src/ui/{AttachmentImportPanel.tsx → panels/AttachmentImportPanel.tsx} +1 -1
  42. package/src/ui/{AuthPanel.tsx → panels/AuthPanel.tsx} +5 -5
  43. package/src/ui/{BackendPicker.tsx → panels/BackendPicker.tsx} +2 -2
  44. package/src/ui/{ModePicker.tsx → panels/ModePicker.tsx} +2 -2
  45. package/src/ui/{ModelPicker.tsx → panels/ModelPicker.tsx} +2 -2
  46. package/src/ui/{ModelPickerScreen.tsx → panels/ModelPickerScreen.tsx} +11 -9
  47. package/src/ui/{ModelReasoningPicker.tsx → panels/ModelReasoningPicker.tsx} +5 -5
  48. package/src/ui/{Panel.tsx → panels/Panel.tsx} +1 -1
  49. package/src/ui/{PermissionsPanel.tsx → panels/PermissionsPanel.tsx} +3 -3
  50. package/src/ui/{PlanActionPicker.tsx → panels/PlanActionPicker.tsx} +2 -2
  51. package/src/ui/{PlanReviewPanel.tsx → panels/PlanReviewPanel.tsx} +5 -5
  52. package/src/ui/{ProviderPicker.tsx → panels/ProviderPicker.tsx} +9 -7
  53. package/src/ui/{ReasoningPicker.tsx → panels/ReasoningPicker.tsx} +3 -3
  54. package/src/ui/{SelectionPanel.tsx → panels/SelectionPanel.tsx} +1 -1
  55. package/src/ui/{SettingsPanel.tsx → panels/SettingsPanel.tsx} +3 -3
  56. package/src/ui/{TextEntryPanel.tsx → panels/TextEntryPanel.tsx} +3 -3
  57. package/src/ui/{ThemePicker.tsx → panels/ThemePicker.tsx} +2 -2
  58. package/src/ui/{UpdatePromptPanel.tsx → panels/UpdatePromptPanel.tsx} +54 -44
  59. package/src/ui/{Markdown.tsx → render/Markdown.tsx} +2 -2
  60. package/src/ui/{diffRenderer.ts → render/diffRenderer.ts} +1 -1
  61. package/src/ui/{modeDisplay.ts → render/modeDisplay.ts} +2 -2
  62. package/src/ui/{outputPipeline.ts → render/outputPipeline.ts} +2 -2
  63. package/src/ui/{runtimeDisplay.ts → render/runtimeDisplay.ts} +9 -8
  64. package/src/ui/{ActionRequiredBlock.tsx → timeline/ActionRequiredBlock.tsx} +3 -3
  65. package/src/ui/{AgentBlock.tsx → timeline/AgentBlock.tsx} +10 -10
  66. package/src/ui/{StaticIntroItem.tsx → timeline/StaticIntroItem.tsx} +2 -2
  67. package/src/ui/{ThinkingBlock.tsx → timeline/ThinkingBlock.tsx} +4 -4
  68. package/src/ui/{Timeline.tsx → timeline/Timeline.tsx} +8 -8
  69. package/src/ui/{TranscriptShell.tsx → timeline/TranscriptShell.tsx} +6 -6
  70. package/src/ui/{TurnGroup.tsx → timeline/TurnGroup.tsx} +15 -15
  71. package/src/ui/{progressEntries.ts → timeline/progressEntries.ts} +3 -3
  72. package/src/ui/{runActivityView.ts → timeline/runActivityView.ts} +1 -1
  73. package/src/ui/{timelineMeasure.ts → timeline/timelineMeasure.ts} +14 -14
  74. /package/src/ui/{busyStatusAnimation.ts → chrome/busyStatusAnimation.ts} +0 -0
  75. /package/src/ui/{logoVariants.ts → render/logoVariants.ts} +0 -0
  76. /package/src/ui/{terminalAnswerFormat.ts → render/terminalAnswerFormat.ts} +0 -0
  77. /package/src/ui/{textLayout.ts → render/textLayout.ts} +0 -0
  78. /package/src/ui/{layoutListWindow.ts → timeline/layoutListWindow.ts} +0 -0
package/README.md CHANGED
@@ -577,7 +577,7 @@ npm install -g @golba98/codexa@latest
577
577
 
578
578
  ### Update notice does not appear
579
579
 
580
- Codexa checks for updates on startup and caches the result for 6 hours (stored in `~/.codexa-update-check.json`). The update notice only appears when the npm registry `latest` version is newer than the running version.
580
+ Codexa checks npm for updates on every interactive startup. The local cache at `~/.codexa-update-check.json` is only used as a best-effort fallback if npm is temporarily unavailable. The update notice appears when the npm registry `latest` version is newer than the running version.
581
581
 
582
582
  Published npm versions are immutable. Versions before the fixed update checker may not show update notices even when a newer package exists. If in doubt, update directly:
583
583
 
@@ -588,9 +588,8 @@ npm install -g @golba98/codexa@latest
588
588
  If you expect a notice but don't see one:
589
589
 
590
590
  1. Check the current registry state: `npm view @golba98/codexa dist-tags --json`
591
- 2. Clear the cache to force a fresh check: delete `~/.codexa-update-check.json`, then restart Codexa
592
- 3. Force an explicit check in-app: `/update check`
593
- 4. Update checks are disabled for local dev builds (`codexa-dev` / `cxd`)
591
+ 2. Force an explicit check in-app: `/update check`
592
+ 3. Update checks are disabled for local dev builds (`codexa-dev` / `cxd`)
594
593
 
595
594
  | Symptom | Fix |
596
595
  |---------|-----|
@@ -606,11 +605,8 @@ If you expect a notice but don't see one:
606
605
 
607
606
  See [CHANGELOG.md](CHANGELOG.md) for the full release history.
608
607
 
609
- **Current release: v1.0.4**
608
+ **Current release: v1.0.6**
610
609
 
611
- v1.0.4 is an update-notice reliability fix:
612
- - Older installed versions can detect when npm `latest` is newer
613
- - `/update check` bypasses stale cache and reports a useful status
614
- - Failed registry lookups do not get cached as “up to date”
610
+ v1.0.6 checks npm on every interactive startup and shows an update prompt as soon as Codexa is idle. The prompt offers the correct update command for the package manager that installed Codexa.
615
611
 
616
- > Versions before the fixed update checker may not show update notices. Update directly with `npm install -g @golba98/codexa@latest`.
612
+ > Versions before v1.0.6 can wait for a cached result before discovering a new npm release. Update directly with `npm install -g @golba98/codexa@latest` if needed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@golba98/codexa",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Terminal UI for Codexa — TypeScript + Bun + Ink",
5
5
  "type": "module",
6
6
  "bin": {
package/src/app.tsx CHANGED
@@ -15,12 +15,13 @@ function normalizeRuntimeAvailability(value: unknown): RuntimeAvailability {
15
15
  return "unknown";
16
16
  }
17
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";
18
+ function formatRuntimeProviderLabel(providerId: ProviderId): string {
19
+ if (providerId === "local") return "Local";
20
+ if (providerId === "google") return "Google";
21
+ if (providerId === "anthropic") return "Anthropic";
22
+ if (providerId === "mistral") return "Mistral Vibe CLI";
23
+ if (providerId === "antigravity") return "Antigravity";
24
+ return "OpenAI";
24
25
  }
25
26
 
26
27
  function readDiagnosticString(
@@ -131,6 +132,7 @@ import {
131
132
  normalizeReasoningForModelCapabilities,
132
133
  type CodexModelCapabilities,
133
134
  } from "./core/models/codexModelCapabilities.js";
135
+ import { loadSeededCodexCapabilities } from "./core/models/codexModelsCacheSeed.js";
134
136
  import {
135
137
  buildDevLaunchNotice,
136
138
  buildWorkspaceCommandContext,
@@ -171,11 +173,18 @@ import {
171
173
  getProviderRuntime,
172
174
  isProviderRouteConfigured,
173
175
  isProviderRoutableInCodexa,
176
+ persistProviderDiscovery,
174
177
  resolveActiveProviderRoute,
175
178
  validateProviderRouteActivation,
176
179
  } from "./core/providerRuntime/registry.js";
177
180
  import { hasGeminiApiKey, runGeminiDiagnostics } from "./core/providerRuntime/gemini.js";
178
181
  import { checkLocalProvider, runLocalDiagnostics, setLocalProviderConfig } from "./core/providerRuntime/local.js";
182
+ import {
183
+ detectVibeActiveModel,
184
+ launchMistralVibeCli,
185
+ resetMistralVibeSession,
186
+ resolveVibeExecutable,
187
+ } from "./core/providerRuntime/mistralVibe.js";
179
188
  import { validateAnthropicRoute, ANTHROPIC_ROUTE_SETUP_MESSAGE } from "./core/providerRuntime/anthropic.js";
180
189
  import { providerModelsToCodexCapabilities } from "./core/providerRuntime/models.js";
181
190
  import {
@@ -229,25 +238,25 @@ import {
229
238
  submitPlanFeedback,
230
239
  type PlanFlowState,
231
240
  } from "./session/planFlow.js";
232
- import { AuthPanel } from "./ui/AuthPanel.js";
233
- import { BackendPicker } from "./ui/BackendPicker.js";
234
- import { measureBottomComposerRows, MemoizedBottomComposer } from "./ui/BottomComposer.js";
241
+ import { AuthPanel } from "./ui/panels/AuthPanel.js";
242
+ import { BackendPicker } from "./ui/panels/BackendPicker.js";
243
+ import { measureBottomComposerRows, MemoizedBottomComposer } from "./ui/chrome/BottomComposer.js";
235
244
  import { resolveStartupHeaderMode, useTerminalViewport } from "./ui/layout.js";
236
- import { ModelPickerScreen } from "./ui/ModelPickerScreen.js";
237
- import { ModePicker } from "./ui/ModePicker.js";
238
- import { PlanActionPicker, type PlanActionValue, measurePlanActionPickerRows } from "./ui/PlanActionPicker.js";
239
- import { PermissionsPanel, type PermissionsPanelAction } from "./ui/PermissionsPanel.js";
240
- import { ProviderPicker } from "./ui/ProviderPicker.js";
241
- import { ReasoningPicker } from "./ui/ReasoningPicker.js";
242
- import { AttachmentImportPanel, type PendingImportFile } from "./ui/AttachmentImportPanel.js";
243
- import { SelectionPanel } from "./ui/SelectionPanel.js";
244
- import { SettingsPanel } from "./ui/SettingsPanel.js";
245
- import { UpdatePromptPanel } from "./ui/UpdatePromptPanel.js";
246
- import { measureTextEntryPanelRows, TextEntryPanel } from "./ui/TextEntryPanel.js";
247
- import { ThemePicker } from "./ui/ThemePicker.js";
248
- import { getFocusTargetForScreen, FOCUS_IDS } from "./ui/focus.js";
245
+ import { ModelPickerScreen } from "./ui/panels/ModelPickerScreen.js";
246
+ import { ModePicker } from "./ui/panels/ModePicker.js";
247
+ import { PlanActionPicker, type PlanActionValue, measurePlanActionPickerRows } from "./ui/panels/PlanActionPicker.js";
248
+ import { PermissionsPanel, type PermissionsPanelAction } from "./ui/panels/PermissionsPanel.js";
249
+ import { ProviderPicker } from "./ui/panels/ProviderPicker.js";
250
+ import { ReasoningPicker } from "./ui/panels/ReasoningPicker.js";
251
+ import { AttachmentImportPanel, type PendingImportFile } from "./ui/panels/AttachmentImportPanel.js";
252
+ import { SelectionPanel } from "./ui/panels/SelectionPanel.js";
253
+ import { SettingsPanel } from "./ui/panels/SettingsPanel.js";
254
+ import { UpdatePromptPanel } from "./ui/panels/UpdatePromptPanel.js";
255
+ import { measureTextEntryPanelRows, TextEntryPanel } from "./ui/panels/TextEntryPanel.js";
256
+ import { ThemePicker } from "./ui/panels/ThemePicker.js";
257
+ import { getFocusTargetForScreen, FOCUS_IDS } from "./ui/input/focus.js";
249
258
  import { ThemeProvider, THEMES } from "./ui/theme.js";
250
- import { buildActiveRuntimeDisplay, runtimeDisplayToSummary } from "./ui/runtimeDisplay.js";
259
+ import { buildActiveRuntimeDisplay, runtimeDisplayToSummary } from "./ui/render/runtimeDisplay.js";
251
260
  import {
252
261
  cancelThemeSelection,
253
262
  commitThemeSelection,
@@ -257,13 +266,14 @@ import {
257
266
  type ThemeSelectionState,
258
267
  } from "./ui/themeFlow.js";
259
268
  import { isBusy as isUiBusy } from "./session/types.js";
260
- import { AppShell } from "./ui/AppShell.js";
261
- import { TranscriptShell } from "./ui/TranscriptShell.js";
262
- import type { RuntimeAvailability } from "./ui/RuntimeStatusBar.js";
269
+ import { AppShell } from "./ui/chrome/AppShell.js";
270
+ import { TranscriptShell } from "./ui/timeline/TranscriptShell.js";
271
+ import type { RuntimeAvailability } from "./ui/chrome/RuntimeStatusBar.js";
263
272
  import { checkForUpdates, formatLocalDevUpdateStatus, formatUpdateInstructions, shouldRunStartupUpdateCheck, type UpdateCheckResult } from "./core/version/updateCheck.js";
273
+ import { detectGlobalPackageManager, getUpdateCommand } from "./core/version/packageManager.js";
264
274
  import { isLocalDevChannel } from "./core/version/channel.js";
265
275
  import {
266
- isCacheValid,
276
+ isCacheForRunningVersion,
267
277
  loadUpdateCheckCache,
268
278
  saveUpdateCheckCache,
269
279
  } from "./config/updateCheckCache.js";
@@ -319,7 +329,7 @@ function createProviderMigrationNoticeEvent(
319
329
  createdAt: Date.now(),
320
330
  title: sanitizeTerminalOutput("Provider migrated"),
321
331
  content: sanitizeTerminalOutput(
322
- `${formatRuntimeProviderLabel(notice.deprecatedProviderId as ProviderId)} provider is no longer supported. Reverted to ${resolvedProviderLabel}.`,
332
+ `${formatRuntimeProviderLabel(notice.deprecatedProviderId as ProviderId)} provider is no longer supported. Reverted to ${resolvedProviderLabel}.`,
323
333
  { preserveTabs: false, tabSize: 2 },
324
334
  ),
325
335
  };
@@ -385,9 +395,6 @@ export function App({ launchArgs }: AppProps) {
385
395
  ? projectInstructionsLoad.instructions
386
396
  : null;
387
397
  const initialSettings = useRef(loadSettings());
388
- const skippedUpdateVersionRef = useRef<string | null>(
389
- initialSettings.current.updateCheck.skippedUpdateVersion ?? null,
390
- );
391
398
  const initialProviderWorkspaceConfig = useRef<ProviderWorkspaceConfig>(loadProviderWorkspaceConfig(workspaceRoot));
392
399
  const initialLayeredConfig = useRef<LayeredConfigResult | null>(null);
393
400
  if (initialLayeredConfig.current === null) {
@@ -483,8 +490,14 @@ export function App({ launchArgs }: AppProps) {
483
490
  const [authStatusBusy, setAuthStatusBusy] = useState(false);
484
491
  // Running character total across the conversation — used to estimate token usage
485
492
  const [conversationChars, setConversationChars] = useState(0);
486
- const [modelCapabilities, setModelCapabilities] = useState<CodexModelCapabilities | null>(null);
493
+ // Seeded synchronously from local caches (codex's models_cache.json or the
494
+ // persisted last-good discovery) so the model picker opens instantly with
495
+ // real models; live discovery replaces this in the background.
496
+ const [modelCapabilities, setModelCapabilities] = useState<CodexModelCapabilities | null>(() => loadSeededCodexCapabilities());
487
497
  const [modelCapabilitiesBusy, setModelCapabilitiesBusy] = useState(false);
498
+ // True while a provider route switch is validating (subprocess probes);
499
+ // drives the model picker's loading state for non-openai providers.
500
+ const [routeSwitchBusy, setRouteSwitchBusy] = useState(false);
488
501
  const [activeContextMetadata, setActiveContextMetadata] = useState<ModelContextMetadata | null>(null);
489
502
  const { stdout } = useStdout();
490
503
  const { stdin } = useStdin();
@@ -535,6 +548,9 @@ export function App({ launchArgs }: AppProps) {
535
548
  const [planFlow, setPlanFlow] = useState<PlanFlowState>(createInitialPlanFlowState);
536
549
  const [initialRevisionText, setInitialRevisionText] = useState("");
537
550
  const [updateCheckResult, setUpdateCheckResult] = useState<UpdateCheckResult | null>(null);
551
+ const [startupUpdateDismissed, setStartupUpdateDismissed] = useState(false);
552
+ // Launcher path is fixed for the process lifetime, so detect once.
553
+ const globalPackageManager = useMemo(() => detectGlobalPackageManager(), []);
538
554
  // Transcript mode leaves mouse reporting off so wheel/trackpad input scrolls
539
555
  // the terminal emulator's native scrollback instead of an in-app viewport.
540
556
  const mouseCapture = (mouseOverride ?? (terminalMouseMode === "wheel")) && !isMouseIdle;
@@ -658,6 +674,7 @@ export function App({ launchArgs }: AppProps) {
658
674
  setLocalProviderConfig(providerWorkspaceConfig.providers?.local);
659
675
  return buildProviderRegistry({
660
676
  activeModel: model,
677
+ workspaceRoot,
661
678
  workspaceConfig: providerWorkspaceConfig,
662
679
  diagnostics: providerDiagnosticsRef.current,
663
680
  routeErrors: providerRouteErrorsRef.current,
@@ -667,7 +684,7 @@ export function App({ launchArgs }: AppProps) {
667
684
  // updates mutate providerDiagnosticsRef/providerRouteErrorsRef (refs, not state)
668
685
  // and then increment the nonce to trigger a re-read of those refs.
669
686
  // eslint-disable-next-line react-hooks/exhaustive-deps
670
- [model, providerWorkspaceConfig, registryNonce],
687
+ [model, providerWorkspaceConfig, registryNonce, workspaceRoot],
671
688
  );
672
689
  const workspaceDefaultProvider = useMemo(
673
690
  () => providerRegistry.find((provider) => provider.isDefault) ?? providerRegistry[0] ?? null,
@@ -717,7 +734,10 @@ export function App({ launchArgs }: AppProps) {
717
734
  const modelPickerDiscovery = useMemo(() => {
718
735
  if (modelPickerProviderId === "openai") return null;
719
736
  return discoverProviderModels(modelPickerProviderId);
720
- }, [modelPickerProviderId]);
737
+ // registryNonce is intentionally included: route validation discovers models
738
+ // as a side effect and bumps the nonce so an open picker re-reads them.
739
+ // eslint-disable-next-line react-hooks/exhaustive-deps
740
+ }, [modelPickerProviderId, registryNonce]);
721
741
  const providerModelCapabilities = useMemo(() => {
722
742
  if (!modelPickerDiscovery) return null;
723
743
  return providerModelsToCodexCapabilities(modelPickerDiscovery.models, activeProviderRoute.modelId);
@@ -1568,6 +1588,9 @@ export function App({ launchArgs }: AppProps) {
1568
1588
  providerRouteErrorsRef.current["anthropic"] = result.message ?? ANTHROPIC_ROUTE_SETUP_MESSAGE;
1569
1589
  } else {
1570
1590
  delete providerRouteErrorsRef.current["anthropic"];
1591
+ // Persist the freshly discovered catalog so newly released Claude
1592
+ // models replace the stale on-disk cache without a manual refresh.
1593
+ persistProviderDiscovery(discoverProviderModels("anthropic"));
1571
1594
  }
1572
1595
  } catch {
1573
1596
  // Best-effort probe — failures are surfaced only when the user activates the route.
@@ -1603,54 +1626,53 @@ export function App({ launchArgs }: AppProps) {
1603
1626
  // eslint-disable-next-line react-hooks/exhaustive-deps
1604
1627
  }, [workspaceRoot]);
1605
1628
 
1606
- // Non-blocking background update check — runs once at startup.
1629
+ // Non-blocking background update check — fetches npm on every interactive startup.
1607
1630
  useEffect(() => {
1608
1631
  const ucSettings = initialSettings.current.updateCheck ?? DEFAULT_UPDATE_CHECK_SETTINGS;
1609
1632
  if (!shouldRunStartupUpdateCheck(process.env, ucSettings.enabled)) return;
1610
1633
 
1611
- const timer = setTimeout(() => {
1612
- void (async () => {
1613
- try {
1614
- const cache = loadUpdateCheckCache();
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
- }
1627
- return;
1628
- }
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
- }
1644
- } catch {
1645
- // Never crash the TUI on a failed update check.
1646
- }
1647
- })();
1648
- }, 2000);
1649
-
1650
- return () => clearTimeout(timer);
1634
+ void (async () => {
1635
+ const cache = loadUpdateCheckCache();
1636
+ try {
1637
+ const result = await checkForUpdates({ enabled: true });
1638
+ if (result.status === "error") {
1639
+ // A previously confirmed update is still useful when npm is briefly
1640
+ // unreachable, but it must never suppress the next fresh startup check.
1641
+ if (cache?.updateAvailable && cache.latestVersion && isCacheForRunningVersion(cache, APP_VERSION)) {
1642
+ setUpdateCheckResult({
1643
+ status: "update-available",
1644
+ currentVersion: cache.currentVersion,
1645
+ latestVersion: cache.latestVersion,
1646
+ checkedAt: cache.lastChecked,
1647
+ source: "cache",
1648
+ });
1649
+ }
1650
+ return;
1651
+ }
1652
+
1653
+ setUpdateCheckResult(result);
1654
+ saveUpdateCheckCache({
1655
+ lastChecked: result.checkedAt,
1656
+ currentVersion: result.currentVersion,
1657
+ latestVersion: result.latestVersion,
1658
+ updateAvailable: result.status === "update-available",
1659
+ });
1660
+ } catch {
1661
+ // Never crash the TUI on a failed update check.
1662
+ }
1663
+ })();
1651
1664
  // eslint-disable-next-line react-hooks/exhaustive-deps
1652
1665
  }, []);
1653
1666
 
1667
+ // A startup update prompt must not interrupt an active run or another panel.
1668
+ // Keep the result until the user returns to the idle main screen instead.
1669
+ useEffect(() => {
1670
+ if (startupUpdateDismissed || busy || screen !== "main") return;
1671
+ if (updateCheckResult?.status === "update-available" && updateCheckResult.latestVersion) {
1672
+ setScreen("update-prompt");
1673
+ }
1674
+ }, [busy, screen, startupUpdateDismissed, updateCheckResult]);
1675
+
1654
1676
  // Track clear epoch to suppress stale command result events
1655
1677
  useEffect(() => {
1656
1678
  clearEpochRef.current = sessionState.clearEpoch;
@@ -1993,6 +2015,12 @@ export function App({ launchArgs }: AppProps) {
1993
2015
  }
1994
2016
 
1995
2017
  modelSelectionInFlightRef.current = true;
2018
+ // Reflect the target provider in the model picker immediately: route
2019
+ // validation below can take seconds (subprocess probes), and until it
2020
+ // persists the new active route, modelPickerProviderId would otherwise
2021
+ // keep resolving to the stale route.
2022
+ setPendingRouteProviderId(providerId);
2023
+ setRouteSwitchBusy(true);
1996
2024
  traceInputDebug("model_selection_app_start", getInputDebugSnapshot({
1997
2025
  handler: "setModelAndReasoningWithNotice",
1998
2026
  model: nextModel,
@@ -2062,6 +2090,10 @@ export function App({ launchArgs }: AppProps) {
2062
2090
  model: nextModel,
2063
2091
  reasoningLevel: normalizedReasoning,
2064
2092
  }));
2093
+ if (providerId === "mistral" && activeProviderRoute.providerId !== "mistral") {
2094
+ // Switching to Vibe starts a fresh CLI conversation instead of resuming a stale one.
2095
+ resetMistralVibeSession(workspaceRoot);
2096
+ }
2065
2097
  persistActiveRoute(providerId, nextModel, normalizedReasoning, validation.backendKind, geminiSelection);
2066
2098
  setPendingRouteProviderId(null);
2067
2099
  traceInputDebug("model_selection_app_success", getInputDebugSnapshot({
@@ -2079,8 +2111,10 @@ export function App({ launchArgs }: AppProps) {
2079
2111
  reasoning: nextReasoning,
2080
2112
  error: message,
2081
2113
  }));
2114
+ setPendingRouteProviderId(null);
2082
2115
  appendErrorEvent("Model selection failed", message);
2083
2116
  } finally {
2117
+ setRouteSwitchBusy(false);
2084
2118
  modelSelectionInFlightRef.current = false;
2085
2119
  returnToChatMode("selection");
2086
2120
  }
@@ -2126,32 +2160,10 @@ export function App({ launchArgs }: AppProps) {
2126
2160
  }, [applyWorkspaceDisplayMode, showBusyLoader, terminalMouseMode, terminalTitleMode, workspaceDisplayMode]);
2127
2161
 
2128
2162
  const handleSkipUpdateForSession = useCallback(() => {
2163
+ setStartupUpdateDismissed(true);
2129
2164
  setScreen("main");
2130
2165
  }, []);
2131
2166
 
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
-
2155
2167
  const setApprovalPolicyWithNotice = useCallback((nextValue: RuntimeApprovalPolicy) => {
2156
2168
  const gate = guardConfigMutation("mode", busy);
2157
2169
  if (!gate.allowed) {
@@ -2269,7 +2281,11 @@ export function App({ launchArgs }: AppProps) {
2269
2281
  }, [appendSystemEvent, busy]);
2270
2282
 
2271
2283
  const openProviderPicker = useCallback(() => {
2272
- setPendingRouteProviderId(null);
2284
+ // Mid route switch, keep the pending id so initialProviderId highlights
2285
+ // the provider being activated instead of the stale route.
2286
+ if (!modelSelectionInFlightRef.current) {
2287
+ setPendingRouteProviderId(null);
2288
+ }
2273
2289
  const gate = guardConfigMutation("backend", busy);
2274
2290
  if (!gate.allowed) {
2275
2291
  appendSystemEvent("Busy", gate.message ?? "Finish the current run before opening providers.");
@@ -2277,6 +2293,31 @@ export function App({ launchArgs }: AppProps) {
2277
2293
  }
2278
2294
 
2279
2295
  setScreen("provider-picker");
2296
+ providerDiagnosticsRef.current.mistral = {
2297
+ ...providerDiagnosticsRef.current.mistral,
2298
+ selectedModel: detectVibeActiveModel({ cwd: workspaceRoot }).modelId,
2299
+ availabilityStatus: "checking",
2300
+ };
2301
+ void resolveVibeExecutable({ cwd: workspaceRoot }).then((resolvedCommand) => {
2302
+ if (!isMountedRef.current) return;
2303
+ const modelDetection = detectVibeActiveModel({ cwd: workspaceRoot });
2304
+ providerDiagnosticsRef.current.mistral = {
2305
+ resolvedCommand,
2306
+ selectedModel: modelDetection.modelId,
2307
+ modelSource: modelDetection.source,
2308
+ configPath: modelDetection.configPath,
2309
+ availabilityStatus: resolvedCommand ? "available" : "unavailable",
2310
+ };
2311
+ setRegistryNonce((n) => n + 1);
2312
+ }).catch(() => {
2313
+ if (!isMountedRef.current) return;
2314
+ providerDiagnosticsRef.current.mistral = {
2315
+ selectedModel: detectVibeActiveModel({ cwd: workspaceRoot }).modelId,
2316
+ resolvedCommand: null,
2317
+ availabilityStatus: "unavailable",
2318
+ };
2319
+ setRegistryNonce((n) => n + 1);
2320
+ });
2280
2321
  markProviderAvailability("local", "checking", "provider-picker-open");
2281
2322
  void checkLocalProvider({ override: providerWorkspaceConfig.providers?.local }).then((result) => {
2282
2323
  if (!isMountedRef.current) return;
@@ -2290,7 +2331,7 @@ export function App({ launchArgs }: AppProps) {
2290
2331
  }
2291
2332
  setRegistryNonce((n) => n + 1);
2292
2333
  }).catch(() => undefined);
2293
- }, [appendSystemEvent, busy, markProviderAvailability, providerWorkspaceConfig.providers]);
2334
+ }, [appendSystemEvent, busy, markProviderAvailability, providerWorkspaceConfig.providers, workspaceRoot]);
2294
2335
 
2295
2336
  const setWorkspaceDefaultProviderWithNotice = useCallback((providerId: ProviderId) => {
2296
2337
  const provider = findProvider(providerRegistry, providerId);
@@ -2307,7 +2348,9 @@ export function App({ launchArgs }: AppProps) {
2307
2348
  const routeConfigured = isProviderRouteConfigured(providerId);
2308
2349
  appendSystemEvent(
2309
2350
  "Provider default updated",
2310
- provider.routeMode === "in-codexa" && routeConfigured
2351
+ provider.routeMode === "launch-only"
2352
+ ? `${provider.displayName} is now the workspace default external CLI. Active chat route remains ${activeRouteProvider?.displayName ?? "OpenAI"} / ${model}.`
2353
+ : provider.routeMode === "in-codexa" && routeConfigured
2311
2354
  ? `${provider.displayName} is now the workspace default provider. Active chat route remains ${activeRouteProvider?.displayName ?? "OpenAI"} / ${model}.`
2312
2355
  : `${provider.displayName} is set as the workspace default, but in-Codexa routing is not configured yet. Active chat route remains ${activeRouteProvider?.displayName ?? "OpenAI"} / ${model}.`,
2313
2356
  );
@@ -2466,6 +2509,7 @@ export function App({ launchArgs }: AppProps) {
2466
2509
  cwd: workspaceRoot,
2467
2510
  localConfig: providerId === "local" ? providerWorkspaceConfig.providers?.local : undefined,
2468
2511
  }).then((discovery) => {
2512
+ persistProviderDiscovery(discovery);
2469
2513
  if (discovery.diagnostics) {
2470
2514
  providerDiagnosticsRef.current[providerId] = discovery.diagnostics as Record<string, string | number | boolean | null>;
2471
2515
  }
@@ -2557,10 +2601,10 @@ export function App({ launchArgs }: AppProps) {
2557
2601
  setScreen("main");
2558
2602
  appendSystemEvent(
2559
2603
  "Provider launch",
2560
- `Suspending Codexa and launching ${provider.displayName}. Codexa will resume when the external CLI exits.`,
2604
+ `Suspending Codexa and launching ${provider.displayName}${providerId === "mistral" ? ` / ${provider.currentModel}` : ""}. Codexa will resume when the external CLI exits.`,
2561
2605
  );
2562
2606
 
2563
- void launchProviderCli(provider, {
2607
+ const launchOptions = {
2564
2608
  cwd: workspaceRoot,
2565
2609
  stdin,
2566
2610
  beforeLaunch: () => {
@@ -2570,9 +2614,18 @@ export function App({ launchArgs }: AppProps) {
2570
2614
  afterLaunch: () => {
2571
2615
  terminalControl.setMouseReporting(effectiveMouseCapture, "src/app.tsx:providerLaunch.restoreMouse");
2572
2616
  },
2573
- }).then((result) => {
2617
+ };
2618
+ const launchPromise = providerId === "mistral"
2619
+ ? launchMistralVibeCli(provider, launchOptions)
2620
+ : launchProviderCli(provider, launchOptions);
2621
+
2622
+ void launchPromise.then((result) => {
2574
2623
  if (!isMountedRef.current) return;
2575
- appendSystemEvent("Provider launch", result.message);
2624
+ if (providerId === "mistral" && (result.status === "missing-command" || result.status === "spawn-error")) {
2625
+ appendErrorEvent("Mistral Vibe launch failed", result.message);
2626
+ } else {
2627
+ appendSystemEvent("Provider launch", result.message);
2628
+ }
2576
2629
  }).catch((error) => {
2577
2630
  if (!isMountedRef.current) return;
2578
2631
  const message = error instanceof Error ? error.message : "Provider launch failed.";
@@ -2599,7 +2652,11 @@ export function App({ launchArgs }: AppProps) {
2599
2652
  ]);
2600
2653
 
2601
2654
  const openModelPicker = useCallback(() => {
2602
- setPendingRouteProviderId(null);
2655
+ // While a route switch is validating, keep the pending id so the picker
2656
+ // shows the target provider instead of snapping back to the stale route.
2657
+ if (!modelSelectionInFlightRef.current) {
2658
+ setPendingRouteProviderId(null);
2659
+ }
2603
2660
  traceInputDebug("model_picker_open_request", getInputDebugSnapshot({
2604
2661
  handler: "openModelPicker",
2605
2662
  currentScreen: screen,
@@ -3121,6 +3178,7 @@ export function App({ launchArgs }: AppProps) {
3121
3178
  activeTurnIdRef.current = null;
3122
3179
  activeRunLifecycleRef.current = null;
3123
3180
  activeRunTimingRef.current = null;
3181
+ resetMistralVibeSession(workspaceRoot);
3124
3182
  setPlanFlow(resetPlanFlow());
3125
3183
  renderDebug.traceEvent("terminal", "clearReactStateRequested", {
3126
3184
  clearGeneration,
@@ -3139,7 +3197,7 @@ export function App({ launchArgs }: AppProps) {
3139
3197
  liveInkInstanceResolved: Boolean(inkInstance),
3140
3198
  });
3141
3199
  }
3142
- }, [cancelActiveRun, clearFrameBoundaryController, inkInstance, launchContext, providerWorkspaceConfig, resetToHomeScreen, sessionState.clearEpoch, stdout.columns, terminalControl]);
3200
+ }, [cancelActiveRun, clearFrameBoundaryController, inkInstance, launchContext, providerWorkspaceConfig, resetToHomeScreen, sessionState.clearEpoch, stdout.columns, terminalControl, workspaceRoot]);
3143
3201
 
3144
3202
  const handleShellExecute = useCallback((command: string) => {
3145
3203
  const safeCommand = sanitizeTerminalInput(command).trim();
@@ -4174,13 +4232,13 @@ export function App({ launchArgs }: AppProps) {
4174
4232
  case "diagnose_providers": {
4175
4233
  const lines: string[] = ["Provider CLI diagnostics:"];
4176
4234
  const diags = providerDiagnosticsRef.current;
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
- };
4235
+ const providerIds = ["openai", "anthropic", "local", "antigravity"] as const;
4236
+ const labels: Record<string, string> = {
4237
+ openai: "OpenAI/Codex",
4238
+ anthropic: "Anthropic/Claude",
4239
+ local: "Local OpenAI-compatible",
4240
+ antigravity: "Antigravity CLI",
4241
+ };
4184
4242
  for (const id of providerIds) {
4185
4243
  const diag = diags[id];
4186
4244
  lines.push(`\n ${labels[id] ?? id}:`);
@@ -4355,7 +4413,10 @@ export function App({ launchArgs }: AppProps) {
4355
4413
  if (freshResult?.status === "update-available" && freshResult.latestVersion) {
4356
4414
  setScreen("update-prompt");
4357
4415
  } else {
4358
- appendSystemEvent("Update", formatUpdateInstructions(freshResult));
4416
+ appendSystemEvent(
4417
+ "Update",
4418
+ formatUpdateInstructions(freshResult, getUpdateCommand(globalPackageManager).displayCommand),
4419
+ );
4359
4420
  }
4360
4421
  })();
4361
4422
  return;
@@ -4447,6 +4508,7 @@ export function App({ launchArgs }: AppProps) {
4447
4508
  dispatchSession,
4448
4509
  findUserPromptForTurn,
4449
4510
  focusManager,
4511
+ globalPackageManager,
4450
4512
  handleCopy,
4451
4513
  handleClear,
4452
4514
  handleQuit,
@@ -4671,8 +4733,12 @@ export function App({ launchArgs }: AppProps) {
4671
4733
  clearCount={sessionState.clearCount}
4672
4734
  headerConfig={effectiveHeaderConfig}
4673
4735
  updateAvailable={
4674
- updateCheckResult?.status === "update-available" && updateCheckResult.latestVersion
4675
- ? { latestVersion: updateCheckResult.latestVersion, currentVersion: updateCheckResult.currentVersion }
4736
+ screen !== "update-prompt" && updateCheckResult?.status === "update-available" && updateCheckResult.latestVersion
4737
+ ? {
4738
+ latestVersion: updateCheckResult.latestVersion,
4739
+ currentVersion: updateCheckResult.currentVersion,
4740
+ updateCommand: getUpdateCommand(globalPackageManager).displayCommand,
4741
+ }
4676
4742
  : null
4677
4743
  }
4678
4744
  panel={
@@ -4705,7 +4771,8 @@ export function App({ launchArgs }: AppProps) {
4705
4771
  currentModel={modelPickerCurrentModel}
4706
4772
  currentReasoning={modelPickerCurrentReasoning}
4707
4773
  activeProviderLabel={modelPickerProviderLabel}
4708
- isLoading={modelPickerProviderId === "openai" && modelCapabilitiesBusy && modelPickerModels.length === 0}
4774
+ isLoading={modelPickerModels.length === 0
4775
+ && ((modelPickerProviderId === "openai" && modelCapabilitiesBusy) || routeSwitchBusy)}
4709
4776
  emptyMessage={modelPickerEmptyMessage}
4710
4777
  onSelect={(m, r, geminiSelection) => {
4711
4778
  if (pendingRouteProviderId && pendingRouteProviderId !== activeProviderRoute.providerId) {
@@ -4929,9 +4996,9 @@ export function App({ launchArgs }: AppProps) {
4929
4996
  focusId={FOCUS_IDS.updatePrompt}
4930
4997
  currentVersion={updateCheckResult.currentVersion}
4931
4998
  latestVersion={updateCheckResult.latestVersion}
4932
- onSkip={handleSkipUpdateForSession}
4933
- onSkipUntilNextVersion={handleSkipUpdateVersion}
4934
- />
4999
+ packageManager={globalPackageManager}
5000
+ onSkip={handleSkipUpdateForSession}
5001
+ />
4935
5002
  )}
4936
5003
  </>
4937
5004
  }