diff --git a/packages/cli/src/ui/hooks/useSlashCompletion.integration.test.ts b/packages/cli/src/ui/hooks/useSlashCompletion.integration.test.ts index f7431a1180..5ab790361c 100644 --- a/packages/cli/src/ui/hooks/useSlashCompletion.integration.test.ts +++ b/packages/cli/src/ui/hooks/useSlashCompletion.integration.test.ts @@ -121,4 +121,52 @@ describe('useSlashCompletion integration', () => { expect(result.current.suggestions[0]?.value).toBe('model'); expect(result.current.suggestions[1]?.value).toBe('memory'); }); + + it('prefers command name match over alias match for same-strength prefixes', async () => { + // Real-world conflict: /re matches `resume` (name) and `reset` (alias of clear). + // Without the name-vs-alias sort dimension, fzf gives the shorter `reset` a higher + // score, so `clear` would appear first. The fix ensures `resume` (name match) + // always ranks above any alias match at the same strength level. + const slashCommands = [ + createTestCommand({ + name: 'clear', + altNames: ['reset', 'new'], + description: 'Clear conversation history', + }), + createTestCommand({ + name: 'resume', + altNames: ['continue'], + description: 'Resume a previous session', + }), + createTestCommand({ + name: 'recap', + description: 'Show session recap', + }), + ]; + + const { result } = renderHook(() => + useTestHarnessForSlashCompletion( + true, + '/re', + slashCommands, + mockCommandContext, + ), + ); + + await waitFor(() => { + expect(result.current.suggestions.length).toBeGreaterThan(1); + }); + + // resume and recap are name matches; reset is an alias match. + // Name matches must come before alias matches regardless of fzf score. + const names = result.current.suggestions.map((s) => s.value); + const resumeIndex = names.indexOf('resume'); + const recapIndex = names.indexOf('recap'); + const clearIndex = names.indexOf('clear'); + expect(resumeIndex).toBeGreaterThanOrEqual(0); + expect(recapIndex).toBeGreaterThanOrEqual(0); + expect(clearIndex).toBeGreaterThanOrEqual(0); + expect(resumeIndex).toBeLessThan(clearIndex); + expect(recapIndex).toBeLessThan(clearIndex); + }); }); diff --git a/packages/cli/src/ui/hooks/useSlashCompletion.ts b/packages/cli/src/ui/hooks/useSlashCompletion.ts index 8f73ba545d..b8cbf7a012 100644 --- a/packages/cli/src/ui/hooks/useSlashCompletion.ts +++ b/packages/cli/src/ui/hooks/useSlashCompletion.ts @@ -237,10 +237,18 @@ function compareRankedCommandMatches( left: RankedCommandMatch, right: RankedCommandMatch, ): number { + // Name match beats alias match — e.g. /re should prefer `resume` (name) + // over `clear` via its `reset` alias, since users type the primary name + // more often than obscure alternates. + const leftIsName = left.matchedAlias === undefined ? 1 : 0; + const rightIsName = right.matchedAlias === undefined ? 1 : 0; + const nameVsAlias = rightIsName - leftIsName; + return ( right.matchStrength - left.matchStrength || right.completionPriority - left.completionPriority || right.recentScore - left.recentScore || + nameVsAlias || right.score - left.score || left.start - right.start || left.itemLength - right.itemLength ||