@oh-my-pi/pi-coding-agent 15.7.2 → 15.7.5

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 (188) hide show
  1. package/CHANGELOG.md +114 -6
  2. package/dist/types/cli/args.d.ts +1 -1
  3. package/dist/types/cli/extension-flags.d.ts +36 -0
  4. package/dist/types/config/config-file.d.ts +4 -0
  5. package/dist/types/config/file-lock.d.ts +23 -0
  6. package/dist/types/config/keybindings.d.ts +2 -1
  7. package/dist/types/config/model-registry.d.ts +6 -0
  8. package/dist/types/config/settings-schema.d.ts +69 -65
  9. package/dist/types/edit/hashline/diff.d.ts +3 -3
  10. package/dist/types/eval/__tests__/agent-bridge.test.d.ts +1 -0
  11. package/dist/types/eval/__tests__/budget-bridge.test.d.ts +1 -0
  12. package/dist/types/eval/__tests__/idle-timeout.test.d.ts +1 -0
  13. package/dist/types/eval/agent-bridge.d.ts +25 -0
  14. package/dist/types/eval/backend.d.ts +17 -2
  15. package/dist/types/eval/budget-bridge.d.ts +29 -0
  16. package/dist/types/eval/idle-timeout.d.ts +28 -0
  17. package/dist/types/eval/js/executor.d.ts +8 -0
  18. package/dist/types/eval/js/tool-bridge.d.ts +2 -1
  19. package/dist/types/eval/py/executor.d.ts +13 -0
  20. package/dist/types/exec/bash-executor.d.ts +1 -0
  21. package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
  22. package/dist/types/extensibility/extensions/runner.d.ts +7 -0
  23. package/dist/types/extensibility/plugins/git-url.d.ts +11 -1
  24. package/dist/types/extensibility/plugins/manager.d.ts +12 -1
  25. package/dist/types/extensibility/shared-events.d.ts +2 -2
  26. package/dist/types/internal-urls/local-protocol.d.ts +19 -9
  27. package/dist/types/internal-urls/types.d.ts +14 -0
  28. package/dist/types/lsp/client.d.ts +3 -0
  29. package/dist/types/mcp/manager.d.ts +14 -5
  30. package/dist/types/memory-backend/index.d.ts +1 -1
  31. package/dist/types/memory-backend/resolve.d.ts +1 -1
  32. package/dist/types/memory-backend/types.d.ts +3 -3
  33. package/dist/types/mnemopi/backend.d.ts +4 -0
  34. package/dist/types/mnemopi/config.d.ts +29 -0
  35. package/dist/types/mnemopi/state.d.ts +72 -0
  36. package/dist/types/modes/components/custom-editor.d.ts +2 -2
  37. package/dist/types/modes/components/omfg-panel.d.ts +19 -0
  38. package/dist/types/modes/controllers/command-controller.d.ts +6 -0
  39. package/dist/types/modes/controllers/input-controller.d.ts +1 -3
  40. package/dist/types/modes/controllers/omfg-controller.d.ts +10 -0
  41. package/dist/types/modes/controllers/omfg-rule.d.ts +26 -0
  42. package/dist/types/modes/gradient-highlight.d.ts +5 -1
  43. package/dist/types/modes/interactive-mode.d.ts +7 -3
  44. package/dist/types/modes/magic-keywords.d.ts +14 -0
  45. package/dist/types/modes/markdown-prose.d.ts +27 -0
  46. package/dist/types/modes/orchestrate.d.ts +7 -2
  47. package/dist/types/modes/shared.d.ts +1 -1
  48. package/dist/types/modes/turn-budget.d.ts +18 -0
  49. package/dist/types/modes/types.d.ts +7 -3
  50. package/dist/types/modes/ultrathink.d.ts +7 -2
  51. package/dist/types/modes/workflow.d.ts +15 -0
  52. package/dist/types/sdk.d.ts +13 -3
  53. package/dist/types/session/agent-session.d.ts +36 -17
  54. package/dist/types/session/session-manager.d.ts +18 -0
  55. package/dist/types/session/session-storage.d.ts +6 -0
  56. package/dist/types/session/shake-types.d.ts +24 -0
  57. package/dist/types/task/executor.d.ts +2 -2
  58. package/dist/types/task/repair-args.d.ts +52 -0
  59. package/dist/types/tiny/models.d.ts +1 -1
  60. package/dist/types/tiny/title-client.d.ts +28 -2
  61. package/dist/types/tiny/title-protocol.d.ts +8 -5
  62. package/dist/types/tools/find.d.ts +1 -1
  63. package/dist/types/tools/index.d.ts +19 -3
  64. package/dist/types/tools/memory-edit.d.ts +1 -1
  65. package/dist/types/tools/path-utils.d.ts +7 -0
  66. package/dist/types/tui/output-block.d.ts +7 -7
  67. package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
  68. package/package.json +10 -10
  69. package/scripts/build-binary.ts +0 -1
  70. package/src/autoresearch/tools/run-experiment.ts +45 -113
  71. package/src/cli/args.ts +39 -16
  72. package/src/cli/extension-flags.ts +48 -0
  73. package/src/cli/plugin-cli.ts +11 -2
  74. package/src/cli.ts +59 -0
  75. package/src/config/config-file.ts +98 -13
  76. package/src/config/file-lock.ts +60 -17
  77. package/src/config/keybindings.ts +78 -27
  78. package/src/config/model-registry.ts +7 -1
  79. package/src/config/settings-schema.ts +73 -67
  80. package/src/config/settings.ts +22 -0
  81. package/src/edit/hashline/diff.ts +81 -24
  82. package/src/edit/renderer.ts +16 -12
  83. package/src/eval/__tests__/agent-bridge.test.ts +433 -0
  84. package/src/eval/__tests__/budget-bridge.test.ts +69 -0
  85. package/src/eval/__tests__/idle-timeout.test.ts +66 -0
  86. package/src/eval/__tests__/shared-executors.test.ts +21 -0
  87. package/src/eval/agent-bridge.ts +295 -0
  88. package/src/eval/backend.ts +17 -2
  89. package/src/eval/budget-bridge.ts +48 -0
  90. package/src/eval/idle-timeout.ts +80 -0
  91. package/src/eval/js/executor.ts +35 -7
  92. package/src/eval/js/index.ts +2 -1
  93. package/src/eval/js/shared/prelude.txt +85 -1
  94. package/src/eval/js/tool-bridge.ts +9 -0
  95. package/src/eval/py/executor.ts +41 -14
  96. package/src/eval/py/index.ts +2 -1
  97. package/src/eval/py/prelude.py +132 -1
  98. package/src/exec/bash-executor.ts +2 -3
  99. package/src/extensibility/custom-tools/types.ts +2 -2
  100. package/src/extensibility/extensions/runner.ts +12 -2
  101. package/src/extensibility/plugins/git-url.ts +90 -4
  102. package/src/extensibility/plugins/manager.ts +103 -7
  103. package/src/extensibility/shared-events.ts +2 -2
  104. package/src/internal-urls/docs-index.generated.ts +88 -88
  105. package/src/internal-urls/local-protocol.ts +23 -11
  106. package/src/internal-urls/types.ts +15 -0
  107. package/src/lsp/client.ts +28 -5
  108. package/src/main.ts +44 -55
  109. package/src/mcp/manager.ts +87 -4
  110. package/src/memory-backend/index.ts +1 -1
  111. package/src/memory-backend/resolve.ts +3 -3
  112. package/src/memory-backend/types.ts +3 -3
  113. package/src/{mnemosyne → mnemopi}/backend.ts +70 -70
  114. package/src/{mnemosyne → mnemopi}/config.ts +36 -36
  115. package/src/{mnemosyne → mnemopi}/state.ts +67 -67
  116. package/src/modes/components/agent-dashboard.ts +6 -6
  117. package/src/modes/components/custom-editor.ts +4 -11
  118. package/src/modes/components/extensions/state-manager.ts +3 -4
  119. package/src/modes/components/footer.ts +8 -9
  120. package/src/modes/components/hook-selector.ts +86 -20
  121. package/src/modes/components/oauth-selector.ts +93 -21
  122. package/src/modes/components/omfg-panel.ts +141 -0
  123. package/src/modes/components/settings-defs.ts +2 -2
  124. package/src/modes/components/tips.txt +2 -1
  125. package/src/modes/components/tool-execution.ts +38 -19
  126. package/src/modes/components/tree-selector.ts +4 -3
  127. package/src/modes/components/user-message-selector.ts +94 -19
  128. package/src/modes/components/user-message.ts +8 -1
  129. package/src/modes/controllers/command-controller.ts +25 -0
  130. package/src/modes/controllers/event-controller.ts +68 -3
  131. package/src/modes/controllers/input-controller.ts +14 -11
  132. package/src/modes/controllers/mcp-command-controller.ts +1 -1
  133. package/src/modes/controllers/omfg-controller.ts +283 -0
  134. package/src/modes/controllers/omfg-rule.ts +647 -0
  135. package/src/modes/controllers/selector-controller.ts +1 -0
  136. package/src/modes/gradient-highlight.ts +23 -6
  137. package/src/modes/interactive-mode.ts +41 -7
  138. package/src/modes/magic-keywords.ts +20 -0
  139. package/src/modes/markdown-prose.ts +247 -0
  140. package/src/modes/orchestrate.ts +17 -11
  141. package/src/modes/shared.ts +3 -11
  142. package/src/modes/turn-budget.ts +31 -0
  143. package/src/modes/types.ts +7 -1
  144. package/src/modes/ultrathink.ts +16 -10
  145. package/src/modes/utils/hotkeys-markdown.ts +1 -1
  146. package/src/modes/workflow.ts +42 -0
  147. package/src/prompts/system/omfg-user.md +51 -0
  148. package/src/prompts/system/project-prompt.md +3 -2
  149. package/src/prompts/system/subagent-system-prompt.md +12 -8
  150. package/src/prompts/system/system-prompt.md +9 -6
  151. package/src/prompts/system/workflow-notice.md +70 -0
  152. package/src/prompts/tools/eval.md +13 -1
  153. package/src/prompts/tools/memory-edit.md +1 -1
  154. package/src/sdk.ts +63 -33
  155. package/src/session/agent-session.ts +290 -55
  156. package/src/session/session-manager.ts +32 -0
  157. package/src/session/session-storage.ts +68 -8
  158. package/src/session/shake-types.ts +43 -0
  159. package/src/slash-commands/builtin-registry.ts +39 -16
  160. package/src/task/executor.ts +17 -7
  161. package/src/task/index.ts +9 -8
  162. package/src/task/repair-args.ts +117 -0
  163. package/src/tiny/models.ts +2 -2
  164. package/src/tiny/title-client.ts +133 -43
  165. package/src/tiny/title-protocol.ts +10 -5
  166. package/src/tiny/worker.ts +3 -46
  167. package/src/tools/ast-edit.ts +3 -0
  168. package/src/tools/ast-grep.ts +3 -0
  169. package/src/tools/eval.ts +202 -26
  170. package/src/tools/find.ts +20 -6
  171. package/src/tools/gh.ts +1 -0
  172. package/src/tools/grouped-file-output.ts +9 -2
  173. package/src/tools/index.ts +17 -5
  174. package/src/tools/memory-edit.ts +4 -4
  175. package/src/tools/memory-recall.ts +5 -5
  176. package/src/tools/memory-reflect.ts +5 -5
  177. package/src/tools/memory-retain.ts +4 -4
  178. package/src/tools/path-utils.ts +13 -2
  179. package/src/tools/read.ts +1 -0
  180. package/src/tools/render-utils.ts +2 -1
  181. package/src/tools/search.ts +491 -76
  182. package/src/tui/output-block.ts +37 -75
  183. package/src/utils/git.ts +9 -3
  184. package/dist/types/mnemosyne/backend.d.ts +0 -4
  185. package/dist/types/mnemosyne/config.d.ts +0 -29
  186. package/dist/types/mnemosyne/state.d.ts +0 -72
  187. /package/dist/types/{mnemosyne → mnemopi}/index.d.ts +0 -0
  188. /package/src/{mnemosyne → mnemopi}/index.ts +0 -0
