@oh-my-pi/pi-coding-agent 16.0.4 → 16.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (270) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/dist/cli.js +2027 -1396
  3. package/dist/types/advisor/advise-tool.d.ts +31 -19
  4. package/dist/types/autoresearch/tools/init-experiment.d.ts +13 -17
  5. package/dist/types/autoresearch/tools/log-experiment.d.ts +17 -19
  6. package/dist/types/autoresearch/tools/run-experiment.d.ts +3 -4
  7. package/dist/types/autoresearch/tools/update-notes.d.ts +4 -5
  8. package/dist/types/cli/args.d.ts +1 -0
  9. package/dist/types/cli/bench-cli.d.ts +6 -0
  10. package/dist/types/cli/ttsr-cli.d.ts +39 -0
  11. package/dist/types/commands/launch.d.ts +3 -0
  12. package/dist/types/commands/ttsr.d.ts +57 -0
  13. package/dist/types/commit/agentic/tools/analyze-file.d.ts +4 -5
  14. package/dist/types/commit/agentic/tools/git-file-diff.d.ts +4 -5
  15. package/dist/types/commit/agentic/tools/git-hunk.d.ts +5 -6
  16. package/dist/types/commit/agentic/tools/git-overview.d.ts +4 -5
  17. package/dist/types/commit/agentic/tools/propose-changelog.d.ts +23 -24
  18. package/dist/types/commit/agentic/tools/propose-commit.d.ts +11 -32
  19. package/dist/types/commit/agentic/tools/recent-commits.d.ts +3 -4
  20. package/dist/types/commit/agentic/tools/schemas.d.ts +6 -27
  21. package/dist/types/commit/agentic/tools/split-commit.d.ts +28 -49
  22. package/dist/types/commit/changelog/generate.d.ts +12 -13
  23. package/dist/types/commit/shared-llm.d.ts +10 -37
  24. package/dist/types/config/config-file.d.ts +4 -4
  25. package/dist/types/config/keybindings.d.ts +5 -0
  26. package/dist/types/config/models-config-schema.d.ts +625 -990
  27. package/dist/types/config/models-config.d.ts +229 -217
  28. package/dist/types/config/settings-schema.d.ts +144 -25
  29. package/dist/types/edit/hashline/params.d.ts +7 -11
  30. package/dist/types/edit/index.d.ts +2 -1
  31. package/dist/types/edit/modes/apply-patch.d.ts +4 -5
  32. package/dist/types/edit/modes/patch.d.ts +15 -24
  33. package/dist/types/edit/modes/replace.d.ts +16 -17
  34. package/dist/types/eval/js/index.d.ts +1 -0
  35. package/dist/types/extensibility/custom-commands/types.d.ts +6 -3
  36. package/dist/types/extensibility/custom-tools/types.d.ts +8 -5
  37. package/dist/types/extensibility/extensions/runner.d.ts +5 -2
  38. package/dist/types/extensibility/extensions/types.d.ts +14 -10
  39. package/dist/types/extensibility/hooks/types.d.ts +7 -4
  40. package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +13 -5
  41. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +17 -0
  42. package/dist/types/extensibility/shared-events.d.ts +22 -1
  43. package/dist/types/extensibility/typebox.d.ts +80 -58
  44. package/dist/types/goals/tools/goal-tool.d.ts +11 -24
  45. package/dist/types/index.d.ts +2 -0
  46. package/dist/types/lsp/index.d.ts +11 -26
  47. package/dist/types/lsp/types.d.ts +12 -28
  48. package/dist/types/main.d.ts +1 -0
  49. package/dist/types/mcp/client.d.ts +8 -0
  50. package/dist/types/modes/components/btw-panel.d.ts +1 -0
  51. package/dist/types/modes/components/custom-editor.d.ts +3 -1
  52. package/dist/types/modes/components/status-line/component.d.ts +1 -1
  53. package/dist/types/modes/components/status-line/context-thresholds.d.ts +0 -1
  54. package/dist/types/modes/controllers/btw-controller.d.ts +2 -0
  55. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  56. package/dist/types/modes/interactive-mode.d.ts +3 -0
  57. package/dist/types/modes/rpc/rpc-types.d.ts +1 -1
  58. package/dist/types/modes/setup-wizard/index.d.ts +1 -0
  59. package/dist/types/modes/setup-wizard/startup-splash.d.ts +7 -0
  60. package/dist/types/modes/theme/theme.d.ts +1 -1
  61. package/dist/types/modes/types.d.ts +3 -0
  62. package/dist/types/modes/utils/context-usage.d.ts +12 -0
  63. package/dist/types/sdk.d.ts +8 -1
  64. package/dist/types/session/agent-session.d.ts +24 -0
  65. package/dist/types/session/session-persistence.d.ts +4 -0
  66. package/dist/types/startup-splash.d.ts +12 -0
  67. package/dist/types/task/types.d.ts +47 -48
  68. package/dist/types/tools/ask.d.ts +26 -27
  69. package/dist/types/tools/ast-edit.d.ts +17 -17
  70. package/dist/types/tools/ast-grep.d.ts +12 -13
  71. package/dist/types/tools/bash.d.ts +20 -17
  72. package/dist/types/tools/browser.d.ts +46 -71
  73. package/dist/types/tools/checkpoint.d.ts +14 -15
  74. package/dist/types/tools/debug.d.ts +82 -145
  75. package/dist/types/tools/eval.d.ts +30 -40
  76. package/dist/types/tools/find.d.ts +17 -18
  77. package/dist/types/tools/gh.d.ts +49 -78
  78. package/dist/types/tools/image-gen.d.ts +20 -36
  79. package/dist/types/tools/inspect-image.d.ts +10 -11
  80. package/dist/types/tools/irc.d.ts +22 -33
  81. package/dist/types/tools/job.d.ts +11 -12
  82. package/dist/types/tools/learn.d.ts +21 -28
  83. package/dist/types/tools/manage-skill.d.ts +13 -22
  84. package/dist/types/tools/memory-edit.d.ts +15 -24
  85. package/dist/types/tools/memory-recall.d.ts +7 -8
  86. package/dist/types/tools/memory-reflect.d.ts +9 -10
  87. package/dist/types/tools/memory-retain.d.ts +13 -14
  88. package/dist/types/tools/read.d.ts +8 -8
  89. package/dist/types/tools/resolve.d.ts +11 -18
  90. package/dist/types/tools/review.d.ts +9 -15
  91. package/dist/types/tools/search-tool-bm25.d.ts +9 -10
  92. package/dist/types/tools/search.d.ts +16 -17
  93. package/dist/types/tools/ssh.d.ts +14 -15
  94. package/dist/types/tools/todo.d.ts +27 -43
  95. package/dist/types/tools/tts.d.ts +8 -9
  96. package/dist/types/tools/write.d.ts +9 -10
  97. package/dist/types/tui/code-cell.d.ts +2 -0
  98. package/dist/types/tui/index.d.ts +1 -0
  99. package/dist/types/tui/width-aware-text.d.ts +23 -0
  100. package/dist/types/utils/image-vision-fallback.d.ts +28 -0
  101. package/dist/types/utils/markit.d.ts +10 -1
  102. package/dist/types/web/search/index.d.ts +17 -28
  103. package/dist/types/web/search/providers/base.d.ts +1 -0
  104. package/dist/types/web/search/providers/gemini.d.ts +1 -0
  105. package/dist/types/web/search/providers/perplexity.d.ts +0 -2
  106. package/dist/types/web/search/types.d.ts +32 -26
  107. package/package.json +14 -13
  108. package/scripts/omp +1 -1
  109. package/src/advisor/__tests__/advisor.test.ts +103 -1
  110. package/src/advisor/advise-tool.ts +47 -11
  111. package/src/autoresearch/tools/init-experiment.ts +13 -16
  112. package/src/autoresearch/tools/log-experiment.ts +15 -18
  113. package/src/autoresearch/tools/run-experiment.ts +3 -3
  114. package/src/autoresearch/tools/update-notes.ts +4 -4
  115. package/src/cli/args.ts +1 -0
  116. package/src/cli/bench-cli.ts +30 -7
  117. package/src/cli/flag-tables.ts +8 -0
  118. package/src/cli/ttsr-cli.ts +995 -0
  119. package/src/cli-commands.ts +1 -0
  120. package/src/cli.ts +7 -1
  121. package/src/collab/host.ts +2 -2
  122. package/src/commands/launch.ts +3 -0
  123. package/src/commands/ttsr.ts +125 -0
  124. package/src/commit/agentic/tools/analyze-file.ts +4 -4
  125. package/src/commit/agentic/tools/git-file-diff.ts +4 -4
  126. package/src/commit/agentic/tools/git-hunk.ts +7 -5
  127. package/src/commit/agentic/tools/git-overview.ts +4 -4
  128. package/src/commit/agentic/tools/propose-changelog.ts +18 -15
  129. package/src/commit/agentic/tools/propose-commit.ts +6 -6
  130. package/src/commit/agentic/tools/recent-commits.ts +3 -3
  131. package/src/commit/agentic/tools/schemas.ts +8 -20
  132. package/src/commit/agentic/tools/split-commit.ts +19 -23
  133. package/src/commit/analysis/summary.ts +7 -5
  134. package/src/commit/changelog/generate.ts +15 -11
  135. package/src/commit/shared-llm.ts +17 -24
  136. package/src/config/config-file.ts +13 -15
  137. package/src/config/keybindings.ts +6 -0
  138. package/src/config/models-config-schema.ts +206 -179
  139. package/src/config/settings-schema.ts +118 -2
  140. package/src/discovery/builtin-rules/index.ts +2 -0
  141. package/src/discovery/builtin-rules/ts-import-type.md +2 -2
  142. package/src/discovery/builtin-rules/ts-no-any.md +11 -2
  143. package/src/discovery/builtin-rules/ts-no-inline-cast-access.md +55 -0
  144. package/src/edit/hashline/params.ts +12 -11
  145. package/src/edit/index.ts +5 -4
  146. package/src/edit/modes/apply-patch.ts +4 -4
  147. package/src/edit/modes/patch.ts +15 -18
  148. package/src/edit/modes/replace.ts +13 -17
  149. package/src/edit/renderer.ts +0 -1
  150. package/src/eval/agent-bridge.ts +11 -13
  151. package/src/eval/completion-bridge.ts +25 -17
  152. package/src/eval/js/context-manager.ts +17 -2
  153. package/src/eval/js/index.ts +1 -1
  154. package/src/eval/py/executor.ts +2 -2
  155. package/src/eval/py/runner.py +44 -0
  156. package/src/extensibility/custom-commands/loader.ts +5 -3
  157. package/src/extensibility/custom-commands/types.ts +6 -3
  158. package/src/extensibility/custom-tools/loader.ts +4 -2
  159. package/src/extensibility/custom-tools/types.ts +8 -5
  160. package/src/extensibility/extensions/loader.ts +4 -2
  161. package/src/extensibility/extensions/runner.ts +20 -2
  162. package/src/extensibility/extensions/types.ts +22 -8
  163. package/src/extensibility/hooks/loader.ts +5 -2
  164. package/src/extensibility/hooks/types.ts +7 -4
  165. package/src/extensibility/legacy-pi-ai-shim.ts +42 -5
  166. package/src/extensibility/legacy-pi-coding-agent-shim.ts +113 -0
  167. package/src/extensibility/plugins/legacy-pi-compat.ts +13 -13
  168. package/src/extensibility/shared-events.ts +24 -0
  169. package/src/extensibility/tool-proxy.ts +4 -1
  170. package/src/extensibility/typebox.ts +778 -251
  171. package/src/goals/guided-setup.ts +12 -3
  172. package/src/goals/tools/goal-tool.ts +6 -6
  173. package/src/index.ts +2 -0
  174. package/src/internal-urls/docs-index.generated.ts +15 -13
  175. package/src/lsp/types.ts +13 -27
  176. package/src/main.ts +29 -21
  177. package/src/mcp/client.ts +38 -13
  178. package/src/mcp/render.ts +102 -89
  179. package/src/modes/components/agent-hub.ts +11 -4
  180. package/src/modes/components/branch-summary-message.ts +1 -0
  181. package/src/modes/components/btw-panel.ts +5 -1
  182. package/src/modes/components/collab-prompt-message.ts +9 -7
  183. package/src/modes/components/compaction-summary-message.ts +1 -0
  184. package/src/modes/components/custom-editor.ts +18 -0
  185. package/src/modes/components/custom-message.ts +1 -0
  186. package/src/modes/components/footer.ts +6 -5
  187. package/src/modes/components/hook-message.ts +1 -0
  188. package/src/modes/components/read-tool-group.ts +9 -3
  189. package/src/modes/components/skill-message.ts +1 -0
  190. package/src/modes/components/status-line/component.ts +139 -15
  191. package/src/modes/components/status-line/context-thresholds.ts +0 -1
  192. package/src/modes/components/todo-reminder.ts +1 -0
  193. package/src/modes/components/tool-execution.ts +17 -10
  194. package/src/modes/components/ttsr-notification.ts +1 -0
  195. package/src/modes/components/user-message.ts +6 -6
  196. package/src/modes/controllers/btw-controller.ts +69 -1
  197. package/src/modes/controllers/event-controller.ts +2 -7
  198. package/src/modes/controllers/input-controller.ts +29 -0
  199. package/src/modes/controllers/selector-controller.ts +10 -3
  200. package/src/modes/interactive-mode.ts +42 -10
  201. package/src/modes/rpc/rpc-types.ts +1 -1
  202. package/src/modes/setup-wizard/index.ts +1 -0
  203. package/src/modes/setup-wizard/scenes/sign-in.ts +77 -5
  204. package/src/modes/setup-wizard/startup-splash.ts +107 -0
  205. package/src/modes/theme/theme.ts +133 -143
  206. package/src/modes/types.ts +3 -0
  207. package/src/modes/utils/context-usage.ts +37 -20
  208. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  209. package/src/prompts/system/system-prompt.md +1 -0
  210. package/src/prompts/tools/image-attachment-describe-system.md +8 -0
  211. package/src/prompts/tools/image-attachment-describe.md +10 -0
  212. package/src/sdk.ts +35 -22
  213. package/src/session/agent-session.ts +715 -255
  214. package/src/session/session-history-format.ts +11 -2
  215. package/src/session/session-loader.ts +19 -32
  216. package/src/session/session-persistence.ts +27 -11
  217. package/src/session/snapcompact-inline.ts +1 -1
  218. package/src/slash-commands/builtin-registry.ts +4 -11
  219. package/src/ssh/connection-manager.ts +3 -2
  220. package/src/startup-splash.ts +19 -0
  221. package/src/task/executor.ts +12 -7
  222. package/src/task/types.ts +44 -41
  223. package/src/tool-discovery/tool-index.ts +17 -4
  224. package/src/tools/ask.ts +14 -14
  225. package/src/tools/ast-edit.ts +17 -14
  226. package/src/tools/ast-grep.ts +10 -9
  227. package/src/tools/bash.ts +15 -10
  228. package/src/tools/browser/launch.ts +13 -0
  229. package/src/tools/browser.ts +26 -32
  230. package/src/tools/checkpoint.ts +7 -7
  231. package/src/tools/debug.ts +72 -69
  232. package/src/tools/eval.ts +18 -19
  233. package/src/tools/find.ts +20 -13
  234. package/src/tools/gh.ts +29 -49
  235. package/src/tools/image-gen.ts +94 -57
  236. package/src/tools/inspect-image.ts +8 -9
  237. package/src/tools/irc.ts +12 -12
  238. package/src/tools/job.ts +6 -6
  239. package/src/tools/learn.ts +11 -14
  240. package/src/tools/manage-skill.ts +19 -23
  241. package/src/tools/memory-edit.ts +8 -8
  242. package/src/tools/memory-recall.ts +4 -4
  243. package/src/tools/memory-reflect.ts +5 -5
  244. package/src/tools/memory-retain.ts +9 -11
  245. package/src/tools/puppeteer/02_stealth_hairline.txt +1 -1
  246. package/src/tools/puppeteer/04_stealth_iframe.txt +4 -4
  247. package/src/tools/puppeteer/05_stealth_webgl.txt +1 -1
  248. package/src/tools/puppeteer/10_stealth_plugins.txt +6 -4
  249. package/src/tools/puppeteer/12_stealth_codecs.txt +2 -2
  250. package/src/tools/puppeteer/13_stealth_worker.txt +1 -1
  251. package/src/tools/read.ts +197 -19
  252. package/src/tools/report-tool-issue.ts +6 -6
  253. package/src/tools/resolve.ts +6 -6
  254. package/src/tools/review.ts +10 -12
  255. package/src/tools/search-tool-bm25.ts +5 -5
  256. package/src/tools/search.ts +20 -29
  257. package/src/tools/ssh.ts +8 -8
  258. package/src/tools/todo.ts +16 -19
  259. package/src/tools/tts.ts +16 -15
  260. package/src/tools/write.ts +5 -5
  261. package/src/tui/code-cell.ts +44 -3
  262. package/src/tui/index.ts +1 -0
  263. package/src/tui/width-aware-text.ts +58 -0
  264. package/src/utils/image-vision-fallback.ts +197 -0
  265. package/src/utils/markit.ts +17 -2
  266. package/src/web/search/index.ts +21 -9
  267. package/src/web/search/providers/base.ts +1 -0
  268. package/src/web/search/providers/gemini.ts +56 -18
  269. package/src/web/search/providers/perplexity.ts +373 -126
  270. package/src/web/search/types.ts +28 -48
