diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index a26b8cfdfe88..c6509e451889 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -806,6 +806,20 @@ function App(props: { onSnapshot?: () => Promise }) { } }) + sdk.event.on(SessionApi.Event.Updated.type, (evt) => { + if ( + route.data.type === "session" && + route.data.sessionID === evt.properties.info.id && + evt.properties.info.time.archived + ) { + route.navigate({ type: "home" }) + toast.show({ + variant: "info", + message: "The current session was archived", + }) + } + }) + event.on("session.error", (evt) => { const error = evt.properties.error if (error && typeof error === "object" && error.name === "MessageAbortedError") return diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx index 72d60767bb9a..66cb623c7b00 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx @@ -30,6 +30,7 @@ export function DialogSessionList() { const sdk = useSDK() const toast = useToast() const [toDelete, setToDelete] = createSignal() + const [showArchived, setShowArchived] = createSignal(false) const [search, setSearch] = createDebouncedSignal("", 150) const [searchResults, { refetch }] = createResource( @@ -115,7 +116,11 @@ export function DialogSessionList() { const options = createMemo(() => { const today = new Date().toDateString() return sessions() - .filter((x) => x.parentID === undefined) + .filter((x) => { + if (x.parentID !== undefined) return false + if (showArchived()) return !!x.time.archived + return !x.time.archived + }) .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 @@ -179,7 +184,7 @@ export function DialogSessionList() { return ( { + if (showArchived()) { + sdk.client.session.update({ + sessionID: option.value, + time: { archived: 0 }, + }) + return + } route.navigate({ type: "session", sessionID: option.value, @@ -195,59 +207,141 @@ export function DialogSessionList() { dialog.clear() }} keybind={[ - { - keybind: keybind.all.session_delete?.[0], - 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), - }) + ...(showArchived() + ? [] + : [ + { + keybind: keybind.all.session_delete?.[0], + title: "delete", + onTrigger: async (option: { value: string }) => { + 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(undefined) - return - } - } catch (err) { - if (session?.workspaceID) { - recover(session) - } else { - toast.show({ - variant: "error", - title: "Failed to delete session", - message: errorMessage(err), + setToDelete(option.value) + }, + }, + { + keybind: keybind.all.session_archive?.[0], + title: "archive", + onTrigger: async (option: { value: string }) => { + sdk.client.session.update({ + sessionID: option.value, + time: { archived: Date.now() }, }) - } - setToDelete(undefined) - return - } - if (status && status !== "connected") { - await sync.session.refresh() - } - if (search()) await refetch() - setToDelete(undefined) - return - } - setToDelete(option.value) - }, - }, + }, + }, + { + keybind: keybind.all.session_rename?.[0], + title: "rename", + onTrigger: async (option: { value: string }) => { + dialog.replace(() => ) + }, + }, + ]), + ...(showArchived() + ? [ + { + keybind: keybind.all.session_delete?.[0], + title: "delete", + onTrigger: async (option: { value: string }) => { + 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) + }, + }, + { + keybind: keybind.all.session_archive?.[0], + title: "unarchive", + onTrigger: async (option: { value: string }) => { + sdk.client.session.update({ + sessionID: option.value, + time: { archived: 0 }, + }) + }, + }, + ] + : []), { - keybind: keybind.all.session_rename?.[0], - title: "rename", - onTrigger: async (option) => { - dialog.replace(() => ) + keybind: Keybind.parse("tab")[0], + title: showArchived() ? "active" : "archived", + onTrigger: async () => { + setShowArchived((prev) => !prev) + setToDelete(undefined) }, }, { diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 8855338d1d4b..828a720c94e5 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -539,6 +539,25 @@ export function Session() { dialog.clear() }, }, + { + title: "Archive session", + value: "session.archive", + keybind: "session_archive", + category: "Session", + slash: { + name: "archive", + }, + onSelect: async (dialog) => { + await sdk.client.session + .update({ + sessionID: route.sessionID, + time: { archived: Date.now() }, + }) + .then(() => toast.show({ message: "Session archived", variant: "success" })) + .catch(() => toast.show({ message: "Failed to archive session", variant: "error" })) + dialog.clear() + }, + }, { title: "Undo previous message", value: "session.undo", diff --git a/packages/opencode/src/config/keybinds.ts b/packages/opencode/src/config/keybinds.ts index a84fc0b37d58..bc7ff530274d 100644 --- a/packages/opencode/src/config/keybinds.ts +++ b/packages/opencode/src/config/keybinds.ts @@ -30,6 +30,7 @@ const KeybindsSchema = Schema.Struct({ session_fork: keybind("none", "Fork session from message"), session_rename: keybind("ctrl+r", "Rename session"), session_delete: keybind("ctrl+d", "Delete session"), + session_archive: keybind("none", "Archive session"), stash_delete: keybind("ctrl+d", "Delete stash entry"), model_provider_list: keybind("ctrl+a", "Open provider list from model dialog"), model_favorite_toggle: keybind("ctrl+f", "Toggle model favorite status"),