@golba98/codexa 1.0.3 → 1.0.4

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 (85) hide show
  1. package/README.md +17 -18
  2. package/bin/codexa.js +62 -144
  3. package/package.json +4 -3
  4. package/src/app.tsx +533 -275
  5. package/src/commands/handler.ts +2 -2
  6. package/src/config/buildInfo.ts +2 -2
  7. package/src/config/layeredConfig.ts +1 -1
  8. package/src/config/runtimeConfig.ts +1 -1
  9. package/src/config/settings.ts +1 -1
  10. package/src/config/trustStore.ts +1 -1
  11. package/src/core/README.md +52 -0
  12. package/src/core/agent/loop.ts +282 -0
  13. package/src/core/agent/protocol.ts +211 -0
  14. package/src/core/agent/tools.ts +414 -0
  15. package/src/core/{codexExecArgs.ts → codex/codexExecArgs.ts} +2 -2
  16. package/src/core/{codexLaunch.ts → codex/codexLaunch.ts} +3 -3
  17. package/src/core/{codexPrompt.ts → codex/codexPrompt.ts} +3 -3
  18. package/src/core/debug/modelStateDebug.ts +34 -0
  19. package/src/core/executables/antigravityExecutable.ts +48 -0
  20. package/src/core/executables/executableResolver.ts +5 -1
  21. package/src/core/perf/renderDebug.ts +10 -6
  22. package/src/core/providerLauncher/registry.ts +30 -14
  23. package/src/core/providerLauncher/types.ts +11 -9
  24. package/src/core/providerLauncher/workspaceConfig.ts +41 -26
  25. package/src/core/providerRuntime/anthropic.ts +7 -1
  26. package/src/core/providerRuntime/antigravity.ts +305 -0
  27. package/src/core/providerRuntime/claudeCodeDiscovery.ts +268 -22
  28. package/src/core/providerRuntime/claudeCodeDiscoveryDebug.ts +55 -0
  29. package/src/core/providerRuntime/contextMetadata.ts +12 -24
  30. package/src/core/providerRuntime/local.ts +129 -51
  31. package/src/core/providerRuntime/models.ts +22 -11
  32. package/src/core/providerRuntime/registry.ts +58 -31
  33. package/src/core/providerRuntime/types.ts +19 -14
  34. package/src/core/providers/codexSubprocess.ts +2 -2
  35. package/src/core/providers/types.ts +1 -1
  36. package/src/core/{attachments.ts → shared/attachments.ts} +27 -4
  37. package/src/core/{cleanupFastFail.ts → shared/cleanupFastFail.ts} +1 -1
  38. package/src/core/{hollowResponseFormat.ts → shared/hollowResponseFormat.ts} +1 -1
  39. package/src/core/terminal/clearFrameBoundary.ts +814 -0
  40. package/src/core/terminal/frameLock.ts +109 -0
  41. package/src/core/terminal/inkRenderReset.ts +123 -0
  42. package/src/core/terminal/terminalControl.ts +22 -0
  43. package/src/core/terminal/terminalTitle.ts +16 -102
  44. package/src/core/{channel.ts → version/channel.ts} +1 -1
  45. package/src/core/{updateCheck.ts → version/updateCheck.ts} +10 -5
  46. package/src/core/{launchContext.ts → workspace/launchContext.ts} +9 -16
  47. package/src/core/{planStorage.ts → workspace/planStorage.ts} +2 -2
  48. package/src/core/{workspaceGuard.ts → workspace/workspaceGuard.ts} +97 -13
  49. package/src/headless/execRunner.ts +2 -2
  50. package/src/index.tsx +43 -89
  51. package/src/session/appSession.ts +10 -7
  52. package/src/session/chatLifecycle.ts +1 -1
  53. package/src/session/liveRenderScheduler.ts +1 -1
  54. package/src/session/types.ts +1 -1
  55. package/src/ui/AppShell.tsx +672 -706
  56. package/src/ui/AttachmentImportPanel.tsx +2 -2
  57. package/src/ui/BottomComposer.tsx +145 -144
  58. package/src/ui/ModelPickerScreen.tsx +216 -36
  59. package/src/ui/ModelReasoningPicker.tsx +1 -1
  60. package/src/ui/PlanReviewPanel.tsx +1 -1
  61. package/src/ui/ProviderPicker.tsx +735 -321
  62. package/src/ui/RuntimeStatusBar.tsx +108 -0
  63. package/src/ui/SelectionPanel.tsx +4 -0
  64. package/src/ui/SettingsPanel.tsx +1 -1
  65. package/src/ui/TextEntryPanel.tsx +1 -1
  66. package/src/ui/Timeline.tsx +1619 -1470
  67. package/src/ui/TopHeader.tsx +46 -30
  68. package/src/ui/TranscriptShell.tsx +322 -0
  69. package/src/ui/TurnGroup.tsx +2 -2
  70. package/src/ui/UpdateAvailableCard.tsx +3 -3
  71. package/src/ui/UpdatePromptPanel.tsx +16 -22
  72. package/src/ui/layout.ts +298 -24
  73. package/src/ui/layoutListWindow.ts +145 -0
  74. package/src/ui/logoVariants.ts +4 -8
  75. package/src/ui/runtimeDisplay.ts +15 -3
  76. package/src/ui/textLayout.ts +15 -4
  77. package/src/ui/timelineMeasure.ts +194 -122
  78. package/src/core/codex.ts +0 -124
  79. package/src/ui/StaticTranscriptItem.tsx +0 -56
  80. /package/src/core/{inputDebug.ts → debug/inputDebug.ts} +0 -0
  81. /package/src/core/{clipboard.ts → shared/clipboard.ts} +0 -0
  82. /package/src/core/{githubDiagnostics.ts → shared/githubDiagnostics.ts} +0 -0
  83. /package/src/core/{projectInstructions.ts → workspace/projectInstructions.ts} +0 -0
  84. /package/src/core/{workspaceActivity.ts → workspace/workspaceActivity.ts} +0 -0
  85. /package/src/core/{workspaceRoot.ts → workspace/workspaceRoot.ts} +0 -0