@@ -10,9 +10,12 @@ import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
10
10
  import * as z from "zod/v4";
11
11
  import { recordFileSnapshot } from "../edit/file-snapshot-store";
12
12
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
13
+ import type { LocalProtocolOptions } from "../internal-urls/local-protocol";
14
+ import { InternalUrlRouter } from "../internal-urls/router";
15
+ import type { InternalResource, ResolveContext } from "../internal-urls/types";
13
16
  import type { Theme } from "../modes/theme/theme";
14
17
  import searchDescription from "../prompts/tools/search.md" with { type: "text" };
15
- import { DEFAULT_MAX_COLUMN, type TruncationResult, truncateHead } from "../session/streaming-output";
18
+ import { DEFAULT_MAX_COLUMN, type TruncationResult, truncateHead, truncateLine } from "../session/streaming-output";
16
19
  import { Ellipsis, fileHyperlink, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
17
20
  import { resolveFileDisplayMode } from "../utils/file-display-mode";
18
21
  import type { ToolSession } from ".";
@@ -31,6 +34,7 @@ import {
31
34
  isLineInRanges,
32
35
  type LineRange,
33
36
  parseLineRanges,
37
+ type ResolvedSearchTarget,
34
38
  resolveReadPath,
35
39
  resolveToolSearchScope,
36
40
  splitPathAndSel,
@@ -257,6 +261,343 @@ async function resolveArchiveSearchPaths(
257
261
  return { resolvedPaths, displayMap, displaySet, unreadable, cleanup };
258
262
  }
259
263
 
264
+ interface VirtualSearchResource {
265
+ path: string;
266
+ content: string;
267
+ ranges?: readonly LineRange[];
268
+ }
269
+
270
+ interface InternalSearchInputResolution {
271
+ paths: string[];
272
+ resolvedPathsByInput: string[];
273
+ virtualResources: VirtualSearchResource[];
274
+ virtualPathSet: Set<string>;
275
+ virtualInputIndexes: Set<number>;
276
+ immutableSourcePaths: Set<string>;
277
+ virtualScopePath?: string;
278
+ }
279
+
280
+ interface IndexedContentLines {
281
+ lines: string[];
282
+ starts: number[];
283
+ }
284
+
285
+ const INTERNAL_URL_DISPLAY_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
286
+ const OMP_ROOT_URL_RE = /^omp:\/\/\/?$/i;
287
+
288
+ function normalizeSearchLine(line: string): string {
289
+ return line.endsWith("\r") ? line.slice(0, -1) : line;
290
+ }
291
+
292
+ function splitSearchLines(content: string): string[] {
293
+ const lines = content.split("\n");
294
+ if (lines.length > 0 && lines[lines.length - 1] === "") {
295
+ lines.pop();
296
+ }
297
+ return lines.map(normalizeSearchLine);
298
+ }
299
+
300
+ function indexSearchLines(content: string): IndexedContentLines {
301
+ const rawLines = content.split("\n");
302
+ if (rawLines.length > 0 && rawLines[rawLines.length - 1] === "") {
303
+ rawLines.pop();
304
+ }
305
+ const lines: string[] = [];
306
+ const starts: number[] = [];
307
+ let offset = 0;
308
+ for (const rawLine of rawLines) {
309
+ starts.push(offset);
310
+ lines.push(normalizeSearchLine(rawLine));
311
+ offset += rawLine.length + 1;
312
+ }
313
+ return { lines, starts };
314
+ }
315
+
316
+ function findLineIndex(starts: readonly number[], offset: number): number {
317
+ if (starts.length === 0) return -1;
318
+ let low = 0;
319
+ let high = starts.length - 1;
320
+ while (low <= high) {
321
+ const mid = Math.floor((low + high) / 2);
322
+ if (starts[mid] <= offset) {
323
+ low = mid + 1;
324
+ } else {
325
+ high = mid - 1;
326
+ }
327
+ }
328
+ return Math.max(0, high);
329
+ }
330
+
331
+ function lineAllowed(lineNumber: number, ranges: readonly LineRange[] | undefined): boolean {
332
+ return !ranges || isLineInRanges(lineNumber, ranges);
333
+ }
334
+
335
+ function makeContextLine(lines: readonly string[], lineIndex: number): { lineNumber: number; line: string } {
336
+ const { text } = truncateLine(lines[lineIndex] ?? "", DEFAULT_MAX_COLUMN);
337
+ return { lineNumber: lineIndex + 1, line: text };
338
+ }
339
+
340
+ function makeVirtualMatch(
341
+ resource: VirtualSearchResource,
342
+ lines: readonly string[],
343
+ lineIndex: number,
344
+ contextBefore: number,
345
+ contextAfter: number,
346
+ ): GrepMatch {
347
+ const lineNumber = lineIndex + 1;
348
+ const { text, wasTruncated } = truncateLine(lines[lineIndex] ?? "", DEFAULT_MAX_COLUMN);
349
+ const match: GrepMatch = {
350
+ path: resource.path,
351
+ lineNumber,
352
+ line: text,
353
+ };
354
+ if (wasTruncated) match.truncated = true;
355
+
356
+ if (contextBefore > 0) {
357
+ const before: NonNullable<GrepMatch["contextBefore"]> = [];
358
+ const start = Math.max(0, lineIndex - contextBefore);
359
+ for (let idx = start; idx < lineIndex; idx++) {
360
+ const contextLineNumber = idx + 1;
361
+ if (lineAllowed(contextLineNumber, resource.ranges)) {
362
+ before.push(makeContextLine(lines, idx));
363
+ }
364
+ }
365
+ if (before.length > 0) match.contextBefore = before;
366
+ }
367
+
368
+ if (contextAfter > 0) {
369
+ const after: NonNullable<GrepMatch["contextAfter"]> = [];
370
+ const end = Math.min(lines.length - 1, lineIndex + contextAfter);
371
+ for (let idx = lineIndex + 1; idx <= end; idx++) {
372
+ const contextLineNumber = idx + 1;
373
+ if (lineAllowed(contextLineNumber, resource.ranges)) {
374
+ after.push(makeContextLine(lines, idx));
375
+ }
376
+ }
377
+ if (after.length > 0) match.contextAfter = after;
378
+ }
379
+
380
+ return match;
381
+ }
382
+
383
+ function compileVirtualRegex(pattern: string, ignoreCase: boolean, multiline: boolean): RegExp {
384
+ const flags = `${ignoreCase ? "i" : ""}${multiline ? "gm" : ""}`;
385
+ try {
386
+ return new RegExp(pattern, flags);
387
+ } catch (err) {
388
+ const message = err instanceof Error ? err.message : String(err);
389
+ throw new ToolError(`Invalid regex: ${message.replace(/^Invalid regular expression:\s*/i, "")}`);
390
+ }
391
+ }
392
+
393
+ function searchVirtualResourceLines(
394
+ resource: VirtualSearchResource,
395
+ regex: RegExp,
396
+ contextBefore: number,
397
+ contextAfter: number,
398
+ maxCount: number,
399
+ ): { matches: GrepMatch[]; totalMatches: number; limitReached: boolean } {
400
+ const lines = splitSearchLines(resource.content);
401
+ const matches: GrepMatch[] = [];
402
+ let totalMatches = 0;
403
+ let limitReached = false;
404
+
405
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
406
+ const lineNumber = lineIndex + 1;
407
+ if (!lineAllowed(lineNumber, resource.ranges)) continue;
408
+ regex.lastIndex = 0;
409
+ if (!regex.test(lines[lineIndex] ?? "")) continue;
410
+ totalMatches++;
411
+ if (matches.length >= maxCount) {
412
+ limitReached = true;
413
+ continue;
414
+ }
415
+ matches.push(makeVirtualMatch(resource, lines, lineIndex, contextBefore, contextAfter));
416
+ }
417
+
418
+ return { matches, totalMatches, limitReached };
419
+ }
420
+
421
+ function searchVirtualResourceMultiline(
422
+ resource: VirtualSearchResource,
423
+ regex: RegExp,
424
+ contextBefore: number,
425
+ contextAfter: number,
426
+ maxCount: number,
427
+ ): { matches: GrepMatch[]; totalMatches: number; limitReached: boolean } {
428
+ const indexed = indexSearchLines(resource.content);
429
+ const matches: GrepMatch[] = [];
430
+ const matchedLines = new Set<number>();
431
+ let totalMatches = 0;
432
+ let limitReached = false;
433
+
434
+ while (true) {
435
+ const match = regex.exec(resource.content);
436
+ if (match === null) break;
437
+ const lineIndex = findLineIndex(indexed.starts, match.index);
438
+ if (lineIndex >= 0) {
439
+ const lineNumber = lineIndex + 1;
440
+ if (!matchedLines.has(lineNumber) && lineAllowed(lineNumber, resource.ranges)) {
441
+ matchedLines.add(lineNumber);
442
+ totalMatches++;
443
+ if (matches.length >= maxCount) {
444
+ limitReached = true;
445
+ } else {
446
+ matches.push(makeVirtualMatch(resource, indexed.lines, lineIndex, contextBefore, contextAfter));
447
+ }
448
+ }
449
+ }
450
+ if (match[0].length === 0) {
451
+ regex.lastIndex++;
452
+ }
453
+ }
454
+
455
+ return { matches, totalMatches, limitReached };
456
+ }
457
+
458
+ function searchVirtualResources(
459
+ resources: readonly VirtualSearchResource[],
460
+ pattern: string,
461
+ ignoreCase: boolean,
462
+ multiline: boolean,
463
+ contextBefore: number,
464
+ contextAfter: number,
465
+ maxCount: number,
466
+ ): GrepResult {
467
+ if (resources.length === 0) {
468
+ return { matches: [], totalMatches: 0, filesWithMatches: 0, filesSearched: 0, limitReached: false };
469
+ }
470
+ const regex = compileVirtualRegex(pattern, ignoreCase, multiline);
471
+ const matches: GrepMatch[] = [];
472
+ const filesWithMatches = new Set<string>();
473
+ let totalMatches = 0;
474
+ let limitReached = false;
475
+
476
+ for (const resource of resources) {
477
+ const remaining = Math.max(maxCount - matches.length, 0);
478
+ const resourceResult = multiline
479
+ ? searchVirtualResourceMultiline(resource, regex, contextBefore, contextAfter, remaining)
480
+ : searchVirtualResourceLines(resource, regex, contextBefore, contextAfter, remaining);
481
+ if (resourceResult.totalMatches > 0) {
482
+ filesWithMatches.add(resource.path);
483
+ }
484
+ totalMatches += resourceResult.totalMatches;
485
+ limitReached = limitReached || resourceResult.limitReached;
486
+ matches.push(...resourceResult.matches);
487
+ }
488
+
489
+ return {
490
+ matches,
491
+ totalMatches,
492
+ filesWithMatches: filesWithMatches.size,
493
+ filesSearched: resources.length,
494
+ limitReached,
495
+ };
496
+ }
497
+
498
+ function mergeGrepResults(left: GrepResult, right: GrepResult, maxCount: number): GrepResult {
499
+ if (left.matches.length === 0) return right;
500
+ if (right.matches.length === 0) return left;
501
+ const combinedMatches = [...left.matches, ...right.matches];
502
+ const matches = combinedMatches.length > maxCount ? combinedMatches.slice(0, maxCount) : combinedMatches;
503
+ return {
504
+ matches,
505
+ totalMatches: left.totalMatches + right.totalMatches,
506
+ filesWithMatches: new Set(matches.map(match => match.path)).size,
507
+ filesSearched: left.filesSearched + right.filesSearched,
508
+ limitReached: left.limitReached || right.limitReached || matches.length < combinedMatches.length,
509
+ };
510
+ }
511
+
512
+ async function expandVirtualInternalResource(
513
+ rawPath: string,
514
+ resource: InternalResource,
515
+ internalRouter: InternalUrlRouter,
516
+ context: ResolveContext,
517
+ ranges: readonly LineRange[] | undefined,
518
+ ): Promise<VirtualSearchResource[]> {
519
+ if (OMP_ROOT_URL_RE.test(rawPath)) {
520
+ const completions = await internalRouter.complete("omp", "");
521
+ if (completions && completions.length > 0) {
522
+ const resources: VirtualSearchResource[] = [];
523
+ const seen = new Set<string>();
524
+ for (const completion of completions) {
525
+ if (seen.has(completion.value)) continue;
526
+ seen.add(completion.value);
527
+ const docUrl = `omp://${completion.value}`;
528
+ const doc = await internalRouter.resolve(docUrl, context);
529
+ if (!doc.sourcePath) {
530
+ resources.push({ path: docUrl, content: doc.content, ranges });
531
+ }
532
+ }
533
+ if (resources.length > 0) return resources;
534
+ }
535
+ }
536
+
537
+ return [{ path: rawPath, content: resource.content, ranges }];
538
+ }
539
+
540
+ async function resolveInternalSearchInputs(opts: {
541
+ pathSpecs: readonly SearchPathSpec[];
542
+ resolvedPaths: string[];
543
+ cwd: string;
544
+ settings: unknown;
545
+ signal?: AbortSignal;
546
+ archiveDisplayMap: ReadonlyMap<string, string>;
547
+ localProtocolOptions?: LocalProtocolOptions;
548
+ }): Promise<InternalSearchInputResolution> {
549
+ const internalRouter = InternalUrlRouter.instance();
550
+ const paths = opts.resolvedPaths.slice();
551
+ const virtualResources: VirtualSearchResource[] = [];
552
+ const virtualPathSet = new Set<string>();
553
+ const virtualInputIndexes = new Set<number>();
554
+ const immutableSourcePaths = new Set<string>();
555
+ let virtualScopePath: string | undefined;
556
+ const context: ResolveContext = {
557
+ cwd: opts.cwd,
558
+ settings: opts.settings,
559
+ signal: opts.signal,
560
+ localProtocolOptions: opts.localProtocolOptions,
561
+ };
562
+
563
+ for (let idx = 0; idx < paths.length; idx++) {
564
+ const rawPath = paths[idx];
565
+ if (!rawPath || opts.archiveDisplayMap.has(rawPath) || !internalRouter.canHandle(rawPath)) {
566
+ continue;
567
+ }
568
+ if (hasGlobPathChars(rawPath)) {
569
+ throw new ToolError(`Glob patterns are not supported for internal URLs: ${rawPath}`);
570
+ }
571
+ const resource = await internalRouter.resolve(rawPath, context);
572
+ if (resource.sourcePath) {
573
+ paths[idx] = resource.sourcePath;
574
+ if (resource.immutable) {
575
+ immutableSourcePaths.add(path.resolve(resource.sourcePath));
576
+ }
577
+ continue;
578
+ }
579
+
580
+ const ranges = opts.pathSpecs[idx]?.ranges;
581
+ const expanded = await expandVirtualInternalResource(rawPath, resource, internalRouter, context, ranges);
582
+ virtualInputIndexes.add(idx);
583
+ for (const virtual of expanded) {
584
+ virtualResources.push(virtual);
585
+ virtualPathSet.add(virtual.path);
586
+ }
587
+ virtualScopePath = virtualScopePath ? `${virtualScopePath}, ${rawPath}` : rawPath;
588
+ }
589
+
590
+ return {
591
+ resolvedPathsByInput: paths,
592
+ paths: paths.filter((_, idx) => !virtualInputIndexes.has(idx)),
593
+ virtualResources,
594
+ virtualPathSet,
595
+ virtualInputIndexes,
596
+ immutableSourcePaths,
597
+ virtualScopePath,
598
+ };
599
+ }
600
+
260
601
  export interface SearchToolDetails {
261
602
  truncation?: TruncationResult;
262
603
  fileLimitReached?: number;
@@ -333,6 +674,17 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
333
674
  cleanup: cleanupArchiveScratch,
334
675
  } = await resolveArchiveSearchPaths(paths, this.session.cwd);
335
676
  try {
677
+ const internalResolution = await resolveInternalSearchInputs({
678
+ pathSpecs,
679
+ resolvedPaths,
680
+ cwd: this.session.cwd,
681
+ settings: this.session.settings,
682
+ signal,
683
+ archiveDisplayMap,
684
+ localProtocolOptions: this.session.localProtocolOptions,
685
+ });
686
+ const searchablePaths = internalResolution.paths;
687
+ const { virtualResources, virtualPathSet, virtualInputIndexes } = internalResolution;
336
688
  // Build the per-file line-range filter (keyed by absolute path) now that
337
689
  // archive entries have been materialized to scratch files. Plain entries
338
690
  // resolve through `resolveReadPath`; archive entries are keyed by the
@@ -341,10 +693,12 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
341
693
  for (let idx = 0; idx < pathSpecs.length; idx++) {
342
694
  const spec = pathSpecs[idx];
343
695
  if (!spec.ranges) continue;
344
- const resolved = resolvedPaths[idx];
696
+ if (virtualInputIndexes.has(idx)) continue;
697
+ const resolved = internalResolution.resolvedPathsByInput[idx];
698
+ if (!resolved) continue;
345
699
  if (resolved === spec.clean && !archiveDisplayMap.has(resolved)) {
346
700
  // Non-archive entry; ensure the cleaned path resolves to a regular file.
347
- const absKey = path.resolve(resolveReadPath(spec.clean, this.session.cwd));
701
+ const absKey = path.resolve(resolveReadPath(resolved, this.session.cwd));
348
702
  const stats = await stat(absKey).catch(() => null);
349
703
  if (!stats) {
350
704
  throw new ToolError(`Path not found for line-range selector: ${spec.original}`);
@@ -360,7 +714,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
360
714
  }
361
715
  }
362
716
 
363
- if (archiveUnreadable.length > 0 && resolvedPaths.length === archiveUnreadable.length) {
717
+ if (archiveUnreadable.length > 0 && searchablePaths.length === archiveUnreadable.length) {
364
718
  // All inputs were archive selectors we couldn't materialize; surface the
365
719
  // reason instead of a downstream "path not found" from the scope resolver.
366
720
  throw new ToolError(
@@ -376,22 +730,58 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
376
730
  const patternHasNewline = normalizedPattern.includes("\n") || normalizedPattern.includes("\\n");
377
731
  const effectiveMultiline = patternHasNewline;
378
732
 
379
- const scope = await resolveToolSearchScope({
380
- rawPaths: resolvedPaths,
381
- cwd: this.session.cwd,
382
- internalUrlAction: "search",
383
- trackImmutableSources: true,
384
- surfaceExactFilePaths: true,
385
- multipathStatHint: " (`paths` entries must each exist relative to cwd)",
386
- });
387
- const { searchPath, isDirectory, multiTargets, exactFilePaths, missingPaths, immutableSourcePaths } = scope;
388
- // When the only input was an archive selector, surface that selector instead
389
- // of the temp scratch path the resolver substituted in.
390
- const scopePath =
391
- resolvedPaths.length === 1 && archiveDisplayMap.get(searchPath)
392
- ? (archiveDisplayMap.get(searchPath) as string)
393
- : scope.scopePath;
394
- if (missingPaths.length > 0 && missingPaths.length === resolvedPaths.length) {
733
+ let searchPath: string;
734
+ let scopePath: string;
735
+ let globFilter: string | undefined;
736
+ let isDirectory: boolean;
737
+ let multiTargets: ResolvedSearchTarget[] | undefined;
738
+ let exactFilePaths: string[] | undefined;
739
+ let missingPaths: string[];
740
+ const immutableSourcePaths = new Set(internalResolution.immutableSourcePaths);
741
+ if (searchablePaths.length > 0) {
742
+ const scope = await resolveToolSearchScope({
743
+ rawPaths: searchablePaths,
744
+ cwd: this.session.cwd,
745
+ internalUrlAction: "search",
746
+ trackImmutableSources: true,
747
+ surfaceExactFilePaths: true,
748
+ multipathStatHint: " (`paths` entries must each exist relative to cwd)",
749
+ settings: this.session.settings,
750
+ signal,
751
+ localProtocolOptions: this.session.localProtocolOptions,
752
+ });
753
+ searchPath = scope.searchPath;
754
+ isDirectory = scope.isDirectory;
755
+ multiTargets = scope.multiTargets;
756
+ exactFilePaths = scope.exactFilePaths;
757
+ missingPaths = scope.missingPaths;
758
+ globFilter = scope.globFilter;
759
+ for (const immutablePath of scope.immutableSourcePaths) {
760
+ immutableSourcePaths.add(immutablePath);
761
+ }
762
+ // When the only input was an archive selector, surface that selector instead
763
+ // of the temp scratch path the resolver substituted in.
764
+ const physicalScopePath =
765
+ searchablePaths.length === 1 && archiveDisplayMap.get(searchPath)
766
+ ? (archiveDisplayMap.get(searchPath) as string)
767
+ : scope.scopePath;
768
+ scopePath = internalResolution.virtualScopePath
769
+ ? `${physicalScopePath}, ${internalResolution.virtualScopePath}`
770
+ : physicalScopePath;
771
+ } else {
772
+ searchPath = this.session.cwd;
773
+ scopePath = internalResolution.virtualScopePath ?? ".";
774
+ globFilter = undefined;
775
+ isDirectory = false;
776
+ multiTargets = undefined;
777
+ exactFilePaths = undefined;
778
+ missingPaths = [];
779
+ }
780
+ if (
781
+ missingPaths.length > 0 &&
782
+ missingPaths.length === searchablePaths.length &&
783
+ virtualResources.length === 0
784
+ ) {
395
785
  const archiveHint =
396
786
  archiveUnreadable.length > 0
397
787
  ? ` (archive members were not searchable: ${archiveUnreadable.join(", ")})`
@@ -400,34 +790,78 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
400
790
  `Path not found: ${missingPaths.join(", ")}; pass each path as its own array element${archiveHint}`,
401
791
  );
402
792
  }
403
- const { globFilter } = scope;
404
793
  const baseDisplayMode = resolveFileDisplayMode(this.session);
405
794
 
406
795
  const effectiveOutputMode = GrepOutputMode.Content;
407
- // Multi-scope = more than one file may match. We fetch up to
408
- // INTERNAL_TOTAL_CAP matches from native grep, then in JS group by
409
- // file, apply a per-file cap (so one hot file doesn't crowd the
410
- // window), and round-robin emit from up to DEFAULT_FILE_LIMIT files.
411
- const isMultiScope = isDirectory || Boolean(exactFilePaths) || Boolean(multiTargets);
796
+ const isMultiScope =
797
+ isDirectory ||
798
+ Boolean(exactFilePaths) ||
799
+ Boolean(multiTargets) ||
800
+ (virtualResources.length > 0 && (virtualResources.length > 1 || searchablePaths.length > 0));
412
801
  const perFileMatchCap = isMultiScope ? MULTI_FILE_PER_FILE_MATCHES : SINGLE_FILE_MATCHES;
413
802
 
414
803
  // Run grep
415
- let result: GrepResult;
804
+ let result: GrepResult = {
805
+ matches: [],
806
+ totalMatches: 0,
807
+ filesWithMatches: 0,
808
+ filesSearched: 0,
809
+ limitReached: false,
810
+ };
416
811
  try {
417
- if (exactFilePaths || multiTargets) {
418
- const matches: GrepMatch[] = [];
419
- let limitReached = false;
420
- let totalMatches = 0;
421
- let filesSearched = 0;
422
- const targets = exactFilePaths
423
- ? exactFilePaths.map(filePath => ({ basePath: filePath, glob: undefined as string | undefined }))
424
- : (multiTargets ?? []);
425
- for (const target of targets) {
426
- const targetResult = await grep(
812
+ if (searchablePaths.length > 0) {
813
+ if (exactFilePaths || multiTargets) {
814
+ const matches: GrepMatch[] = [];
815
+ let limitReached = false;
816
+ let totalMatches = 0;
817
+ let filesSearched = 0;
818
+ const targets = exactFilePaths
819
+ ? exactFilePaths.map(filePath => ({
820
+ basePath: filePath,
821
+ glob: undefined as string | undefined,
822
+ }))
823
+ : (multiTargets ?? []);
824
+ for (const target of targets) {
825
+ const targetResult = await grep(
826
+ {
827
+ pattern: normalizedPattern,
828
+ path: target.basePath,
829
+ glob: target.glob,
830
+ ignoreCase,
831
+ multiline: effectiveMultiline,
832
+ hidden: true,
833
+ gitignore: useGitignore,
834
+ cache: false,
835
+ maxCount: INTERNAL_TOTAL_CAP,
836
+ contextBefore: normalizedContextBefore,
837
+ contextAfter: normalizedContextAfter,
838
+ maxColumns: DEFAULT_MAX_COLUMN,
839
+ mode: effectiveOutputMode,
840
+ },
841
+ undefined,
842
+ );
843
+ limitReached = limitReached || Boolean(targetResult.limitReached);
844
+ totalMatches += targetResult.totalMatches;
845
+ filesSearched += targetResult.filesSearched;
846
+ for (const match of targetResult.matches) {
847
+ const absolute = path.resolve(target.basePath, match.path);
848
+ const rebased = path.relative(searchPath, absolute).replace(/\\/g, "/");
849
+ matches.push({ ...match, path: rebased });
850
+ }
851
+ }
852
+ result = {
853
+ matches,
854
+ totalMatches: exactFilePaths ? matches.length : totalMatches,
855
+ filesWithMatches: new Set(matches.map(match => match.path)).size,
856
+ filesSearched: exactFilePaths ? exactFilePaths.length : filesSearched,
857
+ limitReached,
858
+ };
859
+ } else {
860
+ result = await grep(
427
861
  {
428
862
  pattern: normalizedPattern,
429
- path: target.basePath,
430
- glob: target.glob,
863
+ path: searchPath,
864
+ glob: globFilter,
431
865
  ignoreCase,
432
866
  multiline: effectiveMultiline,
433
867
  hidden: true,
@@ -441,41 +875,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
441
875
  },
442
876
  undefined,
443
877
  );
444
- limitReached = limitReached || Boolean(targetResult.limitReached);
445
- totalMatches += targetResult.totalMatches;
446
- filesSearched += targetResult.filesSearched;
447
- for (const match of targetResult.matches) {
448
- const absolute = path.resolve(target.basePath, match.path);
449
- const rebased = path.relative(searchPath, absolute).replace(/\\/g, "/");
450
- matches.push({ ...match, path: rebased });
451
- }
452
878
  }
453
- result = {
454
- matches,
455
- totalMatches: exactFilePaths ? matches.length : totalMatches,
456
- filesWithMatches: new Set(matches.map(match => match.path)).size,
457
- filesSearched: exactFilePaths ? exactFilePaths.length : filesSearched,
458
- limitReached,
459
- };
460
- } else {
461
- result = await grep(
462
- {
463
- pattern: normalizedPattern,
464
- path: searchPath,
465
- glob: globFilter,
466
- ignoreCase,
467
- multiline: effectiveMultiline,
468
- hidden: true,
469
- gitignore: useGitignore,
470
- cache: false,
471
- maxCount: INTERNAL_TOTAL_CAP,
472
- contextBefore: normalizedContextBefore,
473
- contextAfter: normalizedContextAfter,
474
- maxColumns: DEFAULT_MAX_COLUMN,
475
- mode: effectiveOutputMode,
476
- },
477
- undefined,
478
- );
479
879
  }
480
880
  } catch (err) {
481
881
  if (err instanceof Error && /^regex(?: parse)? error/i.test(err.message)) {
@@ -483,6 +883,16 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
483
883
  }
484
884
  throw err;
485
885
  }
886
+ const virtualResult = searchVirtualResources(
887
+ virtualResources,
888
+ normalizedPattern,
889
+ ignoreCase,
890
+ effectiveMultiline,
891
+ normalizedContextBefore,
892
+ normalizedContextAfter,
893
+ INTERNAL_TOTAL_CAP,
894
+ );
895
+ result = mergeGrepResults(result, virtualResult, INTERNAL_TOTAL_CAP);
486
896
  if (rangesByAbsPath.size > 0) {
487
897
  const filteredMatches: GrepMatch[] = [];
488
898
  for (const match of result.matches) {
@@ -521,7 +931,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
521
931
  }
522
932
 
523
933
  const formatPath = (filePath: string): string =>
524
- archiveDisplaySet.has(filePath)
934
+ archiveDisplaySet.has(filePath) || virtualPathSet.has(filePath)
525
935
  ? filePath
526
936
  : formatResultPath(filePath, isDirectory, searchPath, this.session.cwd);
527
937
 
@@ -612,7 +1022,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
612
1022
  const hashContexts = new Map<string, { tag: string }>();
613
1023
  if (baseDisplayMode.hashLines) {
614
1024
  for (const relativePath of fileList) {
615
- if (archiveDisplaySet.has(relativePath)) continue;
1025
+ if (archiveDisplaySet.has(relativePath) || virtualPathSet.has(relativePath)) continue;
616
1026
  const absoluteFilePath = path.resolve(this.session.cwd, relativePath);
617
1027
  if (immutableSourcePaths.has(absoluteFilePath)) continue;
618
1028
  // Mint a whole-file content tag so any anchor validates while the
@@ -668,7 +1078,8 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
668
1078
  }
669
1079
  return { model: modelOut, display: displayOut };
670
1080
  };
671
- if (isDirectory) {
1081
+ const useGroupedOutput = isDirectory || isMultiScope;
1082
+ if (useGroupedOutput) {
672
1083
  const grouped = formatGroupedFiles(fileList, relativePath => {
673
1084
  const rendered = renderMatchesForFile(relativePath);
674
1085
  const hashContext = hashContexts.get(relativePath);
@@ -902,6 +1313,10 @@ export const searchToolRenderer = {
902
1313
  .slice(2)
903
1314
  .trimEnd()
904
1315
  .replace(/\s+\([^)]*\)\s*$/, "");
1316
+ if (INTERNAL_URL_DISPLAY_RE.test(raw)) {
1317
+ contextDir = "";
1318
+ return uiTheme.fg("accent", line);
1319
+ }
905
1320
  const isDirectory = raw.endsWith("/");
906
1321
  const name = isDirectory ? raw.replace(/\/$/, "") : raw.replace(/#[0-9a-f]+$/, "");
907
1322
  if (isDirectory) {