import { createMemo, createResource, createSignal, onMount } from "solid-js" import path from "path" import type { SessionInfo } from "@opencode-ai/client" import { useDialog } from "../ui/dialog" import { DialogSelect } from "../ui/dialog-select" import { useRoute } from "../context/route" import { useData } from "../context/data" import { Keymap } from "../context/keymap" import { Locale } from "../util/locale" import { useTheme } from "../context/theme" import { useClient } from "../context/client" import { useLocal } from "../context/local" import { createDebouncedSignal } from "../util/signal" import { useToast } from "../ui/toast" import { DialogSessionRename } from "./dialog-session-rename" import { Spinner } from "./spinner" import { errorMessage } from "../util/error" export function DialogSessionList() { const dialog = useDialog() const route = useRoute() const data = useData() const { theme } = useTheme() const client = useClient() const local = useLocal() const toast = useToast() const [filter, setFilter] = createSignal("") const shortcuts = Keymap.useShortcuts() const [search, setSearch] = createDebouncedSignal("", 150) const [toDelete, setToDelete] = createSignal() const [searchResults] = createResource(search, async (query) => { if (!query) return try { if (!data.location.info()) await data.location.sync() const current = data.location.info() if (!current) throw new Error("Location unavailable") const response = await client.api.session.list({ project: current.project.id, search: query, limit: 50, order: "desc", parentID: null, }) return { query, sessions: response.data, error: undefined } } catch (error) { // A transient transport failure must degrade search, not crash the TUI // through the root ErrorBoundary when the errored resource is read. return { query, sessions: [] as SessionInfo[], error } } }) const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined)) const localSessions = createMemo(() => { const query = filter().trim().toLowerCase() const sessions = data.session.list() if (!query) return sessions return sessions.filter((session) => !session.parentID && session.title.toLowerCase().includes(query)) }) const sessions = createMemo(() => { const query = filter() const local = localSessions() if (!query) return local if (query !== search() || searchResults.loading) return local const result = searchResults() if (result?.query !== query || result.error) return local return result.sessions }) const searchState = createMemo(() => { const query = filter() if (!query) return { message: "No sessions available", error: false } if (query !== search() || searchResults.loading) return { message: "Searching sessions…", error: false } const result = searchResults() if (result?.query === query && result.error) return { message: "Could not search sessions. Change the search to try again.", error: true } return { message: "No sessions found", error: false } }) const quickSwitchHint = createMemo(() => { const first = shortcuts.get("session.quick_switch.1") const last = shortcuts.get("session.quick_switch.9") if (!first || !last) return return quickSwitchRange(first, last) }) const quickSwitchFooterHints = createMemo(() => { const hint = quickSwitchHint() return hint && local.session.slots().length > 0 ? [{ title: "switch", label: hint }] : [] }) const options = createMemo(() => { const today = new Date().toDateString() const sessionMap = new Map( sessions() .filter((session) => !session.parentID) .map((session) => [session.id, session]), ) const pinned = local.session.pinned().filter((sessionID) => sessionMap.has(sessionID)) const pinnedSet = new Set(pinned) const slotByID = new Map(local.session.slots().map((sessionID, index) => [sessionID, index + 1])) const option = (session: SessionInfo, category: string) => { const directory = session.location.directory const footer = directory !== data.location.info()?.project.directory ? Locale.truncate(path.basename(directory), 20) : "" const slot = slotByID.get(session.id) const deleting = toDelete() === session.id return { title: deleting ? `Press ${shortcuts.get("session.delete")} again to confirm` : session.title, value: session.id, category, footer, bg: deleting ? theme.error : undefined, gutter: data.session.family(session.id).some((id) => data.session.status(id) === "running") ? () => : slot === undefined ? undefined : () => {slot}, } } const remaining = sessions() .filter((session) => !session.parentID && !pinnedSet.has(session.id)) .map((session) => { const date = new Date(session.time.updated).toDateString() return option(session, date === today ? "Today" : date) }) return [...pinned.map((sessionID) => option(sessionMap.get(sessionID)!, "Pinned")), ...remaining] }) onMount(() => dialog.setSize("large")) return ( { setFilter(query) setSearch(query) }} emptyView={ No sessions available } noMatchView={ {searchState().message} } onMove={() => setToDelete(undefined)} onSelect={(option) => { route.navigate({ type: "session", sessionID: option.value }) dialog.clear() }} actions={[ { command: "session.pin.toggle", title: "pin/unpin", onTrigger: (option: { value: string }) => local.session.togglePin(option.value), }, { command: "session.delete", title: "delete", onTrigger: (option: { value: string }) => { if (toDelete() !== option.value) { setToDelete(option.value) return } void client.api.session.remove({ sessionID: option.value }).catch((error) => { setToDelete(undefined) toast.show({ message: `Failed to delete session: ${errorMessage(error)}`, variant: "error", duration: 5000, }) }) }, }, { command: "session.rename", title: "rename", onTrigger: (option: { value: string }) => DialogSessionRename.show(dialog, option.value, data.session.get(option.value)?.title), }, ]} footerHints={quickSwitchFooterHints()} /> ) } function quickSwitchRange(first: string, last: string) { const prefix = first.slice(0, -1) if (first.endsWith("1") && last === `${prefix}9`) return `${prefix}1-9` return `${first} through ${last}` }