import { useDialog } from "@opencode-ai/ui/context/dialog" import { Dialog } from "@opencode-ai/ui/dialog" import { FileIcon } from "@opencode-ai/ui/file-icon" import { Icon } from "@opencode-ai/ui/icon" import { Keybind } from "@opencode-ai/ui/keybind" import { List } from "@opencode-ai/ui/list" import { base64Encode } from "@opencode-ai/util/encode" import { getDirectory, getFilename } from "@opencode-ai/util/path" import { useNavigate, useParams } from "@solidjs/router" import { createMemo, createSignal, Match, onCleanup, Show, Switch } from "solid-js" import { formatKeybind, useCommand, type CommandOption } from "@/context/command" import { useGlobalSDK } from "@/context/global-sdk" import { useGlobalSync } from "@/context/global-sync" import { useLayout } from "@/context/layout" import { useFile } from "@/context/file" import { useLanguage } from "@/context/language" import { decode64 } from "@/utils/base64" import { getRelativeTime } from "@/utils/time" type EntryType = "command" | "file" | "session" type Entry = { id: string type: EntryType title: string description?: string keybind?: string category: string option?: CommandOption path?: string directory?: string sessionID?: string archived?: number updated?: number } type DialogSelectFileMode = "all" | "files" const ENTRY_LIMIT = 5 const COMMON_COMMAND_IDS = [ "session.new", "workspace.new", "session.previous", "session.next", "terminal.toggle", "review.toggle", ] as const const uniqueEntries = (items: Entry[]) => { const seen = new Set() const out: Entry[] = [] for (const item of items) { if (seen.has(item.id)) continue seen.add(item.id) out.push(item) } return out } const createCommandEntry = (option: CommandOption, category: string): Entry => ({ id: "command:" + option.id, type: "command", title: option.title, description: option.description, keybind: option.keybind, category, option, }) const createFileEntry = (path: string, category: string): Entry => ({ id: "file:" + path, type: "file", title: path, category, path, }) const createSessionEntry = ( input: { directory: string id: string title: string description: string archived?: number updated?: number }, category: string, ): Entry => ({ id: `session:${input.directory}:${input.id}`, type: "session", title: input.title, description: input.description, category, directory: input.directory, sessionID: input.id, archived: input.archived, updated: input.updated, }) function createCommandEntries(props: { filesOnly: () => boolean command: ReturnType language: ReturnType }) { const allowed = createMemo(() => { if (props.filesOnly()) return [] return props.command.options.filter( (option) => !option.disabled && !option.id.startsWith("suggested.") && option.id !== "file.open", ) }) const list = createMemo(() => { const category = props.language.t("palette.group.commands") return allowed().map((option) => createCommandEntry(option, category)) }) const picks = createMemo(() => { const all = allowed() const order = new Map(COMMON_COMMAND_IDS.map((id, index) => [id, index])) const picked = all.filter((option) => order.has(option.id)) const base = picked.length ? picked : all.slice(0, ENTRY_LIMIT) const sorted = picked.length ? [...base].sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0)) : base const category = props.language.t("palette.group.commands") return sorted.map((option) => createCommandEntry(option, category)) }) return { allowed, list, picks } } function createFileEntries(props: { file: ReturnType tabs: () => ReturnType["tabs"]> language: ReturnType }) { const recent = createMemo(() => { const all = props.tabs().all() const active = props.tabs().active() const order = active ? [active, ...all.filter((item) => item !== active)] : all const seen = new Set() const category = props.language.t("palette.group.files") const items: Entry[] = [] for (const item of order) { const path = props.file.pathFromTab(item) if (!path) continue if (seen.has(path)) continue seen.add(path) items.push(createFileEntry(path, category)) } return items.slice(0, ENTRY_LIMIT) }) const root = createMemo(() => { const category = props.language.t("palette.group.files") const nodes = props.file.tree.children("") const paths = nodes .filter((node) => node.type === "file") .map((node) => node.path) .sort((a, b) => a.localeCompare(b)) return paths.slice(0, ENTRY_LIMIT).map((path) => createFileEntry(path, category)) }) return { recent, root } } function createSessionEntries(props: { workspaces: () => string[] label: (directory: string) => string globalSDK: ReturnType language: ReturnType }) { const state: { token: number inflight: Promise | undefined cached: Entry[] | undefined } = { token: 0, inflight: undefined, cached: undefined, } const sessions = (text: string) => { const query = text.trim() if (!query) { state.token += 1 state.inflight = undefined state.cached = undefined return [] as Entry[] } if (state.cached) return state.cached if (state.inflight) return state.inflight const current = state.token const dirs = props.workspaces() if (dirs.length === 0) return [] as Entry[] state.inflight = Promise.all( dirs.map((directory) => { const description = props.label(directory) return props.globalSDK.client.session .list({ directory, roots: true }) .then((x) => (x.data ?? []) .filter((s) => !!s?.id) .map((s) => ({ id: s.id, title: s.title ?? props.language.t("command.session.new"), description, directory, archived: s.time?.archived, updated: s.time?.updated, })), ) .catch( () => [] as { id: string title: string description: string directory: string archived?: number updated?: number }[], ) }), ) .then((results) => { if (state.token !== current) return [] as Entry[] const seen = new Set() const category = props.language.t("command.category.session") const next = results .flat() .filter((item) => { const key = `${item.directory}:${item.id}` if (seen.has(key)) return false seen.add(key) return true }) .map((item) => createSessionEntry(item, category)) state.cached = next return next }) .catch(() => [] as Entry[]) .finally(() => { state.inflight = undefined }) return state.inflight } return { sessions } } export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFile?: (path: string) => void }) { const command = useCommand() const language = useLanguage() const layout = useLayout() const file = useFile() const dialog = useDialog() const params = useParams() const navigate = useNavigate() const globalSDK = useGlobalSDK() const globalSync = useGlobalSync() const filesOnly = () => props.mode === "files" 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 commandEntries = createCommandEntries({ filesOnly, command, language }) const fileEntries = createFileEntries({ file, tabs, language }) const projectDirectory = createMemo(() => decode64(params.dir) ?? "") const project = createMemo(() => { const directory = projectDirectory() if (!directory) return return layout.projects.list().find((p) => p.worktree === directory || p.sandboxes?.includes(directory)) }) const workspaces = createMemo(() => { const directory = projectDirectory() const current = project() if (!current) return directory ? [directory] : [] const dirs = [current.worktree, ...(current.sandboxes ?? [])] if (directory && !dirs.includes(directory)) return [...dirs, directory] return dirs }) const homedir = createMemo(() => globalSync.data.path.home) const label = (directory: string) => { const current = project() const kind = current && directory === current.worktree ? language.t("workspace.type.local") : language.t("workspace.type.sandbox") const [store] = globalSync.child(directory, { bootstrap: false }) const home = homedir() const path = home ? directory.replace(home, "~") : directory const name = store.vcs?.branch ?? getFilename(directory) return `${kind} : ${name || path}` } const { sessions } = createSessionEntries({ workspaces, label, globalSDK, language }) const items = async (text: string) => { const query = text.trim() setGrouped(query.length > 0) if (!query && filesOnly()) { const loaded = file.tree.state("")?.loaded const pending = loaded ? Promise.resolve() : file.tree.list("") const next = uniqueEntries([...fileEntries.recent(), ...fileEntries.root()]) if (loaded || next.length > 0) { void pending return next } await pending return uniqueEntries([...fileEntries.recent(), ...fileEntries.root()]) } if (!query) return [...commandEntries.picks(), ...fileEntries.recent()] if (filesOnly()) { const files = await file.searchFiles(query) const category = language.t("palette.group.files") return files.map((path) => createFileEntry(path, category)) } const [files, nextSessions] = await Promise.all([file.searchFiles(query), Promise.resolve(sessions(query))]) const category = language.t("palette.group.files") const entries = files.map((path) => createFileEntry(path, category)) return [...commandEntries.list(), ...nextSessions, ...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) if (!view().reviewPanel.opened()) view().reviewPanel.open() layout.fileTree.setTab("all") props.onOpenFile?.(path) tabs().setActive(value) } 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.type === "session") { if (!item.directory || !item.sessionID) return navigate(`/${base64Encode(item.directory)}/session/${item.sessionID}`) return } if (!item.path) return open(item.path) } onCleanup(() => { if (state.committed) return state.cleanup?.() }) return ( item.id} filterKeys={["title", "description", "category"]} groupBy={grouped() ? (item) => item.category : () => ""} onMove={handleMove} onSelect={handleSelect} > {(item) => (
{getDirectory(item.path ?? "")} {getFilename(item.path ?? "")}
} >
{item.title} {item.description}
{formatKeybind(item.keybind ?? "")}
{item.title} {item.description}
{getRelativeTime(new Date(item.updated!).toISOString())}
)}
) }