@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
@@ -1,4 +1,5 @@
1
- import type { ReasoningEffortCapability } from "../models/codexModelCapabilities.js";
1
+ import type { ReasoningEffortCapability } from "../models/codexModelCapabilities.js";
2
+ import { formatReasoningLabel } from "../../config/settings.js";
2
3
 
3
4
  export const CLAUDE_CODE_EFFORT_LEVELS: readonly ReasoningEffortCapability[] = [
4
5
  { id: "low", label: "Low", description: "Claude Code low effort." },
@@ -10,8 +11,10 @@ export const CLAUDE_CODE_EFFORT_LEVELS: readonly ReasoningEffortCapability[] = [
10
11
 
11
12
  export const CLAUDE_CODE_EFFORT_IDS = new Set(CLAUDE_CODE_EFFORT_LEVELS.map((level) => level.id));
12
13
 
13
- export function getClaudeCodeEffortLevels(ids: readonly string[]): readonly ReasoningEffortCapability[] {
14
- return ids
15
- .map((id) => CLAUDE_CODE_EFFORT_LEVELS.find((level) => level.id === id))
16
- .filter((level): level is ReasoningEffortCapability => Boolean(level));
17
- }
14
+ export function getClaudeCodeEffortLevels(ids: readonly string[]): readonly ReasoningEffortCapability[] {
15
+ return ids.map((id) => CLAUDE_CODE_EFFORT_LEVELS.find((level) => level.id === id) ?? {
16
+ id,
17
+ label: formatReasoningLabel(id),
18
+ description: null,
19
+ });
20
+ }
@@ -1,10 +1,13 @@
1
1
  import { codexSubprocessProvider } from "../providers/codexSubprocess.js";
2
+ import { loadSeededOpenAiModels } from "../models/codexModelsCacheSeed.js";
3
+ import { loadCachedProviderModels, saveCachedProviderModels } from "../models/providerModelCache.js";
2
4
  import type { BackendRunHandlers } from "../providers/types.js";
3
5
  import type { ProviderId, ProviderActiveRoute, ProviderWorkspaceOverride } from "../providerLauncher/types.js";
4
- import { anthropicRuntime } from "./anthropic.js";
5
- import { geminiRuntime } from "./gemini.js";
6
- import { localRuntime } from "./local.js";
7
- import { antigravityRuntime, ANTIGRAVITY_DEFAULT_MODEL_ID, migrateAntigravityLegacyModelId } from "./antigravity.js";
6
+ import { anthropicRuntime } from "./anthropic.js";
7
+ import { geminiRuntime } from "./gemini.js";
8
+ import { localRuntime } from "./local.js";
9
+ import { antigravityRuntime, ANTIGRAVITY_DEFAULT_MODEL_ID, migrateAntigravityLegacyModelId } from "./antigravity.js";
10
+ import { mistralVibeRuntime } from "./mistralVibe.js";
8
11
  import {
9
12
  ANTHROPIC_FALLBACK_MODELS,
10
13
  GEMINI_DEFAULT_MODEL_ID,
@@ -28,11 +31,14 @@ const openAiRuntime: ProviderRuntime = {
28
31
  routeAvailable: true,
29
32
  routeStatus: "Uses the configured Codex/OpenAI backend inside Codexa.",
30
33
  launchAvailable: true,
34
+ // Seeded from local caches (codex's own models_cache.json or Codexa's
35
+ // last-good discovery); live app-server discovery refreshes them via
36
+ // getCodexModelCapabilities in the app run loop.
31
37
  discoverModels: () => ({
32
38
  status: "ready",
33
39
  providerId: "openai",
34
40
  backendKind: "codex-cli-auth",
35
- models: [],
41
+ models: loadSeededOpenAiModels()?.models ?? [],
36
42
  }),
37
43
  run: (request: ProviderChatRequest, handlers: BackendRunHandlers) => {
38
44
  handlers.onProgress?.({
@@ -72,11 +78,12 @@ function unavailableRuntime(providerId: ProviderId, label: string): ProviderRunt
72
78
 
73
79
  const PROVIDER_RUNTIMES: Record<ProviderId, ProviderRuntime> = {
74
80
  openai: openAiRuntime,
75
- anthropic: anthropicRuntime,
76
- google: geminiRuntime,
77
- local: localRuntime,
78
- antigravity: antigravityRuntime,
79
- };
81
+ anthropic: anthropicRuntime,
82
+ google: geminiRuntime,
83
+ mistral: mistralVibeRuntime,
84
+ local: localRuntime,
85
+ antigravity: antigravityRuntime,
86
+ };
80
87
 
81
88
  export function getProviderRuntime(providerId: ProviderId): ProviderRuntime {
82
89
  return PROVIDER_RUNTIMES[providerId];
@@ -97,17 +104,39 @@ export function getProviderRouteSetupMessage(providerId: ProviderId): string {
97
104
  }
98
105
 
99
106
  export function discoverProviderModels(providerId: ProviderId): ProviderModelDiscoveryResult {
100
- return getProviderRuntime(providerId).discoverModels();
107
+ const result = getProviderRuntime(providerId).discoverModels();
108
+ // When the live/synchronous probe only produced fallback entries (or none),
109
+ // prefer the last-good discovery persisted from a previous session.
110
+ const hasRuntimeModels = result.models.some((model) => model.source && model.source !== "fallback");
111
+ if (result.status === "ready" && !hasRuntimeModels) {
112
+ const cached = loadCachedProviderModels(providerId);
113
+ if (cached) {
114
+ return { ...result, models: cached.models };
115
+ }
116
+ }
117
+ return result;
118
+ }
119
+
120
+ // Persist a ready discovery so the next session starts from last-good models.
121
+ export function persistProviderDiscovery(discovery: ProviderModelDiscoveryResult): void {
122
+ const runtimeModels = discovery.models.filter((model) => model.source && model.source !== "fallback");
123
+ if (discovery.status !== "ready" || runtimeModels.length === 0) {
124
+ return;
125
+ }
126
+ saveCachedProviderModels(discovery.providerId, {
127
+ discoveredAt: Date.now(),
128
+ models: runtimeModels,
129
+ });
101
130
  }
102
131
 
103
132
  export async function validateProviderRouteActivation(options: {
104
133
  route: ProviderRoute;
105
- workspaceRoot: string;
106
- geminiCommandPath?: string | null;
107
- claudeCommandPath?: string | null;
108
- antigravityCommandPath?: string | null;
109
- localConfig?: ProviderWorkspaceOverride | null;
110
- }): Promise<ProviderRouteValidationResult> {
134
+ workspaceRoot: string;
135
+ geminiCommandPath?: string | null;
136
+ claudeCommandPath?: string | null;
137
+ antigravityCommandPath?: string | null;
138
+ localConfig?: ProviderWorkspaceOverride | null;
139
+ }): Promise<ProviderRouteValidationResult> {
111
140
  const runtime = getProviderRuntime(options.route.providerId);
112
141
  if (!runtime.routeAvailable) {
113
142
  return {
@@ -157,7 +186,7 @@ export function resolveActiveProviderRoute(options: {
157
186
  currentReasoning: string;
158
187
  }): ActiveProviderRoute {
159
188
  const configuredRoute = options.workspaceConfigActiveRoute;
160
- if (configuredRoute && configuredRoute.providerId !== "google" && isProviderRoutableInCodexa(configuredRoute.providerId)) {
189
+ if (configuredRoute && configuredRoute.providerId !== "google" && isProviderRoutableInCodexa(configuredRoute.providerId)) {
161
190
  const route: ActiveProviderRoute = {
162
191
  providerId: configuredRoute.providerId,
163
192
  modelId: configuredRoute.modelId,
@@ -166,36 +195,48 @@ export function resolveActiveProviderRoute(options: {
166
195
  ...(configuredRoute.modelSelection ? { modelSelection: configuredRoute.modelSelection } : {}),
167
196
  };
168
197
 
169
- if (route.providerId === "google" && route.modelSelection) {
170
- route.modelId = resolveGeminiModelId(route.modelSelection);
171
- } else if (route.providerId === "google") {
172
- route.modelId = normalizeGeminiModelId(route.modelId);
173
- } else if (route.providerId === "anthropic") {
174
- const discovery = discoverProviderModels("anthropic");
175
- const stillAvailable = discovery.models.some((model) =>
176
- model.modelId === route.modelId ||
177
- model.id === route.modelId ||
178
- model.canonicalId === route.modelId
179
- );
180
- const hasNonFallbackModels = discovery.models.some((model) => model.source !== "fallback");
181
- if (discovery.status === "ready" && hasNonFallbackModels && discovery.models.length > 0 && !stillAvailable) {
182
- route.modelId = discovery.models[0]!.modelId;
183
- }
184
- } else if (route.providerId === "local") {
185
- const discovery = discoverProviderModels("local");
186
- const selectedModel = typeof discovery.diagnostics?.selectedModel === "string"
198
+ if (route.providerId === "google" && route.modelSelection) {
199
+ route.modelId = resolveGeminiModelId(route.modelSelection);
200
+ } else if (route.providerId === "google") {
201
+ route.modelId = normalizeGeminiModelId(route.modelId);
202
+ } else if (route.providerId === "anthropic") {
203
+ const discovery = discoverProviderModels("anthropic");
204
+ const stillAvailable = discovery.models.some((model) =>
205
+ model.modelId === route.modelId ||
206
+ model.id === route.modelId ||
207
+ model.canonicalId === route.modelId
208
+ );
209
+ const hasNonFallbackModels = discovery.models.some((model) => model.source !== "fallback");
210
+ if (discovery.status === "ready" && hasNonFallbackModels && discovery.models.length > 0 && !stillAvailable) {
211
+ route.modelId = discovery.models[0]!.modelId;
212
+ }
213
+ } else if (route.providerId === "local") {
214
+ const discovery = discoverProviderModels("local");
215
+ const selectedModel = typeof discovery.diagnostics?.selectedModel === "string"
187
216
  ? discovery.diagnostics.selectedModel.trim()
188
217
  : "";
189
- if (discovery.status === "ready" && selectedModel) {
190
- route.modelId = selectedModel;
191
- }
218
+ if (discovery.status === "ready" && selectedModel) {
219
+ route.modelId = selectedModel;
220
+ }
192
221
  } else if (route.providerId === "antigravity") {
193
222
  const migrated = migrateAntigravityLegacyModelId(route.modelId);
194
223
  route.modelId = migrated.modelId;
195
224
  if (!route.reasoning && migrated.reasoning) {
196
225
  route.reasoning = migrated.reasoning;
197
226
  }
198
- }
227
+ const discovery = discoverProviderModels("antigravity");
228
+ if (discovery.status === "ready" && discovery.models.length > 0) {
229
+ let model = discovery.models.find((item) => item.modelId === route.modelId || item.id === route.modelId);
230
+ if (!model) {
231
+ model = discovery.models[0];
232
+ route.modelId = model.modelId;
233
+ }
234
+ const levels = model.supportedReasoningLevels;
235
+ if (levels?.length && (!route.reasoning || !levels.some((level) => level.id === route.reasoning))) {
236
+ route.reasoning = model.defaultReasoningLevel ?? levels[0]?.id;
237
+ }
238
+ }
239
+ }
199
240
 
200
241
  return route;
201
242
  }
@@ -214,17 +255,21 @@ export function getDefaultRouteModel(providerId: ProviderId, currentOpenAiModel:
214
255
  if (discovered.status === "ready" && discovered.models.length > 0) {
215
256
  return discovered.models[0].modelId;
216
257
  }
217
- return ANTHROPIC_FALLBACK_MODELS[0]?.modelId ?? "sonnet";
258
+ return ANTHROPIC_FALLBACK_MODELS[0]?.modelId ?? "sonnet";
218
259
  }
219
260
  if (providerId === "google") {
220
261
  return GEMINI_FALLBACK_MODELS[0]?.modelId ?? GEMINI_DEFAULT_MODEL_ID;
221
262
  }
222
- if (providerId === "local") {
223
- const discovery = discoverProviderModels("local");
224
- return discovery.models[0]?.modelId ?? "Local default";
225
- }
263
+ if (providerId === "local") {
264
+ const discovery = discoverProviderModels("local");
265
+ return discovery.models[0]?.modelId ?? "Local default";
266
+ }
267
+ if (providerId === "mistral") {
268
+ const discovery = discoverProviderModels("mistral");
269
+ return discovery.models[0]?.modelId ?? "Vibe default";
270
+ }
226
271
  if (providerId === "antigravity") {
227
- return ANTIGRAVITY_DEFAULT_MODEL_ID;
228
- }
229
- return currentOpenAiModel;
230
- }
272
+ return discoverProviderModels("antigravity").models[0]?.modelId ?? ANTIGRAVITY_DEFAULT_MODEL_ID;
273
+ }
274
+ return currentOpenAiModel;
275
+ }
@@ -10,6 +10,7 @@ export type ProviderBackendKind =
10
10
  | "codex-cli-auth"
11
11
  | "gemini-cli-auth"
12
12
  | "claude-code-auth"
13
+ | "mistral-vibe-cli-auth"
13
14
  | "antigravity-cli-auth"
14
15
  | "openai-api-key"
15
16
  | "gemini-api-key"
package/src/index.tsx CHANGED
@@ -18,11 +18,24 @@ import {
18
18
  import { resolveInkRenderInstance, type InkRenderInstance } from "./core/terminal/inkRenderReset.js";
19
19
  import { resetFrameLockForResize, wrapStdoutWithFrameLock } from "./core/terminal/frameLock.js";
20
20
 
21
- type RenderHandle = Pick<Instance, "clear" | "cleanup" | "waitUntilExit">;
22
- const KITTY_KEYBOARD_OPTIONS: RenderOptions["kittyKeyboard"] = {
23
- mode: "auto",
24
- flags: ["disambiguateEscapeCodes"],
25
- };
21
+ type RenderHandle = Pick<Instance, "clear" | "cleanup" | "waitUntilExit">;
22
+
23
+ export function resolveKittyKeyboardOptions(
24
+ env: Record<string, string | undefined>,
25
+ ): RenderOptions["kittyKeyboard"] | undefined {
26
+ // Ink's auto mode probes with CSI ? u. Kitty's response can race Ink's
27
+ // regular input listeners and leak into both the first frame and composer.
28
+ // A direct Kitty session is already known to support the protocol, so enable
29
+ // it without probing. Avoid protocol negotiation in all other terminals.
30
+ if ((env.TERM ?? "").toLowerCase() !== "xterm-kitty") {
31
+ return undefined;
32
+ }
33
+
34
+ return {
35
+ mode: "enabled",
36
+ flags: ["disambiguateEscapeCodes"],
37
+ };
38
+ }
26
39
 
27
40
  export interface AppStdout {
28
41
  isTTY: boolean;
@@ -222,10 +235,11 @@ export function startApp({
222
235
  process.on("uncaughtException", handleFatal);
223
236
  process.on("unhandledRejection", handleFatal);
224
237
 
225
- renderHandle = renderApp(<App launchArgs={launchArgs} />, {
226
- kittyKeyboard: KITTY_KEYBOARD_OPTIONS,
227
- stdout: wrappedStdout as any,
228
- });
238
+ const kittyKeyboard = resolveKittyKeyboardOptions(env);
239
+ renderHandle = renderApp(<App launchArgs={launchArgs} />, {
240
+ ...(kittyKeyboard ? { kittyKeyboard } : {}),
241
+ stdout: wrappedStdout as any,
242
+ });
229
243
 
230
244
  // Resolve the real Ink class instance to get access to lastOutput,
231
245
  // onRender, calculateLayout, etc. Gracefully degrades to null in tests.
@@ -1,4 +1,11 @@
1
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
1
4
  import { resolveRuntimeConfig, type RuntimeConfig, DEFAULT_RUNTIME_CONFIG, type ResolvedRuntimeConfig } from "../config/runtimeConfig.js";
5
+ import type { ProviderId } from "../core/providerLauncher/types.js";
6
+ import { resetAnthropicRouteValidationCacheForTests } from "../core/providerRuntime/anthropic.js";
7
+ import { resetAntigravityRouteValidationCacheForTests } from "../core/providerRuntime/antigravity.js";
8
+ import type { ProviderModel } from "../core/providerRuntime/types.js";
2
9
 
3
10
  export function makeResolvedRuntime(overrides: Partial<RuntimeConfig> = {}): ResolvedRuntimeConfig {
4
11
  return resolveRuntimeConfig({
@@ -12,3 +19,86 @@ export function makeResolvedRuntime(overrides: Partial<RuntimeConfig> = {}): Res
12
19
  }
13
20
 
14
21
  export const TEST_RUNTIME = makeResolvedRuntime();
22
+
23
+ function agyModel(id: string, label: string): ProviderModel {
24
+ return {
25
+ id,
26
+ modelId: id,
27
+ label,
28
+ description: null,
29
+ defaultReasoningLevel: null,
30
+ supportedReasoningLevels: null,
31
+ source: "discovered",
32
+ };
33
+ }
34
+
35
+ /**
36
+ * The catalog `agy models` reports on a configured machine. Antigravity resolves
37
+ * its status, backend kind, and model labels from whatever discovery last found,
38
+ * so tests that assert on any of those need a catalog seeded rather than
39
+ * whatever happens to be installed.
40
+ */
41
+ export const ANTIGRAVITY_FIXTURE_MODELS: readonly ProviderModel[] = [
42
+ agyModel("gemini-3.5-flash", "Gemini 3.5 Flash"),
43
+ agyModel("gemini-3.1-pro", "Gemini 3.1 Pro"),
44
+ agyModel("claude-sonnet-4-6", "Claude Sonnet 4.6"),
45
+ agyModel("claude-sonnet-4-6-think", "Claude Sonnet 4.6 (Thinking)"),
46
+ agyModel("gpt-oss-120b", "GPT-OSS 120B"),
47
+ ];
48
+
49
+ /**
50
+ * Drops every provider's in-memory discovery result.
51
+ *
52
+ * Bun shares the module registry across test files, so a catalog discovered by
53
+ * one test (some probe the real `claude` / `agy` CLIs) otherwise survives into
54
+ * the next and takes precedence over both the cache and the fallback list —
55
+ * making assertions depend on file order and on what is installed locally.
56
+ */
57
+ export function resetProviderDiscoveryState(): void {
58
+ resetAntigravityRouteValidationCacheForTests();
59
+ resetAnthropicRouteValidationCacheForTests();
60
+ }
61
+
62
+ /**
63
+ * Runs `fn` with HOME pointed at a throwaway directory holding only the provider
64
+ * model cache built from `seed`.
65
+ *
66
+ * Provider discovery reads `~/.codexa-model-cache.json`, so a test that calls it
67
+ * without this is asserting against the developer's own machine — it passes with
68
+ * a warm cache and fails on a cold one (or in CI). HOME is read per call (see
69
+ * `getProviderModelCacheFile`), so redirecting the env var here is enough.
70
+ *
71
+ * Antigravity's in-memory discovery is reset around the body as well: Bun shares
72
+ * the module registry across test files, so a catalog discovered by one file
73
+ * otherwise leaks into the next and takes precedence over the seeded cache,
74
+ * making results depend on file order. Seed exactly what the assertions need.
75
+ */
76
+ export function withSeededModelCache<T>(
77
+ seed: Partial<Record<ProviderId, readonly ProviderModel[]>>,
78
+ fn: () => T,
79
+ ): T {
80
+ const tempHome = mkdtempSync(join(tmpdir(), "codexa-test-home-"));
81
+ const previousHome = process.env.HOME;
82
+ const previousUserProfile = process.env.USERPROFILE;
83
+ try {
84
+ process.env.HOME = tempHome;
85
+ delete process.env.USERPROFILE;
86
+ const providers = Object.fromEntries(
87
+ Object.entries(seed).map(([providerId, models]) => [providerId, { discoveredAt: 1, models }]),
88
+ );
89
+ writeFileSync(
90
+ join(tempHome, ".codexa-model-cache.json"),
91
+ `${JSON.stringify({ version: 1, providers }, null, 2)}\n`,
92
+ "utf8",
93
+ );
94
+ resetProviderDiscoveryState();
95
+ return fn();
96
+ } finally {
97
+ if (previousHome === undefined) delete process.env.HOME;
98
+ else process.env.HOME = previousHome;
99
+ if (previousUserProfile === undefined) delete process.env.USERPROFILE;
100
+ else process.env.USERPROFILE = previousUserProfile;
101
+ rmSync(tempHome, { recursive: true, force: true });
102
+ resetProviderDiscoveryState();
103
+ }
104
+ }
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
  import { Box, Text } from "ink";
3
- import { useTheme } from "./theme.js";
3
+ import { useTheme } from "../theme.js";
4
4
 
5
5
  // ─── WaveBar ─────────────────────────────────────────────────────────────────
6
6
  // Audio-visualizer style wave. Renders as a single <Text> node to minimise
@@ -1,7 +1,7 @@
1
1
  import React, { useState, useEffect } from "react";
2
2
  import { Text } from "ink";
3
- import type { UIState, ExternalCliStatus } from "../session/types.js";
4
- import { useTheme } from "./theme.js";
3
+ import type { UIState, ExternalCliStatus } from "../../session/types.js";
4
+ import { useTheme } from "../theme.js";
5
5
 
6
6
  const SPINNER_FRAMES = ["?", "?", "?", "?", "?", "?", "?", "?", "?", "?"];
7
7
  const STREAMING_FRAMES = ["?", "?", "?", "?"];
@@ -36,7 +36,7 @@ export function ActivityIndicator({ uiState, externalCliStatus = "idle" }: Activ
36
36
  let bold = false;
37
37
 
38
38
  if (isError) {
39
- glyph = "�";
39
+ glyph = "�";
40
40
  color = theme.error;
41
41
  bold = true;
42
42
  } else if (isAction) {
@@ -1,8 +1,8 @@
1
1
  import React, { useEffect, useState } from "react";
2
2
  import { Text } from "ink";
3
- import * as renderDebug from "../core/perf/renderDebug.js";
4
- import { useTheme } from "./theme.js";
5
- import { sanitizeTerminalOutput } from "../core/terminal/terminalSanitize.js";
3
+ import * as renderDebug from "../../core/perf/renderDebug.js";
4
+ import { useTheme } from "../theme.js";
5
+ import { sanitizeTerminalOutput } from "../../core/terminal/terminalSanitize.js";
6
6
  import { BUSY_STATUS_FRAME_MS, BUSY_STATUS_FRAMES, getBusyStatusFrame } from "./busyStatusAnimation.js";
7
7
 
8
8
  interface AnimatedStatusTextProps {
@@ -1,12 +1,12 @@
1
1
  import React, { memo, useEffect, useMemo, useRef, useState } from "react";
2
2
  import { Box, Text, useStdin } from "ink";
3
- import { useTheme } from "./theme.js";
4
- import type { RuntimeSummary } from "../config/runtimeConfig.js";
5
- import type { CodexAuthState } from "../core/auth/codexAuth.js";
6
- import { HEADER_CONFIG_DEFAULTS, type HeaderConfig } from "../config/settings.js";
7
- import * as renderDebug from "../core/perf/renderDebug.js";
8
- import type { Screen, TimelineEvent, UIState } from "../session/types.js";
9
- import { isBusy } from "../session/types.js";
3
+ import { useTheme } from "../theme.js";
4
+ import type { RuntimeSummary } from "../../config/runtimeConfig.js";
5
+ import type { CodexAuthState } from "../../core/auth/codexAuth.js";
6
+ import { HEADER_CONFIG_DEFAULTS, type HeaderConfig } from "../../config/settings.js";
7
+ import * as renderDebug from "../../core/perf/renderDebug.js";
8
+ import type { Screen, TimelineEvent, UIState } from "../../session/types.js";
9
+ import { isBusy } from "../../session/types.js";
10
10
  import {
11
11
  ActivePanelLayoutContext,
12
12
  type ActivePanelLayout,
@@ -24,7 +24,7 @@ import {
24
24
  MIN_TERMINAL_COLS,
25
25
  MIN_TERMINAL_ROWS,
26
26
  type TerminalViewport,
27
- } from "./layout.js";
27
+ } from "../layout.js";
28
28
  import {
29
29
  buildActiveRenderItems,
30
30
  buildStaticRenderItems,
@@ -33,9 +33,9 @@ import {
33
33
  Timeline,
34
34
  TimelineRowView,
35
35
  type TimelineItem,
36
- } from "./Timeline.js";
37
- import { buildNativeTranscriptParts, type NativeTranscriptRowItem, type TimelineRow } from "./timelineMeasure.js";
38
- import type { TerminalSelectionProfile } from "../core/terminal/terminalSelection.js";
36
+ } from "../timeline/Timeline.js";
37
+ import { buildNativeTranscriptParts, type NativeTranscriptRowItem, type TimelineRow } from "../timeline/timelineMeasure.js";
38
+ import type { TerminalSelectionProfile } from "../../core/terminal/terminalSelection.js";
39
39
  import { MemoizedTopHeader, measureTopHeaderRows, type UpdateAvailableInfo } from "./TopHeader.js";
40
40
 
41
41
  const COMPACT_HEADER_TO_COMPOSER_GAP_ROWS = 1;
@@ -1,9 +1,9 @@
1
1
  import React, { memo, useEffect, useMemo, useRef, useState } from "react";
2
2
  import { Box, Text, useFocus, useInput, useStdin } from "ink";
3
- import { formatContextCompact } from "../core/providerRuntime/contextMetadata.js";
4
- import type { ModelSpec } from "../core/models/modelSpecs.js";
5
- import type { ExternalCliStatus, UIState } from "../session/types.js";
6
- import { FOCUS_IDS } from "./focus.js";
3
+ import { formatContextCompact } from "../../core/providerRuntime/contextMetadata.js";
4
+ import type { ModelSpec } from "../../core/models/modelSpecs.js";
5
+ import type { ExternalCliStatus, UIState } from "../../session/types.js";
6
+ import { FOCUS_IDS } from "../input/focus.js";
7
7
  import {
8
8
  createInputViewport,
9
9
  deleteInputBackward,
@@ -14,22 +14,22 @@ import {
14
14
  moveCursorRight,
15
15
  normalizeInputText,
16
16
  normalizeCursorOffset,
17
- } from "./inputBuffer.js";
18
- import { getModeDisplaySpec } from "./modeDisplay.js";
17
+ } from "../input/inputBuffer.js";
18
+ import { getModeDisplaySpec } from "../render/modeDisplay.js";
19
19
  import { ActivityIndicator } from "./ActivityIndicator.js";
20
20
  import { measureRunFooterRows, MemoizedRunFooter } from "./RunFooter.js";
21
- import { useTheme } from "./theme.js";
22
- import { clampVisualText, getShellWidth, type Layout } from "./layout.js";
23
- import { getTextWidth, splitTextAtColumn } from "./textLayout.js";
24
- import { useThrottledValue } from "./useThrottledValue.js";
25
- import { sanitizeTerminalOutput } from "../core/terminal/terminalSanitize.js";
26
- import { getStdinDebugState, traceInputDebug } from "../core/debug/inputDebug.js";
27
- import * as renderDebug from "../core/perf/renderDebug.js";
21
+ import { useTheme } from "../theme.js";
22
+ import { clampVisualText, getShellWidth, type Layout } from "../layout.js";
23
+ import { getTextWidth, splitTextAtColumn } from "../render/textLayout.js";
24
+ import { useThrottledValue } from "../useThrottledValue.js";
25
+ import { sanitizeTerminalOutput } from "../../core/terminal/terminalSanitize.js";
26
+ import { getStdinDebugState, traceInputDebug } from "../../core/debug/inputDebug.js";
27
+ import * as renderDebug from "../../core/perf/renderDebug.js";
28
28
  import { AnimatedStatusText } from "./AnimatedStatusText.js";
29
29
  import { isAnimatedBusyState } from "./busyStatusAnimation.js";
30
30
  import { Spinner } from "./Spinner.js";
31
- import type { TerminalSelectionProfile } from "../core/terminal/terminalSelection.js";
32
- import { getSlashCommandSuggestions, type CommandSuggestion } from "./slashCommands.js";
31
+ import type { TerminalSelectionProfile } from "../../core/terminal/terminalSelection.js";
32
+ import { getSlashCommandSuggestions, type CommandSuggestion } from "../input/slashCommands.js";
33
33
 
34
34
  // ─── Types & constants ────────────────────────────────────────────────────────
35
35
 
@@ -1,8 +1,8 @@
1
1
  import React from "react";
2
2
  import type { ReactNode } from "react";
3
3
  import { Box, Text } from "ink";
4
- import { useTheme } from "./theme.js";
5
- import { getVisualWidth } from "./layout.js";
4
+ import { useTheme } from "../theme.js";
5
+ import { getVisualWidth } from "../layout.js";
6
6
 
7
7
  interface DashCardProps {
8
8
  cols: number;
@@ -1,8 +1,8 @@
1
1
  import React, { memo } from "react";
2
2
  import { Box, Text } from "ink";
3
- import type { UIState } from "../session/types.js";
4
- import * as renderDebug from "../core/perf/renderDebug.js";
5
- import { useTheme } from "./theme.js";
3
+ import type { UIState } from "../../session/types.js";
4
+ import * as renderDebug from "../../core/perf/renderDebug.js";
5
+ import { useTheme } from "../theme.js";
6
6
  import { AnimatedStatusText } from "./AnimatedStatusText.js";
7
7
  import { isAnimatedBusyState } from "./busyStatusAnimation.js";
8
8
 
@@ -1,8 +1,8 @@
1
1
  import React from "react";
2
2
  import { Box, Text } from "ink";
3
- import { useTheme } from "./theme.js";
4
- import type { Layout } from "./layout.js";
5
- import * as renderDebug from "../core/perf/renderDebug.js";
3
+ import { useTheme } from "../theme.js";
4
+ import type { Layout } from "../layout.js";
5
+ import * as renderDebug from "../../core/perf/renderDebug.js";
6
6
 
7
7
  export type RuntimeAvailability = "available" | "checking" | "reconnecting" | "unavailable" | "unknown";
8
8
 
@@ -1,6 +1,6 @@
1
1
  import React, { startTransition, useEffect, useState } from "react";
2
2
  import { Text } from "ink";
3
- import { useTheme } from "./theme.js";
3
+ import { useTheme } from "../theme.js";
4
4
 
5
5
  const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
6
6
 
@@ -1,15 +1,15 @@
1
1
  import React, { memo } from "react";
2
2
  import { Box, Text } from "ink";
3
- import { HEADER_CONFIG_DEFAULTS, type HeaderConfig } from "../config/settings.js";
4
- import { formatCodexaBrandLabel } from "../core/version/channel.js";
5
- import { formatVersionLabel } from "../core/version/updateCheck.js";
6
- import type { RuntimeSummary } from "../config/runtimeConfig.js";
7
- import type { CodexAuthState } from "../core/auth/codexAuth.js";
8
- import { getAuthStateLabel } from "../core/auth/codexAuth.js";
9
- import * as renderDebug from "../core/perf/renderDebug.js";
10
- import { useTheme } from "./theme.js";
11
- import { clampVisualText, isDecorativeLayoutMode, type Layout, useAppLayoutBudget } from "./layout.js";
12
- import { getTextWidth } from "./textLayout.js";
3
+ import { HEADER_CONFIG_DEFAULTS, type HeaderConfig } from "../../config/settings.js";
4
+ import { formatCodexaBrandLabel } from "../../core/version/channel.js";
5
+ import { formatVersionLabel } from "../../core/version/updateCheck.js";
6
+ import type { RuntimeSummary } from "../../config/runtimeConfig.js";
7
+ import type { CodexAuthState } from "../../core/auth/codexAuth.js";
8
+ import { getAuthStateLabel } from "../../core/auth/codexAuth.js";
9
+ import * as renderDebug from "../../core/perf/renderDebug.js";
10
+ import { useTheme } from "../theme.js";
11
+ import { clampVisualText, isDecorativeLayoutMode, type Layout, useAppLayoutBudget } from "../layout.js";
12
+ import { getTextWidth } from "../render/textLayout.js";
13
13
  import { UPDATE_CARD_ROWS, UpdateAvailableCard } from "./UpdateAvailableCard.js";
14
14
  import {
15
15
  LOGO_LARGE,
@@ -18,7 +18,7 @@ import {
18
18
  LOGO_LARGE_MIN_COLS,
19
19
  LOGO_LARGE_MIN_ROWS,
20
20
  getLogoWidth,
21
- } from "./logoVariants.js";
21
+ } from "../render/logoVariants.js";
22
22
 
23
23
  // Re-exported for backward compatibility with existing tests.
24
24
  export const HEADER_WORDMARK_LINES = LOGO_LARGE;
@@ -434,4 +434,4 @@ export const MemoizedTopHeader = memo(TopHeader, (prev, next) => {
434
434
  export const MIN_LOGO_TERMINAL_WIDTH = LOGO_COMPACT_MIN_COLS;
435
435
 
436
436
  // Re-export for consumers that reference these constants directly.
437
- export { LOGO_LARGE_MIN_COLS, LOGO_MEDIUM_MIN_COLS, LOGO_COMPACT_MIN_COLS } from "./logoVariants.js";
437
+ export { LOGO_LARGE_MIN_COLS, LOGO_MEDIUM_MIN_COLS, LOGO_COMPACT_MIN_COLS } from "../render/logoVariants.js";
@@ -1,8 +1,8 @@
1
1
  import React from "react";
2
2
  import { Box, Text } from "ink";
3
- import { CODEXA_UPDATE_COMMAND, formatVersionLabel } from "../core/version/updateCheck.js";
4
- import { clampVisualText } from "./layout.js";
5
- import { useTheme } from "./theme.js";
3
+ import { CODEXA_UPDATE_COMMAND, formatVersionLabel } from "../../core/version/updateCheck.js";
4
+ import { clampVisualText } from "../layout.js";
5
+ import { useTheme } from "../theme.js";
6
6
 
7
7
  export const UPDATE_CARD_CONTENT_ROWS = 4; // title + available + using + command
8
8
  export const UPDATE_CARD_ROWS = UPDATE_CARD_CONTENT_ROWS + 2; // +2 for top/bottom border rows
@@ -1,4 +1,4 @@
1
- import { formatTerminalAnswerInline } from "./terminalAnswerFormat.js";
1
+ import { formatTerminalAnswerInline } from "../render/terminalAnswerFormat.js";
2
2
 
3
3
  function stripOuterQuotes(s: string): string {
4
4
  if ((s.startsWith("'") && s.endsWith("'")) || (s.startsWith('"') && s.endsWith('"'))) {
@@ -1,4 +1,4 @@
1
- import type { Screen } from "../session/types.js";
1
+ import type { Screen } from "../../session/types.js";
2
2
 
3
3
  export const FOCUS_IDS = {
4
4
  composer: "composer",