Skip to content

Command Palette (Phase 4)

Synced read-only from /home/synnet/mirrors/synvirt-product/docs/modules/web-ux-v2/COMMAND_PALETTE.md. Edit at the source, not here.

The v2 console’s command palette is the keyboard-first surface. It opens with Cmd+K (macOS) / Ctrl+K (everywhere else) and lets the operator navigate, create, switch theme, or sign out without touching the mouse.

┌────────────────────────────────────────────────┐
│ 🔍 Type a command — navigate, create, switch …│
├────────────────────────────────────────────────┤
│ CREATE │
│ [+] Create virtual machine… Open the wizard│
│ [↑] Upload installation media Add an ISO │
│ NAVIGATE │
│ [▤] Go to Dashboard │
│ [▤] Go to Virtual machines │
│ ... │
├────────────────────────────────────────────────┤
│ ↑ ↓ Navigate · ⏎ Run · Esc Close · 12 commands│
└────────────────────────────────────────────────┘

Built on Headless UI’s Combobox so keyboard navigation, ARIA roles, and focus management work out of the box.

Piece Responsibility
CommandPalette.vue Modal + Combobox UI
useCommandPalette() Module-level singleton state — registry, query, filter
useGlobalShortcuts() Window keyboard listener (Cmd/Ctrl+K, Cmd/Ctrl+B)
useUiStore.paletteOpen Open/close flag everyone reads

The palette is mounted by AppShell so any view can open it via useUiStore().openPalette().

Score formula:

  1. Exact substring anywhere in label, hint, group, or keywords. Bonus 20 points if the hit starts at offset 0.
  2. Subsequence — every character of the query appears in order somewhere in the searchable text. Score = matched / target length.
  3. Otherwise drop the row.

Top results land first; tie-breaks fall back to registration order.

import { useCommandPalette } from '@/composables/useCommandPalette';
import { Plus } from 'lucide-vue-next';
const palette = useCommandPalette();
palette.register({
id: 'create-snapshot',
label: 'Create snapshot…',
hint: 'Open the snapshot dialog',
icon: Plus,
group: 'create',
keywords: ['snapshot', 'backup', 'restore'],
keys: ['', 'S'],
run: () => doSomething(),
});

registerWithCleanup is the safer variant — it auto-unregisters on component teardown so a route change won’t leak phantom entries.

AppShell.vue::registerBuiltins() registers the always-available set:

Group Command Action
create Create virtual machine… Navigate /vms/new
create Upload installation media Navigate /iso-library?upload=1
navigate Go to Dashboard Navigate /
navigate Go to Virtual machines Navigate /vms
navigate Go to ISO library Navigate /iso-library
theme Switch to light theme useTheme.setMode('light')
theme Switch to dark theme useTheme.setMode('dark')
theme Follow system theme useTheme.setMode('system')
session Toggle sidebar (⌘/Ctrl B) useUiStore.toggleSidebar()
session Sign out (Phase 5 stub)

The global listener in useGlobalShortcuts.ts watches:

  • Cmd/Ctrl + K — toggle the palette.
  • Cmd/Ctrl + B — toggle the sidebar.

Anything else falls through to whatever is focused. Inside the palette, Combobox handles ///Esc directly.