@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
@@ -16,9 +16,9 @@ import {
16
16
  searchDiscoverableTools,
17
17
  summarizeDiscoverableTools,
18
18
  } from "../tool-discovery/tool-index";
19
- import { renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
19
+ import { framedBlock, renderStatusLine, truncateToWidth } from "../tui";
20
20
  import type { ToolSession } from ".";
21
- import { formatCount, replaceTabs, TRUNCATE_LENGTHS } from "./render-utils";
21
+ import { formatCount, formatExpandHint, formatMoreItems, replaceTabs, TRUNCATE_LENGTHS } from "./render-utils";
22
22
  import { ToolError } from "./tool-errors";
23
23
 
24
24
  const DEFAULT_LIMIT = 8;
@@ -171,6 +171,25 @@ function renderMatchLines(match: SearchToolBm25Match, theme: Theme): string[] {
171
171
  return lines;
172
172
  }
173
173
 
174
+ function renderMatchBullets(tools: SearchToolBm25Match[], expanded: boolean, theme: Theme): string[] {
175
+ const shown = expanded ? tools.length : Math.min(tools.length, COLLAPSED_MATCH_LIMIT);
176
+ const bullet = theme.fg("dim", theme.format.bullet);
177
+ const lines: string[] = [];
178
+ for (let i = 0; i < shown; i++) {
179
+ const itemLines = renderMatchLines(tools[i]!, theme);
180
+ lines.push(`${bullet} ${itemLines[0]}`);
181
+ for (let j = 1; j < itemLines.length; j++) {
182
+ lines.push(` ${itemLines[j]}`);
183
+ }
184
+ }
185
+ const remaining = tools.length - shown;
186
+ if (remaining > 0) {
187
+ const hint = formatExpandHint(theme, expanded, true);
188
+ lines.push(`${theme.fg("muted", formatMoreItems(remaining, "tool"))}${hint ? ` ${hint}` : ""}`);
189
+ }
190
+ return lines;
191
+ }
192
+
174
193
  function renderFallbackResult(text: string, theme: Theme): Component {
175
194
  const header = renderStatusLine({ icon: "warning", title: TOOL_DISCOVERY_TITLE }, theme);
176
195
  const bodyLines = (text || "Tool discovery completed")
@@ -271,14 +290,11 @@ export const searchToolBm25Renderer = {
271
290
  renderCall(args: SearchToolBm25Params, _options: RenderResultOptions, uiTheme: Theme): Component {
272
291
  const query = typeof args.query === "string" ? replaceTabs(args.query.trim()) : "";
273
292
  const meta = args.limit ? [`limit:${args.limit}`] : [];
274
- return new Text(
275
- renderStatusLine(
276
- { icon: "pending", title: TOOL_DISCOVERY_TITLE, description: query || "(empty query)", meta },
277
- uiTheme,
278
- ),
279
- 0,
280
- 0,
293
+ const header = renderStatusLine(
294
+ { icon: "pending", title: TOOL_DISCOVERY_TITLE, description: query || "(empty query)", meta },
295
+ uiTheme,
281
296
  );
297
+ return new Text(header, 0, 0);
282
298
  },
283
299
 
284
300
  renderResult(
@@ -305,7 +321,9 @@ export const searchToolBm25Renderer = {
305
321
  const safeQuery = replaceTabs(details.query);
306
322
  const header = renderStatusLine(
307
323
  {
308
- icon: details.tools.length > 0 ? "success" : "warning",
324
+ ...(details.tools.length > 0
325
+ ? { iconOverride: uiTheme.fg("accent", uiTheme.symbol("icon.search")) }
326
+ : { icon: "warning" as const }),
309
327
  title: TOOL_DISCOVERY_TITLE,
310
328
  description: truncateToWidth(safeQuery, MATCH_LABEL_LEN),
311
329
  meta,
@@ -318,19 +336,14 @@ export const searchToolBm25Renderer = {
318
336
  return new Text(`${header}\n${uiTheme.fg("muted", emptyMessage)}`, 0, 0);
319
337
  }
320
338
 
321
- const lines = [header];
322
- const treeLines = renderTreeList(
323
- {
324
- items: details.tools,
325
- expanded: options.expanded,
326
- maxCollapsed: COLLAPSED_MATCH_LIMIT,
327
- itemType: "tool",
328
- renderItem: match => renderMatchLines(match, uiTheme),
329
- },
330
- uiTheme,
331
- );
332
- lines.push(...treeLines);
333
- return new Text(lines.join("\n"), 0, 0);
339
+ return framedBlock(uiTheme, width => ({
340
+ header,
341
+ sections: [{ lines: renderMatchBullets(details.tools, options.expanded ?? false, uiTheme) }],
342
+ state: "success",
343
+ borderColor: "borderMuted",
344
+ applyBg: false,
345
+ width,
346
+ }));
334
347
  },
335
348
 
336
349
  mergeCallAndResult: true,
@@ -19,6 +19,8 @@ import { DEFAULT_MAX_COLUMN, type TruncationResult, truncateHead, truncateLine }
19
19
  import {
20
20
  Ellipsis,
21
21
  fileHyperlink,
22
+ getTreeBranch,
23
+ getTreeContinuePrefix,
22
24
  renderStatusLine,
23
25
  renderTreeList,
24
26
  truncateToWidth,
@@ -34,9 +36,9 @@ import {
34
36
  parseArchivePathCandidates,
35
37
  } from "./archive-reader";
36
38
  import { createFileRecorder, formatResultPath } from "./file-recorder";
37
- import { formatGroupedFiles } from "./grouped-file-output";
39
+ import { classifyGroupedLines, formatGroupedFiles, groupLineIndicesByBlank } from "./grouped-file-output";
38
40
  import { formatMatchLine } from "./match-line-format";
39
- import { formatFullOutputReference, type OutputMeta } from "./output-meta";
41
+ import type { OutputMeta } from "./output-meta";
40
42
  import {
41
43
  expandDelimitedPathEntries,
42
44
  hasGlobPathChars,
@@ -56,8 +58,9 @@ import {
56
58
  formatCount,
57
59
  formatEmptyMessage,
58
60
  formatErrorMessage,
61
+ formatMoreItems,
59
62
  PREVIEW_LIMITS,
60
- splitGroupsByBlankLine,
63
+ replaceTabs,
61
64
  } from "./render-utils";
62
65
  import { ToolError } from "./tool-errors";
63
66
  import { toolResult } from "./tool-result";
@@ -283,7 +286,6 @@ interface IndexedContentLines {
283
286
  starts: number[];
284
287
  }
285
288
 
286
- const INTERNAL_URL_DISPLAY_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
287
289
  const OMP_ROOT_URL_RE = /^omp:\/\/(?:\/?|docs\/?)$/i;
288
290
 
289
291
  function normalizeSearchLine(line: string): string {
@@ -619,6 +621,10 @@ export interface SearchToolDetails {
619
621
  /** Absolute base directory used during search. Used by the renderer to resolve
620
622
  * display-relative paths to absolute paths for OSC 8 hyperlinks. */
621
623
  searchPath?: string;
624
+ /** Session cwd at search time. The renderer resolves the display-relative
625
+ * (cwd-relative) header/match paths against this for OSC 8 hyperlinks;
626
+ * `searchPath` is the scope label target, not the display-path base. */
627
+ cwd?: string;
622
628
  /** User-supplied paths whose base directory was missing on disk. The tool
623
629
  * skipped these and continued with the surviving entries; surfaced as a
624
630
  * non-fatal warning in the renderer and in the model-facing text. */
@@ -1001,6 +1007,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
1001
1007
  const details: SearchToolDetails = {
1002
1008
  scopePath,
1003
1009
  searchPath,
1010
+ cwd: this.session.cwd,
1004
1011
  matchCount: 0,
1005
1012
  fileCount: 0,
1006
1013
  files: [],
@@ -1127,6 +1134,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
1127
1134
  const details: SearchToolDetails = {
1128
1135
  scopePath,
1129
1136
  searchPath,
1137
+ cwd: this.session.cwd,
1130
1138
  matchCount: selectedMatches.length,
1131
1139
  fileCount: fileList.length,
1132
1140
  files: fileList,
@@ -1169,6 +1177,10 @@ interface SearchRenderArgs {
1169
1177
  }
1170
1178
 
1171
1179
  const COLLAPSED_TEXT_LIMIT = PREVIEW_LIMITS.COLLAPSED_LINES * 2;
1180
+ /** Line budget for the expanded view. Larger than collapsed so expanding
1181
+ * reveals more matches with context, but still bounded so a single hot file
1182
+ * whose matches span the whole file can't dump its entire length. */
1183
+ const EXPANDED_TEXT_LIMIT = PREVIEW_LIMITS.EXPANDED_LINES * 2;
1172
1184
 
1173
1185
  const SEARCH_CODE_FRAME_LINE_RE = /^\s*\*?(\d+)│/;
1174
1186
 
@@ -1190,6 +1202,140 @@ function parseSearchDisplayLineNumber(line: string): number | undefined {
1190
1202
  return Number.parseInt(match[1]!, 10);
1191
1203
  }
1192
1204
 
1205
+ const SEARCH_MATCH_LINE_RE = /^\s*\*\d+(?:│|[:|])/;
1206
+
1207
+ interface RenderedSearchLine {
1208
+ raw: string;
1209
+ styled: string;
1210
+ }
1211
+
1212
+ function isSearchMatchLine(line: string): boolean {
1213
+ return SEARCH_MATCH_LINE_RE.test(line);
1214
+ }
1215
+
1216
+ function isSearchHeaderLine(line: string): boolean {
1217
+ return /^#+ /.test(line);
1218
+ }
1219
+
1220
+ const URL_HEADER_PREFIX_RE = /^#+\s+/;
1221
+
1222
+ function renderSearchDisplayLines(
1223
+ lines: readonly string[],
1224
+ headerBase: string | undefined,
1225
+ fileScope: string | undefined,
1226
+ uiTheme: Theme,
1227
+ ): RenderedSearchLine[] {
1228
+ const contexts = classifyGroupedLines(lines, headerBase, fileScope);
1229
+ // `classifyGroupedLines` can't resolve internal URLs (TUI-only), so track the
1230
+ // resolved URL target here and use it for the body lines that follow.
1231
+ let urlFile: string | undefined;
1232
+ return lines.map((line, index) => {
1233
+ const ctx = contexts[index]!;
1234
+ if (ctx.kind === "dir") {
1235
+ urlFile = undefined;
1236
+ const styled = uiTheme.fg("accent", line);
1237
+ return { raw: line, styled: ctx.headerPath ? fileHyperlink(ctx.headerPath, styled) : styled };
1238
+ }
1239
+ if (ctx.kind === "file") {
1240
+ if (ctx.isUrl) {
1241
+ const raw = line
1242
+ .replace(URL_HEADER_PREFIX_RE, "")
1243
+ .trimEnd()
1244
+ .replace(/\s+\([^)]*\)\s*$/, "");
1245
+ const linked = linkUrlLikeSearchHeader(raw, uiTheme.fg("accent", line));
1246
+ urlFile = linked.absPath;
1247
+ return { raw: line, styled: linked.line };
1248
+ }
1249
+ urlFile = undefined;
1250
+ // Root-level files keep the bright accent; nested file headers are dimmed.
1251
+ const styled = uiTheme.fg(ctx.depth === 1 ? "accent" : "dim", line);
1252
+ return { raw: line, styled: ctx.headerPath ? fileHyperlink(ctx.headerPath, styled) : styled };
1253
+ }
1254
+ const styled = uiTheme.fg("toolOutput", line);
1255
+ const lineNumber = parseSearchDisplayLineNumber(line);
1256
+ const filePath = ctx.filePath ?? urlFile;
1257
+ return {
1258
+ raw: line,
1259
+ styled: filePath && lineNumber !== undefined ? fileHyperlink(filePath, styled, { line: lineNumber }) : styled,
1260
+ };
1261
+ });
1262
+ }
1263
+
1264
+ function compactSearchPreviewGroup(group: RenderedSearchLine[]): RenderedSearchLine[] {
1265
+ const compact = group.filter(line => isSearchHeaderLine(line.raw) || isSearchMatchLine(line.raw));
1266
+ return compact.length > 0 ? compact : group;
1267
+ }
1268
+
1269
+ function countPreviewMatches(lines: readonly RenderedSearchLine[], hasMarkedMatches: boolean): number {
1270
+ if (hasMarkedMatches) return lines.reduce((count, line) => count + (isSearchMatchLine(line.raw) ? 1 : 0), 0);
1271
+ return lines.reduce((count, line) => count + (!isSearchHeaderLine(line.raw) && line.raw.length > 0 ? 1 : 0), 0);
1272
+ }
1273
+
1274
+ function renderBudgetedSearchGroups(
1275
+ groups: RenderedSearchLine[][],
1276
+ maxLines: number,
1277
+ matchCount: number,
1278
+ uiTheme: Theme,
1279
+ compact: boolean,
1280
+ ): string[] {
1281
+ if (maxLines <= 0) return [];
1282
+ const renderedGroups = groups
1283
+ .map(group => (compact ? compactSearchPreviewGroup(group) : group))
1284
+ .filter(group => group.length > 0);
1285
+ if (renderedGroups.length === 0) return [];
1286
+
1287
+ let totalLines = 0;
1288
+ let totalMarkedMatches = 0;
1289
+ let totalFallbackMatches = 0;
1290
+ for (const group of renderedGroups) {
1291
+ totalLines += group.length;
1292
+ totalMarkedMatches += countPreviewMatches(group, true);
1293
+ totalFallbackMatches += countPreviewMatches(group, false);
1294
+ }
1295
+ const hasMarkedMatches = totalMarkedMatches > 0;
1296
+ const needsSummary = totalLines > maxLines;
1297
+ const contentBudget = needsSummary ? Math.max(maxLines - 1, 0) : maxLines;
1298
+ const visibleGroups: RenderedSearchLine[][] = [];
1299
+ let visibleLineCount = 0;
1300
+ let visibleMatches = 0;
1301
+ for (const group of renderedGroups) {
1302
+ if (visibleLineCount >= contentBudget) break;
1303
+ const available = contentBudget - visibleLineCount;
1304
+ const take = Math.min(group.length, available);
1305
+ if (take <= 0) break;
1306
+ const visibleGroup = group.slice(0, take);
1307
+ visibleGroups.push(visibleGroup);
1308
+ visibleLineCount += visibleGroup.length;
1309
+ visibleMatches += countPreviewMatches(visibleGroup, hasMarkedMatches);
1310
+ }
1311
+
1312
+ const totalMatches = hasMarkedMatches ? totalMarkedMatches : Math.max(matchCount, totalFallbackMatches);
1313
+ const hiddenMatches = Math.max(totalMatches - visibleMatches, 0);
1314
+ const hiddenLines = Math.max(totalLines - visibleLineCount, 0);
1315
+ const hasSummary = needsSummary && (hiddenMatches > 0 || hiddenLines > 0);
1316
+ const lines: string[] = [];
1317
+ for (let i = 0; i < visibleGroups.length; i++) {
1318
+ const group = visibleGroups[i]!;
1319
+ const isLast = !hasSummary && i === visibleGroups.length - 1;
1320
+ const prefix = `${uiTheme.fg("dim", getTreeBranch(isLast, uiTheme))} `;
1321
+ const continuePrefix = uiTheme.fg("dim", getTreeContinuePrefix(isLast, uiTheme));
1322
+ lines.push(`${prefix}${replaceTabs(group[0]!.styled)}`);
1323
+ for (let j = 1; j < group.length; j++) {
1324
+ lines.push(`${continuePrefix}${replaceTabs(group[j]!.styled)}`);
1325
+ }
1326
+ }
1327
+ if (hasSummary) {
1328
+ const hiddenLabel =
1329
+ hiddenMatches > 0 ? formatMoreItems(hiddenMatches, "match") : formatMoreItems(hiddenLines, "line");
1330
+ lines.push(`${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg("muted", hiddenLabel)}`);
1331
+ }
1332
+ return lines;
1333
+ }
1334
+
1335
+ function searchStatusIcon(uiTheme: Theme): string {
1336
+ return uiTheme.fg("toolTitle", uiTheme.symbol("icon.search"));
1337
+ }
1338
+
1193
1339
  export const searchToolRenderer = {
1194
1340
  inline: true,
1195
1341
  renderCall(args: SearchRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
@@ -1201,10 +1347,10 @@ export const searchToolRenderer = {
1201
1347
  if (args.skip !== undefined && args.skip > 0) meta.push(`skip:${args.skip}`);
1202
1348
 
1203
1349
  const text = renderStatusLine(
1204
- { icon: "pending", title: "Search", description: args.pattern || "?", meta },
1350
+ { icon: "pending", title: "Search", titleColor: "toolTitle", description: args.pattern || "?", meta },
1205
1351
  uiTheme,
1206
1352
  );
1207
- return new Text(text, 0, 0);
1353
+ return new Text(text, 1, 0);
1208
1354
  },
1209
1355
 
1210
1356
  renderResult(
@@ -1217,7 +1363,7 @@ export const searchToolRenderer = {
1217
1363
 
1218
1364
  if (result.isError || details?.error) {
1219
1365
  const errorText = details?.error || result.content?.find(c => c.type === "text")?.text || "Unknown error";
1220
- return new Text(formatErrorMessage(errorText, uiTheme), 0, 0);
1366
+ return new Text(formatErrorMessage(errorText, uiTheme), 1, 0);
1221
1367
  }
1222
1368
 
1223
1369
  const hasDetailedData = details?.matchCount !== undefined || details?.fileCount !== undefined;
@@ -1225,12 +1371,18 @@ export const searchToolRenderer = {
1225
1371
  if (!hasDetailedData) {
1226
1372
  const textContent = result.details?.displayContent ?? result.content?.find(c => c.type === "text")?.text;
1227
1373
  if (!textContent || textContent === "No matches found") {
1228
- return new Text(formatEmptyMessage("No matches found", uiTheme), 0, 0);
1374
+ return new Text(formatEmptyMessage("No matches found", uiTheme), 1, 0);
1229
1375
  }
1230
1376
  const lines = textContent.split("\n").filter(line => line.trim() !== "");
1231
1377
  const description = args?.pattern ?? undefined;
1232
1378
  const header = renderStatusLine(
1233
- { icon: "success", title: "Search", description, meta: [formatCount("item", lines.length)] },
1379
+ {
1380
+ iconOverride: searchStatusIcon(uiTheme),
1381
+ title: "Search",
1382
+ titleColor: "toolTitle",
1383
+ description,
1384
+ meta: [formatCount("item", lines.length)],
1385
+ },
1234
1386
  uiTheme,
1235
1387
  );
1236
1388
  return createCachedComponent(
@@ -1249,6 +1401,7 @@ export const searchToolRenderer = {
1249
1401
  );
1250
1402
  return [header, ...listLines].map(l => truncateToWidth(l, width, Ellipsis.Omit));
1251
1403
  },
1404
+ { paddingX: 1 },
1252
1405
  );
1253
1406
  }
1254
1407
 
@@ -1269,12 +1422,12 @@ export const searchToolRenderer = {
1269
1422
  const scopeMeta = searchScopeMeta(details);
1270
1423
  if (scopeMeta) meta.push(scopeMeta);
1271
1424
  const header = renderStatusLine(
1272
- { icon: "warning", title: "Search", description: args?.pattern, meta },
1425
+ { icon: "warning", title: "Search", titleColor: "toolTitle", description: args?.pattern, meta },
1273
1426
  uiTheme,
1274
1427
  );
1275
1428
  const lines = [header, formatEmptyMessage("No matches found", uiTheme)];
1276
1429
  if (missingNote) lines.push(missingNote);
1277
- return new Text(lines.join("\n"), 0, 0);
1430
+ return new Text(lines.join("\n"), 1, 0);
1278
1431
  }
1279
1432
 
1280
1433
  const summaryParts = [formatCount("match", matchCount), formatCount("file", fileCount)];
@@ -1284,104 +1437,45 @@ export const searchToolRenderer = {
1284
1437
  if (truncated) meta.push(uiTheme.fg("warning", "truncated"));
1285
1438
  const description = args?.pattern ?? undefined;
1286
1439
  const header = renderStatusLine(
1287
- { icon: truncated ? "warning" : "success", title: "Search", description, meta },
1440
+ {
1441
+ ...(truncated ? { icon: "warning" as const } : { iconOverride: searchStatusIcon(uiTheme) }),
1442
+ title: "Search",
1443
+ titleColor: "toolTitle",
1444
+ description,
1445
+ meta,
1446
+ },
1288
1447
  uiTheme,
1289
1448
  );
1290
1449
 
1291
1450
  const textContent = result.details?.displayContent ?? result.content?.find(c => c.type === "text")?.text ?? "";
1292
- const matchGroups = splitGroupsByBlankLine(textContent.split("\n"));
1293
-
1294
- const renderedFileLimit = details?.fileLimitReached;
1295
- const renderedPerFileLimit = details?.perFileLimitReached;
1296
- const truncationReasons: string[] = [];
1297
- if (renderedFileLimit) truncationReasons.push(`first ${renderedFileLimit} files (skip to paginate)`);
1298
- if (renderedPerFileLimit) truncationReasons.push(`first ${renderedPerFileLimit} matches per file`);
1299
- if (truncation) truncationReasons.push(truncation.truncatedBy === "lines" ? "line limit" : "size limit");
1300
- if (limits?.columnTruncated) truncationReasons.push(`line length ${limits.columnTruncated.maxColumn}`);
1301
- if (truncation?.artifactId) truncationReasons.push(formatFullOutputReference(truncation.artifactId));
1451
+ const allLines = textContent.split("\n");
1452
+ // Resolve hyperlinks once over the whole output so a nested directory stack
1453
+ // reconstructs correctly across blank-line group boundaries.
1454
+ // Header/match display paths are cwd-relative, so resolve them against cwd
1455
+ // (falling back to searchPath for legacy results that predate `cwd`); the
1456
+ // scoped file's absolute path seeds body lines in single-file searches.
1457
+ const renderedLines = renderSearchDisplayLines(
1458
+ allLines,
1459
+ details?.cwd ?? details?.searchPath,
1460
+ details?.searchPath,
1461
+ uiTheme,
1462
+ );
1463
+ const matchGroups = groupLineIndicesByBlank(allLines).map(indices => indices.map(i => renderedLines[i]!));
1302
1464
 
1303
1465
  const extraLines: string[] = [];
1304
- if (truncationReasons.length > 0) {
1305
- extraLines.push(uiTheme.fg("warning", `truncated: ${truncationReasons.join(", ")}`));
1306
- }
1307
1466
  if (missingNote) extraLines.push(missingNote);
1308
1467
 
1309
1468
  return createCachedComponent(
1310
1469
  () => options.expanded,
1311
1470
  width => {
1312
- const collapsedMatchLineBudget = Math.max(COLLAPSED_TEXT_LIMIT - extraLines.length, 0);
1313
- const searchBase = details?.searchPath;
1314
- const matchLines = renderTreeList(
1315
- {
1316
- items: matchGroups,
1317
- expanded: options.expanded,
1318
- maxCollapsed: matchGroups.length,
1319
- maxCollapsedLines: collapsedMatchLineBudget,
1320
- itemType: "match",
1321
- renderItem: group => {
1322
- // Track directory/file context within a group so headers and code-frame
1323
- // lines link to the backing file, with line-specific links for matches.
1324
- let contextDir = searchBase ?? "";
1325
- const hasFileHeader = group.some(line => line.startsWith("# "));
1326
- let currentFilePath: string | undefined = hasFileHeader ? undefined : searchBase;
1327
- return group.map(line => {
1328
- if (line.startsWith("## ")) {
1329
- // Strip optional ` (suffix)` and `#hash` before resolving.
1330
- const fileName = line
1331
- .slice(3)
1332
- .trimEnd()
1333
- .replace(/\s+\([^)]*\)\s*$/, "")
1334
- .replace(/#[0-9a-f]+$/, "");
1335
- const absPath = contextDir && fileName ? path.join(contextDir, fileName) : undefined;
1336
- currentFilePath = absPath;
1337
- const styled = uiTheme.fg("dim", line);
1338
- return absPath ? fileHyperlink(absPath, styled) : styled;
1339
- }
1340
- if (line.startsWith("# ")) {
1341
- const raw = line
1342
- .slice(2)
1343
- .trimEnd()
1344
- .replace(/\s+\([^)]*\)\s*$/, "");
1345
- if (INTERNAL_URL_DISPLAY_RE.test(raw)) {
1346
- contextDir = "";
1347
- const styled = uiTheme.fg("accent", line);
1348
- const linked = linkUrlLikeSearchHeader(raw, styled);
1349
- currentFilePath = linked.absPath;
1350
- return linked.line;
1351
- }
1352
- const isDirectory = raw.endsWith("/");
1353
- const name = isDirectory ? raw.replace(/\/$/, "") : raw.replace(/#[0-9a-f]+$/, "");
1354
- if (isDirectory) {
1355
- const absPath = searchBase
1356
- ? name === "."
1357
- ? searchBase
1358
- : path.join(searchBase, name)
1359
- : undefined;
1360
- if (absPath) {
1361
- contextDir = absPath;
1362
- }
1363
- currentFilePath = undefined;
1364
- const styled = uiTheme.fg("accent", line);
1365
- return absPath ? fileHyperlink(absPath, styled) : styled;
1366
- }
1367
- // Root-level file emitted by formatGroupedFiles when the directory is `.`.
1368
- const absPath = searchBase && name ? path.join(searchBase, name) : undefined;
1369
- currentFilePath = absPath;
1370
- const styled = uiTheme.fg("accent", line);
1371
- return absPath ? fileHyperlink(absPath, styled) : styled;
1372
- }
1373
- const styled = uiTheme.fg("toolOutput", line);
1374
- const lineNumber = parseSearchDisplayLineNumber(line);
1375
- return currentFilePath && lineNumber !== undefined
1376
- ? fileHyperlink(currentFilePath, styled, { line: lineNumber })
1377
- : styled;
1378
- });
1379
- },
1380
- },
1381
- uiTheme,
1471
+ const budget = Math.max(
1472
+ (options.expanded ? EXPANDED_TEXT_LIMIT : COLLAPSED_TEXT_LIMIT) - extraLines.length,
1473
+ 0,
1382
1474
  );
1475
+ const matchLines = renderBudgetedSearchGroups(matchGroups, budget, matchCount, uiTheme, !options.expanded);
1383
1476
  return [header, ...matchLines, ...extraLines].map(l => truncateToWidth(l, width, Ellipsis.Omit));
1384
1477
  },
1478
+ { paddingX: 1 },
1385
1479
  );
1386
1480
  },
1387
1481
  mergeCallAndResult: true,
@@ -5,6 +5,14 @@ import { ToolError } from "./tool-errors";
5
5
  const SQLITE_MAGIC = new Uint8Array([
6
6
  0x53, 0x51, 0x4c, 0x69, 0x74, 0x65, 0x20, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x20, 0x33, 0x00,
7
7
  ]);
8
+
9
+ export function looksLikeSqlite(bytes: Uint8Array): boolean {
10
+ if (bytes.byteLength < SQLITE_MAGIC.byteLength) return false;
11
+ for (const [index, byte] of SQLITE_MAGIC.entries()) {
12
+ if (bytes[index] !== byte) return false;
13
+ }
14
+ return true;
15
+ }
8
16
  const SQLITE_PATH_PATTERN = /\.(?:sqlite3?|db3?)(?=(?::|\?|$))/gi;
9
17
  const DEFAULT_QUERY_LIMIT = 20;
10
18
  const DEFAULT_SCHEMA_SAMPLE_LIMIT = 5;
@@ -443,18 +451,7 @@ export function parseSqlitePathCandidates(filePath: string): SqlitePathCandidate
443
451
 
444
452
  export async function isSqliteFile(absolutePath: string): Promise<boolean> {
445
453
  try {
446
- const bytes = await Bun.file(absolutePath).slice(0, SQLITE_MAGIC.byteLength).bytes();
447
- if (bytes.length !== SQLITE_MAGIC.byteLength) {
448
- return false;
449
- }
450
-
451
- for (const [index, byte] of SQLITE_MAGIC.entries()) {
452
- if (bytes[index] !== byte) {
453
- return false;
454
- }
455
- }
456
-
457
- return true;
454
+ return looksLikeSqlite(await Bun.file(absolutePath).slice(0, SQLITE_MAGIC.byteLength).bytes());
458
455
  } catch {
459
456
  return false;
460
457
  }