@oh-my-pi/pi-coding-agent 15.10.0 → 15.10.2

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 (238) hide show
  1. package/CHANGELOG.md +142 -1
  2. package/dist/types/cli/dry-balance-cli.d.ts +15 -1
  3. package/dist/types/cli/startup-cwd.d.ts +2 -0
  4. package/dist/types/commands/launch.d.ts +3 -0
  5. package/dist/types/commit/analysis/conventional.d.ts +2 -2
  6. package/dist/types/commit/analysis/summary.d.ts +2 -2
  7. package/dist/types/commit/changelog/generate.d.ts +2 -2
  8. package/dist/types/commit/changelog/index.d.ts +2 -2
  9. package/dist/types/commit/map-reduce/index.d.ts +3 -3
  10. package/dist/types/commit/map-reduce/map-phase.d.ts +2 -2
  11. package/dist/types/commit/map-reduce/reduce-phase.d.ts +2 -2
  12. package/dist/types/commit/model-selection.d.ts +10 -4
  13. package/dist/types/config/api-key-resolver.d.ts +34 -0
  14. package/dist/types/config/keybindings.d.ts +2 -2
  15. package/dist/types/config/model-provider-priority.d.ts +1 -0
  16. package/dist/types/config/model-registry.d.ts +17 -1
  17. package/dist/types/config/model-resolver.d.ts +4 -1
  18. package/dist/types/config/settings-schema.d.ts +9 -0
  19. package/dist/types/config/settings.d.ts +7 -2
  20. package/dist/types/dap/config.d.ts +14 -1
  21. package/dist/types/dap/types.d.ts +10 -0
  22. package/dist/types/debug/report-bundle.d.ts +3 -0
  23. package/dist/types/edit/file-snapshot-store.d.ts +18 -10
  24. package/dist/types/eval/py/__tests__/prelude.test.d.ts +1 -0
  25. package/dist/types/extensibility/extensions/types.d.ts +4 -1
  26. package/dist/types/lsp/client.d.ts +10 -0
  27. package/dist/types/lsp/utils.d.ts +3 -2
  28. package/dist/types/main.d.ts +3 -9
  29. package/dist/types/mcp/tool-bridge.d.ts +2 -0
  30. package/dist/types/modes/components/chat-block.d.ts +64 -0
  31. package/dist/types/modes/components/custom-editor.d.ts +4 -1
  32. package/dist/types/modes/components/overlay-box.d.ts +17 -0
  33. package/dist/types/modes/components/plan-review-overlay.d.ts +59 -0
  34. package/dist/types/modes/components/plan-toc.d.ts +41 -0
  35. package/dist/types/modes/components/read-tool-group.d.ts +2 -0
  36. package/dist/types/modes/components/status-line.d.ts +2 -0
  37. package/dist/types/modes/components/transcript-container.d.ts +11 -0
  38. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  39. package/dist/types/modes/controllers/event-controller.d.ts +17 -1
  40. package/dist/types/modes/controllers/extension-ui-controller.d.ts +0 -1
  41. package/dist/types/modes/controllers/input-controller.d.ts +1 -1
  42. package/dist/types/modes/controllers/streaming-reveal.d.ts +22 -0
  43. package/dist/types/modes/controllers/tan-command-controller.d.ts +6 -0
  44. package/dist/types/modes/interactive-mode.d.ts +16 -5
  45. package/dist/types/modes/magic-keywords.d.ts +1 -1
  46. package/dist/types/modes/markdown-prose.d.ts +1 -1
  47. package/dist/types/modes/theme/theme.d.ts +1 -1
  48. package/dist/types/modes/types.d.ts +21 -5
  49. package/dist/types/modes/utils/copy-targets.d.ts +21 -1
  50. package/dist/types/modes/workflow.d.ts +3 -3
  51. package/dist/types/plan-mode/approved-plan.d.ts +27 -8
  52. package/dist/types/plan-mode/plan-protection.d.ts +4 -4
  53. package/dist/types/sdk.d.ts +2 -0
  54. package/dist/types/session/agent-session.d.ts +21 -0
  55. package/dist/types/session/auth-storage.d.ts +1 -1
  56. package/dist/types/session/messages.d.ts +12 -0
  57. package/dist/types/session/session-manager.d.ts +8 -3
  58. package/dist/types/slash-commands/types.d.ts +4 -6
  59. package/dist/types/task/executor.d.ts +17 -0
  60. package/dist/types/task/index.d.ts +1 -0
  61. package/dist/types/task/render.d.ts +3 -2
  62. package/dist/types/tools/archive-reader.d.ts +5 -0
  63. package/dist/types/tools/ast-edit.d.ts +3 -0
  64. package/dist/types/tools/ast-grep.d.ts +3 -0
  65. package/dist/types/tools/bash.d.ts +1 -0
  66. package/dist/types/tools/eval.d.ts +8 -0
  67. package/dist/types/tools/find.d.ts +8 -4
  68. package/dist/types/tools/gh-cache-invalidation.d.ts +6 -0
  69. package/dist/types/tools/github-cache.d.ts +12 -0
  70. package/dist/types/tools/grouped-file-output.d.ts +95 -12
  71. package/dist/types/tools/memory-render.d.ts +4 -1
  72. package/dist/types/tools/path-utils.d.ts +8 -0
  73. package/dist/types/tools/plan-mode-guard.d.ts +8 -9
  74. package/dist/types/tools/render-utils.d.ts +5 -9
  75. package/dist/types/tools/search.d.ts +6 -2
  76. package/dist/types/tools/sqlite-reader.d.ts +1 -0
  77. package/dist/types/tools/todo.d.ts +3 -2
  78. package/dist/types/tools/write.d.ts +3 -0
  79. package/dist/types/tools/yield.d.ts +8 -0
  80. package/dist/types/tui/output-block.d.ts +16 -4
  81. package/dist/types/tui/status-line.d.ts +3 -0
  82. package/dist/types/utils/enhanced-paste.d.ts +20 -0
  83. package/dist/types/web/search/providers/kimi.d.ts +1 -1
  84. package/package.json +9 -9
  85. package/src/auto-thinking/classifier.ts +5 -1
  86. package/src/cli/args.ts +3 -1
  87. package/src/cli/dry-balance-cli.ts +54 -21
  88. package/src/cli/gallery-cli.ts +4 -1
  89. package/src/cli/gallery-fixtures/misc.ts +29 -0
  90. package/src/cli/startup-cwd.ts +68 -0
  91. package/src/commands/launch.ts +3 -0
  92. package/src/commit/analysis/conventional.ts +2 -2
  93. package/src/commit/analysis/summary.ts +2 -2
  94. package/src/commit/changelog/generate.ts +2 -2
  95. package/src/commit/changelog/index.ts +2 -2
  96. package/src/commit/map-reduce/index.ts +3 -3
  97. package/src/commit/map-reduce/map-phase.ts +2 -2
  98. package/src/commit/map-reduce/reduce-phase.ts +2 -2
  99. package/src/commit/model-selection.ts +36 -11
  100. package/src/commit/pipeline.ts +4 -4
  101. package/src/config/api-key-resolver.ts +58 -0
  102. package/src/config/model-provider-priority.ts +55 -0
  103. package/src/config/model-registry.ts +29 -24
  104. package/src/config/model-resolver.ts +39 -7
  105. package/src/config/settings-schema.ts +10 -0
  106. package/src/config/settings.ts +106 -43
  107. package/src/dap/config.ts +41 -2
  108. package/src/dap/defaults.json +1 -0
  109. package/src/dap/session.ts +1 -0
  110. package/src/dap/types.ts +10 -0
  111. package/src/debug/index.ts +47 -53
  112. package/src/debug/raw-sse-buffer.ts +7 -4
  113. package/src/debug/report-bundle.ts +9 -0
  114. package/src/edit/file-snapshot-store.ts +33 -1
  115. package/src/edit/hashline/filesystem.ts +2 -1
  116. package/src/edit/renderer.ts +82 -78
  117. package/src/eval/__tests__/llm-bridge.test.ts +110 -31
  118. package/src/eval/js/context-manager.ts +32 -15
  119. package/src/eval/llm-bridge.ts +22 -6
  120. package/src/eval/py/__tests__/prelude.test.ts +19 -0
  121. package/src/eval/py/executor.ts +23 -11
  122. package/src/eval/py/prelude.py +1 -1
  123. package/src/extensibility/extensions/types.ts +10 -1
  124. package/src/goals/tools/goal-tool.ts +36 -26
  125. package/src/internal-urls/docs-index.generated.ts +8 -8
  126. package/src/lsp/client.ts +23 -11
  127. package/src/lsp/config.ts +11 -1
  128. package/src/lsp/index.ts +61 -9
  129. package/src/lsp/utils.ts +3 -2
  130. package/src/main.ts +100 -72
  131. package/src/mcp/tool-bridge.ts +2 -0
  132. package/src/memories/index.ts +14 -7
  133. package/src/mnemopi/backend.ts +5 -1
  134. package/src/modes/acp/acp-agent.ts +33 -26
  135. package/src/modes/components/assistant-message.ts +2 -9
  136. package/src/modes/components/chat-block.ts +111 -0
  137. package/src/modes/components/copy-selector.ts +1 -44
  138. package/src/modes/components/custom-editor.ts +164 -109
  139. package/src/modes/components/custom-message.ts +1 -3
  140. package/src/modes/components/execution-shared.ts +1 -2
  141. package/src/modes/components/hook-message.ts +1 -3
  142. package/src/modes/components/model-selector.ts +59 -13
  143. package/src/modes/components/oauth-selector.ts +33 -7
  144. package/src/modes/components/overlay-box.ts +108 -0
  145. package/src/modes/components/plan-review-overlay.ts +799 -0
  146. package/src/modes/components/plan-toc.ts +138 -0
  147. package/src/modes/components/read-tool-group.ts +20 -4
  148. package/src/modes/components/skill-message.ts +0 -1
  149. package/src/modes/components/status-line.ts +19 -4
  150. package/src/modes/components/tips.txt +2 -1
  151. package/src/modes/components/todo-reminder.ts +0 -2
  152. package/src/modes/components/tool-execution.ts +68 -88
  153. package/src/modes/components/transcript-container.ts +84 -24
  154. package/src/modes/components/user-message.ts +2 -3
  155. package/src/modes/controllers/command-controller-shared.ts +7 -6
  156. package/src/modes/controllers/command-controller.ts +57 -55
  157. package/src/modes/controllers/event-controller.ts +67 -40
  158. package/src/modes/controllers/extension-ui-controller.ts +10 -73
  159. package/src/modes/controllers/input-controller.ts +170 -126
  160. package/src/modes/controllers/mcp-command-controller.ts +69 -60
  161. package/src/modes/controllers/selector-controller.ts +23 -25
  162. package/src/modes/controllers/streaming-reveal.ts +212 -0
  163. package/src/modes/controllers/tan-command-controller.ts +173 -0
  164. package/src/modes/interactive-mode.ts +274 -112
  165. package/src/modes/magic-keywords.ts +1 -1
  166. package/src/modes/markdown-prose.ts +1 -1
  167. package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
  168. package/src/modes/theme/shimmer.ts +20 -9
  169. package/src/modes/theme/theme-schema.json +1 -1
  170. package/src/modes/theme/theme.ts +8 -4
  171. package/src/modes/types.ts +21 -7
  172. package/src/modes/utils/copy-targets.ts +133 -27
  173. package/src/modes/utils/ui-helpers.ts +44 -46
  174. package/src/modes/workflow.ts +10 -10
  175. package/src/plan-mode/approved-plan.ts +66 -43
  176. package/src/plan-mode/plan-protection.ts +4 -4
  177. package/src/prompts/system/background-tan-dispatch.md +8 -0
  178. package/src/prompts/system/plan-mode-active.md +67 -58
  179. package/src/prompts/system/plan-mode-approved.md +1 -1
  180. package/src/prompts/system/workflow-notice.md +1 -1
  181. package/src/prompts/tools/bash.md +9 -0
  182. package/src/prompts/tools/browser.md +1 -1
  183. package/src/prompts/tools/eval.md +2 -1
  184. package/src/prompts/tools/read.md +2 -2
  185. package/src/sdk.ts +37 -46
  186. package/src/session/agent-session.ts +119 -18
  187. package/src/session/auth-storage.ts +2 -0
  188. package/src/session/messages.ts +26 -0
  189. package/src/session/session-manager.ts +109 -28
  190. package/src/slash-commands/builtin-registry.ts +36 -9
  191. package/src/slash-commands/types.ts +4 -6
  192. package/src/task/executor.ts +76 -38
  193. package/src/task/index.ts +4 -0
  194. package/src/task/render.ts +211 -147
  195. package/src/tools/archive-reader.ts +64 -0
  196. package/src/tools/ask.ts +119 -164
  197. package/src/tools/ast-edit.ts +98 -71
  198. package/src/tools/ast-grep.ts +37 -43
  199. package/src/tools/bash.ts +57 -6
  200. package/src/tools/browser/tab-supervisor.ts +13 -1
  201. package/src/tools/browser/tab-worker.ts +33 -4
  202. package/src/tools/debug.ts +20 -8
  203. package/src/tools/eval.ts +13 -2
  204. package/src/tools/fetch.ts +297 -7
  205. package/src/tools/find.ts +51 -30
  206. package/src/tools/gh-cache-invalidation.ts +200 -0
  207. package/src/tools/gh-renderer.ts +81 -42
  208. package/src/tools/github-cache.ts +25 -0
  209. package/src/tools/grouped-file-output.ts +272 -48
  210. package/src/tools/image-gen.ts +150 -103
  211. package/src/tools/inspect-image-renderer.ts +63 -41
  212. package/src/tools/inspect-image.ts +10 -3
  213. package/src/tools/job.ts +3 -4
  214. package/src/tools/memory-render.ts +4 -1
  215. package/src/tools/path-utils.ts +28 -2
  216. package/src/tools/plan-mode-guard.ts +66 -39
  217. package/src/tools/read.ts +48 -28
  218. package/src/tools/render-utils.ts +21 -37
  219. package/src/tools/resolve.ts +14 -0
  220. package/src/tools/search-tool-bm25.ts +36 -23
  221. package/src/tools/search.ts +118 -81
  222. package/src/tools/sqlite-reader.ts +9 -12
  223. package/src/tools/todo.ts +118 -52
  224. package/src/tools/write.ts +83 -64
  225. package/src/tools/yield.ts +10 -1
  226. package/src/tui/output-block.ts +60 -13
  227. package/src/tui/status-line.ts +5 -1
  228. package/src/utils/commit-message-generator.ts +11 -3
  229. package/src/utils/enhanced-paste.ts +230 -0
  230. package/src/utils/title-generator.ts +2 -1
  231. package/src/web/search/providers/anthropic.ts +25 -19
  232. package/src/web/search/providers/codex.ts +37 -8
  233. package/src/web/search/providers/exa.ts +11 -3
  234. package/src/web/search/providers/kimi.ts +28 -17
  235. package/src/web/search/providers/parallel.ts +35 -24
  236. package/src/web/search/providers/synthetic.ts +8 -6
  237. package/src/web/search/providers/tavily.ts +9 -8
  238. package/src/web/search/providers/zai.ts +8 -6
