@oh-my-pi/pi-coding-agent 16.1.23 → 16.2.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 (260) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/dist/cli.js +3785 -3695
  3. package/dist/types/advisor/__tests__/emission-guard.test.d.ts +1 -0
  4. package/dist/types/advisor/emission-guard.d.ts +73 -0
  5. package/dist/types/advisor/index.d.ts +1 -0
  6. package/dist/types/advisor/runtime.d.ts +7 -0
  7. package/dist/types/advisor/watchdog.d.ts +13 -0
  8. package/dist/types/cli/flag-tables.d.ts +1 -0
  9. package/dist/types/cli/gallery-fixtures/search.d.ts +1 -1
  10. package/dist/types/collab/display-name.d.ts +3 -0
  11. package/dist/types/collab/host.d.ts +0 -2
  12. package/dist/types/commands/worktree.d.ts +0 -3
  13. package/dist/types/config/inline-tool-descriptors-mode.d.ts +13 -0
  14. package/dist/types/config/model-discovery.d.ts +6 -1
  15. package/dist/types/config/model-registry.d.ts +6 -2
  16. package/dist/types/config/model-resolver.d.ts +12 -0
  17. package/dist/types/config/models-config-schema.d.ts +29 -2
  18. package/dist/types/config/models-config.d.ts +22 -1
  19. package/dist/types/config/settings-schema.d.ts +91 -21
  20. package/dist/types/config/settings.d.ts +2 -0
  21. package/dist/types/dap/config.d.ts +1 -1
  22. package/dist/types/edit/hashline/filesystem.d.ts +4 -2
  23. package/dist/types/edit/renderer.d.ts +4 -0
  24. package/dist/types/extensibility/extensions/types.d.ts +17 -17
  25. package/dist/types/extensibility/hooks/types.d.ts +10 -10
  26. package/dist/types/extensibility/plugins/marketplace/types.d.ts +1 -0
  27. package/dist/types/internal-urls/__tests__/ssh-protocol.test.d.ts +1 -0
  28. package/dist/types/internal-urls/index.d.ts +1 -0
  29. package/dist/types/internal-urls/registry-helpers.d.ts +2 -0
  30. package/dist/types/internal-urls/router.d.ts +1 -1
  31. package/dist/types/internal-urls/ssh-protocol.d.ts +10 -0
  32. package/dist/types/internal-urls/types.d.ts +19 -2
  33. package/dist/types/irc/bus.d.ts +6 -0
  34. package/dist/types/modes/acp/acp-event-mapper.d.ts +1 -0
  35. package/dist/types/modes/components/__tests__/move-overlay.test.d.ts +1 -0
  36. package/dist/types/modes/components/custom-editor.d.ts +7 -0
  37. package/dist/types/modes/components/move-overlay.d.ts +23 -0
  38. package/dist/types/modes/components/plugin-selector.d.ts +2 -1
  39. package/dist/types/modes/components/queue-mode-selector.d.ts +2 -1
  40. package/dist/types/modes/components/select-list-mouse-routing.d.ts +2 -0
  41. package/dist/types/modes/components/settings-defs.d.ts +4 -1
  42. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  43. package/dist/types/modes/components/show-images-selector.d.ts +2 -1
  44. package/dist/types/modes/components/status-line/component.d.ts +2 -0
  45. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  46. package/dist/types/modes/components/theme-selector.d.ts +2 -1
  47. package/dist/types/modes/components/thinking-selector.d.ts +2 -1
  48. package/dist/types/modes/controllers/command-controller.d.ts +10 -1
  49. package/dist/types/modes/controllers/streaming-reveal.d.ts +6 -0
  50. package/dist/types/modes/interactive-mode.d.ts +10 -1
  51. package/dist/types/modes/internal-url-autocomplete.d.ts +1 -1
  52. package/dist/types/modes/running-subagent-badge.d.ts +6 -0
  53. package/dist/types/modes/theme/theme.d.ts +1 -1
  54. package/dist/types/modes/types.d.ts +9 -1
  55. package/dist/types/sdk.d.ts +2 -2
  56. package/dist/types/session/agent-session.d.ts +8 -1
  57. package/dist/types/session/blob-store.d.ts +4 -0
  58. package/dist/types/session/session-manager.d.ts +13 -0
  59. package/dist/types/slash-commands/builtin-registry.d.ts +1 -1
  60. package/dist/types/ssh/__tests__/connection-manager-args.test.d.ts +1 -0
  61. package/dist/types/ssh/__tests__/file-transfer-posix-guard.test.d.ts +1 -0
  62. package/dist/types/ssh/connection-manager.d.ts +2 -0
  63. package/dist/types/ssh/file-transfer.d.ts +79 -0
  64. package/dist/types/ssh/utils.d.ts +6 -0
  65. package/dist/types/system-prompt.d.ts +3 -0
  66. package/dist/types/task/executor.d.ts +16 -0
  67. package/dist/types/tiny/text.d.ts +1 -1
  68. package/dist/types/tools/builtin-names.d.ts +5 -1
  69. package/dist/types/tools/{find.d.ts → glob.d.ts} +15 -15
  70. package/dist/types/tools/{search.d.ts → grep.d.ts} +14 -14
  71. package/dist/types/tools/index.d.ts +3 -3
  72. package/dist/types/tools/path-utils.d.ts +29 -0
  73. package/dist/types/tools/read.d.ts +2 -2
  74. package/dist/types/tools/render-utils.d.ts +8 -0
  75. package/dist/types/tools/renderers.d.ts +11 -0
  76. package/dist/types/tools/ssh.d.ts +2 -0
  77. package/dist/types/tools/todo.d.ts +0 -16
  78. package/dist/types/tools/write.d.ts +2 -2
  79. package/dist/types/utils/active-repo-context.d.ts +8 -0
  80. package/dist/types/utils/image-resize.d.ts +1 -0
  81. package/dist/types/utils/markit-cache.d.ts +23 -0
  82. package/dist/types/utils/markit.d.ts +5 -1
  83. package/dist/types/utils/prompt-path.d.ts +1 -0
  84. package/dist/types/web/search/providers/duckduckgo.d.ts +14 -0
  85. package/dist/types/web/search/providers/firecrawl.d.ts +28 -0
  86. package/dist/types/web/search/providers/tinyfish.d.ts +29 -0
  87. package/dist/types/web/search/providers/xai.d.ts +13 -0
  88. package/dist/types/web/search/types.d.ts +18 -2
  89. package/package.json +30 -15
  90. package/scripts/build-binary.ts +9 -9
  91. package/scripts/bundle-dist.ts +2 -2
  92. package/src/advisor/__tests__/advisor.test.ts +61 -4
  93. package/src/advisor/__tests__/emission-guard.test.ts +147 -0
  94. package/src/advisor/advise-tool.ts +1 -1
  95. package/src/advisor/emission-guard.ts +172 -0
  96. package/src/advisor/index.ts +1 -0
  97. package/src/advisor/runtime.ts +11 -0
  98. package/src/advisor/watchdog.ts +27 -1
  99. package/src/cli/args.ts +5 -2
  100. package/src/cli/auth-broker-cli.ts +17 -0
  101. package/src/cli/config-cli.ts +4 -0
  102. package/src/cli/flag-tables.ts +7 -4
  103. package/src/cli/gallery-cli.ts +14 -2
  104. package/src/cli/gallery-fixtures/edit.ts +60 -0
  105. package/src/cli/gallery-fixtures/fs.ts +17 -17
  106. package/src/cli/gallery-fixtures/search.ts +4 -4
  107. package/src/cli/usage-cli.ts +20 -1
  108. package/src/cli/web-search-cli.ts +1 -1
  109. package/src/collab/display-name.ts +13 -0
  110. package/src/collab/guest.ts +5 -1
  111. package/src/collab/host.ts +2 -13
  112. package/src/commands/worktree.ts +6 -0
  113. package/src/config/inline-tool-descriptors-mode.ts +27 -0
  114. package/src/config/model-discovery.ts +77 -8
  115. package/src/config/model-registry.ts +89 -11
  116. package/src/config/model-resolver.ts +39 -1
  117. package/src/config/models-config-schema.ts +41 -5
  118. package/src/config/models-config.ts +4 -1
  119. package/src/config/settings-schema.ts +85 -27
  120. package/src/config/settings.ts +172 -1
  121. package/src/cursor.ts +1 -1
  122. package/src/dap/config.ts +152 -8
  123. package/src/dap/session.ts +1 -1
  124. package/src/discovery/claude-plugins.ts +3 -2
  125. package/src/discovery/helpers.ts +3 -1
  126. package/src/edit/hashline/diff.ts +14 -3
  127. package/src/edit/hashline/execute.ts +42 -6
  128. package/src/edit/hashline/filesystem.ts +38 -2
  129. package/src/edit/index.ts +1 -0
  130. package/src/edit/modes/patch.ts +11 -1
  131. package/src/edit/renderer.ts +140 -13
  132. package/src/eval/__tests__/agent-bridge.test.ts +101 -0
  133. package/src/eval/agent-bridge.ts +26 -3
  134. package/src/eval/js/tool-bridge.ts +2 -2
  135. package/src/export/html/index.ts +1 -1
  136. package/src/export/html/template.js +3 -1
  137. package/src/export/html/tool-views.generated.js +20 -20
  138. package/src/extensibility/extensions/types.ts +22 -22
  139. package/src/extensibility/hooks/types.ts +11 -11
  140. package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +8 -3
  141. package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +25 -10
  142. package/src/extensibility/plugins/marketplace/manager.ts +22 -0
  143. package/src/extensibility/plugins/marketplace/types.ts +1 -0
  144. package/src/internal-urls/__tests__/ssh-protocol.test.ts +331 -0
  145. package/src/internal-urls/docs-index.generated.txt +2 -2
  146. package/src/internal-urls/docs-index.ts +2 -3
  147. package/src/internal-urls/index.ts +1 -0
  148. package/src/internal-urls/registry-helpers.ts +19 -4
  149. package/src/internal-urls/router.ts +5 -3
  150. package/src/internal-urls/ssh-protocol.ts +367 -0
  151. package/src/internal-urls/types.ts +19 -2
  152. package/src/irc/bus.ts +11 -3
  153. package/src/mcp/tool-bridge.ts +32 -2
  154. package/src/memories/index.ts +1 -1
  155. package/src/modes/acp/acp-agent.ts +14 -1
  156. package/src/modes/acp/acp-event-mapper.ts +91 -27
  157. package/src/modes/components/__tests__/move-overlay.test.ts +166 -0
  158. package/src/modes/components/agent-hub.ts +3 -0
  159. package/src/modes/components/agent-transcript-viewer.ts +9 -7
  160. package/src/modes/components/custom-editor.ts +90 -5
  161. package/src/modes/components/move-overlay.ts +282 -0
  162. package/src/modes/components/plan-review-overlay.ts +35 -35
  163. package/src/modes/components/plugin-selector.ts +6 -1
  164. package/src/modes/components/queue-mode-selector.ts +6 -1
  165. package/src/modes/components/select-list-mouse-routing.ts +35 -0
  166. package/src/modes/components/session-selector.ts +11 -10
  167. package/src/modes/components/settings-defs.ts +14 -1
  168. package/src/modes/components/settings-selector.ts +196 -29
  169. package/src/modes/components/show-images-selector.ts +6 -1
  170. package/src/modes/components/status-line/component.ts +108 -28
  171. package/src/modes/components/status-line/segments.ts +5 -1
  172. package/src/modes/components/status-line/types.ts +2 -0
  173. package/src/modes/components/theme-selector.ts +6 -1
  174. package/src/modes/components/thinking-selector.ts +6 -1
  175. package/src/modes/components/tool-execution.ts +25 -9
  176. package/src/modes/components/tree-selector.ts +5 -5
  177. package/src/modes/controllers/command-controller.ts +140 -47
  178. package/src/modes/controllers/event-controller.ts +59 -3
  179. package/src/modes/controllers/input-controller.ts +68 -2
  180. package/src/modes/controllers/selector-controller.ts +5 -2
  181. package/src/modes/controllers/streaming-reveal.ts +17 -0
  182. package/src/modes/controllers/tool-args-reveal.ts +1 -1
  183. package/src/modes/interactive-mode.ts +116 -50
  184. package/src/modes/internal-url-autocomplete.ts +17 -2
  185. package/src/modes/prompt-action-autocomplete.ts +3 -1
  186. package/src/modes/running-subagent-badge.ts +13 -0
  187. package/src/modes/setup-wizard/scenes/glyph.ts +2 -13
  188. package/src/modes/setup-wizard/scenes/providers.ts +2 -1
  189. package/src/modes/setup-wizard/scenes/theme.ts +6 -12
  190. package/src/modes/setup-wizard/scenes/web-search.ts +8 -13
  191. package/src/modes/setup-wizard/wizard-overlay.ts +6 -5
  192. package/src/modes/theme/theme.ts +9 -1
  193. package/src/modes/types.ts +9 -1
  194. package/src/modes/utils/interactive-context-helpers.ts +1 -1
  195. package/src/priority.json +15 -0
  196. package/src/prompts/advisor/active-repo-watchdog.md +6 -0
  197. package/src/prompts/advisor/context-files.md +8 -0
  198. package/src/prompts/advisor/system.md +21 -7
  199. package/src/prompts/agents/designer.md +1 -1
  200. package/src/prompts/agents/explore.md +1 -1
  201. package/src/prompts/agents/librarian.md +2 -2
  202. package/src/prompts/agents/plan.md +2 -2
  203. package/src/prompts/agents/reviewer.md +1 -1
  204. package/src/prompts/agents/task.md +2 -2
  205. package/src/prompts/system/active-repo-context.md +4 -0
  206. package/src/prompts/system/gemini-tool-call-reminder.md +9 -0
  207. package/src/prompts/system/plan-mode-active.md +3 -3
  208. package/src/prompts/system/project-prompt.md +2 -2
  209. package/src/prompts/system/system-prompt.md +6 -6
  210. package/src/prompts/tools/bash.md +2 -2
  211. package/src/prompts/tools/checkpoint.md +1 -1
  212. package/src/prompts/tools/{find.md → glob.md} +1 -1
  213. package/src/prompts/tools/{search.md → grep.md} +3 -3
  214. package/src/prompts/tools/read.md +4 -2
  215. package/src/sdk.ts +65 -25
  216. package/src/session/agent-session.ts +519 -132
  217. package/src/session/blob-store.ts +24 -0
  218. package/src/session/session-history-format.ts +2 -2
  219. package/src/session/session-manager.ts +46 -0
  220. package/src/slash-commands/builtin-registry.ts +138 -20
  221. package/src/slash-commands/helpers/usage-report.ts +23 -2
  222. package/src/ssh/__tests__/connection-manager-args.test.ts +69 -0
  223. package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +68 -0
  224. package/src/ssh/connection-manager.ts +12 -15
  225. package/src/ssh/file-transfer.ts +209 -0
  226. package/src/ssh/utils.ts +24 -0
  227. package/src/system-prompt.ts +56 -28
  228. package/src/task/executor.ts +96 -36
  229. package/src/task/index.ts +3 -3
  230. package/src/task/render.ts +14 -13
  231. package/src/task/worktree.ts +38 -1
  232. package/src/tiny/text.ts +49 -4
  233. package/src/tiny/worker.ts +3 -3
  234. package/src/tools/bash.ts +2 -2
  235. package/src/tools/builtin-names.ts +26 -2
  236. package/src/tools/{find.ts → glob.ts} +48 -42
  237. package/src/tools/{search.ts → grep.ts} +311 -129
  238. package/src/tools/index.ts +13 -14
  239. package/src/tools/irc.ts +6 -2
  240. package/src/tools/path-utils.ts +64 -1
  241. package/src/tools/read.ts +35 -6
  242. package/src/tools/render-utils.ts +14 -0
  243. package/src/tools/renderers.ts +15 -4
  244. package/src/tools/ssh.ts +17 -2
  245. package/src/tools/todo.ts +4 -26
  246. package/src/tools/write.ts +25 -9
  247. package/src/utils/active-repo-context.ts +143 -0
  248. package/src/utils/image-resize.ts +88 -7
  249. package/src/utils/lang-from-path.ts +3 -3
  250. package/src/utils/markit-cache.ts +166 -0
  251. package/src/utils/markit.ts +86 -18
  252. package/src/utils/prompt-path.ts +3 -0
  253. package/src/utils/title-generator.ts +1 -1
  254. package/src/web/search/provider.ts +54 -34
  255. package/src/web/search/providers/duckduckgo.ts +140 -0
  256. package/src/web/search/providers/firecrawl.ts +144 -0
  257. package/src/web/search/providers/tinyfish.ts +159 -0
  258. package/src/web/search/providers/xai.ts +292 -0
  259. package/src/web/search/providers/zai.ts +142 -56
  260. package/src/web/search/types.ts +6 -2
