Skip to content

Commit de8e9e3

Browse files
committed
fix(exec): preserve single-token run summaries to keep package-script context
Tighten stripLeadingExecDisplayVerb so it only strips `run` when the rest carries a binary plus arguments (the catch-all shape). Single-token `run <X>` summaries survive because package-script names (`npm run dev`, `npm run custom-script`) are indistinguishable from bare-binary invocations (`run python3`) at strip time. This restores arbitrary package-script run labels while still collapsing the reported `run python3 /path` case (issue #97319).
1 parent f27953b commit de8e9e3

2 files changed

Lines changed: 35 additions & 22 deletions

File tree

src/agents/tool-display-exec.ts

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -290,32 +290,37 @@ function summarizeKnownExec(words: string[]): string {
290290
// `run` is the only framework-injected verb in `summarizeKnownExec` output; every
291291
// other leading word (`check git status`, `install dependencies`, `start app`,
292292
// `show last 20 lines`) carries diagnostic meaning and must survive stripping.
293-
// `summarizeKnownExec` also emits a small set of semantic `run <noun>` labels from
294-
// the npm/pnpm/yarn subcommand map (`run tests`, `run build`, `run lint`,
295-
// `run script`); those must also survive so the failure message keeps the
296-
// package-manager script context. Any other `run <binary> ...` shape is
297-
// framework-injected by the catch-all and is stripped so the backtick-wrapped
298-
// text is exactly what was run (issue #97319).
299-
const SEMANTIC_RUN_WORDS: ReadonlySet<string> = new Set(["tests", "build", "lint", "script"]);
293+
// `summarizeKnownExec` also emits `run <noun>` labels from the npm/pnpm/yarn
294+
// subcommand map — both fixed (`run tests`, `run build`, `run lint`, `run script`)
295+
// and arbitrary (`run dev`, `run ${scriptName}`) — which must survive so the
296+
// failure message keeps the package-manager script context. Only the catch-all
297+
// `run <binary> <args>` shape is stripped; that shape always carries at least
298+
// one whitespace-separated argument after the binary, so a `run <single-token>`
299+
// meta is never ambiguous and never stripped (issue #97319).
300300

301301
/**
302-
* Strips a leading framework-injected `run` verb when it precedes a binary
303-
* (e.g. `run python3 /path` -> `python3 /path`) so the action label is not
304-
* backtick-wrapped together with the literal command (issue #97319).
302+
* Strips a leading framework-injected `run` verb when the rest carries both a
303+
* binary and its arguments (e.g. `run python3 /path` -> `python3 /path`) so the
304+
* action label is not backtick-wrapped together with the literal command
305+
* (issue #97319).
305306
*
306307
* Returns the original meta unchanged for any other shape — including action-
307-
* bearing summaries like `install dependencies`, `start app`, `run tests`,
308-
* `run build`, or `show last 20 lines` — so diagnostic verbs and package-manager
309-
* script labels stay visible in failure warnings.
308+
* bearing summaries like `install dependencies`, `start app`, package-manager
309+
* script labels like `run dev`, `run tests`, `run build`, or `show last 20
310+
* lines` — so diagnostic verbs and arbitrary package-script names stay visible
311+
* in failure warnings.
310312
*/
311313
export function stripLeadingExecDisplayVerb(meta: string): string {
312314
const match = meta.match(/^run\s+(\S.*)$/);
313315
if (!match) {
314316
return meta;
315317
}
316318
const rest = match[1];
317-
const firstWord = rest.match(/^([A-Za-z0-9._-]+)/)?.[1];
318-
if (!firstWord || SEMANTIC_RUN_WORDS.has(firstWord)) {
319+
// Only strip when the meta unambiguously came from the catch-all — i.e. the
320+
// rest contains a binary AND its arguments. A single-token `run <X>` could be
321+
// an arbitrary package-script name (`npm run dev`) or a fixed semantic label
322+
// (`run tests`), so we preserve those to avoid dropping script context.
323+
if (!/\s/.test(rest)) {
319324
return meta;
320325
}
321326
return rest;

src/agents/tool-display.test.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -587,7 +587,7 @@ describe("compactRawCommand middle truncation", () => {
587587
});
588588

589589
describe("stripLeadingExecDisplayVerb", () => {
590-
it("strips `run` when followed by any binary so the framework verb stays outside the backticks", () => {
590+
it("strips `run` when followed by a binary plus arguments so the framework verb stays outside the backticks", () => {
591591
expect(stripLeadingExecDisplayVerb("run python3 /path/to/script.py")).toBe(
592592
"python3 /path/to/script.py",
593593
);
@@ -601,19 +601,27 @@ describe("stripLeadingExecDisplayVerb", () => {
601601
expect(stripLeadingExecDisplayVerb("run my-custom-tool --flag value")).toBe(
602602
"my-custom-tool --flag value",
603603
);
604-
expect(stripLeadingExecDisplayVerb("run ./bin/local-script")).toBe("./bin/local-script");
604+
});
605+
606+
it("preserves single-token `run <X>` summaries because package-script names are indistinguishable from bare binaries", () => {
607+
// `summarizeKnownExec` emits `run <scriptName>` for arbitrary npm/pnpm/yarn
608+
// scripts (`npm run dev`, `npm run start`, `npm run custom-script`); those
609+
// single-token metas cannot be told apart from a bare-binary `run python3`
610+
// at strip time, so we keep `run` to preserve the package-script context.
611+
expect(stripLeadingExecDisplayVerb("run tests")).toBe("run tests");
612+
expect(stripLeadingExecDisplayVerb("run build")).toBe("run build");
613+
expect(stripLeadingExecDisplayVerb("run lint")).toBe("run lint");
614+
expect(stripLeadingExecDisplayVerb("run script")).toBe("run script");
615+
expect(stripLeadingExecDisplayVerb("run dev")).toBe("run dev");
616+
expect(stripLeadingExecDisplayVerb("run custom-script")).toBe("run custom-script");
605617
});
606618

607619
it("preserves action-bearing exec summaries that `summarizeKnownExec` emits with semantic verbs", () => {
608-
// `install dependencies`, `start app`, `run tests`, `show last 20 lines`,
620+
// `install dependencies`, `start app`, `show last 20 lines`,
609621
// `check git status`, `find files named …` all carry diagnostic meaning
610622
// beyond the bare command — strip must not drop those verbs.
611623
expect(stripLeadingExecDisplayVerb("install dependencies")).toBe("install dependencies");
612624
expect(stripLeadingExecDisplayVerb("start app")).toBe("start app");
613-
expect(stripLeadingExecDisplayVerb("run tests")).toBe("run tests");
614-
expect(stripLeadingExecDisplayVerb("run build")).toBe("run build");
615-
expect(stripLeadingExecDisplayVerb("run lint")).toBe("run lint");
616-
expect(stripLeadingExecDisplayVerb("run script")).toBe("run script");
617625
expect(stripLeadingExecDisplayVerb("show some-file.txt (workspace)")).toBe(
618626
"show some-file.txt (workspace)",
619627
);

0 commit comments

Comments
 (0)