mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-13 19:46:16 +03:00
feat(app): delete a session from its tab menu (#57453)
This commit is contained in:
committed by
GitHub
Unverified
parent
1b79e9726c
commit
1817f310ec
@@ -161,6 +161,35 @@ for (const tabLayout of ["horizontal", "vertical"] as const) {
|
||||
})
|
||||
}
|
||||
|
||||
for (const tabLayout of ["horizontal", "vertical"] as const) {
|
||||
test(`a ${tabLayout} tab deletes its session after confirmation`, async ({ page }) => {
|
||||
await mockWorkspace(page, { name: "Tabs", sessions: [a, b], seed: { settings: { appearance: { tabLayout } } } })
|
||||
await page.goto(sessionHref(a.id))
|
||||
const title = page.locator(`[data-titlebar-tab-slot]:has(a[href="${sessionHref(a.id)}"]) [data-slot="tab-title"]`)
|
||||
|
||||
const remove = page.waitForRequest(
|
||||
(request) => request.method() === "DELETE" && request.url().endsWith(`/api/session/${a.id}`),
|
||||
)
|
||||
|
||||
await expect(title).toHaveText(a.title)
|
||||
|
||||
await title.click({ button: "right" })
|
||||
await page.getByRole("menuitem", { name: "Delete session…", exact: true }).click()
|
||||
|
||||
const dialog = page.getByRole("dialog")
|
||||
await dialog.getByRole("button", { name: "Cancel", exact: true }).click()
|
||||
await expect(dialog).toHaveCount(0)
|
||||
await expect(title).toHaveText(a.title)
|
||||
|
||||
await title.click({ button: "right" })
|
||||
await page.getByRole("menuitem", { name: "Delete session…", exact: true }).click()
|
||||
await dialog.getByRole("button", { name: "Delete session", exact: true }).click()
|
||||
await remove
|
||||
await expect(title).toHaveCount(0)
|
||||
await expectPath(page, sessionHref(b.id))
|
||||
})
|
||||
}
|
||||
|
||||
test("keyboard navigation follows the visible tab order and skips unresolved tabs", async ({ page }) => {
|
||||
await mockWorkspace(page, { name: "Tabs", sessions: [a, c], seed: { tabs: [a.id, "ses_tab_unresolved", c.id] } })
|
||||
await page.route(
|
||||
|
||||
@@ -32,6 +32,7 @@ export function TabNavItem(props: {
|
||||
fallbackDirectory?: string
|
||||
onRename: (title: string) => Promise<void>
|
||||
onClose: () => void
|
||||
onDelete: () => void
|
||||
onNavigate: () => void
|
||||
active?: boolean
|
||||
suppressNavigation?: boolean
|
||||
@@ -435,6 +436,10 @@ export function TabNavItem(props: {
|
||||
{language.t("common.rename")}
|
||||
</Menu.Item>
|
||||
<Menu.Item onSelect={props.onClose}>{language.t("common.closeTab")}</Menu.Item>
|
||||
<Menu.Separator />
|
||||
<Menu.Item data-variant="critical" disabled={!props.session} onSelect={props.onDelete}>
|
||||
{language.t("session.delete.title")}…
|
||||
</Menu.Item>
|
||||
</Menu.Context.Content>
|
||||
</Menu.Context.Portal>
|
||||
</Menu.Context>
|
||||
|
||||
@@ -18,6 +18,13 @@ import { createTabComposerState } from "@/composer/persistence"
|
||||
import { base64Encode } from "@opencode/util/encode"
|
||||
import { showToast } from "@/shell/notifications/toast"
|
||||
import { adjacentTabKey, mergeVisibleTabOrder } from "./tab-order"
|
||||
import { Button } from "@opencode/ui/button"
|
||||
import { useDialog } from "@opencode/ui/context/dialog"
|
||||
import { Dialog, DialogFooter, DialogHeader, DialogTitleGroup } from "@opencode/ui/dialog"
|
||||
import { sessionTreeIDs } from "@/session/requests/session-request-tree"
|
||||
import { sessionTitle } from "@/session/title"
|
||||
import { errorMessage } from "@/shell/layout/helpers"
|
||||
import { notifySessionTabsRemoved } from "./session-events"
|
||||
import type { SessionInfo } from "@opencode/client/promise"
|
||||
|
||||
function SessionTabSlot(props: {
|
||||
@@ -33,6 +40,7 @@ function SessionTabSlot(props: {
|
||||
fallbackTitle?: string
|
||||
fallbackDirectory?: string
|
||||
onRename: (title: string) => Promise<void>
|
||||
onDelete: () => void
|
||||
onNavigate: (element: HTMLDivElement) => void
|
||||
onClose: () => void
|
||||
onIntent: (hovered: boolean) => void
|
||||
@@ -76,6 +84,7 @@ function SessionTabSlot(props: {
|
||||
fallbackTitle={props.fallbackTitle}
|
||||
fallbackDirectory={props.fallbackDirectory}
|
||||
onRename={props.onRename}
|
||||
onDelete={props.onDelete}
|
||||
onNavigate={() => props.onNavigate(ref)}
|
||||
onClose={props.onClose}
|
||||
active={props.active}
|
||||
@@ -100,6 +109,7 @@ function SessionTabEntry(props: {
|
||||
}) {
|
||||
const tabs = useTabs()
|
||||
const language = useLanguage()
|
||||
const dialog = useDialog()
|
||||
const sdk = createMemo(() => props.serverCtx?.sdk ?? null)
|
||||
const pending = createMemo(() => tabs.pendingSession(props.tab.server, props.tab.sessionId))
|
||||
const cachedSession = createMemo(() => props.serverCtx?.data.session.get(props.tab.sessionId))
|
||||
@@ -156,6 +166,49 @@ function SessionTabEntry(props: {
|
||||
}
|
||||
}
|
||||
|
||||
function DeleteDialog() {
|
||||
const confirm = async () => {
|
||||
const value = session()
|
||||
const ctx = props.serverCtx
|
||||
|
||||
dialog.close()
|
||||
|
||||
if (!value || !ctx) return
|
||||
const ids = sessionTreeIDs(ctx.data.session.list(), value.id)
|
||||
|
||||
try {
|
||||
await ctx.data.session.remove(value.id)
|
||||
notifySessionTabsRemoved({ server: props.tab.server, directory: value.location.directory, sessionIDs: ids })
|
||||
} catch (cause) {
|
||||
showToast({
|
||||
title: language.t("session.delete.failed.title"),
|
||||
description: errorMessage(cause, language.t("session.delete.failed.title")),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog fit>
|
||||
<DialogHeader hideClose>
|
||||
<DialogTitleGroup
|
||||
title={language.t("session.delete.title")}
|
||||
description={language.t("session.delete.confirm", {
|
||||
name: sessionTitle(session()?.title) ?? language.t("session.tab.session"),
|
||||
})}
|
||||
/>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="ghost" autofocus onClick={() => dialog.close()}>
|
||||
{language.t("common.cancel")}
|
||||
</Button>
|
||||
<Button variant="danger" onClick={confirm}>
|
||||
{language.t("session.delete.button")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
createEffect(() => props.onVisibleChange(visible()))
|
||||
createEffect(() => props.onUnreadChange(unread()))
|
||||
|
||||
@@ -232,6 +285,7 @@ function SessionTabEntry(props: {
|
||||
}
|
||||
fallbackDirectory={persisted()?.directory}
|
||||
onRename={rename}
|
||||
onDelete={() => dialog.show(() => <DeleteDialog />)}
|
||||
onNavigate={props.onNavigate}
|
||||
onClose={props.onClose}
|
||||
onIntent={hover}
|
||||
|
||||
@@ -67,6 +67,10 @@
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--menu-v2-fg);
|
||||
|
||||
&[data-variant="critical"] {
|
||||
--menu-v2-fg: var(--v2-state-fg-danger);
|
||||
}
|
||||
|
||||
[data-slot="menu-v2-item-content"] {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
@@ -227,6 +231,10 @@
|
||||
background: var(--surface-raised-base-hover);
|
||||
}
|
||||
|
||||
&[data-variant="critical"] {
|
||||
color: var(--text-on-critical-base);
|
||||
}
|
||||
|
||||
&[data-disabled] {
|
||||
color: var(--text-weak);
|
||||
opacity: 1;
|
||||
|
||||
Reference in New Issue
Block a user