@oh-my-pi/pi-coding-agent 15.9.67 → 15.10.1

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 (266) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/dist/types/cli/args.d.ts +1 -1
  3. package/dist/types/cli/dry-balance-cli.d.ts +15 -1
  4. package/dist/types/cli/gallery-cli.d.ts +43 -0
  5. package/dist/types/cli/gallery-fixtures/agentic.d.ts +2 -0
  6. package/dist/types/cli/gallery-fixtures/codeintel.d.ts +3 -0
  7. package/dist/types/cli/gallery-fixtures/edit.d.ts +3 -0
  8. package/dist/types/cli/gallery-fixtures/fs.d.ts +2 -0
  9. package/dist/types/cli/gallery-fixtures/index.d.ts +4 -0
  10. package/dist/types/cli/gallery-fixtures/interaction.d.ts +3 -0
  11. package/dist/types/cli/gallery-fixtures/memory.d.ts +2 -0
  12. package/dist/types/cli/gallery-fixtures/misc.d.ts +3 -0
  13. package/dist/types/cli/gallery-fixtures/search.d.ts +3 -0
  14. package/dist/types/cli/gallery-fixtures/shell.d.ts +3 -0
  15. package/dist/types/cli/gallery-fixtures/types.d.ts +44 -0
  16. package/dist/types/cli/gallery-fixtures/web.d.ts +2 -0
  17. package/dist/types/cli/gallery-screenshot.d.ts +35 -0
  18. package/dist/types/commands/gallery.d.ts +47 -0
  19. package/dist/types/commit/analysis/conventional.d.ts +2 -2
  20. package/dist/types/commit/analysis/summary.d.ts +2 -2
  21. package/dist/types/commit/changelog/generate.d.ts +2 -2
  22. package/dist/types/commit/changelog/index.d.ts +2 -2
  23. package/dist/types/commit/map-reduce/index.d.ts +3 -3
  24. package/dist/types/commit/map-reduce/map-phase.d.ts +2 -2
  25. package/dist/types/commit/map-reduce/reduce-phase.d.ts +2 -2
  26. package/dist/types/commit/model-selection.d.ts +10 -4
  27. package/dist/types/config/api-key-resolver.d.ts +34 -0
  28. package/dist/types/config/keybindings.d.ts +6 -1
  29. package/dist/types/config/model-id-affixes.d.ts +2 -0
  30. package/dist/types/config/model-registry.d.ts +25 -2
  31. package/dist/types/config/settings-schema.d.ts +41 -6
  32. package/dist/types/dap/config.d.ts +14 -1
  33. package/dist/types/dap/types.d.ts +10 -0
  34. package/dist/types/extensibility/plugins/marketplace-auto-update.d.ts +8 -0
  35. package/dist/types/lsp/types.d.ts +10 -0
  36. package/dist/types/lsp/utils.d.ts +3 -2
  37. package/dist/types/main.d.ts +3 -2
  38. package/dist/types/memory-backend/index.d.ts +2 -1
  39. package/dist/types/memory-backend/resolve.d.ts +1 -1
  40. package/dist/types/memory-backend/types.d.ts +1 -1
  41. package/dist/types/modes/components/chat-block.d.ts +64 -0
  42. package/dist/types/modes/components/custom-editor.d.ts +5 -1
  43. package/dist/types/modes/components/overlay-box.d.ts +17 -0
  44. package/dist/types/modes/components/plan-review-overlay.d.ts +59 -0
  45. package/dist/types/modes/components/plan-toc.d.ts +41 -0
  46. package/dist/types/modes/components/read-tool-group.d.ts +2 -0
  47. package/dist/types/modes/components/tool-execution.d.ts +18 -0
  48. package/dist/types/modes/components/transcript-container.d.ts +11 -0
  49. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  50. package/dist/types/modes/controllers/event-controller.d.ts +0 -1
  51. package/dist/types/modes/controllers/extension-ui-controller.d.ts +0 -1
  52. package/dist/types/modes/controllers/input-controller.d.ts +1 -1
  53. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  54. package/dist/types/modes/controllers/streaming-reveal.d.ts +22 -0
  55. package/dist/types/modes/controllers/tan-command-controller.d.ts +6 -0
  56. package/dist/types/modes/index.d.ts +5 -4
  57. package/dist/types/modes/interactive-mode.d.ts +16 -6
  58. package/dist/types/modes/setup-version.d.ts +11 -0
  59. package/dist/types/modes/setup-wizard/index.d.ts +2 -1
  60. package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +2 -1
  61. package/dist/types/modes/theme/theme.d.ts +1 -1
  62. package/dist/types/modes/types.d.ts +19 -6
  63. package/dist/types/modes/utils/copy-targets.d.ts +21 -1
  64. package/dist/types/plan-mode/approved-plan.d.ts +27 -8
  65. package/dist/types/plan-mode/plan-protection.d.ts +4 -4
  66. package/dist/types/sdk.d.ts +3 -1
  67. package/dist/types/session/agent-session.d.ts +21 -0
  68. package/dist/types/session/messages.d.ts +12 -0
  69. package/dist/types/session/session-manager.d.ts +3 -1
  70. package/dist/types/slash-commands/types.d.ts +4 -6
  71. package/dist/types/task/executor.d.ts +14 -0
  72. package/dist/types/task/index.d.ts +1 -0
  73. package/dist/types/task/render.d.ts +3 -2
  74. package/dist/types/telemetry-export.d.ts +1 -1
  75. package/dist/types/tools/archive-reader.d.ts +5 -0
  76. package/dist/types/tools/ast-edit.d.ts +3 -0
  77. package/dist/types/tools/ast-grep.d.ts +3 -0
  78. package/dist/types/tools/bash.d.ts +1 -0
  79. package/dist/types/tools/eval-render.d.ts +1 -8
  80. package/dist/types/tools/fetch.d.ts +15 -7
  81. package/dist/types/tools/find.d.ts +8 -4
  82. package/dist/types/tools/grouped-file-output.d.ts +95 -12
  83. package/dist/types/tools/memory-render.d.ts +4 -1
  84. package/dist/types/tools/plan-mode-guard.d.ts +8 -9
  85. package/dist/types/tools/render-utils.d.ts +13 -9
  86. package/dist/types/tools/renderers.d.ts +16 -2
  87. package/dist/types/tools/search.d.ts +5 -1
  88. package/dist/types/tools/sqlite-reader.d.ts +1 -0
  89. package/dist/types/tools/todo.d.ts +3 -2
  90. package/dist/types/tools/write.d.ts +5 -0
  91. package/dist/types/tui/output-block.d.ts +16 -4
  92. package/dist/types/tui/status-line.d.ts +3 -0
  93. package/dist/types/utils/enhanced-paste.d.ts +20 -0
  94. package/dist/types/web/scrapers/github.d.ts +22 -0
  95. package/dist/types/web/search/providers/kimi.d.ts +1 -1
  96. package/dist/types/web/search/providers/perplexity.d.ts +8 -1
  97. package/dist/types/web/search/types.d.ts +1 -1
  98. package/package.json +9 -9
  99. package/scripts/dev-launch +42 -0
  100. package/scripts/dev-launch-preload.ts +19 -0
  101. package/src/auto-thinking/classifier.ts +5 -1
  102. package/src/cli/args.ts +2 -2
  103. package/src/cli/dry-balance-cli.ts +52 -17
  104. package/src/cli/gallery-cli.ts +226 -0
  105. package/src/cli/gallery-fixtures/agentic.ts +292 -0
  106. package/src/cli/gallery-fixtures/codeintel.ts +188 -0
  107. package/src/cli/gallery-fixtures/edit.ts +194 -0
  108. package/src/cli/gallery-fixtures/fs.ts +153 -0
  109. package/src/cli/gallery-fixtures/index.ts +40 -0
  110. package/src/cli/gallery-fixtures/interaction.ts +49 -0
  111. package/src/cli/gallery-fixtures/memory.ts +81 -0
  112. package/src/cli/gallery-fixtures/misc.ts +250 -0
  113. package/src/cli/gallery-fixtures/search.ts +213 -0
  114. package/src/cli/gallery-fixtures/shell.ts +167 -0
  115. package/src/cli/gallery-fixtures/types.ts +41 -0
  116. package/src/cli/gallery-fixtures/web.ts +158 -0
  117. package/src/cli/gallery-screenshot.ts +279 -0
  118. package/src/cli-commands.ts +1 -0
  119. package/src/commands/gallery.ts +52 -0
  120. package/src/commands/launch.ts +1 -1
  121. package/src/commit/analysis/conventional.ts +2 -2
  122. package/src/commit/analysis/summary.ts +2 -2
  123. package/src/commit/changelog/generate.ts +2 -2
  124. package/src/commit/changelog/index.ts +2 -2
  125. package/src/commit/map-reduce/index.ts +3 -3
  126. package/src/commit/map-reduce/map-phase.ts +2 -2
  127. package/src/commit/map-reduce/reduce-phase.ts +2 -2
  128. package/src/commit/model-selection.ts +33 -9
  129. package/src/commit/pipeline.ts +4 -4
  130. package/src/config/api-key-resolver.ts +58 -0
  131. package/src/config/keybindings.ts +15 -6
  132. package/src/config/model-equivalence.ts +35 -12
  133. package/src/config/model-id-affixes.ts +39 -22
  134. package/src/config/model-registry.ts +41 -18
  135. package/src/config/settings-schema.ts +28 -5
  136. package/src/config/settings.ts +31 -2
  137. package/src/dap/client.ts +14 -16
  138. package/src/dap/config.ts +41 -2
  139. package/src/dap/defaults.json +1 -0
  140. package/src/dap/session.ts +1 -0
  141. package/src/dap/types.ts +10 -0
  142. package/src/debug/index.ts +40 -54
  143. package/src/edit/renderer.ts +111 -119
  144. package/src/eval/__tests__/agent-bridge.test.ts +75 -32
  145. package/src/eval/__tests__/llm-bridge.test.ts +90 -31
  146. package/src/eval/agent-bridge.ts +34 -7
  147. package/src/eval/llm-bridge.ts +8 -3
  148. package/src/extensibility/extensions/runner.ts +1 -0
  149. package/src/extensibility/plugins/doctor.ts +0 -1
  150. package/src/extensibility/plugins/marketplace-auto-update.ts +49 -0
  151. package/src/goals/tools/goal-tool.ts +37 -27
  152. package/src/internal-urls/docs-index.generated.ts +10 -10
  153. package/src/lsp/client.ts +104 -55
  154. package/src/lsp/types.ts +10 -0
  155. package/src/lsp/utils.ts +3 -2
  156. package/src/main.ts +53 -56
  157. package/src/memories/index.ts +12 -5
  158. package/src/memory-backend/index.ts +13 -1
  159. package/src/memory-backend/resolve.ts +3 -5
  160. package/src/memory-backend/types.ts +1 -1
  161. package/src/mnemopi/backend.ts +5 -1
  162. package/src/modes/acp/acp-agent.ts +33 -26
  163. package/src/modes/components/assistant-message.ts +2 -9
  164. package/src/modes/components/chat-block.ts +111 -0
  165. package/src/modes/components/copy-selector.ts +1 -44
  166. package/src/modes/components/custom-editor.ts +33 -1
  167. package/src/modes/components/custom-message.ts +1 -3
  168. package/src/modes/components/execution-shared.ts +1 -2
  169. package/src/modes/components/hook-message.ts +1 -3
  170. package/src/modes/components/overlay-box.ts +108 -0
  171. package/src/modes/components/plan-review-overlay.ts +799 -0
  172. package/src/modes/components/plan-toc.ts +138 -0
  173. package/src/modes/components/read-tool-group.ts +20 -4
  174. package/src/modes/components/skill-message.ts +0 -1
  175. package/src/modes/components/status-line.ts +3 -5
  176. package/src/modes/components/tips.txt +1 -0
  177. package/src/modes/components/todo-reminder.ts +0 -2
  178. package/src/modes/components/tool-execution.ts +115 -90
  179. package/src/modes/components/transcript-container.ts +84 -24
  180. package/src/modes/components/user-message.ts +1 -2
  181. package/src/modes/controllers/command-controller-shared.ts +7 -6
  182. package/src/modes/controllers/command-controller.ts +70 -57
  183. package/src/modes/controllers/event-controller.ts +41 -40
  184. package/src/modes/controllers/extension-ui-controller.ts +10 -73
  185. package/src/modes/controllers/input-controller.ts +135 -122
  186. package/src/modes/controllers/mcp-command-controller.ts +69 -60
  187. package/src/modes/controllers/selector-controller.ts +25 -27
  188. package/src/modes/controllers/streaming-reveal.ts +212 -0
  189. package/src/modes/controllers/tan-command-controller.ts +173 -0
  190. package/src/modes/index.ts +5 -4
  191. package/src/modes/interactive-mode.ts +171 -82
  192. package/src/modes/setup-version.ts +11 -0
  193. package/src/modes/setup-wizard/index.ts +3 -2
  194. package/src/modes/setup-wizard/scenes/web-search.ts +3 -2
  195. package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
  196. package/src/modes/theme/theme-schema.json +1 -1
  197. package/src/modes/theme/theme.ts +8 -4
  198. package/src/modes/types.ts +19 -8
  199. package/src/modes/utils/context-usage.ts +10 -6
  200. package/src/modes/utils/copy-targets.ts +133 -27
  201. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  202. package/src/modes/utils/ui-helpers.ts +44 -46
  203. package/src/plan-mode/approved-plan.ts +66 -43
  204. package/src/plan-mode/plan-protection.ts +4 -4
  205. package/src/prompts/system/background-tan-dispatch.md +8 -0
  206. package/src/prompts/system/plan-mode-active.md +67 -58
  207. package/src/prompts/system/plan-mode-approved.md +1 -1
  208. package/src/sdk.ts +32 -60
  209. package/src/session/agent-session.ts +89 -13
  210. package/src/session/messages.ts +26 -0
  211. package/src/session/session-manager.ts +13 -5
  212. package/src/slash-commands/builtin-registry.ts +37 -10
  213. package/src/slash-commands/helpers/usage-report.ts +2 -0
  214. package/src/slash-commands/types.ts +4 -6
  215. package/src/task/executor.ts +25 -4
  216. package/src/task/index.ts +4 -0
  217. package/src/task/render.ts +212 -148
  218. package/src/telemetry-export.ts +25 -7
  219. package/src/tools/archive-reader.ts +64 -0
  220. package/src/tools/ask.ts +119 -164
  221. package/src/tools/ast-edit.ts +98 -71
  222. package/src/tools/ast-grep.ts +37 -43
  223. package/src/tools/bash.ts +50 -6
  224. package/src/tools/debug.ts +20 -8
  225. package/src/tools/eval-backends.ts +6 -17
  226. package/src/tools/eval-render.ts +21 -18
  227. package/src/tools/eval.ts +5 -4
  228. package/src/tools/fetch.ts +391 -91
  229. package/src/tools/find.ts +44 -30
  230. package/src/tools/gh-renderer.ts +81 -42
  231. package/src/tools/grouped-file-output.ts +272 -48
  232. package/src/tools/image-gen.ts +150 -103
  233. package/src/tools/inspect-image-renderer.ts +63 -41
  234. package/src/tools/inspect-image.ts +8 -1
  235. package/src/tools/job.ts +3 -4
  236. package/src/tools/memory-render.ts +4 -1
  237. package/src/tools/plan-mode-guard.ts +21 -39
  238. package/src/tools/read.ts +23 -16
  239. package/src/tools/render-utils.ts +38 -40
  240. package/src/tools/renderers.ts +16 -1
  241. package/src/tools/report-tool-issue.ts +1 -1
  242. package/src/tools/resolve.ts +14 -0
  243. package/src/tools/search-tool-bm25.ts +36 -23
  244. package/src/tools/search.ts +189 -95
  245. package/src/tools/sqlite-reader.ts +9 -12
  246. package/src/tools/todo.ts +138 -59
  247. package/src/tools/write.ts +100 -60
  248. package/src/tui/output-block.ts +60 -13
  249. package/src/tui/status-line.ts +5 -1
  250. package/src/utils/commit-message-generator.ts +9 -1
  251. package/src/utils/enhanced-paste.ts +202 -0
  252. package/src/utils/title-generator.ts +2 -1
  253. package/src/web/scrapers/github.ts +255 -3
  254. package/src/web/scrapers/youtube.ts +3 -2
  255. package/src/web/search/providers/anthropic.ts +25 -19
  256. package/src/web/search/providers/exa.ts +11 -3
  257. package/src/web/search/providers/kimi.ts +28 -17
  258. package/src/web/search/providers/parallel.ts +35 -24
  259. package/src/web/search/providers/perplexity.ts +199 -51
  260. package/src/web/search/providers/synthetic.ts +8 -6
  261. package/src/web/search/providers/tavily.ts +9 -8
  262. package/src/web/search/providers/zai.ts +8 -6
  263. package/src/web/search/render.ts +39 -54
  264. package/src/web/search/types.ts +5 -1
  265. package/dist/types/eval/__tests__/shared-executors.test.d.ts +0 -1
  266. package/src/eval/__tests__/shared-executors.test.ts +0 -609
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Bun `--preload` shim for the omp dev launcher (`scripts/dev-launch`).
3
+ *
4
+ * The launcher starts Bun from an empty, bunfig-free directory so a foreign
5
+ * project's `bunfig.toml` `preload` cannot run inside the omp CLI: Bun reads
6
+ * `bunfig.toml` from the *current working directory* on startup and evaluates
7
+ * its `preload` entries before the entrypoint, so a bun-shebang bin inherits
8
+ * whatever `preload` the directory you launched from declares (and crashes if
9
+ * that preload can't resolve). This shim is loaded before the entrypoint's
10
+ * imports run, so it restores the user's real working directory in time for
11
+ * import-time snapshots (e.g. `getProjectDir()` in `@oh-my-pi/pi-utils/dirs`).
12
+ */
13
+ const launchCwd = process.env.OMP_LAUNCH_CWD;
14
+ if (launchCwd) {
15
+ delete process.env.OMP_LAUNCH_CWD;
16
+ try {
17
+ process.chdir(launchCwd);
18
+ } catch {}
19
+ }
@@ -15,6 +15,7 @@
15
15
  */
16
16
  import { type AssistantMessage, completeSimple, Effort, type Model } from "@oh-my-pi/pi-ai";
17
17
  import { prompt } from "@oh-my-pi/pi-utils";
18
+
18
19
  import type { ModelRegistry } from "../config/model-registry";
19
20
  import { resolveRoleSelection } from "../config/model-resolver";
20
21
  import type { Settings } from "../config/settings";
@@ -82,7 +83,10 @@ async function classifyOnline(input: string, deps: ClassifyDifficultyDeps): Prom
82
83
  messages: [{ role: "user", content: input, timestamp: Date.now() }],
83
84
  },
