@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,279 @@
1
+ /**
2
+ * Render `omp gallery` output to PNG screenshots via VHS.
3
+ *
4
+ * ANSI escapes are invisible to anything that can only read raw bytes (e.g.
5
+ * agents), so `--screenshot` drives the rendered gallery through a real virtual
6
+ * terminal (VHS + ttyd + ffmpeg) and writes the captured frame to disk. The
7
+ * gallery is pre-rendered to truecolor ANSI in this process — where the user's
8
+ * theme and symbol preset are correct — then `cat`'d inside VHS so the captured
9
+ * pixels match exactly what the live TUI would draw.
10
+ *
11
+ * VHS is a hard dependency of this path: if it is not installed we fail loudly
12
+ * rather than degrade to a lossy fallback.
13
+ */
14
+ import * as fs from "node:fs";
15
+ import * as os from "node:os";
16
+ import * as path from "node:path";
17
+ import { $which } from "@oh-my-pi/pi-utils";
18
+ import { theme } from "../modes/theme/theme";
19
+ import type { GallerySection } from "./gallery-cli";
20
+
21
+ /** Nerd Font family so the gallery's icon glyphs (PUA) render instead of tofu. */
22
+ export const DEFAULT_SCREENSHOT_FONT = "JetBrainsMono Nerd Font";
23
+ export const DEFAULT_SCREENSHOT_FONT_SIZE = 18;
24
+
25
+ /** Inner padding (px) VHS leaves around the terminal grid. */
26
+ const PADDING = 14;
27
+ const LINE_HEIGHT = 1.0;
28
+ /**
29
+ * Upper-bound cell metrics relative to font size. Real monospace cells are
30
+ * smaller, so over-provisioning the canvas guarantees the gallery never
31
+ * soft-wraps (too few columns) or scrolls off the top (too few rows). The slack
32
+ * shows up only as a modest background margin, which is harmless for review.
33
+ */
34
+ const CELL_WIDTH_RATIO = 0.65;
35
+ const CELL_HEIGHT_RATIO = 1.5;
36
+ /** Keep each image well under headless-Chromium's tall-canvas limits. */
37
+ const MAX_IMAGE_HEIGHT_PX = 8000;
38
+
39
+ export interface GalleryScreenshotOptions {
40
+ /** Gallery render width in columns (matches the ANSI line width). */
41
+ width: number;
42
+ /** VHS `FontFamily`. */
43
+ font?: string;
44
+ /** VHS `FontSize`. */
45
+ fontSize?: number;
46
+ /**
47
+ * Output destination. When omitted, PNGs land in a fresh temp directory.
48
+ * With multiple images the path is suffixed (`name-01.png`, `name-02.png`).
49
+ */
50
+ out?: string;
51
+ }
52
+
53
+ /**
54
+ * Capture the gallery sections as one or more PNGs and return their absolute
55
+ * paths. Tall galleries are split across images so no single capture exceeds
56
+ * the terminal-canvas height limit.
57
+ */
58
+ export async function captureGalleryScreenshots(
59
+ sections: GallerySection[],
60
+ options: GalleryScreenshotOptions,
61
+ ): Promise<string[]> {
62
+ const vhs = $which("vhs");
63
+ if (!vhs) {
64
+ throw new Error(
65
+ "`omp gallery --screenshot` requires VHS, which is not installed. " +
66
+ "Install it (e.g. `brew install vhs`, or see https://github.com/charmbracelet/vhs) and retry.",
67
+ );
68
+ }
69
+
70
+ const font = options.font ?? DEFAULT_SCREENSHOT_FONT;
71
+ const fontSize = options.fontSize ?? DEFAULT_SCREENSHOT_FONT_SIZE;
72
+ const cellHeight = fontSize * Math.max(LINE_HEIGHT, 1) * CELL_HEIGHT_RATIO;
73
+ const cellWidth = fontSize * CELL_WIDTH_RATIO;
74
+ const rowBudget = Math.max(40, Math.floor((MAX_IMAGE_HEIGHT_PX - 2 * PADDING) / cellHeight) - 2);
75
+ const chunks = chunkGallerySections(sections, rowBudget);
76
+ const themeJson = buildVhsTheme();
77
+
78
+ const baseDir = options.out
79
+ ? path.dirname(path.resolve(options.out))
80
+ : fs.mkdtempSync(path.join(os.tmpdir(), "omp-gallery-"));
81
+ await fs.promises.mkdir(baseDir, { recursive: true });
82
+
83
+ const outPaths: string[] = [];
84
+ for (let i = 0; i < chunks.length; i++) {
85
+ if (chunks.length > 1) {
86
+ process.stderr.write(`Rendering gallery screenshot ${i + 1}/${chunks.length}…\n`);
87
+ }
88
+ const outPng = resolveScreenshotOutputPath(options.out, baseDir, i, chunks.length);
89
+ const lines = chunks[i].flatMap(section => section.lines);
90
+ await renderChunk({ vhs, lines, outPng, font, fontSize, cellWidth, cellHeight, width: options.width, themeJson });
91
+ outPaths.push(outPng);
92
+ }
93
+ return outPaths;
94
+ }
95
+
96
+ interface RenderChunkArgs {
97
+ vhs: string;
98
+ lines: string[];
99
+ outPng: string;
100
+ font: string;
101
+ fontSize: number;
102
+ cellWidth: number;
103
+ cellHeight: number;
104
+ width: number;
105
+ themeJson: string;
106
+ }
107
+
108
+ async function renderChunk(args: RenderChunkArgs): Promise<void> {
109
+ const rows = args.lines.length;
110
+ const widthPx = Math.ceil(args.width * args.cellWidth) + 2 * PADDING;
111
+ const heightPx = Math.ceil((rows + 2) * args.cellHeight) + 2 * PADDING;
112
+
113
+ const dir = path.dirname(args.outPng);
114
+ const stem = path.basename(args.outPng, path.extname(args.outPng));
115
+ const ansiPath = path.join(dir, `.${stem}.ansi`);
116
+ const tapePath = path.join(dir, `.${stem}.tape`);
117
+ const gifPath = path.join(dir, `.${stem}.gif`);
118
+
119
+ // CRLF so each gallery line is its own terminal row regardless of how the
120
+ // captured shell handles bare LF.
121
+ await Bun.write(ansiPath, `${args.lines.join("\r\n")}\r\n`);
122
+ await Bun.write(
123
+ tapePath,
124
+ buildTape({
125
+ gifPath,
126
+ outPng: args.outPng,
127
+ ansiPath,
128
+ widthPx,
129
+ heightPx,
130
+ font: args.font,
131
+ fontSize: args.fontSize,
132
+ themeJson: args.themeJson,
133
+ }),
134
+ );
135
+
136
+ try {
137
+ const result = await Bun.$`${args.vhs} ${tapePath}`.quiet().nothrow();
138
+ if (result.exitCode !== 0 || !(await Bun.file(args.outPng).exists())) {
139
+ const detail = result.stderr.toString().trim() || result.stdout.toString().trim();
140
+ throw new Error(`VHS failed to render the gallery screenshot${detail ? `: ${detail.slice(-600)}` : ""}`);
141
+ }
142
+ } finally {
143
+ await Promise.all([
144
+ fs.promises.rm(ansiPath, { force: true }),
145
+ fs.promises.rm(tapePath, { force: true }),
146
+ fs.promises.rm(gifPath, { force: true }),
147
+ ]);
148
+ }
149
+ }
150
+
151
+ interface TapeArgs {
152
+ gifPath: string;
153
+ outPng: string;
154
+ ansiPath: string;
155
+ widthPx: number;
156
+ heightPx: number;
157
+ font: string;
158
+ fontSize: number;
159
+ themeJson: string;
160
+ }
161
+
162
+ function buildTape(args: TapeArgs): string {
163
+ // `Output` (a throwaway GIF) is mandatory for VHS to record; the screenshot
164
+ // is captured from the final visible frame. Setup is hidden so the typed
165
+ // `cat` command and shell prompt never appear in the capture, and a trailing
166
+ // `sleep` keeps the shell from drawing a fresh prompt under the output.
167
+ const shellCommand = `clear; cat ${shellSingleQuote(args.ansiPath)}; sleep 120`;
168
+ return `${[
169
+ `Output ${JSON.stringify(args.gifPath)}`,
170
+ `Set Width ${args.widthPx}`,
171
+ `Set Height ${args.heightPx}`,
172
+ `Set FontFamily ${JSON.stringify(args.font)}`,
173
+ `Set FontSize ${args.fontSize}`,
174
+ `Set Padding ${PADDING}`,
175
+ `Set LineHeight ${LINE_HEIGHT}`,
176
+ `Set Theme ${args.themeJson}`,
177
+ "Hide",
178
+ `Type ${JSON.stringify(shellCommand)}`,
179
+ "Enter",
180
+ "Sleep 1.2s",
181
+ "Show",
182
+ "Sleep 400ms",
183
+ `Screenshot ${JSON.stringify(args.outPng)}`,
184
+ ].join("\n")}\n`;
185
+ }
186
+
187
+ /**
188
+ * Build the VHS terminal theme. Only background/foreground/cursor matter: the
189
+ * gallery emits truecolor (`38;2`/`48;2`) escapes, so the 16-color palette is
190
+ * never consulted — it is filler to satisfy VHS's theme schema.
191
+ */
192
+ function buildVhsTheme(): string {
193
+ const background = parseAnsiRgb(theme.getBgAnsi("statusLineBg")) ?? (theme.isLight ? "#ffffff" : "#1a1a1a");
194
+ const foreground = theme.isLight ? "#1a1a1a" : "#d4d4d4";
195
+ const selection = theme.isLight ? "#c8d6ff" : "#404862";
196
+ return JSON.stringify({
197
+ name: "omp-gallery",
198
+ background,
199
+ foreground,
200
+ cursor: foreground,
201
+ selection,
202
+ black: "#000000",
203
+ red: "#ff5555",
204
+ green: "#50fa7b",
205
+ yellow: "#f1fa8c",
206
+ blue: "#6272ff",
207
+ magenta: "#ff79c6",
208
+ cyan: "#8be9fd",
209
+ white: "#bfbfbf",
210
+ brightBlack: "#4d4d4d",
211
+ brightRed: "#ff6e6e",
212
+ brightGreen: "#69ff94",
213
+ brightYellow: "#ffffa5",
214
+ brightBlue: "#8aa0ff",
215
+ brightMagenta: "#ff92df",
216
+ brightCyan: "#a4ffff",
217
+ brightWhite: "#ffffff",
218
+ });
219
+ }
220
+
221
+ /** Extract `#rrggbb` from a truecolor SGR escape (`…38;2;r;g;b…` / `…48;2;…`). */
222
+ function parseAnsiRgb(ansi: string): string | undefined {
223
+ const match = /[34]8;2;(\d+);(\d+);(\d+)/.exec(ansi);
224
+ if (!match) return undefined;
225
+ const hex = (value: string) => Number(value).toString(16).padStart(2, "0");
226
+ return `#${hex(match[1])}${hex(match[2])}${hex(match[3])}`;
227
+ }
228
+
229
+ /** POSIX single-quote a path for embedding in the VHS shell command. */
230
+ function shellSingleQuote(value: string): string {
231
+ return `'${value.replace(/'/g, `'\\''`)}'`;
232
+ }
233
+
234
+ /**
235
+ * Resolve a chunk's PNG path. A single image keeps the bare name (or the exact
236
+ * `out`); multiple images gain a zero-padded `-NN` suffix so they sort and never
237
+ * collide.
238
+ */
239
+ export function resolveScreenshotOutputPath(
240
+ out: string | undefined,
241
+ baseDir: string,
242
+ index: number,
243
+ total: number,
244
+ ): string {
245
+ if (total === 1) {
246
+ return out ? path.resolve(out) : path.join(baseDir, "gallery.png");
247
+ }
248
+ const suffix = String(index + 1).padStart(2, "0");
249
+ if (out) {
250
+ const resolved = path.resolve(out);
251
+ const ext = path.extname(resolved) || ".png";
252
+ const stem = path.basename(resolved, ext);
253
+ return path.join(path.dirname(resolved), `${stem}-${suffix}${ext}`);
254
+ }
255
+ return path.join(baseDir, `gallery-${suffix}.png`);
256
+ }
257
+
258
+ /**
259
+ * Group whole tool sections into chunks that stay under `rowBudget` rows. A
260
+ * single section larger than the budget gets its own (taller) image rather than
261
+ * being split mid-renderer.
262
+ */
263
+ export function chunkGallerySections(sections: GallerySection[], rowBudget: number): GallerySection[][] {
264
+ const chunks: GallerySection[][] = [];
265
+ let current: GallerySection[] = [];
266
+ let currentRows = 0;
267
+ for (const section of sections) {
268
+ const rows = section.lines.length;
269
+ if (current.length > 0 && currentRows + rows > rowBudget) {
270
+ chunks.push(current);
271
+ current = [];
272
+ currentRows = 0;
273
+ }
274
+ current.push(section);
275
+ currentRows += rows;
276
+ }
277
+ if (current.length > 0) chunks.push(current);
278
+ return chunks.length > 0 ? chunks : [[]];
279
+ }
@@ -22,6 +22,7 @@ export const commands: CommandEntry[] = [
22
22
  { name: "config", load: () => import("./commands/config").then(m => m.default) },
23
23
  { name: "dry-balance", load: () => import("./commands/dry-balance").then(m => m.default) },
24
24
  { name: "grep", load: () => import("./commands/grep").then(m => m.default) },
25
+ { name: "gallery", load: () => import("./commands/gallery").then(m => m.default) },
25
26
  { name: "grievances", load: () => import("./commands/grievances").then(m => m.default) },
26
27
  { name: "install", load: () => import("./commands/install").then(m => m.default) },
27
28
  { name: "plugin", load: () => import("./commands/plugin").then(m => m.default) },
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Render every built-in tool's renderer across its lifecycle states.
3
+ */
4
+ import { Command, Flags } from "@oh-my-pi/pi-utils/cli";
5
+ import { GALLERY_STATES, type GalleryState, runGalleryCommand } from "../cli/gallery-cli";
6
+
7
+ export default class Gallery extends Command {
8
+ static description = "Preview tool renderers across streaming, in-progress, success, and failure states";
9
+
10
+ static flags = {
11
+ tool: Flags.string({ char: "t", description: "Render a single tool by name" }),
12
+ state: Flags.string({
13
+ char: "s",
14
+ description: "Render only the given lifecycle state(s)",
15
+ options: [...GALLERY_STATES],
16
+ multiple: true,
17
+ }),
18
+ width: Flags.integer({ char: "w", description: "Render width in columns" }),
19
+ expanded: Flags.boolean({
20
+ char: "e",
21
+ description: "Render the expanded variant of each renderer",
22
+ default: false,
23
+ }),
24
+ plain: Flags.boolean({ description: "Strip ANSI styling from the output", default: false }),
25
+ screenshot: Flags.boolean({
26
+ description:
27
+ "Capture the rendered output as PNG screenshot(s) via VHS instead of printing ANSI (requires vhs)",
28
+ default: false,
29
+ }),
30
+ out: Flags.string({
31
+ char: "o",
32
+ description: "Screenshot output path (with --screenshot); suffixed per image when split across multiple",
33
+ }),
34
+ font: Flags.string({ description: "Screenshot font family (default: JetBrainsMono Nerd Font)" }),
35
+ "font-size": Flags.integer({ description: "Screenshot font size in points (default: 18)" }),
36
+ };
37
+
38
+ async run(): Promise<void> {
39
+ const { flags } = await this.parse(Gallery);
40
+ await runGalleryCommand({
41
+ tool: flags.tool,
42
+ states: flags.state as GalleryState[] | undefined,
43
+ width: flags.width,
44
+ expanded: flags.expanded,
45
+ plain: flags.plain,
46
+ screenshot: flags.screenshot,
47
+ out: flags.out,
48
+ font: flags.font,
49
+ fontSize: flags["font-size"],
50
+ });
51
+ }
52
+ }
@@ -2,7 +2,7 @@
2
2
  * Root command for the coding agent CLI.
3
3
  */
4
4
 
5
- import { THINKING_EFFORTS } from "@oh-my-pi/pi-ai";
5
+ import { THINKING_EFFORTS } from "@oh-my-pi/pi-ai/effort";
6
6
  import { APP_NAME } from "@oh-my-pi/pi-utils";
7
7
  import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli";
8
8
  import { parseArgs } from "../cli/args";
@@ -1,5 +1,5 @@
1
1
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
- import type { Api, Model } from "@oh-my-pi/pi-ai";
2
+ import type { Api, ApiKey, Model } from "@oh-my-pi/pi-ai";
3
3
  import { completeSimple } from "@oh-my-pi/pi-ai";
4
4
  import { prompt } from "@oh-my-pi/pi-utils";
5
5
  import analysisSystemPrompt from "../../commit/prompts/analysis-system.md" with { type: "text" };
@@ -14,7 +14,7 @@ const ConventionalAnalysisTool = createConventionalAnalysisTool(
14
14
 
15
15
  export interface ConventionalAnalysisInput {
16
16
  model: Model<Api>;
17
- apiKey: string;
17
+ apiKey: ApiKey;
18
18
  thinkingLevel?: ThinkingLevel;
19
19
  contextFiles?: Array<{ path: string; content: string }>;
20
20
  userContext?: string;
@@ -1,5 +1,5 @@
1
1
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
- import type { Api, AssistantMessage, Model } from "@oh-my-pi/pi-ai";
2
+ import type { Api, ApiKey, AssistantMessage, Model } from "@oh-my-pi/pi-ai";
3
3
  import { completeSimple, validateToolCall } from "@oh-my-pi/pi-ai";
4
4
  import { prompt } from "@oh-my-pi/pi-utils";
5
5
  import * as z from "zod/v4";
@@ -19,7 +19,7 @@ const SummaryTool = {
19
19
 
20
20
  export interface SummaryInput {
21
21
  model: Model<Api>;
22
- apiKey: string;
22
+ apiKey: ApiKey;
23
23
  thinkingLevel?: ThinkingLevel;
24
24
  commitType: string;
25
25
  scope: string | null;
@@ -1,5 +1,5 @@
1
1
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
- import type { Api, AssistantMessage, Model } from "@oh-my-pi/pi-ai";
2
+ import type { Api, ApiKey, AssistantMessage, Model } from "@oh-my-pi/pi-ai";
3
3
  import { completeSimple, validateToolCall } from "@oh-my-pi/pi-ai";
4
4
  import { prompt } from "@oh-my-pi/pi-utils";
5
5
  import * as z from "zod/v4";
@@ -25,7 +25,7 @@ export const changelogTool = {
25
25
 
26
26
  export interface ChangelogPromptInput {
27
27
  model: Model<Api>;
28
- apiKey: string;
28
+ apiKey: ApiKey;
29
29
  thinkingLevel?: ThinkingLevel;
30
30
  changelogPath: string;
31
31
  isPackageChangelog: boolean;
@@ -1,6 +1,6 @@
1
1
  import * as path from "node:path";
2
2
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
3
- import type { Api, Model } from "@oh-my-pi/pi-ai";
3
+ import type { Api, ApiKey, Model } from "@oh-my-pi/pi-ai";
4
4
  import { logger } from "@oh-my-pi/pi-utils";
5
5
  import { CHANGELOG_CATEGORIES } from "../../commit/types";
6
6
  import * as git from "../../utils/git";
@@ -15,7 +15,7 @@ const DEFAULT_MAX_DIFF_CHARS = 120_000;
15
15
  export interface ChangelogFlowInput {
16
16
  cwd: string;
17
17
  model: Model<Api>;
18
- apiKey: string;
18
+ apiKey: ApiKey;
19
19
  thinkingLevel?: ThinkingLevel;
20
20
  stagedFiles: string[];
21
21
  dryRun: boolean;
@@ -1,5 +1,5 @@
1
1
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
- import type { Api, Model } from "@oh-my-pi/pi-ai";
2
+ import type { Api, ApiKey, Model } from "@oh-my-pi/pi-ai";
3
3
  import { $env } from "@oh-my-pi/pi-utils";
4
4
  import { parseFileDiffs } from "../../commit/git/diff";
5
5
  import type { ConventionalAnalysis } from "../../commit/types";
@@ -21,10 +21,10 @@ export interface MapReduceSettings {
21
21
 
22
22
  export interface MapReduceInput {
23
23
  model: Model<Api>;
24
- apiKey: string;
24
+ apiKey: ApiKey;
25
25
  thinkingLevel?: ThinkingLevel;
26
26
  smolModel: Model<Api>;
27
- smolApiKey: string;
27
+ smolApiKey: ApiKey;
28
28
  smolThinkingLevel?: ThinkingLevel;
29
29
  diff: string;
30
30
  stat: string;
@@ -1,5 +1,5 @@
1
1
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
- import type { Api, AssistantMessage, Message, Model } from "@oh-my-pi/pi-ai";
2
+ import type { Api, ApiKey, AssistantMessage, Message, Model } from "@oh-my-pi/pi-ai";
3
3
  import { completeSimple } from "@oh-my-pi/pi-ai";
4
4
  import { prompt } from "@oh-my-pi/pi-utils";
5
5
  import fileObserverSystemPrompt from "../../commit/prompts/file-observer-system.md" with { type: "text" };
@@ -18,7 +18,7 @@ const RETRY_BACKOFF_MS = 1000;
18
18
 
19
19
  export interface MapPhaseInput {
20
20
  model: Model<Api>;
21
- apiKey: string;
21
+ apiKey: ApiKey;
22
22
  thinkingLevel?: ThinkingLevel;
23
23
  files: FileDiff[];
24
24
  config?: {
@@ -1,5 +1,5 @@
1
1
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
- import type { Api, Model } from "@oh-my-pi/pi-ai";
2
+ import type { Api, ApiKey, Model } from "@oh-my-pi/pi-ai";
3
3
  import { completeSimple } from "@oh-my-pi/pi-ai";
4
4
  import { prompt } from "@oh-my-pi/pi-utils";
5
5
  import reduceSystemPrompt from "../../commit/prompts/reduce-system.md" with { type: "text" };
@@ -12,7 +12,7 @@ const ReduceTool = createConventionalAnalysisTool("Synthesize file observations
12
12
 
13
13
  export interface ReducePhaseInput {
14
14
  model: Model<Api>;
15
- apiKey: string;
15
+ apiKey: ApiKey;
16
16
  thinkingLevel?: ThinkingLevel;
17
17
  observations: FileObservation[];
18
18
  stat: string;
@@ -1,5 +1,6 @@
1
1
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
- import type { Api, Model } from "@oh-my-pi/pi-ai";
2
+ import type { Api, ApiKey, Model } from "@oh-my-pi/pi-ai";
3
+ import type { ApiKeyResolverRegistry } from "../config/api-key-resolver";
3
4
  import { MODEL_ROLE_IDS } from "../config/model-registry";
4
5
  import {
5
6
  type ModelLookupRegistry,
@@ -12,13 +13,19 @@ import MODEL_PRIO from "../priority.json" with { type: "json" };
12
13
 
13
14
  export interface ResolvedCommitModel {
14
15
  model: Model<Api>;
15
- apiKey: string;
16
+ /**
17
+ * Resolver for the model's bearer: re-resolves on 401 / usage-limit so the
18
+ * whole commit pipeline (analysis, map/reduce, changelog) inherits the
19
+ * central force-refresh + account-rotation policy.
20
+ */
21
+ apiKey: ApiKey;
16
22
  thinkingLevel?: ThinkingLevel;
17
23
  }
18
24
 
19
- type CommitModelRegistry = ModelLookupRegistry & {
20
- getApiKey: (model: Model<Api>) => Promise<string | undefined>;
21
- };
25
+ type CommitModelRegistry = ModelLookupRegistry &
26
+ ApiKeyResolverRegistry & {
27
+ getApiKey: (model: Model<Api>) => Promise<string | undefined>;
28
+ };
22
29
 
23
30
  export async function resolvePrimaryModel(
24
31
  override: string | undefined,
@@ -38,20 +45,32 @@ export async function resolvePrimaryModel(
38
45
  if (!apiKey) {
39
46
  throw new Error(`No API key available for model ${model.provider}/${model.id}`);
40
47
  }
41
- return { model, apiKey, thinkingLevel: resolved?.thinkingLevel };
48
+ return {
49
+ model,
50
+ apiKey: modelRegistry.resolver(model.provider, { baseUrl: model.baseUrl }),
51
+ thinkingLevel: resolved?.thinkingLevel,
52
+ };
42
53
  }
43
54
 
44
55
  export async function resolveSmolModel(
45
56
  settings: Settings,
46
57
  modelRegistry: CommitModelRegistry,
47
58
  fallbackModel: Model<Api>,
48
- fallbackApiKey: string,
59
+ fallbackApiKey: ApiKey,
49
60
  ): Promise<ResolvedCommitModel> {
50
61
  const available = modelRegistry.getAvailable();
51
62
  const resolvedSmol = resolveRoleSelection(["smol"], settings, available, modelRegistry);
52
63
  if (resolvedSmol?.model) {
53
64
  const apiKey = await modelRegistry.getApiKey(resolvedSmol.model);
54
- if (apiKey) return { model: resolvedSmol.model, apiKey, thinkingLevel: resolvedSmol.thinkingLevel };
65
+ if (apiKey) {
66
+ return {
67
+ model: resolvedSmol.model,
68
+ apiKey: modelRegistry.resolver(resolvedSmol.model.provider, {
69
+ baseUrl: resolvedSmol.model.baseUrl,
70
+ }),
71
+ thinkingLevel: resolvedSmol.thinkingLevel,
72
+ };
73
+ }
55
74
  }
56
75
 
57
76
  const matchPreferences = { usageOrder: settings.getStorage()?.getModelUsageOrder() };
@@ -59,7 +78,12 @@ export async function resolveSmolModel(
59
78
  const candidate = parseModelPattern(pattern, available, matchPreferences, { modelRegistry }).model;
60
79
  if (!candidate) continue;
61
80
  const apiKey = await modelRegistry.getApiKey(candidate);
62
- if (apiKey) return { model: candidate, apiKey };
81
+ if (apiKey) {
82
+ return {
83
+ model: candidate,
84
+ apiKey: modelRegistry.resolver(candidate.provider, { baseUrl: candidate.baseUrl }),
85
+ };
86
+ }
63
87
  }
64
88
 
65
89
  return { model: fallbackModel, apiKey: fallbackApiKey };
@@ -1,6 +1,6 @@
1
1
  import * as path from "node:path";
2
2
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
3
- import type { Api, Model } from "@oh-my-pi/pi-ai";
3
+ import type { Api, ApiKey, Model } from "@oh-my-pi/pi-ai";
4
4
  import { getProjectDir, logger, prompt } from "@oh-my-pi/pi-utils";
5
5
  import { ModelRegistry } from "../config/model-registry";
6
6
  import { Settings } from "../config/settings";
@@ -145,10 +145,10 @@ async function generateAnalysis(input: {
145
145
  contextFiles: Array<{ path: string; content: string }>;
146
146
  userContext?: string;
147
147
  primaryModel: Model<Api>;
148
- primaryApiKey: string;
148
+ primaryApiKey: ApiKey;
149
149
  primaryThinkingLevel?: ThinkingLevel;
150
150
  smolModel: Model<Api>;
151
- smolApiKey: string;
151
+ smolApiKey: ApiKey;
152
152
  smolThinkingLevel?: ThinkingLevel;
153
153
  commitSettings: {
154
154
  mapReduceEnabled: boolean;
@@ -206,7 +206,7 @@ async function generateSummaryWithRetry(input: {
206
206
  analysis: ConventionalAnalysis;
207
207
  stat: string;
208
208
  model: Model<Api>;
209
- apiKey: string;
209
+ apiKey: ApiKey;
210
210
  thinkingLevel?: ThinkingLevel;
211
211
  userContext?: string;
212
212
  }): Promise<{ summary: string }> {
@@ -0,0 +1,58 @@
1
+ import type { ApiKeyResolver, AuthStorage } from "@oh-my-pi/pi-ai";
2
+
3
+ export interface ApiKeyResolverOptions {
4
+ /** Session id for credential stickiness; read at resolve time by the caller. */
5
+ sessionId?: string;
6
+ /** Provider base URL hint forwarded to the auth-storage cascade. */
7
+ baseUrl?: string;
8
+ }
9
+
10
+ /**
11
+ * Minimal slice of `ModelRegistry` the resolver needs. Typed structurally so
12
+ * narrower registry shells (e.g. the commit pipeline's `CommitModelRegistry`)
13
+ * can build resolvers without depending on the full class.
14
+ */
15
+ export interface ApiKeyResolverRegistry {
16
+ getApiKeyForProvider(
17
+ provider: string,
18
+ sessionId?: string,
19
+ options?: { baseUrl?: string; forceRefresh?: boolean; signal?: AbortSignal },
20
+ ): Promise<string | undefined>;
21
+ authStorage: Pick<AuthStorage, "rotateSessionCredential">;
22
+ /**
23
+ * Build an {@link ApiKeyResolver} implementing the central a/b/c auth-retry
24
+ * policy: initial → resolve; step (b) → force-refresh same account; step (c)
25
+ * → rotate to a sibling credential, then re-resolve.
26
+ *
27
+ * The resolver is stateless (safe to reuse across requests). Callers that
28
+ * need the initial key for a guard can call `resolveApiKeyOnce(resolver)`.
29
+ */
30
+ resolver(provider: string, options?: ApiKeyResolverOptions): ApiKeyResolver;
31
+ }
32
+
33
+ /**
34
+ * Default implementation of {@link ApiKeyResolverRegistry.resolver}.
35
+ * Also usable standalone for structural registries that don't carry the method.
36
+ */
37
+ export function createApiKeyResolver(
38
+ registry: Pick<ApiKeyResolverRegistry, "getApiKeyForProvider" | "authStorage">,
39
+ provider: string,
40
+ options: ApiKeyResolverOptions = {},
41
+ ): ApiKeyResolver {
42
+ const { sessionId, baseUrl } = options;
43
+ return async ({ lastChance, error, signal }) => {
44
+ if (error === undefined) {
45
+ return registry.getApiKeyForProvider(provider, sessionId, { baseUrl });
46
+ }
47
+ if (lastChance) {
48
+ // Account constraint (401 / usage / account-rate-limit): rotate to a
49
+ // sibling credential. We do NOT honor any retry-after here — if a
50
+ // sibling exists we switch immediately; the precise no-sibling backoff
51
+ // is owned by `markUsageLimitReached` (default + server usage-report
52
+ // reset) and the outer whole-turn retry layer.
53
+ await registry.authStorage.rotateSessionCredential(provider, sessionId, { error, signal });
54
+ return registry.getApiKeyForProvider(provider, sessionId, { baseUrl });
55
+ }
56
+ return registry.getApiKeyForProvider(provider, sessionId, { baseUrl, forceRefresh: true, signal });
57
+ };
58
+ }