import { Component, createSignal, For, Show, createEffect, createMemo } 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: (command: Command) => 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 [selectedCommandId, setSelectedCommandId] = createSignal(null) const [isPointerSelecting, setIsPointerSelecting] = createSignal(false) let inputRef: HTMLInputElement | undefined let listRef: HTMLDivElement | undefined const categoryOrder = ["Custom Commands", "Instance", "Session", "Agent & Model", "Input & Focus", "System", "Other"] as const type CommandGroup = { category: string; commands: Command[]; startIndex: number } type ProcessedCommands = { groups: CommandGroup[]; ordered: Command[] } const processedCommands = createMemo(() => { const source = props.commands ?? [] const q = query().trim().toLowerCase() const filtered = q ? source.filter((cmd) => { const label = typeof cmd.label === "function" ? cmd.label() : cmd.label const labelMatch = 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 }) : source const groupsMap = new Map() for (const cmd of filtered) { const category = cmd.category || "Other" const list = groupsMap.get(category) if (list) { list.push(cmd) } else { groupsMap.set(category, [cmd]) } } const groups: CommandGroup[] = [] const ordered: Command[] = [] const processedCategories = new Set() const addGroup = (category: string) => { const cmds = groupsMap.get(category) if (!cmds || cmds.length === 0 || processedCategories.has(category)) return groups.push({ category, commands: cmds, startIndex: ordered.length }) ordered.push(...cmds) processedCategories.add(category) } for (const category of categoryOrder) { addGroup(category) } for (const [category] of groupsMap) { addGroup(category) } return { groups, ordered } }) const groupedCommandList = () => processedCommands().groups const orderedCommands = () => processedCommands().ordered const selectedIndex = createMemo(() => { const ordered = orderedCommands() if (ordered.length === 0) return -1 const id = selectedCommandId() if (!id) return 0 const index = ordered.findIndex((cmd) => cmd.id === id) return index >= 0 ? index : 0 }) createEffect(() => { if (props.open) { setQuery("") setSelectedCommandId(null) setIsPointerSelecting(false) setTimeout(() => inputRef?.focus(), 100) } }) createEffect(() => { const ordered = orderedCommands() if (ordered.length === 0) { if (selectedCommandId() !== null) { setSelectedCommandId(null) } return } const currentId = selectedCommandId() if (!currentId || !ordered.some((cmd) => cmd.id === currentId)) { setSelectedCommandId(ordered[0].id) } }) createEffect(() => { const index = selectedIndex() if (!listRef || index < 0) return const selectedButton = listRef.querySelector(`[data-command-index="${index}"]`) as HTMLElement if (selectedButton) { selectedButton.scrollIntoView({ block: "nearest", behavior: "smooth" }) } }) function handleKeyDown(e: KeyboardEvent) { const ordered = orderedCommands() if (e.key === "Escape") { e.preventDefault() e.stopPropagation() props.onClose() return } if (ordered.length === 0) { if (e.key === "ArrowDown" || e.key === "ArrowUp" || e.key === "Enter") { e.preventDefault() e.stopPropagation() } return } if (e.key === "ArrowDown") { e.preventDefault() e.stopPropagation() setIsPointerSelecting(false) const current = selectedIndex() const nextIndex = Math.min((current < 0 ? 0 : current) + 1, ordered.length - 1) setSelectedCommandId(ordered[nextIndex]?.id ?? null) } else if (e.key === "ArrowUp") { e.preventDefault() e.stopPropagation() setIsPointerSelecting(false) const current = selectedIndex() const nextIndex = current <= 0 ? ordered.length - 1 : current - 1 setSelectedCommandId(ordered[nextIndex]?.id ?? null) } else if (e.key === "Enter") { e.preventDefault() e.stopPropagation() const index = selectedIndex() if (index < 0 || index >= ordered.length) return const command = ordered[index] if (!command) return props.onExecute(command) props.onClose() } } function handleCommandClick(command: Command) { props.onExecute(command) props.onClose() } function handlePointerLeave() { setIsPointerSelecting(false) } return ( !open && props.onClose()}>
Command Palette Search and execute commands
) } export default CommandPalette