84
85
  {
85
- apiKey,
86
+ apiKey: deps.registry.resolver(model.provider, {
87
+ sessionId: deps.sessionId,
88
+ baseUrl: model.baseUrl,
89
+ }),
86
90
  maxTokens,
87
91
  disableReasoning: true,
88
92
  metadata,
package/src/cli/args.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * CLI argument parsing and help display
3
3
  */
4
- import { type Effort, THINKING_EFFORTS } from "@oh-my-pi/pi-ai";
4
+ import { type Effort, THINKING_EFFORTS } from "@oh-my-pi/pi-ai/effort";
5
5
  import { APP_NAME, CONFIG_DIR_NAME, logger } from "@oh-my-pi/pi-utils";
6
6
  import chalk from "chalk";
7
7
  import { parseEffort } from "../thinking";
@@ -284,7 +284,7 @@ export function getExtraHelpText(): string {
284
284
  ${chalk.dim("# Search & Tools")}
285
285
  EXA_API_KEY - Exa web search
286
286
  BRAVE_API_KEY - Brave web search
287
- PERPLEXITY_API_KEY - Perplexity web search (API)
287
+ PERPLEXITY_API_KEY - Perplexity web search API key (optional; anonymous fallback)
288
288
  PERPLEXITY_COOKIES - Perplexity web search (session cookie)
289
289
  TAVILY_API_KEY - Tavily web search
290
290
  ANTHROPIC_SEARCH_API_KEY - Anthropic web search (override; isolates search from main ANTHROPIC_API_KEY)
@@ -1,8 +1,10 @@
1
1
  import type {
2
2
  Api,
3
+ ApiKeyResolver,
3
4
  AssistantMessage,
4
5
  AssistantMessageEvent,
5
6
  AssistantMessageEventStream,
7
+ AuthCredentialSnapshotEntry,
6
8
  Context,
7
9
  Model,
8
10
  OAuthAccess,
@@ -59,6 +61,12 @@ export interface DryBalanceAuthStorage {
59
61
  options?: DryBalanceAuthOptions,
60
62
  ): Promise<OAuthAccess | undefined>;
61
63
  getOAuthAccesses?(provider: string, options?: DryBalanceAuthOptions): Promise<OAuthAccessResolution[]>;
64
+ /**
65
+ * Force-refresh a single credential by id (step (b) of the auth-retry
66
+ * policy). The bench re-mints the failing account's token in place on a
67
+ * 401 rather than rotating accounts — it is measuring each account.
68
+ */
69
+ forceRefreshCredentialById?(id: number, signal?: AbortSignal): Promise<AuthCredentialSnapshotEntry>;
62
70
  }
63
71
 
64
72
  export interface DryBalanceModelRegistry {
@@ -152,6 +160,8 @@ export interface DryBalanceDependencies {
152
160
  now?: () => number;
153
161
  stdoutIsTTY?: boolean;
154
162
  stderrIsTTY?: boolean;
163
+ stdoutColumns?: number;
164
+ stderrColumns?: number;
155
165
  }
156
166
 
157
167
  type DryBalanceAttemptResult =
@@ -181,6 +191,7 @@ type DryBalanceBenchTarget =
181
191
  ok: true;
182
192
  account: string;
183
193
  accessToken: string;
194
+ credentialId?: number;
184
195
  }
185
196
  | {
186
197
  ok: false;
@@ -310,10 +321,11 @@ function renderBenchStatusLine(
310
321
  }
311
322
  }
312
323
 
313
- function createBenchProgressSink(
324
+ export function createBenchProgressSink(
314
325
  total: number,
315
326
  write: (text: string) => void,
316
327
  interactive: boolean,
328
+ columns: number,
317
329
  ): DryBalanceBenchProgressSink {
318
330
  const statuses: DryBalanceBenchProgressStatus[] = Array.from({ length: total }, () => ({ state: "waiting" }));
319
331
  if (!interactive) {
@@ -333,13 +345,21 @@ function createBenchProgressSink(
333
345
  let frame = 0;
334
346
  let lineCount = 0;
335
347
  let timer: NodeJS.Timeout | undefined;
348
+ const width = Number.isFinite(columns) && columns > 0 ? Math.trunc(columns) : 80;
336
349
  const render = (): void => {
337
350
  const lines = [
338
351
  chalk.bold("bench requests"),
339
352
  ...statuses.map((status, index) => renderBenchStatusLine(status, index, total, frame)),
340
353
  ];
341
- if (lineCount > 0) write(`\x1b[${lineCount}A`);
342
- write(`${lines.map(line => `\x1b[2K${line}`).join("\n")}\n`);
354
+ // Anchor every redraw at column 0 and terminate each row with CRLF: a
355
+ // bare `\n` only returns to column 0 when the tty performs ONLCR
356
+ // translation, which is off whenever the terminal is in raw mode — there
357
+ // the old column-preserving cursor-up staircased each frame into
358
+ // scrollback. Cap each line to the terminal width so a wrapped row never
359
+ // desyncs the `\x1b[<n>A` cursor-up from the logical line count.
360
+ const move = lineCount > 0 ? `\x1b[${lineCount}A` : "";
361
+ const body = lines.map(line => `\x1b[2K${truncateToWidth(line, width)}`).join("\r\n");
362
+ write(`${move}\r${body}\r\n`);
343
363
  lineCount = lines.length;
344
364
  };
345
365
  render();
@@ -370,13 +390,23 @@ function createBenchProgressSink(
370
390
  async function runBenchRequest(
371
391
  model: Model<Api>,
372
392
  sessionId: string,
373
- account: string,
374
- accessToken: string,
393
+ target: Extract<DryBalanceBenchTarget, { ok: true }>,
394
+ authStorage: DryBalanceAuthStorage,
375
395
  streamFn: DryBalanceStreamSimple,
376
396
  now: () => number,
377
397
  ): Promise<DryBalanceBenchResult> {
398
+ const { account, accessToken, credentialId } = target;
378
399
  const startedAt = now();
379
400
  let firstTokenAt: number | undefined;
401
+ // Re-mint the cached token on a 401: a peer/broker may have rotated it out
402
+ // from under our snapshot (Anthropic rotates refresh tokens on every use).
403
+ // The bench measures one account, so the switch step intentionally declines.
404
+ const apiKey: ApiKeyResolver = async ({ lastChance, error }) => {
405
+ if (error === undefined) return accessToken;
406
+ if (lastChance || credentialId === undefined || !authStorage.forceRefreshCredentialById) return undefined;
407
+ const refreshed = await authStorage.forceRefreshCredentialById(credentialId);
408
+ return refreshed.credential.type === "oauth" ? refreshed.credential.access : undefined;
409
+ };
380
410
  try {
381
411
  const context: Context = {
382
412
  messages: [
@@ -389,7 +419,7 @@ async function runBenchRequest(
389
419
  ],
390
420
  };
391
421
  const stream = streamFn(model, context, {
392
- apiKey: accessToken,
422
+ apiKey,
393
423
  sessionId,
394
424
  maxTokens: resolveBenchMaxTokens(model),
395
425
  temperature: 0.2,
@@ -454,7 +484,7 @@ async function resolveBenchTargets(
454
484
  seen.add(key);
455
485
  const account = extractAccount(entry);
456
486
  if (entry.ok) {
457
- targets.push({ ok: true, account, accessToken: entry.accessToken });
487
+ targets.push({ ok: true, account, accessToken: entry.accessToken, credentialId: entry.credentialId });
458
488
  } else {
459
489
  targets.push({ ok: false, account, error: entry.error });
460
490
  }
@@ -465,6 +495,7 @@ async function resolveBenchTargets(
465
495
  async function runBenchTargets(
466
496
  model: Model<Api>,
467
497
  targets: DryBalanceBenchTarget[],
498
+ authStorage: DryBalanceAuthStorage,
468
499
  randomSessionId: () => string,
469
500
  progress: DryBalanceBenchProgressSink | undefined,
470
501
  streamFn: DryBalanceStreamSimple,
@@ -482,14 +513,7 @@ async function runBenchTargets(
482
513
  return result;
483
514
  }
484
515
  progress?.markRunning(index, target.account);
485
- const result = await runBenchRequest(
486
- model,
487
- randomSessionId(),
488
- target.account,
489
- target.accessToken,
490
- streamFn,
491
- now,
492
- );
516
+ const result = await runBenchRequest(model, randomSessionId(), target, authStorage, streamFn, now);
493
517
  progress?.complete(index, result);
494
518
  return result;
495
519
  }),
@@ -792,8 +816,19 @@ export async function runDryBalanceCommand(
792
816
  const progressInteractive = command.flags.json
793
817
  ? (deps.stderrIsTTY ?? process.stderr.isTTY === true)
794
818
  : (deps.stdoutIsTTY ?? process.stdout.isTTY === true);
795
- progress = createBenchProgressSink(targets.length, progressWrite, progressInteractive);
796
- benchResults = await runBenchTargets(model, targets, randomSessionId, progress, streamFn, now);
819
+ const progressColumns = command.flags.json
820
+ ? (deps.stderrColumns ?? process.stderr.columns ?? 80)
821
+ : (deps.stdoutColumns ?? process.stdout.columns ?? 80);
822
+ progress = createBenchProgressSink(targets.length, progressWrite, progressInteractive, progressColumns);
823
+ benchResults = await runBenchTargets(
824
+ model,
825
+ targets,
826
+ runtime.modelRegistry.authStorage,
827
+ randomSessionId,
828
+ progress,
829
+ streamFn,
830
+ now,
831
+ );
797
832
  results = targets.map(target =>
798
833
  target.ok ? { ok: true, account: target.account } : { ok: false, reason: target.error },
799
834
  );
@@ -0,0 +1,226 @@
1
+ /**
2
+ * `omp gallery` — render every built-in tool's renderer across its lifecycle.
3
+ *
4
+ * For each tool with a registered renderer, the gallery drives a real
5
+ * {@link ToolExecutionComponent} through four states — streaming arguments,
6
+ * arguments complete (in progress), success, and failure — and prints the
7
+ * rendered output to stdout. It exists for visual QA of tool renderers without
8
+ * having to provoke each state through a live agent session.
9
+ */
10
+ import type { AgentTool } from "@oh-my-pi/pi-agent-core";
11
+ import type { TUI } from "@oh-my-pi/pi-tui";
12
+ import { getProjectDir } from "@oh-my-pi/pi-utils";
13
+ import { Settings } from "../config/settings";
14
+ import { ToolExecutionComponent } from "../modes/components/tool-execution";
15
+ import { initTheme, theme } from "../modes/theme/theme";
16
+ import { toolRenderers } from "../tools/renderers";
17
+ import { type GalleryFixture, type GalleryResult, galleryFixtures } from "./gallery-fixtures";
18
+ import { captureGalleryScreenshots } from "./gallery-screenshot";
19
+
20
+ /** Lifecycle states the gallery renders, in display order. */
21
+ export const GALLERY_STATES = ["streaming", "progress", "success", "error"] as const;
22
+ export type GalleryState = (typeof GALLERY_STATES)[number];
23
+
24
+ const STATE_LABELS: Record<GalleryState, string> = {
25
+ streaming: "streaming args",
26
+ progress: "in progress",
27
+ success: "done",
28
+ error: "failed",
29
+ };
30
+
31
+ export interface GalleryCommandArgs {
32
+ /** Render width in columns (defaults to terminal width, clamped). */
33
+ width?: number;
34
+ /** Restrict to a single tool name. */
35
+ tool?: string;
36
+ /** Restrict to specific lifecycle states. */
37
+ states?: GalleryState[];
38
+ /** Render the expanded variant of each renderer. */
39
+ expanded?: boolean;
40
+ /** Strip ANSI styling from the output (useful when redirecting to a file). */
41
+ plain?: boolean;
42
+ /** Capture the rendered gallery as PNG screenshot(s) via VHS instead of printing ANSI. */
43
+ screenshot?: boolean;
44
+ /** Screenshot output path (single image) or base path (suffixed when split across images). */
45
+ out?: string;
46
+ /** Font family for screenshots (must be installed; Nerd Font recommended for icon glyphs). */
47
+ font?: string;
48
+ /** Font size in points for screenshots. */
49
+ fontSize?: number;
50
+ }
51
+
52
+ /** One tool's rendered lifecycle, as ANSI lines: a leading blank, the section rule, then each state. */
53
+ export interface GallerySection {
54
+ heading: string;
55
+ lines: string[];
56
+ }
57
+
58
+ const GENERIC_ERROR: GalleryResult = {
59
+ content: [{ type: "text", text: "Error: operation failed" }],
60
+ isError: true,
61
+ };
62
+
63
+ /**
64
+ * Build the fake `AgentTool` the component needs for its label, edit mode, and —
65
+ * for `customRendered` fixtures — the renderer functions that route it through
66
+ * the same custom-tool branch production uses (see {@link GalleryFixture}).
67
+ */
68
+ function fakeToolFor(name: string, fixture: GalleryFixture | undefined): AgentTool | undefined {
69
+ if (!fixture?.label && !fixture?.editMode && !fixture?.customRendered) return undefined;
70
+ const tool: Record<string, unknown> = { name, label: fixture.label ?? name, mode: fixture.editMode };
71
+ if (fixture.customRendered) {
72
+ const renderer = toolRenderers[name] as
73
+ | { renderCall?: unknown; renderResult?: unknown; mergeCallAndResult?: unknown; inline?: unknown }
74
+ | undefined;
75
+ if (renderer) {
76
+ tool.renderCall = renderer.renderCall;
77
+ tool.renderResult = renderer.renderResult;
78
+ tool.mergeCallAndResult = renderer.mergeCallAndResult;
79
+ tool.inline = renderer.inline;
80
+ }
81
+ }
82
+ return tool as unknown as AgentTool;
83
+ }
84
+
85
+ /** The curated fixture for a tool, or a generic one for registry tools lacking sample data. */
86
+ export function resolveFixture(name: string): GalleryFixture {
87
+ return (
88
+ galleryFixtures[name] ??
89
+ ({
90
+ args: { note: `sample ${name} call` },
91
+ result: { content: [{ type: "text", text: `${name} completed` }] },
92
+ } satisfies GalleryFixture)
93
+ );
94
+ }
95
+
96
+ /**
97
+ * Render a single tool/state pair to lines. Builds a fresh component, drives it
98
+ * to the requested state, settles any async edit preview, then snapshots the
99
+ * render and stops all animation timers.
100
+ */
101
+ export async function renderGalleryState(
102
+ name: string,
103
+ fixture: GalleryFixture,
104
+ state: GalleryState,
105
+ width: number,
106
+ expanded = false,
107
+ ): Promise<string[]> {
108
+ const tool = fakeToolFor(name, fixture);
109
+ const streamingArgs = state === "streaming" ? (fixture.streamingArgs ?? fixture.args) : fixture.args;
110
+ // The component only calls `requestRender` during a static render;
111
+ // `imageBudget` is consulted solely when images render, which the gallery
112
+ // disables. A cast avoids constructing a real terminal.
113
+ const ui = { requestRender() {} } as unknown as TUI;
114
+ const component = new ToolExecutionComponent(name, streamingArgs, { showImages: false }, tool, ui, getProjectDir());
115
+ component.setExpanded(expanded);
116
+
117
+ if (state !== "streaming") {
118
+ component.setArgsComplete();
119
+ }
120
+ if (state === "success") {
121
+ component.updateResult(fixture.result, false);
122
+ } else if (state === "error") {
123
+ component.updateResult(fixture.errorResult ?? GENERIC_ERROR, false);
124
+ }
125
+
126
+ // Edit-like renderers compute their diff preview off the render path; wait
127
+ // for it to settle so the snapshot is deterministic instead of racing a tick.
128
+ await component.whenPreviewSettled();
129
+
130
+ const lines = component.render(width);
131
+ component.stopAnimation();
132
+ return lines;
133
+ }
134
+
135
+ function resolveWidth(requested: number | undefined): number {
136
+ const fallback = process.stdout.columns ?? 100;
137
+ const width = requested ?? fallback;
138
+ return Math.max(40, Math.min(200, width));
139
+ }
140
+
141
+ function sectionRule(label: string, width: number): string {
142
+ const prefix = `── ${label} `;
143
+ const fill = Math.max(0, width - prefix.length);
144
+ return theme.fg("accent", theme.bold(`${prefix}${"─".repeat(fill)}`));
145
+ }
146
+
147
+ /**
148
+ * Render each requested tool's lifecycle into ANSI section blocks. The block
149
+ * layout (leading blank, section rule, then a blank + dim label + body per
150
+ * state) is shared by the stdout and screenshot paths so both stay identical.
151
+ */
152
+ async function renderGallerySections(
153
+ names: string[],
154
+ states: GalleryState[],
155
+ width: number,
156
+ expanded: boolean,
157
+ ): Promise<GallerySection[]> {
158
+ const sections: GallerySection[] = [];
159
+ for (const name of names) {
160
+ const fixture = resolveFixture(name);
161
+ const heading = fixture.label && fixture.label !== name ? `${name} — ${fixture.label}` : name;
162
+ const lines: string[] = ["", sectionRule(heading, width)];
163
+ for (const state of states) {
164
+ lines.push("", theme.fg("dim", ` · ${STATE_LABELS[state]}`));
165
+ try {
166
+ for (const line of await renderGalleryState(name, fixture, state, width, expanded)) lines.push(line);
167
+ } catch (err) {
168
+ lines.push(theme.fg("error", ` render failed: ${String(err)}`));
169
+ }
170
+ }
171
+ sections.push({ heading, lines });
172
+ }
173
+ return sections;
174
+ }
175
+
176
+ /**
177
+ * Render the gallery. Iterates the renderer registry (or a single tool),
178
+ * printing each requested lifecycle state under a labeled section — or, with
179
+ * `screenshot`, capturing the rendered output as PNG(s) via VHS.
180
+ */
181
+ export async function runGalleryCommand(args: GalleryCommandArgs): Promise<void> {
182
+ const settingsInstance = await Settings.init();
183
+ // Screenshots must carry exact theme RGB regardless of how the invoking
184
+ // terminal advertises its color support, so force truecolor before the theme
185
+ // (and therefore every SGR escape it emits) is built.
186
+ if (args.screenshot) process.env.COLORTERM = "truecolor";
187
+ await initTheme(
188
+ false,
189
+ settingsInstance.get("symbolPreset"),
190
+ settingsInstance.get("colorBlindMode"),
191
+ settingsInstance.get("theme.dark"),
192
+ settingsInstance.get("theme.light"),
193
+ );
194
+
195
+ const width = resolveWidth(args.width);
196
+ const expanded = args.expanded ?? false;
197
+ const states = args.states && args.states.length > 0 ? args.states : [...GALLERY_STATES];
198
+
199
+ // Renderer-registry tools plus fixture-only tools (no dedicated renderer,
200
+ // e.g. `report_tool_issue` / custom extension tools) so the gallery covers
201
+ // the generic fallback + custom-tool branches too.
202
+ const allNames = Array.from(new Set([...Object.keys(toolRenderers), ...Object.keys(galleryFixtures)])).sort();
203
+ const names = args.tool ? allNames.filter(name => name === args.tool) : allNames;
204
+ if (args.tool && names.length === 0) {
205
+ process.stdout.write(`Unknown tool '${args.tool}'. Known tools: ${allNames.join(", ")}\n`);
206
+ return;
207
+ }
208
+
209
+ const sections = await renderGallerySections(names, states, width, expanded);
210
+
211
+ if (args.screenshot) {
212
+ const paths = await captureGalleryScreenshots(sections, {
213
+ width,
214
+ font: args.font,
215
+ fontSize: args.fontSize,
216
+ out: args.out,
217
+ });
218
+ process.stdout.write(`${paths.join("\n")}\n`);
219
+ return;
220
+ }
221
+
222
+ const lines = sections.flatMap(section => section.lines);
223
+ lines.push("");
224
+ const text = lines.map(line => (args.plain ? Bun.stripANSI(line) : line)).join("\n");
225
+ process.stdout.write(`${text}\n`);
226
+ }