@golba98/codexa 1.0.4 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/package.json +1 -1
  2. package/src/app.tsx +128 -47
  3. package/src/commands/handler.ts +5 -16
  4. package/src/config/buildInfo.ts +2 -2
  5. package/src/core/models/codexModelCapabilities.ts +57 -4
  6. package/src/core/models/codexModelsCacheSeed.ts +153 -0
  7. package/src/core/models/providerModelCache.ts +106 -0
  8. package/src/core/process/CommandRunner.ts +12 -1
  9. package/src/core/providerLauncher/registry.ts +37 -7
  10. package/src/core/providerLauncher/types.ts +2 -1
  11. package/src/core/providerRuntime/anthropic.ts +3 -5
  12. package/src/core/providerRuntime/antigravity.ts +246 -90
  13. package/src/core/providerRuntime/claudeCodeDiscovery.ts +724 -692
  14. package/src/core/providerRuntime/mistralVibe.ts +663 -0
  15. package/src/core/providerRuntime/models.ts +18 -4
  16. package/src/core/providerRuntime/reasoning.ts +9 -6
  17. package/src/core/providerRuntime/registry.ts +94 -49
  18. package/src/core/providerRuntime/types.ts +1 -0
  19. package/src/index.tsx +23 -9
  20. package/src/test/runtimeTestUtils.ts +90 -0
  21. package/src/ui/{ActivityBars.tsx → chrome/ActivityBars.tsx} +1 -1
  22. package/src/ui/{ActivityIndicator.tsx → chrome/ActivityIndicator.tsx} +3 -3
  23. package/src/ui/{AnimatedStatusText.tsx → chrome/AnimatedStatusText.tsx} +3 -3
  24. package/src/ui/{AppShell.tsx → chrome/AppShell.tsx} +11 -11
  25. package/src/ui/{BottomComposer.tsx → chrome/BottomComposer.tsx} +15 -15
  26. package/src/ui/{DashCard.tsx → chrome/DashCard.tsx} +2 -2
  27. package/src/ui/{RunFooter.tsx → chrome/RunFooter.tsx} +3 -3
  28. package/src/ui/{RuntimeStatusBar.tsx → chrome/RuntimeStatusBar.tsx} +3 -3
  29. package/src/ui/{Spinner.tsx → chrome/Spinner.tsx} +1 -1
  30. package/src/ui/{TopHeader.tsx → chrome/TopHeader.tsx} +12 -12
  31. package/src/ui/{UpdateAvailableCard.tsx → chrome/UpdateAvailableCard.tsx} +3 -3
  32. package/src/ui/{commandNormalize.ts → input/commandNormalize.ts} +1 -1
  33. package/src/ui/{focus.ts → input/focus.ts} +1 -1
  34. package/src/ui/{inputBuffer.ts → input/inputBuffer.ts} +3 -3
  35. package/src/ui/{AttachmentImportPanel.tsx → panels/AttachmentImportPanel.tsx} +1 -1
  36. package/src/ui/{AuthPanel.tsx → panels/AuthPanel.tsx} +5 -5
  37. package/src/ui/{BackendPicker.tsx → panels/BackendPicker.tsx} +2 -2
  38. package/src/ui/{ModePicker.tsx → panels/ModePicker.tsx} +2 -2
  39. package/src/ui/{ModelPicker.tsx → panels/ModelPicker.tsx} +2 -2
  40. package/src/ui/{ModelPickerScreen.tsx → panels/ModelPickerScreen.tsx} +11 -9
  41. package/src/ui/{ModelReasoningPicker.tsx → panels/ModelReasoningPicker.tsx} +5 -5
  42. package/src/ui/{Panel.tsx → panels/Panel.tsx} +1 -1
  43. package/src/ui/{PermissionsPanel.tsx → panels/PermissionsPanel.tsx} +3 -3
  44. package/src/ui/{PlanActionPicker.tsx → panels/PlanActionPicker.tsx} +2 -2
  45. package/src/ui/{PlanReviewPanel.tsx → panels/PlanReviewPanel.tsx} +5 -5
  46. package/src/ui/{ProviderPicker.tsx → panels/ProviderPicker.tsx} +9 -7
  47. package/src/ui/{ReasoningPicker.tsx → panels/ReasoningPicker.tsx} +3 -3
  48. package/src/ui/{SelectionPanel.tsx → panels/SelectionPanel.tsx} +1 -1
  49. package/src/ui/{SettingsPanel.tsx → panels/SettingsPanel.tsx} +3 -3
  50. package/src/ui/{TextEntryPanel.tsx → panels/TextEntryPanel.tsx} +3 -3
  51. package/src/ui/{ThemePicker.tsx → panels/ThemePicker.tsx} +2 -2
  52. package/src/ui/{UpdatePromptPanel.tsx → panels/UpdatePromptPanel.tsx} +2 -2
  53. package/src/ui/{Markdown.tsx → render/Markdown.tsx} +2 -2
  54. package/src/ui/{diffRenderer.ts → render/diffRenderer.ts} +1 -1
  55. package/src/ui/{modeDisplay.ts → render/modeDisplay.ts} +2 -2
  56. package/src/ui/{outputPipeline.ts → render/outputPipeline.ts} +2 -2
  57. package/src/ui/{runtimeDisplay.ts → render/runtimeDisplay.ts} +9 -8
  58. package/src/ui/{ActionRequiredBlock.tsx → timeline/ActionRequiredBlock.tsx} +3 -3
  59. package/src/ui/{AgentBlock.tsx → timeline/AgentBlock.tsx} +10 -10
  60. package/src/ui/{StaticIntroItem.tsx → timeline/StaticIntroItem.tsx} +2 -2
  61. package/src/ui/{ThinkingBlock.tsx → timeline/ThinkingBlock.tsx} +4 -4
  62. package/src/ui/{Timeline.tsx → timeline/Timeline.tsx} +8 -8
  63. package/src/ui/{TranscriptShell.tsx → timeline/TranscriptShell.tsx} +6 -6
  64. package/src/ui/{TurnGroup.tsx → timeline/TurnGroup.tsx} +15 -15
  65. package/src/ui/{progressEntries.ts → timeline/progressEntries.ts} +3 -3
  66. package/src/ui/{runActivityView.ts → timeline/runActivityView.ts} +1 -1
  67. package/src/ui/{timelineMeasure.ts → timeline/timelineMeasure.ts} +14 -14
  68. /package/src/ui/{busyStatusAnimation.ts → chrome/busyStatusAnimation.ts} +0 -0
  69. /package/src/ui/{slashCommands.ts → input/slashCommands.ts} +0 -0
  70. /package/src/ui/{logoVariants.ts → render/logoVariants.ts} +0 -0
  71. /package/src/ui/{terminalAnswerFormat.ts → render/terminalAnswerFormat.ts} +0 -0
  72. /package/src/ui/{textLayout.ts → render/textLayout.ts} +0 -0
  73. /package/src/ui/{layoutListWindow.ts → timeline/layoutListWindow.ts} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@golba98/codexa",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
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,9 +266,9 @@ 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";
264
273
  import { isLocalDevChannel } from "./core/version/channel.js";
