From 9fe3f371622cad9cc1a3cbf044288abd0252192f Mon Sep 17 00:00:00 2001 From: David Brailovsky Date: Wed, 14 Oct 2026 15:34:49 +0200 Subject: [PATCH] fix(coding-agent): keep empty quoted prompt template arguments Empty quoted arguments ("" or '') were dropped, shifting later positional arguments and preventing positional defaults from applying. closes #10867 --- packages/coding-agent/CHANGELOG.md | 1 + .../coding-agent/src/core/prompt-templates.ts | 9 +++++-- .../test/prompt-templates.test.ts | 24 +++++++++++++++++-- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 8cff984f1..625286550 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -14,6 +14,7 @@ - Fixed repeated codemode timeouts or aborts accumulating surviving workers; admission now limits live workers to two across sessions, counting cancelled workers until actual exit ([#10748](https://github.com/earendil-works/pi/issues/10748)) - Fixed `--model` choosing a provider by catalog order when several providers offer the match; Pi now prefers the provider with credentials and otherwise reports the ambiguity. `--models` and `enabledModels` add a model for each provider that offers it and warn about the ambiguity ([#10160](https://github.com/earendil-works/pi/issues/10160)) - Fixed `read` line-range labels for numeric-string or null arguments in the TUI, session tree, HTML exports, and examples ([#9887](https://github.com/earendil-works/pi/issues/9887)) +- Fixed prompt templates dropping empty quoted arguments (`""` or `''`), which shifted later positional arguments and prevented positional defaults from applying ([#10867](https://github.com/earendil-works/pi/issues/10867)) ## [1.2.0] - 2026-10-12 diff --git a/packages/coding-agent/src/core/prompt-templates.ts b/packages/coding-agent/src/core/prompt-templates.ts index f5b21a665..7a0b34432 100644 --- a/packages/coding-agent/src/core/prompt-templates.ts +++ b/packages/coding-agent/src/core/prompt-templates.ts @@ -25,6 +25,8 @@ export interface PromptTemplate { export function parseCommandArgs(argsString: string): string[] { const args: string[] = []; let current = ""; + // Tracks whether a token has started, so quoted empty strings ("" or '') are kept as arguments + let hasToken = false; let inQuote: string | null = null; for (let i = 0; i < argsString.length; i++) { @@ -38,17 +40,20 @@ export function parseCommandArgs(argsString: string): string[] { } } else if (char === '"' || char === "'") { inQuote = char; + hasToken = true; } else if (/\s/.test(char)) { - if (current) { + if (hasToken) { args.push(current); current = ""; + hasToken = false; } } else { current += char; + hasToken = true; } } - if (current) { + if (hasToken) { args.push(current); } diff --git a/packages/coding-agent/test/prompt-templates.test.ts b/packages/coding-agent/test/prompt-templates.test.ts index eaf59c12a..d67346ab9 100644 --- a/packages/coding-agent/test/prompt-templates.test.ts +++ b/packages/coding-agent/test/prompt-templates.test.ts @@ -383,8 +383,10 @@ describe("parseCommandArgs", () => { }); test("should handle quoted empty string", () => { - // Note: Empty quotes are skipped by current implementation - expect(parseCommandArgs('"" " "')).toEqual([" "]); + // Empty quotes keep their position as an empty argument (#10867) + expect(parseCommandArgs('"" " "')).toEqual(["", " "]); + expect(parseCommandArgs("a '' c")).toEqual(["a", "", "c"]); + expect(parseCommandArgs('a ""')).toEqual(["a", ""]); }); test("should handle arguments with special characters", () => { @@ -433,6 +435,24 @@ describe("parseCommandArgs", () => { // ============================================================================ describe("expandPromptTemplate", () => { + // #10867 + test("should use positional default for empty quoted argument without shifting later args", () => { + const template = { + name: "review", + description: "test", + content: `Review $2. Focus: \${1:-correctness}.`, + sourceInfo: { path: "/tmp/review.md", source: "local", scope: "temporary", origin: "top-level" } as const, + filePath: "/tmp/review.md", + }; + + expect(expandPromptTemplate('/review "" src/index.ts', [template])).toBe( + "Review src/index.ts. Focus: correctness.", + ); + expect(expandPromptTemplate("/review '' src/index.ts", [template])).toBe( + "Review src/index.ts. Focus: correctness.", + ); + }); + test("should split template arguments on unquoted newlines", () => { const result = expandPromptTemplate("/arg-test label-2\n\nHere is some description #2.", [ {