fix(tool-display): avoid recursive search summaries (#114933)

* fix(tool-display): avoid recursive search summaries

Fixes #113401.\n\nCo-authored-by: qingminlong <qing.minlong@xydigit.com>

* fix(tool-display): recognize neutral search summaries

* style(tool-display): format summary matcher
This commit is contained in:
Peter Steinberger
2026-07-28 02:03:56 -04:00
committed by GitHub
parent 6dbe7ff1e2
commit ca82f7dfbc
2 changed files with 74 additions and 0 deletions
+24
View File
@@ -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) {
+50
View File
@@ -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({