Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 79 additions & 1 deletion packages/app/src/components/session/session-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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(() => (
<DialogDeleteArchivedSession
session={session}
onConfirm={async () => {
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)
Expand Down Expand Up @@ -838,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) => {
Expand Down Expand Up @@ -1006,6 +1039,7 @@ function SessionChatsDropdown() {
session={session}
onOpen={openSession}
onUnarchive={unarchiveSession}
onDelete={deleteArchivedSession}
/>
)}
</For>
Expand Down Expand Up @@ -1090,6 +1124,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)

Expand All @@ -1106,7 +1141,7 @@ function ArchivedSessionDropdownRow(props: {
{title()}
</span>
</button>
<div class="absolute right-1.5 top-1/2 flex -translate-y-1/2 items-center opacity-0 group-hover/archived:opacity-100 focus-within:opacity-100 transition-opacity">
<div class="absolute right-1.5 top-1/2 flex -translate-y-1/2 items-center gap-0.5 opacity-0 group-hover/archived:opacity-100 focus-within:opacity-100 transition-opacity">
<TooltipV2 placement="top" value="Unarchive">
<IconButtonV2
data-action="session-dropdown-unarchive"
Expand All @@ -1121,7 +1156,50 @@ function ArchivedSessionDropdownRow(props: {
}}
/>
</TooltipV2>
<TooltipV2 placement="top" value="Delete permanently">
<IconButtonV2
data-action="session-dropdown-delete"
variant="ghost-muted"
size="large"
icon={<Icon name="trash" size="small" />}
aria-label="Delete permanently"
onClick={(event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
void props.onDelete(props.session)
}}
/>
</TooltipV2>
</div>
</div>
)
}

// 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 (
<DialogV2 fit>
<DialogHeader hideClose>
<DialogTitleGroup
title={language.t("session.delete.title")}
description={language.t("session.delete.confirm", { name: name() })}
/>
</DialogHeader>
<DialogFooter>
<ButtonV2 variant="ghost" onClick={props.onCancel}>
{language.t("common.cancel")}
</ButtonV2>
<ButtonV2 variant="danger" onClick={props.onConfirm}>
{language.t("session.delete.button")}
</ButtonV2>
</DialogFooter>
</DialogV2>
)
}
82 changes: 80 additions & 2 deletions packages/app/src/pages/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -419,6 +420,33 @@ function HomeDesign() {
}
}

// amicode#310: permanently delete an archived session with confirmation dialog.
function deleteArchivedSession(session: Session) {
dialog.show(() => (
<DialogDeleteArchivedSession
session={session}
onConfirm={async () => {
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()
Expand Down Expand Up @@ -837,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)
Expand Down Expand Up @@ -1247,6 +1279,7 @@ function HomeDesign() {
session={session}
openSession={openSession}
onUnarchive={unarchiveSession}
onDelete={deleteArchivedSession}
/>
)}
</For>
Expand Down Expand Up @@ -2122,10 +2155,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)

Expand All @@ -2143,7 +2178,7 @@ function ArchivedSessionRow(props: {
</span>
</button>
<div
class="absolute right-1.5 top-1/2 flex -translate-y-1/2 items-center opacity-0 group-hover/archived:opacity-100 focus-within:opacity-100 transition-opacity"
class="absolute right-1.5 top-1/2 flex -translate-y-1/2 items-center gap-0.5 opacity-0 group-hover/archived:opacity-100 focus-within:opacity-100 transition-opacity"
>
<TooltipV2 placement="top" value="Unarchive">
<IconButtonV2
Expand All @@ -2159,8 +2194,51 @@ function ArchivedSessionRow(props: {
}}
/>
</TooltipV2>
<TooltipV2 placement="top" value="Delete permanently">
<IconButtonV2
data-action="home-session-delete"
variant="ghost-muted"
size="large"
icon={<Icon name="trash" size="small" />}
aria-label="Delete permanently"
onClick={(event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
void props.onDelete(props.session)
}}
/>
</TooltipV2>
</div>
</div>
)
}

// 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 (
<DialogV2 fit>
<DialogHeader hideClose>
<DialogTitleGroup
title={language.t("session.delete.title")}
description={language.t("session.delete.confirm", { name: name() })}
/>
</DialogHeader>
<DialogFooter>
<ButtonV2 variant="ghost" onClick={props.onCancel}>
{language.t("common.cancel")}
</ButtonV2>
<ButtonV2 variant="danger" onClick={props.onConfirm}>
{language.t("session.delete.button")}
</ButtonV2>
</DialogFooter>
</DialogV2>
)
}

2 changes: 1 addition & 1 deletion packages/ui/src/context/dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions packages/ui/src/v2/components/tooltip-v2.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading