feat(coding-agent): added unified path targets for grep family
- Consolidated grep, ast-grep, and ast-edit on required `path`, replacing `glob`/`lang`/`sel` with inline file, dir, glob, list, and URL targets. - Changed ast-grep and grep schemas to require a single `pat` string and use `skip` pagination instead of array patterns or offsets. - Updated argument validation to reject empty `path` and invalid `skip`, and routed grep context to session settings only. - Updated tool prompts and tests to reflect new path globbing semantics and `first N` truncation output text.
This commit is contained in:
@@ -177,16 +177,11 @@ export class CursorExecHandlers implements ICursorExecHandlers {
|
||||
|
||||
async grep(args: Parameters<NonNullable<ICursorExecHandlers["grep"]>>[0]) {
|
||||
const toolCallId = decodeToolCallId(args.toolCallId);
|
||||
const grepPath = args.glob ? `${args.path || "."}/${args.glob}` : args.path || ".";
|
||||
const toolResultMessage = await executeTool(this.options, "grep", toolCallId, {
|
||||
pattern: args.pattern,
|
||||
path: args.path || undefined,
|
||||
glob: args.glob || undefined,
|
||||
mode: args.outputMode || undefined,
|
||||
context: args.context ?? args.contextBefore ?? args.contextAfter ?? undefined,
|
||||
ignore_case: args.caseInsensitive || undefined,
|
||||
type: args.type || undefined,
|
||||
limit: args.headLimit ?? undefined,
|
||||
multiline: args.multiline || undefined,
|
||||
path: grepPath,
|
||||
i: args.caseInsensitive || undefined,
|
||||
});
|
||||
return toolResultMessage;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as nodePath from "node:path";
|
||||
import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
|
||||
import { StringEnum } from "@oh-my-pi/pi-coding-agent";
|
||||
import { StringEnum } from "@oh-my-pi/pi-ai";
|
||||
import {
|
||||
ChunkAnchorStyle,
|
||||
ChunkEditOp,
|
||||
|
||||
@@ -2,12 +2,12 @@ Performs structural AST-aware rewrites via native ast-grep.
|
||||
|
||||
<instruction>
|
||||
- Use for codemods and structural rewrites where plain text replace is unsafe
|
||||
- `path` accepts a comma-separated list in addition to file/dir/glob
|
||||
- Set `lang` explicitly in mixed-language trees for deterministic rewrites
|
||||
- `path` is required and accepts a file, directory, glob, comma-separated path list, or internal URL
|
||||
- Language is inferred from `path`; narrow `path` to one language for deterministic rewrites
|
||||
- Metavariables captured in `pat` (`$A`, `$$$ARGS`) are substituted into that entry's `out` template
|
||||
- **Patterns match AST structure, not text.** `$NAME` = one node (captured); `$_` = one without binding; `$$$NAME` = zero-or-more (lazy — stops at next matchable element); `$$$` = zero-or-more without binding. Use `$$$NAME`, **NOT** `$$NAME` — the two-dollar form is invalid. Metavariable names are UPPERCASE and **MUST** be the whole AST node — partial text like `prefix$VAR` or `"hello $NAME"` does NOT work
|
||||
- When the same metavariable appears twice, both occurrences **MUST** match identical code (`$A == $A` matches `x == x`, not `x == y`)
|
||||
- Rewrite patterns **MUST** parse as a single valid AST node. For method fragments or body snippets that don't parse standalone, wrap in context (e.g. `class $_ { … }`) and set `sel` to target the inner node — match and replacement target the selected node, not the wrapper. If ast-grep reports `Multiple AST nodes are detected`, wrap and use `sel`
|
||||
- Rewrite patterns **MUST** parse as a single valid AST node. For method fragments or body snippets that don't parse standalone, wrap in context (e.g. `class $_ { … }`)
|
||||
- For TS declarations/methods, tolerate unknown annotations: `async function $NAME($$$ARGS): $_ { $$$BODY }` or `class $_ { method($ARG: $_): $_ { $$$BODY } }`
|
||||
- Delete matched code with empty `out`: `{"pat":"console.log($$$)","out":""}`
|
||||
- Each rewrite is a 1:1 structural substitution — cannot split one capture across multiple nodes or merge multiple captures into one
|
||||
@@ -19,22 +19,18 @@ Performs structural AST-aware rewrites via native ast-grep.
|
||||
</output>
|
||||
|
||||
<examples>
|
||||
# Rename a call site across a directory
|
||||
`{"ops":[{"pat":"oldApi($$$ARGS)","out":"newApi($$$ARGS)"}],"lang":"typescript","path":"src/"}`
|
||||
# Delete matching calls (empty `out` removes the node)
|
||||
`{"ops":[{"pat":"console.log($$$ARGS)","out":""}],"lang":"typescript","path":"src/"}`
|
||||
# Rename a call site across TypeScript files
|
||||
`{"ops":[{"pat":"oldApi($$$ARGS)","out":"newApi($$$ARGS)"}],"path":"src/**/*.ts"}`
|
||||
# Delete matching calls
|
||||
`{"ops":[{"pat":"console.log($$$ARGS)","out":""}],"path":"src/**/*.ts"}`
|
||||
# Rewrite import source path
|
||||
`{"ops":[{"pat":"import { $$$IMPORTS } from \"old-package\"","out":"import { $$$IMPORTS } from \"new-package\""}],"lang":"typescript","path":"src/"}`
|
||||
`{"ops":[{"pat":"import { $$$IMPORTS } from \"old-package\"","out":"import { $$$IMPORTS } from \"new-package\""}],"path":"src/**/*.ts"}`
|
||||
# Modernize to optional chaining (same metavariable enforces identity)
|
||||
`{"ops":[{"pat":"$A && $A()","out":"$A?.()"}],"lang":"typescript","path":"src/"}`
|
||||
`{"ops":[{"pat":"$A && $A()","out":"$A?.()"}],"path":"src/**/*.ts"}`
|
||||
# Swap two arguments using captures
|
||||
`{"ops":[{"pat":"assertEqual($A, $B)","out":"assertEqual($B, $A)"}],"lang":"typescript","path":"tests/"}`
|
||||
# Rename a function declaration while tolerating any return type annotation
|
||||
`{"ops":[{"pat":"async function fetchData($$$ARGS): $_ { $$$BODY }","out":"async function loadData($$$ARGS): $_ { $$$BODY }"}],"sel":"function_declaration","lang":"typescript","path":"src/api.ts"}`
|
||||
# Rewrite a method body fragment by wrapping in parseable context (sel targets the inner node)
|
||||
`{"ops":[{"pat":"class $_ { async execute($INPUT: $_) { $$$BEFORE; const $PARSED = $_.parse($INPUT); $$$AFTER } }","out":"class $_ { async execute($INPUT: $_) { $$$BEFORE; const $PARSED = $SCHEMA.parse($INPUT); $$$AFTER } }"}],"sel":"method_definition","lang":"typescript","path":"src/tools/todo.ts"}`
|
||||
`{"ops":[{"pat":"assertEqual($A, $B)","out":"assertEqual($B, $A)"}],"path":"tests/**/*.ts"}`
|
||||
# Python — convert print calls to logging
|
||||
`{"ops":[{"pat":"print($$$ARGS)","out":"logger.info($$$ARGS)"}],"lang":"python","path":"src/"}`
|
||||
`{"ops":[{"pat":"print($$$ARGS)","out":"logger.info($$$ARGS)"}],"path":"src/**/*.py"}`
|
||||
</examples>
|
||||
|
||||
<critical>
|
||||
|
||||
@@ -2,18 +2,18 @@ Performs structural code search using AST matching via native ast-grep.
|
||||
|
||||
<instruction>
|
||||
- Use when syntax shape matters more than raw text (calls, declarations, specific language constructs)
|
||||
- `path` accepts a comma-separated list in addition to file/dir/glob
|
||||
- Set `lang` explicitly in mixed-language trees to avoid parse noise from non-source files
|
||||
- Multiple patterns in `pat` run in one native pass, merged, then `offset`/`limit` applied
|
||||
- `path` is required and accepts a file, directory, glob, comma-separated path list, or internal URL
|
||||
- Language is inferred from `path`; narrow `path` to one language when mixed-language trees could cause parse noise
|
||||
- `pat` is a single AST pattern. Run separate calls for distinct unrelated patterns
|
||||
- **Patterns match AST structure, not text** — whitespace/formatting is ignored
|
||||
- `$NAME` captures one node; `$_` matches one without binding; `$$$NAME` captures zero-or-more (lazy — stops at next matchable element); `$$$` matches zero-or-more without binding. Use `$$$NAME`, **NOT** `$$NAME` — the two-dollar form is invalid and produces a parse error
|
||||
- Metavariable names are UPPERCASE and must be the whole AST node — partial-text like `prefix$VAR`, `"hello $NAME"`, or `a $OP b` does NOT work; match the whole node instead
|
||||
- When the same metavariable appears twice, both occurrences **MUST** match identical code (`$A == $A` matches `x == x`, not `x == y`)
|
||||
- Patterns **MUST** parse as a single valid AST node for the target language. For method fragments or body snippets that don't parse standalone, wrap in valid context (e.g. `class $_ { … }`) and set `sel` to target the inner node — results return for the selected node, not the outer wrapper. If ast-grep reports `Multiple AST nodes are detected`, the pattern isn't a single parseable node — wrap and use `sel`
|
||||
- C++ qualified calls used as expression statements need the statement semicolon in the pattern: use `ns::doThing($ARG);`, `$CALLEE($ARG)`, or wrap a statement snippet and select `call_expression`. Without `;`, tree-sitter-cpp may parse `ns::doThing($ARG)` as declaration-like syntax and return no matches
|
||||
- Patterns **MUST** parse as a single valid AST node for the inferred target language. For method fragments or body snippets that don't parse standalone, wrap in valid context (e.g. `class $_ { … }`)
|
||||
- C++ qualified calls used as expression statements need the statement semicolon in the pattern: use `ns::doThing($ARG);`, `$CALLEE($ARG);`, or wrap a statement snippet. Without `;`, tree-sitter-cpp may parse `ns::doThing($ARG)` as declaration-like syntax and return no matches
|
||||
- For TS declarations/methods, tolerate unknown annotations: `async function $NAME($$$ARGS): $_ { $$$BODY }` or `class $_ { method($ARG: $_): $_ { $$$BODY } }`
|
||||
- Declaration forms are structurally distinct — top-level `function foo`, class method `foo()`, and `const foo = () => {}` are different AST shapes; search the right form before concluding absence
|
||||
- Loosest existence check: `pat: ["executeBash"]` with `sel: "identifier"`
|
||||
- Loosest existence check: `pat: "executeBash"` with a narrow `path`
|
||||
</instruction>
|
||||
|
||||
<output>
|
||||
@@ -22,26 +22,20 @@ Performs structural code search using AST matching via native ast-grep.
|
||||
</output>
|
||||
|
||||
<examples>
|
||||
# Multi-pattern scoped search
|
||||
`{"pat":["console.log($$$)","console.error($$$)"],"lang":"typescript","path":"src/"}`
|
||||
# Named imports from a specific package (quoted string inside pattern)
|
||||
`{"pat":["import { $$$IMPORTS } from \"react\""],"lang":"typescript","path":"src/"}`
|
||||
# Arrow functions assigned to a const (distinct AST from function declarations)
|
||||
`{"pat":["const $NAME = ($$$ARGS) => $BODY"],"lang":"typescript","path":"src/utils/"}`
|
||||
# Search TypeScript files under src
|
||||
`{"pat":"console.log($$$)","path":"src/**/*.ts"}`
|
||||
# Named imports from a specific package
|
||||
`{"pat":"import { $$$IMPORTS } from \"react\"","path":"src/**/*.ts"}`
|
||||
# Arrow functions assigned to a const
|
||||
`{"pat":"const $NAME = ($$$ARGS) => $BODY","path":"src/utils/**/*.ts"}`
|
||||
# Method call on any object, ignoring method name with `$_`
|
||||
`{"pat":["logger.$_($$$ARGS)"],"lang":"typescript","path":"src/"}`
|
||||
# Contextual pattern with selector — match the identifier `foo`, not the whole call
|
||||
`{"pat":["foo()"],"sel":"identifier","lang":"typescript","path":"src/utils.ts"}`
|
||||
# Match a function declaration while tolerating any return type annotation (sel targets the inner node)
|
||||
`{"pat":["async function processItems($$$ARGS): $_ { $$$BODY }"],"sel":"function_declaration","lang":"typescript","path":"src/worker.ts"}`
|
||||
# Match a method body fragment by wrapping in parseable context and selecting the method
|
||||
`{"pat":["class $_ { async execute($INPUT: $_) { $$$BEFORE; const $PARSED = $_.parse($INPUT); $$$AFTER } }"],"sel":"method_definition","lang":"typescript","path":"src/tools/todo.ts"}`
|
||||
`{"pat":"logger.$_($$$ARGS)","path":"src/**/*.ts"}`
|
||||
# Loosest existence check for a symbol in one file
|
||||
`{"pat":["processItems"],"sel":"identifier","lang":"typescript","path":"src/worker.ts"}`
|
||||
`{"pat":"processItems","path":"src/worker.ts"}`
|
||||
</examples>
|
||||
|
||||
<critical>
|
||||
- Avoid repo-root AST scans when the target is language-specific — narrow `path` first
|
||||
- Parse issues are query failure, not evidence of absence: repair the pattern or tighten `path`/`glob`/`lang` before concluding "no matches"
|
||||
- Avoid repo-root scans — narrow `path` first
|
||||
- Parse issues are query failure, not evidence of absence: repair the pattern or tighten `path` before concluding "no matches"
|
||||
- For broad/open-ended exploration across subsystems, use Task tool with explore subagent first
|
||||
</critical>
|
||||
|
||||
@@ -2,9 +2,8 @@ Searches files using powerful regex matching.
|
||||
|
||||
<instruction>
|
||||
- Supports full regex syntax (e.g., `log.*Error`, `function\\s+\\w+`); literal braces need escaping (`interface\\{\\}` for `interface{}` in Go)
|
||||
- `path` also accepts comma-separated path lists; pair with `glob` when you need a relative file filter in addition to `type`
|
||||
- For cross-line patterns like `struct \\{[\\s\\S]*?field`, set `multiline: true`
|
||||
- If the pattern contains a literal `\n`, `multiline` defaults to true automatically
|
||||
- `path` is required and accepts a file, directory, glob, comma-separated path list, or internal URL
|
||||
- Cross-line patterns are detected from literal `\n` or escaped `\\n` in `pattern`
|
||||
</instruction>
|
||||
|
||||
<output>
|
||||
|
||||
@@ -15,7 +15,6 @@ import type { ToolSession } from ".";
|
||||
import { createFileRecorder, formatResultPath } from "./file-recorder";
|
||||
import type { OutputMeta } from "./output-meta";
|
||||
import {
|
||||
combineSearchGlobs,
|
||||
hasGlobPathChars,
|
||||
normalizePathLikeInput,
|
||||
parseSearchPath,
|
||||
@@ -46,21 +45,10 @@ const astEditSchema = Type.Object({
|
||||
minItems: 1,
|
||||
description: "rewrite ops",
|
||||
}),
|
||||
lang: Type.Optional(Type.String({ description: "language override", examples: ["typescript", "python"] })),
|
||||
path: Type.Optional(
|
||||
Type.String({
|
||||
description: "path or glob to rewrite",
|
||||
examples: ["src/", "src/foo.ts"],
|
||||
}),
|
||||
),
|
||||
glob: Type.Optional(Type.String({ description: "glob filter", examples: ["**/*.ts"] })),
|
||||
sel: Type.Optional(
|
||||
Type.String({
|
||||
description: "contextual pattern selector",
|
||||
examples: ["function_declaration", "call_expression", "identifier"],
|
||||
}),
|
||||
),
|
||||
limit: Type.Optional(Type.Number({ description: "max replacements" })),
|
||||
path: Type.String({
|
||||
description: "file, directory, glob, or comma-separated paths to rewrite",
|
||||
examples: ["src/", "src/foo.ts", "src/**/*.ts"],
|
||||
}),
|
||||
});
|
||||
|
||||
export interface AstEditToolDetails {
|
||||
@@ -115,10 +103,6 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
||||
seenPatterns.add(pat);
|
||||
}
|
||||
const normalizedRewrites = Object.fromEntries(ops);
|
||||
const maxReplacements = params.limit !== undefined ? Math.floor(params.limit) : undefined;
|
||||
if (maxReplacements !== undefined && (!Number.isFinite(maxReplacements) || maxReplacements < 1)) {
|
||||
throw new ToolError("limit must be a positive number");
|
||||
}
|
||||
const maxFiles = $envpos("PI_MAX_AST_FILES", 1000);
|
||||
|
||||
const formatScopePath = (targetPath: string): string => {
|
||||
@@ -127,8 +111,11 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
||||
};
|
||||
let searchPath: string | undefined;
|
||||
let scopePath: string | undefined;
|
||||
let globFilter = params.glob ? normalizePathLikeInput(params.glob) || undefined : undefined;
|
||||
const rawPath = params.path ? normalizePathLikeInput(params.path) || undefined : undefined;
|
||||
let globFilter: string | undefined;
|
||||
const rawPath = normalizePathLikeInput(params.path);
|
||||
if (rawPath.length === 0) {
|
||||
throw new ToolError("`path` must be a non-empty path or glob");
|
||||
}
|
||||
if (rawPath) {
|
||||
const internalRouter = this.session.internalRouter;
|
||||
if (internalRouter?.canHandle(rawPath)) {
|
||||
@@ -150,7 +137,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
||||
} else {
|
||||
const parsedPath = parseSearchPath(rawPath);
|
||||
searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
|
||||
globFilter = combineSearchGlobs(parsedPath.glob, globFilter);
|
||||
globFilter = parsedPath.glob;
|
||||
scopePath = formatScopePath(searchPath);
|
||||
}
|
||||
}
|
||||
@@ -167,12 +154,9 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
||||
|
||||
const result = await astEdit({
|
||||
rewrites: normalizedRewrites,
|
||||
lang: params.lang?.trim(),
|
||||
path: resolvedSearchPath,
|
||||
glob: globFilter,
|
||||
selector: params.sel?.trim(),
|
||||
dryRun: true,
|
||||
maxReplacements,
|
||||
maxFiles,
|
||||
failOnParseError: false,
|
||||
signal,
|
||||
@@ -295,7 +279,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
||||
count: fileReplacementCounts.get(filePath) ?? 0,
|
||||
}));
|
||||
if (result.limitReached) {
|
||||
outputLines.push("", "Limit reached; narrow path or increase limit.");
|
||||
outputLines.push("", "Limit reached; narrow path.");
|
||||
}
|
||||
if (dedupedParseErrors.length) {
|
||||
outputLines.push("", ...formatParseErrors(dedupedParseErrors));
|
||||
@@ -311,12 +295,9 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
||||
apply: async (_reason: string) => {
|
||||
const applyResult = await astEdit({
|
||||
rewrites: normalizedRewrites,
|
||||
lang: params.lang?.trim(),
|
||||
path: resolvedSearchPath,
|
||||
glob: globFilter,
|
||||
selector: params.sel?.trim(),
|
||||
dryRun: false,
|
||||
maxReplacements,
|
||||
maxFiles,
|
||||
failOnParseError: false,
|
||||
});
|
||||
@@ -391,10 +372,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
||||
|
||||
interface AstEditRenderArgs {
|
||||
ops?: Array<{ pat?: string; out?: string }>;
|
||||
lang?: string;
|
||||
path?: string;
|
||||
sel?: string;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
const COLLAPSED_CHANGE_LIMIT = PREVIEW_LIMITS.COLLAPSED_LINES * 2;
|
||||
@@ -403,9 +381,7 @@ export const astEditToolRenderer = {
|
||||
inline: true,
|
||||
renderCall(args: AstEditRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
||||
const meta: string[] = [];
|
||||
if (args.lang) meta.push(`lang:${args.lang}`);
|
||||
if (args.path) meta.push(`in ${args.path}`);
|
||||
if (args.limit !== undefined) meta.push(`limit:${args.limit}`);
|
||||
const rewriteCount = args.ops?.length ?? 0;
|
||||
if (rewriteCount > 1) meta.push(`${rewriteCount} rewrites`);
|
||||
|
||||
@@ -495,7 +471,7 @@ export const astEditToolRenderer = {
|
||||
|
||||
const extraLines: string[] = [];
|
||||
if (limitReached) {
|
||||
extraLines.push(uiTheme.fg("warning", "limit reached; narrow path or increase limit"));
|
||||
extraLines.push(uiTheme.fg("warning", "limit reached; narrow path"));
|
||||
}
|
||||
if (details?.parseErrors?.length) {
|
||||
const total = details.parseErrors.length;
|
||||
|
||||
@@ -15,7 +15,6 @@ import { createFileRecorder, formatResultPath } from "./file-recorder";
|
||||
import { formatMatchLine } from "./match-line-format";
|
||||
import type { OutputMeta } from "./output-meta";
|
||||
import {
|
||||
combineSearchGlobs,
|
||||
hasGlobPathChars,
|
||||
normalizePathLikeInput,
|
||||
parseSearchPath,
|
||||
@@ -36,28 +35,12 @@ import { ToolError } from "./tool-errors";
|
||||
import { toolResult } from "./tool-result";
|
||||
|
||||
const astGrepSchema = Type.Object({
|
||||
pat: Type.Array(Type.String(), {
|
||||
minItems: 1,
|
||||
description: "ast patterns",
|
||||
examples: [["console.log($$$)"]],
|
||||
pat: Type.String({ description: "ast pattern", examples: ["console.log($$$)"] }),
|
||||
path: Type.String({
|
||||
description: "file, directory, glob, or comma-separated paths to search",
|
||||
examples: ["src/", "src/foo.ts", "src/**/*.ts"],
|
||||
}),
|
||||
lang: Type.Optional(Type.String({ description: "language override", examples: ["typescript", "python"] })),
|
||||
path: Type.Optional(
|
||||
Type.String({
|
||||
description: "path or glob to search",
|
||||
examples: ["src/", "src/foo.ts"],
|
||||
}),
|
||||
),
|
||||
glob: Type.Optional(Type.String({ description: "glob filter", examples: ["**/*.ts"] })),
|
||||
sel: Type.Optional(
|
||||
Type.String({
|
||||
description: "contextual pattern selector",
|
||||
examples: ["function_declaration", "call_expression", "identifier"],
|
||||
}),
|
||||
),
|
||||
limit: Type.Optional(Type.Number({ description: "max matches", default: 50 })),
|
||||
offset: Type.Optional(Type.Number({ description: "skip first n matches", default: 0 })),
|
||||
context: Type.Optional(Type.Number({ description: "context lines" })),
|
||||
skip: Type.Optional(Type.Number({ description: "matches to skip", default: 0 })),
|
||||
});
|
||||
|
||||
export interface AstGrepToolDetails {
|
||||
@@ -94,55 +77,48 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
||||
_context?: AgentToolContext,
|
||||
): Promise<AgentToolResult<AstGrepToolDetails>> {
|
||||
return untilAborted(signal, async () => {
|
||||
const patterns = [...new Set(params.pat.map(pattern => pattern.trim()).filter(pattern => pattern.length > 0))];
|
||||
if (patterns.length === 0) {
|
||||
throw new ToolError("`pat` must include at least one non-empty pattern");
|
||||
const pattern = params.pat.trim();
|
||||
if (pattern.length === 0) {
|
||||
throw new ToolError("`pat` must be a non-empty pattern");
|
||||
}
|
||||
const limit = params.limit === undefined ? 50 : Math.floor(params.limit);
|
||||
if (!Number.isFinite(limit) || limit < 1) {
|
||||
throw new ToolError("Limit must be a positive number");
|
||||
const patterns = [pattern];
|
||||
const skip = params.skip === undefined ? 0 : Math.floor(params.skip);
|
||||
if (!Number.isFinite(skip) || skip < 0) {
|
||||
throw new ToolError("skip must be a non-negative number");
|
||||
}
|
||||
const offset = params.offset === undefined ? 0 : Math.floor(params.offset);
|
||||
if (!Number.isFinite(offset) || offset < 0) {
|
||||
throw new ToolError("Offset must be a non-negative number");
|
||||
}
|
||||
const context = params.context === undefined ? undefined : Math.floor(params.context);
|
||||
if (context !== undefined && (!Number.isFinite(context) || context < 0)) {
|
||||
throw new ToolError("Context must be a non-negative number");
|
||||
}
|
||||
|
||||
const formatScopePath = (targetPath: string): string => {
|
||||
const relative = path.relative(this.session.cwd, targetPath).replace(/\\/g, "/");
|
||||
return relative.length === 0 ? "." : relative;
|
||||
};
|
||||
let searchPath: string | undefined;
|
||||
let scopePath: string | undefined;
|
||||
let globFilter = params.glob ? normalizePathLikeInput(params.glob) || undefined : undefined;
|
||||
const rawPath = params.path ? normalizePathLikeInput(params.path) || undefined : undefined;
|
||||
if (rawPath) {
|
||||
const internalRouter = this.session.internalRouter;
|
||||
if (internalRouter?.canHandle(rawPath)) {
|
||||
if (hasGlobPathChars(rawPath)) {
|
||||
throw new ToolError(`Glob patterns are not supported for internal URLs: ${rawPath}`);
|
||||
}
|
||||
const resource = await internalRouter.resolve(rawPath);
|
||||
if (!resource.sourcePath) {
|
||||
throw new ToolError(`Cannot search internal URL without backing file: ${rawPath}`);
|
||||
}
|
||||
searchPath = resource.sourcePath;
|
||||
scopePath = formatScopePath(searchPath);
|
||||
let globFilter: string | undefined;
|
||||
const rawPath = normalizePathLikeInput(params.path);
|
||||
if (rawPath.length === 0) {
|
||||
throw new ToolError("`path` must be a non-empty path or glob");
|
||||
}
|
||||
const internalRouter = this.session.internalRouter;
|
||||
if (internalRouter?.canHandle(rawPath)) {
|
||||
if (hasGlobPathChars(rawPath)) {
|
||||
throw new ToolError(`Glob patterns are not supported for internal URLs: ${rawPath}`);
|
||||
}
|
||||
const resource = await internalRouter.resolve(rawPath);
|
||||
if (!resource.sourcePath) {
|
||||
throw new ToolError(`Cannot search internal URL without backing file: ${rawPath}`);
|
||||
}
|
||||
searchPath = resource.sourcePath;
|
||||
scopePath = formatScopePath(searchPath);
|
||||
} else {
|
||||
const multiSearchPath = await resolveMultiSearchPath(rawPath, this.session.cwd, globFilter);
|
||||
if (multiSearchPath) {
|
||||
searchPath = multiSearchPath.basePath;
|
||||
globFilter = multiSearchPath.glob;
|
||||
scopePath = multiSearchPath.scopePath;
|
||||
} else {
|
||||
const multiSearchPath = await resolveMultiSearchPath(rawPath, this.session.cwd, globFilter);
|
||||
if (multiSearchPath) {
|
||||
searchPath = multiSearchPath.basePath;
|
||||
globFilter = multiSearchPath.glob;
|
||||
scopePath = multiSearchPath.scopePath;
|
||||
} else {
|
||||
const parsedPath = parseSearchPath(rawPath);
|
||||
searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
|
||||
globFilter = combineSearchGlobs(parsedPath.glob, globFilter);
|
||||
scopePath = formatScopePath(searchPath);
|
||||
}
|
||||
const parsedPath = parseSearchPath(rawPath);
|
||||
searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
|
||||
globFilter = parsedPath.glob;
|
||||
scopePath = formatScopePath(searchPath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,13 +134,9 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
||||
|
||||
const result = await astGrep({
|
||||
patterns,
|
||||
lang: params.lang?.trim(),
|
||||
path: resolvedSearchPath,
|
||||
glob: globFilter,
|
||||
selector: params.sel?.trim(),
|
||||
limit,
|
||||
offset,
|
||||
context,
|
||||
offset: skip,
|
||||
includeMeta: true,
|
||||
signal,
|
||||
});
|
||||
@@ -201,7 +173,7 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
||||
|
||||
if (result.matches.length === 0) {
|
||||
const noMatchMessage = dedupedParseErrors.length
|
||||
? "No matches found. Parse issues mean the query may be mis-scoped; narrow `path`/`glob` or set `lang` before concluding absence."
|
||||
? "No matches found. Parse issues mean the query may be mis-scoped; narrow `path` before concluding absence."
|
||||
: "No matches found";
|
||||
const parseMessage = dedupedParseErrors.length
|
||||
? `\n${formatParseErrors(dedupedParseErrors).join("\n")}`
|
||||
@@ -308,13 +280,9 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
||||
// =============================================================================
|
||||
|
||||
interface AstGrepRenderArgs {
|
||||
pat?: string[];
|
||||
lang?: string;
|
||||
pat?: string;
|
||||
path?: string;
|
||||
sel?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
context?: number;
|
||||
skip?: number;
|
||||
}
|
||||
|
||||
const COLLAPSED_MATCH_LIMIT = PREVIEW_LIMITS.COLLAPSED_LINES * 2;
|
||||
@@ -323,15 +291,10 @@ export const astGrepToolRenderer = {
|
||||
inline: true,
|
||||
renderCall(args: AstGrepRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
||||
const meta: string[] = [];
|
||||
if (args.lang) meta.push(`lang:${args.lang}`);
|
||||
if (args.path) meta.push(`in ${args.path}`);
|
||||
if (args.sel) meta.push("selector");
|
||||
if (args.limit !== undefined && args.limit > 0) meta.push(`limit:${args.limit}`);
|
||||
if (args.offset !== undefined && args.offset > 0) meta.push(`offset:${args.offset}`);
|
||||
if (args.context !== undefined) meta.push(`context:${args.context}`);
|
||||
if (args.pat && args.pat.length > 1) meta.push(`${args.pat.length} patterns`);
|
||||
if (args.skip !== undefined && args.skip > 0) meta.push(`skip:${args.skip}`);
|
||||
|
||||
const description = args.pat?.length === 1 ? args.pat[0] : args.pat ? `${args.pat.length} patterns` : "?";
|
||||
const description = args.pat ?? "?";
|
||||
const text = renderStatusLine({ icon: "pending", title: "AST Grep", description, meta }, uiTheme);
|
||||
return new Text(text, 0, 0);
|
||||
},
|
||||
@@ -355,19 +318,14 @@ export const astGrepToolRenderer = {
|
||||
const limitReached = details?.limitReached ?? false;
|
||||
|
||||
if (matchCount === 0) {
|
||||
const description = args?.pat?.length === 1 ? args.pat[0] : undefined;
|
||||
const description = args?.pat;
|
||||
const meta = ["0 matches"];
|
||||
if (details?.scopePath) meta.push(`in ${details.scopePath}`);
|
||||
if (filesSearched > 0) meta.push(`searched ${filesSearched}`);
|
||||
const header = renderStatusLine({ icon: "warning", title: "AST Grep", description, meta }, uiTheme);
|
||||
const lines = [header, formatEmptyMessage("No matches found", uiTheme)];
|
||||
if (details?.parseErrors?.length) {
|
||||
lines.push(
|
||||
uiTheme.fg(
|
||||
"warning",
|
||||
"Query may be mis-scoped; narrow `path`/`glob` or set `lang` before concluding absence",
|
||||
),
|
||||
);
|
||||
lines.push(uiTheme.fg("warning", "Query may be mis-scoped; narrow `path` before concluding absence"));
|
||||
const capped = details.parseErrors.slice(0, PARSE_ERRORS_LIMIT);
|
||||
for (const err of capped) {
|
||||
lines.push(uiTheme.fg("warning", ` - ${err}`));
|
||||
@@ -384,7 +342,7 @@ export const astGrepToolRenderer = {
|
||||
if (details?.scopePath) meta.push(`in ${details.scopePath}`);
|
||||
meta.push(`searched ${filesSearched}`);
|
||||
if (limitReached) meta.push(uiTheme.fg("warning", "limit reached"));
|
||||
const description = args?.pat?.length === 1 ? args.pat[0] : undefined;
|
||||
const description = args?.pat;
|
||||
const header = renderStatusLine(
|
||||
{ icon: limitReached ? "warning" : "success", title: "AST Grep", description, meta },
|
||||
uiTheme,
|
||||
|
||||
@@ -19,7 +19,6 @@ import { createFileRecorder } from "./file-recorder";
|
||||
import { formatMatchLine } from "./match-line-format";
|
||||
import { formatFullOutputReference, type OutputMeta } from "./output-meta";
|
||||
import {
|
||||
combineSearchGlobs,
|
||||
hasGlobPathChars,
|
||||
normalizePathLikeInput,
|
||||
parseSearchPath,
|
||||
@@ -38,16 +37,13 @@ import { toolResult } from "./tool-result";
|
||||
|
||||
const grepSchema = Type.Object({
|
||||
pattern: Type.String({ description: "regex pattern", examples: ["function\\s+\\w+", "TODO"] }),
|
||||
path: Type.Optional(Type.String({ description: "path to search", examples: ["src/", "src/foo.ts"] })),
|
||||
glob: Type.Optional(Type.String({ description: "glob filter", examples: ["*.js"] })),
|
||||
type: Type.Optional(Type.String({ description: "file type filter", examples: ["js", "py", "rust"] })),
|
||||
path: Type.String({
|
||||
description: "file, directory, glob, comma-separated paths, or internal URL to search",
|
||||
examples: ["src/", "src/foo.ts", "src/**/*.ts"],
|
||||
}),
|
||||
i: Type.Optional(Type.Boolean({ description: "case-insensitive search", default: false })),
|
||||
pre: Type.Optional(Type.Number({ description: "lines before matches" })),
|
||||
post: Type.Optional(Type.Number({ description: "lines after matches" })),
|
||||
multiline: Type.Optional(Type.Boolean({ description: "multiline matching" })),
|
||||
gitignore: Type.Optional(Type.Boolean({ description: "respect gitignore", default: true })),
|
||||
limit: Type.Optional(Type.Number({ description: "max matches", default: 20 })),
|
||||
offset: Type.Optional(Type.Number({ description: "skip first n matches", default: 0 })),
|
||||
skip: Type.Optional(Type.Number({ description: "matches to skip", default: 0 })),
|
||||
});
|
||||
|
||||
export type GrepToolInput = Static<typeof grepSchema>;
|
||||
@@ -98,7 +94,7 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
||||
_onUpdate?: AgentToolUpdateCallback<GrepToolDetails>,
|
||||
_toolContext?: AgentToolContext,
|
||||
): Promise<AgentToolResult<GrepToolDetails>> {
|
||||
const { pattern, path: searchDir, glob, type, i, gitignore, pre, post, multiline, limit, offset } = params;
|
||||
const { pattern, path: searchDir, i, gitignore, skip } = params;
|
||||
|
||||
return untilAborted(signal, async () => {
|
||||
const normalizedPattern = pattern.trim();
|
||||
@@ -107,25 +103,16 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
||||
throw new ToolError("Pattern must not be empty");
|
||||
}
|
||||
|
||||
const normalizedOffset = offset === undefined ? 0 : Number.isFinite(offset) ? Math.floor(offset) : Number.NaN;
|
||||
if (normalizedOffset < 0 || !Number.isFinite(normalizedOffset)) {
|
||||
throw new ToolError("Offset must be a non-negative number");
|
||||
const normalizedSkip = skip === undefined ? 0 : Number.isFinite(skip) ? Math.floor(skip) : Number.NaN;
|
||||
if (normalizedSkip < 0 || !Number.isFinite(normalizedSkip)) {
|
||||
throw new ToolError("Skip must be a non-negative number");
|
||||
}
|
||||
|
||||
const rawLimit = limit === undefined ? undefined : Number.isFinite(limit) ? Math.floor(limit) : Number.NaN;
|
||||
if (rawLimit !== undefined && (!Number.isFinite(rawLimit) || rawLimit < 0)) {
|
||||
throw new ToolError("Limit must be a non-negative number");
|
||||
}
|
||||
const normalizedLimit = rawLimit !== undefined && rawLimit > 0 ? rawLimit : undefined;
|
||||
|
||||
const defaultContextBefore = this.session.settings.get("grep.contextBefore");
|
||||
const defaultContextAfter = this.session.settings.get("grep.contextAfter");
|
||||
const normalizedContextBefore = pre ?? defaultContextBefore;
|
||||
const normalizedContextAfter = post ?? defaultContextAfter;
|
||||
const normalizedContextBefore = this.session.settings.get("grep.contextBefore");
|
||||
const normalizedContextAfter = this.session.settings.get("grep.contextAfter");
|
||||
const ignoreCase = i ?? false;
|
||||
const useGitignore = gitignore ?? true;
|
||||
const patternHasNewline = normalizedPattern.includes("\n") || normalizedPattern.includes("\\n");
|
||||
const effectiveMultiline = multiline ?? patternHasNewline;
|
||||
const effectiveMultiline = patternHasNewline;
|
||||
|
||||
const useHashLines = resolveFileDisplayMode(this.session).hashLines;
|
||||
const formatScopePath = (targetPath: string): string => {
|
||||
@@ -135,41 +122,36 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
||||
let searchPath: string;
|
||||
let scopePath: string;
|
||||
let exactFilePaths: string[] | undefined;
|
||||
let globFilter = glob ? normalizePathLikeInput(glob) || undefined : undefined;
|
||||
const internalRouter = this.session.internalRouter;
|
||||
if (searchDir?.trim()) {
|
||||
const rawPath = normalizePathLikeInput(searchDir);
|
||||
if (internalRouter?.canHandle(rawPath)) {
|
||||
if (hasGlobPathChars(rawPath)) {
|
||||
throw new ToolError(`Glob patterns are not supported for internal URLs: ${rawPath}`);
|
||||
}
|
||||
const resource = await internalRouter.resolve(rawPath);
|
||||
if (!resource.sourcePath) {
|
||||
throw new ToolError(`Cannot grep internal URL without a backing file: ${rawPath}`);
|
||||
}
|
||||
searchPath = resource.sourcePath;
|
||||
scopePath = formatScopePath(searchPath);
|
||||
} else {
|
||||
const multiSearchPath = await resolveMultiSearchPath(rawPath, this.session.cwd, globFilter);
|
||||
if (multiSearchPath) {
|
||||
searchPath = multiSearchPath.basePath;
|
||||
globFilter = multiSearchPath.exactFilePaths ? undefined : multiSearchPath.glob;
|
||||
exactFilePaths = multiSearchPath.exactFilePaths;
|
||||
scopePath = multiSearchPath.scopePath;
|
||||
} else {
|
||||
const parsedPath = parseSearchPath(rawPath);
|
||||
searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
|
||||
if (parsedPath.glob) {
|
||||
globFilter = combineSearchGlobs(parsedPath.glob, globFilter);
|
||||
}
|
||||
scopePath = formatScopePath(searchPath);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
searchPath = resolveToCwd(".", this.session.cwd);
|
||||
scopePath = ".";
|
||||
let globFilter: string | undefined;
|
||||
const rawPath = normalizePathLikeInput(searchDir);
|
||||
if (rawPath.length === 0) {
|
||||
throw new ToolError("`path` must be a non-empty path or glob");
|
||||
}
|
||||
const internalRouter = this.session.internalRouter;
|
||||
if (internalRouter?.canHandle(rawPath)) {
|
||||
if (hasGlobPathChars(rawPath)) {
|
||||
throw new ToolError(`Glob patterns are not supported for internal URLs: ${rawPath}`);
|
||||
}
|
||||
const resource = await internalRouter.resolve(rawPath);
|
||||
if (!resource.sourcePath) {
|
||||
throw new ToolError(`Cannot grep internal URL without a backing file: ${rawPath}`);
|
||||
}
|
||||
searchPath = resource.sourcePath;
|
||||
scopePath = formatScopePath(searchPath);
|
||||
} else {
|
||||
const multiSearchPath = await resolveMultiSearchPath(rawPath, this.session.cwd, globFilter);
|
||||
if (multiSearchPath) {
|
||||
searchPath = multiSearchPath.basePath;
|
||||
globFilter = multiSearchPath.exactFilePaths ? undefined : multiSearchPath.glob;
|
||||
exactFilePaths = multiSearchPath.exactFilePaths;
|
||||
scopePath = multiSearchPath.scopePath;
|
||||
} else {
|
||||
const parsedPath = parseSearchPath(rawPath);
|
||||
searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
|
||||
globFilter = parsedPath.glob;
|
||||
scopePath = formatScopePath(searchPath);
|
||||
}
|
||||
}
|
||||
|
||||
let isDirectory: boolean;
|
||||
try {
|
||||
const stat = await Bun.file(searchPath).stat();
|
||||
@@ -180,7 +162,7 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
||||
}
|
||||
|
||||
const effectiveOutputMode = GrepOutputMode.Content;
|
||||
const effectiveLimit = normalizedLimit ?? DEFAULT_MATCH_LIMIT;
|
||||
const effectiveLimit = DEFAULT_MATCH_LIMIT;
|
||||
const internalLimit = Math.min(effectiveLimit * 5, 2000);
|
||||
|
||||
// Run grep
|
||||
@@ -194,7 +176,6 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
||||
{
|
||||
pattern: normalizedPattern,
|
||||
path: exactFilePath,
|
||||
type: type?.trim() || undefined,
|
||||
ignoreCase,
|
||||
multiline: effectiveMultiline,
|
||||
hidden: true,
|
||||
@@ -211,7 +192,7 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
||||
const relativeFilePath = path.relative(searchPath, exactFilePath).replace(/\\/g, "/");
|
||||
matches.push(...fileResult.matches.map(match => ({ ...match, path: relativeFilePath })));
|
||||
}
|
||||
const offsetMatches = matches.slice(normalizedOffset);
|
||||
const offsetMatches = matches.slice(normalizedSkip);
|
||||
result = {
|
||||
matches: offsetMatches,
|
||||
totalMatches: offsetMatches.length,
|
||||
@@ -225,14 +206,13 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
||||
pattern: normalizedPattern,
|
||||
path: searchPath,
|
||||
glob: globFilter,
|
||||
type: type?.trim() || undefined,
|
||||
ignoreCase,
|
||||
multiline: effectiveMultiline,
|
||||
hidden: true,
|
||||
gitignore: useGitignore,
|
||||
cache: false,
|
||||
maxCount: internalLimit,
|
||||
offset: normalizedOffset > 0 ? normalizedOffset : undefined,
|
||||
offset: normalizedSkip > 0 ? normalizedSkip : undefined,
|
||||
contextBefore: normalizedContextBefore,
|
||||
contextAfter: normalizedContextAfter,
|
||||
maxColumns: DEFAULT_MAX_COLUMN,
|
||||
@@ -407,6 +387,9 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
||||
outputLines.push(...renderChunkedMatchesForFile(relativePath));
|
||||
}
|
||||
}
|
||||
if (matchLimitReached || result.limitReached) {
|
||||
outputLines.push("", "Result limit reached; narrow path.");
|
||||
}
|
||||
const rawOutput = outputLines.join("\n");
|
||||
const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
|
||||
const truncated = Boolean(matchLimitReached || result.limitReached || truncation.truncated);
|
||||
@@ -424,12 +407,7 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
||||
resultLimitReached: result.limitReached ? internalLimit : undefined,
|
||||
};
|
||||
if (truncation.truncated) details.truncation = truncation;
|
||||
const resultBuilder = toolResult(details)
|
||||
.text(truncation.content)
|
||||
.limits({
|
||||
matchLimit: matchLimitReached ? effectiveLimit : undefined,
|
||||
resultLimit: result.limitReached ? internalLimit : undefined,
|
||||
});
|
||||
const resultBuilder = toolResult(details).text(truncation.content);
|
||||
if (truncation.truncated) {
|
||||
resultBuilder.truncation(truncation, { direction: "head" });
|
||||
}
|
||||
@@ -524,6 +502,9 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
||||
if (hasContextLines && outputLines.length > 0) {
|
||||
outputLines.unshift("[grep] match lines use ':'; context lines use '-'.");
|
||||
}
|
||||
if (matchLimitReached || result.limitReached) {
|
||||
outputLines.push("", "Result limit reached; narrow path.");
|
||||
}
|
||||
const rawOutput = outputLines.join("\n");
|
||||
const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
|
||||
const output = truncation.content;
|
||||
@@ -546,11 +527,7 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
||||
if (linesTruncated) details.linesTruncated = true;
|
||||
const resultBuilder = toolResult(details)
|
||||
.text(output)
|
||||
.limits({
|
||||
matchLimit: matchLimitReached ? effectiveLimit : undefined,
|
||||
resultLimit: result.limitReached ? internalLimit : undefined,
|
||||
columnMax: linesTruncated ? DEFAULT_MAX_COLUMN : undefined,
|
||||
});
|
||||
.limits({ columnMax: linesTruncated ? DEFAULT_MAX_COLUMN : undefined });
|
||||
if (truncation.truncated) {
|
||||
resultBuilder.truncation(truncation, { direction: "head" });
|
||||
}
|
||||
@@ -566,15 +543,9 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
||||
interface GrepRenderArgs {
|
||||
pattern: string;
|
||||
path?: string;
|
||||
glob?: string;
|
||||
type?: string;
|
||||
i?: boolean;
|
||||
gitignore?: boolean;
|
||||
pre?: number;
|
||||
post?: number;
|
||||
multiline?: boolean;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
skip?: number;
|
||||
}
|
||||
|
||||
const COLLAPSED_TEXT_LIMIT = PREVIEW_LIMITS.COLLAPSED_LINES * 2;
|
||||
@@ -584,19 +555,9 @@ export const grepToolRenderer = {
|
||||
renderCall(args: GrepRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
||||
const meta: string[] = [];
|
||||
if (args.path) meta.push(`in ${args.path}`);
|
||||
if (args.glob) meta.push(`glob:${args.glob}`);
|
||||
if (args.type) meta.push(`type:${args.type}`);
|
||||
if (args.i) meta.push("case:insensitive");
|
||||
if (args.gitignore === false) meta.push("gitignore:false");
|
||||
if (args.pre !== undefined && args.pre > 0) {
|
||||
meta.push(`pre:${args.pre}`);
|
||||
}
|
||||
if (args.post !== undefined && args.post > 0) {
|
||||
meta.push(`post:${args.post}`);
|
||||
}
|
||||
if (args.multiline) meta.push("multiline");
|
||||
if (args.limit !== undefined && args.limit > 0) meta.push(`limit:${args.limit}`);
|
||||
if (args.offset !== undefined && args.offset > 0) meta.push(`offset:${args.offset}`);
|
||||
if (args.skip !== undefined && args.skip > 0) meta.push(`skip:${args.skip}`);
|
||||
|
||||
const text = renderStatusLine(
|
||||
{ icon: "pending", title: "Grep", description: args.pattern || "?", meta },
|
||||
@@ -708,9 +669,11 @@ export const grepToolRenderer = {
|
||||
}
|
||||
}
|
||||
|
||||
const renderedMatchLimit = details?.matchLimitReached ?? limits?.matchLimit?.reached;
|
||||
const renderedResultLimit = details?.resultLimitReached ?? limits?.resultLimit?.reached;
|
||||
const truncationReasons: string[] = [];
|
||||
if (limits?.matchLimit) truncationReasons.push(`limit ${limits.matchLimit.reached} matches`);
|
||||
if (limits?.resultLimit) truncationReasons.push(`limit ${limits.resultLimit.reached} results`);
|
||||
if (renderedMatchLimit) truncationReasons.push(`first ${renderedMatchLimit} matches`);
|
||||
if (renderedResultLimit) truncationReasons.push(`first ${renderedResultLimit} results`);
|
||||
if (truncation) truncationReasons.push(truncation.truncatedBy === "lines" ? "line limit" : "size limit");
|
||||
if (limits?.columnTruncated) truncationReasons.push(`line length ${limits.columnTruncated.maxColumn}`);
|
||||
if (truncation?.artifactId) truncationReasons.push(formatFullOutputReference(truncation.artifactId));
|
||||
|
||||
Reference in New Issue
Block a user