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 }, + ); }, }); }