@oh-my-pi/pi-coding-agent 15.7.1 → 15.7.3

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