@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
@@ -6,10 +6,127 @@ function isUrlLikePath(filePath: string): boolean {
6
6
  return URL_LIKE_PATH_RE.test(filePath);
7
7
  }
8
8
 
9
+ // =============================================================================
10
+ // Multi-level path tree
11
+ // =============================================================================
12
+ //
13
+ // File listings (grep / ast-grep / ast-edit / lsp diagnostics / find) used to
14
+ // group by the *immediate* parent directory and print the full directory path in
15
+ // every header. For results spread across a deep tree — or rooted outside cwd,
16
+ // where paths stay absolute — that repeated the shared prefix on every line. The
17
+ // tree below folds single-child directory chains (so the common prefix collapses
18
+ // into one header) and nests the rest, charging the model one token per path
19
+ // segment instead of one per file.
20
+
21
+ interface PathTreeNode {
22
+ /** Direct file leaves, in first-seen order. */
23
+ files: Array<{ name: string; key: string }>;
24
+ /** Dedup set for `files` (a glob can surface the same path twice on retry). */
25
+ fileNames: Set<string>;
26
+ /** Child directories, in first-seen order. */
27
+ subdirs: Array<{ name: string; node: PathTreeNode }>;
28
+ /** Dedup index for `subdirs`. */
29
+ dirIndex: Map<string, PathTreeNode>;
30
+ }
31
+
32
+ export interface PathTreeInput {
33
+ /** Path string; absolute, cwd-relative, or url-like. Backslashes are normalized. */
34
+ path: string;
35
+ /** Whether the leaf itself is a directory (trailing-slash match from find). */
36
+ isDir: boolean;
37
+ /** Opaque key carried onto file events for section lookup. Defaults to `path`. */
38
+ key?: string;
39
+ }
40
+
41
+ /** One node emitted while walking the tree: a folded directory or a file leaf. */
42
+ export interface GroupedTreeEvent {
43
+ kind: "dir" | "file";
44
+ /** 0-based nesting depth (root children are depth 0). */
45
+ depth: number;
46
+ /** Folded chain for dirs (e.g. `a/b/c`, no trailing slash); basename for files. */
47
+ name: string;
48
+ /** File key for `kind === "file"`; empty string for directories. */
49
+ key: string;
50
+ }
51
+
52
+ function createNode(): PathTreeNode {
53
+ return { files: [], fileNames: new Set(), subdirs: [], dirIndex: new Map() };
54
+ }
55
+
56
+ function addFile(node: PathTreeNode, name: string, key: string): void {
57
+ if (node.fileNames.has(name)) return;
58
+ node.fileNames.add(name);
59
+ node.files.push({ name, key });
60
+ }
61
+
62
+ /**
63
+ * Build a directory tree from a flat list of paths. URL-like entries are kept
64
+ * whole as root-level file leaves (they have no meaningful directory structure).
65
+ * Absolute paths carry a leading empty segment so they share a common `/` root
66
+ * and fold like any other prefix.
67
+ */
68
+ export function buildPathTree(entries: Iterable<PathTreeInput>): PathTreeNode {
69
+ const root = createNode();
70
+ for (const { path: rawPath, isDir, key } of entries) {
71
+ const normalized = rawPath.replace(/\\/g, "/");
72
+ const fileKey = key ?? rawPath;
73
+ if (isUrlLikePath(normalized)) {
74
+ addFile(root, normalized, fileKey);
75
+ continue;
76
+ }
77
+ const trimmed = normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
78
+ if (trimmed.length === 0) continue;
79
+ const segments = trimmed.split("/");
80
+ const dirCount = isDir ? segments.length : segments.length - 1;
81
+ let node = root;
82
+ for (let i = 0; i < dirCount; i++) {
83
+ const segment = segments[i]!;
84
+ let child = node.dirIndex.get(segment);
85
+ if (!child) {
86
+ child = createNode();
87
+ node.dirIndex.set(segment, child);
88
+ node.subdirs.push({ name: segment, node: child });
89
+ }
90
+ node = child;
91
+ }
92
+ if (!isDir) {
93
+ addFile(node, segments[segments.length - 1]!, fileKey);
94
+ }
95
+ }
96
+ return root;
97
+ }
98
+
99
+ /**
100
+ * Depth-first walk yielding directory and file events. Directories collapse their
101
+ * single-child chains (`a` → `a/b` → `a/b/c`) so a shared prefix becomes one
102
+ * header. Each node's direct files are emitted before its subdirectories, keeping
103
+ * a file unambiguously attached to the header above it.
104
+ */
105
+ export function* walkPathTree(node: PathTreeNode, depth = 0): Generator<GroupedTreeEvent> {
106
+ for (const file of node.files) {
107
+ yield { kind: "file", depth, name: file.name, key: file.key };
108
+ }
109
+ for (const subdir of node.subdirs) {
110
+ let dirNode = subdir.node;
111
+ const parts = [subdir.name];
112
+ while (dirNode.files.length === 0 && dirNode.subdirs.length === 1) {
113
+ const only = dirNode.subdirs[0]!;
114
+ parts.push(only.name);
115
+ dirNode = only.node;
116
+ }
117
+ yield { kind: "dir", depth, name: parts.join("/"), key: "" };
118
+ yield* walkPathTree(dirNode, depth + 1);
119
+ }
120
+ }
121
+
122
+ // =============================================================================
123
+ // Grouped file output (grep / ast-grep / ast-edit / lsp diagnostics)
124
+ // =============================================================================
125
+
9
126
  /**
10
127
  * One file's contribution to a grouped file output. The header itself is generated
11
- * by `formatGroupedFiles` (single `#` for root files, `##` for files inside a dir);
12
- * use `headerSuffix` to tack on extras like ` (1 replacement)`.
128
+ * by `formatGroupedFiles` (one `#` per nesting level); use `headerSuffix` to tack
129
+ * on extras like ` (1 replacement)`.
13
130
  */
