feat(coding-agent,tui): support argument-hint frontmatter in prompt templates (#2780)
* feat(coding-agent,tui): support argument-hint frontmatter in prompt templates Parse argument-hint from prompt template frontmatter and display it in the autocomplete dropdown description, matching Claude Code's convention for custom commands. Frontmatter format: --- description: Code review argument-hint: "[file | #PR | PR-URL]" --- The hint renders in the description column of the autocomplete list: review [file | #PR | PR-URL] — Code review Closes #2761 * docs(coding-agent,tui): add argument-hint documentation, tests, and built-in hints - Document argument-hint frontmatter in prompt-templates.md with <required>/[optional] convention - Add argument-hint to built-in prompts: pr, is, wr - Expand tests: required/optional hints, missing hints, empty hints, special characters - Add changelog entries for coding-agent and tui
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
---
|
||||
description: Analyze GitHub issues (bugs or feature requests)
|
||||
argument-hint: "<issue>"
|
||||
---
|
||||
Analyze GitHub issue(s): $ARGUMENTS
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
description: Review PRs from URLs with structured issue and code analysis
|
||||
argument-hint: "<PR-URL>"
|
||||
---
|
||||
You are given one or more GitHub PR URLs: $@
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
description: Finish the current task end-to-end with changelog, commit, and push
|
||||
argument-hint: "[instructions]"
|
||||
---
|
||||
Wrap it.
|
||||
|
||||
|
||||
@@ -270,6 +270,8 @@ await runtime.fork("entry-id");
|
||||
|
||||
- Added label timestamps to the session tree with a `Shift+T` toggle in `/tree`, smart date formatting, and timestamp preservation through branching ([#2691](https://github.com/badlogic/pi-mono/pull/2691) by [@w-winter](https://github.com/w-winter))
|
||||
|
||||
- Added `argument-hint` frontmatter field for prompt templates, displayed before the description in the autocomplete dropdown ([#2761](https://github.com/badlogic/pi-mono/issues/2761))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed startup resource loading to reuse the initial `ResourceLoader` for the first runtime, so extensions are not loaded twice before session startup and `session_start` handlers still fire for singleton-style extensions ([#2766](https://github.com/badlogic/pi-mono/issues/2766))
|
||||
|
||||
@@ -30,6 +30,27 @@ Review the staged changes (`git diff --cached`). Focus on:
|
||||
|
||||
- The filename becomes the command name. `review.md` becomes `/review`.
|
||||
- `description` is optional. If missing, the first non-empty line is used.
|
||||
- `argument-hint` is optional. When set, the hint is displayed before the description in the autocomplete dropdown.
|
||||
|
||||
### Argument Hints
|
||||
|
||||
Use `argument-hint` in frontmatter to show expected arguments in autocomplete. Use `<angle brackets>` for required arguments and `[square brackets]` for optional ones:
|
||||
|
||||
```markdown
|
||||
---
|
||||
description: Review PRs from URLs with structured issue and code analysis
|
||||
argument-hint: "<PR-URL>"
|
||||
---
|
||||
```
|
||||
|
||||
This renders in the autocomplete dropdown as:
|
||||
|
||||
```
|
||||
→ pr <PR-URL> — Review PRs from URLs with structured issue and code analysis
|
||||
is <issue> — Analyze GitHub issues (bugs or feature requests)
|
||||
wr [instructions] — Finish the current task end-to-end
|
||||
cl — Audit changelog entries before release
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { createSyntheticSourceInfo, type SourceInfo } from "./source-info.js";
|
||||
export interface PromptTemplate {
|
||||
name: string;
|
||||
description: string;
|
||||
argumentHint?: string;
|
||||
content: string;
|
||||
sourceInfo: SourceInfo;
|
||||
filePath: string; // Absolute path to the template file
|
||||
@@ -121,6 +122,7 @@ function loadTemplateFromFile(filePath: string, sourceInfo: SourceInfo): PromptT
|
||||
return {
|
||||
name,
|
||||
description,
|
||||
...(frontmatter["argument-hint"] && { argumentHint: frontmatter["argument-hint"] }),
|
||||
content: body,
|
||||
sourceInfo,
|
||||
filePath,
|
||||
|
||||
@@ -432,6 +432,7 @@ export class InteractiveMode {
|
||||
const templateCommands: SlashCommand[] = this.session.promptTemplates.map((cmd) => ({
|
||||
name: cmd.name,
|
||||
description: this.prefixAutocompleteDescription(cmd.description, cmd.sourceInfo),
|
||||
...(cmd.argumentHint && { argumentHint: cmd.argumentHint }),
|
||||
}));
|
||||
|
||||
// Convert extension commands to SlashCommand format
|
||||
|
||||
@@ -8,8 +8,11 @@
|
||||
* - Edge cases and integration between parsing and substitution
|
||||
*/
|
||||
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { parseCommandArgs, substituteArgs } from "../src/core/prompt-templates.js";
|
||||
import { mkdirSync, rmSync, writeFileSync } from "fs";
|
||||
import { tmpdir } from "os";
|
||||
import { join } from "path";
|
||||
import { afterAll, describe, expect, test } from "vitest";
|
||||
import { loadPromptTemplates, parseCommandArgs, substituteArgs } from "../src/core/prompt-templates.js";
|
||||
|
||||
// ============================================================================
|
||||
// substituteArgs
|
||||
@@ -379,3 +382,123 @@ describe("parseCommandArgs + substituteArgs integration", () => {
|
||||
expect(substituteArgs(template1, args)).toBe(substituteArgs(template2, args));
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// loadPromptTemplates - argument-hint frontmatter
|
||||
// ============================================================================
|
||||
|
||||
describe("loadPromptTemplates - argument-hint", () => {
|
||||
const testDir = join(tmpdir(), `pi-test-prompts-${Date.now()}`);
|
||||
|
||||
function writeTemplate(name: string, content: string) {
|
||||
mkdirSync(testDir, { recursive: true });
|
||||
writeFileSync(join(testDir, `${name}.md`), content);
|
||||
}
|
||||
|
||||
test("should parse required argument-hint from frontmatter", () => {
|
||||
writeTemplate(
|
||||
"pr",
|
||||
`---
|
||||
description: Review PRs from URLs with structured issue and code analysis
|
||||
argument-hint: "<PR-URL>"
|
||||
---
|
||||
You are given one or more GitHub PR URLs: $@`,
|
||||
);
|
||||
|
||||
const templates = loadPromptTemplates({
|
||||
promptPaths: [testDir],
|
||||
includeDefaults: false,
|
||||
});
|
||||
|
||||
const pr = templates.find((t) => t.name === "pr");
|
||||
expect(pr).toBeDefined();
|
||||
expect(pr!.argumentHint).toBe("<PR-URL>");
|
||||
expect(pr!.description).toBe("Review PRs from URLs with structured issue and code analysis");
|
||||
});
|
||||
|
||||
test("should parse optional argument-hint from frontmatter", () => {
|
||||
writeTemplate(
|
||||
"wr",
|
||||
`---
|
||||
description: Finish the current task end-to-end with changelog, commit, and push
|
||||
argument-hint: "[instructions]"
|
||||
---
|
||||
Wrap it. Additional instructions: $ARGUMENTS`,
|
||||
);
|
||||
|
||||
const templates = loadPromptTemplates({
|
||||
promptPaths: [testDir],
|
||||
includeDefaults: false,
|
||||
});
|
||||
|
||||
const wr = templates.find((t) => t.name === "wr");
|
||||
expect(wr).toBeDefined();
|
||||
expect(wr!.argumentHint).toBe("[instructions]");
|
||||
expect(wr!.description).toBe("Finish the current task end-to-end with changelog, commit, and push");
|
||||
});
|
||||
|
||||
test("should leave argumentHint undefined when not specified", () => {
|
||||
writeTemplate(
|
||||
"cl",
|
||||
`---
|
||||
description: Audit changelog entries before release
|
||||
---
|
||||
Audit changelog entries for all commits since the last release.`,
|
||||
);
|
||||
|
||||
const templates = loadPromptTemplates({
|
||||
promptPaths: [testDir],
|
||||
includeDefaults: false,
|
||||
});
|
||||
|
||||
const cl = templates.find((t) => t.name === "cl");
|
||||
expect(cl).toBeDefined();
|
||||
expect(cl!.argumentHint).toBeUndefined();
|
||||
});
|
||||
|
||||
test("should ignore empty argument-hint", () => {
|
||||
writeTemplate(
|
||||
"empty-hint",
|
||||
`---
|
||||
description: A command with empty hint
|
||||
argument-hint: ""
|
||||
---
|
||||
Do something`,
|
||||
);
|
||||
|
||||
const templates = loadPromptTemplates({
|
||||
promptPaths: [testDir],
|
||||
includeDefaults: false,
|
||||
});
|
||||
|
||||
const tmpl = templates.find((t) => t.name === "empty-hint");
|
||||
expect(tmpl).toBeDefined();
|
||||
expect(tmpl!.argumentHint).toBeUndefined();
|
||||
});
|
||||
|
||||
test("should preserve argument-hint with special characters", () => {
|
||||
writeTemplate(
|
||||
"is",
|
||||
`---
|
||||
description: Analyze GitHub issues (bugs or feature requests)
|
||||
argument-hint: "<issue>"
|
||||
---
|
||||
Analyze GitHub issue(s): $ARGUMENTS`,
|
||||
);
|
||||
|
||||
const templates = loadPromptTemplates({
|
||||
promptPaths: [testDir],
|
||||
includeDefaults: false,
|
||||
});
|
||||
|
||||
const is = templates.find((t) => t.name === "is");
|
||||
expect(is).toBeDefined();
|
||||
expect(is!.argumentHint).toBe("<issue>");
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
try {
|
||||
rmSync(testDir, { recursive: true, force: true });
|
||||
} catch {}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -62,6 +62,10 @@
|
||||
- Fixed slash-command argument autocomplete to await async `getArgumentCompletions()` results and ignore invalid return values, preventing crashes when extension commands provide asynchronous completions ([#2719](https://github.com/badlogic/pi-mono/issues/2719))
|
||||
- Fixed non-capturing overlay padding from inflating scrollback and corrupting the viewport on terminal widen ([#2758](https://github.com/badlogic/pi-mono/pull/2758) by [@dotBeeps](https://github.com/dotBeeps))
|
||||
|
||||
### Added
|
||||
|
||||
- Added `argumentHint` to `SlashCommand` interface, displayed before the description in the autocomplete dropdown ([#2761](https://github.com/badlogic/pi-mono/issues/2761))
|
||||
|
||||
## [0.64.0] - 2026-03-29
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -223,6 +223,7 @@ type Awaitable<T> = T | Promise<T>;
|
||||
export interface SlashCommand {
|
||||
name: string;
|
||||
description?: string;
|
||||
argumentHint?: string;
|
||||
// Function to get argument completions for this command
|
||||
// Returns null if no argument completion is available
|
||||
getArgumentCompletions?(argumentPrefix: string): Awaitable<AutocompleteItem[] | null>;
|
||||
@@ -303,11 +304,17 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
||||
|
||||
if (spaceIndex === -1) {
|
||||
const prefix = textBeforeCursor.slice(1);
|
||||
const commandItems = this.commands.map((cmd) => ({
|
||||
name: "name" in cmd ? cmd.name : cmd.value,
|
||||
label: "name" in cmd ? cmd.name : cmd.label,
|
||||
description: cmd.description,
|
||||
}));
|
||||
const commandItems = this.commands.map((cmd) => {
|
||||
const name = "name" in cmd ? cmd.name : cmd.value;
|
||||
const hint = "argumentHint" in cmd && cmd.argumentHint ? cmd.argumentHint : undefined;
|
||||
const desc = cmd.description ?? "";
|
||||
const fullDesc = hint ? (desc ? `${hint} — ${desc}` : hint) : desc;
|
||||
return {
|
||||
name,
|
||||
label: name,
|
||||
description: fullDesc || undefined,
|
||||
};
|
||||
});
|
||||
|
||||
const filtered = fuzzyFilter(commandItems, prefix, (item) => item.name).map((item) => ({
|
||||
value: item.name,
|
||||
|
||||
Reference in New Issue
Block a user