Buckets:
| 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/core/util/encode" | |
| import { getDirectory, getFilename } from "@opencode-ai/core/util/path" | |
| import { useNavigate } from "@solidjs/router" | |
| import { createMemo, createSignal, Match, onCleanup, Show, Switch } from "solid-js" | |
| import { formatKeybind, useCommand, type CommandOption } from "@/context/command" | |
| import { useServerSDK } from "@/context/server-sdk" | |
| import { useServerSync } from "@/context/server-sync" | |
| import { useLayout } from "@/context/layout" | |
| import { useFile } from "@/context/file" | |
| import { useLanguage } from "@/context/language" | |
| import { useSessionLayout } from "@/pages/session/session-layout" | |
| import { createSessionTabs } from "@/pages/session/helpers" | |
| 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<string>() | |
| 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<typeof useCommand> | |
| language: ReturnType<typeof useLanguage> | |
| }) { | |
| const allowed = createMemo(() => { | |
| if (props.filesOnly()) return [] | |
| return props.command.options.filter( | |
| (option) => | |
| !option.disabled && !option.hidden && !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<string, number>(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<typeof useFile> | |
| tabs: () => ReturnType<ReturnType<typeof useLayout>["tabs"]> | |
| language: ReturnType<typeof useLanguage> | |
| }) { | |
| const tabState = createSessionTabs({ | |
| tabs: props.tabs, | |
| pathFromTab: props.file.pathFromTab, | |
| normalizeTab: (tab) => (tab.startsWith("file://") ? props.file.tab(tab) : tab), | |
| }) | |
| const recent = createMemo(() => { | |
| const all = tabState.openedTabs() | |
| const active = tabState.activeFileTab() | |
| const order = active ? [active, ...all.filter((item) => item !== active)] : all | |
| const seen = new Set<string>() | |
| 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 | |
| serverSDK: ReturnType<typeof useServerSDK> | |
| language: ReturnType<typeof useLanguage> | |
| }) { | |
| const state: { | |
| token: number | |
| inflight: Promise<Entry[]> | 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.serverSDK.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<string>() | |
| 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 navigate = useNavigate() | |
| const serverSDK = useServerSDK() | |
| const serverSync = useServerSync() | |
| const { params, tabs, view } = useSessionLayout() | |
| const filesOnly = () => props.mode === "files" | |
| 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(() => serverSync.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] = serverSync.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, serverSDK, 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) | |
| void tabs().open(value) | |
| void 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 ( | |
| <Dialog class="pt-3 pb-0 !max-h-[480px]" transition> | |
| <List | |
| class="px-3" | |
| search={{ | |
| placeholder: filesOnly() | |
| ? language.t("session.header.searchFiles") | |
| : language.t("palette.search.placeholder"), | |
| autofocus: true, | |
| hideIcon: true, | |
| }} | |
| emptyMessage={language.t("palette.empty")} | |
| loadingMessage={language.t("common.loading")} | |
| items={items} | |
| key={(item) => item.id} | |
| filterKeys={["title", "description", "category"]} | |
| skipFilter={(item) => item.type === "file"} | |
| groupBy={grouped() ? (item) => item.category : () => ""} | |
| onMove={handleMove} | |
| onSelect={handleSelect} | |
| > | |
| {(item) => ( | |
| <Switch | |
| fallback={ | |
| <div class="w-full flex items-center justify-between rounded-md pl-1"> | |
| <div class="flex items-center gap-x-3 grow min-w-0"> | |
| <FileIcon node={{ path: item.path ?? "", type: "file" }} class="shrink-0 size-4" /> | |
| <div class="flex items-center text-14-regular"> | |
| <span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0"> | |
| {getDirectory(item.path ?? "")} | |
| </span> | |
| <span class="text-text-strong whitespace-nowrap">{getFilename(item.path ?? "")}</span> | |
| </div> | |
| </div> | |
| </div> | |
| } | |
| > | |
| <Match when={item.type === "command"}> | |
| <div class="w-full flex items-center justify-between gap-4"> | |
| <div class="flex items-center gap-2 min-w-0"> | |
| <span class="text-14-regular text-text-strong whitespace-nowrap">{item.title}</span> | |
| <Show when={item.description}> | |
| <span class="text-14-regular text-text-weak truncate">{item.description}</span> | |
| </Show> | |
| </div> | |
| <Show when={item.keybind}> | |
| <Keybind class="rounded-[4px]">{formatKeybind(item.keybind ?? "", language.t)}</Keybind> | |
| </Show> | |
| </div> | |
| </Match> | |
| <Match when={item.type === "session"}> | |
| <div class="w-full flex items-center justify-between rounded-md pl-1"> | |
| <div class="flex items-center gap-x-3 grow min-w-0"> | |
| <Icon name="bubble-5" size="small" class="shrink-0 text-icon-weak" /> | |
| <div class="flex items-center gap-2 min-w-0"> | |
| <span | |
| class="text-14-regular text-text-strong truncate" | |
| classList={{ "opacity-70": !!item.archived }} | |
| > | |
| {item.title} | |
| </span> | |
| <Show when={item.description}> | |
| <span | |
| class="text-14-regular text-text-weak truncate" | |
| classList={{ "opacity-70": !!item.archived }} | |
| > | |
| {item.description} | |
| </span> | |
| </Show> | |
| </div> | |
| </div> | |
| <Show when={item.updated}> | |
| <span class="text-12-regular text-text-weak whitespace-nowrap ml-2"> | |
| {getRelativeTime(new Date(item.updated!).toISOString(), language.t)} | |
| </span> | |
| </Show> | |
| </div> | |
| </Match> | |
| </Switch> | |
| )} | |
| </List> | |
| </Dialog> | |
| ) | |
| } | |
Xet Storage Details
- Size:
- 15.4 kB
- Xet hash:
- 6386f1290ead408217ca12272a2eba8e6ecf1d6c35282ad4cffa885e5555aaa0
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.