import { getDirectory, getFilename } from "@opencode-ai/core/util/path" import { FileIcon } from "@opencode-ai/ui/file-icon" import { ScrollView } from "@opencode-ai/ui/scroll-view" import { Dialog, DialogBody } from "@opencode-ai/ui/v2/dialog-v2" import { Icon } from "@opencode-ai/ui/v2/icon" import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2" import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2" import { createEffect, createMemo, createResource, createSignal, For, Match, Show, Switch } from "solid-js" import { formatKeybindParts } from "@/context/command" import { useLanguage } from "@/context/language" import { getRelativeTime } from "@/utils/time" import { createCommandPaletteFileEntry, createCommandPaletteModel, uniqueCommandPaletteEntries, type CommandPaletteEntry, } from "./command-palette" import "./dialog-command-palette-v2.css" function groups(entries: CommandPaletteEntry[]) { const map = new Map() for (const entry of entries) map.set(entry.category, [...(map.get(entry.category) ?? []), entry]) return Array.from(map.entries()).map(([category, entries]) => ({ category, entries })) } function matchesEntry(entry: CommandPaletteEntry, query: string) { const value = query.toLowerCase() return [entry.title, entry.description, entry.category].some((text) => text?.toLowerCase().includes(value)) } export function DialogCommandPaletteV2(props: { onOpenFile?: (path: string) => void }) { const palette = createCommandPaletteModel(props) const [query, setQuery] = createSignal("") const [active, setActive] = createSignal(0) const loadItems = async (text: string) => { const q = text.trim() if (!q) return [...palette.preferredCommandEntries(), ...palette.recentFileEntries()] const [files, nextSessions] = await Promise.all([palette.file.searchFiles(q), Promise.resolve(palette.sessions(q))]) const category = palette.language.t("palette.group.files") return [ ...palette.commandEntries().filter((entry) => matchesEntry(entry, q)), ...nextSessions.filter((entry) => matchesEntry(entry, q)), ...files.map((path) => createCommandPaletteFileEntry(path, category)), ] } const [entries] = createResource(query, loadItems, { initialValue: [] as CommandPaletteEntry[] }) // Render stale results while a new query loads to avoid flashing "Loading" per keystroke. const visibleEntries = createMemo(() => uniqueCommandPaletteEntries(entries.latest ?? [])) const groupedEntries = createMemo(() => groups(visibleEntries())) const activeEntry = createMemo(() => visibleEntries()[active()]) createEffect(() => { query() visibleEntries() setActive(0) }) createEffect(() => { palette.highlight(activeEntry()) }) let resultsRef: HTMLDivElement | undefined const move = (delta: -1 | 1) => { const count = visibleEntries().length if (count === 0) return setActive((index) => (index + delta + count) % count) requestAnimationFrame(() => { resultsRef?.querySelector("[data-active]")?.scrollIntoView({ block: "nearest" }) }) } const handleKeyDown = (event: KeyboardEvent) => { if (event.key === "ArrowDown") { event.preventDefault() move(1) return } if (event.key === "ArrowUp") { event.preventDefault() move(-1) return } if (event.key === "Enter") { event.preventDefault() palette.select(activeEntry()) return } if (event.key === "Escape") { event.preventDefault() palette.close() } } return ( (resultsRef = el)}>
0} fallback={
{entries.loading ? palette.language.t("common.loading") : palette.language.t("palette.empty")}
} > {(group) => (
{group.category}
{(item) => ( setActive(visibleEntries().findIndex((entry) => entry.id === item.id))} onSelect={() => palette.select(item)} /> )}
)}
) } function PaletteRow(props: { item: CommandPaletteEntry active: boolean language: ReturnType onActive: () => void onSelect: () => void }) { return ( ) }