package/src/tools/tts.ts CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
7
7
  import { type ApiKey, ProviderHttpError, withAuth } from "@oh-my-pi/pi-ai";
8
- import { z } from "zod/v4";
8
+ import { type } from "arktype";
9
9
  import { settings } from "../config/settings";
10
10
  import type { CustomTool, CustomToolContext } from "../extensibility/custom-tools/types";
11
11
  import { ohMyPiXAIUserAgent, resolveXAIHttpCredentials } from "../lib/xai-http";
@@ -16,7 +16,6 @@ import { formatPathRelativeToCwd, resolveToCwd } from "./path-utils";
16
16
 
17
17
  // Hermes tts_tool.py L167-171
18
18
  const DEFAULT_XAI_VOICE_ID = "eve" as const;
19
- const DEFAULT_XAI_LANGUAGE = "en" as const;
20
19
  const DEFAULT_XAI_SAMPLE_RATE = 24_000;
21
20
  const DEFAULT_XAI_BIT_RATE = 128_000;
22
21
  const XAI_MAX_TEXT_LENGTH = 15_000;
@@ -31,15 +30,17 @@ const formatVoiceList = (): string =>
31
30
  type TtsCodec = "mp3" | "wav";
32
31
  type TtsBackend = "local" | "xai";
33
32
 
