@oh-my-pi/pi-coding-agent 15.7.2 → 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.
- package/CHANGELOG.md +75 -6
- package/dist/types/cli/args.d.ts +1 -1
- package/dist/types/cli/extension-flags.d.ts +36 -0
- package/dist/types/config/config-file.d.ts +4 -0
- package/dist/types/config/file-lock.d.ts +23 -0
- package/dist/types/config/keybindings.d.ts +2 -1
- package/dist/types/config/model-registry.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +88 -65
- package/dist/types/edit/hashline/diff.d.ts +3 -3
- package/dist/types/eval/__tests__/agent-bridge.test.d.ts +1 -0
- package/dist/types/eval/__tests__/budget-bridge.test.d.ts +1 -0
- package/dist/types/eval/__tests__/idle-timeout.test.d.ts +1 -0
- package/dist/types/eval/agent-bridge.d.ts +25 -0
- package/dist/types/eval/backend.d.ts +17 -2
- package/dist/types/eval/budget-bridge.d.ts +29 -0
- package/dist/types/eval/idle-timeout.d.ts +28 -0
- package/dist/types/eval/js/executor.d.ts +8 -0
- package/dist/types/eval/js/tool-bridge.d.ts +2 -1
- package/dist/types/eval/py/executor.d.ts +13 -0
- package/dist/types/exec/bash-executor.d.ts +1 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
- package/dist/types/extensibility/extensions/runner.d.ts +7 -0
- package/dist/types/extensibility/plugins/git-url.d.ts +11 -1
- package/dist/types/extensibility/plugins/manager.d.ts +12 -1
- package/dist/types/extensibility/shared-events.d.ts +2 -2
- package/dist/types/memory-backend/index.d.ts +1 -1
- package/dist/types/memory-backend/resolve.d.ts +1 -1
- package/dist/types/memory-backend/types.d.ts +3 -3
- package/dist/types/mnemopi/backend.d.ts +4 -0
- package/dist/types/mnemopi/config.d.ts +29 -0
- package/dist/types/mnemopi/state.d.ts +72 -0
- package/dist/types/modes/components/custom-editor.d.ts +2 -2
- package/dist/types/modes/components/omfg-panel.d.ts +19 -0
- package/dist/types/modes/controllers/command-controller.d.ts +7 -0
- package/dist/types/modes/controllers/input-controller.d.ts +1 -3
- package/dist/types/modes/controllers/omfg-controller.d.ts +10 -0
- package/dist/types/modes/controllers/omfg-rule.d.ts +26 -0
- package/dist/types/modes/gradient-highlight.d.ts +5 -1
- package/dist/types/modes/interactive-mode.d.ts +7 -3
- package/dist/types/modes/magic-keywords.d.ts +14 -0
- package/dist/types/modes/markdown-prose.d.ts +27 -0
- package/dist/types/modes/orchestrate.d.ts +7 -2
- package/dist/types/modes/shared.d.ts +1 -1
- package/dist/types/modes/turn-budget.d.ts +18 -0
- package/dist/types/modes/types.d.ts +7 -3
- package/dist/types/modes/ultrathink.d.ts +7 -2
- package/dist/types/modes/workflow.d.ts +15 -0
- package/dist/types/sdk.d.ts +13 -3
- package/dist/types/session/agent-session.d.ts +40 -17
- package/dist/types/session/session-manager.d.ts +18 -0
- package/dist/types/session/session-storage.d.ts +6 -0
- package/dist/types/session/shake-types.d.ts +24 -0
- package/dist/types/task/executor.d.ts +2 -2
- package/dist/types/tiny/models.d.ts +15 -1
- package/dist/types/tiny/title-protocol.d.ts +4 -0
- package/dist/types/tools/index.d.ts +19 -3
- package/dist/types/tools/memory-edit.d.ts +1 -1
- package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
- package/package.json +10 -10
- package/src/autoresearch/tools/run-experiment.ts +45 -113
- package/src/cli/args.ts +39 -16
- package/src/cli/extension-flags.ts +48 -0
- package/src/cli/plugin-cli.ts +11 -2
- package/src/config/config-file.ts +98 -13
- package/src/config/file-lock.ts +60 -17
- package/src/config/keybindings.ts +78 -27
- package/src/config/model-registry.ts +7 -1
- package/src/config/settings-schema.ts +94 -67
- package/src/config/settings.ts +12 -0
- package/src/edit/hashline/diff.ts +81 -24
- package/src/edit/renderer.ts +16 -12
- package/src/eval/__tests__/agent-bridge.test.ts +433 -0
- package/src/eval/__tests__/budget-bridge.test.ts +69 -0
- package/src/eval/__tests__/idle-timeout.test.ts +66 -0
- package/src/eval/__tests__/shared-executors.test.ts +21 -0
- package/src/eval/agent-bridge.ts +295 -0
- package/src/eval/backend.ts +17 -2
- package/src/eval/budget-bridge.ts +48 -0
- package/src/eval/idle-timeout.ts +80 -0
- package/src/eval/js/executor.ts +35 -7
- package/src/eval/js/index.ts +2 -1
- package/src/eval/js/shared/prelude.txt +85 -1
- package/src/eval/js/tool-bridge.ts +9 -0
- package/src/eval/py/executor.ts +41 -14
- package/src/eval/py/index.ts +2 -1
- package/src/eval/py/prelude.py +132 -1
- package/src/exec/bash-executor.ts +2 -3
- package/src/extensibility/custom-tools/types.ts +2 -2
- package/src/extensibility/extensions/runner.ts +12 -2
- package/src/extensibility/plugins/git-url.ts +90 -4
- package/src/extensibility/plugins/manager.ts +103 -7
- package/src/extensibility/shared-events.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +88 -88
- package/src/main.ts +44 -55
- package/src/memory-backend/index.ts +1 -1
- package/src/memory-backend/resolve.ts +3 -3
- package/src/memory-backend/types.ts +3 -3
- package/src/{mnemosyne → mnemopi}/backend.ts +70 -70
- package/src/{mnemosyne → mnemopi}/config.ts +36 -36
- package/src/{mnemosyne → mnemopi}/state.ts +67 -67
- package/src/modes/components/agent-dashboard.ts +6 -6
- package/src/modes/components/custom-editor.ts +4 -11
- package/src/modes/components/extensions/state-manager.ts +3 -4
- package/src/modes/components/footer.ts +8 -9
- package/src/modes/components/hook-selector.ts +86 -20
- package/src/modes/components/oauth-selector.ts +93 -21
- package/src/modes/components/omfg-panel.ts +141 -0
- package/src/modes/components/settings-defs.ts +2 -2
- package/src/modes/components/tips.txt +2 -1
- package/src/modes/components/tool-execution.ts +38 -19
- package/src/modes/components/tree-selector.ts +4 -3
- package/src/modes/components/user-message-selector.ts +94 -19
- package/src/modes/components/user-message.ts +8 -1
- package/src/modes/controllers/command-controller.ts +57 -0
- package/src/modes/controllers/event-controller.ts +60 -2
- package/src/modes/controllers/input-controller.ts +14 -11
- package/src/modes/controllers/omfg-controller.ts +283 -0
- package/src/modes/controllers/omfg-rule.ts +647 -0
- package/src/modes/controllers/selector-controller.ts +1 -0
- package/src/modes/gradient-highlight.ts +23 -6
- package/src/modes/interactive-mode.ts +41 -7
- package/src/modes/magic-keywords.ts +20 -0
- package/src/modes/markdown-prose.ts +247 -0
- package/src/modes/orchestrate.ts +17 -11
- package/src/modes/shared.ts +3 -11
- package/src/modes/turn-budget.ts +31 -0
- package/src/modes/types.ts +7 -1
- package/src/modes/ultrathink.ts +16 -10
- package/src/modes/utils/hotkeys-markdown.ts +1 -1
- package/src/modes/workflow.ts +42 -0
- package/src/prompts/system/omfg-user.md +51 -0
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/prompts/system/workflow-notice.md +70 -0
- package/src/prompts/tools/eval.md +13 -1
- package/src/prompts/tools/memory-edit.md +1 -1
- package/src/sdk.ts +63 -33
- package/src/session/agent-session.ts +373 -56
- package/src/session/session-manager.ts +32 -0
- package/src/session/session-storage.ts +68 -8
- package/src/session/shake-types.ts +44 -0
- package/src/slash-commands/builtin-registry.ts +41 -16
- package/src/task/executor.ts +3 -3
- package/src/task/index.ts +6 -6
- package/src/tiny/models.ts +30 -2
- package/src/tiny/title-protocol.ts +11 -1
- package/src/tiny/worker.ts +19 -7
- package/src/tools/eval.ts +202 -26
- package/src/tools/grouped-file-output.ts +9 -2
- package/src/tools/index.ts +17 -5
- package/src/tools/memory-edit.ts +4 -4
- package/src/tools/memory-recall.ts +5 -5
- package/src/tools/memory-reflect.ts +5 -5
- package/src/tools/memory-retain.ts +4 -4
- package/src/tools/render-utils.ts +2 -1
- package/src/tools/search.ts +480 -76
- package/dist/types/mnemosyne/backend.d.ts +0 -4
- package/dist/types/mnemosyne/config.d.ts +0 -29
- package/dist/types/mnemosyne/state.d.ts +0 -72
- /package/dist/types/{mnemosyne → mnemopi}/index.d.ts +0 -0
- /package/src/{mnemosyne → mnemopi}/index.ts +0 -0
package/src/tools/search.ts
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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 &&
|
|
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
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
:
|
|
394
|
-
|
|
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
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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 (
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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:
|
|
430
|
-
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
|
-
|
|
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;
|