import { Component, createSignal, For, Show, onMount, createEffect } from "solid-js" import { Dialog } from "@kobalte/core/dialog" import type { Command } from "../lib/commands" import Kbd from "./kbd" interface CommandPaletteProps { open: boolean onClose: () => void commands: Command[] onExecute: (commandId: string) => void } function buildShortcutString(shortcut: Command["shortcut"]): string { if (!shortcut) return "" const parts: string[] = [] if (shortcut.meta || shortcut.ctrl) parts.push("cmd") if (shortcut.shift) parts.push("shift") if (shortcut.alt) parts.push("alt") parts.push(shortcut.key) return parts.join("+") } const CommandPalette: Component = (props) => { const [query, setQuery] = createSignal("") const [selectedIndex, setSelectedIndex] = createSignal(0) let inputRef: HTMLInputElement | undefined let listRef: HTMLDivElement | undefined const filteredCommands = () => { const q = query().toLowerCase() if (!q) return props.commands return props.commands.filter((cmd) => { const labelMatch = cmd.label.toLowerCase().includes(q) const descMatch = cmd.description.toLowerCase().includes(q) const keywordMatch = cmd.keywords?.some((k) => k.toLowerCase().includes(q)) const categoryMatch = cmd.category?.toLowerCase().includes(q) return labelMatch || descMatch || keywordMatch || categoryMatch }) } const groupedCommands = () => { const filtered = filteredCommands() const groups = new Map() 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() 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("") setSelectedIndex(0) setTimeout(() => inputRef?.focus(), 100) } }) createEffect(() => { const max = Math.max(0, filteredCommands().length - 1) if (selectedIndex() > max) { setSelectedIndex(max) } }) createEffect(() => { const index = selectedIndex() if (!listRef) return const selectedButton = listRef.querySelector(`[data-command-index="${index}"]`) as HTMLElement if (selectedButton) { selectedButton.scrollIntoView({ block: "nearest", behavior: "smooth" }) } }) function handleKeyDown(e: KeyboardEvent) { const filtered = filteredCommands() if (e.key === "ArrowDown") { e.preventDefault() setSelectedIndex((i) => Math.min(i + 1, filtered.length - 1)) } else if (e.key === "ArrowUp") { e.preventDefault() setSelectedIndex((i) => Math.max(i - 1, 0)) } else if (e.key === "Enter") { e.preventDefault() const selected = filtered[selectedIndex()] if (selected) { props.onExecute(selected.id) props.onClose() } } else if (e.key === "Escape") { e.preventDefault() props.onClose() } } function handleCommandClick(commandId: string) { props.onExecute(commandId) props.onClose() } return ( !open && props.onClose()}>
Command Palette Search and execute commands
{ setQuery(e.currentTarget.value) setSelectedIndex(0) }} placeholder="Type a command or search..." class="flex-1 bg-transparent outline-none text-gray-900 dark:text-gray-100 placeholder-gray-400" />
0} fallback={
No commands found for "{query()}"
} > {([category, commands]) => { let globalIndex = 0 for (const [cat, cmds] of groupedCommands().entries()) { if (cat === category) break globalIndex += cmds.length } return (
{category}
{(command, localIndex) => { const commandIndex = globalIndex + localIndex() return ( ) }}
) }}
) } export default CommandPalette