feat(autocomplete): add /thinking completions, GSD subcommand descriptions, and test coverage (#1019)
- Add argument completions for /thinking command with all 6 levels (off, minimal, low, medium, high, xhigh) and descriptions - Add descriptions to all GSD 2nd-level subcommand completions across 14 subcommand groups (auto, mode, parallel, setup, prefs, remote, next, history, undo, export, cleanup, knowledge, doctor, dispatch) - Add 35 new tests for autocomplete and fuzzy matching systems
This commit is contained in:
parent
2e34e83a26
commit
fd9565299c
5 changed files with 478 additions and 40 deletions
46
.plans/autocomplete-qol-improvements.md
Normal file
46
.plans/autocomplete-qol-improvements.md
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# Plan: Autocomplete QOL Improvements
|
||||
|
||||
## Goal
|
||||
Maximize quality-of-life for the autocomplete system by adding missing argument completions, improving discoverability with descriptions, and adding test coverage.
|
||||
|
||||
## Changes
|
||||
|
||||
### 1. Add `/thinking` argument completions (interactive-mode.ts)
|
||||
- Add `getArgumentCompletions` to the `thinking` builtin command
|
||||
- Values: `off`, `minimal`, `low`, `medium`, `high`, `xhigh` with descriptions
|
||||
- Location: `setupAutocomplete()` in interactive-mode.ts, after the `/model` block
|
||||
|
||||
### 2. Add descriptions to GSD 2nd-level subcommand completions (commands.ts)
|
||||
- Currently `/gsd auto --verbose` shows label only, no description
|
||||
- Add descriptions to all 2nd-level completion items across:
|
||||
- `auto` flags: --verbose, --debug
|
||||
- `mode` subcommands: global, project
|
||||
- `parallel` subcommands: start, status, stop, pause, resume, merge
|
||||
- `setup` subcommands: llm, search, remote, keys, prefs
|
||||
- `prefs` subcommands: global, project, status, wizard, setup, import-claude
|
||||
- `remote` subcommands: slack, discord, status, disconnect
|
||||
- `next` flags: --verbose, --dry-run
|
||||
- `history` flags: --cost, --phase, --model, 10, 20, 50
|
||||
- `undo`: --force
|
||||
- `export` flags: --json, --markdown, --html, --html --all
|
||||
- `cleanup` subcommands: branches, snapshots
|
||||
- `knowledge` subcommands: rule, pattern, lesson
|
||||
- `doctor` modes: fix, heal, audit
|
||||
- `dispatch` phases: research, plan, execute, complete, reassess, uat, replan
|
||||
|
||||
### 3. Add test coverage for autocomplete.ts and fuzzy.ts
|
||||
- Test file: `packages/pi-tui/src/tests/autocomplete.test.ts`
|
||||
- Cover: slash command completion, argument completion, @ file prefix extraction, path prefix extraction, apply completion
|
||||
- Test file: `packages/pi-tui/src/tests/fuzzy.test.ts`
|
||||
- Cover: basic matching, scoring, word boundaries, gap penalties, token splitting, alphanumeric swaps
|
||||
|
||||
## Files Modified
|
||||
- `packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts` — thinking completions
|
||||
- `src/resources/extensions/gsd/commands.ts` — 2nd-level descriptions
|
||||
- `packages/pi-tui/src/tests/autocomplete.test.ts` — new test file
|
||||
- `packages/pi-tui/src/tests/fuzzy.test.ts` — new test file
|
||||
|
||||
## Testing
|
||||
- Run existing test suite to verify no regressions
|
||||
- Run new test files
|
||||
- Build to verify TypeScript compiles
|
||||
|
|
@ -319,6 +319,23 @@ export class InteractiveMode {
|
|||
};
|
||||
}
|
||||
|
||||
// Add argument completions for /thinking
|
||||
const thinkingCommand = slashCommands.find((command) => command.name === "thinking");
|
||||
if (thinkingCommand) {
|
||||
thinkingCommand.getArgumentCompletions = (prefix: string): AutocompleteItem[] | null => {
|
||||
const levels = [
|
||||
{ value: "off", label: "off", description: "Disable extended thinking" },
|
||||
{ value: "minimal", label: "minimal", description: "Minimal thinking budget" },
|
||||
{ value: "low", label: "low", description: "Low thinking budget" },
|
||||
{ value: "medium", label: "medium", description: "Medium thinking budget" },
|
||||
{ value: "high", label: "high", description: "High thinking budget" },
|
||||
{ value: "xhigh", label: "xhigh", description: "Maximum thinking budget" },
|
||||
];
|
||||
const filtered = levels.filter((l) => l.value.startsWith(prefix.trim().toLowerCase()));
|
||||
return filtered.length > 0 ? filtered : null;
|
||||
};
|
||||
}
|
||||
|
||||
// Convert prompt templates to SlashCommand format for autocomplete
|
||||
const templateCommands: SlashCommand[] = this.session.promptTemplates.map((cmd) => ({
|
||||
name: cmd.name,
|
||||
|
|
|
|||
186
packages/pi-tui/src/__tests__/autocomplete.test.ts
Normal file
186
packages/pi-tui/src/__tests__/autocomplete.test.ts
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { CombinedAutocompleteProvider } from "../autocomplete.js";
|
||||
import type { SlashCommand } from "../autocomplete.js";
|
||||
|
||||
function makeProvider(commands: SlashCommand[] = [], basePath: string = "/tmp") {
|
||||
return new CombinedAutocompleteProvider(commands, basePath);
|
||||
}
|
||||
|
||||
const sampleCommands: SlashCommand[] = [
|
||||
{ name: "settings", description: "Open settings menu" },
|
||||
{ name: "model", description: "Select model" },
|
||||
{ name: "session", description: "Show session info" },
|
||||
{ name: "export", description: "Export session" },
|
||||
{ name: "thinking", description: "Set thinking level" },
|
||||
];
|
||||
|
||||
describe("CombinedAutocompleteProvider — slash commands", () => {
|
||||
it("returns all commands for bare /", () => {
|
||||
const provider = makeProvider(sampleCommands);
|
||||
const result = provider.getSuggestions(["/"], 0, 1);
|
||||
assert.ok(result, "should return suggestions");
|
||||
assert.equal(result!.items.length, sampleCommands.length);
|
||||
assert.equal(result!.prefix, "/");
|
||||
});
|
||||
|
||||
it("filters commands by typed prefix", () => {
|
||||
const provider = makeProvider(sampleCommands);
|
||||
const result = provider.getSuggestions(["/se"], 0, 3);
|
||||
assert.ok(result);
|
||||
assert.equal(result!.items.length, 2); // settings, session
|
||||
assert.ok(result!.items.some((i) => i.value === "settings"));
|
||||
assert.ok(result!.items.some((i) => i.value === "session"));
|
||||
});
|
||||
|
||||
it("returns null when no commands match", () => {
|
||||
const provider = makeProvider(sampleCommands);
|
||||
const result = provider.getSuggestions(["/zzz"], 0, 4);
|
||||
assert.equal(result, null);
|
||||
});
|
||||
|
||||
it("includes description in suggestions", () => {
|
||||
const provider = makeProvider(sampleCommands);
|
||||
const result = provider.getSuggestions(["/mod"], 0, 4);
|
||||
assert.ok(result);
|
||||
assert.equal(result!.items[0]?.description, "Select model");
|
||||
});
|
||||
|
||||
it("does not trigger slash commands mid-line", () => {
|
||||
const provider = makeProvider(sampleCommands);
|
||||
// "/" not at position 0 in the line — should not match slash commands
|
||||
const result = provider.getSuggestions(["hello /se"], 0, 9);
|
||||
assert.equal(result, null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("CombinedAutocompleteProvider — argument completions", () => {
|
||||
it("returns argument completions for commands that support them", () => {
|
||||
const commands: SlashCommand[] = [
|
||||
{
|
||||
name: "thinking",
|
||||
description: "Set thinking level",
|
||||
getArgumentCompletions: (prefix) => {
|
||||
const levels = ["off", "low", "medium", "high"];
|
||||
const filtered = levels
|
||||
.filter((l) => l.startsWith(prefix.trim()))
|
||||
.map((l) => ({ value: l, label: l }));
|
||||
return filtered.length > 0 ? filtered : null;
|
||||
},
|
||||
},
|
||||
];
|
||||
const provider = makeProvider(commands);
|
||||
const result = provider.getSuggestions(["/thinking m"], 0, 11);
|
||||
assert.ok(result);
|
||||
assert.equal(result!.items.length, 1);
|
||||
assert.equal(result!.items[0]?.value, "medium");
|
||||
});
|
||||
|
||||
it("returns null for commands without argument completions", () => {
|
||||
const provider = makeProvider(sampleCommands);
|
||||
const result = provider.getSuggestions(["/settings foo"], 0, 13);
|
||||
assert.equal(result, null);
|
||||
});
|
||||
|
||||
it("returns all arg completions for empty prefix after space", () => {
|
||||
const commands: SlashCommand[] = [
|
||||
{
|
||||
name: "test",
|
||||
description: "Test command",
|
||||
getArgumentCompletions: (prefix) => {
|
||||
const subs = ["start", "stop", "status"];
|
||||
const filtered = subs
|
||||
.filter((s) => s.startsWith(prefix.trim()))
|
||||
.map((s) => ({ value: s, label: s }));
|
||||
return filtered.length > 0 ? filtered : null;
|
||||
},
|
||||
},
|
||||
];
|
||||
const provider = makeProvider(commands);
|
||||
const result = provider.getSuggestions(["/test "], 0, 6);
|
||||
assert.ok(result);
|
||||
assert.equal(result!.items.length, 3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("CombinedAutocompleteProvider — @ file prefix extraction", () => {
|
||||
it("detects @ at start of line", () => {
|
||||
const provider = makeProvider();
|
||||
// @ triggers fuzzy file search — we can't test the actual file results
|
||||
// but we can test that getSuggestions returns null (no files in /tmp matching)
|
||||
// rather than crashing
|
||||
const result = provider.getSuggestions(["@nonexistent_xyz"], 0, 16);
|
||||
// May return null or empty — the key thing is it doesn't crash
|
||||
assert.ok(result === null || result.items.length >= 0);
|
||||
});
|
||||
|
||||
it("detects @ after space", () => {
|
||||
const provider = makeProvider();
|
||||
const result = provider.getSuggestions(["check @nonexistent_xyz"], 0, 22);
|
||||
assert.ok(result === null || result.items.length >= 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("CombinedAutocompleteProvider — applyCompletion", () => {
|
||||
it("applies slash command completion with trailing space", () => {
|
||||
const provider = makeProvider(sampleCommands);
|
||||
const result = provider.applyCompletion(["/se"], 0, 3, { value: "settings", label: "settings" }, "/se");
|
||||
assert.equal(result.lines[0], "/settings ");
|
||||
assert.equal(result.cursorCol, 10); // after "/settings "
|
||||
});
|
||||
|
||||
it("applies file path completion for @ prefix", () => {
|
||||
const provider = makeProvider();
|
||||
const result = provider.applyCompletion(
|
||||
["@src/"],
|
||||
0,
|
||||
5,
|
||||
{ value: "@src/index.ts", label: "index.ts" },
|
||||
"@src/",
|
||||
);
|
||||
assert.equal(result.lines[0], "@src/index.ts ");
|
||||
});
|
||||
|
||||
it("applies directory completion without trailing space", () => {
|
||||
const provider = makeProvider();
|
||||
const result = provider.applyCompletion(
|
||||
["@sr"],
|
||||
0,
|
||||
3,
|
||||
{ value: "@src/", label: "src/" },
|
||||
"@sr",
|
||||
);
|
||||
// Directories should not get trailing space so user can continue typing
|
||||
assert.ok(!result.lines[0]!.endsWith(" "));
|
||||
});
|
||||
|
||||
it("preserves text after cursor", () => {
|
||||
const provider = makeProvider(sampleCommands);
|
||||
const result = provider.applyCompletion(
|
||||
["/se and more text"],
|
||||
0,
|
||||
3,
|
||||
{ value: "settings", label: "settings" },
|
||||
"/se",
|
||||
);
|
||||
assert.ok(result.lines[0]!.includes("and more text"));
|
||||
});
|
||||
});
|
||||
|
||||
describe("CombinedAutocompleteProvider — force file suggestions", () => {
|
||||
it("does not trigger for slash commands", () => {
|
||||
const provider = makeProvider(sampleCommands);
|
||||
const result = provider.getForceFileSuggestions(["/set"], 0, 4);
|
||||
assert.equal(result, null);
|
||||
});
|
||||
|
||||
it("shouldTriggerFileCompletion returns false for slash commands", () => {
|
||||
const provider = makeProvider(sampleCommands);
|
||||
assert.equal(provider.shouldTriggerFileCompletion(["/set"], 0, 4), false);
|
||||
});
|
||||
|
||||
it("shouldTriggerFileCompletion returns true for regular text", () => {
|
||||
const provider = makeProvider();
|
||||
assert.equal(provider.shouldTriggerFileCompletion(["some text"], 0, 9), true);
|
||||
});
|
||||
});
|
||||
112
packages/pi-tui/src/__tests__/fuzzy.test.ts
Normal file
112
packages/pi-tui/src/__tests__/fuzzy.test.ts
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { fuzzyMatch, fuzzyFilter } from "../fuzzy.js";
|
||||
|
||||
describe("fuzzyMatch", () => {
|
||||
it("matches exact string", () => {
|
||||
const result = fuzzyMatch("hello", "hello");
|
||||
assert.equal(result.matches, true);
|
||||
});
|
||||
|
||||
it("matches substring characters in order", () => {
|
||||
const result = fuzzyMatch("hlo", "hello");
|
||||
assert.equal(result.matches, true);
|
||||
});
|
||||
|
||||
it("does not match when characters are out of order", () => {
|
||||
const result = fuzzyMatch("olh", "hello");
|
||||
assert.equal(result.matches, false);
|
||||
});
|
||||
|
||||
it("empty query matches everything", () => {
|
||||
const result = fuzzyMatch("", "anything");
|
||||
assert.equal(result.matches, true);
|
||||
assert.equal(result.score, 0);
|
||||
});
|
||||
|
||||
it("does not match when query is longer than text", () => {
|
||||
const result = fuzzyMatch("toolong", "short");
|
||||
assert.equal(result.matches, false);
|
||||
});
|
||||
|
||||
it("is case insensitive", () => {
|
||||
const result = fuzzyMatch("ABC", "abcdef");
|
||||
assert.equal(result.matches, true);
|
||||
});
|
||||
|
||||
it("rewards consecutive matches with lower score", () => {
|
||||
const consecutive = fuzzyMatch("hel", "hello");
|
||||
const gapped = fuzzyMatch("hlo", "hello");
|
||||
assert.ok(consecutive.score < gapped.score, "consecutive matches should score lower (better)");
|
||||
});
|
||||
|
||||
it("rewards word boundary matches", () => {
|
||||
const boundary = fuzzyMatch("sc", "slash-command");
|
||||
const nonBoundary = fuzzyMatch("sc", "describe");
|
||||
assert.ok(boundary.score < nonBoundary.score, "word boundary matches should score lower (better)");
|
||||
});
|
||||
|
||||
it("handles alphanumeric swap (e.g., opus3 matches opus-3)", () => {
|
||||
const result = fuzzyMatch("opus3", "opus-3");
|
||||
assert.equal(result.matches, true);
|
||||
});
|
||||
|
||||
it("handles numeric-alpha swap", () => {
|
||||
const result = fuzzyMatch("3opus", "opus-3");
|
||||
assert.equal(result.matches, true);
|
||||
});
|
||||
|
||||
it("does not match completely unrelated strings", () => {
|
||||
const result = fuzzyMatch("xyz", "hello");
|
||||
assert.equal(result.matches, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("fuzzyFilter", () => {
|
||||
const items = ["settings", "session", "share", "model", "compact", "export"];
|
||||
|
||||
it("returns all items for empty query", () => {
|
||||
const result = fuzzyFilter(items, "", (x) => x);
|
||||
assert.equal(result.length, items.length);
|
||||
});
|
||||
|
||||
it("filters to matching items only", () => {
|
||||
const result = fuzzyFilter(items, "se", (x) => x);
|
||||
assert.ok(result.includes("settings"));
|
||||
assert.ok(result.includes("session"));
|
||||
assert.ok(!result.includes("model"));
|
||||
});
|
||||
|
||||
it("sorts by match quality (best first)", () => {
|
||||
const result = fuzzyFilter(items, "ex", (x) => x);
|
||||
assert.equal(result[0], "export");
|
||||
});
|
||||
|
||||
it("supports space-separated tokens (all must match)", () => {
|
||||
const data = ["anthropic/opus", "anthropic/sonnet", "openai/gpt4"];
|
||||
const result = fuzzyFilter(data, "ant opus", (x) => x);
|
||||
assert.equal(result.length, 1);
|
||||
assert.equal(result[0], "anthropic/opus");
|
||||
});
|
||||
|
||||
it("returns empty array when no items match", () => {
|
||||
const result = fuzzyFilter(items, "zzz", (x) => x);
|
||||
assert.equal(result.length, 0);
|
||||
});
|
||||
|
||||
it("works with custom getText function", () => {
|
||||
const objects = [
|
||||
{ name: "alpha", id: 1 },
|
||||
{ name: "beta", id: 2 },
|
||||
{ name: "gamma", id: 3 },
|
||||
];
|
||||
const result = fuzzyFilter(objects, "bet", (o) => o.name);
|
||||
assert.equal(result.length, 1);
|
||||
assert.equal(result[0]?.name, "beta");
|
||||
});
|
||||
|
||||
it("handles whitespace-only query as empty", () => {
|
||||
const result = fuzzyFilter(items, " ", (x) => x);
|
||||
assert.equal(result.length, items.length);
|
||||
});
|
||||
});
|
||||
|
|
@ -131,93 +131,161 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|||
|
||||
if (parts[0] === "auto" && parts.length <= 2) {
|
||||
const flagPrefix = parts[1] ?? "";
|
||||
return ["--verbose", "--debug"]
|
||||
.filter((f) => f.startsWith(flagPrefix))
|
||||
.map((f) => ({ value: `auto ${f}`, label: f }));
|
||||
const flags = [
|
||||
{ flag: "--verbose", desc: "Show detailed execution output" },
|
||||
{ flag: "--debug", desc: "Enable debug logging" },
|
||||
];
|
||||
return flags
|
||||
.filter((f) => f.flag.startsWith(flagPrefix))
|
||||
.map((f) => ({ value: `auto ${f.flag}`, label: f.flag, description: f.desc }));
|
||||
}
|
||||
|
||||
if (parts[0] === "mode" && parts.length <= 2) {
|
||||
const subPrefix = parts[1] ?? "";
|
||||
return ["global", "project"]
|
||||
.filter((cmd) => cmd.startsWith(subPrefix))
|
||||
.map((cmd) => ({ value: `mode ${cmd}`, label: cmd }));
|
||||
const modes = [
|
||||
{ cmd: "global", desc: "Edit global workflow mode" },
|
||||
{ cmd: "project", desc: "Edit project-specific workflow mode" },
|
||||
];
|
||||
return modes
|
||||
.filter((m) => m.cmd.startsWith(subPrefix))
|
||||
.map((m) => ({ value: `mode ${m.cmd}`, label: m.cmd, description: m.desc }));
|
||||
}
|
||||
|
||||
if (parts[0] === "parallel" && parts.length <= 2) {
|
||||
const subPrefix = parts[1] ?? "";
|
||||
return ["start", "status", "stop", "pause", "resume", "merge"]
|
||||
.filter((cmd) => cmd.startsWith(subPrefix))
|
||||
.map((cmd) => ({ value: `parallel ${cmd}`, label: cmd }));
|
||||
const subs = [
|
||||
{ cmd: "start", desc: "Start parallel milestone orchestration" },
|
||||
{ cmd: "status", desc: "Show parallel worker statuses" },
|
||||
{ cmd: "stop", desc: "Stop all parallel workers" },
|
||||
{ cmd: "pause", desc: "Pause a specific worker" },
|
||||
{ cmd: "resume", desc: "Resume a paused worker" },
|
||||
{ cmd: "merge", desc: "Merge completed milestone branches" },
|
||||
];
|
||||
return subs
|
||||
.filter((s) => s.cmd.startsWith(subPrefix))
|
||||
.map((s) => ({ value: `parallel ${s.cmd}`, label: s.cmd, description: s.desc }));
|
||||
}
|
||||
|
||||
if (parts[0] === "setup" && parts.length <= 2) {
|
||||
const subPrefix = parts[1] ?? "";
|
||||
return ["llm", "search", "remote", "keys", "prefs"]
|
||||
.filter((cmd) => cmd.startsWith(subPrefix))
|
||||
.map((cmd) => ({ value: `setup ${cmd}`, label: cmd }));
|
||||
const subs = [
|
||||
{ cmd: "llm", desc: "Configure LLM provider settings" },
|
||||
{ cmd: "search", desc: "Configure web search provider" },
|
||||
{ cmd: "remote", desc: "Configure remote integrations" },
|
||||
{ cmd: "keys", desc: "Manage API keys" },
|
||||
{ cmd: "prefs", desc: "Configure global preferences" },
|
||||
];
|
||||
return subs
|
||||
.filter((s) => s.cmd.startsWith(subPrefix))
|
||||
.map((s) => ({ value: `setup ${s.cmd}`, label: s.cmd, description: s.desc }));
|
||||
}
|
||||
|
||||
if (parts[0] === "prefs" && parts.length <= 2) {
|
||||
const subPrefix = parts[1] ?? "";
|
||||
return ["global", "project", "status", "wizard", "setup", "import-claude"]
|
||||
.filter((cmd) => cmd.startsWith(subPrefix))
|
||||
.map((cmd) => ({ value: `prefs ${cmd}`, label: cmd }));
|
||||
const subs = [
|
||||
{ cmd: "global", desc: "Edit global preferences file" },
|
||||
{ cmd: "project", desc: "Edit project preferences file" },
|
||||
{ cmd: "status", desc: "Show effective preferences" },
|
||||
{ cmd: "wizard", desc: "Interactive preferences wizard" },
|
||||
{ cmd: "setup", desc: "First-time preferences setup" },
|
||||
{ cmd: "import-claude", desc: "Import settings from Claude Code" },
|
||||
];
|
||||
return subs
|
||||
.filter((s) => s.cmd.startsWith(subPrefix))
|
||||
.map((s) => ({ value: `prefs ${s.cmd}`, label: s.cmd, description: s.desc }));
|
||||
}
|
||||
|
||||
if (parts[0] === "remote" && parts.length <= 2) {
|
||||
const subPrefix = parts[1] ?? "";
|
||||
return ["slack", "discord", "status", "disconnect"]
|
||||
.filter((cmd) => cmd.startsWith(subPrefix))
|
||||
.map((cmd) => ({ value: `remote ${cmd}`, label: cmd }));
|
||||
const subs = [
|
||||
{ cmd: "slack", desc: "Configure Slack integration" },
|
||||
{ cmd: "discord", desc: "Configure Discord integration" },
|
||||
{ cmd: "status", desc: "Show remote connection status" },
|
||||
{ cmd: "disconnect", desc: "Disconnect remote integrations" },
|
||||
];
|
||||
return subs
|
||||
.filter((s) => s.cmd.startsWith(subPrefix))
|
||||
.map((s) => ({ value: `remote ${s.cmd}`, label: s.cmd, description: s.desc }));
|
||||
}
|
||||
|
||||
if (parts[0] === "next" && parts.length <= 2) {
|
||||
const flagPrefix = parts[1] ?? "";
|
||||
return ["--verbose", "--dry-run"]
|
||||
.filter((f) => f.startsWith(flagPrefix))
|
||||
.map((f) => ({ value: `next ${f}`, label: f }));
|
||||
const flags = [
|
||||
{ flag: "--verbose", desc: "Show detailed step output" },
|
||||
{ flag: "--dry-run", desc: "Preview next step without executing" },
|
||||
];
|
||||
return flags
|
||||
.filter((f) => f.flag.startsWith(flagPrefix))
|
||||
.map((f) => ({ value: `next ${f.flag}`, label: f.flag, description: f.desc }));
|
||||
}
|
||||
|
||||
if (parts[0] === "history" && parts.length <= 2) {
|
||||
const flagPrefix = parts[1] ?? "";
|
||||
return ["--cost", "--phase", "--model", "10", "20", "50"]
|
||||
.filter((f) => f.startsWith(flagPrefix))
|
||||
.map((f) => ({ value: `history ${f}`, label: f }));
|
||||
const flags = [
|
||||
{ flag: "--cost", desc: "Show cost breakdown per entry" },
|
||||
{ flag: "--phase", desc: "Filter by phase type" },
|
||||
{ flag: "--model", desc: "Filter by model used" },
|
||||
{ flag: "10", desc: "Show last 10 entries" },
|
||||
{ flag: "20", desc: "Show last 20 entries" },
|
||||
{ flag: "50", desc: "Show last 50 entries" },
|
||||
];
|
||||
return flags
|
||||
.filter((f) => f.flag.startsWith(flagPrefix))
|
||||
.map((f) => ({ value: `history ${f.flag}`, label: f.flag, description: f.desc }));
|
||||
}
|
||||
|
||||
if (parts[0] === "undo" && parts.length <= 2) {
|
||||
return [{ value: "undo --force", label: "--force" }];
|
||||
return [{ value: "undo --force", label: "--force", description: "Skip confirmation prompt" }];
|
||||
}
|
||||
|
||||
if (parts[0] === "export" && parts.length <= 2) {
|
||||
const flagPrefix = parts[1] ?? "";
|
||||
return ["--json", "--markdown", "--html", "--html --all"]
|
||||
.filter((f) => f.startsWith(flagPrefix))
|
||||
.map((f) => ({ value: `export ${f}`, label: f }));
|
||||
const flags = [
|
||||
{ flag: "--json", desc: "Export as JSON" },
|
||||
{ flag: "--markdown", desc: "Export as Markdown" },
|
||||
{ flag: "--html", desc: "Export as HTML" },
|
||||
{ flag: "--html --all", desc: "Export all milestones as HTML" },
|
||||
];
|
||||
return flags
|
||||
.filter((f) => f.flag.startsWith(flagPrefix))
|
||||
.map((f) => ({ value: `export ${f.flag}`, label: f.flag, description: f.desc }));
|
||||
}
|
||||
|
||||
if (parts[0] === "cleanup" && parts.length <= 2) {
|
||||
const subPrefix = parts[1] ?? "";
|
||||
return ["branches", "snapshots"]
|
||||
.filter((cmd) => cmd.startsWith(subPrefix))
|
||||
.map((cmd) => ({ value: `cleanup ${cmd}`, label: cmd }));
|
||||
const subs = [
|
||||
{ cmd: "branches", desc: "Remove merged milestone branches" },
|
||||
{ cmd: "snapshots", desc: "Remove old execution snapshots" },
|
||||
];
|
||||
return subs
|
||||
.filter((s) => s.cmd.startsWith(subPrefix))
|
||||
.map((s) => ({ value: `cleanup ${s.cmd}`, label: s.cmd, description: s.desc }));
|
||||
}
|
||||
|
||||
if (parts[0] === "knowledge" && parts.length <= 2) {
|
||||
const subPrefix = parts[1] ?? "";
|
||||
return ["rule", "pattern", "lesson"]
|
||||
.filter((cmd) => cmd.startsWith(subPrefix))
|
||||
.map((cmd) => ({ value: `knowledge ${cmd}`, label: cmd }));
|
||||
const subs = [
|
||||
{ cmd: "rule", desc: "Add a project rule (always/never do X)" },
|
||||
{ cmd: "pattern", desc: "Add a code pattern to follow" },
|
||||
{ cmd: "lesson", desc: "Record a lesson learned" },
|
||||
];
|
||||
return subs
|
||||
.filter((s) => s.cmd.startsWith(subPrefix))
|
||||
.map((s) => ({ value: `knowledge ${s.cmd}`, label: s.cmd, description: s.desc }));
|
||||
}
|
||||
|
||||
if (parts[0] === "doctor") {
|
||||
const modePrefix = parts[1] ?? "";
|
||||
const modes = ["fix", "heal", "audit"];
|
||||
const modes = [
|
||||
{ cmd: "fix", desc: "Auto-fix detected issues" },
|
||||
{ cmd: "heal", desc: "AI-driven deep healing" },
|
||||
{ cmd: "audit", desc: "Run health audit without fixing" },
|
||||
];
|
||||
|
||||
if (parts.length <= 2) {
|
||||
return modes
|
||||
.filter((cmd) => cmd.startsWith(modePrefix))
|
||||
.map((cmd) => ({ value: `doctor ${cmd}`, label: cmd }));
|
||||
.filter((m) => m.cmd.startsWith(modePrefix))
|
||||
.map((m) => ({ value: `doctor ${m.cmd}`, label: m.cmd, description: m.desc }));
|
||||
}
|
||||
|
||||
return [];
|
||||
|
|
@ -225,9 +293,18 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|||
|
||||
if (parts[0] === "dispatch" && parts.length <= 2) {
|
||||
const phasePrefix = parts[1] ?? "";
|
||||
return ["research", "plan", "execute", "complete", "reassess", "uat", "replan"]
|
||||
.filter((cmd) => cmd.startsWith(phasePrefix))
|
||||
.map((cmd) => ({ value: `dispatch ${cmd}`, label: cmd }));
|
||||
const phases = [
|
||||
{ cmd: "research", desc: "Run research phase" },
|
||||
{ cmd: "plan", desc: "Run planning phase" },
|
||||
{ cmd: "execute", desc: "Run execution phase" },
|
||||
{ cmd: "complete", desc: "Run completion phase" },
|
||||
{ cmd: "reassess", desc: "Reassess current progress" },
|
||||
{ cmd: "uat", desc: "Run user acceptance testing" },
|
||||
{ cmd: "replan", desc: "Replan the current slice" },
|
||||
];
|
||||
return phases
|
||||
.filter((p) => p.cmd.startsWith(phasePrefix))
|
||||
.map((p) => ({ value: `dispatch ${p.cmd}`, label: p.cmd, description: p.desc }));
|
||||
}
|
||||
|
||||
return [];
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue