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
This commit is contained in:
David Brailovsky 2026-10-14 15:34:49 +02:00
parent 910149912c
commit 9fe3f37162
3 changed files with 30 additions and 4 deletions

View file

@ -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

View file

@ -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);
}

View file

@ -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.", [
{