From 68d35eb31e6105e29dbd78bdc307bb12aac4eef9 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Tue, 11 Aug 2026 02:02:03 +0200 Subject: [PATCH 1/4] feat(app): add 'Delete permanently' hover action on archived sessions (#310) Add a trash-icon hover button next to the existing Unarchive button on archived session rows in both the home dashboard flyout and the in-session header dropdown. Clicking it opens a confirmation dialog (reusing the existing i18n keys and DialogV2 pattern) before permanently deleting the session via the SDK. Closes #310 --- .../src/components/session/session-header.tsx | 78 ++++++++++++++++++- packages/app/src/pages/home.tsx | 76 +++++++++++++++++- 2 files changed, 152 insertions(+), 2 deletions(-) diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index c05e7d479..4a2921423 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -8,6 +8,9 @@ import { Keybind } from "@opencode-ai/ui/keybind" import { writeClipboardViaBridge } from "@/components/prompt-input/clipboard-bridge" import { showToast } from "@/utils/toast" import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { DialogFooter, DialogHeader, DialogTitleGroup, DialogV2 } from "@opencode-ai/ui/v2/dialog-v2" +import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2" import { getFilename } from "@opencode-ai/core/util/path" import { batch, createEffect, createMemo, createSignal, For, onCleanup, onMount, Show } from "solid-js" import { createStore } from "solid-js/store" @@ -785,6 +788,34 @@ function SessionChatsDropdown() { } } + // amicode#310: permanently delete an archived session with confirmation dialog. + const dialog = useDialog() + function deleteArchivedSession(session: Session) { + dialog.show(() => ( + { + const ctx = getServerCtx() + if (!ctx) return + try { + await (ctx.sdk.client.session.delete as Function)({ + sessionID: session.id, + directory: session.directory, + }) + setArchivedSessions((prev) => prev.filter((s) => s.id !== session.id)) + } catch (cause) { + showToast({ + title: language.t("session.delete.failed.title"), + description: String(cause), + }) + } + dialog.close() + }} + onCancel={() => dialog.close()} + /> + )) + } + async function openSession(session: Session) { // Close flyout first so its Portal unmounts cleanly. setOpen(false) @@ -1006,6 +1037,7 @@ function SessionChatsDropdown() { session={session} onOpen={openSession} onUnarchive={unarchiveSession} + onDelete={deleteArchivedSession} /> )} @@ -1090,6 +1122,7 @@ function ArchivedSessionDropdownRow(props: { session: Session onOpen: (session: Session) => void onUnarchive: (session: Session) => void + onDelete: (session: Session) => void }) { const title = createMemo(() => sessionTitle(props.session.title) || props.session.id) @@ -1106,7 +1139,7 @@ function ArchivedSessionDropdownRow(props: { {title()} -
+
+ + } + aria-label="Delete permanently" + onClick={(event: MouseEvent) => { + event.preventDefault() + event.stopPropagation() + void props.onDelete(props.session) + }} + /> +
) } + +// amicode#310: confirmation dialog for permanently deleting an archived session. +function DialogDeleteArchivedSession(props: { + session: Session + onConfirm: () => void + onCancel: () => void +}) { + const language = useLanguage() + const name = createMemo(() => sessionTitle(props.session.title) || props.session.id) + + return ( + + + + + + + {language.t("common.cancel")} + + + {language.t("session.delete.button")} + + + + ) +} diff --git a/packages/app/src/pages/home.tsx b/packages/app/src/pages/home.tsx index 147f3150f..15d5de6ee 100644 --- a/packages/app/src/pages/home.tsx +++ b/packages/app/src/pages/home.tsx @@ -34,6 +34,7 @@ import { Icon } from "@opencode-ai/ui/icon" import { usePlatform } from "@/context/platform" import { DateTime } from "luxon" import { useDialog } from "@opencode-ai/ui/context/dialog" +import { DialogFooter, DialogHeader, DialogTitleGroup, DialogV2 } from "@opencode-ai/ui/v2/dialog-v2" import { useDirectoryPicker } from "@/components/directory-picker" import { DialogSelectServer, useServerManagementController } from "@/components/dialog-select-server" import { DialogServerV2 } from "@/components/settings-v2/dialog-server-v2" @@ -419,6 +420,33 @@ function HomeDesign() { } } + // amicode#310: permanently delete an archived session with confirmation dialog. + function deleteArchivedSession(session: Session) { + dialog.show(() => ( + { + const ctx = focusedServerCtx() + if (!ctx) return + try { + await (ctx.sdk.client.session.delete as Function)({ + sessionID: session.id, + directory: session.directory, + }) + setArchivedSessions((prev) => prev.filter((s) => s.id !== session.id)) + } catch (cause) { + showToast({ + title: language.t("session.delete.failed.title"), + description: errorMessage(cause, language.t("common.requestFailed")), + }) + } + dialog.close() + }} + onCancel={() => dialog.close()} + /> + )) + } + // amicode#273 AC7: extend search results to include archived sessions with badge. const archivedSearchResults = createMemo(() => { const query = search().toLowerCase() @@ -1247,6 +1275,7 @@ function HomeDesign() { session={session} openSession={openSession} onUnarchive={unarchiveSession} + onDelete={deleteArchivedSession} /> )} @@ -2122,10 +2151,12 @@ function HomeCardsSkeleton() { } // amicode#273 AC5: archived session row — click opens read-only, hover shows unarchive. +// amicode#310: added onDelete for permanent deletion. function ArchivedSessionRow(props: { session: Session openSession: (session: Session) => void onUnarchive: (session: Session) => void + onDelete: (session: Session) => void }) { const title = createMemo(() => sessionTitle(props.session.title) || props.session.id) @@ -2143,7 +2174,7 @@ function ArchivedSessionRow(props: {
+ + } + aria-label="Delete permanently" + onClick={(event: MouseEvent) => { + event.preventDefault() + event.stopPropagation() + void props.onDelete(props.session) + }} + /> +
) } +// amicode#310: confirmation dialog for permanently deleting an archived session. +function DialogDeleteArchivedSession(props: { + session: Session + onConfirm: () => void + onCancel: () => void +}) { + const language = useLanguage() + const name = createMemo(() => sessionTitle(props.session.title) || props.session.id) + + return ( + + + + + + + {language.t("common.cancel")} + + + {language.t("session.delete.button")} + + + + ) +} + From bb192cb55b6b4c39565074c7d4ed88d81d0ee719 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Tue, 11 Aug 2026 02:07:04 +0200 Subject: [PATCH 2/4] fix(app): use v1 Icon for trash glyph, fix tooltip z-index behind flyout - Switch delete button from IconV2 (which lacks a 'trash' entry and silently falls back to '+') to the v1 Icon component which has the proper trash SVG. - Bump TooltipV2 z-index from 1000 to 10000 so tooltips render above the sessions flyout (z-index 9999). This fixes the hover bubble appearing behind the titlebar dropdown for all tooltip actions (unarchive, delete). --- packages/app/src/components/session/session-header.tsx | 2 +- packages/app/src/pages/home.tsx | 2 +- packages/ui/src/v2/components/tooltip-v2.css | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index 4a2921423..cc75353c8 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -1159,7 +1159,7 @@ function ArchivedSessionDropdownRow(props: { data-action="session-dropdown-delete" variant="ghost-muted" size="large" - icon={} + icon={} aria-label="Delete permanently" onClick={(event: MouseEvent) => { event.preventDefault() diff --git a/packages/app/src/pages/home.tsx b/packages/app/src/pages/home.tsx index 15d5de6ee..34cec8b8b 100644 --- a/packages/app/src/pages/home.tsx +++ b/packages/app/src/pages/home.tsx @@ -2195,7 +2195,7 @@ function ArchivedSessionRow(props: { data-action="home-session-delete" variant="ghost-muted" size="large" - icon={} + icon={} aria-label="Delete permanently" onClick={(event: MouseEvent) => { event.preventDefault() diff --git a/packages/ui/src/v2/components/tooltip-v2.css b/packages/ui/src/v2/components/tooltip-v2.css index 315d1f7cd..3461bad39 100644 --- a/packages/ui/src/v2/components/tooltip-v2.css +++ b/packages/ui/src/v2/components/tooltip-v2.css @@ -3,11 +3,11 @@ } [data-popper-positioner]:has([data-component="tooltip-v2"]) { - z-index: 1000; + z-index: 10000; } [data-component="tooltip-v2"] { - z-index: 1000; + z-index: 10000; box-sizing: border-box; display: inline-flex; flex-direction: row; From 396e52f883cdc0866b849bf6dc2dc8207e999c0c Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Tue, 11 Aug 2026 02:16:06 +0200 Subject: [PATCH 3/4] fix(ui): bump dialog z-index above the sessions flyout (9999) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The useDialog context computed z-index as 50 + layer*10, rendering the dialog overlay and container behind the titlebar sessions flyout (z-index 9999). Bump the base to 10000 so confirmation dialogs appear on top. The inline style from dialog.tsx overrides the CSS fallback in dialog-v2.css, so the CSS values (z-index: 50) are left as-is — they only matter for non-portalled dialogs. --- packages/ui/src/context/dialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/context/dialog.tsx b/packages/ui/src/context/dialog.tsx index 39ef8ea1c..f8ed14614 100644 --- a/packages/ui/src/context/dialog.tsx +++ b/packages/ui/src/context/dialog.tsx @@ -77,7 +77,7 @@ function init() { const mount = (element: DialogElement, owner: Owner, onClose: (() => void) | undefined, layer: number) => { const id = Math.random().toString(36).slice(2) - const zIndex = 50 + layer * 10 + const zIndex = 10000 + layer * 10 let dispose: (() => void) | undefined let setClosing: ((closing: boolean) => void) | undefined From 14a2a9b55d0397d7305a03e5e30f21e50aeb01de Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Tue, 11 Aug 2026 02:19:02 +0200 Subject: [PATCH 4/4] fix(app): keep sessions flyout open while delete dialog is active The outside-click handlers on both the titlebar and dashboard sessions flyouts were closing the dropdown when the user clicked inside the delete confirmation dialog (portalled to body, outside flyoutRoot). Add a check to skip dismissal when the click target is inside a dialog layer. --- packages/app/src/components/session/session-header.tsx | 2 ++ packages/app/src/pages/home.tsx | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index cc75353c8..ddb0aa975 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -869,6 +869,8 @@ function SessionChatsDropdown() { const target = e.target as Node if (flyoutRoot?.contains(target)) return if (triggerRef?.contains(target)) return + // Don't dismiss if the click landed inside a dialog (e.g. delete confirmation) + if (target instanceof Element && target.closest("[data-dialog-layer], [data-component='dialog-overlay']")) return setOpen(false) } const onKey = (e: KeyboardEvent) => { diff --git a/packages/app/src/pages/home.tsx b/packages/app/src/pages/home.tsx index 34cec8b8b..f3634edb7 100644 --- a/packages/app/src/pages/home.tsx +++ b/packages/app/src/pages/home.tsx @@ -865,7 +865,11 @@ function HomeDesign() { createEffect(() => { if (!sessionsOpen()) return const onDown = (e: MouseEvent) => { - if (flyoutRoot && !flyoutRoot.contains(e.target as Node)) setSessionsOpen(false) + const target = e.target as Node + if (flyoutRoot && flyoutRoot.contains(target)) return + // Don't dismiss if the click landed inside a dialog (e.g. delete confirmation) + if (target instanceof Element && target.closest("[data-dialog-layer], [data-component='dialog-overlay']")) return + setSessionsOpen(false) } const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") setSessionsOpen(false)