@@ -1,4 +1,6 @@
1
1
  import { sanitizeTerminalOutput } from "../terminal/terminalSanitize.js";
2
+ import { runAgentLoop, type AgentChatMessage, type AgentChatResponse } from "../agent/loop.js";
3
+ import { parseOpenAiToolCalls } from "../agent/protocol.js";
2
4
  import type { BackendRunHandlers } from "../providers/types.js";
3
5
  import type { ProviderWorkspaceOverride } from "../providerLauncher/types.js";
4
6
  import type {
@@ -456,21 +458,110 @@ function getCachedSelectedModel(config: LocalProviderConfig, routeModel: string)
456
458
  return selectFallbackLocalModel(config, discoveredIds) ?? routeModel;
457
459
  }
458
460
 
459
- function extractNonStreamingText(body: unknown): string {
460
- if (typeof body !== "object" || body === null) return "";
461
- const choices = (body as { choices?: unknown }).choices;
462
- if (!Array.isArray(choices)) return "";
463
- return choices
464
- .map((choice) => {
465
- if (typeof choice !== "object" || choice === null) return "";
466
- const message = (choice as { message?: { content?: unknown }; text?: unknown }).message;
467
- if (typeof message?.content === "string") return message.content;
468
- const text = (choice as { text?: unknown }).text;
469
- return typeof text === "string" ? text : "";
470
- })
471
- .join("")
472
- .trim();
473
- }
461
+ interface LocalResponseDiagnostics {
462
+ choiceCount: number;
463
+ finishReasons: string[];
464
+ recognizedFields: string[];
465
+ topLevelKeys: string[];
466
+ }
467
+
468
+ interface ExtractedLocalResponse extends AgentChatResponse {
469
+ diagnostics: LocalResponseDiagnostics;
470
+ }
471
+
472
+ function textFromContent(value: unknown): string {
473
+ if (typeof value === "string") return value;
474
+ if (Array.isArray(value)) return value.map(textFromContent).join("");
475
+ if (!isRecord(value)) return "";
476
+
477
+ // OpenAI-compatible servers commonly emit content as typed parts, such as
478
+ // { type: "text", text: "..." } or { type: "output_text", text: "..." }.
479
+ if (typeof value.text === "string") return value.text;
480
+ if (typeof value.content === "string" || Array.isArray(value.content)) {
481
+ return textFromContent(value.content);
482
+ }
483
+ if ((value.type === "text" || value.type === "output_text") && typeof value.value === "string") {
484
+ return value.value;
485
+ }
486
+ return "";
487
+ }
488
+
489
+ function messageFieldText(message: Record<string, unknown>, fields: readonly string[], recognizedFields: Set<string>): string {
490
+ for (const field of fields) {
491
+ const text = textFromContent(message[field]);
492
+ if (text.trim()) {
493
+ recognizedFields.add(`message.${field}`);
494
+ return text;
495
+ }
496
+ }
497
+ return "";
498
+ }
499
+
500
+ function extractNonStreamingResponse(body: unknown): ExtractedLocalResponse {
501
+ const topLevelKeys = isRecord(body) ? Object.keys(body).sort() : [];
502
+ const choices = isRecord(body) && Array.isArray(body.choices) ? body.choices : [];
503
+ const recognizedFields = new Set<string>();
504
+ const finishReasons: string[] = [];
505
+ const toolCalls = choices.flatMap((choice) => {
506
+ if (!isRecord(choice)) return [];
507
+ if (typeof choice.finish_reason === "string") finishReasons.push(choice.finish_reason);
508
+ const message = isRecord(choice.message) ? choice.message : null;
509
+ return message ? parseOpenAiToolCalls(message.tool_calls) : [];
510
+ });
511
+
512
+ const content: string[] = [];
513
+ const reasoning: string[] = [];
514
+ for (const choice of choices) {
515
+ if (!isRecord(choice)) continue;
516
+ const message = isRecord(choice.message) ? choice.message : null;
517
+ const messageContent = message
518
+ ? messageFieldText(message, ["content"], recognizedFields)
519
+ : "";
520
+ const legacyText = textFromContent(choice.text);
521
+ if (legacyText.trim()) recognizedFields.add("choice.text");
522
+ const answer = messageContent || legacyText;
523
+ if (answer.trim()) {
524
+ content.push(answer);
525
+ continue;
526
+ }
527
+
528
+ const reasoningText = message
529
+ ? messageFieldText(message, ["reasoning_content", "reasoning", "analysis"], recognizedFields)
530
+ : "";
531
+ const choiceReasoning = !reasoningText
532
+ ? messageFieldText(choice, ["reasoning_content", "reasoning", "analysis"], recognizedFields)
533
+ : "";
534
+ if ((reasoningText || choiceReasoning).trim()) {
535
+ reasoning.push(reasoningText || choiceReasoning);
536
+ }
537
+ }
538
+
539
+ return {
540
+ // Reasoning is a fallback only. A response with both fields should show its
541
+ // final answer, while reasoning-only servers still produce useful text.
542
+ text: (content.length > 0 ? content : reasoning).join("").trim(),
543
+ ...(toolCalls.length > 0 ? { toolCalls } : {}),
544
+ diagnostics: {
545
+ choiceCount: choices.length,
546
+ finishReasons: [...new Set(finishReasons)],
547
+ recognizedFields: [...recognizedFields].sort(),
548
+ topLevelKeys,
549
+ },
550
+ };
551
+ }
552
+
553
+ function emptyResponseError(config: LocalProviderConfig, model: string, diagnostics: LocalResponseDiagnostics): Error {
554
+ const finishReasons = diagnostics.finishReasons.length > 0 ? diagnostics.finishReasons.join(", ") : "none";
555
+ const fields = diagnostics.recognizedFields.length > 0 ? diagnostics.recognizedFields.join(", ") : "none";
556
+ const keys = diagnostics.topLevelKeys.length > 0 ? diagnostics.topLevelKeys.join(", ") : "none";
557
+ return new Error([
558
+ "Local OpenAI-compatible API returned no assistant text.",
559
+ `Endpoint: ${config.baseUrl}/chat/completions`,
560
+ `Model: ${model}`,
561
+ `Response details: choices=${diagnostics.choiceCount}; finish_reason=${finishReasons}; recognized_fields=${fields}; top_level_keys=${keys}.`,
562
+ "Check the local server response format or retry the prompt.",
563
+ ].join("\n"));
564
+ }
474
565
 
