Merge pull request #5553 from dannote/prompt-template-defaults
Add prompt template argument defaults
This commit is contained in:
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- Added default-value expansion for prompt template positional arguments, e.g. `${1:-7}` ([#5507](https://github.com/earendil-works/pi/issues/5507)).
|
||||||
- Added `areExperimentalFeaturesEnabled` feature guard to allow users to opt-in to early features.
|
- Added `areExperimentalFeaturesEnabled` feature guard to allow users to opt-in to early features.
|
||||||
- Added `ctx.isProjectTrusted()` for extensions to observe the effective project trust decision, including temporary trust decisions ([#5523](https://github.com/earendil-works/pi/issues/5523)).
|
- Added `ctx.isProjectTrusted()` for extensions to observe the effective project trust decision, including temporary trust decisions ([#5523](https://github.com/earendil-works/pi/issues/5523)).
|
||||||
- Added a global `defaultProjectTrust` setting to choose whether unresolved project trust asks, always trusts, or never trusts by default.
|
- Added a global `defaultProjectTrust` setting to choose whether unresolved project trust asks, always trusts, or never trusts by default.
|
||||||
|
|||||||
@@ -64,10 +64,11 @@ Type `/` followed by the template name in the editor. Autocomplete shows availab
|
|||||||
|
|
||||||
## Arguments
|
## Arguments
|
||||||
|
|
||||||
Templates support positional arguments and simple slicing:
|
Templates support positional arguments, defaults, and simple slicing:
|
||||||
|
|
||||||
- `$1`, `$2`, ... positional args
|
- `$1`, `$2`, ... positional args
|
||||||
- `$@` or `$ARGUMENTS` for all args joined
|
- `$@` or `$ARGUMENTS` for all args joined
|
||||||
|
- `${1:-default}` uses arg 1 when present/non-empty, otherwise `default`
|
||||||
- `${@:N}` for args from the Nth position (1-indexed)
|
- `${@:N}` for args from the Nth position (1-indexed)
|
||||||
- `${@:N:L}` for `L` args starting at N
|
- `${@:N:L}` for `L` args starting at N
|
||||||
|
|
||||||
@@ -80,6 +81,12 @@ description: Create a component
|
|||||||
Create a React component named $1 with features: $@
|
Create a React component named $1 with features: $@
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Default values are useful for optional arguments:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
Summarize the current state in ${1:-7} bullet points.
|
||||||
|
```
|
||||||
|
|
||||||
Usage: `/component Button "onClick handler" "disabled support"`
|
Usage: `/component Button "onClick handler" "disabled support"`
|
||||||
|
|
||||||
## Loading Rules
|
## Loading Rules
|
||||||
|
|||||||
@@ -59,46 +59,45 @@ export function parseCommandArgs(argsString: string): string[] {
|
|||||||
* Supports:
|
* Supports:
|
||||||
* - $1, $2, ... for positional args
|
* - $1, $2, ... for positional args
|
||||||
* - $@ and $ARGUMENTS for all args
|
* - $@ and $ARGUMENTS for all args
|
||||||
|
* - ${N:-default} for positional arg N with default when missing/empty
|
||||||
* - ${@:N} for args from Nth onwards (bash-style slicing)
|
* - ${@:N} for args from Nth onwards (bash-style slicing)
|
||||||
* - ${@:N:L} for L args starting from Nth
|
* - ${@:N:L} for L args starting from Nth
|
||||||
*
|
*
|
||||||
* Note: Replacement happens on the template string only. Argument values
|
* Note: Replacement happens on the template string only. Argument and default values
|
||||||
* containing patterns like $1, $@, or $ARGUMENTS are NOT recursively substituted.
|
* containing patterns like $1, $@, or $ARGUMENTS are NOT recursively substituted.
|
||||||
*/
|
*/
|
||||||
export function substituteArgs(content: string, args: string[]): string {
|
export function substituteArgs(content: string, args: string[]): string {
|
||||||
let result = content;
|
|
||||||
|
|
||||||
// Replace $1, $2, etc. with positional args FIRST (before wildcards)
|
|
||||||
// This prevents wildcard replacement values containing $<digit> patterns from being re-substituted
|
|
||||||
result = result.replace(/\$(\d+)/g, (_, num) => {
|
|
||||||
const index = parseInt(num, 10) - 1;
|
|
||||||
return args[index] ?? "";
|
|
||||||
});
|
|
||||||
|
|
||||||
// Replace ${@:start} or ${@:start:length} with sliced args (bash-style)
|
|
||||||
// Process BEFORE simple $@ to avoid conflicts
|
|
||||||
result = result.replace(/\$\{@:(\d+)(?::(\d+))?\}/g, (_, startStr, lengthStr) => {
|
|
||||||
let start = parseInt(startStr, 10) - 1; // Convert to 0-indexed (user provides 1-indexed)
|
|
||||||
// Treat 0 as 1 (bash convention: args start at 1)
|
|
||||||
if (start < 0) start = 0;
|
|
||||||
|
|
||||||
if (lengthStr) {
|
|
||||||
const length = parseInt(lengthStr, 10);
|
|
||||||
return args.slice(start, start + length).join(" ");
|
|
||||||
}
|
|
||||||
return args.slice(start).join(" ");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Pre-compute all args joined (optimization)
|
|
||||||
const allArgs = args.join(" ");
|
const allArgs = args.join(" ");
|
||||||
|
|
||||||
// Replace $ARGUMENTS with all args joined (new syntax, aligns with Claude, Codex, OpenCode)
|
return content.replace(
|
||||||
result = result.replace(/\$ARGUMENTS/g, allArgs);
|
/\$\{(\d+):-([^}]*)\}|\$\{@:(\d+)(?::(\d+))?\}|\$(ARGUMENTS|@|\d+)/g,
|
||||||
|
(_match, defaultNum, defaultValue, sliceStart, sliceLength, simple) => {
|
||||||
|
if (defaultNum) {
|
||||||
|
const index = parseInt(defaultNum, 10) - 1;
|
||||||
|
const value = args[index];
|
||||||
|
return value ? value : defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
// Replace $@ with all args joined (existing syntax)
|
if (sliceStart) {
|
||||||
result = result.replace(/\$@/g, allArgs);
|
let start = parseInt(sliceStart, 10) - 1; // Convert to 0-indexed (user provides 1-indexed)
|
||||||
|
// Treat 0 as 1 (bash convention: args start at 1)
|
||||||
|
if (start < 0) start = 0;
|
||||||
|
|
||||||
return result;
|
if (sliceLength) {
|
||||||
|
const length = parseInt(sliceLength, 10);
|
||||||
|
return args.slice(start, start + length).join(" ");
|
||||||
|
}
|
||||||
|
return args.slice(start).join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (simple === "ARGUMENTS" || simple === "@") {
|
||||||
|
return allArgs;
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = parseInt(simple, 10) - 1;
|
||||||
|
return args[index] ?? "";
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadTemplateFromFile(filePath: string, sourceInfo: SourceInfo): PromptTemplate | null {
|
function loadTemplateFromFile(filePath: string, sourceInfo: SourceInfo): PromptTemplate | null {
|
||||||
|
|||||||
@@ -190,6 +190,52 @@ describe("substituteArgs", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// substituteArgs - Positional Defaults
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
describe("substituteArgs - positional defaults", () => {
|
||||||
|
test("should use default when positional arg is missing", () => {
|
||||||
|
expect(substituteArgs(`List exactly \${1:-7} next steps`, [])).toBe("List exactly 7 next steps");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should use positional arg when present", () => {
|
||||||
|
expect(substituteArgs(`List exactly \${1:-7} next steps`, ["3"])).toBe("List exactly 3 next steps");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should use default when positional arg is empty", () => {
|
||||||
|
expect(substituteArgs(`Mode: \${1:-brief}`, [""])).toBe("Mode: brief");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should support multiple positional defaults", () => {
|
||||||
|
expect(substituteArgs(`\${1:-7} \${2:-brief}`, [])).toBe("7 brief");
|
||||||
|
expect(substituteArgs(`\${1:-7} \${2:-brief}`, ["3"])).toBe("3 brief");
|
||||||
|
expect(substituteArgs(`\${1:-7} \${2:-brief}`, ["3", "verbose"])).toBe("3 verbose");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should not recursively substitute patterns in arg values", () => {
|
||||||
|
expect(substituteArgs(`\${1:-7}`, ["$ARGUMENTS"])).toBe("$ARGUMENTS");
|
||||||
|
expect(substituteArgs(`\${1:-7}`, ["$1"])).toBe("$1");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should not recursively substitute patterns in default values", () => {
|
||||||
|
expect(substituteArgs(`\${1:-$ARGUMENTS}`, ["a", "b"])).toBe("a");
|
||||||
|
expect(substituteArgs(`\${3:-$ARGUMENTS}`, ["a", "b"])).toBe("$ARGUMENTS");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should support defaults with spaces", () => {
|
||||||
|
expect(substituteArgs(`\${1:-seven steps}`, [])).toBe("seven steps");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should support out-of-range positional defaults", () => {
|
||||||
|
expect(substituteArgs(`\${3:-fallback}`, ["a", "b"])).toBe("fallback");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should mix positional defaults with existing placeholders", () => {
|
||||||
|
expect(substituteArgs(`$1 \${2:-x} $ARGUMENTS`, ["a"])).toBe("a x a");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// substituteArgs - Array Slicing (Bash-Style)
|
// substituteArgs - Array Slicing (Bash-Style)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user