From 657f3d5089cc20315ead234367ee8f1f18efd754 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 15 Jan 2026 17:58:17 -0600 Subject: feat(app): unified search for commands and files --- packages/app/src/components/dialog-select-file.tsx | 178 ++++++++++++++++++--- 1 file changed, 153 insertions(+), 25 deletions(-) (limited to 'packages/app/src/components') diff --git a/packages/app/src/components/dialog-select-file.tsx b/packages/app/src/components/dialog-select-file.tsx index 461f8a0c0..3b80c2687 100644 --- a/packages/app/src/components/dialog-select-file.tsx +++ b/packages/app/src/components/dialog-select-file.tsx @@ -4,11 +4,26 @@ import { FileIcon } from "@opencode-ai/ui/file-icon" import { List } from "@opencode-ai/ui/list" import { getDirectory, getFilename } from "@opencode-ai/util/path" import { useParams } from "@solidjs/router" -import { createMemo } from "solid-js" +import { createMemo, createSignal, onCleanup, Show } from "solid-js" +import { formatKeybind, useCommand, type CommandOption } from "@/context/command" import { useLayout } from "@/context/layout" import { useFile } from "@/context/file" +type EntryType = "command" | "file" + +type Entry = { + id: string + type: EntryType + title: string + description?: string + keybind?: string + category: "Commands" | "Files" + option?: CommandOption + path?: string +} + export function DialogSelectFile() { + const command = useCommand() const layout = useLayout() const file = useFile() const dialog = useDialog() @@ -16,35 +31,148 @@ export function DialogSelectFile() { const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) const tabs = createMemo(() => layout.tabs(sessionKey())) const view = createMemo(() => layout.view(sessionKey())) + const state = { cleanup: undefined as (() => void) | void, committed: false } + const [grouped, setGrouped] = createSignal(false) + const common = ["session.new", "session.previous", "session.next", "terminal.toggle", "review.toggle"] + const limit = 5 + + const allowed = createMemo(() => + command.options.filter( + (option) => !option.disabled && !option.id.startsWith("suggested.") && option.id !== "file.open", + ), + ) + + const commandItem = (option: CommandOption): Entry => ({ + id: "command:" + option.id, + type: "command", + title: option.title, + description: option.description, + keybind: option.keybind, + category: "Commands", + option, + }) + + const fileItem = (path: string): Entry => ({ + id: "file:" + path, + type: "file", + title: path, + category: "Files", + path, + }) + + const list = createMemo(() => allowed().map(commandItem)) + + const picks = createMemo(() => { + const all = allowed() + const order = new Map(common.map((id, index) => [id, index])) + const picked = all.filter((option) => order.has(option.id)) + const base = picked.length ? picked : all.slice(0, limit) + const sorted = picked.length ? [...base].sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0)) : base + return sorted.map(commandItem) + }) + + const recent = createMemo(() => { + const all = tabs().all() + const active = tabs().active() + const order = active ? [active, ...all.filter((item) => item !== active)] : all + const seen = new Set() + const items: Entry[] = [] + + for (const item of order) { + const path = file.pathFromTab(item) + if (!path) continue + if (seen.has(path)) continue + seen.add(path) + items.push(fileItem(path)) + } + + return items.slice(0, limit) + }) + + const items = async (filter: string) => { + const query = filter.trim() + setGrouped(query.length > 0) + if (!query) return [...picks(), ...recent()] + const files = await file.searchFiles(query) + const entries = files.map(fileItem) + return [...list(), ...entries] + } + + const handleMove = (item: Entry | undefined) => { + state.cleanup?.() + if (!item) return + if (item.type !== "command") return + state.cleanup = item.option?.onHighlight?.() + } + + const open = (path: string) => { + const value = file.tab(path) + tabs().open(value) + file.load(path) + view().reviewPanel.open() + } + + const handleSelect = (item: Entry | undefined) => { + if (!item) return + state.committed = true + state.cleanup = undefined + dialog.close() + + if (item.type === "command") { + item.option?.onSelect?.("palette") + return + } + + if (!item.path) return + open(item.path) + } + + onCleanup(() => { + if (state.committed) return + state.cleanup?.() + }) + return ( - + x} - onSelect={(path) => { - if (path) { - const value = file.tab(path) - tabs().open(value) - file.load(path) - view().reviewPanel.open() - } - dialog.close() - }} + search={{ placeholder: "Search files and commands", autofocus: true }} + emptyMessage="No results found" + items={items} + key={(item) => item.id} + filterKeys={["title", "description", "category"]} + groupBy={(item) => (grouped() ? item.category : "")} + onMove={handleMove} + onSelect={handleSelect} > - {(i) => ( -
-
- -
- - {getDirectory(i)} - - {getFilename(i)} + {(item) => ( + +
+ +
+ + {getDirectory(item.path ?? "")} + + {getFilename(item.path ?? "")} +
+
+
+ } + > +
+
+ {item.title} + + {item.description} +
+ + {formatKeybind(item.keybind ?? "")} +
-
+ )}
-- cgit v1.2.3