diff options
| -rw-r--r-- | packages/app/src/context/global-sync.tsx | 14 | ||||
| -rw-r--r-- | packages/app/src/i18n/en.ts | 6 | ||||
| -rw-r--r-- | packages/app/src/pages/layout.tsx | 132 |
3 files changed, 137 insertions, 15 deletions
diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index 18eacbd60..4964ef6e4 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -475,6 +475,20 @@ function createGlobalSync() { ) break } + case "session.deleted": { + const result = Binary.search(store.session, event.properties.info.id, (s) => s.id) + if (result.found) { + setStore( + "session", + produce((draft) => { + draft.splice(result.index, 1) + }), + ) + } + if (event.properties.info.parentID) break + setStore("sessionTotal", (value) => Math.max(0, value - 1)) + break + } case "session.diff": setStore("session_diff", event.properties.sessionID, reconcile(event.properties.diff, { key: "file" })) break diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index f2ba88c59..8cb0c87ef 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -450,6 +450,7 @@ export const dict = { "common.learnMore": "Learn more", "common.rename": "Rename", "common.reset": "Reset", + "common.archive": "Archive", "common.delete": "Delete", "common.close": "Close", "common.edit": "Edit", @@ -627,6 +628,11 @@ export const dict = { "settings.permissions.tool.doom_loop.title": "Doom Loop", "settings.permissions.tool.doom_loop.description": "Detect repeated tool calls with identical input", + "session.delete.failed.title": "Failed to delete session", + "session.delete.title": "Delete session", + "session.delete.confirm": 'Delete session "{{name}}"?', + "session.delete.button": "Delete session", + "workspace.new": "New workspace", "workspace.type.local": "local", "workspace.type.sandbox": "sandbox", diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 41a76b4d6..685398f7d 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -819,6 +819,49 @@ export default function Layout(props: ParentProps) { } } + async function deleteSession(session: Session) { + const [store, setStore] = globalSync.child(session.directory) + const sessions = (store.session ?? []).filter((s) => !s.parentID && !s.time?.archived) + const index = sessions.findIndex((s) => s.id === session.id) + const nextSession = sessions[index + 1] ?? sessions[index - 1] + + const result = await globalSDK.client.session + .delete({ directory: session.directory, sessionID: session.id }) + .then((x) => x.data) + .catch((err) => { + showToast({ + title: language.t("session.delete.failed.title"), + description: errorMessage(err), + }) + return false + }) + + if (!result) return + + setStore( + produce((draft) => { + const removed = new Set<string>([session.id]) + const collect = (parentID: string) => { + for (const item of draft.session) { + if (item.parentID !== parentID) continue + removed.add(item.id) + collect(item.id) + } + } + collect(session.id) + draft.session = draft.session.filter((s) => !removed.has(s.id)) + }), + ) + + if (session.id === params.id) { + if (nextSession) { + navigate(`/${params.dir}/session/${nextSession.id}`) + } else { + navigate(`/${params.dir}/session`) + } + } + } + command.register(() => { const commands: CommandOption[] = [ { @@ -1145,6 +1188,33 @@ export default function Layout(props: ParentProps) { }) } + function DialogDeleteSession(props: { session: Session }) { + const handleDelete = async () => { + await deleteSession(props.session) + dialog.close() + } + + return ( + <Dialog title={language.t("session.delete.title")} fit> + <div class="flex flex-col gap-4 pl-6 pr-2.5 pb-3"> + <div class="flex flex-col gap-1"> + <span class="text-14-regular text-text-strong"> + {language.t("session.delete.confirm", { name: props.session.title })} + </span> + </div> + <div class="flex justify-end gap-2"> + <Button variant="ghost" size="large" onClick={() => dialog.close()}> + {language.t("common.cancel")} + </Button> + <Button variant="primary" size="large" onClick={handleDelete}> + {language.t("session.delete.button")} + </Button> + </div> + </div> + </Dialog> + ) + } + function DialogDeleteWorkspace(props: { directory: string }) { const name = createMemo(() => getFilename(props.directory)) const [data, setData] = createStore({ @@ -1485,6 +1555,8 @@ export default function Layout(props: ParentProps) { const hoverAllowed = createMemo(() => !props.mobile && layout.sidebar.opened()) const hoverEnabled = createMemo(() => (props.popover ?? true) && hoverAllowed()) const isActive = createMemo(() => props.session.id === params.id) + const [menuOpen, setMenuOpen] = createSignal(false) + const [pendingRename, setPendingRename] = createSignal(false) const messageLabel = (message: Message) => { const parts = sessionStore.part[message.id] ?? [] @@ -1495,7 +1567,7 @@ export default function Layout(props: ParentProps) { const item = ( <A href={`${props.slug}/session/${props.session.id}`} - class={`flex items-center justify-between gap-3 min-w-0 text-left w-full focus:outline-none transition-[padding] group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7 ${props.dense ? "py-0.5" : "py-1"}`} + class={`flex items-center justify-between gap-3 min-w-0 text-left w-full focus:outline-none transition-[padding] ${menuOpen() ? "pr-7" : ""} group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7 ${props.dense ? "py-0.5" : "py-1"}`} onMouseEnter={() => prefetchSession(props.session, "high")} onFocus={() => prefetchSession(props.session, "high")} onClick={() => setHoverSession(undefined)} @@ -1588,21 +1660,51 @@ export default function Layout(props: ParentProps) { </HoverCard> </Show> <div - class={`hidden group-hover/session:flex group-active/session:flex group-focus-within/session:flex text-text-base gap-1 items-center absolute ${props.dense ? "top-0.5 right-0.5" : "top-1 right-1"}`} + class={`absolute ${props.dense ? "top-0.5 right-0.5" : "top-1 right-1"} flex items-center gap-0.5 transition-opacity`} + classList={{ + "opacity-100 pointer-events-auto": menuOpen(), + "opacity-0 pointer-events-none": !menuOpen(), + "group-hover/session:opacity-100 group-hover/session:pointer-events-auto": true, + "group-focus-within/session:opacity-100 group-focus-within/session:pointer-events-auto": true, + }} > - <TooltipKeybind - placement={props.mobile ? "bottom" : "right"} - title={language.t("command.session.archive")} - keybind={command.keybind("session.archive")} - gutter={8} - > - <IconButton - icon="archive" - variant="ghost" - onClick={() => archiveSession(props.session)} - aria-label={language.t("command.session.archive")} - /> - </TooltipKeybind> + <DropdownMenu open={menuOpen()} onOpenChange={setMenuOpen}> + <Tooltip value={language.t("common.moreOptions")} placement="top"> + <DropdownMenu.Trigger + as={IconButton} + icon="dot-grid" + variant="ghost" + class="size-6 rounded-md data-[expanded]:bg-surface-base-active" + aria-label={language.t("common.moreOptions")} + /> + </Tooltip> + <DropdownMenu.Portal> + <DropdownMenu.Content + onCloseAutoFocus={(event) => { + if (!pendingRename()) return + event.preventDefault() + setPendingRename(false) + openEditor(`session:${props.session.id}`, props.session.title) + }} + > + <DropdownMenu.Item + onSelect={() => { + setPendingRename(true) + setMenuOpen(false) + }} + > + <DropdownMenu.ItemLabel>{language.t("common.rename")}</DropdownMenu.ItemLabel> + </DropdownMenu.Item> + <DropdownMenu.Item onSelect={() => archiveSession(props.session)}> + <DropdownMenu.ItemLabel>{language.t("common.archive")}</DropdownMenu.ItemLabel> + </DropdownMenu.Item> + <DropdownMenu.Separator /> + <DropdownMenu.Item onSelect={() => dialog.show(() => <DialogDeleteSession session={props.session} />)}> + <DropdownMenu.ItemLabel>{language.t("common.delete")}</DropdownMenu.ItemLabel> + </DropdownMenu.Item> + </DropdownMenu.Content> + </DropdownMenu.Portal> + </DropdownMenu> </div> </div> ) |
