diff --git a/packages/cli/src/ui/hooks/useSlashCompletion.integration.test.ts b/packages/cli/src/ui/hooks/useSlashCompletion.integration.test.ts index 5ab790361c..62672e2870 100644 --- a/packages/cli/src/ui/hooks/useSlashCompletion.integration.test.ts +++ b/packages/cli/src/ui/hooks/useSlashCompletion.integration.test.ts @@ -31,6 +31,10 @@ function useTestHarnessForSlashCompletion( query: string | null, slashCommands: readonly SlashCommand[], commandContext: CommandContext, + recentCommands?: ReadonlyMap< + string, + { name: string; usedAt: number; count: number } + >, ) { const [suggestions, setSuggestions] = useState([]); const [isLoadingSuggestions, setIsLoadingSuggestions] = useState(false); @@ -41,6 +45,7 @@ function useTestHarnessForSlashCompletion( query, slashCommands, commandContext, + recentCommands, setSuggestions, setIsLoadingSuggestions, setIsPerfectMatch, @@ -169,4 +174,53 @@ describe('useSlashCompletion integration', () => { expect(resumeIndex).toBeLessThan(clearIndex); expect(recapIndex).toBeLessThan(clearIndex); }); + + it('prefers command name match over alias match even when alias has recentScore', async () => { + // Regression test: recentScore should not shadow nameVsAlias dimension. + // Real-world conflict: /re matches `resume` (name) and `reset` (alias of clear). + // Even if `clear` was used recently (giving it a recentScore > 0), `resume` + // (name match) should still rank first because name-vs-alias is checked before + // recentScore in the sort chain. + const now = Date.now(); + const slashCommands = [ + createTestCommand({ + name: 'clear', + altNames: ['reset', 'new'], + description: 'Clear conversation history', + }), + createTestCommand({ + name: 'resume', + altNames: ['continue'], + description: 'Resume a previous session', + }), + ]; + + // Give `clear` a recentScore to simulate it was used recently + const recentCommands = new Map([ + ['clear', { name: 'clear', usedAt: now, count: 1 }], + ]); + + const { result } = renderHook(() => + useTestHarnessForSlashCompletion( + true, + '/re', + slashCommands, + mockCommandContext, + recentCommands, + ), + ); + + await waitFor(() => { + expect(result.current.suggestions.length).toBeGreaterThan(1); + }); + + // resume is a name match; reset is an alias match. + // Even with recentScore boosting `clear`, name match must win. + const names = result.current.suggestions.map((s) => s.value); + const resumeIndex = names.indexOf('resume'); + const clearIndex = names.indexOf('clear'); + expect(resumeIndex).toBeGreaterThanOrEqual(0); + expect(clearIndex).toBeGreaterThanOrEqual(0); + expect(resumeIndex).toBeLessThan(clearIndex); + }); }); diff --git a/packages/cli/src/ui/hooks/useSlashCompletion.ts b/packages/cli/src/ui/hooks/useSlashCompletion.ts index b8cbf7a012..2b94d18748 100644 --- a/packages/cli/src/ui/hooks/useSlashCompletion.ts +++ b/packages/cli/src/ui/hooks/useSlashCompletion.ts @@ -247,8 +247,8 @@ function compareRankedCommandMatches( return ( right.matchStrength - left.matchStrength || right.completionPriority - left.completionPriority || - right.recentScore - left.recentScore || nameVsAlias || + right.recentScore - left.recentScore || right.score - left.score || left.start - right.start || left.itemLength - right.itemLength ||