@@ -2,7 +2,13 @@ import { mkdtemp, rm, stat, writeFile } from "node:fs/promises";
2
2
  import { tmpdir } from "node:os";
3
3
  import * as path from "node:path";
4
4
  import { formatHashlineHeader } from "@oh-my-pi/hashline";
5
- import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
5
+ import type {
6
+ AgentTool,
7
+ AgentToolContext,
8
+ AgentToolResult,
9
+ AgentToolUpdateCallback,
10
+ ToolTier,
11
+ } from "@oh-my-pi/pi-agent-core";
6
12
  import { type GrepMatch, GrepOutputMode, type GrepResult, grep } from "@oh-my-pi/pi-natives";
7
13
  import type { Component } from "@oh-my-pi/pi-tui";
8
14
  import { Text } from "@oh-my-pi/pi-tui";
@@ -14,7 +20,7 @@ import type { LocalProtocolOptions } from "../internal-urls/local-protocol";
14
20
  import { InternalUrlRouter } from "../internal-urls/router";
15
21
  import type { InternalResource, ResolveContext } from "../internal-urls/types";
16
22
  import type { Theme } from "../modes/theme/theme";
17
- import searchDescription from "../prompts/tools/search.md" with { type: "text" };
23
+ import grepDescription from "../prompts/tools/grep.md" with { type: "text" };
18
24
  import { DEFAULT_MAX_COLUMN, type TruncationResult, truncateHead, truncateLine } from "../session/streaming-output";
