Add command palette with 19 commands and improved keyboard navigation

Implement VSCode-style command palette (Cmd+Shift+P) with comprehensive command organization:
- 19 commands organized into 5 categories: Instance, Session, Agent & Model, Input & Focus, System
- Category-based grouping with proper visual hierarchy
- Keyboard shortcuts displayed for all applicable commands
- Search/filter by command name, description, keywords, or category

Add keyboard shortcuts:
- Cmd+Shift+A: Open agent selector
- Cmd+Shift+M: Open model selector (existing)
- Instance/Session navigation shortcuts (Cmd+[/], Cmd+Shift+[/])

Fix keyboard navigation:
- Model selector now highlights options with arrow keys using data-[highlighted] attribute
- Agent selector properly opens via keyboard shortcut
- Global keyboard handler skips Combobox/Select components to allow native navigation

Improve discoverability:
- Prominent centered Command Palette hint in connection status bar
- Keyboard shortcut hints next to agent and model selectors

Complete task 020-command-palette
This commit is contained in:
Shantur Rathore
2025-10-23 22:32:49 +01:00
parent b06b8104a5
commit 54569b166d
9 changed files with 566 additions and 322 deletions

View File

@@ -36,10 +36,39 @@ const CommandPalette: Component<CommandPaletteProps> = (props) => {
const labelMatch = cmd.label.toLowerCase().includes(q)
const descMatch = cmd.description.toLowerCase().includes(q)
const keywordMatch = cmd.keywords?.some((k) => k.toLowerCase().includes(q))
return labelMatch || descMatch || keywordMatch
const categoryMatch = cmd.category?.toLowerCase().includes(q)
return labelMatch || descMatch || keywordMatch || categoryMatch
})
}
const groupedCommands = () => {
const filtered = filteredCommands()
const groups = new Map<string, Command[]>()
for (const cmd of filtered) {
const category = cmd.category || "Other"
if (!groups.has(category)) {
groups.set(category, [])
}
groups.get(category)!.push(cmd)
}
const categoryOrder = ["Instance", "Session", "Agent & Model", "Input & Focus", "System", "Other"]
const sorted = new Map<string, Command[]>()
for (const cat of categoryOrder) {
if (groups.has(cat)) {
sorted.set(cat, groups.get(cat)!)
}
}
for (const [cat, cmds] of groups) {
if (!sorted.has(cat)) {
sorted.set(cat, cmds)
}
}
return sorted
}
createEffect(() => {
if (props.open) {
setQuery("")
@@ -123,27 +152,49 @@ const CommandPalette: Component<CommandPaletteProps> = (props) => {
when={filteredCommands().length > 0}
fallback={<div class="p-8 text-center text-gray-500">No commands found for "{query()}"</div>}
>
<For each={filteredCommands()}>
{(command, index) => (
<button
type="button"
onClick={() => handleCommandClick(command.id)}
class={`w-full px-4 py-3 flex items-start gap-3 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors cursor-pointer border-none text-left ${
index() === selectedIndex() ? "bg-blue-50 dark:bg-blue-900/20" : ""
}`}
onMouseEnter={() => setSelectedIndex(index())}
>
<div class="flex-1 min-w-0">
<div class="font-medium text-gray-900 dark:text-gray-100">{command.label}</div>
<div class="text-sm text-gray-600 dark:text-gray-400 mt-0.5">{command.description}</div>
</div>
<Show when={command.shortcut}>
<div class="mt-1">
<Kbd shortcut={buildShortcutString(command.shortcut)} />
<For each={Array.from(groupedCommands().entries())}>
{([category, commands]) => {
let globalIndex = 0
for (const [cat, cmds] of groupedCommands().entries()) {
if (cat === category) break
globalIndex += cmds.length
}
return (
<div class="py-2">
<div class="px-4 py-2 text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide">
{category}
</div>
</Show>
</button>
)}
<For each={commands}>
{(command, localIndex) => {
const commandIndex = globalIndex + localIndex()
return (
<button
type="button"
onClick={() => handleCommandClick(command.id)}
class={`w-full px-4 py-3 flex items-start gap-3 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors cursor-pointer border-none text-left ${
commandIndex === selectedIndex() ? "bg-blue-50 dark:bg-blue-900/20" : ""
}`}
onMouseEnter={() => setSelectedIndex(commandIndex)}
>
<div class="flex-1 min-w-0">
<div class="font-medium text-gray-900 dark:text-gray-100">{command.label}</div>
<div class="text-sm text-gray-600 dark:text-gray-400 mt-0.5">
{command.description}
</div>
</div>
<Show when={command.shortcut}>
<div class="mt-1">
<Kbd shortcut={buildShortcutString(command.shortcut)} />
</div>
</Show>
</button>
)
}}
</For>
</div>
)
}}
</For>
</Show>
</div>