| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250 |
- import { useDialog } from "@tui/ui/dialog"
- import { DialogSelect } from "@tui/ui/dialog-select"
- import { useRoute } from "@tui/context/route"
- import { useSync } from "@tui/context/sync"
- import { createMemo, createResource, createSignal, onMount, type JSX } from "solid-js"
- import { Locale } from "@/util/locale"
- import { useProject } from "@tui/context/project"
- import { useTheme } from "../context/theme"
- import { useSDK } from "../context/sdk"
- import { Flag } from "@opencode-ai/core/flag/flag"
- import { DialogSessionRename } from "./dialog-session-rename"
- import { createDebouncedSignal } from "../util/signal"
- import { useToast } from "../ui/toast"
- import { openWorkspaceSelect, type WorkspaceSelection, warpWorkspaceSession } from "./dialog-workspace-create"
- import { Spinner } from "./spinner"
- import { errorMessage } from "@/util/error"
- import { DialogSessionDeleteFailed } from "./dialog-session-delete-failed"
- import { WorkspaceLabel } from "./workspace-label"
- import { useCommandShortcut } from "../keymap"
- export function DialogSessionList() {
- const dialog = useDialog()
- const route = useRoute()
- const sync = useSync()
- const project = useProject()
- const { theme } = useTheme()
- const sdk = useSDK()
- const toast = useToast()
- const [toDelete, setToDelete] = createSignal<string>()
- const [search, setSearch] = createDebouncedSignal("", 150)
- const deleteHint = useCommandShortcut("dialog.action.delete")
- const [searchResults, { refetch }] = createResource(
- () => ({ query: search(), filter: sync.session.query() }),
- async (input) => {
- if (!input.query) return undefined
- const result = await sdk.client.session.list({ search: input.query, limit: 30, ...input.filter })
- return result.data ?? []
- },
- )
- const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined))
- const sessions = createMemo(() => searchResults() ?? sync.data.session)
- function recover(session: NonNullable<ReturnType<typeof sessions>[number]>) {
- const workspace = project.workspace.get(session.workspaceID!)
- const list = () => dialog.replace(() => <DialogSessionList />)
- const warp = async (selection: WorkspaceSelection) => {
- const workspaceID = await (async () => {
- if (selection.type === "none") return null
- if (selection.type === "existing") return selection.workspaceID
- const result = await sdk.client.experimental.workspace
- .create({ type: selection.workspaceType, branch: null })
- .catch(() => undefined)
- const workspace = result?.data
- if (!workspace) {
- toast.show({
- message: `Failed to create workspace: ${errorMessage(result?.error ?? "no response")}`,
- variant: "error",
- })
- return
- }
- await project.workspace.sync()
- return workspace.id
- })()
- if (workspaceID === undefined) return
- await warpWorkspaceSession({
- dialog,
- sdk,
- sync,
- project,
- toast,
- sourceWorkspaceID: session.workspaceID,
- workspaceID,
- sessionID: session.id,
- copyChanges: false,
- done: list,
- })
- }
- dialog.replace(() => (
- <DialogSessionDeleteFailed
- session={session.title}
- workspace={workspace?.name ?? session.workspaceID!}
- onDone={list}
- onDelete={async () => {
- const current = currentSessionID()
- const info = current ? sync.data.session.find((item) => item.id === current) : undefined
- const result = await sdk.client.experimental.workspace.remove({ id: session.workspaceID! })
- if (result.error) {
- toast.show({
- variant: "error",
- title: "Failed to delete workspace",
- message: errorMessage(result.error),
- })
- return false
- }
- await project.workspace.sync()
- await sync.session.refresh()
- if (search()) await refetch()
- if (info?.workspaceID === session.workspaceID) {
- route.navigate({ type: "home" })
- }
- return true
- }}
- onRestore={() => {
- void openWorkspaceSelect({
- dialog,
- sdk,
- sync,
- project,
- toast,
- onSelect: (selection) => {
- void warp(selection)
- },
- })
- return false
- }}
- />
- ))
- }
- const options = createMemo(() => {
- const today = new Date().toDateString()
- return sessions()
- .filter((x) => x.parentID === undefined)
- .toSorted((a, b) => {
- const updatedDay = new Date(b.time.updated).setHours(0, 0, 0, 0) - new Date(a.time.updated).setHours(0, 0, 0, 0)
- if (updatedDay !== 0) return updatedDay
- return b.time.created - a.time.created
- })
- .map((x) => {
- const workspace = x.workspaceID ? project.workspace.get(x.workspaceID) : undefined
- let footer: JSX.Element | string = ""
- if (Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) {
- if (x.workspaceID) {
- footer = workspace ? (
- <WorkspaceLabel
- type={workspace.type}
- name={workspace.name}
- status={project.workspace.status(x.workspaceID) ?? "error"}
- />
- ) : (
- <WorkspaceLabel type="unknown" name={x.workspaceID} status="error" />
- )
- }
- } else {
- footer = Locale.time(x.time.updated)
- }
- const date = new Date(x.time.updated)
- let category = date.toDateString()
- if (category === today) {
- category = "Today"
- }
- const isDeleting = toDelete() === x.id
- const status = sync.data.session_status?.[x.id]
- const isWorking = status?.type === "busy" || status?.type === "retry"
- return {
- title: isDeleting ? `Press ${deleteHint()} again to confirm` : x.title,
- bg: isDeleting ? theme.error : undefined,
- value: x.id,
- category,
- footer,
- gutter: isWorking ? () => <Spinner /> : undefined,
- }
- })
- })
- onMount(() => {
- dialog.setSize("large")
- })
- return (
- <DialogSelect
- title="Sessions"
- options={options()}
- skipFilter={true}
- current={currentSessionID()}
- onFilter={setSearch}
- onMove={() => {
- setToDelete(undefined)
- }}
- onSelect={(option) => {
- route.navigate({
- type: "session",
- sessionID: option.value,
- })
- dialog.clear()
- }}
- actions={[
- {
- command: "dialog.action.delete",
- title: "delete",
- onTrigger: async (option) => {
- if (toDelete() === option.value) {
- const session = sessions().find((item) => item.id === option.value)
- const status = session?.workspaceID ? project.workspace.status(session.workspaceID) : undefined
- try {
- const result = await sdk.client.session.delete({
- sessionID: option.value,
- })
- if (result.error) {
- if (session?.workspaceID) {
- recover(session)
- } else {
- toast.show({
- variant: "error",
- title: "Failed to delete session",
- message: errorMessage(result.error),
- })
- }
- setToDelete(undefined)
- return
- }
- } catch (err) {
- if (session?.workspaceID) {
- recover(session)
- } else {
- toast.show({
- variant: "error",
- title: "Failed to delete session",
- message: errorMessage(err),
- })
- }
- setToDelete(undefined)
- return
- }
- if (status && status !== "connected") {
- await sync.session.refresh()
- }
- if (search()) await refetch()
- setToDelete(undefined)
- return
- }
- setToDelete(option.value)
- },
- },
- {
- command: "dialog.action.rename",
- title: "rename",
- onTrigger: async (option) => {
- dialog.replace(() => <DialogSessionRename session={option.value} />)
- },
- },
- ]}
- />
- )
- }
|