diff --git a/src/components/CommandPalette.test.tsx b/src/components/CommandPalette.test.tsx
index 6469cc0a..58a61283 100644
--- a/src/components/CommandPalette.test.tsx
+++ b/src/components/CommandPalette.test.tsx
@@ -165,4 +165,64 @@ describe('CommandPalette', () => {
expect(screen.getByText('↵ select')).toBeInTheDocument()
expect(screen.getByText('esc close')).toBeInTheDocument()
})
+
+ describe('relevance ranking', () => {
+ const relevanceCommands: CommandAction[] = [
+ makeCommand({ id: 'create-note', label: 'Create New Note', group: 'Note' }),
+ makeCommand({ id: 'toggle-raw', label: 'Toggle Raw Editor', group: 'View' }),
+ makeCommand({ id: 'switch-theme', label: 'Switch Theme', group: 'Appearance', keywords: ['dark', 'light'] }),
+ makeCommand({ id: 'search-notes', label: 'Search Notes', group: 'Navigation' }),
+ ]
+
+ function getVisibleLabels() {
+ return screen.getAllByText(
+ (_content, el) =>
+ el?.tagName === 'SPAN' &&
+ el.classList.contains('text-foreground') &&
+ !!el.textContent,
+ ).map(el => el.textContent)
+ }
+
+ it('ranks "Toggle Raw Editor" before "Create New Note" for query "raw"', () => {
+ render()
+ fireEvent.change(screen.getByPlaceholderText('Type a command...'), { target: { value: 'raw' } })
+
+ const labels = getVisibleLabels()
+ const rawIdx = labels.indexOf('Toggle Raw Editor')
+ const createIdx = labels.indexOf('Create New Note')
+ expect(rawIdx).toBeGreaterThanOrEqual(0)
+ expect(createIdx).toBeGreaterThanOrEqual(0)
+ expect(rawIdx).toBeLessThan(createIdx)
+ })
+
+ it('ranks "Create New Note" first for query "new note"', () => {
+ render()
+ fireEvent.change(screen.getByPlaceholderText('Type a command...'), { target: { value: 'new note' } })
+
+ const labels = getVisibleLabels()
+ expect(labels[0]).toBe('Create New Note')
+ })
+
+ it('ranks theme commands first for query "theme"', () => {
+ render()
+ fireEvent.change(screen.getByPlaceholderText('Type a command...'), { target: { value: 'theme' } })
+
+ const labels = getVisibleLabels()
+ expect(labels[0]).toBe('Switch Theme')
+ })
+
+ it('preserves default section order with empty query', () => {
+ render()
+
+ const groupHeaders = screen.getAllByText(
+ (_content, el) =>
+ el?.tagName === 'DIV' &&
+ el.classList.contains('uppercase') &&
+ !!el.textContent,
+ ).map(el => el.textContent)
+
+ // Default order: Navigation < Note < View < Appearance
+ expect(groupHeaders).toEqual(['Navigation', 'Note', 'View', 'Appearance'])
+ })
+ })
})
diff --git a/src/components/CommandPalette.tsx b/src/components/CommandPalette.tsx
index ba360460..0197d2e5 100644
--- a/src/components/CommandPalette.tsx
+++ b/src/components/CommandPalette.tsx
@@ -30,16 +30,21 @@ function matchCommand(query: string, cmd: CommandAction): ScoredCommand | null {
return null
}
-function groupResults(commands: CommandAction[]): { group: CommandGroup; items: CommandAction[] }[] {
+function groupResults(
+ commands: CommandAction[],
+ byRelevance: boolean,
+): { group: CommandGroup; items: CommandAction[] }[] {
const map = new Map()
for (const cmd of commands) {
const list = map.get(cmd.group)
if (list) list.push(cmd)
else map.set(cmd.group, [cmd])
}
- return Array.from(map.entries())
- .sort((a, b) => groupSortKey(a[0]) - groupSortKey(b[0]))
- .map(([group, items]) => ({ group, items }))
+ const entries = Array.from(map.entries())
+ if (!byRelevance) {
+ entries.sort((a, b) => groupSortKey(a[0]) - groupSortKey(b[0]))
+ }
+ return entries.map(([group, items]) => ({ group, items }))
}
export function CommandPalette({ open, commands, onClose }: CommandPaletteProps) {
@@ -70,7 +75,8 @@ export function CommandPalette({ open, commands, onClose }: CommandPaletteProps)
.map(r => r.command)
}, [enabledCommands, query])
- const groups = useMemo(() => groupResults(filtered), [filtered])
+ const hasQuery = !!query.trim()
+ const groups = useMemo(() => groupResults(filtered, hasQuery), [filtered, hasQuery])
const flatList = useMemo(() => groups.flatMap(g => g.items), [groups])
useEffect(() => {