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([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 ( + +
+
+ + {language.t("session.delete.confirm", { name: props.session.title })} + +
+
+ + +
+
+
+ ) + } + 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 = ( prefetchSession(props.session, "high")} onFocus={() => prefetchSession(props.session, "high")} onClick={() => setHoverSession(undefined)} @@ -1588,21 +1660,51 @@ export default function Layout(props: ParentProps) {
- - archiveSession(props.session)} - aria-label={language.t("command.session.archive")} - /> - + + + + + + { + if (!pendingRename()) return + event.preventDefault() + setPendingRename(false) + openEditor(`session:${props.session.id}`, props.session.title) + }} + > + { + setPendingRename(true) + setMenuOpen(false) + }} + > + {language.t("common.rename")} + + archiveSession(props.session)}> + {language.t("common.archive")} + + + dialog.show(() => )}> + {language.t("common.delete")} + + + +
)