feat(tui): support session deletion

This commit is contained in:
Dax Raad 2026-07-06 19:19:22 -04:00
commit c0ea1a43f9
18 changed files with 563 additions and 273 deletions

View file

@ -1068,7 +1068,7 @@ function App(props: { onSnapshot?: () => Promise<string[]>; pluginHost: TuiPlugi
})
event.on("session.deleted", (evt) => {
if (route.data.type === "session" && route.data.sessionID === evt.data.info.id) {
if (route.data.type === "session" && route.data.sessionID === evt.data.sessionID) {
route.navigate({ type: "home" })
toast.show({
variant: "info",

View file

@ -1,4 +1,4 @@
import { createMemo, createResource, onMount } from "solid-js"
import { createMemo, createResource, createSignal, onMount } from "solid-js"
import path from "path"
import type { SessionV2Info } from "@opencode-ai/sdk/v2"
import { useDialog } from "../ui/dialog"
@ -15,6 +15,7 @@ import { useToast } from "../ui/toast"
import { useCommandShortcut } from "../keymap"
import { DialogSessionRename } from "./dialog-session-rename"
import { Spinner } from "./spinner"
import { errorMessage } from "../util/error"
export function DialogSessionList() {
const dialog = useDialog()
@ -26,8 +27,10 @@ export function DialogSessionList() {
const local = useLocal()
const toast = useToast()
const [search, setSearch] = createDebouncedSignal("", 150)
const [toDelete, setToDelete] = createSignal<string>()
const quickSwitch1 = useCommandShortcut("session.quick_switch.1")
const quickSwitch9 = useCommandShortcut("session.quick_switch.9")
const deleteHint = useCommandShortcut("session.delete")
const [searchResults] = createResource(search, async (query) => {
if (!query) return
@ -78,11 +81,13 @@ export function DialogSessionList() {
const directory = session.location.directory
const footer = directory !== project.data.project.mainDir ? Locale.truncate(path.basename(directory), 20) : ""
const slot = slotByID.get(session.id)
const deleting = toDelete() === session.id
return {
title: session.title,
title: deleting ? `Press ${deleteHint()} 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")
? () => <Spinner />
@ -104,9 +109,6 @@ export function DialogSessionList() {
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"
@ -114,6 +116,7 @@ export function DialogSessionList() {
skipFilter={true}
current={currentSessionID()}
onFilter={setSearch}
onMove={() => setToDelete(undefined)}
onSelect={(option) => {
route.navigate({ type: "session", sessionID: option.value })
dialog.clear()
@ -127,7 +130,22 @@ export function DialogSessionList() {
{
command: "session.delete",
title: "delete",
onTrigger: () => unavailable("Deleting"),
onTrigger: (option: { value: string }) => {
if (toDelete() !== option.value) {
setToDelete(option.value)
return
}
void sdk.client.v2.session
.remove({ sessionID: option.value }, { throwOnError: true })
.catch((error) => {
setToDelete(undefined)
toast.show({
message: `Failed to delete session: ${errorMessage(error)}`,
variant: "error",
duration: 5000,
})
})
},
},
{
command: "session.rename",

View file

@ -213,11 +213,35 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
)
}
function removeSession(sessionID: string) {
messageIndex.delete(sessionID)
setStore(
"session",
produce((draft) => {
delete draft.info[sessionID]
delete draft.status[sessionID]
delete draft.compaction[sessionID]
delete draft.message[sessionID]
delete draft.input[sessionID]
delete draft.permission[sessionID]
delete draft.form[sessionID]
for (const [rootID, family] of Object.entries(draft.family)) {
const next = family.filter((id) => id !== sessionID)
if (next.length === 0) delete draft.family[rootID]
else draft.family[rootID] = next
}
}),
)
}
function handleEvent(event: V2Event) {
switch (event.type) {
case "session.created":
void result.session.refresh(event.data.sessionID)
break
case "session.deleted":
removeSession(event.data.sessionID)
break
case "catalog.updated":
void Promise.all([
result.location.model.refresh(event.location),

View file

@ -470,7 +470,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
}
event.on("session.deleted", (evt) => {
prune(evt.data.info.id)
prune(evt.data.sessionID)
})
return {