From ca82f7dfbc27fcd05e44573be17022879ccec7f9 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 28 Jul 2026 02:03:56 -0400 Subject: [PATCH] fix(tool-display): avoid recursive search summaries (#114933) * fix(tool-display): avoid recursive search summaries Fixes #113401.\n\nCo-authored-by: qingminlong * fix(tool-display): recognize neutral search summaries * style(tool-display): format summary matcher --- src/agents/tool-display-exec.ts | 24 ++++++++++++++++ src/agents/tool-display.test.ts | 50 +++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/src/agents/tool-display-exec.ts b/src/agents/tool-display-exec.ts index 92aed68ab7a8..37e4b61338bd 100644 --- a/src/agents/tool-display-exec.ts +++ b/src/agents/tool-display-exec.ts @@ -118,6 +118,9 @@ function summarizeKnownExec(words: string[]): string { const pattern = optionValue(words, ["-e", "--regexp"]) ?? positional[0]; const target = positional.length > 1 ? positional.at(-1) : undefined; if (pattern) { + if (isUnsafeSearchSummaryPattern(pattern)) { + return target ? `search text in ${target}` : "search text"; + } return target ? `search "${pattern}" in ${target}` : `search "${pattern}"`; } return "search text"; @@ -290,6 +293,27 @@ function summarizeKnownExec(words: string[]): string { return /^[A-Za-z0-9._/-]+$/.test(arg) ? `run ${bin} ${arg}` : `run ${bin}`; } +function isUnsafeSearchSummaryPattern(pattern: string): boolean { + const trimmed = pattern.trim(); + return ( + !trimmed || + pattern.length > 120 || + /[\r\n`]/u.test(pattern) || + /^Bash failed:/iu.test(trimmed) || + containsGeneratedSearchSummary(trimmed) + ); +} + +// Match the two labels this formatter emits, without hiding normal prose such as +// "search engine" or "search textual data". +const GENERATED_SEARCH_SUMMARY_FRAGMENT_RE = /^search\s+(?:["']|text(?:\s+in(?:\s|$)|$))/iu; + +function containsGeneratedSearchSummary(pattern: string): boolean { + return pattern + .split(/(?:\||->)/u) + .some((fragment) => GENERATED_SEARCH_SUMMARY_FRAGMENT_RE.test(fragment.trim())); +} + function summarizePipeline(stage: string): string { const pipeline = splitTopLevelPipes(stage); if (pipeline.length > 1) { diff --git a/src/agents/tool-display.test.ts b/src/agents/tool-display.test.ts index fec213fa93cc..e58f973a67c2 100644 --- a/src/agents/tool-display.test.ts +++ b/src/agents/tool-display.test.ts @@ -224,6 +224,56 @@ describe("tool display details", () => { expect(detail).toBe("print lines 1-80 from extensions/discord/src/draft-stream.ts"); }); + it("keeps normal search patterns concise", () => { + for (const [command, expected] of [ + ['rg "foo|bar" src/agents', 'search "foo|bar" in src/agents'], + ["rg 'search engine' src/agents", 'search "search engine" in src/agents'], + ["rg 'search textual data' src/agents", 'search "search textual data" in src/agents'], + ["rg 'research text in docs' src/agents", 'search "research text in docs" in src/agents'], + ]) { + expect( + formatToolDetail( + resolveToolDisplay({ name: "exec", args: { command }, detailMode: "explain" }), + ), + ).toBe(expected); + } + }); + + it("uses a neutral label for recursive or malformed search patterns", () => { + for (const command of [ + `rg 'search "foo" in src/agents' src`, + `rg 'Bash failed: search "foo" in src|search "bar"' src`, + `rg 'run printf -> search "foo" in src' src`, + "rg 'search text' src", + "rg 'search text in src/agents' src", + "rg 'foo|search text in src/agents' src", + "rg 'run printf -> search text' src", + "rg 'line1\nline2' src", + "rg 'line with trailing newline\n' src", + "rg '`generated command`' src", + `rg '${"x".repeat(121)}' src`, + `rg '${" ".repeat(121)}x' src`, + ]) { + expect( + formatToolDetail( + resolveToolDisplay({ name: "exec", args: { command }, detailMode: "explain" }), + ), + ).toBe("search text in src"); + } + }); + + it("sanitizes recursive search patterns inside pipelines", () => { + expect( + formatToolDetail( + resolveToolDisplay({ + name: "exec", + args: { command: `printf x | rg 'search "foo" in src' .` }, + detailMode: "explain", + }), + ), + ).toBe("print text -> search text in ."); + }); + it("moves cd path to context suffix and appends raw command", () => { const detail = formatToolDetail( resolveToolDisplay({