From 3292ff146c7f0fc1b0ef707a7f0c3d77bb2de4d2 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 14 Aug 2026 11:09:02 -0700 Subject: [PATCH] fix(ui): keep the archive Undo working after leaving the Sessions page (#123749) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The archive toast outlives the Sessions page (toast host is app-shell level), but its Undo action was gated on the page's request scope, which dies the moment the operator navigates away — clicking Undo within the 6s window then silently did nothing. Run the un-archive through the shared sessions mutations store directly (it already fails closed on connection replacement and publishes errors), mirroring the always-mounted sidebar undo sibling. --- ui/src/pages/sessions/sessions-page.test.ts | 34 +++++++++++++++++++++ ui/src/pages/sessions/sessions-page.ts | 21 ++++++------- 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/ui/src/pages/sessions/sessions-page.test.ts b/ui/src/pages/sessions/sessions-page.test.ts index 06b5afac2959..7dc3fe07039b 100644 --- a/ui/src/pages/sessions/sessions-page.test.ts +++ b/ui/src/pages/sessions/sessions-page.test.ts @@ -131,6 +131,40 @@ describe("sessions page lifecycle", () => { ); }); + it("keeps the archive Undo working after navigating off the Sessions page", async () => { + const key = "agent:main:navigated"; + const patch = vi.fn(async () => ({ + ok: true as const, + path: "", + key, + entry: { sessionId: key }, + })); + const sessions = createSessions({ patch }); + const mutableGateway = createGateway({} as GatewayBrowserClient); + mutableGateway.emit({ sessionKey: key }); + const page = await createPage(createContext(mutableGateway.gateway, sessions)); + const toast = document.createElement("openclaw-toast-host"); + document.body.append(toast); + await toast.updateComplete; + + await page.archiveSessionWithUndo({ + key, + sessionId: "session-nav", + pinned: false, + } as GatewaySessionRow); + await toast.updateComplete; + // The toast host outlives the page; navigation unmounts the page element. + page.remove(); + toast.querySelector(".app-toast__action")?.click(); + await vi.waitFor(() => expect(patch).toHaveBeenCalledTimes(2)); + expect(patch).toHaveBeenNthCalledWith( + 2, + key, + { archived: false }, + { agentId: undefined, expectedSessionId: "session-nav" }, + ); + }); + it("submits one trimmed bounded transcript search and adopts its status", async () => { const response = deferred(); const request = vi.fn(() => response.promise); diff --git a/ui/src/pages/sessions/sessions-page.ts b/ui/src/pages/sessions/sessions-page.ts index b7d9bc4c4ae5..1f33d18e6573 100644 --- a/ui/src/pages/sessions/sessions-page.ts +++ b/ui/src/pages/sessions/sessions-page.ts @@ -1193,21 +1193,20 @@ class SessionsPage extends OpenClawLightDomElement { if (result !== "completed" || !this.isRequestScopeCurrent(scope)) { return; } + // Undo is captured before showing the toast: the toast host outlives this + // page, so the action must run against the shared mutations store (which + // fails closed on connection replacement) rather than page scope — a + // page-scope check would silently no-op after navigating away. + const agentId = this.sessionAgentId(row.key, scope.context); showToast({ message: t("sessionsView.sessionArchived"), actionLabel: t("common.undo"), onAction: () => { - void (async () => { - if (!this.isRequestScopeCurrent(scope)) { - return; - } - await this.patchSession( - row.key, - { archived: false, ...(row.pinned === true ? { pinned: true } : {}) }, - scope, - row.sessionId, - ); - })(); + void scope.sessions.patch( + row.key, + { archived: false, ...(row.pinned === true ? { pinned: true } : {}) }, + { agentId, expectedSessionId: row.sessionId }, + ); }, }); }