34
- const ttsSchema = z.object({
35
- text: z.string().min(1).max(XAI_MAX_TEXT_LENGTH),
36
- voice_id: z.string().default(DEFAULT_XAI_VOICE_ID),
37
- language: z.string().default(DEFAULT_XAI_LANGUAGE),
38
- output_path: z.string(),
39
- sample_rate: z.number().int().optional(),
40
- bit_rate: z.number().int().optional(),
33
+ const ttsSchema = type({
34
+ text: "1 <= string <= 15000",
35
+ voice_id: "string = 'eve'",
36
+ language: "string = 'en'",
37
+ output_path: "string",
38
+ sample_rate: "number.integer?",
39
+ bit_rate: "number.integer?",
41
40
  });
42
41
 
42
+ type TtsSchemaType = typeof ttsSchema.infer;
43
+
43
44
  interface TtsToolDetails {
44
45
  bytes: number;
45
46
  voiceId: string;
@@ -87,13 +88,13 @@ function readStringSetting(key: "providers.tts" | "tts.localModel" | "tts.localV
87
88
  }
88
89
 
89
90
  async function synthesizeXai(
90
- params: z.infer<typeof ttsSchema>,
91
+ params: TtsSchemaType,
91
92
  ctx: CustomToolContext,
92
93
  outputPath: string,
93
94
  displayPath: string,
94
95
  codec: TtsCodec,
95
96
  signal: AbortSignal | undefined,
96
- ): Promise<AgentToolResult<TtsToolDetails, typeof ttsSchema>> {
97
+ ): Promise<AgentToolResult<TtsToolDetails, TtsSchemaType>> {
97
98
  const creds = await resolveXAIHttpCredentials(ctx.modelRegistry);
98
99
  if (!creds) {
99
100
  return {
@@ -187,11 +188,11 @@ async function synthesizeXai(
187
188
  }
188
189
 
189
190
  async function synthesizeLocal(
190
- params: z.infer<typeof ttsSchema>,
191
+ params: TtsSchemaType,
191
192
  cwd: string,
192
193
  outputPath: string,
193
194
  signal: AbortSignal | undefined,
194
- ): Promise<AgentToolResult<TtsToolDetails, typeof ttsSchema>> {
195
+ ): Promise<AgentToolResult<TtsToolDetails, TtsSchemaType>> {
195
196
  const modelSetting = readStringSetting("tts.localModel");
196
197
  const modelKey = modelSetting && isTtsLocalModelKey(modelSetting) ? modelSetting : DEFAULT_TTS_LOCAL_MODEL_KEY;
197
198
  const voice = readStringSetting("tts.localVoice") || DEFAULT_TTS_VOICE;
@@ -242,11 +243,11 @@ export const ttsTool: CustomTool<typeof ttsSchema, TtsToolDetails> = {
242
243
  parameters: ttsSchema,
243
244
  async execute(
244
245
  _toolCallId: string,
245
- params: z.infer<typeof ttsSchema>,
246
+ params: TtsSchemaType,
246
247
  _onUpdate,
247
248
  ctx: CustomToolContext,
248
249
  signal?: AbortSignal,
249
- ): Promise<AgentToolResult<TtsToolDetails, typeof ttsSchema>> {
250
+ ): Promise<AgentToolResult<TtsToolDetails, TtsSchemaType>> {
250
251
  const cwd = ctx.sessionManager.getCwd();
251
252
  const outputPath = resolveToCwd(params.output_path, cwd);
252
253
  const displayPath = formatPathRelativeToCwd(outputPath, cwd);
@@ -6,7 +6,7 @@ import { formatHashlineHeader, stripHashlinePrefixes } from "@oh-my-pi/hashline"
6
6
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
7
7
  import type { Component } from "@oh-my-pi/pi-tui";
8
8
  import { isEnoent, isRecord, prompt, untilAborted } from "@oh-my-pi/pi-utils";
9
- import { z } from "zod/v4";
9
+ import { type } from "arktype";
10
10
 
11
11
  import { canonicalSnapshotKey, getFileSnapshotStore } from "../edit/file-snapshot-store";
12
12
  import { normalizeToLF } from "../edit/normalize";
@@ -71,12 +71,12 @@ async function loadFflate(): Promise<typeof import("fflate")> {
71
71
  return fflateModulePromise;
72
72
  }
73
73
 
74
- const writeSchema = z.object({
75
- path: z.string().describe("file path"),
76
- content: z.string().describe("file content"),
74
+ const writeSchema = type({
75
+ path: type("string").describe("file path"),
76
+ content: type("string").describe("file content"),
77
77
  });
78
78
 
79
- export type WriteToolInput = z.infer<typeof writeSchema>;
79
+ export type WriteToolInput = typeof writeSchema.infer;
80
80
 
81
81
  /** Details returned by the write tool for TUI rendering */
82
82
  export interface WriteToolDetails {
@@ -33,6 +33,8 @@ export interface CodeCellOptions {
33
33
  codeTail?: boolean;
34
34
  expanded?: boolean;
35
35
  width: number;
36
+ codeStartLine?: number;
37
+ codeLineNumbers?: Array<number | null>;
36
38
  }
37
39
 
38
40
  function getState(status?: CodeCellOptions["status"]): State | undefined {
@@ -99,7 +101,17 @@ function collapseCarriageReturns(line: string): string {
99
101
  return idx < 0 ? line : line.slice(idx + 1);
100
102
  }
101
103
  export function renderCodeCell(options: CodeCellOptions, theme: Theme): string[] {
102
- const { code, language, output, expanded = false, outputMaxLines = 6, codeMaxLines = 12, width } = options;
104
+ const {
105
+ code,
106
+ language,
107
+ output,
108
+ expanded = false,
109
+ outputMaxLines = 6,
110
+ codeMaxLines = 12,
111
+ width,
112
+ codeStartLine,
113
+ codeLineNumbers,
114
+ } = options;
103
115
  const { title, meta } = formatHeader(options, theme);
104
116
  const state = getState(options.status);
105
117
 
@@ -111,16 +123,45 @@ export function renderCodeCell(options: CodeCellOptions, theme: Theme): string[]
111
123
  const startIndex = tail ? rawCodeLines.length - maxCodeLines : 0;
112
124
  const visibleCode = rawCodeLines.slice(startIndex, startIndex + maxCodeLines).join("\n");
113
125
  const codeLines = highlightCode(visibleCode, language);
126
+
127
+ let visibleLineNumbers: Array<number | null> | undefined;
128
+ let lineNumberWidth = 0;
129
+ if (codeLineNumbers) {
130
+ visibleLineNumbers = codeLineNumbers.slice(startIndex, startIndex + maxCodeLines);
131
+ } else if (codeStartLine !== undefined) {
132
+ visibleLineNumbers = Array.from({ length: maxCodeLines }, (_, i) => codeStartLine + startIndex + i);
133
+ }
134
+
135
+ if (visibleLineNumbers) {
136
+ const validLineNums = visibleLineNumbers.filter((n): n is number => n !== null && n !== undefined);
137
+ const maxVal = validLineNums.length > 0 ? Math.max(...validLineNums) : 0;
138
+ if (maxVal > 0) {
139
+ lineNumberWidth = Math.max(2, String(maxVal).length);
140
+ }
141
+ }
142
+
143
+ if (lineNumberWidth > 0 && visibleLineNumbers) {
144
+ for (let i = 0; i < codeLines.length; i++) {
145
+ const lineNum = visibleLineNumbers[i];
146
+ const gutter =
147
+ lineNum !== null && lineNum !== undefined
148
+ ? String(lineNum).padStart(lineNumberWidth, " ")
149
+ : " ".repeat(lineNumberWidth);
150
+ codeLines[i] = theme.fg("dim", `${gutter} `) + codeLines[i];
151
+ }
152
+ }
153
+
114
154
  if (hiddenCodeLines > 0) {
115
155
  const hint = formatExpandHint(theme, expanded, hiddenCodeLines > 0);
156
+ const gutterPad = lineNumberWidth > 0 ? " ".repeat(lineNumberWidth + 1) : "";
116
157
  if (tail) {
117
158
  // Earlier rows scrolled above the live tail window — mark them on top so
118
159
  // the newest streamed line stays pinned to the bottom of the box.
119
160
  const earlier = `… ${hiddenCodeLines} earlier line${hiddenCodeLines === 1 ? "" : "s"}${hint ? ` ${hint}` : ""}`;
120
- codeLines.unshift(theme.fg("dim", earlier));
161
+ codeLines.unshift(theme.fg("dim", gutterPad + earlier));
121
162
  } else {
122
163
  const moreLine = `${formatMoreItems(hiddenCodeLines, "line")}${hint ? ` ${hint}` : ""}`;
123
- codeLines.push(theme.fg("dim", moreLine));
164
+ codeLines.push(theme.fg("dim", gutterPad + moreLine));
124
165
  }
125
166
  }
126
167
 
package/src/tui/index.ts CHANGED
@@ -10,3 +10,4 @@ export * from "./status-line";
10
10
  export * from "./tree-list";
11
11
  export * from "./types";
12
12
  export * from "./utils";
13
+ export * from "./width-aware-text";
@@ -0,0 +1,58 @@
1
+ import { type Component, getPaddingX, Text } from "@oh-my-pi/pi-tui";
2
+
3
+ /**
4
+ * Text whose content is (re)formatted against the actual render width.
5
+ *
6
+ * A plain `Text` receives an already-formatted string and only wraps it at
7
+ * render time, so width-dependent layout (per-line truncation, inline previews)
8
+ * has to be decided before the width is known. Renderers used to cope by
9
+ * hard-capping output lines at a fixed column count (e.g. 80), which truncated
10
+ * to roughly a third of a wide terminal. This defers formatting to
11
+ * `render(width)`: it computes the same content width the inner `Text` uses
12
+ * (mirroring its tight-layout flag so the budget can't desync), hands that to
13
+ * the formatter, and delegates margins/background/vertical padding to the inner
14
+ * `Text`. Lines the formatter caps at `contentWidth` fit exactly and so never
15
+ * wrap.
16
+ */
17
+ export class WidthAwareText implements Component {
18
+ #format: (contentWidth: number) => string;
19
+ readonly #paddingX: number;
20
+ #inner: Text;
21
+ #cachedContentWidth = -1;
22
+ #cachedText: string | undefined;
23
+ #ignoreTight = false;
24
+
25
+ constructor(format: (contentWidth: number) => string, paddingX = 1, paddingY = 1) {
26
+ this.#format = format;
27
+ this.#paddingX = paddingX;
28
+ this.#inner = new Text("", paddingX, paddingY);
29
+ }
30
+
31
+ setCustomBgFn(customBgFn?: (text: string) => string): void {
32
+ this.#inner.setCustomBgFn(customBgFn);
33
+ }
34
+
35
+ setIgnoreTight(ignore: boolean): this {
36
+ this.#ignoreTight = ignore;
37
+ this.#inner.setIgnoreTight(ignore);
38
+ this.invalidate();
39
+ return this;
40
+ }
41
+
42
+ invalidate(): void {
43
+ this.#cachedContentWidth = -1;
44
+ this.#cachedText = undefined;
45
+ this.#inner.invalidate();
46
+ }
47
+
48
+ render(width: number): readonly string[] {
49
+ const paddingX = this.#ignoreTight ? this.#paddingX : getPaddingX(this.#paddingX);
50
+ const contentWidth = Math.max(1, width - paddingX * 2);
51
+ if (this.#cachedText === undefined || contentWidth !== this.#cachedContentWidth) {
52
+ this.#cachedContentWidth = contentWidth;
53
+ this.#cachedText = this.#format(contentWidth);
54
+ this.#inner.setText(this.#cachedText);
55
+ }
56
+ return this.#inner.render(width);
57
+ }
58
+ }
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Vision fallback for text-only models. When a user attaches an image to a model
3
+ * that cannot accept image input, this:
4
+ * 1. saves each image under the session `local://` root (for later analysis), and
5
+ * 2. asks a vision-capable model to describe it and injects that description as
6
+ * a text block in place of the image:
7
+ *
8
+ * <image path="local://image-<hash>.png">
9
+ * <description>
10
+ * </image>
11
+ *
12
+ * Without this the provider layer drops the image entirely (NON_VISION_IMAGE_PLACEHOLDER).
13
+ */
14
+ import * as path from "node:path";
15
+ import {
16
+ type AgentTelemetry,
17
+ type AgentTelemetryConfig,
18
+ instrumentedCompleteSimple,
19
+ resolveTelemetry,
20
+ } from "@oh-my-pi/pi-agent-core";
21
+ import type { Api, completeSimple, ImageContent, Model, TextContent } from "@oh-my-pi/pi-ai";
22
+ import { logger, prompt, toError } from "@oh-my-pi/pi-utils";
23
+ import { extractTextContent } from "../commit/utils";
24
+ import type { ModelRegistry } from "../config/model-registry";
25
+ import { expandRoleAlias, getModelMatchPreferences, resolveModelFromString } from "../config/model-resolver";
26
+ import type { Settings } from "../config/settings";
27
+ import { type LocalProtocolOptions, resolveLocalRoot } from "../internal-urls";
28
+ import describeUserPrompt from "../prompts/tools/image-attachment-describe.md" with { type: "text" };
29
+ import describeSystemPrompt from "../prompts/tools/image-attachment-describe-system.md" with { type: "text" };
30
+
31
+ /** Telemetry tag for the oneshot vision-description calls. */
32
+ const ONESHOT_KIND = "image_attachment_describe";
33
+
34
+ const NO_VISION_MODEL_NOTE =
35
+ "[No vision-capable model is configured, so this image could not be described automatically. " +
36
+ "The image was saved; configure a vision model role (modelRoles.vision) and use the inspect_image tool to analyze it.]";
37
+
38
+ const DESCRIPTION_UNAVAILABLE_NOTE =
39
+ "[Image description unavailable: the vision model returned no usable text. The image was saved for further analysis.]";
40
+
41
+ /** Registry surface needed to resolve a vision model and authorize requests. */
42
+ export type VisionFallbackRegistry = Pick<ModelRegistry, "getAvailable" | "getApiKey" | "resolver"> &
43
+ Partial<Pick<ModelRegistry, "resolveCanonicalModel" | "getCanonicalVariants" | "getCanonicalId">>;
44
+
45
+ export interface DescribeAttachedImagesDeps {
46
+ /** Active (text-only) model the prompt is destined for. */
47
+ activeModel: Model<Api>;
48
+ modelRegistry: VisionFallbackRegistry;
49
+ settings: Settings;
50
+ /** Inputs for resolving the session-scoped `local://` root. */
51
+ localProtocolOptions: LocalProtocolOptions;
52
+ /** `provider/id` of the active model; a last-resort vision-model candidate (filtered to image-capable). */
53
+ activeModelString?: string;
54
+ telemetryConfig?: AgentTelemetryConfig;
55
+ sessionId?: string;
56
+ /** Test seam: overrides the underlying completeSimple call. */
57
+ completeImpl?: typeof completeSimple;
58
+ }
59
+
60
+ /** Map an image MIME type to a file extension for the saved artifact. */
61
+ function extensionForMime(mimeType: string): string {
62
+ const subtype = mimeType.split("/")[1]?.toLowerCase() ?? "";
63
+ switch (subtype) {
64
+ case "jpeg":
65
+ case "jpg":
66
+ return "jpg";
67
+ case "png":
68
+ return "png";
69
+ case "gif":
70
+ return "gif";
71
+ case "webp":
72
+ return "webp";
73
+ default: {
74
+ const sanitized = subtype.replace(/[^a-z0-9]/g, "");
75
+ return sanitized || "png";
76
+ }
77
+ }
78
+ }
79
+
80
+ /** Content-addressed file name so re-pasting the same image reuses one artifact. */
81
+ function imageFileName(image: ImageContent): string {
82
+ const hash = Bun.hash(image.data).toString(16);
83
+ return `image-${hash}.${extensionForMime(image.mimeType)}`;
84
+ }
85
+
86
+ /** Persist an image under the local root; returns its `local://` URL. */
87
+ async function saveImage(image: ImageContent, localRoot: string): Promise<string> {
88
+ const fileName = imageFileName(image);
89
+ const filePath = path.join(localRoot, fileName);
90
+ // Content-addressed: identical bytes overwrite themselves harmlessly. Bun.write creates parent dirs.
91
+ await Bun.write(filePath, Buffer.from(image.data, "base64"));
92
+ return `local://${fileName}`;
93
+ }
94
+
95
+ function formatImageBlock(localUrl: string, description: string): string {
96
+ return `<image path="${localUrl}">\n${description}\n</image>`;
97
+ }
98
+
99
+ /**
100
+ * Resolve a vision-capable model, mirroring the inspect_image priority
101
+ * (`pi/vision` → `pi/default` → active → first image-capable available), but
102
+ * never returning a text-only model.
103
+ */
104
+ function resolveVisionModel(deps: DescribeAttachedImagesDeps): Model<Api> | undefined {
105
+ const available = deps.modelRegistry.getAvailable();
106
+ if (available.length === 0) return undefined;
107
+ const preferences = getModelMatchPreferences(deps.settings);
108
+ const resolvePattern = (pattern: string | undefined): Model<Api> | undefined => {
109
+ if (!pattern) return undefined;
110
+ const expanded = expandRoleAlias(pattern, deps.settings);
111
+ const model = resolveModelFromString(expanded, available, preferences, deps.modelRegistry);
112
+ return model?.input.includes("image") ? model : undefined;
113
+ };
114
+ return (
115
+ resolvePattern("pi/vision") ??
116
+ resolvePattern("pi/default") ??
117
+ resolvePattern(deps.activeModelString) ??
118
+ available.find(model => model.input.includes("image"))
119
+ );
120
+ }
121
+
122
+ /** Run one vision-description round-trip; returns trimmed text or `null` on any failure. */
123
+ async function describeImage(
124
+ image: ImageContent,
125
+ visionModel: Model<Api>,
126
+ deps: DescribeAttachedImagesDeps,
127
+ telemetry: AgentTelemetry | undefined,
128
+ signal: AbortSignal | undefined,
129
+ ): Promise<string | null> {
130
+ try {
131
+ const response = await instrumentedCompleteSimple(
132
+ visionModel,
133
+ {
134
+ systemPrompt: [prompt.render(describeSystemPrompt)],
135
+ messages: [
136
+ {
137
+ role: "user",
138
+ content: [
139
+ { type: "image", data: image.data, mimeType: image.mimeType },
140
+ { type: "text", text: prompt.render(describeUserPrompt) },
141
+ ],
142
+ timestamp: Date.now(),
143
+ },
144
+ ],
145
+ },
146
+ { apiKey: deps.modelRegistry.resolver(visionModel, deps.sessionId), signal },
147
+ { telemetry, oneshotKind: ONESHOT_KIND, completeImpl: deps.completeImpl },
148
+ );
149
+ if (response.stopReason === "error" || response.stopReason === "aborted") {
150
+ logger.warn("image attachment description did not complete", {
151
+ stopReason: response.stopReason,
152
+ model: `${visionModel.provider}/${visionModel.id}`,
153
+ });
154
+ return null;
155
+ }
156
+ const text = extractTextContent(response).trim();
157
+ return text.length > 0 ? text : null;
158
+ } catch (err) {
159
+ logger.warn("image attachment description failed", {
160
+ error: toError(err).message,
161
+ model: `${visionModel.provider}/${visionModel.id}`,
162
+ });
163
+ return null;
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Save each attached image under `local://` and replace it with a descriptive
169
+ * text block. Returns one {@link TextContent} per input image, in order. Never
170
+ * throws for an individual image: a failed description falls back to a note while
171
+ * the saved-path block is still emitted.
172
+ */
173
+ export async function describeAttachedImagesForTextModel(
174
+ images: readonly ImageContent[],
175
+ deps: DescribeAttachedImagesDeps,
176
+ signal?: AbortSignal,
177
+ ): Promise<TextContent[]> {
178
+ const localRoot = resolveLocalRoot(deps.localProtocolOptions);
179
+ const visionModel = resolveVisionModel(deps);
180
+ const apiKey = visionModel ? await deps.modelRegistry.getApiKey(visionModel, deps.sessionId) : undefined;
181
+ const canDescribe = Boolean(visionModel && apiKey);
182
+ const telemetry = resolveTelemetry(deps.telemetryConfig, deps.sessionId);
183
+
184
+ return Promise.all(
185
+ images.map(async (image): Promise<TextContent> => {
186
+ const localUrl = await saveImage(image, localRoot);
187
+ let description: string;
188
+ if (canDescribe && visionModel) {
189
+ description =
190
+ (await describeImage(image, visionModel, deps, telemetry, signal)) ?? DESCRIPTION_UNAVAILABLE_NOTE;
191
+ } else {
192
+ description = NO_VISION_MODEL_NOTE;
193
+ }
194
+ return { type: "text", text: formatImageBlock(localUrl, description) };
195
+ }),
196
+ );
197
+ }
@@ -8,6 +8,16 @@ export interface MarkitConversionResult {
8
8
  error?: string;
9
9
  }
10
10
 
11
+ export interface MarkitFileConversionOptions {
12
+ /**
13
+ * Directory the PDF converter writes extracted images/diagrams into. When
14
+ * set, each embedded image is rendered to `<id>.png` and referenced by path
15
+ * in the markdown; when unset, markit emits an `<!-- image: <id> ... -->`
16
+ * placeholder comment instead.
17
+ */
18
+ imageDir?: string;
19
+ }
20
+
11
21
  interface MuPdfWasmModuleConfig {
12
22
  print?: (...values: unknown[]) => void;
13
23
  printErr?: (...values: unknown[]) => void;
@@ -77,9 +87,14 @@ function finalizeConversion(markdown?: string): MarkitConversionResult {
77
87
  return { content: "", ok: false, error: "Conversion produced no output" };
78
88
  }
79
89
 
80
- export async function convertFileWithMarkit(filePath: string, signal?: AbortSignal): Promise<MarkitConversionResult> {
90
+ export async function convertFileWithMarkit(
91
+ filePath: string,
92
+ signal?: AbortSignal,
93
+ options?: MarkitFileConversionOptions,
94
+ ): Promise<MarkitConversionResult> {
95
+ const extra = options?.imageDir ? { imageDir: options.imageDir } : undefined;
81
96
  try {
82
- const result = await runMarkitConversion(markit => markit.convertFile(filePath), signal);
97
+ const result = await runMarkitConversion(markit => markit.convertFile(filePath, extra), signal);
83
98
  return finalizeConversion(result.markdown);
84
99
  } catch (error) {
85
100
  if (error instanceof ToolAbortError) {
@@ -7,7 +7,8 @@
7
7
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
8
8
  import type { AuthStorage } from "@oh-my-pi/pi-ai";
9
9
  import { prompt } from "@oh-my-pi/pi-utils";
10
- import { z } from "zod/v4";
10
+ import { type } from "arktype";
11
+ import { settings } from "../../config/settings";
11
12
  import type { CustomTool, CustomToolContext, RenderResultOptions } from "../../extensibility/custom-tools/types";
12
13
  import type { Theme } from "../../modes/theme/theme";
13
14
  import webSearchSystemPrompt from "../../prompts/system/web-search.md" with { type: "text" };
@@ -22,16 +23,16 @@ import type { SearchProviderId, SearchResponse } from "./types";
22
23
  import { SearchProviderError } from "./types";
23
24
 
24
25
  /** Web search tool parameters schema */
25
- export const webSearchSchema = z.object({
26
- query: z.string().describe("search query"),
27
- recency: z.enum(["day", "week", "month", "year"]).describe("recency filter").optional(),
28
- limit: z.number().describe("max results").optional(),
29
- max_tokens: z.number().describe("max output tokens").optional(),
30
- temperature: z.number().describe("sampling temperature").optional(),
31
- num_search_results: z.number().describe("number of search results").optional(),
26
+ export const webSearchSchema = type({
27
+ query: "string",
28
+ recency: "'day' | 'week' | 'month' | 'year'?",
29
+ limit: "number?",
30
+ max_tokens: "number?",
31
+ temperature: "number?",
32
+ num_search_results: "number?",
32
33
  });
33
34
 
34
- export type SearchToolParams = z.infer<typeof webSearchSchema>;
35
+ export type SearchToolParams = typeof webSearchSchema.infer;
35
36
 
36
37
  export interface SearchQueryParams extends SearchToolParams {
37
38
  provider?: SearchProviderId | "auto";
@@ -153,6 +154,16 @@ async function executeSearch(
153
154
  };
154
155
  }
155
156
 
157
+ // Invariant across providers; read once and tolerate an uninitialized
158
+ // Settings singleton (e.g. `omp q ...` CLI path, unit tests) so the
159
+ // provider-fallback loop never aborts before any provider runs.
160
+ let antigravityEndpointMode: "auto" | "production" | "sandbox" | undefined;
161
+ try {
162
+ antigravityEndpointMode = settings.get("providers.antigravityEndpoint");
163
+ } catch {
164
+ antigravityEndpointMode = undefined;
165
+ }
166
+
156
167
  const failures: Array<{ provider: SearchProvider; error: unknown }> = [];
157
168
  let lastProvider = providers[0];
158
169
  for (const provider of providers) {
@@ -169,6 +180,7 @@ async function executeSearch(
169
180
  signal,
170
181
  authStorage,
171
182
  sessionId,
183
+ antigravityEndpointMode,
172
184
  });
173
185
 
174
186
  if (!hasRenderableSearchContent(response)) {
@@ -51,6 +51,7 @@ export interface SearchParams {
51
51
  * caller's agent session when available; otherwise omit.
52
52
  */
53
53
  sessionId?: string;
54
+ antigravityEndpointMode?: "auto" | "production" | "sandbox";
54
55
  }
55
56
 
56
57
  /** Base class for web search providers. */
@@ -52,6 +52,7 @@ export interface GeminiSearchParams extends GeminiToolParams {
52
52
  authStorage: AuthStorage;
53
53
  sessionId?: string;
54
54
  fetch?: FetchImpl;
55
+ antigravityEndpointMode?: "auto" | "production" | "sandbox";
55
56
  }
56
57
 
57
58
  export function buildGeminiRequestTools(params: GeminiToolParams): Array<Record<string, Record<string, unknown>>> {
@@ -163,6 +164,7 @@ async function callGeminiSearch(
163
164
  toolParams: GeminiToolParams,
164
165
  fetchImpl: FetchImpl | undefined,
165
166
  signal: AbortSignal | undefined,
167
+ mode?: "auto" | "production" | "sandbox",
166
168
  ): Promise<{
167
169
  answer: string;
168
170
  sources: SearchSource[];
@@ -171,7 +173,19 @@ async function callGeminiSearch(
171
173
  model: string;
172
174
  usage?: { inputTokens: number; outputTokens: number; totalTokens: number };
173
175
  }> {
174
- const endpoints = auth.isAntigravity ? ANTIGRAVITY_ENDPOINT_FALLBACKS : [DEFAULT_ENDPOINT];
176
+ let endpoints: string[];
177
+ if (auth.isAntigravity) {
178
+ const m = mode ?? "auto";
179
+ if (m === "sandbox") {
180
+ endpoints = [ANTIGRAVITY_SANDBOX_ENDPOINT];
181
+ } else if (m === "production") {
182
+ endpoints = [ANTIGRAVITY_DAILY_ENDPOINT];
183
+ } else {
184
+ endpoints = [...ANTIGRAVITY_ENDPOINT_FALLBACKS];
185
+ }
186
+ } else {
187
+ endpoints = [DEFAULT_ENDPOINT];
188
+ }
175
189
  const headers = auth.isAntigravity ? { "User-Agent": getAntigravityUserAgent() } : getGeminiCliHeaders();
176
190
 
177
191
  const requestMetadata = auth.isAntigravity
@@ -187,12 +201,7 @@ async function callGeminiSearch(
187
201
 
188
202
  const normalizedSystemPrompt = systemPrompt?.toWellFormed();
189
203
  const systemInstructionParts: Array<{ text: string }> = [
190
- ...(auth.isAntigravity
191
- ? [
192
- { text: ANTIGRAVITY_SYSTEM_INSTRUCTION },
193
- { text: `Please ignore following [ignore]${ANTIGRAVITY_SYSTEM_INSTRUCTION}[/ignore]` },
194
- ]
195
- : []),
204
+ ...(auth.isAntigravity ? [{ text: ANTIGRAVITY_SYSTEM_INSTRUCTION }] : []),
196
205
  ...(normalizedSystemPrompt ? [{ text: normalizedSystemPrompt }] : []),
197
206
  ];
198
207
 
@@ -238,16 +247,45 @@ async function callGeminiSearch(
238
247
  body: JSON.stringify(requestBody),
239
248
  signal: withHardTimeout(signal),
240
249
  });
241
- const urlFor = (attempt: number) =>
242
- `${endpoints[Math.min(attempt, endpoints.length - 1)]}/v1internal:streamGenerateContent?alt=sse`;
243
-
244
- const response = await fetchWithRetry(urlFor, {
245
- ...buildInit(),
246
- fetch: fetchImpl,
247
- maxAttempts: MAX_RETRIES + 1,
248
- defaultDelayMs: attempt => BASE_DELAY_MS * 2 ** attempt,
249
- maxDelayMs: RATE_LIMIT_BUDGET_MS,
250
- });
250
+
251
+ let response: Response | undefined;
252
+
253
+ for (let i = 0; i < endpoints.length; i++) {
254
+ const endpoint = endpoints[i];
255
+ const isLastEndpoint = i === endpoints.length - 1;
256
+ try {
257
+ response = await fetchWithRetry(() => `${endpoint}/v1internal:streamGenerateContent?alt=sse`, {
258
+ ...buildInit(),
259
+ fetch: fetchImpl,
260
+ maxAttempts: isLastEndpoint ? MAX_RETRIES + 1 : 1,
261
+ defaultDelayMs: attempt => BASE_DELAY_MS * 2 ** attempt,
262
+ maxDelayMs: RATE_LIMIT_BUDGET_MS,
263
+ });
264
+
265
+ if (response.ok) {
266
+ break;
267
+ }
268
+
269
+ if (response.status === 429 || (response.status >= 500 && response.status < 600)) {
270
+ if (!isLastEndpoint) {
271
+ continue;
272
+ }
273
+ }
274
+ break;
275
+ } catch (error) {
276
+ if (isLastEndpoint) {
277
+ throw error;
278
+ }
279
+ }
280
+ }
281
+
282
+ if (!response?.ok) {
283
+ const errorText = response ? await response.text() : "Network error";
284
+ const status = response?.status ?? 502;
285
+ const classified = classifyProviderHttpError("gemini", status, errorText);
286
+ if (classified) throw classified;
287
+ throw new SearchProviderError("gemini", `Gemini Cloud Code API error (${status}): ${errorText}`, status);
288
+ }
251
289
 
252
290
  if (!response.ok) {
253
291
  const errorText = await response.text();
@@ -410,7 +448,6 @@ export async function searchGemini(params: GeminiSearchParams): Promise<SearchRe
410
448
  // re-resolved access may omit projectId, in which case the seed's
411
449
  // project is still the right tenant for the credential. The
412
450
  // `fetchWithRetry` transport backoff stays INSIDE this attempt — auth
413
- // retry wraps transport retry.
414
451
  callGeminiSearch(
415
452
  {
416
453
  accessToken: access.accessToken,
@@ -428,6 +465,7 @@ export async function searchGemini(params: GeminiSearchParams): Promise<SearchRe
428
465
  },
429
466
  params.fetch,
430
467
  params.signal,
468
+ params.antigravityEndpointMode,
431
469
  ),
432
470
  { sessionId: params.sessionId, signal: params.signal, seed: seed.access },
433
471
  );