From f09ec76de4b4556e75b909855f8ee84f004a7c50 Mon Sep 17 00:00:00 2001 From: RoboClaw Date: Wed, 26 Aug 2026 07:06:06 -0700 Subject: [PATCH] fix(ui): keep limited-access dismissal through reconnect (#130127) * fix(ui): preserve access snooze through reconnect Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com> * test(commands): preserve fs-safe mock exports --------- Co-authored-by: roboclaw-bot <309084314+roboclaw-bot@users.noreply.github.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com> --- ui/src/components/sidebar-attention.ts | 8 ++++++-- ui/src/e2e/device-scope-upgrade.e2e.test.ts | 17 ++++++++++++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/ui/src/components/sidebar-attention.ts b/ui/src/components/sidebar-attention.ts index ba465aa1eba0..69f89e7080bc 100644 --- a/ui/src/components/sidebar-attention.ts +++ b/ui/src/components/sidebar-attention.ts @@ -374,11 +374,15 @@ class SidebarAttention extends OpenClawLightDomElement { if (!this.dismissedScope || !this.context) { return; } + const snapshot = this.context.gateway.snapshot; + const scopes = snapshot.hello?.auth?.scopes; const entry = buildScopeUpgradeInboxEntry({ - scopes: this.context.gateway.snapshot.hello?.auth?.scopes, + scopes, state: this.context.scopeUpgrade.state, }); - if (!entry?.dismissal) { + // A disconnect makes access unresolved, not resolved. Keep the snooze until + // connected scope facts or an active request lifecycle authoritatively retire it. + if (snapshot.phase === "connected" && scopes !== undefined && !entry?.dismissal) { this.dismissed = clearSidebarAttentionDismissal(this.dismissedScope, "scopeUpgrade"); } } diff --git a/ui/src/e2e/device-scope-upgrade.e2e.test.ts b/ui/src/e2e/device-scope-upgrade.e2e.test.ts index 42b268a4127c..1efd18f8d9ad 100644 --- a/ui/src/e2e/device-scope-upgrade.e2e.test.ts +++ b/ui/src/e2e/device-scope-upgrade.e2e.test.ts @@ -44,6 +44,15 @@ function requireRecord(value: unknown): Record { return value as Record; } +async function gatewayPhase(page: Page): Promise { + return page.evaluate(() => { + const app = document.querySelector("openclaw-app") as HTMLElement & { + runtime?: { context: { gateway: { snapshot: { phase: string } } } }; + }; + return app.runtime?.context.gateway.snapshot.phase; + }); +} + async function captureProof(page: Page, name: string): Promise { if (!proofDir) { return; @@ -134,7 +143,7 @@ describeControlUiE2e("Control UI live device scope upgrade", () => { it("moves limited access into the Inbox and persists its dismissal", async () => { const desktopContext = await createContext(); const desktop = await desktopContext.newPage(); - await installMockGateway(desktop, { operatorScopes: LIMITED_SCOPES }); + const gateway = await installMockGateway(desktop, { operatorScopes: LIMITED_SCOPES }); await desktop.goto(`${server.baseUrl}activity`); expect(await desktop.locator(".scope-upgrade-status-trigger").count()).toBe(0); @@ -151,6 +160,12 @@ describeControlUiE2e("Control UI live device scope upgrade", () => { await desktopPanel.getByRole("tab", { name: "System", exact: true }).waitFor(); await captureProof(desktop, "desktop-inbox-limited-access-dismissed.png"); + await gateway.setOnline(false); + await expect.poll(() => gatewayPhase(desktop)).toBe("reconnecting"); + await gateway.setOnline(true); + await expect.poll(() => gatewayPhase(desktop)).toBe("connected"); + await expect.poll(() => desktopInbox.getAttribute("aria-label")).toBe("0 inbox items"); + await desktop.reload(); await desktop.locator("openclaw-app-shell").waitFor(); await expect.poll(() => desktopInbox.getAttribute("aria-label")).toBe("0 inbox items");