@@ -1,4 +1,6 @@
1
+ import { Database } from "bun:sqlite";
1
2
  import * as fs from "node:fs/promises";
3
+ import * as os from "node:os";
2
4
  import * as path from "node:path";
3
5
  import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
4
6
  import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
@@ -8,6 +10,7 @@ import { $which, ptree, truncate } from "@oh-my-pi/pi-utils";
8
10
  import { parseHTML } from "linkedom";
9
11
  import { LRUCache } from "lru-cache/raw";
10
12
  import type { Settings } from "../config/settings";
13
+ import { readEditableNotebookText } from "../edit/notebook";
11
14
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
12
15
  import { type Theme, theme } from "../modes/theme/theme";
13
16
  import type { ToolSession } from "../sdk";
@@ -22,10 +25,12 @@ import { specialHandlers } from "../web/scrapers";
22
25
  import type { RenderResult } from "../web/scrapers/types";
23
26
  import { finalizeOutput, loadPage, looksLikeHtml, MAX_OUTPUT_CHARS } from "../web/scrapers/types";
24
27
  import { convertWithMarkit, fetchBinary } from "../web/scrapers/utils";
28
+ import { type ArchiveFormat, listArchiveRoot, sniffArchiveFormat } from "./archive-reader";
25
29
  import { applyListLimit } from "./list-limit";
