Move entity results above actions in command palette and tighten fuzzy matching

- Reorder result hierarchy: Pages → Entities → Actions → Settings
  (doc files and database records now appear right after page matches)
- Disable fuzzy matching for terms under 5 characters to prevent
  false positives like "test" matching "text" (all SMS pages)
- Prefix matching still works for short terms (e.g. "mail" → MailHog)

Bunker Admin
This commit is contained in:
bunker-admin 2026-02-28 09:22:29 -07:00
parent 46fc92fab8
commit 41d86782b4
2 changed files with 32 additions and 9 deletions

View File

@ -194,7 +194,7 @@ export default function CommandPalette() {
}, [query, recentItems, allItems, favoriteCommandItems]); }, [query, recentItems, allItems, favoriteCommandItems]);
// Flatten all results into a single list for keyboard navigation. // Flatten all results into a single list for keyboard navigation.
// Order: Favorites → Recent → Pages → Actions → Entities → Settings // Order: Favorites → Recent → Pages → Entities → Actions → Settings
const flatList = useMemo((): FlatItem[] => { const flatList = useMemo((): FlatItem[] => {
const items: FlatItem[] = []; const items: FlatItem[] = [];
if (!query) { if (!query) {
@ -207,14 +207,15 @@ export default function CommandPalette() {
} else if (parsed.showScopeList) { } else if (parsed.showScopeList) {
// No items when showing scope selector // No items when showing scope selector
} else { } else {
// Pages (navigation) first, then actions
const pages = commandResults.filter((i) => i.category === 'navigation'); const pages = commandResults.filter((i) => i.category === 'navigation');
const actions = commandResults.filter((i) => i.category === 'action'); const actions = commandResults.filter((i) => i.category === 'action');
const settings = commandResults.filter((i) => i.category === 'settings'); const settings = commandResults.filter((i) => i.category === 'settings');
// Pages first (exact name matches ranked highest by MiniSearch)
for (const item of pages) items.push({ type: 'command', item }); for (const item of pages) items.push({ type: 'command', item });
for (const item of actions) items.push({ type: 'command', item }); // Entities next (database records like doc files, campaigns, users)
// Entities in the middle
for (const item of entityResults) items.push({ type: 'entity', item }); for (const item of entityResults) items.push({ type: 'entity', item });
// Actions after entities
for (const item of actions) items.push({ type: 'command', item });
// Settings last // Settings last
for (const item of settings) items.push({ type: 'command', item }); for (const item of settings) items.push({ type: 'command', item });
} }
@ -268,6 +269,7 @@ export default function CommandPalette() {
// Group commands by category for display, in priority order: // Group commands by category for display, in priority order:
// Pages (navigation) → Actions → Settings (least frequent) // Pages (navigation) → Actions → Settings (least frequent)
// Entities are interleaved between pages and actions in the render phase.
// NOTE: These hooks must stay above the early return to satisfy Rules of Hooks // NOTE: These hooks must stay above the early return to satisfy Rules of Hooks
const CATEGORY_ORDER: (CommandCategory | 'favorites' | 'recent')[] = [ const CATEGORY_ORDER: (CommandCategory | 'favorites' | 'recent')[] = [
'favorites', 'recent', 'navigation', 'action', 'settings', 'favorites', 'recent', 'navigation', 'action', 'settings',
@ -451,15 +453,15 @@ export default function CommandPalette() {
</div> </div>
)} )}
{/* Render in priority order: Pages Actions Entities Settings {/* Render in priority order: Pages Entities Actions Settings
For no-query mode: Favorites Recent (no entities) */} For no-query mode: Favorites Recent (no entities) */}
{(() => { {(() => {
const sections: React.ReactNode[] = []; const sections: React.ReactNode[] = [];
const commandEntries = Array.from(groupedCommands.entries()); const commandEntries = Array.from(groupedCommands.entries());
// Render command groups that come before entities (everything except settings) // Pages (navigation) first — skip actions and settings for now
for (const [groupKey, items] of commandEntries) { for (const [groupKey, items] of commandEntries) {
if (groupKey === 'settings') continue; // settings rendered after entities if (groupKey === 'settings' || groupKey === 'action') continue;
sections.push( sections.push(
<CommandGroupSection <CommandGroupSection
key={groupKey} key={groupKey}
@ -476,7 +478,7 @@ export default function CommandPalette() {
flatIndex += items.length; flatIndex += items.length;
} }
// Entity groups (only when searching) // Entities right after pages (doc files, campaigns, users, etc.)
if (query && !parsed.showScopeList) { if (query && !parsed.showScopeList) {
for (const [entityType, items] of groupedEntities.entries()) { for (const [entityType, items] of groupedEntities.entries()) {
sections.push( sections.push(
@ -495,6 +497,25 @@ export default function CommandPalette() {
} }
} }
// Actions after entities
const actionItems = groupedCommands.get('action');
if (actionItems) {
sections.push(
<CommandGroupSection
key="action"
groupKey="action"
items={actionItems}
flatIndexRef={{ current: flatIndex }}
selectedIndex={selectedIndex}
favoritePaths={favoritePaths}
token={token}
onSelect={handleSelect}
onHover={setSelectedIndex}
/>,
);
flatIndex += actionItems.length;
}
// Settings last // Settings last
const settingsItems = groupedCommands.get('settings'); const settingsItems = groupedCommands.get('settings');
if (settingsItems) { if (settingsItems) {

View File

@ -87,7 +87,9 @@ export function useCommandIndex() {
searchOptions: { searchOptions: {
boost: { title: 10, keywordsJoined: 3, group: 1 }, boost: { title: 10, keywordsJoined: 3, group: 1 },
prefix: true, prefix: true,
fuzzy: 0.2, // Only allow fuzzy matching for terms 5+ chars to prevent
// false positives like "test" → "text"
fuzzy: (term) => (term.length >= 5 ? 0.2 : false),
}, },
}); });