475
566
  function parseStreamDelta(line: string): string | null {
476
567
  const trimmed = line.trim();
@@ -521,18 +612,19 @@ async function postLocalChatCompletion(options: {
521
612
  request: ProviderChatRequest;
522
613
  config: LocalProviderConfig;
523
614
  stream: boolean;
615
+ messages?: readonly AgentChatMessage[];
524
616
  fetchImpl: FetchImpl;
525
617
  signal?: AbortSignal;
526
618
  handlers: BackendRunHandlers;
527
619
  capProfile: import("./capabilityProfile.js").ModelCapabilityProfile;
528
- }): Promise<string> {
620
+ }): Promise<AgentChatResponse> {
529
621
  const model = getCachedSelectedModel(options.config, options.request.route.modelId);
530
622
  const includeSystemPrompt = options.capProfile.supportsSystemPrompt !== false;
531
- const messages = [
623
+ const messages: readonly AgentChatMessage[] = options.messages ?? [
532
624
  ...(includeSystemPrompt && options.request.projectInstructions?.content
533
- ? [{ role: "system", content: options.request.projectInstructions.content }]
625
+ ? [{ role: "system" as const, content: options.request.projectInstructions.content }]
534
626
  : []),
535
- { role: "user", content: options.request.prompt },
627
+ { role: "user" as const, content: options.request.prompt },
536
628
  ];
537
629
  const response = await options.fetchImpl(`${options.config.baseUrl}/chat/completions`, {
538
630
  method: "POST",
@@ -560,11 +652,15 @@ async function postLocalChatCompletion(options: {
560
652
  }
561
653
 
562
654
  if (options.stream) {
563
- return readStreamingResponse(response, options.handlers);
655
+ return { text: await readStreamingResponse(response, options.handlers) };
564
656
  }
565
657
 
566
- const parsed = JSON.parse(await response.text()) as unknown;
567
- return extractNonStreamingText(parsed);
658
+ const parsed = JSON.parse(await response.text()) as unknown;
659
+ const extracted = extractNonStreamingResponse(parsed);
660
+ if (!extracted.text.trim() && (extracted.toolCalls?.length ?? 0) === 0) {
661
+ throw emptyResponseError(options.config, model, extracted.diagnostics);
662
+ }
663
+ return extracted;
568
664
  }
569
665
 
570
666
  function isModelNotLoadedError(status: number, body: string): boolean {
@@ -589,42 +685,24 @@ export async function runLocalOpenAiCompatible(
589
685
  providerConfig: request.localConfig ?? configuredOverride,
590
686
  rawMetadata: rawMeta,
591
687
  });
592
- const streamingSupported = capProfile.supportsStreaming !== false;
593
-
594
- if (streamingSupported) {
595
- try {
596
- const streamed = await postLocalChatCompletion({
688
+ const text = await runAgentLoop({
689
+ request,
690
+ handlers,
691
+ includeSystemPrompt: capProfile.supportsSystemPrompt !== false,
692
+ signal: options.signal,
693
+ sendMessages: async (messages) =>
694
+ postLocalChatCompletion({
597
695
  request,
598
696
  config,
599
- stream: true,
697
+ messages,
698
+ stream: false,
600
699
  fetchImpl,
601
700
  signal: options.signal,
602
701
  handlers,
603
702
  capProfile,
604
- });
605
- if (streamed) return streamed;
606
- } catch (error) {
607
- if (options.signal?.aborted) throw error;
608
- handlers.onProgress?.({
609
- id: "local-stream-fallback",
610
- source: "stderr",
611
- text: "Local streaming request failed; retrying without streaming.",
612
- });
613
- }
614
- }
615
-
616
- const text = await postLocalChatCompletion({
617
- request,
618
- config,
619
- stream: false,
620
- fetchImpl,
621
- signal: options.signal,
622
- handlers,
623
- capProfile,
703
+ }),
624
704
  });
625
- if (!text) {
626
- throw new Error("Local OpenAI-compatible API returned no assistant text.");
627
- }
705
+ if (!text) throw new Error("Local OpenAI-compatible API returned no assistant text after tool execution.");
628
706
  handlers.onAssistantDelta?.(text);
629
707
  return text;
630
708
  }
@@ -73,16 +73,20 @@ export const GEMINI_FALLBACK_MODELS: readonly ProviderModel[] = [
73
73
  },
74
74
  ] as const;
75
75
 
76
+ function fallbackModelDescription(family: string): string {
77
+ return `Claude ${family} alias — version unknown because model discovery is unavailable`;
78
+ }
79
+
76
80
  export const ANTHROPIC_FALLBACK_MODELS: readonly ProviderModel[] = [
77
81
  {
78
82
  id: "opus",
79
83
  modelId: "opus",
80
- label: "Opus 4.7",
81
- description: "Claude Opus 4.7 - Fallback defaults",
84
+ label: "Claude Opus (version unknown)",
85
+ description: fallbackModelDescription("Opus"),
82
86
  defaultReasoningLevel: "xhigh",
83
87
  supportedReasoningLevels: getClaudeCodeEffortLevels(["low", "medium", "high", "xhigh", "max"]),
84
88
  source: "fallback",
85
- canonicalId: "claude-opus-4-7",
89
+ canonicalId: "opus",
86
90
  family: "opus",
87
91
  effortSource: "fallback",
88
92
  effortVerified: false,
@@ -90,12 +94,12 @@ export const ANTHROPIC_FALLBACK_MODELS: readonly ProviderModel[] = [
90
94
  {
91
95
  id: "sonnet",
92
96
  modelId: "sonnet",
93
- label: "Sonnet 4.6",
94
- description: "Claude Sonnet 4.6 - Fallback defaults",
97
+ label: "Claude Sonnet (version unknown)",
98
+ description: fallbackModelDescription("Sonnet"),
95
99
  defaultReasoningLevel: "high",
96
100
  supportedReasoningLevels: getClaudeCodeEffortLevels(["low", "medium", "high", "max"]),
97
101
  source: "fallback",
98
- canonicalId: "claude-sonnet-4-6",
102
+ canonicalId: "sonnet",
99
103
  family: "sonnet",
100
104
  effortSource: "fallback",
101
105
  effortVerified: false,
@@ -103,20 +107,27 @@ export const ANTHROPIC_FALLBACK_MODELS: readonly ProviderModel[] = [
103
107
  {
104
108
  id: "haiku",
105
109
  modelId: "haiku",
106
- label: "Haiku 4.5",
107
- description: "Claude Haiku 4.5 - Fallback defaults; effort metadata unverified",
110
+ label: "Claude Haiku (version unknown)",
111
+ description: fallbackModelDescription("Haiku"),
108
112
  defaultReasoningLevel: "medium",
109
113
  supportedReasoningLevels: getClaudeCodeEffortLevels(["low", "medium", "high"]),
110
114
  source: "fallback",
111
- canonicalId: "claude-haiku-4-5",
115
+ canonicalId: "haiku",
112
116
  family: "haiku",
113
117
  effortSource: "fallback",
114
118
  effortVerified: false,
115
119
  },
116
- ] as const;
120
+ ];
117
121
 
118
122
  function isRuntimeSource(source: ProviderModel["source"]): boolean {
119
- return source === "discovered" || source === "claude-code" || source === "settings" || source === "config";
123
+ return source === "discovered"
124
+ || source === "claude-code"
125
+ || source === "claude-code-command"
126
+ || source === "claude-code-package"
127
+ || source === "claude-code-cache"
128
+ || source === "claude-code-config"
129
+ || source === "settings"
130
+ || source === "config";
120
131
  }
121
132
 
122
133
  export function providerModelsToCodexCapabilities(
@@ -1,9 +1,10 @@
1
1
  import { codexSubprocessProvider } from "../providers/codexSubprocess.js";
2
2
  import type { BackendRunHandlers } from "../providers/types.js";
3
3
  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";
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";
7
8
  import {
8
9
  ANTHROPIC_FALLBACK_MODELS,
9
10
  GEMINI_DEFAULT_MODEL_ID,
@@ -71,10 +72,11 @@ function unavailableRuntime(providerId: ProviderId, label: string): ProviderRunt
71
72
 
72
73
  const PROVIDER_RUNTIMES: Record<ProviderId, ProviderRuntime> = {
73
74
  openai: openAiRuntime,
74
- anthropic: anthropicRuntime,
75
- google: geminiRuntime,
76
- local: localRuntime,
77
- };
75
+ anthropic: anthropicRuntime,
76
+ google: geminiRuntime,
77
+ local: localRuntime,
78
+ antigravity: antigravityRuntime,
79
+ };
78
80
 
79
81
  export function getProviderRuntime(providerId: ProviderId): ProviderRuntime {
80
82
  return PROVIDER_RUNTIMES[providerId];
@@ -100,11 +102,12 @@ export function discoverProviderModels(providerId: ProviderId): ProviderModelDis
100
102
 
101
103
  export async function validateProviderRouteActivation(options: {
102
104
  route: ProviderRoute;
103
- workspaceRoot: string;
104
- geminiCommandPath?: string | null;
105
- claudeCommandPath?: string | null;
106
- localConfig?: ProviderWorkspaceOverride | null;
107
- }): Promise<ProviderRouteValidationResult> {
105
+ workspaceRoot: string;
106
+ geminiCommandPath?: string | null;
107
+ claudeCommandPath?: string | null;
108
+ antigravityCommandPath?: string | null;
109
+ localConfig?: ProviderWorkspaceOverride | null;
110
+ }): Promise<ProviderRouteValidationResult> {
108
111
  const runtime = getProviderRuntime(options.route.providerId);
109
112
  if (!runtime.routeAvailable) {
110
113
  return {
@@ -154,7 +157,7 @@ export function resolveActiveProviderRoute(options: {
154
157
  currentReasoning: string;
155
158
  }): ActiveProviderRoute {
156
159
  const configuredRoute = options.workspaceConfigActiveRoute;
157
- if (configuredRoute && isProviderRoutableInCodexa(configuredRoute.providerId)) {
160
+ if (configuredRoute && configuredRoute.providerId !== "google" && isProviderRoutableInCodexa(configuredRoute.providerId)) {
158
161
  const route: ActiveProviderRoute = {
159
162
  providerId: configuredRoute.providerId,
160
163
  modelId: configuredRoute.modelId,
@@ -163,19 +166,36 @@ export function resolveActiveProviderRoute(options: {
163
166
  ...(configuredRoute.modelSelection ? { modelSelection: configuredRoute.modelSelection } : {}),
164
167
  };
165
168
 
166
- if (route.providerId === "google" && route.modelSelection) {
167
- route.modelId = resolveGeminiModelId(route.modelSelection);
168
- } else if (route.providerId === "google") {
169
- route.modelId = normalizeGeminiModelId(route.modelId);
170
- } else if (route.providerId === "local") {
171
- const discovery = discoverProviderModels("local");
172
- const selectedModel = typeof discovery.diagnostics?.selectedModel === "string"
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"
173
187
  ? discovery.diagnostics.selectedModel.trim()
174
188
  : "";
175
- if (discovery.status === "ready" && selectedModel) {
176
- route.modelId = selectedModel;
177
- }
178
- }
189
+ if (discovery.status === "ready" && selectedModel) {
190
+ route.modelId = selectedModel;
191
+ }
192
+ } else if (route.providerId === "antigravity") {
193
+ const migrated = migrateAntigravityLegacyModelId(route.modelId);
194
+ route.modelId = migrated.modelId;
195
+ if (!route.reasoning && migrated.reasoning) {
196
+ route.reasoning = migrated.reasoning;
197
+ }
198
+ }
179
199
 
180
200
  return route;
181
201
  }
@@ -190,14 +210,21 @@ export function resolveActiveProviderRoute(options: {
190
210
 
191
211
  export function getDefaultRouteModel(providerId: ProviderId, currentOpenAiModel: string): string {
192
212
  if (providerId === "anthropic") {
193
- return ANTHROPIC_FALLBACK_MODELS[0]?.modelId ?? "claude-sonnet-4-20250514";
213
+ const discovered = discoverProviderModels("anthropic");
214
+ if (discovered.status === "ready" && discovered.models.length > 0) {
215
+ return discovered.models[0].modelId;
216
+ }
217
+ return ANTHROPIC_FALLBACK_MODELS[0]?.modelId ?? "sonnet";
194
218
  }
195
219
  if (providerId === "google") {
196
220
  return GEMINI_FALLBACK_MODELS[0]?.modelId ?? GEMINI_DEFAULT_MODEL_ID;
197
221
  }
198
- if (providerId === "local") {
199
- const discovery = discoverProviderModels("local");
200
- return discovery.models[0]?.modelId ?? "Local default";
201
- }
202
- return currentOpenAiModel;
203
- }
222
+ if (providerId === "local") {
223
+ const discovery = discoverProviderModels("local");
224
+ return discovery.models[0]?.modelId ?? "Local default";
225
+ }
226
+ if (providerId === "antigravity") {
227
+ return ANTIGRAVITY_DEFAULT_MODEL_ID;
228
+ }
229
+ return currentOpenAiModel;
230
+ }
@@ -1,5 +1,5 @@
1
1
  import type { ReasoningEffortCapability } from "../models/codexModelCapabilities.js";
2
- import type { ProjectInstructions } from "../projectInstructions.js";
2
+ import type { ProjectInstructions } from "../workspace/projectInstructions.js";
3
3
  import type { BackendRunHandlers } from "../providers/types.js";
4
4
  import type { ResolvedRuntimeConfig } from "../../config/runtimeConfig.js";
5
5
  export type { ResolvedRuntimeConfig };
@@ -7,10 +7,11 @@ import type { ProviderId } from "../providerLauncher/types.js";
7
7
  import type { ProviderWorkspaceOverride } from "../providerLauncher/types.js";
8
8
 
9
9
  export type ProviderBackendKind =
10
- | "codex-cli-auth"
11
- | "gemini-cli-auth"
12
- | "claude-code-auth"
13
- | "openai-api-key"
10
+ | "codex-cli-auth"
11
+ | "gemini-cli-auth"
12
+ | "claude-code-auth"
13
+ | "antigravity-cli-auth"
14
+ | "openai-api-key"
14
15
  | "gemini-api-key"
15
16
  | "anthropic-api-key"
16
17
  | "local-openai-compatible"
@@ -23,10 +24,13 @@ export interface ProviderModel {
23
24
  description: string | null;
24
25
  defaultReasoningLevel: string | null;
25
26
  supportedReasoningLevels: readonly ReasoningEffortCapability[] | null;
26
- source?: "discovered" | "claude-code" | "settings" | "config" | "fallback";
27
- canonicalId?: string;
28
- family?: string;
29
- effortSource?: "claude-code" | "settings" | "config" | "fallback";
27
+ source?: "discovered" | "claude-code" | "claude-code-command" | "claude-code-package" | "claude-code-cache" | "claude-code-config" | "settings" | "config" | "fallback";
28
+ canonicalId?: string;
29
+ family?: string;
30
+ version?: string;
31
+ isFallback?: boolean;
32
+ discoveryKind?: "models" | "aliases";
33
+ effortSource?: "claude-code" | "claude-code-command" | "claude-code-package" | "claude-code-cache" | "claude-code-config" | "settings" | "config" | "fallback";
30
34
  effortVerified?: boolean;
31
35
  raw?: unknown;
32
36
  }
@@ -58,11 +62,12 @@ export type ActiveProviderRoute = ProviderRoute;
58
62
 
59
63
  export interface ProviderRouteValidationRequest {
60
64
  route: ProviderRoute;
61
- workspaceRoot: string;
62
- geminiCommandPath?: string | null;
63
- claudeCommandPath?: string | null;
64
- localConfig?: ProviderWorkspaceOverride | null;
65
- }
65
+ workspaceRoot: string;
66
+ geminiCommandPath?: string | null;
67
+ claudeCommandPath?: string | null;
68
+ antigravityCommandPath?: string | null;
69
+ localConfig?: ProviderWorkspaceOverride | null;
70
+ }
66
71
 
67
72
  export interface ProviderRouteValidationResult {
68
73
  status: "ready" | "not-configured";
@@ -1,8 +1,8 @@
1
1
  import { spawn } from "child_process";
2
2
  import { formatCodexLaunchError, spawnCodexProcess } from "../executables/codexExecutable.js";
3
- import { prepareCodexExecLaunch } from "../codexLaunch.js";
3
+ import { prepareCodexExecLaunch } from "../codex/codexLaunch.js";
4
4
  import * as perf from "../perf/profiler.js";
5
- import { buildCodexPrompt } from "../codexPrompt.js";
5
+ import { buildCodexPrompt } from "../codex/codexPrompt.js";
6
6
  import { createTerminalTitleSequenceStripper } from "../terminal/terminalTitle.js";
7
7
  import { createCodexJsonStreamParser } from "./codexJsonStream.js";
8
8
  import {
@@ -1,6 +1,6 @@
1
1
  import type { AvailableBackend } from "../../config/settings.js";
2
2
  import type { ResolvedRuntimeConfig } from "../../config/runtimeConfig.js";
3
- import type { ProjectInstructions } from "../projectInstructions.js";
3
+ import type { ProjectInstructions } from "../workspace/projectInstructions.js";
4
4
  import type { RunProgressSource, RunToolActivity } from "../../session/types.js";
5
5
 
6
6
  export interface BackendProgressUpdate {
@@ -1,5 +1,10 @@
1
- import { access, copyFile, mkdir } from "node:fs/promises";
1
+ import { access, copyFile, mkdir, stat } from "node:fs/promises";
2
+ import { existsSync } from "node:fs";
2
3
  import path from "node:path";
4
+ import {
5
+ isSkippedExternalDependencyPath,
6
+ normalizeDiagnosticPath,
7
+ } from "../workspace/workspaceGuard.js";
3
8
 
4
9
  export const IMAGE_EXTENSIONS = new Set([
5
10
  ".png",
@@ -44,10 +49,28 @@ export async function resolveAttachmentDestPath(
44
49
  export async function importExternalFile(
45
50
  srcPath: string,
46
51
  attachmentsDir: string,
47
- ): Promise<string> {
52
+ ): Promise<string | null> {
53
+ const normalized = normalizeDiagnosticPath(srcPath);
54
+
55
+ if (isSkippedExternalDependencyPath(normalized)) {
56
+ return null;
57
+ }
58
+
59
+ try {
60
+ if (!existsSync(normalized)) {
61
+ return null;
62
+ }
63
+ const s = await stat(normalized);
64
+ if (!s.isFile()) {
65
+ return null;
66
+ }
67
+ } catch {
68
+ return null;
69
+ }
70
+
48
71
  await mkdir(attachmentsDir, { recursive: true });
49
- const destPath = await resolveAttachmentDestPath(srcPath, attachmentsDir);
50
- await copyFile(srcPath, destPath);
72
+ const destPath = await resolveAttachmentDestPath(normalized, attachmentsDir);
73
+ await copyFile(normalized, destPath);
51
74
  return destPath;
52
75
  }
53
76
 
@@ -1,4 +1,4 @@
1
- import type { RunToolActivity } from "../session/types.js";
1
+ import type { RunToolActivity } from "../../session/types.js";
2
2
 
3
3
  const DELETE_COMMAND_PATTERN =
4
4
  /(?:^|[\s;&|])(?:remove-item|rm|rmdir|del|erase|unlink)\b/i;
@@ -1,4 +1,4 @@
1
- import type { HollowResponseResult } from "./codexPrompt.js";
1
+ import type { HollowResponseResult } from "../codex/codexPrompt.js";
2
2
 
3
3
  const MESSAGES: Record<string, [string, string]> = {
4
4
  greeting: [