19
25
  import {
20
26
  Ellipsis,
@@ -40,6 +46,7 @@ import {
40
46
  isLineInRanges,
41
47
  type LineRange,
42
48
  parseLineRanges,
49
+ pathTargetsSsh,
43
50
  type ResolvedSearchTarget,
44
51
  resolveReadPath,
45
52
  resolveToolSearchScope,
@@ -77,7 +84,7 @@ const searchSchema = type({
77
84
  .describe("files to skip before collecting results — use to paginate when the prior call hit the file limit"),
78
85
  });
79
86
 
80
- export type SearchToolInput = typeof searchSchema.infer;
87
+ export type GrepToolInput = typeof searchSchema.infer;
81
88
  export function toPathList(input: string | string[] | undefined): string[] {
82
89
  return typeof input === "string" ? [input] : (input ?? []);
83
90
  }
@@ -110,14 +117,39 @@ const SEARCH_GREP_TIMEOUT_MS = 30_000;
110
117
  * line-range selector peeled off the trailing `:N-M` (or `:N+K`, `:N,M`, …)
111
118
  * chunk via {@link splitPathAndSel}.
112
119
  */
113
- interface SearchPathSpec {
120
+ interface GrepPathSpec {
114
121
  original: string;
115
122
  clean: string;
116
123
  ranges?: [LineRange, ...LineRange[]];
117
124
  }
118
125
 
119
- function parsePathSpecs(rawEntries: readonly string[]): SearchPathSpec[] {
120
- const specs: SearchPathSpec[] = [];
126
+ /**
127
+ * Mirror of read's `parseSel` selector grammar (`read.ts`) so `grep` accepts
128
+ * exactly the internal-URL selectors `read` accepts: a single chunk that is a
129
+ * line range, `raw`, or `conflicts`; or a two-chunk compound of exactly one `raw`
130
+ * plus one line range. Everything else (`:-10`, `:1-1:1-2`, `:conflicts:1-1`,
131
+ * `:raw:conflicts`) is rejected.
132
+ *
133
+ * This mirrors the *accepted set* of `parseSel`; `read` rejects the same shapes
134
+ * caller-side when a peeled internal-URL selector parses as `none`, so neither
135
+ * tool silently widens on a malformed compound. Keep in sync with `read.parseSel`.
136
+ */
137
+ function isReadSelectorGrammar(sel: string): boolean {
138
+ if (sel.includes(":")) {
139
+ const chunks = sel.split(":");
140
+ if (chunks.length !== 2) return false;
141
+ const [a, b] = chunks as [string, string];
142
+ const aIsRaw = a.toLowerCase() === "raw";
143
+ const bIsRaw = b.toLowerCase() === "raw";
144
+ const rangeChunk = aIsRaw ? b : bIsRaw ? a : null;
145
+ return rangeChunk !== null && parseLineRanges(rangeChunk) !== null;
146
+ }
147
+ const lower = sel.toLowerCase();
148
+ return lower === "raw" || lower === "conflicts" || parseLineRanges(sel) !== null;
149
+ }
150
+
151
+ function parsePathSpecs(rawEntries: readonly string[]): GrepPathSpec[] {
152
+ const specs: GrepPathSpec[] = [];
121
153
  for (const entry of rawEntries) {
122
154
  // Internal URLs (`artifact://`, `skill://`, …) use the URL-aware splitter,
123
155
  // which peels selector-shaped tails only for selector-capable schemes and
@@ -127,11 +159,14 @@ function parsePathSpecs(rawEntries: readonly string[]): SearchPathSpec[] {
127
159
  // still honor any embedded line range as a match filter.
128
160
  const internalSplit = splitInternalUrlSel(entry);
129
161
  if (internalSplit.sel !== undefined) {
130
- specs.push({
131
- original: entry,
132
- clean: internalSplit.path,
133
- ranges: selectorLineRanges(internalSplit.sel),
134
- });
162
+ // Reject selectors read's parseSel would reject (`:-10`, `:1-1:1-2`,
163
+ // `:conflicts:1-1`) instead of silently widening the search or dropping a chunk.
164
+ if (!isReadSelectorGrammar(internalSplit.sel)) {
165
+ throw new ToolError(
166
+ `paths entry "${entry}" has an invalid selector ":${internalSplit.sel}" — use ":N-M" line ranges, ":raw"/":conflicts", a range plus ":raw", or percent-encode a literal ":" as %3A`,
167
+ );
168
+ }
169
+ specs.push({ original: entry, clean: internalSplit.path, ranges: selectorLineRanges(internalSplit.sel) });
135
170
  continue;
136
171
  }
137
172
  const split = splitPathAndSel(entry);
@@ -322,6 +357,11 @@ function indexSearchLines(content: string): IndexedContentLines {
322
357
  return { lines, starts };
323
358
  }
324
359
 
360
+ function lineAllowed(lineNumber: number, ranges: readonly LineRange[] | undefined): boolean {
361
+ return !ranges || isLineInRanges(lineNumber, ranges);
362
+ }
363
+
364
+ /** Binary search for the index of the line containing byte `offset`. */
325
365
  function findLineIndex(starts: readonly number[], offset: number): number {
326
366
  if (starts.length === 0) return -1;
327
367
  let low = 0;
@@ -337,8 +377,131 @@ function findLineIndex(starts: readonly number[], offset: number): number {
337
377
  return Math.max(0, high);
338
378
  }
339
379
 
340
- function lineAllowed(lineNumber: number, ranges: readonly LineRange[] | undefined): boolean {
341
- return !ranges || isLineInRanges(lineNumber, ranges);
380
+ /**
381
+ * JS-`RegExp` fallback returning matched line indexes for a virtual resource too
382
+ * large for native grep (>`NATIVE_GREP_MAX_FILE_BYTES`, which native grep silently
383
+ * skips). Mirrors the native probe's output (sorted, deduped indexes) so
384
+ * `buildVirtualMatches` rebuilds context/ranges identically; only the regex dialect
385
+ * differs for these oversized inputs (the pre-RE2-parity behavior).
386
+ */
387
+ function jsMatchedLineIndexes(
388
+ content: string,
389
+ lines: readonly string[],
390
+ pattern: string,
391
+ ignoreCase: boolean,
392
+ multiline: boolean,
393
+ ): number[] {
394
+ const flags = `${ignoreCase ? "i" : ""}${multiline ? "gm" : ""}`;
395
+ let regex: RegExp;
396
+ try {
397
+ regex = new RegExp(pattern, flags);
398
+ } catch (err) {
399
+ const message = err instanceof Error ? err.message : String(err);
400
+ throw new ToolError(`Invalid regex: ${message.replace(/^Invalid regular expression:\s*/i, "")}`);
401
+ }
402
+ if (!multiline) {
403
+ const out: number[] = [];
404
+ for (let i = 0; i < lines.length; i++) {
405
+ regex.lastIndex = 0;
406
+ if (regex.test(lines[i] ?? "")) out.push(i);
407
+ }
408
+ return out;
409
+ }
410
+ const { starts } = indexSearchLines(content);
411
+ const seen = new Set<number>();
412
+ const out: number[] = [];
413
+ let match = regex.exec(content);
414
+ while (match !== null) {
415
+ const lineIndex = findLineIndex(starts, match.index);
416
+ if (lineIndex >= 0 && !seen.has(lineIndex)) {
417
+ seen.add(lineIndex);
418
+ out.push(lineIndex);
419
+ }
420
+ if (match[0].length === 0) regex.lastIndex++;
421
+ match = regex.exec(content);
422
+ }
423
+ out.sort((a, b) => a - b);
424
+ return out;
425
+ }
426
+
427
+ /**
428
+ * Native-grep an oversized (>NATIVE_GREP_MAX_FILE_BYTES) line-mode virtual resource
429
+ * in line-boundary chunks (each <= the cap) so it keeps RE2 dialect parity instead of
430
+ * the JS fallback. Each chunk's matched line numbers are offset by its starting line
431
+ * index. A single line larger than the cap can't be native-grepped, so that one line
432
+ * is JS-tested. Returns sorted 0-based line indexes.
433
+ */
434
+ async function nativeChunkedLineIndexes(
435
+ dir: string,
436
+ resourceIdx: number,
437
+ content: string,
438
+ pattern: string,
439
+ ignoreCase: boolean,
440
+ signal: AbortSignal | undefined,
441
+ ): Promise<number[]> {
442
+ const rawLines = content.split("\n");
443
+ if (rawLines.length > 0 && rawLines[rawLines.length - 1] === "") rawLines.pop();
444
+ const indexes: number[] = [];
445
+ let chunkStart = 0;
446
+ let chunkBytes = 0;
447
+ let chunkLines: string[] = [];
448
+ let chunkSeq = 0;
449
+ const flush = async (): Promise<void> => {
450
+ if (chunkLines.length === 0) return;
451
+ const scratch = path.resolve(dir, `${resourceIdx}-chunk-${chunkSeq++}`);
452
+ await writeFile(scratch, chunkLines.join("\n"));
453
+ const probe = await grep(
454
+ {
455
+ pattern,
456
+ path: scratch,
457
+ ignoreCase,
458
+ multiline: false,
459
+ hidden: true,
460
+ gitignore: false,
461
+ maxCount: chunkLines.length,
462
+ contextBefore: 0,
463
+ contextAfter: 0,
464
+ maxColumns: DEFAULT_MAX_COLUMN,
465
+ mode: GrepOutputMode.Content,
466
+ signal,
467
+ timeoutMs: SEARCH_GREP_TIMEOUT_MS,
468
+ },
469
+ undefined,
470
+ );
471
+ for (const match of probe.matches) indexes.push(chunkStart + match.lineNumber - 1);
472
+ chunkLines = [];
473
+ chunkBytes = 0;
474
+ };
475
+ let lineRegex: RegExp | undefined;
476
+ for (let i = 0; i < rawLines.length; i++) {
477
+ const line = rawLines[i];
478
+ const lineBytes = Buffer.byteLength(line, "utf8") + 1;
479
+ if (lineBytes > NATIVE_GREP_MAX_FILE_BYTES) {
480
+ await flush();
481
+ if (!lineRegex) {
482
+ try {
483
+ lineRegex = new RegExp(pattern, ignoreCase ? "i" : "");
484
+ } catch (err) {
485
+ const message = err instanceof Error ? err.message : String(err);
486
+ throw new ToolError(`Invalid regex: ${message.replace(/^Invalid regular expression:\s*/i, "")}`);
487
+ }
488
+ }
489
+ lineRegex.lastIndex = 0;
490
+ if (lineRegex.test(line)) indexes.push(i);
491
+ chunkStart = i + 1;
492
+ continue;
493
+ }
494
+ if (chunkLines.length > 0 && chunkBytes + lineBytes > NATIVE_GREP_MAX_FILE_BYTES) {
495
+ await flush();
496
+ chunkStart = i;
497
+ }
498
+ if (chunkLines.length === 0) chunkStart = i;
499
+ chunkLines.push(line);
500
+ chunkBytes += lineBytes;
501
+ }
502
+ await flush();
503
+ indexes.sort((a, b) => a - b);
504
+ return indexes;
342
505
  }
343
506
 
344
507
  function makeContextLine(lines: readonly string[], lineIndex: number): { lineNumber: number; line: string } {
@@ -426,70 +589,7 @@ function buildVirtualMatches(
426
589
  return matches;
427
590
  }
428
591
 
429
- function compileVirtualRegex(pattern: string, ignoreCase: boolean, multiline: boolean): RegExp {
430
- const flags = `${ignoreCase ? "i" : ""}${multiline ? "gm" : ""}`;
431
- try {
432
- return new RegExp(pattern, flags);
433
- } catch (err) {
434
- const message = err instanceof Error ? err.message : String(err);
435
- throw new ToolError(`Invalid regex: ${message.replace(/^Invalid regular expression:\s*/i, "")}`);
436
- }
437
- }
438
-
439
- function searchVirtualResourceLines(
440
- resource: VirtualSearchResource,
441
- regex: RegExp,
442
- contextBefore: number,
443
- contextAfter: number,
444
- maxCount: number,
445
- ): { matches: GrepMatch[]; totalMatches: number; limitReached: boolean } {
446
- const lines = splitSearchLines(resource.content);
447
- const matchedIndexes: number[] = [];
448
-
449
- for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
450
- const lineNumber = lineIndex + 1;
451
- if (!lineAllowed(lineNumber, resource.ranges)) continue;
452
- regex.lastIndex = 0;
453
- if (!regex.test(lines[lineIndex] ?? "")) continue;
454
- matchedIndexes.push(lineIndex);
455
- }
456
-
457
- const matches = buildVirtualMatches(resource, lines, matchedIndexes, contextBefore, contextAfter, maxCount);
458
- return { matches, totalMatches: matchedIndexes.length, limitReached: matchedIndexes.length > matches.length };
459
- }
460
-
461
- function searchVirtualResourceMultiline(
462
- resource: VirtualSearchResource,
463
- regex: RegExp,
464
- contextBefore: number,
465
- contextAfter: number,
466
- maxCount: number,
467
- ): { matches: GrepMatch[]; totalMatches: number; limitReached: boolean } {
468
- const indexed = indexSearchLines(resource.content);
469
- const matchedLines = new Set<number>();
470
- const matchedIndexes: number[] = [];
471
-
472
- while (true) {
473
- const match = regex.exec(resource.content);
474
- if (match === null) break;
475
- const lineIndex = findLineIndex(indexed.starts, match.index);
476
- if (lineIndex >= 0) {
477
- const lineNumber = lineIndex + 1;
478
- if (!matchedLines.has(lineNumber) && lineAllowed(lineNumber, resource.ranges)) {
479
- matchedLines.add(lineNumber);
480
- matchedIndexes.push(lineIndex);
481
- }
482
- }
483
- if (match[0].length === 0) {
484
- regex.lastIndex++;
485
- }
486
- }
487
-
488
- const matches = buildVirtualMatches(resource, indexed.lines, matchedIndexes, contextBefore, contextAfter, maxCount);
489
- return { matches, totalMatches: matchedIndexes.length, limitReached: matchedIndexes.length > matches.length };
490
- }
491
-
492
- function searchVirtualResources(
592
+ async function searchVirtualResources(
493
593
  resources: readonly VirtualSearchResource[],
494
594
  pattern: string,
495
595
  ignoreCase: boolean,
@@ -497,29 +597,85 @@ function searchVirtualResources(
497
597
  contextBefore: number,
498
598
  contextAfter: number,
499
599
  maxCount: number,
500
- ): GrepResult {
600
+ signal?: AbortSignal,
601
+ ): Promise<GrepResult> {
501
602
  if (resources.length === 0) {
502
603
  return { matches: [], totalMatches: 0, filesWithMatches: 0, filesSearched: 0, limitReached: false };
503
604
  }
504
- const regex = compileVirtualRegex(pattern, ignoreCase, multiline);
505
605
  const matches: GrepMatch[] = [];
506
606
  const filesWithMatches = new Set<string>();
507
607
  let totalMatches = 0;
508
608
  let limitReached = false;
509
-
510
- for (const resource of resources) {
511
- const remaining = Math.max(maxCount - matches.length, 0);
512
- const resourceResult = multiline
513
- ? searchVirtualResourceMultiline(resource, regex, contextBefore, contextAfter, remaining)
514
- : searchVirtualResourceLines(resource, regex, contextBefore, contextAfter, remaining);
515
- if (resourceResult.totalMatches > 0) {
516
- filesWithMatches.add(resource.path);
609
+ // Detect matched line numbers with native grep (RE2) — the SAME matcher local
610
+ // search uses — so a pattern valid for local grep but not JS `RegExp` (`(?i)x`,
611
+ // `[[:digit:]]`) behaves identically on virtual/remote resources. The JS helpers
612
+ // below then rebuild the exact forward-only, range-trimmed context windows the
613
+ // virtual-search contract requires.
614
+ const dir = await mkdtemp(path.join(tmpdir(), "omp-search-virtual-"));
615
+ try {
616
+ for (let idx = 0; idx < resources.length; idx++) {
617
+ const resource = resources[idx];
618
+ const remaining = Math.max(maxCount - matches.length, 0);
619
+ if (remaining === 0) {
620
+ limitReached = true;
621
+ break;
622
+ }
623
+ const lines = multiline ? indexSearchLines(resource.content).lines : splitSearchLines(resource.content);
624
+ let matchedIndexes: number[];
625
+ if (Buffer.byteLength(resource.content, "utf8") > NATIVE_GREP_MAX_FILE_BYTES) {
626
+ // Native grep skips files above its 4 MiB cap. Search oversized content in
627
+ // line-boundary chunks so line-mode keeps RE2 parity; multiline can't be chunked
628
+ // without missing matches that span a chunk boundary, so it falls back to JS
629
+ // (dialect-as-JS only for these oversized multiline inputs).
630
+ matchedIndexes = (
631
+ multiline
632
+ ? jsMatchedLineIndexes(resource.content, lines, pattern, ignoreCase, true)
633
+ : await nativeChunkedLineIndexes(dir, idx, resource.content, pattern, ignoreCase, signal)
634
+ ).filter(lineIndex => lineAllowed(lineIndex + 1, resource.ranges));
635
+ } else {
636
+ const scratch = path.resolve(dir, `${idx}`);
637
+ await writeFile(scratch, resource.content);
638
+ const probe = await grep(
639
+ {
640
+ pattern,
641
+ path: scratch,
642
+ ignoreCase,
643
+ multiline,
644
+ hidden: true,
645
+ gitignore: false,
646
+ // A ranged selector must see every match so the range filter below never
647
+ // drops in-range hits that fall after the cap; matches can't exceed the
648
+ // line count. Unranged search keeps the overall result cap.
649
+ maxCount: resource.ranges ? Math.max(lines.length, 1) : INTERNAL_TOTAL_CAP,
650
+ contextBefore: 0,
651
+ contextAfter: 0,
652
+ maxColumns: DEFAULT_MAX_COLUMN,
653
+ mode: GrepOutputMode.Content,
654
+ signal,
655
+ timeoutMs: SEARCH_GREP_TIMEOUT_MS,
656
+ },
657
+ undefined,
658
+ );
659
+ matchedIndexes = [...new Set(probe.matches.map(match => match.lineNumber - 1))]
660
+ .filter(lineIndex => lineAllowed(lineIndex + 1, resource.ranges))
661
+ .sort((a, b) => a - b);
662
+ }
663
+ const resourceMatches = buildVirtualMatches(
664
+ resource,
665
+ lines,
666
+ matchedIndexes,
667
+ contextBefore,
668
+ contextAfter,
669
+ remaining,
670
+ );
671
+ if (matchedIndexes.length > 0) filesWithMatches.add(resource.path);
672
+ totalMatches += matchedIndexes.length;
673
+ limitReached = limitReached || matchedIndexes.length > resourceMatches.length;
674
+ matches.push(...resourceMatches);
517
675
  }
518
- totalMatches += resourceResult.totalMatches;
519
- limitReached = limitReached || resourceResult.limitReached;
520
- matches.push(...resourceResult.matches);
676
+ } finally {
677
+ await rm(dir, { recursive: true, force: true }).catch(() => {});
521
678
  }
522
-
523
679
  return {
524
680
  matches,
525
681
  totalMatches,
@@ -572,7 +728,7 @@ async function expandVirtualInternalResource(
572
728
  }
573
729
 
574
730
  async function resolveInternalSearchInputs(opts: {
575
- pathSpecs: readonly SearchPathSpec[];
731
+ pathSpecs: readonly GrepPathSpec[];
576
732
  resolvedPaths: string[];
577
733
  cwd: string;
578
734
  settings: unknown;
@@ -594,6 +750,7 @@ async function resolveInternalSearchInputs(opts: {
594
750
  signal: opts.signal,
595
751
  localProtocolOptions: opts.localProtocolOptions,
596
752
  skills: opts.skills,
753
+ skipDirectoryListing: true,
597
754
  };
598
755
 
599
756
  for (let idx = 0; idx < paths.length; idx++) {
@@ -601,10 +758,22 @@ async function resolveInternalSearchInputs(opts: {
601
758
  if (!rawPath || opts.archiveDisplayMap.has(rawPath) || !internalRouter.canHandle(rawPath)) {
602
759
  continue;
603
760
  }
604
- if (hasGlobPathChars(rawPath)) {
761
+ // `ssh://[::1]/path` carries `[`/`]` in the IPv6 authority — glob metacharacters
762
+ // — so check only the path portion for ssh:// (the SSH handler reads a single
763
+ // remote file; there is no glob expansion). A glob in the remote path still trips.
764
+ const globTarget = /^ssh:\/\//i.test(rawPath) ? rawPath.replace(/^ssh:\/\/[^/]*/i, "") : rawPath;
765
+ if (hasGlobPathChars(globTarget)) {
605
766
  throw new ToolError(`Glob patterns are not supported for internal URLs: ${rawPath}`);
606
767
  }
607
768
  const resource = await internalRouter.resolve(rawPath, context);
769
+ // A directory listing with no backing local path (e.g. a remote ssh:// dir)
770
+ // has no real contents to grep — searching its listing text would be
771
+ // misleading. Local/skill/vault dir resources set `sourcePath` and skip this.
772
+ if (resource.isDirectory && !resource.sourcePath) {
773
+ throw new ToolError(
774
+ `search cannot recurse the directory listing at ${rawPath}; search a specific file under it (e.g. ${rawPath.replace(/\/+$/, "")}/<file>) or read ${rawPath} to list its entries`,
775
+ );
776
+ }
608
777
  if (resource.sourcePath) {
609
778
  paths[idx] = resource.sourcePath;
610
779
  if (resource.immutable) {
@@ -634,7 +803,7 @@ async function resolveInternalSearchInputs(opts: {
634
803
  };
635
804
  }
636
805
 
637
- export interface SearchToolDetails {
806
+ export interface GrepToolDetails {
638
807
  truncation?: TruncationResult;
639
808
  fileLimitReached?: number;
640
809
  perFileLimitReached?: number;
@@ -666,19 +835,20 @@ export interface SearchToolDetails {
666
835
 
667
836
  type SearchParams = typeof searchSchema.infer;
668
837
 
669
- export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDetails> {
670
- readonly name = "search";
671
- readonly approval = "read" as const;
672
- readonly label = "Search";
838
+ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails> {
839
+ readonly name = "grep";
840
+ readonly approval = (args: unknown): ToolTier =>
841
+ toPathList((args as { paths?: string | string[] }).paths).some(pathTargetsSsh) ? "exec" : "read";
842
+ readonly label = "Grep";
673
843
  readonly loadMode = "discoverable";
674
- readonly summary = "Search file contents using ripgrep (fast text search)";
844
+ readonly summary = "Grep file contents using ripgrep (fast regex search)";
675
845
  readonly description: string;
676
846
  readonly parameters = searchSchema;
677
847
  readonly strict = true;
678
848
 
679
849
  constructor(private readonly session: ToolSession) {
680
850
  const displayMode = resolveFileDisplayMode(session);
681
- this.description = prompt.render(searchDescription, {
851
+ this.description = prompt.render(grepDescription, {
682
852
  IS_HL_MODE: displayMode.hashLines,
683
853
  IS_LINE_NUMBER_MODE: !displayMode.hashLines && displayMode.lineNumbers,
684
854
  });
@@ -688,9 +858,9 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
688
858
  _toolCallId: string,
689
859
  params: SearchParams,
690
860
  signal?: AbortSignal,
691
- _onUpdate?: AgentToolUpdateCallback<SearchToolDetails>,
861
+ _onUpdate?: AgentToolUpdateCallback<GrepToolDetails>,
692
862
  _toolContext?: AgentToolContext,
693
- ): Promise<AgentToolResult<SearchToolDetails>> {
863
+ ): Promise<AgentToolResult<GrepToolDetails>> {
694
864
  const { pattern, paths: rawPaths, case: caseSensitive, gitignore, skip } = params;
695
865
 
696
866
  return untilAborted(signal, async () => {
@@ -773,8 +943,8 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
773
943
  `or pass a UTF-8 text member.`,
774
944
  );
775
945
  }
776
- const normalizedContextBefore = this.session.settings.get("search.contextBefore");
777
- const normalizedContextAfter = this.session.settings.get("search.contextAfter");
946
+ const normalizedContextBefore = this.session.settings.get("grep.contextBefore");
947
+ const normalizedContextAfter = this.session.settings.get("grep.contextAfter");
778
948
  const ignoreCase = !(caseSensitive ?? true);
779
949
  const useGitignore = gitignore ?? true;
780
950
  const patternHasNewline = normalizedPattern.includes("\n") || normalizedPattern.includes("\\n");
@@ -952,20 +1122,32 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
952
1122
  }
953
1123
  if (err instanceof Error && err.message.includes("Aborted: Timeout")) {
954
1124
  throw new ToolError(
955
- `Search timed out after ${SEARCH_GREP_TIMEOUT_MS / 1000}s; narrow paths or pattern, or scope with \`find\` first`,
1125
+ `Grep timed out after ${SEARCH_GREP_TIMEOUT_MS / 1000}s; narrow paths or pattern, or scope with \`glob\` first`,
956
1126
  );
957
1127
  }
958
1128
  throw err;
959
1129
  }
960
- const virtualResult = searchVirtualResources(
961
- virtualResources,
962
- normalizedPattern,
963
- ignoreCase,
964
- effectiveMultiline,
965
- normalizedContextBefore,
966
- normalizedContextAfter,
967
- INTERNAL_TOTAL_CAP,
968
- );
1130
+ let virtualResult: GrepResult;
1131
+ try {
1132
+ virtualResult = await searchVirtualResources(
1133
+ virtualResources,
1134
+ normalizedPattern,
1135
+ ignoreCase,
1136
+ effectiveMultiline,
1137
+ normalizedContextBefore,
1138
+ normalizedContextAfter,
1139
+ INTERNAL_TOTAL_CAP,
1140
+ signal,
1141
+ );
1142
+ } catch (err) {
1143
+ if (err instanceof Error && /^regex(?: parse)? error/i.test(err.message)) {
1144
+ throw new ToolError(err.message.replace(/^regex(?: parse)? error:?\s*/i, "Invalid regex: "));
1145
+ }
1146
+ if (err instanceof SyntaxError) {
1147
+ throw new ToolError(`Invalid regex: ${err.message}`);
1148
+ }
1149
+ throw err;
1150
+ }
969
1151
  result = mergeGrepResults(result, virtualResult, INTERNAL_TOTAL_CAP);
970
1152
  if (rangesByAbsPath.size > 0) {
971
1153
  const filteredMatches: GrepMatch[] = [];
@@ -1108,7 +1290,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
1108
1290
  .filter((s): s is string => Boolean(s))
1109
1291
  .join("\n") || undefined;
1110
1292
  if (selectedMatches.length === 0) {
1111
- const details: SearchToolDetails = {
1293
+ const details: GrepToolDetails = {
1112
1294
  scopePath,
1113
1295
  searchPath,
1114
1296
  cwd: this.session.cwd,
@@ -1245,7 +1427,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
1245
1427
  const truncated = Boolean(
1246
1428
  fileLimitReached || perFileLimitReached || result.limitReached || truncation.truncated || linesTruncated,
1247
1429
  );
1248
- const details: SearchToolDetails = {
1430
+ const details: GrepToolDetails = {
1249
1431
  scopePath,
1250
1432
  searchPath,
1251
1433
  cwd: this.session.cwd,
@@ -1282,7 +1464,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
1282
1464
  // TUI Renderer
1283
1465
  // =============================================================================
1284
1466
 
1285
- interface SearchRenderArgs {
1467
+ interface GrepRenderArgs {
1286
1468
  pattern: string;
1287
1469
  paths?: string | string[];
1288
1470
  case?: boolean;
@@ -1298,7 +1480,7 @@ const EXPANDED_TEXT_LIMIT = PREVIEW_LIMITS.EXPANDED_LINES * 2;
1298
1480
 
1299
1481
  const SEARCH_CODE_FRAME_LINE_RE = /^\s*\*?(\d+)│/;
1300
1482
 
1301
- function searchScopeMeta(details: SearchToolDetails | undefined): string | undefined {
1483
+ function searchScopeMeta(details: GrepToolDetails | undefined): string | undefined {
1302
1484
  if (!details?.scopePath) return undefined;
1303
1485
  const label = details.searchPath ? fileHyperlink(details.searchPath, details.scopePath) : details.scopePath;
1304
1486
  return `in ${label}`;
@@ -1446,13 +1628,13 @@ function renderBudgetedSearchGroups(
1446
1628
  return lines;
1447
1629
  }
1448
1630
 
1449
- function searchStatusIcon(uiTheme: Theme): string {
1631
+ function grepStatusIcon(uiTheme: Theme): string {
1450
1632
  return uiTheme.fg("toolTitle", uiTheme.symbol("icon.search"));
1451
1633
  }
1452
1634
 
1453
- export const searchToolRenderer = {
1635
+ export const grepToolRenderer = {
1454
1636
  inline: true,
1455
- renderCall(args: SearchRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
1637
+ renderCall(args: GrepRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
1456
1638
  const paths = toPathList(args.paths);
1457
1639
  const meta: string[] = [];
1458
1640
  if (paths.length) meta.push(`in ${paths.join(", ")}`);
@@ -1461,17 +1643,17 @@ export const searchToolRenderer = {
1461
1643
  if (args.skip !== undefined && args.skip > 0) meta.push(`skip:${args.skip}`);
1462
1644
 
1463
1645
  const text = renderStatusLine(
1464
- { icon: "pending", title: "Search", titleColor: "toolTitle", description: args.pattern || "?", meta },
1646
+ { icon: "pending", title: "Grep", titleColor: "toolTitle", description: args.pattern || "?", meta },
1465
1647
  uiTheme,
1466
1648
  );
1467
1649
  return new Text(text, 1, 0);
1468
1650
  },
1469
1651
 
1470
1652
  renderResult(
1471
- result: { content: Array<{ type: string; text?: string }>; details?: SearchToolDetails; isError?: boolean },
1653
+ result: { content: Array<{ type: string; text?: string }>; details?: GrepToolDetails; isError?: boolean },
1472
1654
  options: RenderResultOptions,
1473
1655
  uiTheme: Theme,
1474
- args?: SearchRenderArgs,
1656
+ args?: GrepRenderArgs,
1475
1657
  ): Component {
1476
1658
  const details = result.details;
1477
1659
 
@@ -1491,8 +1673,8 @@ export const searchToolRenderer = {
1491
1673
  const description = args?.pattern ?? undefined;
1492
1674
  const header = renderStatusLine(
1493
1675
  {
1494
- iconOverride: searchStatusIcon(uiTheme),
1495
- title: "Search",
1676
+ iconOverride: grepStatusIcon(uiTheme),
1677
+ title: "Grep",
1496
1678
  titleColor: "toolTitle",
1497
1679
  description,
1498
1680
  meta: [formatCount("item", lines.length)],
@@ -1536,7 +1718,7 @@ export const searchToolRenderer = {
1536
1718
  const scopeMeta = searchScopeMeta(details);
1537
1719
  if (scopeMeta) meta.push(scopeMeta);
1538
1720
  const header = renderStatusLine(
1539
- { icon: "warning", title: "Search", titleColor: "toolTitle", description: args?.pattern, meta },
1721
+ { icon: "warning", title: "Grep", titleColor: "toolTitle", description: args?.pattern, meta },
1540
1722
  uiTheme,
1541
1723
  );
1542
1724
  const lines = [header, formatEmptyMessage("No matches found", uiTheme)];
@@ -1552,8 +1734,8 @@ export const searchToolRenderer = {
1552
1734
  const description = args?.pattern ?? undefined;
1553
1735
  const header = renderStatusLine(
1554
1736
  {
1555
- ...(truncated ? { icon: "warning" as const } : { iconOverride: searchStatusIcon(uiTheme) }),
1556
- title: "Search",
1737
+ ...(truncated ? { icon: "warning" as const } : { iconOverride: grepStatusIcon(uiTheme) }),
1738
+ title: "Grep",
1557
1739
  titleColor: "toolTitle",
1558
1740
  description,
1559
1741
  meta,