opencode/packages/tui/src/component/dialog-session-list.tsx
2026-06-26 14:57:01 -04:00

136 lines
4.7 KiB
TypeScript

import { createMemo, createResource, onMount } from "solid-js"
import path from "path"
import type { SessionV2Info } from "@opencode-ai/sdk/v2"
import { useDialog } from "../ui/dialog"
import { DialogSelect } from "../ui/dialog-select"
import { useRoute } from "../context/route"
import { useData } from "../context/data"
import { Locale } from "../util/locale"
import { useProject } from "../context/project"
import { useTheme } from "../context/theme"
import { useSDK } from "../context/sdk"
import { useLocal } from "../context/local"
import { createDebouncedSignal } from "../util/signal"
import { useToast } from "../ui/toast"
import { useCommandShortcut } from "../keymap"
import { Spinner } from "./spinner"
export function DialogSessionList() {
const dialog = useDialog()
const route = useRoute()
const data = useData()
const project = useProject()
const { theme } = useTheme()
const sdk = useSDK()
const local = useLocal()
const toast = useToast()
const [search, setSearch] = createDebouncedSignal("", 150)
const quickSwitch1 = useCommandShortcut("session.quick_switch.1")
const quickSwitch9 = useCommandShortcut("session.quick_switch.9")
const [searchResults] = createResource(search, async (query) => {
if (!query) return
const response = await sdk.client.v2.session.list(
{ search: query, limit: 50, order: "desc" },
{ throwOnError: true },
)
return { query, sessions: response.data.data }
})
const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined))
const sessions = createMemo(() => {
const query = search()
if (!query) return data.session.list()
const result = searchResults()
return result?.query === query ? result.sessions : []
})
const quickSwitchHint = createMemo(() => {
const first = quickSwitch1()
const last = quickSwitch9()
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: SessionV2Info, category: string) => {
const directory = session.location.directory
const footer = directory !== project.data.project.mainDir ? Locale.truncate(path.basename(directory), 20) : ""
const slot = slotByID.get(session.id)
return {
title: session.title,
value: session.id,
category,
footer,
gutter:
data.session.status(session.id) === "running"
? () => <Spinner />
: slot === undefined
? undefined
: () => <text fg={theme.accent}>{slot}</text>,
}
}
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"))
const unavailable = (feature: string) =>
toast.show({ message: `${feature} is not implemented for V2 sessions yet`, variant: "error", duration: 5000 })
return (
<DialogSelect
title="Sessions"
options={options()}
skipFilter={true}
current={currentSessionID()}
onFilter={setSearch}
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: () => unavailable("Deleting"),
},
{
command: "session.rename",
title: "rename",
onTrigger: () => unavailable("Renaming"),
},
]}
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}`
}