feat: autocomplete structured commands
This commit is contained in:
@@ -382,6 +382,86 @@ describe("structured connection: data flow", () => {
|
||||
describe("structured connection: turn/composer gating", () => {
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
const commandNotice = (commands: Array<{ name: string; description: string; hint?: string }>) =>
|
||||
row("commands", "notice", {
|
||||
sessionUpdate: "available_commands_update",
|
||||
availableCommands: commands.map(({ name, description, hint }) => ({
|
||||
name,
|
||||
description,
|
||||
...(hint ? { input: { hint } } : {}),
|
||||
})),
|
||||
});
|
||||
|
||||
it("autocompletes the live ACP command catalog with keyboard navigation", () => {
|
||||
const ws = new FakeSocket();
|
||||
const conn = mount({ sockets: [ws] });
|
||||
ws.open();
|
||||
ws.emit({
|
||||
type: "transcript_snapshot",
|
||||
snapshot: {
|
||||
cursor: 1,
|
||||
rows: [
|
||||
commandNotice([
|
||||
{ name: "yolo", description: "Implement directly" },
|
||||
{ name: "blitz", description: "Complete a milestone", hint: "<milestone>" },
|
||||
]),
|
||||
],
|
||||
},
|
||||
});
|
||||
ws.emit({ type: "reduced_state", state: idleState });
|
||||
const input = conn.el.querySelector<HTMLTextAreaElement>(".structured-input")!;
|
||||
const menu = conn.el.querySelector<HTMLElement>(".structured-command-menu")!;
|
||||
input.value = "/";
|
||||
input.dispatchEvent(new Event("input"));
|
||||
|
||||
expect(menu.hidden).toBe(false);
|
||||
expect([...menu.querySelectorAll(".structured-command-name")].map((node) => node.textContent)).toEqual([
|
||||
"/blitz",
|
||||
"/yolo",
|
||||
]);
|
||||
expect(menu.querySelector(".structured-command-hint")!.textContent).toBe("<milestone>");
|
||||
|
||||
input.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true }));
|
||||
input.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
|
||||
expect(input.value).toBe("/yolo ");
|
||||
expect(menu.hidden).toBe(true);
|
||||
expect(sendStructuredPrompt).not.toHaveBeenCalled();
|
||||
|
||||
input.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
|
||||
expect(sendStructuredPrompt).toHaveBeenCalledWith("s1", "/yolo ");
|
||||
conn.dispose();
|
||||
});
|
||||
|
||||
it("filters commands as the operator types and accepts a touch selection", () => {
|
||||
const ws = new FakeSocket();
|
||||
const conn = mount({ sockets: [ws] });
|
||||
ws.open();
|
||||
ws.emit({
|
||||
type: "transcript_snapshot",
|
||||
snapshot: {
|
||||
cursor: 1,
|
||||
rows: [
|
||||
commandNotice([
|
||||
{ name: "hourlog", description: "Reconcile hours" },
|
||||
{ name: "humanizer", description: "Rewrite prose" },
|
||||
{ name: "blitz", description: "Complete a milestone" },
|
||||
]),
|
||||
],
|
||||
},
|
||||
});
|
||||
ws.emit({ type: "reduced_state", state: idleState });
|
||||
const input = conn.el.querySelector<HTMLTextAreaElement>(".structured-input")!;
|
||||
input.value = "/hum";
|
||||
input.dispatchEvent(new Event("input"));
|
||||
const options = conn.el.querySelectorAll<HTMLElement>(".structured-command-option");
|
||||
expect(options).toHaveLength(1);
|
||||
expect(options[0].textContent).toContain("/humanizer");
|
||||
options[0].click();
|
||||
expect(input.value).toBe("/humanizer ");
|
||||
expect(document.activeElement).toBe(input);
|
||||
conn.dispose();
|
||||
});
|
||||
|
||||
it("disables the composer while a turn is active and re-enables when idle", () => {
|
||||
const ws = new FakeSocket();
|
||||
const conn = mount({ sockets: [ws] });
|
||||
|
||||
Reference in New Issue
Block a user