From b6df91fdd9c5360a868fae44b2d73ee010f073a3 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 24 Aug 2026 23:55:17 -0700 Subject: [PATCH] fix(tui): preserve slash command alias completions (#129039) --- src/tui/commands.test.ts | 31 ++++++++++++++++++++++++++++++- src/tui/commands.ts | 8 ++++---- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/tui/commands.test.ts b/src/tui/commands.test.ts index 99fdcb636e59..2bb16d2ec0e9 100644 --- a/src/tui/commands.test.ts +++ b/src/tui/commands.test.ts @@ -64,6 +64,29 @@ describe("getSlashCommands", () => { ]); }); + it.each([ + { command: "think", alias: "thinking", level: "max" }, + { command: "think", alias: "t", level: "max" }, + { command: "think", alias: "t", level: "default" }, + { command: "verbose", alias: "v", level: "full" }, + { command: "reasoning", alias: "reason", level: "stream" }, + { command: "elevated", alias: "elev", level: "ask" }, + ])("keeps /$command $level completion on its /$alias alias", ({ command, alias, level }) => { + for (const local of [false, true]) { + const commands = getSlashCommands({ + local, + thinkingLevels: [{ id: "max", label: "max" }], + }); + const canonical = commands.find((candidate) => candidate.name === command); + const alternate = commands.find((candidate) => candidate.name === alias); + + expect(alternate?.getArgumentCompletions?.(level)).toEqual( + canonical?.getArgumentCompletions?.(level), + ); + expect(shouldSubmitExactArgumentCompletion(`/${alias} ${level}`, commands)).toBe(true); + } + }); + it.each([{}, { local: true }])("exposes usage cost in completion and help", (options) => { const commands = getSlashCommands(options); const usage = commands.find((command) => command.name === "usage"); @@ -92,9 +115,12 @@ describe("getSlashCommands", () => { it("keeps session status on the shared command path and exposes gateway status separately", () => { const commands = getSlashCommands(); const status = commands.find((command) => command.name === "status"); + const identityAlias = commands.find((command) => command.name === "id"); const gatewayStatus = commands.find((command) => command.name === "gateway-status"); const openclaw = commands.find((command) => command.name === "openclaw"); expect(status?.description).toBe("Show current status."); + expect(identityAlias?.description).toBe("Show your sender id."); + expect(identityAlias?.getArgumentCompletions?.("")).toBeUndefined(); expect(gatewayStatus?.description).toBe("Show gateway status summary"); expect(openclaw?.description).toBe("Return to OpenClaw"); }); @@ -172,7 +198,7 @@ describe("getSlashCommands", () => { dynamicCommands: [ { name: "dreaming", - textAliases: ["/dreaming"], + textAliases: ["/dreaming", "/dream"], description: "Enable or disable memory dreaming.", source: "plugin", scope: "both", @@ -184,6 +210,9 @@ describe("getSlashCommands", () => { expect(commands.find((command) => command.name === "dreaming")?.description).toBe( "Enable or disable memory dreaming.", ); + expect( + commands.find((command) => command.name === "dream")?.getArgumentCompletions?.(""), + ).toBeUndefined(); }); it("only advertises shared commands that local mode can route", () => { diff --git a/src/tui/commands.ts b/src/tui/commands.ts index c22a0bdd7681..183881840397 100644 --- a/src/tui/commands.ts +++ b/src/tui/commands.ts @@ -203,7 +203,7 @@ function normalizeSlashCommandName(value: string): string { function appendSlashCommand( commands: SlashCommand[], - seen: Set, + seen: Map, name: string, description: string, getArgumentCompletions?: SlashCommand["getArgumentCompletions"], @@ -212,7 +212,7 @@ function appendSlashCommand( if (!normalizedName || seen.has(normalizedName)) { return; } - seen.add(normalizedName); + seen.set(normalizedName, getArgumentCompletions); commands.push({ name: normalizedName, description, getArgumentCompletions }); } @@ -247,7 +247,7 @@ export function getSlashCommands(options: SlashCommandOptions = {}): SlashComman ? options.thinkingLevels.map((level) => level.label) : listThinkingLevelLabels(options.provider, options.model, undefined, options.agentRuntime); const commands: SlashCommand[] = []; - const seen = new Set(); + const seen = new Map(); for (const command of TUI_COMMAND_DESCRIPTORS) { if ( command.shared || @@ -287,7 +287,7 @@ export function getSlashCommands(options: SlashCommandOptions = {}): SlashComman } const aliases = command.textAliases.length > 0 ? command.textAliases : [`/${command.key}`]; for (const alias of aliases) { - appendSlashCommand(commands, seen, alias, command.description); + appendSlashCommand(commands, seen, alias, command.description, seen.get(command.key)); } }