diff --git a/apps/desktop/src/app/chat/composer/hooks/use-slash-completions.ts b/apps/desktop/src/app/chat/composer/hooks/use-slash-completions.ts index a41e7f561c..0f364c5ccd 100644 --- a/apps/desktop/src/app/chat/composer/hooks/use-slash-completions.ts +++ b/apps/desktop/src/app/chat/composer/hooks/use-slash-completions.ts @@ -12,7 +12,7 @@ import { type DesktopThemeCommandOption, filterDesktopCommandsCatalog, isDesktopSlashExtensionCommand, - isDesktopSlashSuggestion, + isDesktopSlashSuggestionWithOptions, rankSkillCommands, slashCompletionGroup } from '@/lib/desktop-slash-commands' @@ -221,6 +221,12 @@ export function useSlashCompletions(options: { const isArgCompletion = replaceFrom > 1 const prefix = isArgCompletion ? text.slice(0, replaceFrom) : '' + // An alias the user typed to completion (`/reset`) must surface even + // though aliases are hidden while browsing — otherwise the popover + // says "no matches" for a command Enter happily executes (#57641). + // Only an EXACT match unlocks it; a partial prefix keeps hiding. + const exactAliasQuery = isArgCompletion ? undefined : commandText(query).toLowerCase() + const decorated = (result.items ?? []) .map(item => { if (!isArgCompletion) { @@ -231,7 +237,10 @@ export function useSlashCompletions(options: { return { ...item, text: `${prefix}${argText}` } }) - .filter(item => isArgCompletion || isDesktopSlashSuggestion(item.text)) + .filter( + item => + isArgCompletion || isDesktopSlashSuggestionWithOptions(item.text, { exactAlias: exactAliasQuery }) + ) .map(item => ({ ...item, // Arg suggestions (e.g. `/handoff `) live under one diff --git a/apps/desktop/src/lib/desktop-slash-commands.test.ts b/apps/desktop/src/lib/desktop-slash-commands.test.ts index d8620a985b..c49dedc118 100644 --- a/apps/desktop/src/lib/desktop-slash-commands.test.ts +++ b/apps/desktop/src/lib/desktop-slash-commands.test.ts @@ -11,6 +11,7 @@ import { isDesktopSlashCommand, isDesktopSlashExtensionCommand, isDesktopSlashSuggestion, + isDesktopSlashSuggestionWithOptions, isModelPickerCommand, isPickerCommand, rankSkillCommands, @@ -149,6 +150,22 @@ describe('desktop slash command curation', () => { expect(isDesktopSlashCommand('/reset')).toBe(true) }) + it('surfaces an alias the user typed exactly, gated on desktop availability (#57641)', () => { + // Browsing (no exact query): aliases stay hidden — popover stays lean. + expect(isDesktopSlashSuggestionWithOptions('/reset')).toBe(false) + // Exact typed query: the alias must appear, not "no matches". + expect(isDesktopSlashSuggestionWithOptions('/reset', { exactAlias: '/reset' })).toBe(true) + expect(isDesktopSlashSuggestionWithOptions('/reset', { exactAlias: 'reset' })).toBe(true) + // Partial prefixes and other queries keep aliases hidden. + expect(isDesktopSlashSuggestionWithOptions('/reset', { exactAlias: 're' })).toBe(false) + expect(isDesktopSlashSuggestionWithOptions('/reset', { exactAlias: '/new' })).toBe(false) + // A different alias never rides along with this query. + expect(isDesktopSlashSuggestionWithOptions('/fork', { exactAlias: '/reset' })).toBe(false) + // Aliases whose canonical has no desktop surface stay hidden even on an + // exact match (isDesktopSlashCommand gate). + expect(isDesktopSlashSuggestionWithOptions('/reload_mcp', { exactAlias: '/reload_mcp' })).toBe(false) + }) + it('filters built-in catalog noise but keeps skill / quick-command extensions', () => { const filtered = filterDesktopCommandsCatalog({ categories: [ diff --git a/apps/desktop/src/lib/desktop-slash-commands.ts b/apps/desktop/src/lib/desktop-slash-commands.ts index f952882106..f8308969a4 100644 --- a/apps/desktop/src/lib/desktop-slash-commands.ts +++ b/apps/desktop/src/lib/desktop-slash-commands.ts @@ -556,10 +556,28 @@ export function isDesktopSlashCommand(command: string): boolean { /** Gates discovery in the popover/completions. */ export function isDesktopSlashSuggestion(command: string): boolean { + return isDesktopSlashSuggestionWithOptions(command, {}) +} + +/** + * Same gate, with the one escape hatch the composer needs: an alias the user + * typed EXACTLY (`/reset`, not a browsing prefix) must surface, or the empty + * "no matches" popover reads as "this command doesn't exist" while Enter still + * executes it (#57641). Gated on `isDesktopSlashCommand` so aliases whose + * canonical has no desktop surface (e.g. `/reload_mcp`) stay hidden. + */ +export function isDesktopSlashSuggestionWithOptions( + command: string, + options: { exactAlias?: string } = {} +): boolean { const normalized = normalizeCommand(command) // Aliases stay hidden so the popover isn't cluttered with duplicates. if (isAliasCommand(normalized)) { + if (options.exactAlias != null) { + return normalizeCommand(options.exactAlias) === normalized && isDesktopSlashCommand(normalized) + } + return false }