26
30
  import { formatStyledArtifactReference, type OutputMeta } from "./output-meta";
27
31
  import { type LineRange, parseLineRanges } from "./path-utils";
28
- import { formatExpandHint, getDomain, replaceTabs } from "./render-utils";
32
+ import { formatBytes, formatExpandHint, getDomain, replaceTabs } from "./render-utils";
33
+ import { listTables, looksLikeSqlite, renderTableList } from "./sqlite-reader";
29
34
  import { ToolAbortError, ToolError } from "./tool-errors";
30
35
  import { toolResult } from "./tool-result";
31
36
  import { clampTimeout } from "./tool-timeouts";
@@ -46,8 +51,6 @@ const CONVERTIBLE_MIMES = new Set([
46
51
  "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
47
52
  "application/rtf",
48
53
  "application/epub+zip",
49
- "application/x-ipynb+json",
50
- "application/zip",
51
54
  "image/png",
52
55
  "image/jpeg",
53
56
  "image/gif",
@@ -67,7 +70,6 @@ const CONVERTIBLE_EXTENSIONS = new Set([
67
70
  ".xlsx",
68
71
  ".rtf",
69
72
  ".epub",
70
- ".ipynb",
71
73
  ".png",
72
74
  ".jpg",
73
75
  ".jpeg",
@@ -78,6 +80,27 @@ const CONVERTIBLE_EXTENSIONS = new Set([
78
80
  ".ogg",
79
81
  ]);
80
82
 
83
+ const NOTEBOOK_MIMES = new Set(["application/x-ipynb+json"]);
84
+ const NOTEBOOK_EXTENSIONS = new Set([".ipynb"]);
85
+
86
+ const SQLITE_MIMES = new Set([
87
+ "application/vnd.sqlite3",
88
+ "application/x-sqlite3",
89
+ "application/sqlite3",
90
+ "application/sqlite",
91
+ ]);
92
+ const SQLITE_EXTENSIONS = new Set([".sqlite", ".sqlite3", ".db", ".db3"]);
93
+
94
+ const ARCHIVE_MIMES = new Set([
95
+ "application/zip",
96
+ "application/x-zip-compressed",
97
+ "application/x-tar",
98
+ "application/tar",
99
+ "application/gzip",
100
+ "application/x-gzip",
101
+ ]);
102
+ const ARCHIVE_EXTENSIONS = new Set([".zip", ".tar", ".tar.gz", ".tgz", ".gz"]);
103
+
81
104
  const IMAGE_MIME_BY_EXTENSION = new Map<string, string>([
82
105
  [".png", "image/png"],
83
106
  [".jpg", "image/jpeg"],
@@ -261,6 +284,12 @@ function normalizeMime(contentType: string): string {
261
284
  return contentType.split(";")[0].trim().toLowerCase();
262
285
  }
263
286
 
287
+ function getFilenameExtensionHint(filename: string): string {
288
+ const lower = filename.toLowerCase();
289
+ if (lower.endsWith(".tar.gz")) return ".tar.gz";
290
+ return path.extname(filename).toLowerCase();
291
+ }
292
+
264
293
  /**
265
294
  * Get extension from URL or Content-Disposition
266
295
  */
@@ -269,7 +298,7 @@ function getExtensionHint(url: string, contentDisposition?: string): string {
269
298
  if (contentDisposition) {
270
299
  const match = contentDisposition.match(/filename[*]?=["']?([^"';\n]+)/i);
271
300
  if (match) {
272
- const ext = path.extname(match[1]).toLowerCase();
301
+ const ext = getFilenameExtensionHint(match[1]);
273
302
  if (ext) return ext;
274
303
  }
275
304
  }
@@ -277,7 +306,7 @@ function getExtensionHint(url: string, contentDisposition?: string): string {
277
306
  // Fall back to URL path
278
307
  try {
279
308
  const pathname = new URL(url).pathname;
280
- const ext = path.extname(pathname).toLowerCase();
309
+ const ext = getFilenameExtensionHint(pathname);
281
310
  if (ext) return ext;
282
311
  } catch {}
283
312
 
@@ -738,6 +767,254 @@ type FetchRenderResult = RenderResult & {
738
767
  image?: FetchImagePayload;
739
768
  };
740
769
 
770
+ const BINARY_SAMPLE_CHARS = 4096;
771
+ const URL_ARCHIVE_LIST_LIMIT = 500;
772
+ const URL_SQLITE_LIST_LIMIT = 500;
773
+
774
+ function sampleLooksBinary(text: string): boolean {
775
+ const limit = Math.min(text.length, BINARY_SAMPLE_CHARS);
776
+ if (limit === 0) return false;
777
+
778
+ let replacementCount = 0;
779
+ for (let index = 0; index < limit; index++) {
780
+ const code = text.charCodeAt(index);
781
+ if (code === 0) return true;
782
+ if (code === 0xfffd) replacementCount++;
783
+ }
784
+
785
+ return replacementCount >= 3 && replacementCount / limit > 0.01;
786
+ }
787
+
788
+ function isNotebookHint(mime: string, extensionHint: string): boolean {
789
+ return NOTEBOOK_MIMES.has(mime) || NOTEBOOK_EXTENSIONS.has(extensionHint);
790
+ }
791
+
792
+ function isSqliteHint(mime: string, extensionHint: string): boolean {
793
+ return SQLITE_MIMES.has(mime) || SQLITE_EXTENSIONS.has(extensionHint);
794
+ }
795
+
796
+ function isArchiveHint(mime: string, extensionHint: string): boolean {
797
+ return ARCHIVE_MIMES.has(mime) || ARCHIVE_EXTENSIONS.has(extensionHint);
798
+ }
799
+
800
+ function getArchiveFormatHint(mime: string, extensionHint: string): ArchiveFormat | undefined {
801
+ if (extensionHint === ".zip" || mime === "application/zip" || mime === "application/x-zip-compressed") {
802
+ return "zip";
803
+ }
804
+ if (extensionHint === ".tar" || mime === "application/x-tar" || mime === "application/tar") {
805
+ return "tar";
806
+ }
807
+ if (
808
+ extensionHint === ".tar.gz" ||
809
+ extensionHint === ".tgz" ||
810
+ extensionHint === ".gz" ||
811
+ mime === "application/gzip" ||
812
+ mime === "application/x-gzip"
813
+ ) {
814
+ return "tar.gz";
815
+ }
816
+ return undefined;
817
+ }
818
+
819
+ function formatErrorMessage(error: unknown): string {
820
+ return error instanceof Error ? error.message : String(error);
821
+ }
822
+
823
+ function binaryContentType(mime: string): string {
824
+ return mime || "application/octet-stream";
825
+ }
826
+
827
+ function buildBinaryNotice(finalUrl: string, mime: string, byteLength?: number): string {
828
+ const size = byteLength === undefined ? "unknown size" : formatBytes(byteLength);
829
+ return `[Binary content: ${binaryContentType(mime)}, ${size}] ${finalUrl}`;
830
+ }
831
+
832
+ function buildBinaryPayloadResult(
833
+ url: string,
834
+ finalUrl: string,
835
+ mime: string,
836
+ method: string,
837
+ content: string,
838
+ fetchedAt: string,
839
+ notes: string[],
840
+ ): FetchRenderResult {
841
+ const output = finalizeOutput(content);
842
+ return {
843
+ url,
844
+ finalUrl,
845
+ contentType: binaryContentType(mime),
846
+ method,
847
+ content: output.content,
848
+ fetchedAt,
849
+ truncated: output.truncated,
850
+ notes,
851
+ };
852
+ }
853
+
854
+ async function withTempBinaryFile<T>(
855
+ prefix: string,
856
+ extension: string,
857
+ bytes: Uint8Array,
858
+ readTempFile: (tempPath: string) => Promise<T>,
859
+ ): Promise<T> {
860
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), prefix));
861
+ const tempPath = path.join(tempDir, `payload${extension}`);
862
+ try {
863
+ await Bun.write(tempPath, bytes);
864
+ return await readTempFile(tempPath);
865
+ } finally {
866
+ await fs.rm(tempDir, { recursive: true, force: true });
867
+ }
868
+ }
869
+
870
+ async function renderNotebookPayload(bytes: Uint8Array, displayUrl: string): Promise<string> {
871
+ return withTempBinaryFile("omp-url-notebook-", ".ipynb", bytes, tempPath =>
872
+ readEditableNotebookText(tempPath, displayUrl),
873
+ );
874
+ }
875
+
876
+ async function renderSqlitePayload(bytes: Uint8Array): Promise<string> {
877
+ return withTempBinaryFile("omp-url-sqlite-", ".sqlite", bytes, async tempPath => {
878
+ let db: Database | null = null;
879
+ try {
880
+ db = new Database(tempPath, { readonly: true, strict: true });
881
+ db.run("PRAGMA busy_timeout = 3000");
882
+ const listLimit = applyListLimit(listTables(db), { limit: URL_SQLITE_LIST_LIMIT });
883
+ return renderTableList(listLimit.items);
884
+ } finally {
885
+ db?.close();
886
+ }
887
+ });
888
+ }
889
+
890
+ async function tryRenderBinaryPayload(
891
+ url: string,
892
+ finalUrl: string,
893
+ mime: string,
894
+ extHint: string,
895
+ rawContent: string,
896
+ timeout: number,
897
+ signal: AbortSignal | undefined,
898
+ fetchedAt: string,
899
+ notes: readonly string[],
900
+ ): Promise<FetchRenderResult | null> {
901
+ const hasNotebookHint = isNotebookHint(mime, extHint);
902
+ const hasSqliteHint = isSqliteHint(mime, extHint);
903
+ const hasArchiveHint = isArchiveHint(mime, extHint);
904
+ const rawLooksBinary = sampleLooksBinary(rawContent);
905
+ if (!hasNotebookHint && !hasSqliteHint && !hasArchiveHint && !rawLooksBinary) {
906
+ return null;
907
+ }
908
+
909
+ const resultNotes = [...notes];
910
+ const binary = await fetchBinary(finalUrl, timeout, signal);
911
+ if (!binary.ok) {
912
+ resultNotes.push(binary.error ? `Binary fetch failed: ${binary.error}` : "Binary fetch failed");
913
+ return buildBinaryPayloadResult(
914
+ url,
915
+ finalUrl,
916
+ mime,
917
+ "binary",
918
+ buildBinaryNotice(finalUrl, mime),
919
+ fetchedAt,
920
+ resultNotes,
921
+ );
922
+ }
923
+
924
+ const binaryExtHint = getExtensionHint(finalUrl, binary.contentDisposition) || extHint;
925
+ if (isNotebookHint(mime, binaryExtHint)) {
926
+ try {
927
+ return buildBinaryPayloadResult(
928
+ url,
929
+ finalUrl,
930
+ mime,
931
+ "notebook",
932
+ await renderNotebookPayload(binary.buffer, finalUrl),
933
+ fetchedAt,
934
+ resultNotes,
935
+ );
936
+ } catch (error) {
937
+ resultNotes.push(`Notebook rendering failed: ${formatErrorMessage(error)}`);
938
+ return buildBinaryPayloadResult(
939
+ url,
940
+ finalUrl,
941
+ mime,
942
+ "binary",
943
+ buildBinaryNotice(finalUrl, mime, binary.buffer.byteLength),
944
+ fetchedAt,
945
+ resultNotes,
946
+ );
947
+ }
948
+ }
949
+
950
+ if (isSqliteHint(mime, binaryExtHint) || looksLikeSqlite(binary.buffer)) {
951
+ try {
952
+ return buildBinaryPayloadResult(
953
+ url,
954
+ finalUrl,
955
+ mime,
956
+ "sqlite",
957
+ await renderSqlitePayload(binary.buffer),
958
+ fetchedAt,
959
+ resultNotes,
960
+ );
961
+ } catch (error) {
962
+ resultNotes.push(`SQLite rendering failed: ${formatErrorMessage(error)}`);
963
+ return buildBinaryPayloadResult(
964
+ url,
965
+ finalUrl,
966
+ mime,
967
+ "binary",
968
+ buildBinaryNotice(finalUrl, mime, binary.buffer.byteLength),
969
+ fetchedAt,
970
+ resultNotes,
971
+ );
972
+ }
973
+ }
974
+
975
+ const hintedArchiveFormat = getArchiveFormatHint(mime, binaryExtHint);
976
+ const shouldArchiveSniff = hintedArchiveFormat !== undefined || !isConvertible(mime, binaryExtHint);
977
+ const archiveFormat = hintedArchiveFormat ?? (shouldArchiveSniff ? sniffArchiveFormat(binary.buffer) : undefined);
978
+ if (archiveFormat) {
979
+ try {
980
+ return buildBinaryPayloadResult(
981
+ url,
982
+ finalUrl,
983
+ mime,
984
+ "archive",
985
+ await listArchiveRoot(binary.buffer, archiveFormat, { limit: URL_ARCHIVE_LIST_LIMIT }),
986
+ fetchedAt,
987
+ resultNotes,
988
+ );
989
+ } catch (error) {
990
+ resultNotes.push(`Archive rendering failed: ${formatErrorMessage(error)}`);
991
+ return buildBinaryPayloadResult(
992
+ url,
993
+ finalUrl,
994
+ mime,
995
+ "binary",
996
+ buildBinaryNotice(finalUrl, mime, binary.buffer.byteLength),
997
+ fetchedAt,
998
+ resultNotes,
999
+ );
1000
+ }
1001
+ }
1002
+
1003
+ if (rawLooksBinary) {
1004
+ return buildBinaryPayloadResult(
1005
+ url,
1006
+ finalUrl,
1007
+ mime,
1008
+ "binary",
1009
+ buildBinaryNotice(finalUrl, mime, binary.buffer.byteLength),
1010
+ fetchedAt,
1011
+ resultNotes,
1012
+ );
1013
+ }
1014
+
1015
+ return null;
1016
+ }
1017
+
741
1018
  // =============================================================================
742
1019
  // Unified Special Handler Dispatch
743
1020
  // =============================================================================
@@ -984,6 +1261,19 @@ async function renderUrl(
984
1261
  }
985
1262
  }
986
1263
 
1264
+ const binaryPayloadResult = await tryRenderBinaryPayload(
1265
+ url,
1266
+ finalUrl,
1267
+ mime,
1268
+ extHint,
1269
+ rawContent,
1270
+ timeout,
1271
+ signal,
1272
+ fetchedAt,
1273
+ notes,
1274
+ );
1275
+ if (binaryPayloadResult) return binaryPayloadResult;
1276
+
987
1277
  // Step 4: Handle non-HTML text content
988
1278
  const isHtml = mime.includes("html") || mime.includes("xhtml");
989
1279
  const isJson = mime.includes("json");
@@ -992,7 +1282,7 @@ async function renderUrl(
992
1282
  const isFeed = mime.includes("rss") || mime.includes("atom") || mime.includes("feed");
993
1283
 
994
1284
  // Raw mode skips every text-shaping branch below (JSON pretty-print, feed-to-markdown,
995
- // HTML extraction) and returns the response body verbatim. The image/markit branches
1285
+ // HTML extraction) and returns the response body verbatim. Binary-oriented branches
996
1286
  // above already ran because raw isn't useful for binary payloads.
997
1287
  if (raw) {
998
1288
  const output = finalizeOutput(rawContent);
package/src/tools/find.ts CHANGED
@@ -13,6 +13,7 @@ import findDescription from "../prompts/tools/find.md" with { type: "text" };
13
13
  import { type TruncationResult, truncateHead } from "../session/streaming-output";
14
14
  import { Ellipsis, fileHyperlink, renderFileList, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
15
15
  import type { ToolSession } from ".";
16
+ import { buildPathTree, walkPathTree } from "./grouped-file-output";
16
17
  import { applyListLimit } from "./list-limit";
17
18
  import { formatFullOutputReference, type OutputMeta } from "./output-meta";
18
19
  import {
@@ -54,34 +55,27 @@ const MIN_GLOB_TIMEOUT_MS = 500;
54
55
  const MAX_GLOB_TIMEOUT_MS = 60_000;
55
56
 
56
57
  /**
57
- * Group find matches by their directory so the model doesn't pay repeated
58
- * tokens for shared path prefixes. Preserves the input order: groups appear in
59
- * the order their first member was emitted (mtime-desc for native glob), and
60
- * within a group entries keep their relative order.
58
+ * Group find matches into a multi-level directory tree so the model doesn't pay
59
+ * repeated tokens for shared path prefixes. Single-child directory chains fold
60
+ * into one header (`# a/b/c/`), so a common prefix including an absolute root
61
+ * for out-of-cwd results — collapses to a single line. Each level adds one `#`;
62
+ * files are listed bare under the deepest directory header that owns them.
63
+ *
64
+ * Order follows the input (mtime-desc for native glob): a directory appears when
65
+ * its first member is emitted, and a node's own files precede its subdirectories.
61
66
  */
62
67
  export function formatFindGroupedOutput(paths: readonly string[]): string {
63
68
  if (paths.length === 0) return "";
64
- const groups = new Map<string, string[]>();
65
- for (const entry of paths) {
66
- const hasTrailingSlash = entry.endsWith("/");
67
- const trimmed = hasTrailingSlash ? entry.slice(0, -1) : entry;
68
- const slash = trimmed.lastIndexOf("/");
69
- const dir = slash === -1 ? "" : trimmed.slice(0, slash);
70
- const base = slash === -1 ? trimmed : trimmed.slice(slash + 1);
71
- const label = hasTrailingSlash ? `${base}/` : base;
72
- const list = groups.get(dir);
73
- if (list) list.push(label);
74
- else groups.set(dir, [label]);
75
- }
76
- const sections: string[] = [];
77
- for (const [dir, entries] of groups) {
78
- if (dir === "") {
79
- sections.push(entries.join("\n"));
69
+ const tree = buildPathTree(paths.map(entry => ({ path: entry, isDir: entry.endsWith("/") })));
70
+ const lines: string[] = [];
71
+ for (const event of walkPathTree(tree)) {
72
+ if (event.kind === "dir") {
73
+ lines.push(`${"#".repeat(event.depth + 1)} ${event.name}/`);
80
74
  } else {
81
- sections.push(`# ${dir}/\n${entries.join("\n")}`);
75
+ lines.push(event.name);
82
76
  }
83
77
  }
84
- return sections.join("\n\n");
78
+ return lines.join("\n");
85
79
  }
86
80
 
87
81
  export interface FindToolDetails {
@@ -359,6 +353,13 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
359
353
  maxResults: effectiveLimit,
360
354
  sortByMtime: true,
361
355
  gitignore: useGitignore,
356
+ // parseFindPattern explicitly prepends "**/" when the user's
357
+ // pattern begins with a glob (so `*.ts` becomes `**/*.ts`).
358
+ // Anything that arrives here without "**/" was scoped to a
359
+ // single directory by the user (e.g. `dir/*`); disable the
360
+ // native auto-recursion so `dir/*` does not silently match
361
+ // `dir/sub/nested.ts`.
362
+ recursive: false,
362
363
  signal: combinedSignal,
363
364
  },
364
365
  onMatch,
@@ -434,6 +435,10 @@ function formatFindRenderPaths(paths: FindRenderArgs["paths"]): string | undefin
434
435
 
435
436
  const COLLAPSED_LIST_LIMIT = PREVIEW_LIMITS.COLLAPSED_ITEMS;
436
437
 
438
+ function findStatusIcon(uiTheme: Theme): string {
439
+ return uiTheme.fg("toolTitle", uiTheme.symbol("icon.search"));
440
+ }
441
+
437
442
  export const findToolRenderer = {
438
443
  inline: true,
439
444
  renderCall(args: FindRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
@@ -441,10 +446,16 @@ export const findToolRenderer = {
441
446
  if (args.limit !== undefined) meta.push(`limit:${args.limit}`);
442
447
 
443
448
  const text = renderStatusLine(
444
- { icon: "pending", title: "Find", description: formatFindRenderPaths(args.paths) || "*", meta },
449
+ {
450
+ icon: "pending",
451
+ title: "Find",
452
+ titleColor: "toolTitle",
453
+ description: formatFindRenderPaths(args.paths) || "*",
454
+ meta,
455
+ },
445
456
  uiTheme,
446
457
  );
447
- return new Text(text, 0, 0);
458
+ return new Text(text, 1, 0);
448
459
  },
449
460
 
450
461
  renderResult(
@@ -457,7 +468,7 @@ export const findToolRenderer = {
457
468
 
458
469
  if (result.isError || details?.error) {
459
470
  const errorText = details?.error || result.content?.find(c => c.type === "text")?.text || "Unknown error";
460
- return new Text(formatErrorMessage(errorText, uiTheme), 0, 0);
471
+ return new Text(formatErrorMessage(errorText, uiTheme), 1, 0);
461
472
  }
462
473
 
463
474
  const hasDetailedData = details?.fileCount !== undefined;
@@ -470,14 +481,15 @@ export const findToolRenderer = {
470
481
  textContent.includes("No files found") ||
471
482
  textContent.trim() === ""
472
483
  ) {
473
- return new Text(formatEmptyMessage("No files found", uiTheme), 0, 0);
484
+ return new Text(formatEmptyMessage("No files found", uiTheme), 1, 0);
474
485
  }
475
486
 
476
487
  const lines = textContent.split("\n").filter(l => l.trim());
477
488
  const header = renderStatusLine(
478
489
  {
479
- icon: "success",
490
+ iconOverride: findStatusIcon(uiTheme),
480
491
  title: "Find",
492
+ titleColor: "toolTitle",
481
493
  description: formatFindRenderPaths(args?.paths),
482
494
  meta: [formatCount("file", lines.length)],
483
495
  },
@@ -498,6 +510,7 @@ export const findToolRenderer = {
498
510
  );
499
511
  return [header, ...listLines].map(l => truncateToWidth(l, width, Ellipsis.Omit));
500
512
  },
513
+ { paddingX: 1 },
501
514
  );
502
515
  }
503
516
 
@@ -513,20 +526,27 @@ export const findToolRenderer = {
513
526
 
514
527
  if (fileCount === 0) {
515
528
  const header = renderStatusLine(
516
- { icon: "warning", title: "Find", description: formatFindRenderPaths(args?.paths), meta: ["0 files"] },
529
+ {
530
+ icon: "warning",
531
+ title: "Find",
532
+ titleColor: "toolTitle",
533
+ description: formatFindRenderPaths(args?.paths),
534
+ meta: ["0 files"],
535
+ },
517
536
  uiTheme,
518
537
  );
519
538
  const lines = [header, formatEmptyMessage("No files found", uiTheme)];
520
539
  if (missingNote) lines.push(missingNote);
521
- return new Text(lines.join("\n"), 0, 0);
540
+ return new Text(lines.join("\n"), 1, 0);
522
541
  }
523
542
  const meta: string[] = [formatCount("file", fileCount)];
524
543
  if (details?.scopePath) meta.push(`in ${details.scopePath}`);
525
544
  if (truncated) meta.push(uiTheme.fg("warning", "truncated"));
526
545
  const header = renderStatusLine(
527
546
  {
528
- icon: truncated ? "warning" : "success",
547
+ ...(truncated ? { icon: "warning" as const } : { iconOverride: findStatusIcon(uiTheme) }),
529
548
  title: "Find",
549
+ titleColor: "toolTitle",
530
550
  description: formatFindRenderPaths(args?.paths),
531
551
  meta,
532
552
  },
@@ -565,6 +585,7 @@ export const findToolRenderer = {
565
585
  );
566
586
  return [header, ...fileLines, ...extraLines].map(l => truncateToWidth(l, width, Ellipsis.Omit));
567
587
  },
588
+ { paddingX: 1 },
568
589
  );
569
590
  },
570
591
  mergeCallAndResult: true,