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