diff --git a/desktop/src/features/agents/ui/agentSessionToolClassifier.test.mjs b/desktop/src/features/agents/ui/agentSessionToolClassifier.test.mjs index 2ca919a427..76615d710b 100644 --- a/desktop/src/features/agents/ui/agentSessionToolClassifier.test.mjs +++ b/desktop/src/features/agents/ui/agentSessionToolClassifier.test.mjs @@ -149,12 +149,16 @@ test("parseBuzzCliCommand preserves single-quoted literals in the --content=valu assert.equal(descriptor?.preview, "use `pnpm test`"); }); -test("parseBuzzCliCommand preserves backslash-escaped substitution characters", () => { +test("parseBuzzCliCommand rejects backslash-escaped substitution characters as non-portable", () => { + // bash treats `\$MESSAGE` as literal text, but PowerShell keeps the backslash + // and expands the variable anyway, so the escaped form cannot be trusted as a + // literal. Only single quoting is literal across the shells BUZZ_SHELL allows. const descriptor = parseBuzzCliCommand( 'buzz messages send --channel agents --content "literal \\$MESSAGE and \\`code\\`"', ); - assert.equal(descriptor?.preview, "literal $MESSAGE and `code`"); + assert.equal(descriptor?.renderClass, "message"); + assert.equal(descriptor?.preview, null); }); test("parseBuzzCliCommand still rejects an unquoted substitution in --content", () => { diff --git a/desktop/src/features/agents/ui/agentSessionToolClassifier.ts b/desktop/src/features/agents/ui/agentSessionToolClassifier.ts index 475a89726a..98e667efcf 100644 --- a/desktop/src/features/agents/ui/agentSessionToolClassifier.ts +++ b/desktop/src/features/agents/ui/agentSessionToolClassifier.ts @@ -519,15 +519,21 @@ function findBuzzCommand(tokens: ShellToken[]): BuzzCommandRange | null { * * `hasSubstitution` is what quote context buys us: `$` and a backtick mean * expansion when they are unquoted or inside double quotes, and mean nothing at - * all inside single quotes or after a backslash. Without this distinction a - * markdown code span in `--content 'use `pnpm test`'` is indistinguishable from - * a real `--content "$(cat file)"`, and both have to be discarded. + * all inside single quotes. Without this distinction a markdown code span in + * `--content 'use `pnpm test`'` is indistinguishable from a real + * `--content "$(cat file)"`, and both have to be discarded. * - * The `$` rule is deliberately conservative — any unescaped `$` outside single - * quotes flags the token, even where bash would leave it literal (a trailing - * `$`, or `$` before a space). Displaying an unexpanded shell expression as if - * it were the sent text is the failure this guard exists to prevent, so it errs - * towards dropping the preview. + * Only single quoting is trusted, because only single quoting is literal in + * every shell `BUZZ_SHELL` supports. A backslash escape is not: PowerShell + * keeps the backslash and still expands `"literal \$MESSAGE"`. So escaped + * substitution characters stay flagged even though bash would treat them as + * literal text. + * + * The `$` rule is deliberately conservative in the same direction — any `$` + * outside single quotes flags the token, even where bash would leave it literal + * (a trailing `$`, or `$` before a space). Displaying an unexpanded shell + * expression as if it were the sent text is the failure this guard exists to + * prevent, so it errs towards dropping the preview. */ export type ShellToken = { value: string; @@ -555,6 +561,13 @@ export function tokenizeShellCommandTokens(command: string): ShellToken[] { for (const char of command) { if (escaping) { + // The value is built as bash would build it, but a backslash escape does + // not earn substitution credit: backslash is not an escape character in + // PowerShell (`"literal \$MESSAGE"` keeps the backslash AND expands the + // variable) or cmd, and `BUZZ_SHELL` explicitly supports both. Only + // single quoting is literal across all of them, so escape forms stay + // conservative and keep #2201's behaviour. + if (isSubstitutionChar(char)) hasSubstitution = true; current += char; escaping = false; continue;