diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index acf007197b5b..7f3372db5887 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -761,6 +761,16 @@ function App(props: { onSnapshot?: () => Promise }) { } }) +event.on("session.archived", (evt) => { + if (route.data.type === "session" && route.data.sessionID === evt.properties.info.id) { + 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 9ecb21e82a52..34d77eb3084b 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 @@ -15,6 +15,7 @@ import { createDebouncedSignal } from "../util/signal" import { useToast } from "../ui/toast" import { DialogWorkspaceCreate, openWorkspaceSession } from "./dialog-workspace-create" import { Spinner } from "./spinner" +import { Keybind } from "@/util/keybind" type WorkspaceStatus = "connected" | "connecting" | "disconnected" | "error" @@ -28,6 +29,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] = createResource(search, async (query) => { @@ -59,7 +61,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) => b.time.updated - a.time.updated) .map((x) => { const workspace = x.workspaceID ? project.workspace.get(x.workspaceID) : undefined @@ -124,7 +130,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, @@ -140,25 +153,61 @@ export function DialogSessionList() { dialog.clear() }} keybind={[ + ...(showArchived() + ? [] + : [ + { + keybind: keybind.all.session_delete?.[0], + title: "delete", + onTrigger: async (option: { value: string }) => { + if (toDelete() === option.value) { + sdk.client.session.delete({ + sessionID: option.value, + }) + setToDelete(undefined) + return + } + 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() }, + }) + }, + }, + { + keybind: keybind.all.session_rename?.[0], + title: "rename", + onTrigger: async (option: { value: string }) => { + dialog.replace(() => ) + }, + }, + ]), + ...(showArchived() + ? [ + { + 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_delete?.[0], - title: "delete", - onTrigger: async (option) => { - if (toDelete() === option.value) { - sdk.client.session.delete({ - sessionID: option.value, - }) - setToDelete(undefined) - return - } - setToDelete(option.value) - }, - }, - { - 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 c6bc231fcad0..06851da9ed47 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -516,6 +516,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/config.ts b/packages/opencode/src/config/config.ts index ab3abaf94e67..005dc9799dd6 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -584,6 +584,7 @@ export namespace Config { session_fork: z.string().optional().default("none").describe("Fork session from message"), session_rename: z.string().optional().default("ctrl+r").describe("Rename session"), session_delete: z.string().optional().default("ctrl+d").describe("Delete session"), + session_archive: z.string().optional().default("none").describe("Archive session"), stash_delete: z.string().optional().default("ctrl+d").describe("Delete stash entry"), model_provider_list: z.string().optional().default("ctrl+a").describe("Open provider list from model dialog"), model_favorite_toggle: z.string().optional().default("ctrl+f").describe("Toggle model favorite status"),