14
131
  export interface GroupedFileSection {
15
132
  /** Optional suffix appended to the file header. */
@@ -28,76 +145,183 @@ export interface GroupedFilesOutput {
28
145
  }
29
146
 
30
147
  /**
31
- * Render a list of files as directory-grouped sections shared by grep, ast-grep,
32
- * ast-edit, and the LSP diagnostic formatter.
148
+ * Render a list of files as a multi-level, prefix-folded directory tree shared by
149
+ * grep, ast-grep, ast-edit, and the LSP diagnostic formatter.
33
150
  *
34
- * Layout:
35
- * # dir/
36
- * ## file.ts
151
+ * Layout (one `#` per level; the shared prefix folds into the top header):
152
+ * # packages/pkg/src/
153
+ * ## root.ts
37
154
  * …body…
38
- *
39
- * # otherdir/
40
- * ## other.ts
155
+ * ## nested/
156
+ * ### child.ts
41
157
  * …body…
42
158
  *
43
- * Files in the project root (directory `.`) become single-`#` headers without a
44
- * `## file` line, matching the existing convention.
159
+ * Files in the (folded) project root become single-`#` headers with no parent
160
+ * directory line. A blank line precedes every directory header and every
161
+ * root-level file so the renderers can split the output into collapsible groups.
45
162
  */
46
163
  export function formatGroupedFiles(
47
164
  files: string[],
48
165
  renderFile: (filePath: string) => GroupedFileSection,
49
166
  ): GroupedFilesOutput {
50
- const filesByDirectory = new Map<string, string[]>();
167
+ const sections = new Map<string, GroupedFileSection>();
168
+ const inputs: PathTreeInput[] = [];
51
169
  for (const filePath of files) {
52
- const directory = isUrlLikePath(filePath) ? "." : path.dirname(filePath).replace(/\\/g, "/");
53
- if (!filesByDirectory.has(directory)) {
54
- filesByDirectory.set(directory, []);
55
- }
56
- filesByDirectory.get(directory)!.push(filePath);
170
+ if (sections.has(filePath)) continue;
171
+ const section = renderFile(filePath);
172
+ if (section.skip) continue;
173
+ sections.set(filePath, section);
174
+ inputs.push({ path: filePath, isDir: false, key: filePath });
57
175
  }
58
176
 
177
+ const tree = buildPathTree(inputs);
59
178
  const model: string[] = [];
60
179
  const display: string[] = [];
180
+ let emitted = false;
61
181
 
62
- const pushSeparatorIfNeeded = () => {
63
- if (model.length > 0) {
182
+ for (const event of walkPathTree(tree)) {
183
+ const hashes = "#".repeat(event.depth + 1);
184
+ const needsSeparator = emitted && (event.depth === 0 || event.kind === "dir");
185
+ if (needsSeparator) {
64
186
  model.push("");
65
187
  display.push("");
66
188
  }
189
+ emitted = true;
190
+ if (event.kind === "dir") {
191
+ const header = `${hashes} ${event.name}/`;
192
+ model.push(header);
193
+ display.push(header);
194
+ continue;
195
+ }
196
+ const section = sections.get(event.key)!;
197
+ const header = `${hashes} ${event.name}${section.headerSuffix ?? ""}`;
198
+ model.push(header, ...section.modelLines);
199
+ display.push(header, ...(section.displayLines ?? section.modelLines));
200
+ }
201
+
202
+ return { model, display };
203
+ }
204
+
205
+ // =============================================================================
206
+ // Parsing grouped output back into per-line context (TUI renderers)
207
+ // =============================================================================
208
+
209
+ const GROUPED_HEADER_RE = /^(#+)\s+(.*)$/;
210
+ const HEADER_SUFFIX_RE = /\s+\([^)]*\)\s*$/;
211
+ const HEADER_HASH_TAG_RE = /#[0-9a-f]+$/i;
212
+
213
+ /** Per-line classification of grouped output, used by renderers for hyperlinks. */
214
+ export interface GroupedLineContext {
215
+ /** Directory header, file header, or any non-header body/content line. */
216
+ kind: "dir" | "file" | "content";
217
+ /** Number of leading `#` for headers; 0 for content lines. */
218
+ depth: number;
219
+ /** Resolved absolute path of the dir/file a header points at (when resolvable). */
220
+ headerPath?: string;
221
+ /** For content lines, the absolute path of the owning file (line hyperlinks). */
222
+ filePath?: string;
223
+ /** Header is an internal/url-like target the caller resolves itself. */
224
+ isUrl?: boolean;
225
+ }
226
+
227
+ function resolveGroupedPath(parent: string | undefined, name: string): string | undefined {
228
+ if (parent === undefined) return undefined;
229
+ if (name === "" || name === ".") return parent;
230
+ // `path.resolve` keeps an absolute `name` intact (out-of-cwd results) while
231
+ // joining a relative folded chain (`packages/pkg/src`) onto the parent.
232
+ return path.resolve(parent, name);
233
+ }
234
+
235
+ /**
236
+ * Walk grouped output lines, tracking a directory stack keyed by header depth, so
237
+ * each header and body line can be linked back to its absolute filesystem path.
238
+ * Reconstruction is stack-based (not per-blank-group) so nested directory headers
239
+ * resolve correctly across the whole output.
240
+ *
241
+ * `headerBase` is the directory the displayed (folded) header paths are relative
242
+ * to — for grep/ast tools that is the session cwd, since display paths are
243
+ * formatted relative to cwd regardless of the (sub)directory the search was
244
+ * scoped to. `fileScope` is the initial owning file for body lines that appear
245
+ * before any header (single-file scopes have no `#` headers); it defaults to
246
+ * `headerBase` and should be passed the scoped file's absolute path.
247
+ */
248
+ export function classifyGroupedLines(
249
+ lines: readonly string[],
250
+ headerBase: string | undefined,
251
+ fileScope: string | undefined = headerBase,
252
+ ): GroupedLineContext[] {
253
+ const result: GroupedLineContext[] = [];
254
+ const dirAtDepth = new Map<number, string>();
255
+ // Body lines before any header (single-file scopes) link to the scoped file.
256
+ let currentFile = fileScope;
257
+
258
+ const clearDeeper = (depth: number) => {
259
+ for (const key of dirAtDepth.keys()) {
260
+ if (key >= depth) dirAtDepth.delete(key);
261
+ }
67
262
  };
68
263
 
69
- for (const [directory, dirFiles] of filesByDirectory) {
70
- if (directory === ".") {
71
- for (const filePath of dirFiles) {
72
- const section = renderFile(filePath);
73
- if (section.skip) continue;
74
- pushSeparatorIfNeeded();
75
- const headerName = isUrlLikePath(filePath) ? filePath : path.basename(filePath);
76
- const header = `# ${headerName}${section.headerSuffix ?? ""}`;
77
- model.push(header, ...section.modelLines);
78
- display.push(header, ...(section.displayLines ?? section.modelLines));
79
- }
264
+ for (const line of lines) {
265
+ const match = GROUPED_HEADER_RE.exec(line);
266
+ if (!match) {
267
+ result.push({ kind: "content", depth: 0, filePath: currentFile });
80
268
  continue;
81
269
  }
82
-
83
- const sections: Array<{ filePath: string; section: GroupedFileSection }> = [];
84
- for (const filePath of dirFiles) {
85
- const section = renderFile(filePath);
86
- if (section.skip) continue;
87
- sections.push({ filePath, section });
270
+ const depth = match[1]!.length;
271
+ const rest = match[2]!.trimEnd();
272
+ if (isUrlLikePath(rest)) {
273
+ clearDeeper(depth);
274
+ currentFile = undefined;
275
+ result.push({ kind: "file", depth, isUrl: true });
276
+ continue;
88
277
  }
89
- if (sections.length === 0) continue;
90
-
91
- pushSeparatorIfNeeded();
92
- const dirHeader = `# ${directory}/`;
93
- model.push(dirHeader);
94
- display.push(dirHeader);
95
- for (const { filePath, section } of sections) {
96
- const fileHeader = `## ${path.basename(filePath)}${section.headerSuffix ?? ""}`;
97
- model.push(fileHeader, ...section.modelLines);
98
- display.push(fileHeader, ...(section.displayLines ?? section.modelLines));
278
+ const parent = depth > 1 ? dirAtDepth.get(depth - 1) : headerBase;
279
+ if (rest.endsWith("/")) {
280
+ const name = rest.slice(0, -1).replace(HEADER_SUFFIX_RE, "");
281
+ const abs = resolveGroupedPath(parent, name);
282
+ clearDeeper(depth);
283
+ if (abs !== undefined) dirAtDepth.set(depth, abs);
284
+ currentFile = undefined;
285
+ result.push({ kind: "dir", depth, headerPath: abs });
286
+ continue;
99
287
  }
288
+ const name = rest.replace(HEADER_SUFFIX_RE, "").replace(HEADER_HASH_TAG_RE, "");
289
+ const abs = name ? resolveGroupedPath(parent, name) : undefined;
290
+ currentFile = abs;
291
+ result.push({ kind: "file", depth, headerPath: abs });
100
292
  }
101
293
 
102
- return { model, display };
294
+ return result;
295
+ }
296
+
297
+ /**
298
+ * Split line indices into blank-line-separated groups, mirroring
299
+ * `splitGroupsByBlankLine`: when any blank line is present, break on runs of
300
+ * blanks; otherwise return a single group of the non-empty lines. Returning
301
+ * indices lets callers slice parallel arrays (raw lines, styled lines, contexts).
302
+ */
303
+ export function groupLineIndicesByBlank(rawLines: readonly string[]): number[][] {
304
+ const hasSeparators = rawLines.some(line => line.trim().length === 0);
305
+ const groups: number[][] = [];
306
+ if (hasSeparators) {
307
+ let current: number[] = [];
308
+ for (let i = 0; i < rawLines.length; i++) {
309
+ if (rawLines[i]!.trim().length === 0) {
310
+ if (current.length > 0) {
311
+ groups.push(current);
312
+ current = [];
313
+ }
314
+ continue;
315
+ }
316
+ current.push(i);
317
+ }
318
+ if (current.length > 0) groups.push(current);
319
+ } else {
320
+ const current: number[] = [];
321
+ for (let i = 0; i < rawLines.length; i++) {
322
+ if (rawLines[i]!.trim().length > 0) current.push(i);
323
+ }
324
+ if (current.length > 0) groups.push(current);
325
+ }
326
+ return groups;
103
327
  }
@@ -1,6 +1,6 @@
1
1
  import * as os from "node:os";
2
2
  import * as path from "node:path";
3
- import { getAntigravityUserAgent, getEnvApiKey, type Model } from "@oh-my-pi/pi-ai";
3
+ import { type ApiKey, getAntigravityUserAgent, getEnvApiKey, type Model, withAuth } from "@oh-my-pi/pi-ai";
4
4
  import {
5
5
  CODEX_BASE_URL,
6
6
  getCodexAccountId,
@@ -20,6 +20,7 @@ import {
20
20
  } from "@oh-my-pi/pi-utils";
21
21
  import * as z from "zod/v4";
22
22
  import packageJson from "../../package.json" with { type: "json" };
23
+
23
24
  import { isAuthenticated, type ModelRegistry } from "../config/model-registry";
24
25
  import type { CustomTool } from "../extensibility/custom-tools/types";
25
26
  import { ohMyPiXAIUserAgent, resolveXAIHttpCredentials } from "../lib/xai-http";
@@ -864,7 +865,10 @@ async function generateOpenAIHostedImage(
864
865
 
865
866
  if (!response.ok) {
866
867
  const errorText = await response.text();
867
- throw new Error(`OpenAI image request failed (${response.status}): ${getOpenAIResponseErrorMessage(errorText)}`);
868
+ throw Object.assign(
869
+ new Error(`OpenAI image request failed (${response.status}): ${getOpenAIResponseErrorMessage(errorText)}`),
870
+ { status: response.status },
871
+ );
868
872
  }
869
873
 
870
874
  const contentType = response.headers.get("content-type") ?? "";
@@ -1037,13 +1041,16 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
1037
1041
  throw new Error("Missing active GPT model for OpenAI image generation");
1038
1042
  }
1039
1043
 
1040
- const parsed = await generateOpenAIHostedImage(
1041
- apiKey.apiKey,
1042
- apiKey.model,
1043
- params,
1044
- resolvedImages,
1045
- requestSignal,
1044
+ const hostedModel = apiKey.model;
1045
+ const hostedKey: ApiKey = ctx.modelRegistry.resolver(hostedModel.provider, {
1046
1046
  sessionId,
1047
+ baseUrl: hostedModel.baseUrl,
1048
+ });
1049
+
1050
+ const parsed = await withAuth(
1051
+ hostedKey,
1052
+ key => generateOpenAIHostedImage(key, hostedModel, params, resolvedImages, requestSignal, sessionId),
1053
+ { signal: requestSignal },
1047
1054
  );
1048
1055
 
1049
1056
  if (parsed.images.length === 0) {
@@ -1088,38 +1095,57 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
1088
1095
  }
1089
1096
 
1090
1097
  const prompt = assemblePrompt(params);
1091
- const requestBody = buildAntigravityRequest(
1092
- prompt,
1093
- model,
1094
- apiKey.projectId,
1095
- params.aspect_ratio,
1096
- params.image_size,
1097
- resolvedImages,
1098
- );
1099
-
1100
- const response = await fetch(`${ANTIGRAVITY_ENDPOINT}/v1internal:streamGenerateContent?alt=sse`, {
1101
- method: "POST",
1102
- headers: {
1103
- Authorization: `Bearer ${apiKey.apiKey}`,
1104
- "Content-Type": "application/json",
1105
- Accept: "text/event-stream",
1106
- "User-Agent": getAntigravityUserAgent(),
1107
- },
1108
- body: JSON.stringify(requestBody),
1109
- signal: requestSignal,
1098
+ const antigravityKey: ApiKey = ctx.modelRegistry.resolver("google-antigravity", {
1099
+ sessionId,
1110
1100
  });
1111
1101
 
1112
- if (!response.ok) {
1113
- const errorText = await response.text();
1114
- let message = errorText;
1115
- try {
1116
- const parsed = JSON.parse(errorText) as { error?: { message?: string } };
1117
- message = parsed.error?.message ?? message;
1118
- } catch {
1119
- // Keep raw text.
1120
- }
1121
- throw new Error(`Antigravity image request failed (${response.status}): ${message}`);
1122
- }
1102
+ const response = await withAuth(
1103
+ antigravityKey,
1104
+ async key => {
1105
+ // On a retry the resolver yields the raw stored credential JSON
1106
+ // ({ token, projectId }); the initial seed is the already-parsed
1107
+ // access token. Tolerate both, falling back to the seed projectId.
1108
+ const rotated = parseAntigravityCredentials(key);
1109
+ const bearer = rotated?.accessToken ?? key;
1110
+ const projectId = rotated?.projectId ?? apiKey.projectId!;
1111
+ const requestBody = buildAntigravityRequest(
1112
+ prompt,
1113
+ model,
1114
+ projectId,
1115
+ params.aspect_ratio,
1116
+ params.image_size,
1117
+ resolvedImages,
1118
+ );
1119
+
1120
+ const resp = await fetch(`${ANTIGRAVITY_ENDPOINT}/v1internal:streamGenerateContent?alt=sse`, {
1121
+ method: "POST",
1122
+ headers: {
1123
+ Authorization: `Bearer ${bearer}`,
1124
+ "Content-Type": "application/json",
1125
+ Accept: "text/event-stream",
1126
+ "User-Agent": getAntigravityUserAgent(),
1127
+ },
1128
+ body: JSON.stringify(requestBody),
1129
+ signal: requestSignal,
1130
+ });
1131
+
1132
+ if (!resp.ok) {
1133
+ const errorText = await resp.text();
1134
+ let message = errorText;
1135
+ try {
1136
+ const parsedErr = JSON.parse(errorText) as { error?: { message?: string } };
1137
+ message = parsedErr.error?.message ?? message;
1138
+ } catch {
1139
+ // Keep raw text.
1140
+ }
1141
+ throw Object.assign(new Error(`Antigravity image request failed (${resp.status}): ${message}`), {
1142
+ status: resp.status,
1143
+ });
1144
+ }
1145
+ return resp;
1146
+ },
1147
+ { signal: requestSignal },
1148
+ );
1123
1149
 
1124
1150
  const parsed = await parseAntigravitySseForImage(response, requestSignal);
1125
1151
  const responseText = parsed.text.length > 0 ? parsed.text.join(" ") : undefined;
@@ -1191,28 +1217,41 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
1191
1217
  : xaiBaseBody;
1192
1218
  const xaiEndpoint = isEdit ? "/images/edits" : "/images/generations";
1193
1219
 
1194
- const xaiResponse = await fetch(`${xaiCreds.baseURL}${xaiEndpoint}`, {
1195
- method: "POST",
1196
- headers: {
1197
- Authorization: `Bearer ${xaiCreds.apiKey}`,
1198
- "Content-Type": "application/json",
1199
- "User-Agent": ohMyPiXAIUserAgent(),
1200
- },
1201
- body: JSON.stringify(xaiBody),
1202
- signal: requestSignal,
1220
+ const xaiKey: ApiKey = ctx.modelRegistry.resolver(xaiCreds.provider, {
1221
+ sessionId,
1222
+ baseUrl: xaiCreds.baseURL,
1203
1223
  });
1204
1224
 
1205
- const xaiRawText = await xaiResponse.text();
1206
- if (!xaiResponse.ok) {
1207
- let message = xaiRawText;
1208
- try {
1209
- const parsedErr = JSON.parse(xaiRawText) as { error?: { message?: string } };
1210
- message = parsedErr.error?.message ?? message;
1211
- } catch {
1212
- // Keep raw text.
1213
- }
1214
- throw new Error(`xAI image request failed (${xaiResponse.status}): ${message}`);
1215
- }
1225
+ const xaiRawText = await withAuth(
1226
+ xaiKey,
1227
+ async key => {
1228
+ const resp = await fetch(`${xaiCreds.baseURL}${xaiEndpoint}`, {
1229
+ method: "POST",
1230
+ headers: {
1231
+ Authorization: `Bearer ${key}`,
1232
+ "Content-Type": "application/json",
1233
+ "User-Agent": ohMyPiXAIUserAgent(),
1234
+ },
1235
+ body: JSON.stringify(xaiBody),
1236
+ signal: requestSignal,
1237
+ });
1238
+ const rawText = await resp.text();
1239
+ if (!resp.ok) {
1240
+ let message = rawText;
1241
+ try {
1242
+ const parsedErr = JSON.parse(rawText) as { error?: { message?: string } };
1243
+ message = parsedErr.error?.message ?? message;
1244
+ } catch {
1245
+ // Keep raw text.
1246
+ }
1247
+ throw Object.assign(new Error(`xAI image request failed (${resp.status}): ${message}`), {
1248
+ status: resp.status,
1249
+ });
1250
+ }
1251
+ return rawText;
1252
+ },
1253
+ { signal: requestSignal },
1254
+ );
1216
1255
 
1217
1256
  const xaiData = JSON.parse(xaiRawText) as {
1218
1257
  data?: Array<{ b64_json?: string; url?: string }>;
@@ -1269,30 +1308,34 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
1269
1308
  messages: [{ role: "user" as const, content: contentParts }],
1270
1309
  };
1271
1310
 
1272
- const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
1273
- method: "POST",
1274
- headers: {
1275
- "Content-Type": "application/json",
1276
- Authorization: `Bearer ${apiKey.apiKey}`,
1277
- "HTTP-Referer": "https://omp.sh/",
1278
- "X-OpenRouter-Title": "Oh-My-Pi",
1279
- "X-OpenRouter-Categories": "cli-agent",
1280
- },
1281
- body: JSON.stringify(requestBody),
1282
- signal: requestSignal,
1283
- });
1284
-
1285
- const rawText = await response.text();
1286
- if (!response.ok) {
1287
- let message = rawText;
1288
- try {
1289
- const parsed = JSON.parse(rawText) as { error?: { message?: string } };
1290
- message = parsed.error?.message ?? message;
1291
- } catch {
1292
- // Keep raw text.
1311
+ const rawText = await withAuth(apiKey.apiKey, async key => {
1312
+ const resp = await fetch("https://openrouter.ai/api/v1/chat/completions", {
1313
+ method: "POST",
1314
+ headers: {
1315
+ "Content-Type": "application/json",
1316
+ Authorization: `Bearer ${key}`,
1317
+ "HTTP-Referer": "https://omp.sh/",
1318
+ "X-OpenRouter-Title": "Oh-My-Pi",
1319
+ "X-OpenRouter-Categories": "cli-agent",
1320
+ },
1321
+ body: JSON.stringify(requestBody),
1322
+ signal: requestSignal,
1323
+ });
1324
+ const text = await resp.text();
1325
+ if (!resp.ok) {
1326
+ let message = text;
1327
+ try {
1328
+ const parsed = JSON.parse(text) as { error?: { message?: string } };
1329
+ message = parsed.error?.message ?? message;
1330
+ } catch {
1331
+ // Keep raw text.
1332
+ }
1333
+ throw Object.assign(new Error(`OpenRouter image request failed (${resp.status}): ${message}`), {
1334
+ status: resp.status,
1335
+ });
1293
1336
  }
1294
- throw new Error(`OpenRouter image request failed (${response.status}): ${message}`);
1295
- }
1337
+ return text;
1338
+ });
1296
1339
 
1297
1340
  const data = JSON.parse(rawText) as OpenRouterResponse;
1298
1341
  const message = data.choices?.[0]?.message;
@@ -1360,30 +1403,34 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
1360
1403
  generationConfig,
1361
1404
  };
1362
1405
 
1363
- const response = await fetch(
1364
- `https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(model)}:generateContent`,
1365
- {
1366
- method: "POST",
1367
- headers: {
1368
- "Content-Type": "application/json",
1369
- "x-goog-api-key": apiKey.apiKey,
1406
+ const rawText = await withAuth(apiKey.apiKey, async key => {
1407
+ const resp = await fetch(
1408
+ `https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(model)}:generateContent`,
1409
+ {
1410
+ method: "POST",
1411
+ headers: {
1412
+ "Content-Type": "application/json",
1413
+ "x-goog-api-key": key,
1414
+ },
1415
+ body: JSON.stringify(requestBody),
1416
+ signal: requestSignal,
1370
1417
  },
1371
- body: JSON.stringify(requestBody),
1372
- signal: requestSignal,
1373
- },
1374
- );
1375
-
1376
- const rawText = await response.text();
1377
- if (!response.ok) {
1378
- let message = rawText;
1379
- try {
1380
- const parsed = JSON.parse(rawText) as { error?: { message?: string } };
1381
- message = parsed.error?.message ?? message;
1382
- } catch {
1383
- // Keep raw text.
1418
+ );
1419
+ const text = await resp.text();
1420
+ if (!resp.ok) {
1421
+ let message = text;
1422
+ try {
1423
+ const parsed = JSON.parse(text) as { error?: { message?: string } };
1424
+ message = parsed.error?.message ?? message;
1425
+ } catch {
1426
+ // Keep raw text.
1427
+ }
1428
+ throw Object.assign(new Error(`Gemini image request failed (${resp.status}): ${message}`), {
1429
+ status: resp.status,
1430
+ });
1384
1431
  }
1385
- throw new Error(`Gemini image request failed (${response.status}): ${message}`);
1386
- }
1432
+ return text;
1433
+ });
1387
1434
 
1388
1435
  const data = JSON.parse(rawText) as GeminiGenerateContentResponse;
1389
1436
  const responseParts = combineParts(data);