diff --git a/packages/app/e2e/client-slash-commands.spec.ts b/packages/app/e2e/client-slash-commands.spec.ts index 8e70ceb08..4923d2307 100644 --- a/packages/app/e2e/client-slash-commands.spec.ts +++ b/packages/app/e2e/client-slash-commands.spec.ts @@ -181,7 +181,7 @@ test.describe("Client slash commands", () => { page, { title: "Slash quit autocomplete e2e" }, async ({ agent, title }) => { - await selectClientSlashCommand(page, "/qu", "/quit"); + await selectClientSlashCommand(page, "/qu", "/exit"); await expectWorkspaceTabHidden(page, agent.id); await expectAgentArchivedInSessions(page, title); }, diff --git a/packages/app/src/client-slash-commands/index.test.ts b/packages/app/src/client-slash-commands/index.test.ts index b22e0c737..3fd4e4b7c 100644 --- a/packages/app/src/client-slash-commands/index.test.ts +++ b/packages/app/src/client-slash-commands/index.test.ts @@ -57,33 +57,41 @@ function createAgent(overrides: Partial = {}): Agent { } describe("resolveClientSlashCommand", () => { - it("declares the exact client commands that execute immediately", () => { - expect(CLIENT_SLASH_COMMANDS.map((command) => [command.name, command.execution])).toEqual([ - ["quit", "immediate"], - ["exit", "immediate"], - ["q", "immediate"], - ["clear", "immediate"], - ["new", "immediate"], + it("declares the exact canonical client commands with their aliases", () => { + expect( + CLIENT_SLASH_COMMANDS.map((command) => [ + command.name, + [...command.aliases], + command.execution, + ]), + ).toEqual([ + ["exit", ["quit", "q"], "immediate"], + ["clear", ["new"], "immediate"], ]); }); - it("resolves exact client commands after trimming", () => { + it("resolves canonical names and aliases after trimming", () => { expect(resolveClientSlashCommand({ text: " /quit ", hasAttachments: false })).toMatchObject({ + name: "exit", kind: "archive-agent", execution: "immediate", }); - expect(resolveClientSlashCommand({ text: "/exit", hasAttachments: false })?.kind).toBe( - "archive-agent", - ); - expect(resolveClientSlashCommand({ text: "/q", hasAttachments: false })?.kind).toBe( - "archive-agent", - ); - expect(resolveClientSlashCommand({ text: "/clear", hasAttachments: false })?.kind).toBe( - "replace-agent-with-draft", - ); - expect(resolveClientSlashCommand({ text: "/new", hasAttachments: false })?.kind).toBe( - "replace-agent-with-draft", - ); + expect(resolveClientSlashCommand({ text: "/exit", hasAttachments: false })).toMatchObject({ + name: "exit", + kind: "archive-agent", + }); + expect(resolveClientSlashCommand({ text: "/q", hasAttachments: false })).toMatchObject({ + name: "exit", + kind: "archive-agent", + }); + expect(resolveClientSlashCommand({ text: "/clear", hasAttachments: false })).toMatchObject({ + name: "clear", + kind: "replace-agent-with-draft", + }); + expect(resolveClientSlashCommand({ text: "/new", hasAttachments: false })).toMatchObject({ + name: "clear", + kind: "replace-agent-with-draft", + }); }); it("leaves provider commands, arguments, ordinary messages, and attachment submits alone", () => { diff --git a/packages/app/src/client-slash-commands/index.ts b/packages/app/src/client-slash-commands/index.ts index 3ab8cdca1..695d07ef3 100644 --- a/packages/app/src/client-slash-commands/index.ts +++ b/packages/app/src/client-slash-commands/index.ts @@ -6,6 +6,7 @@ export type ClientSlashCommandExecution = "immediate" | "insert"; export interface ClientSlashCommand { name: string; + aliases: readonly string[]; description: string; argumentHint: string; kind: ClientSlashCommandKind; @@ -13,22 +14,9 @@ export interface ClientSlashCommand { } export const CLIENT_SLASH_COMMANDS: readonly ClientSlashCommand[] = [ - { - name: "quit", - description: "Archive the current agent", - argumentHint: "", - kind: "archive-agent", - execution: "immediate", - }, { name: "exit", - description: "Archive the current agent", - argumentHint: "", - kind: "archive-agent", - execution: "immediate", - }, - { - name: "q", + aliases: ["quit", "q"], description: "Archive the current agent", argumentHint: "", kind: "archive-agent", @@ -36,13 +24,7 @@ export const CLIENT_SLASH_COMMANDS: readonly ClientSlashCommand[] = [ }, { name: "clear", - description: "Archive this agent and start a fresh draft", - argumentHint: "", - kind: "replace-agent-with-draft", - execution: "immediate", - }, - { - name: "new", + aliases: ["new"], description: "Archive this agent and start a fresh draft", argumentHint: "", kind: "replace-agent-with-draft", @@ -50,7 +32,13 @@ export const CLIENT_SLASH_COMMANDS: readonly ClientSlashCommand[] = [ }, ]; -const COMMAND_BY_NAME = new Map(CLIENT_SLASH_COMMANDS.map((command) => [command.name, command])); +const COMMAND_BY_NAME = new Map(); +for (const command of CLIENT_SLASH_COMMANDS) { + COMMAND_BY_NAME.set(command.name, command); + for (const alias of command.aliases) { + COMMAND_BY_NAME.set(alias, command); + } +} export function resolveClientSlashCommand(input: { text: string; diff --git a/packages/app/src/hooks/use-agent-autocomplete.ts b/packages/app/src/hooks/use-agent-autocomplete.ts index f9ab93ee5..cf875a526 100644 --- a/packages/app/src/hooks/use-agent-autocomplete.ts +++ b/packages/app/src/hooks/use-agent-autocomplete.ts @@ -324,9 +324,13 @@ export function useAgentAutocomplete(input: UseAgentAutocompleteInput): AgentAut ), ...providerCommands, ]; - const matches = availableCommands.filter((entry) => - entry.command.name.toLowerCase().includes(filterLower), - ); + const matches = availableCommands.filter((entry) => { + if (entry.source === "provider") { + return entry.command.name.toLowerCase().includes(filterLower); + } + const candidates = [entry.command.name, ...entry.command.aliases]; + return candidates.some((candidate) => candidate.toLowerCase().includes(filterLower)); + }); const orderedMatches = orderAutocompleteOptions(matches); return orderedMatches.map(mapCommandToOption); }