265
274
  import {
@@ -319,7 +328,7 @@ function createProviderMigrationNoticeEvent(
319
328
  createdAt: Date.now(),
320
329
  title: sanitizeTerminalOutput("Provider migrated"),
321
330
  content: sanitizeTerminalOutput(
322
- `${formatRuntimeProviderLabel(notice.deprecatedProviderId as ProviderId)} provider is no longer supported. Reverted to ${resolvedProviderLabel}.`,
331
+ `${formatRuntimeProviderLabel(notice.deprecatedProviderId as ProviderId)} provider is no longer supported. Reverted to ${resolvedProviderLabel}.`,
323
332
  { preserveTabs: false, tabSize: 2 },
324
333
  ),
325
334
  };
@@ -483,8 +492,14 @@ export function App({ launchArgs }: AppProps) {
483
492
  const [authStatusBusy, setAuthStatusBusy] = useState(false);
484
493
  // Running character total across the conversation — used to estimate token usage
485
494
  const [conversationChars, setConversationChars] = useState(0);
486
- const [modelCapabilities, setModelCapabilities] = useState<CodexModelCapabilities | null>(null);
495
+ // Seeded synchronously from local caches (codex's models_cache.json or the
496
+ // persisted last-good discovery) so the model picker opens instantly with
497
+ // real models; live discovery replaces this in the background.
498
+ const [modelCapabilities, setModelCapabilities] = useState<CodexModelCapabilities | null>(() => loadSeededCodexCapabilities());
487
499
  const [modelCapabilitiesBusy, setModelCapabilitiesBusy] = useState(false);
500
+ // True while a provider route switch is validating (subprocess probes);
501
+ // drives the model picker's loading state for non-openai providers.
502
+ const [routeSwitchBusy, setRouteSwitchBusy] = useState(false);
488
503
  const [activeContextMetadata, setActiveContextMetadata] = useState<ModelContextMetadata | null>(null);
489
504
  const { stdout } = useStdout();
490
505
  const { stdin } = useStdin();
@@ -658,6 +673,7 @@ export function App({ launchArgs }: AppProps) {
658
673
  setLocalProviderConfig(providerWorkspaceConfig.providers?.local);
659
674
  return buildProviderRegistry({
660
675
  activeModel: model,
676
+ workspaceRoot,
661
677
  workspaceConfig: providerWorkspaceConfig,
662
678
  diagnostics: providerDiagnosticsRef.current,
663
679
  routeErrors: providerRouteErrorsRef.current,
@@ -667,7 +683,7 @@ export function App({ launchArgs }: AppProps) {
667
683
  // updates mutate providerDiagnosticsRef/providerRouteErrorsRef (refs, not state)
668
684
  // and then increment the nonce to trigger a re-read of those refs.
669
685
  // eslint-disable-next-line react-hooks/exhaustive-deps
670
- [model, providerWorkspaceConfig, registryNonce],
686
+ [model, providerWorkspaceConfig, registryNonce, workspaceRoot],
671
687
  );
672
688
  const workspaceDefaultProvider = useMemo(
673
689
  () => providerRegistry.find((provider) => provider.isDefault) ?? providerRegistry[0] ?? null,
@@ -717,7 +733,10 @@ export function App({ launchArgs }: AppProps) {
717
733
  const modelPickerDiscovery = useMemo(() => {
718
734
  if (modelPickerProviderId === "openai") return null;
719
735
  return discoverProviderModels(modelPickerProviderId);
720
- }, [modelPickerProviderId]);
736
+ // registryNonce is intentionally included: route validation discovers models
737
+ // as a side effect and bumps the nonce so an open picker re-reads them.
738
+ // eslint-disable-next-line react-hooks/exhaustive-deps
739
+ }, [modelPickerProviderId, registryNonce]);
721
740
  const providerModelCapabilities = useMemo(() => {
722
741
  if (!modelPickerDiscovery) return null;
723
742
  return providerModelsToCodexCapabilities(modelPickerDiscovery.models, activeProviderRoute.modelId);
@@ -1568,6 +1587,9 @@ export function App({ launchArgs }: AppProps) {
1568
1587
  providerRouteErrorsRef.current["anthropic"] = result.message ?? ANTHROPIC_ROUTE_SETUP_MESSAGE;
1569
1588
  } else {
1570
1589
  delete providerRouteErrorsRef.current["anthropic"];
1590
+ // Persist the freshly discovered catalog so newly released Claude
1591
+ // models replace the stale on-disk cache without a manual refresh.
1592
+ persistProviderDiscovery(discoverProviderModels("anthropic"));
1571
1593
  }
1572
1594
  } catch {
1573
1595
  // Best-effort probe — failures are surfaced only when the user activates the route.
@@ -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
  }
@@ -2269,7 +2303,11 @@ export function App({ launchArgs }: AppProps) {
2269
2303
  }, [appendSystemEvent, busy]);
2270
2304
 
2271
2305
  const openProviderPicker = useCallback(() => {
2272
- setPendingRouteProviderId(null);
2306
+ // Mid route switch, keep the pending id so initialProviderId highlights
2307
+ // the provider being activated instead of the stale route.
2308
+ if (!modelSelectionInFlightRef.current) {
2309
+ setPendingRouteProviderId(null);
2310
+ }
2273
2311
  const gate = guardConfigMutation("backend", busy);
2274
2312
  if (!gate.allowed) {
2275
2313
  appendSystemEvent("Busy", gate.message ?? "Finish the current run before opening providers.");
@@ -2277,6 +2315,31 @@ export function App({ launchArgs }: AppProps) {
2277
2315
  }
2278
2316
 
2279
2317
  setScreen("provider-picker");
2318
+ providerDiagnosticsRef.current.mistral = {
2319
+ ...providerDiagnosticsRef.current.mistral,
2320
+ selectedModel: detectVibeActiveModel({ cwd: workspaceRoot }).modelId,
2321
+ availabilityStatus: "checking",
2322
+ };
2323
+ void resolveVibeExecutable({ cwd: workspaceRoot }).then((resolvedCommand) => {
2324
+ if (!isMountedRef.current) return;
2325
+ const modelDetection = detectVibeActiveModel({ cwd: workspaceRoot });
2326
+ providerDiagnosticsRef.current.mistral = {
2327
+ resolvedCommand,
2328
+ selectedModel: modelDetection.modelId,
2329
+ modelSource: modelDetection.source,
2330
+ configPath: modelDetection.configPath,
2331
+ availabilityStatus: resolvedCommand ? "available" : "unavailable",
2332
+ };
2333
+ setRegistryNonce((n) => n + 1);
2334
+ }).catch(() => {
2335
+ if (!isMountedRef.current) return;
2336
+ providerDiagnosticsRef.current.mistral = {
2337
+ selectedModel: detectVibeActiveModel({ cwd: workspaceRoot }).modelId,
2338
+ resolvedCommand: null,
2339
+ availabilityStatus: "unavailable",
2340
+ };
2341
+ setRegistryNonce((n) => n + 1);
2342
+ });
2280
2343
  markProviderAvailability("local", "checking", "provider-picker-open");
2281
2344
  void checkLocalProvider({ override: providerWorkspaceConfig.providers?.local }).then((result) => {
2282
2345
  if (!isMountedRef.current) return;
@@ -2290,7 +2353,7 @@ export function App({ launchArgs }: AppProps) {
2290
2353
  }
2291
2354
  setRegistryNonce((n) => n + 1);
2292
2355
  }).catch(() => undefined);
2293
- }, [appendSystemEvent, busy, markProviderAvailability, providerWorkspaceConfig.providers]);
2356
+ }, [appendSystemEvent, busy, markProviderAvailability, providerWorkspaceConfig.providers, workspaceRoot]);
2294
2357
 
2295
2358
  const setWorkspaceDefaultProviderWithNotice = useCallback((providerId: ProviderId) => {
2296
2359
  const provider = findProvider(providerRegistry, providerId);
@@ -2307,7 +2370,9 @@ export function App({ launchArgs }: AppProps) {
2307
2370
  const routeConfigured = isProviderRouteConfigured(providerId);
2308
2371
  appendSystemEvent(
2309
2372
  "Provider default updated",
2310
- provider.routeMode === "in-codexa" && routeConfigured
2373
+ provider.routeMode === "launch-only"
2374
+ ? `${provider.displayName} is now the workspace default external CLI. Active chat route remains ${activeRouteProvider?.displayName ?? "OpenAI"} / ${model}.`
2375
+ : provider.routeMode === "in-codexa" && routeConfigured
2311
2376
  ? `${provider.displayName} is now the workspace default provider. Active chat route remains ${activeRouteProvider?.displayName ?? "OpenAI"} / ${model}.`
2312
2377
  : `${provider.displayName} is set as the workspace default, but in-Codexa routing is not configured yet. Active chat route remains ${activeRouteProvider?.displayName ?? "OpenAI"} / ${model}.`,
2313
2378
  );
@@ -2466,6 +2531,7 @@ export function App({ launchArgs }: AppProps) {
2466
2531
  cwd: workspaceRoot,
2467
2532
  localConfig: providerId === "local" ? providerWorkspaceConfig.providers?.local : undefined,
2468
2533
  }).then((discovery) => {
2534
+ persistProviderDiscovery(discovery);
2469
2535
  if (discovery.diagnostics) {
2470
2536
  providerDiagnosticsRef.current[providerId] = discovery.diagnostics as Record<string, string | number | boolean | null>;
2471
2537
  }
@@ -2557,10 +2623,10 @@ export function App({ launchArgs }: AppProps) {
2557
2623
  setScreen("main");
2558
2624
  appendSystemEvent(
2559
2625
  "Provider launch",
2560
- `Suspending Codexa and launching ${provider.displayName}. Codexa will resume when the external CLI exits.`,
2626
+ `Suspending Codexa and launching ${provider.displayName}${providerId === "mistral" ? ` / ${provider.currentModel}` : ""}. Codexa will resume when the external CLI exits.`,
2561
2627
  );
2562
2628
 
2563
- void launchProviderCli(provider, {
2629
+ const launchOptions = {
2564
2630
  cwd: workspaceRoot,
2565
2631
  stdin,
2566
2632
  beforeLaunch: () => {
@@ -2570,9 +2636,18 @@ export function App({ launchArgs }: AppProps) {
2570
2636
  afterLaunch: () => {
2571
2637
  terminalControl.setMouseReporting(effectiveMouseCapture, "src/app.tsx:providerLaunch.restoreMouse");
2572
2638
  },
2573
- }).then((result) => {
2639
+ };
2640
+ const launchPromise = providerId === "mistral"
2641
+ ? launchMistralVibeCli(provider, launchOptions)
2642
+ : launchProviderCli(provider, launchOptions);
2643
+
2644
+ void launchPromise.then((result) => {
2574
2645
  if (!isMountedRef.current) return;
2575
- appendSystemEvent("Provider launch", result.message);
2646
+ if (providerId === "mistral" && (result.status === "missing-command" || result.status === "spawn-error")) {
2647
+ appendErrorEvent("Mistral Vibe launch failed", result.message);
2648
+ } else {
2649
+ appendSystemEvent("Provider launch", result.message);
2650
+ }
2576
2651
  }).catch((error) => {
2577
2652
  if (!isMountedRef.current) return;
2578
2653
  const message = error instanceof Error ? error.message : "Provider launch failed.";
@@ -2599,7 +2674,11 @@ export function App({ launchArgs }: AppProps) {
2599
2674
  ]);
2600
2675
 
2601
2676
  const openModelPicker = useCallback(() => {
2602
- setPendingRouteProviderId(null);
2677
+ // While a route switch is validating, keep the pending id so the picker
2678
+ // shows the target provider instead of snapping back to the stale route.
2679
+ if (!modelSelectionInFlightRef.current) {
2680
+ setPendingRouteProviderId(null);
2681
+ }
2603
2682
  traceInputDebug("model_picker_open_request", getInputDebugSnapshot({
2604
2683
  handler: "openModelPicker",
2605
2684
  currentScreen: screen,
@@ -3121,6 +3200,7 @@ export function App({ launchArgs }: AppProps) {
3121
3200
  activeTurnIdRef.current = null;
3122
3201
  activeRunLifecycleRef.current = null;
3123
3202
  activeRunTimingRef.current = null;
3203
+ resetMistralVibeSession(workspaceRoot);
3124
3204
  setPlanFlow(resetPlanFlow());
3125
3205
  renderDebug.traceEvent("terminal", "clearReactStateRequested", {
3126
3206
  clearGeneration,
@@ -3139,7 +3219,7 @@ export function App({ launchArgs }: AppProps) {
3139
3219
  liveInkInstanceResolved: Boolean(inkInstance),
3140
3220
  });
3141
3221
  }
3142
- }, [cancelActiveRun, clearFrameBoundaryController, inkInstance, launchContext, providerWorkspaceConfig, resetToHomeScreen, sessionState.clearEpoch, stdout.columns, terminalControl]);
3222
+ }, [cancelActiveRun, clearFrameBoundaryController, inkInstance, launchContext, providerWorkspaceConfig, resetToHomeScreen, sessionState.clearEpoch, stdout.columns, terminalControl, workspaceRoot]);
3143
3223
 
3144
3224
  const handleShellExecute = useCallback((command: string) => {
3145
3225
  const safeCommand = sanitizeTerminalInput(command).trim();
@@ -4174,13 +4254,13 @@ export function App({ launchArgs }: AppProps) {
4174
4254
  case "diagnose_providers": {
4175
4255
  const lines: string[] = ["Provider CLI diagnostics:"];
4176
4256
  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
- };
4257
+ const providerIds = ["openai", "anthropic", "local", "antigravity"] as const;
4258
+ const labels: Record<string, string> = {
4259
+ openai: "OpenAI/Codex",
4260
+ anthropic: "Anthropic/Claude",
4261
+ local: "Local OpenAI-compatible",
4262
+ antigravity: "Antigravity CLI",
4263
+ };
4184
4264
  for (const id of providerIds) {
4185
4265
  const diag = diags[id];
4186
4266
  lines.push(`\n ${labels[id] ?? id}:`);
@@ -4705,7 +4785,8 @@ export function App({ launchArgs }: AppProps) {
4705
4785
  currentModel={modelPickerCurrentModel}
4706
4786
  currentReasoning={modelPickerCurrentReasoning}
4707
4787
  activeProviderLabel={modelPickerProviderLabel}
4708
- isLoading={modelPickerProviderId === "openai" && modelCapabilitiesBusy && modelPickerModels.length === 0}
4788
+ isLoading={modelPickerModels.length === 0
4789
+ && ((modelPickerProviderId === "openai" && modelCapabilitiesBusy) || routeSwitchBusy)}
4709
4790
  emptyMessage={modelPickerEmptyMessage}
4710
4791
  onSelect={(m, r, geminiSelection) => {
4711
4792
  if (pendingRouteProviderId && pendingRouteProviderId !== activeProviderRoute.providerId) {
@@ -18,7 +18,6 @@ import {
18
18
  AUTH_PREFERENCES,
19
19
  AVAILABLE_BACKENDS,
20
20
  AVAILABLE_MODELS,
21
- AVAILABLE_REASONING_LEVELS,
22
21
  AVAILABLE_THEMES,
23
22
  BUSY_LOADER_SETTING_VALUES,
24
23
  WORKSPACE_DISPLAY_MODES,
@@ -150,10 +149,6 @@ function expandReasoningAliases(arg: string): string {
150
149
  return normalized === "extra high" ? "xhigh" : normalized;
151
150
  }
152
151
 
153
- function isKnownFallbackReasoning(value: string): boolean {
154
- return AVAILABLE_REASONING_LEVELS.some((item) => item.id === value);
155
- }
156
-
157
152
  function simplePolicySetter<T extends string>(
158
153
  rest: string,
159
154
  normalizedRest: string,
@@ -520,17 +515,11 @@ export function handleCommand(text: string, context: CommandContext): CommandRes
520
515
  message: `Unknown reasoning level for ${context.runtime.model}: ${arg}. Valid: ${valid}`,
521
516
  };
522
517
  }
523
- if (isKnownFallbackReasoning(normalized)) {
524
- return {
525
- action: "reasoning",
526
- value: normalized,
527
- message: `Reasoning level switched to ${formatReasoningLabel(normalized)} (runtime metadata unavailable)`,
528
- };
529
- }
530
- return {
531
- action: "unknown",
532
- message: `Unknown reasoning level: ${arg}. Runtime reasoning metadata is unavailable for ${context.runtime.model}.`,
533
- };
518
+ return {
519
+ action: "reasoning",
520
+ value: normalized,
521
+ message: `Reasoning level switched to ${formatReasoningLabel(normalized)} (runtime metadata unavailable; unverified until runtime)`,
522
+ };
534
523
  }
535
524
 
536
525
  case "plan": {
@@ -1,3 +1,3 @@
1
1
  // Generated by scripts/gen-build-info.mjs — do not edit manually.
2
- export const BUILD_COMMIT = "b066a15b77696d2a0945ea7c6c3d07630584296b" as const;
3
- export const APP_VERSION = "1.0.4" as const;
2
+ export const BUILD_COMMIT = "aaffb655176a0acb1bc7800ddbb3e6402e5933f4" as const;
3
+ export const APP_VERSION = "1.0.5" as const;
@@ -1,6 +1,9 @@
1
1
  import { type ChildProcess } from "child_process";
2
2
  import { APP_NAME, APP_VERSION, DEFAULT_MODEL, LEGACY_FALLBACK_MODELS, formatReasoningLabel } from "../../config/settings.js";
3
3
  import { resolveCodexExecutable, spawnCodexProcess } from "../executables/codexExecutable.js";
4
+ import { loadSeededCodexCapabilities } from "./codexModelsCacheSeed.js";
5
+ import { saveCachedProviderModels } from "./providerModelCache.js";
6
+ import type { ProviderModel } from "../providerRuntime/types.js";
4
7
 
5
8
  export type ModelCapabilitySource = "runtime" | "fallback";
6
9
  export type ModelCapabilityStatus = "ready" | "fallback";
@@ -47,6 +50,8 @@ export interface GetCodexModelCapabilitiesOptions extends DiscoverCodexModelCapa
47
50
  ttlMs?: number;
48
51
  resolveExecutable?: typeof resolveCodexExecutable;
49
52
  discover?: typeof discoverCodexModelCapabilities;
53
+ seed?: typeof loadSeededCodexCapabilities;
54
+ persist?: typeof persistCodexModelCapabilities;
50
55
  }
51
56
 
52
57
  interface JsonRpcResponse {
@@ -439,14 +444,36 @@ export async function discoverCodexModelCapabilities(
439
444
  });
440
445
  }
441
446
 
442
- // Cache chain: TTL-based in-memory cache live discovery fallback model list on error.
443
- // Failed discoveries are evicted from the cache so retries are possible.
447
+ // Persist a successful live discovery so the next launch can seed the model
448
+ // picker without spawning the codex app-server. Best-effort by design.
449
+ export function persistCodexModelCapabilities(capabilities: CodexModelCapabilities): void {
450
+ const models: ProviderModel[] = capabilities.models
451
+ .filter((capability) => !capability.hidden)
452
+ .map((capability) => ({
453
+ id: capability.id,
454
+ modelId: capability.model,
455
+ label: capability.label,
456
+ description: capability.description,
457
+ defaultReasoningLevel: capability.defaultReasoningLevel,
458
+ supportedReasoningLevels: capability.supportedReasoningLevels,
459
+ source: "discovered",
460
+ }));
461
+ saveCachedProviderModels("openai", { discoveredAt: capabilities.discoveredAt, models });
462
+ }
463
+
464
+ // Cache chain: TTL-based in-memory cache → live discovery → seeded local caches
465
+ // (codex's own models_cache.json / Codexa's last-good discovery) → static
466
+ // fallback model list. Failed discoveries are evicted from the in-memory cache
467
+ // so retries are possible even when a seed satisfied the request.
444
468
  export async function getCodexModelCapabilities(
445
469
  options: GetCodexModelCapabilitiesOptions = {},
446
470
  ): Promise<CodexModelCapabilities> {
447
471
  const now = options.now?.() ?? Date.now();
448
472
  const ttlMs = options.ttlMs ?? DEFAULT_CACHE_TTL_MS;
473
+ const seed = options.seed ?? loadSeededCodexCapabilities;
474
+ const persist = options.persist ?? persistCodexModelCapabilities;
449
475
  let executable: string | null = null;
476
+ let liveDiscoverySucceeded = false;
450
477
 
451
478
  try {
452
479
  executable = options.executable ?? await (options.resolveExecutable ?? resolveCodexExecutable)();
@@ -462,7 +489,25 @@ export async function getCodexModelCapabilities(
462
489
  includeHidden: options.includeHidden,
463
490
  timeoutMs: options.timeoutMs,
464
491
  now: () => now,
465
- }).catch((error) => createFallbackModelCapabilities(error, { discoveredAt: now, executable }));
492
+ }).then((discovered) => {
493
+ liveDiscoverySucceeded = true;
494
+ try {
495
+ persist(discovered);
496
+ } catch {
497
+ // Persistence must never fail a successful discovery.
498
+ }
499
+ return discovered;
500
+ }).catch((error) => {
501
+ try {
502
+ const seeded = seed();
503
+ if (seeded) {
504
+ return seeded;
505
+ }
506
+ } catch {
507
+ // Seed read failures fall through to the static list.
508
+ }
509
+ return createFallbackModelCapabilities(error, { discoveredAt: now, executable });
510
+ });
466
511
 
467
512
  capabilityCache.set(cacheKey, {
468
513
  expiresAt: now + ttlMs,
@@ -470,12 +515,20 @@ export async function getCodexModelCapabilities(
470
515
  });
471
516
 
472
517
  const result = await promise;
473
- if (result.status === "fallback") {
518
+ if (!liveDiscoverySucceeded) {
474
519
  capabilityCache.delete(cacheKey);
475
520
  }
476
521
 
477
522
  return result;
478
523
  } catch (error) {
524
+ try {
525
+ const seeded = seed();
526
+ if (seeded) {
527
+ return seeded;
528
+ }
529
+ } catch {
530
+ // Seed read failures fall through to the static list.
531
+ }
479
532
  return createFallbackModelCapabilities(error, { discoveredAt: now, executable });
480
533
  }
481
534
  }