From aa25726ebf2c104f80537a0327a5ba11de1665b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Vinueza?= Date: Thu, 16 Apr 2026 16:02:35 -0500 Subject: [PATCH] feat(coding-agent,tui): support argument-hint frontmatter in prompt templates (#2780) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 /[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 --- .pi/prompts/is.md | 1 + .pi/prompts/pr.md | 1 + .pi/prompts/wr.md | 1 + packages/coding-agent/CHANGELOG.md | 2 + .../coding-agent/docs/prompt-templates.md | 21 +++ .../coding-agent/src/core/prompt-templates.ts | 2 + .../src/modes/interactive/interactive-mode.ts | 1 + .../test/prompt-templates.test.ts | 127 +++++++++++++++++- packages/tui/CHANGELOG.md | 4 + packages/tui/src/autocomplete.ts | 17 ++- 10 files changed, 170 insertions(+), 7 deletions(-) diff --git a/.pi/prompts/is.md b/.pi/prompts/is.md index cf7da408..3dee06a5 100644 --- a/.pi/prompts/is.md +++ b/.pi/prompts/is.md @@ -1,5 +1,6 @@ --- description: Analyze GitHub issues (bugs or feature requests) +argument-hint: "" --- Analyze GitHub issue(s): $ARGUMENTS diff --git a/.pi/prompts/pr.md b/.pi/prompts/pr.md index cda6ad79..b8ce2da6 100644 --- a/.pi/prompts/pr.md +++ b/.pi/prompts/pr.md @@ -1,5 +1,6 @@ --- description: Review PRs from URLs with structured issue and code analysis +argument-hint: "" --- You are given one or more GitHub PR URLs: $@ diff --git a/.pi/prompts/wr.md b/.pi/prompts/wr.md index fa0250bc..65108e9c 100644 --- a/.pi/prompts/wr.md +++ b/.pi/prompts/wr.md @@ -1,5 +1,6 @@ --- description: Finish the current task end-to-end with changelog, commit, and push +argument-hint: "[instructions]" --- Wrap it. diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 60183d2f..f63d4f33 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -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)) diff --git a/packages/coding-agent/docs/prompt-templates.md b/packages/coding-agent/docs/prompt-templates.md index d6e540bb..056d2c9a 100644 --- a/packages/coding-agent/docs/prompt-templates.md +++ b/packages/coding-agent/docs/prompt-templates.md @@ -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 `` for required arguments and `[square brackets]` for optional ones: + +```markdown +--- +description: Review PRs from URLs with structured issue and code analysis +argument-hint: "" +--- +``` + +This renders in the autocomplete dropdown as: + +``` +→ pr — Review PRs from URLs with structured issue and code analysis + is — Analyze GitHub issues (bugs or feature requests) + wr [instructions] — Finish the current task end-to-end + cl — Audit changelog entries before release +``` ## Usage diff --git a/packages/coding-agent/src/core/prompt-templates.ts b/packages/coding-agent/src/core/prompt-templates.ts index 7c5dbc3f..4850351f 100644 --- a/packages/coding-agent/src/core/prompt-templates.ts +++ b/packages/coding-agent/src/core/prompt-templates.ts @@ -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, diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 4630c527..94d7b414 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -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 diff --git a/packages/coding-agent/test/prompt-templates.test.ts b/packages/coding-agent/test/prompt-templates.test.ts index 168f7407..bef43f44 100644 --- a/packages/coding-agent/test/prompt-templates.test.ts +++ b/packages/coding-agent/test/prompt-templates.test.ts @@ -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: "" +--- +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(""); + 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: "" +--- +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(""); + }); + + afterAll(() => { + try { + rmSync(testDir, { recursive: true, force: true }); + } catch {} + }); +}); diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 57ddcd8e..628628a7 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -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 diff --git a/packages/tui/src/autocomplete.ts b/packages/tui/src/autocomplete.ts index 792f119b..4ff1b6b7 100644 --- a/packages/tui/src/autocomplete.ts +++ b/packages/tui/src/autocomplete.ts @@ -223,6 +223,7 @@ type Awaitable = T | Promise; 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; @@ -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,