From 81fb7e69c4bb65e752b246bd476573f60b448d97 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 26 Aug 2026 01:12:29 -0700 Subject: [PATCH] improve(ui): fold the sidebar update action into the Inbox (#129977) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * improve(ui): fold the sidebar update action into the Inbox The footer update circle duplicated the Inbox's update entry: same attention state, same dismissal, one extra permanently-tinted control plus layout shims reserving space for it. The update now surfaces only as an Inbox entry — badge for attention, full action row (update, hold, review, dismiss-until-restart) in the panel. Deletes the footer button, its hover-dismiss, sidebar-footer-update.css, and the 80px/88px width compensations; moves the floating-cluster rules to sidebar-attention-floating.css and the build-chip text rules next to their base class in layout.css. Removes the dead startSidebarUpdateAttention shortcut and two orphaned i18n keys. * fix(ui): integrate #129920 top-left alignment with the Inbox-only footer Ports the web-chrome floating-cluster geometry to sidebar-attention-floating.css and points the titlebar alignment test at the always-present Inbox button; the update no longer changes footer geometry, so its scenario plumbing goes. --- ui/config/control-ui-boot-modules.json | 9 +- .../sidebar-attention-layout.browser.test.ts | 2 +- ui/src/components/sidebar-attention-update.ts | 21 -- ui/src/components/sidebar-attention.test.ts | 30 --- ui/src/components/sidebar-attention.ts | 52 +---- ui/src/components/sidebar-build-chip.ts | 1 - .../e2e/chat-side-panel-clearance.e2e.test.ts | 28 --- .../e2e/native-nav-sidebar-toggle.e2e.test.ts | 15 +- ...idebar-attention-scope.e2e.test-support.ts | 2 +- ui/src/e2e/sidebar-customization.e2e.test.ts | 2 +- ui/src/e2e/update-coalesced.e2e.test.ts | 4 +- ui/src/e2e/update-confirmation.e2e.test.ts | 130 ++----------- ui/src/i18n/locales/en.ts | 2 - ui/src/styles/layout.css | 10 + ui/src/styles/sidebar-attention-floating.css | 54 ++++++ ui/src/styles/sidebar-footer-update.css | 179 ------------------ 16 files changed, 100 insertions(+), 441 deletions(-) create mode 100644 ui/src/styles/sidebar-attention-floating.css delete mode 100644 ui/src/styles/sidebar-footer-update.css diff --git a/ui/config/control-ui-boot-modules.json b/ui/config/control-ui-boot-modules.json index 0099329e7c43..c1932e2acd6c 100644 --- a/ui/config/control-ui-boot-modules.json +++ b/ui/config/control-ui-boot-modules.json @@ -995,6 +995,7 @@ "src/agents/tools/automations-tool-name.ts", "src/auto-reply/commands-args.ts", "src/auto-reply/commands-registry.shared.ts", + "src/auto-reply/heartbeat.ts", "src/auto-reply/reply/delivery-hints.ts", "src/auto-reply/reply/inbound-context-marker.ts", "src/auto-reply/reply/queue/normalize.ts", @@ -1251,6 +1252,7 @@ "ui/src/components/session-icon-picker.ts", "ui/src/components/session-leading-indicator.ts", "ui/src/components/session-menu-access.ts", + "ui/src/components/session-menu-actions.ts", "ui/src/components/session-menu-compact.ts", "ui/src/components/session-menu-options.ts", "ui/src/components/session-menu-work.ts", @@ -1269,7 +1271,9 @@ "ui/src/components/settings-ui.ts", "ui/src/components/sidebar-agent-card.ts", "ui/src/components/sidebar-attention-dismissals.ts", + "ui/src/components/sidebar-attention-entries.ts", "ui/src/components/sidebar-attention-items.ts", + "ui/src/components/sidebar-attention-update.ts", "ui/src/components/sidebar-attention.ts", "ui/src/components/sidebar-build-chip-format.ts", "ui/src/components/sidebar-build-chip.ts", @@ -1552,6 +1556,7 @@ "ui/src/pages/chat/chat-thread-items.ts", "ui/src/pages/chat/chat-thread-run-identity.ts", "ui/src/pages/chat/chat-thread.ts", + "ui/src/pages/chat/chat-tool-activity-coalesce.ts", "ui/src/pages/chat/chat-turn-boundary.ts", "ui/src/pages/chat/chat-typing-presence.ts", "ui/src/pages/chat/chat-view-notices.ts", @@ -1560,6 +1565,7 @@ "ui/src/pages/chat/chat-viewer-presence.ts", "ui/src/pages/chat/components/chat-agent-run-frame.ts", "ui/src/pages/chat/components/chat-attachment-admission.ts", + "ui/src/pages/chat/components/chat-attachment-file.ts", "ui/src/pages/chat/components/chat-attachment-href.ts", "ui/src/pages/chat/components/chat-attachments.ts", "ui/src/pages/chat/components/chat-audio-coordinator.ts", @@ -1659,6 +1665,7 @@ "ui/src/pages/chat/components/widget-export.ts", "ui/src/pages/chat/composer-capability-catalog.ts", "ui/src/pages/chat/composer-dictation.ts", + "ui/src/pages/chat/composer-microphone-picker.ts", "ui/src/pages/chat/composer-persistence.ts", "ui/src/pages/chat/connect-error.ts", "ui/src/pages/chat/continue-in-terminal-command.ts", @@ -1764,5 +1771,5 @@ "ui/src/pages/worktrees/route.ts", "ui/src/styles/chat.css", "ui/src/styles/lobster-pet.css", - "ui/src/styles/sidebar-footer-update.css" + "ui/src/styles/sidebar-attention-floating.css" ] diff --git a/ui/src/components/sidebar-attention-layout.browser.test.ts b/ui/src/components/sidebar-attention-layout.browser.test.ts index 3d2852cf11e5..031a15c82cb6 100644 --- a/ui/src/components/sidebar-attention-layout.browser.test.ts +++ b/ui/src/components/sidebar-attention-layout.browser.test.ts @@ -1,7 +1,7 @@ import { afterEach, describe, expect, it } from "vitest"; import "../test-helpers/load-styles.ts"; import "../styles/hub-tabs.css"; -import "../styles/sidebar-footer-update.css"; +import "../styles/sidebar-attention-floating.css"; import "../styles/sidebar-issues.css"; import "./web-awesome-tabs.ts"; // Upgrade the real element: the floating layout once regressed because a base diff --git a/ui/src/components/sidebar-attention-update.ts b/ui/src/components/sidebar-attention-update.ts index f00595b43f03..2e889d4a37bf 100644 --- a/ui/src/components/sidebar-attention-update.ts +++ b/ui/src/components/sidebar-attention-update.ts @@ -1,6 +1,4 @@ import type { ApplicationContext } from "../app/context.ts"; -import { hasNativeUpdateBridge } from "../app/native-link-routing.ts"; -import { confirmAndStartUpdate, type UpdateProgress } from "../app/update-confirmation.ts"; import { isUpdateActionable } from "../app/update-overlay-helpers.ts"; import { canCallGatewayMethod } from "../lib/gateway-methods.ts"; import { @@ -66,22 +64,3 @@ export function resolveSidebarUpdateAttention( present, }; } - -export function startSidebarUpdateAttention(params: { - context: SidebarUpdateContext; - nativeUpdateDeclined: boolean; - watchUpdateProgress?: (listener: (progress: UpdateProgress) => void) => () => void; -}) { - const state = resolveSidebarUpdateAttention(params.context); - if (!state.actionable || state.busy || !state.canUpdate) { - return; - } - const snapshot = params.context.overlays.snapshot; - void confirmAndStartUpdate({ - startGatewayUpdate: () => void params.context.overlays.runUpdate(), - ...(params.watchUpdateProgress ? { watchUpdateProgress: params.watchUpdateProgress } : {}), - updateAvailable: snapshot.updateAvailable, - updateSchedule: snapshot.updateSchedule, - viaNativeApp: !params.nativeUpdateDeclined && hasNativeUpdateBridge(), - }); -} diff --git a/ui/src/components/sidebar-attention.test.ts b/ui/src/components/sidebar-attention.test.ts index a5c626413046..88d29c63d71e 100644 --- a/ui/src/components/sidebar-attention.test.ts +++ b/ui/src/components/sidebar-attention.test.ts @@ -69,7 +69,6 @@ type SidebarAttentionElement = HTMLElement & { context: ApplicationContext; updateComplete: Promise; cronJobs: CronJob[]; - startUpdate(): void; modelAuthStatus: ModelAuthStatusResult | null; loadedAtMs: number; }; @@ -662,35 +661,6 @@ describe("update attention", () => { expect(Boolean(entry?.dismissal)).toBe(dismissible); }); - - it("does not start an update when a failure has no actionable target", () => { - const runUpdate = vi.fn(); - const element = document.createElement("openclaw-sidebar-attention") as SidebarAttentionElement; - element.context = { - gateway: { - snapshot: { - phase: "connected", - hello: { - auth: { role: "operator", scopes: ["operator.admin"] }, - features: { methods: ["update.run"] }, - }, - }, - }, - overlays: { - runUpdate, - snapshot: { - updateAvailable: null, - updateSchedule: null, - updateRunning: false, - updateStatusBanner: { tone: "danger", text: "Update failed" }, - }, - }, - } as unknown as ApplicationContext; - - element.startUpdate(); - - expect(runUpdate).not.toHaveBeenCalled(); - }); }); describe("reconcileSidebarAttentionDismissals", () => { diff --git a/ui/src/components/sidebar-attention.ts b/ui/src/components/sidebar-attention.ts index e81571a8864a..ba465aa1eba0 100644 --- a/ui/src/components/sidebar-attention.ts +++ b/ui/src/components/sidebar-attention.ts @@ -20,7 +20,7 @@ import { loadModelAuthStatus } from "../lib/model-auth.ts"; import { normalizeAgentId } from "../lib/sessions/session-key.ts"; import { OpenClawLightDomElement } from "../lit/openclaw-element.ts"; import { SubscriptionsController } from "../lit/subscriptions-controller.ts"; -import "../styles/sidebar-footer-update.css"; +import "../styles/sidebar-attention-floating.css"; import { icons } from "./icons.ts"; import { CUSTODIAN_PANEL_TOGGLE_EVENT } from "./panel-toggle-contract.ts"; import { @@ -46,10 +46,7 @@ import { compareSidebarAttentionEntries, } from "./sidebar-attention-items.ts"; import type { SidebarAttentionPanelPosition } from "./sidebar-attention-panel.runtime.ts"; -import { - resolveSidebarUpdateAttention, - startSidebarUpdateAttention, -} from "./sidebar-attention-update.ts"; +import { resolveSidebarUpdateAttention } from "./sidebar-attention-update.ts"; import type { IssueTab } from "./sidebar-issues-tabs.ts"; import "./tooltip.ts"; @@ -454,16 +451,6 @@ class SidebarAttention extends OpenClawLightDomElement { ); } - private readonly startUpdate = () => { - if (this.context) { - startSidebarUpdateAttention({ - context: this.context, - nativeUpdateDeclined: this.nativeUpdateDeclined, - watchUpdateProgress: this.watchUpdateProgress, - }); - } - }; - private readonly closeOnOutsidePointer = (event: PointerEvent) => { if (!this.panelOpen || event.composedPath().includes(this)) { return; @@ -620,9 +607,6 @@ class SidebarAttention extends OpenClawLightDomElement { return nothing; } const entries = this.currentInboxEntries(); - const updateEntry = entries.find((entry) => entry.type === "update"); - const updateDismissal = updateEntry?.dismissal ?? null; - const updateState = resolveSidebarUpdateAttention(this.context); const count = sidebarInboxTabCounts(entries).all; const label = t(count === 1 ? "attention.issueCount" : "attention.issueCountPlural", { count: String(count), @@ -655,38 +639,6 @@ class SidebarAttention extends OpenClawLightDomElement { >` : nothing} - ${updateEntry - ? html` - - ${updateDismissal - ? html` - - ` - : nothing} - ` - : nothing} ${this.panelOpen && this.panelRenderer ? this.panelRenderer({ context: this.context, diff --git a/ui/src/components/sidebar-build-chip.ts b/ui/src/components/sidebar-build-chip.ts index 26111ff72fa7..8db36364bcf3 100644 --- a/ui/src/components/sidebar-build-chip.ts +++ b/ui/src/components/sidebar-build-chip.ts @@ -5,7 +5,6 @@ import { CONTROL_UI_BUILD_INFO } from "../build-info.ts"; import { t } from "../i18n/index.ts"; import { shouldHandleNavigationClick } from "../lib/navigation-click.ts"; import { OpenClawLightDomContentsElement } from "../lit/openclaw-element.ts"; -import "../styles/sidebar-footer-update.css"; import { formatBuildChipText, formatSettingsBuildLabel, diff --git a/ui/src/e2e/chat-side-panel-clearance.e2e.test.ts b/ui/src/e2e/chat-side-panel-clearance.e2e.test.ts index 7dc77e9c31ee..b8cede2bebf1 100644 --- a/ui/src/e2e/chat-side-panel-clearance.e2e.test.ts +++ b/ui/src/e2e/chat-side-panel-clearance.e2e.test.ts @@ -40,7 +40,6 @@ function scenario( options: { custodian?: boolean; operatorScopes?: string[]; - updateAvailable?: ControlUiMockGatewayScenario["updateAvailable"]; } = {}, ): ControlUiMockGatewayScenario { return { @@ -84,7 +83,6 @@ function scenario( }, }, ...(options.operatorScopes ? { operatorScopes: options.operatorScopes } : {}), - ...(options.updateAvailable ? { updateAvailable: options.updateAvailable } : {}), sessionKey, workspace: "/workspace/openclaw", workspaceGit: true, @@ -210,13 +208,11 @@ suite.define(() => { deviceLess: false, direction: "ltr", expectedControl: ".shell-chrome-controls__search", - expectedUpdate: false, name: "expanded navigation", navCollapsed: false, operatorScopes: undefined, proof: "expanded-nav", themeMode: "dark" as const, - updateAvailable: undefined, }, { beforeExpandProof: undefined, @@ -224,13 +220,11 @@ suite.define(() => { deviceLess: false, direction: "ltr", expectedControl: ".shell-chrome-controls__search", - expectedUpdate: false, name: "collapsed navigation", navCollapsed: true, operatorScopes: undefined, proof: "collapsed-nav", themeMode: "dark" as const, - updateAvailable: undefined, }, { beforeExpandProof: undefined, @@ -238,17 +232,11 @@ suite.define(() => { deviceLess: false, direction: "ltr", expectedControl: ".shell-chrome-controls__custodian", - expectedUpdate: true, name: "collapsed navigation with custodian and attention", navCollapsed: true, operatorScopes: undefined, proof: "collapsed-nav-custodian-attention", themeMode: "dark" as const, - updateAvailable: { - channel: "stable", - currentVersion: "2026.8.1", - latestVersion: "2026.8.2", - }, }, { beforeExpandProof: undefined, @@ -256,13 +244,11 @@ suite.define(() => { deviceLess: true, direction: "rtl", expectedControl: ".sidebar-attention--floating .sidebar-issues-button", - expectedUpdate: false, name: "collapsed RTL limited-access status and attention", navCollapsed: true, operatorScopes: limitedScopes, proof: "collapsed-rtl-limited-attention", themeMode: "dark" as const, - updateAvailable: undefined, }, ])("keeps expanded panel controls in a compact safe gap for $name", async (testCase) => { await suite.withPage( @@ -282,7 +268,6 @@ suite.define(() => { scenario({ custodian: testCase.custodian, operatorScopes: testCase.operatorScopes, - updateAvailable: testCase.updateAvailable, }), ); await openExpandedFilesPanel(page, testCase.beforeExpandProof); @@ -297,19 +282,6 @@ suite.define(() => { await page.locator(".sidebar-attention--floating .sidebar-issues-button").waitFor(); } await page.locator(testCase.expectedControl).waitFor(); - if (testCase.expectedUpdate) { - const updateSlot = page.locator( - ".sidebar-attention--floating .sidebar-footer-update-slot", - ); - await updateSlot.waitFor(); - await updateSlot.hover(); - await waitForShellLayout(page); - await expectPanelHeaderControlsClearShellChrome(page); - await page.mouse.move(800, 700); - await updateSlot.locator(".sidebar-footer-update").focus(); - await waitForShellLayout(page); - await expectPanelHeaderControlsClearShellChrome(page); - } await waitForShellLayout(page); await expectPanelHeaderControlsClearShellChrome(page); await capturePanel(page, testCase.proof); diff --git a/ui/src/e2e/native-nav-sidebar-toggle.e2e.test.ts b/ui/src/e2e/native-nav-sidebar-toggle.e2e.test.ts index bfcb793d811f..e1b4b822e103 100644 --- a/ui/src/e2e/native-nav-sidebar-toggle.e2e.test.ts +++ b/ui/src/e2e/native-nav-sidebar-toggle.e2e.test.ts @@ -24,11 +24,6 @@ const suite = createControlUiE2eSuite({ const TOAST_PROOF_DIR = path.resolve(".artifacts/control-ui-e2e/toast-layering"); const railProofDir = process.env.OPENCLAW_UI_RAIL_PROOF_DIR?.trim(); const limitedScopes = ["operator.read", "operator.write"]; -const UPDATE_AVAILABLE = { - channel: "stable", - currentVersion: "1.0.0", - latestVersion: "2.0.0", -} as const; const TOAST_SCENARIO: ControlUiMockGatewayScenario = { featureMethods: ["chat.metadata", "chat.startup", "sessions.catalog.list"], methodResponses: { @@ -268,13 +263,7 @@ suite.define(() => { }); it("hosts navigation, search, sessions, and history in web titlebar chrome", async () => { - const page = await openPage({ - scenario: { - operatorScopes: ["operator.admin", "operator.read"], - updateAvailable: UPDATE_AVAILABLE, - }, - webChrome: true, - }); + const page = await openPage({ webChrome: true }); const toolbar = page.locator(".macos-titlebar-controls"); await expect.poll(() => toolbar.isVisible()).toBe(true); await expect.poll(() => page.locator(".shell-chrome-controls").isVisible()).toBe(false); @@ -293,7 +282,7 @@ suite.define(() => { .poll(() => page.locator(".shell").getAttribute("class")) .toContain("shell--nav-collapsed"); await expect.poll(() => newThread.isVisible()).toBe(true); - await page.locator(".sidebar-attention--floating .sidebar-footer-update").waitFor(); + await page.locator(".sidebar-attention--floating .sidebar-issues-button").waitFor(); const toolbarBox = await toolbar.boundingBox(); const attentionBox = await page.locator(".sidebar-attention--floating").boundingBox(); expect(toolbarBox).not.toBeNull(); diff --git a/ui/src/e2e/sidebar-attention-scope.e2e.test-support.ts b/ui/src/e2e/sidebar-attention-scope.e2e.test-support.ts index 5c49717e9f58..ab0ebfd705ab 100644 --- a/ui/src/e2e/sidebar-attention-scope.e2e.test-support.ts +++ b/ui/src/e2e/sidebar-attention-scope.e2e.test-support.ts @@ -275,7 +275,7 @@ export async function runSidebarAttentionScopeFlow(params: SidebarAttentionScope const sidebarUpdate = sidebar.locator( 'openclaw-sidebar-update-card[data-attention-kind="updateAvailable"]', ); - await expect.poll(() => sidebar.locator(".sidebar-footer-update").count()).toBe(1); + await expect.poll(() => sidebar.locator(".sidebar-issues-button__count").count()).toBe(1); await sidebar.locator(".sidebar-issues-button").click(); await expect.poll(() => sidebarUpdate.count()).toBe(1); await expect.poll(() => automationRows.getByText("Writer release digest").count()).toBe(1); diff --git a/ui/src/e2e/sidebar-customization.e2e.test.ts b/ui/src/e2e/sidebar-customization.e2e.test.ts index 60de03d58496..f6bba5f600c7 100644 --- a/ui/src/e2e/sidebar-customization.e2e.test.ts +++ b/ui/src/e2e/sidebar-customization.e2e.test.ts @@ -856,7 +856,7 @@ suite.define(() => { 'openclaw-sidebar-update-card[data-attention-kind="updateAvailable"]', ); const sidebarAutomation = sidebar.locator('[data-attention-kind="cronFailed"]'); - await expect.poll(() => sidebar.locator(".sidebar-footer-update").count()).toBe(1); + await expect.poll(() => sidebar.locator(".sidebar-issues-button__count").count()).toBe(1); await sidebar.locator(".sidebar-issues-button").click(); await expect.poll(() => sidebarUpdate.count()).toBe(1); await expect.poll(() => sidebarAutomation.count()).toBe(1); diff --git a/ui/src/e2e/update-coalesced.e2e.test.ts b/ui/src/e2e/update-coalesced.e2e.test.ts index 3f2e0af8b7f1..ed1ea769b923 100644 --- a/ui/src/e2e/update-coalesced.e2e.test.ts +++ b/ui/src/e2e/update-coalesced.e2e.test.ts @@ -152,7 +152,7 @@ suite.define(() => { ); await updateIssue.locator("summary").click(); await updateIssue.locator(".sidebar-update-card__compact-reason").waitFor(); - expect(await page.locator(".sidebar-footer-update").count()).toBe(1); + expect(await page.locator(".sidebar-issues-button__count").count()).toBe(1); expect(pageErrors).toEqual([]); await captureUpdateProof(page, artifactDir, "package-update-failure.png"); }, @@ -212,7 +212,7 @@ suite.define(() => { { exact: true }, ) .waitFor(); - expect(await page.locator(".sidebar-footer-update").count()).toBe(1); + expect(await page.locator(".sidebar-issues-button__count").count()).toBe(1); expect(pageErrors).toEqual([]); await captureUpdateProof(page, artifactDir, "coalesced-restart-banner.png"); }, diff --git a/ui/src/e2e/update-confirmation.e2e.test.ts b/ui/src/e2e/update-confirmation.e2e.test.ts index 31b4ac1f5440..0f3898cba63a 100644 --- a/ui/src/e2e/update-confirmation.e2e.test.ts +++ b/ui/src/e2e/update-confirmation.e2e.test.ts @@ -69,7 +69,7 @@ async function openConfirmation(page: Page, updateButton: Locator) { } suite.define(() => { - it("shares one dismissal across the footer and Inbox until the Gateway boot changes", async () => { + it("keeps a dismissed Inbox update hidden until the Gateway boot changes", async () => { await suite.withPage( { locale: "en-US", serviceWorkers: "block", viewport: { height: 720, width: 1280 } }, async ({ page }) => { @@ -86,9 +86,9 @@ suite.define(() => { expect((await page.goto(`${suite.server.baseUrl}chat`))?.status()).toBe(200); await gateway.waitForRequest("chat.startup"); - const footerUpdate = page.locator(".sidebar-footer-update"); - await footerUpdate.waitFor(); - expect(await footerUpdate.isEnabled()).toBe(true); + // The Inbox is the sole update surface; the badge carries the attention. + const inboxBadge = page.locator(".sidebar-issues-button__count"); + await inboxBadge.waitFor(); expect( await page .locator("openclaw-sidebar-attention") @@ -97,62 +97,22 @@ suite.define(() => { .map((child) => child.className) .filter((className) => typeof className === "string" && className.length > 0), ), - ).toEqual(["sr-only", "sidebar-issues-button", "sidebar-footer-update-slot"]); - expect((await footerUpdate.boundingBox())?.width).toBe(32); - expect(await page.locator(".sidebar-footer-update__label").isVisible()).toBe(false); - const inboxBox = await page.locator(".sidebar-issues-button").boundingBox(); - const updateSlotBox = await page.locator(".sidebar-footer-update-slot").boundingBox(); - expect(inboxBox).not.toBeNull(); - expect(updateSlotBox).not.toBeNull(); - expect(updateSlotBox!.x - (inboxBox!.x + inboxBox!.width)).toBe(8); - await page.locator(".sidebar-issues-button").click(); - await page - .locator('openclaw-sidebar-update-card[data-attention-kind="updateAvailable"]') - .waitFor(); - await page.locator(".sidebar-issues-button").click(); + ).toEqual(["sr-only", "sidebar-issues-button"]); + await page.locator(".sidebar-shell__footer").screenshot({ + animations: "disabled", + path: path.join(PROOF_DIR, "00-footer-inbox-badge.png"), + }); - await page.locator(".sidebar-shell__footer").screenshot({ - animations: "disabled", - path: path.join(PROOF_DIR, "00-footer-update-rest.png"), - }); - const restingBackground = await footerUpdate.evaluate( - (button) => getComputedStyle(button).backgroundColor, + await page.locator(".sidebar-issues-button").click(); + const updateIssue = page.locator( + 'openclaw-sidebar-update-card[data-attention-kind="updateAvailable"]', ); - await footerUpdate.hover(); - await expect.poll(async () => (await footerUpdate.boundingBox())?.width).toBe(32); - await expect - .poll(() => footerUpdate.evaluate((button) => getComputedStyle(button).backgroundColor)) - .not.toBe(restingBackground); - const hoveredInboxBox = await page.locator(".sidebar-issues-button").boundingBox(); - expect(hoveredInboxBox).not.toBeNull(); - await page.locator(".sidebar-shell__footer").screenshot({ - animations: "disabled", - path: path.join(PROOF_DIR, "01-footer-update-hover.png"), - }); - const dismissButton = page.locator(".sidebar-footer-update__dismiss"); - expect(await dismissButton.getAttribute("aria-label")).toBe( - "Hide until next restart or update", - ); - await expect - .poll(() => dismissButton.evaluate((button) => getComputedStyle(button).opacity)) - .toBe("1"); - await dismissButton.hover(); - await expect.poll(async () => (await footerUpdate.boundingBox())?.width).toBe(32); - await expect - .poll(async () => { - const inboxBoxOnDismiss = await page.locator(".sidebar-issues-button").boundingBox(); - return Math.abs(inboxBoxOnDismiss!.x - hoveredInboxBox!.x); - }) - .toBeLessThanOrEqual(0.5); + await updateIssue.waitFor(); + const dismissButton = updateIssue.locator(".sidebar-issues-panel__dismiss"); await dismissButton.click(); - await footerUpdate.waitFor({ state: "detached" }); - await page.locator(".sidebar-issues-button").click(); - expect( - await page - .locator('openclaw-sidebar-update-card[data-attention-kind="updateAvailable"]') - .count(), - ).toBe(0); - await page.locator(".sidebar-issues-button").click(); + await updateIssue.waitFor({ state: "detached" }); + await page.keyboard.press("Escape"); + await inboxBadge.waitFor({ state: "detached" }); await page.locator(".sidebar-identity-card").click(); await page.getByText("Update available", { exact: true }).waitFor(); @@ -160,12 +120,12 @@ suite.define(() => { await page.reload(); await gateway.waitForRequest("chat.startup"); - expect(await page.locator(".sidebar-footer-update").count()).toBe(0); + expect(await page.locator(".sidebar-issues-button__count").count()).toBe(0); await gateway.setGatewayBootId("gateway-boot-b"); await gateway.setOnline(false); await gateway.setOnline(true); - await page.locator(".sidebar-footer-update").waitFor(); + await inboxBadge.waitFor(); }, ); }); @@ -187,18 +147,6 @@ suite.define(() => { expect((await page.goto(`${suite.server.baseUrl}chat`))?.status()).toBe(200); await gateway.waitForRequest("chat.startup"); - const footerUpdate = page.locator(".sidebar-footer-update"); - await footerUpdate.waitFor(); - expect(await footerUpdate.isDisabled()).toBe(true); - expect((await footerUpdate.boundingBox())?.width).toBe(32); - expect(await footerUpdate.locator(".sidebar-footer-update__icon").isVisible()).toBe(true); - expect(await footerUpdate.evaluate((button) => getComputedStyle(button).opacity)).toBe("1"); - expect(await page.locator(".sidebar-footer-update__dismiss").count()).toBe(0); - await page.locator(".sidebar-shell__footer").screenshot({ - animations: "disabled", - path: path.join(PROOF_DIR, "02-footer-update-disabled.png"), - }); - await page.locator(".sidebar-issues-button").click(); const scopeGuidance = page.locator('[data-attention-kind="scopeUpgrade"]'); await scopeGuidance.locator(".sidebar-issues-panel__dismiss").click(); @@ -224,44 +172,6 @@ suite.define(() => { ); }); - it("keeps the compact update affordance touch-sized in the mobile sidebar", async () => { - await suite.withPage( - { locale: "en-US", serviceWorkers: "block", viewport: { height: 780, width: 420 } }, - async ({ page }) => { - const gateway = await installMockGateway(page, { - gatewayBootId: "gateway-boot-mobile-update", - operatorScopes: ["operator.admin", "operator.read"], - updateAvailable: UPDATE_AVAILABLE, - updateSchedule: { - channel: "stable", - autoEnabled: false, - target: { kind: "package", version: "2.0.0" }, - }, - }); - expect((await page.goto(`${suite.server.baseUrl}chat`))?.status()).toBe(200); - await gateway.waitForRequest("chat.startup"); - await page.getByRole("button", { name: "Expand sidebar" }).click(); - - const footerUpdate = page.locator(".sidebar-footer-update"); - await footerUpdate.waitFor(); - const buttonSize = await footerUpdate.evaluate((button) => { - const style = getComputedStyle(button); - return { height: Number.parseFloat(style.height), width: Number.parseFloat(style.width) }; - }); - expect(buttonSize.width).toBeGreaterThanOrEqual(40); - expect(buttonSize.height).toBeGreaterThanOrEqual(40); - expect(await page.locator(".sidebar-footer-update__label").isVisible()).toBe(false); - expect(await page.locator(".sidebar-shell").evaluate((shell) => shell.scrollWidth)).toBe( - await page.locator(".sidebar-shell").evaluate((shell) => shell.clientWidth), - ); - await page.locator(".sidebar-shell__footer").screenshot({ - animations: "disabled", - path: path.join(PROOF_DIR, "03-footer-update-mobile.png"), - }); - }, - ); - }); - it("opens a confirmation that states the action, target, versions, and restart impact", async () => { await suite.withPage( { locale: "en-US", serviceWorkers: "block", viewport: { height: 720, width: 1280 } }, @@ -417,8 +327,6 @@ suite.define(() => { expect(await gateway.getRequests("update.run")).toHaveLength(1); await page.getByRole("button", { name: "Close", exact: true }).click(); - const footerUpdate = page.locator(".sidebar-footer-update"); - await expect.poll(() => footerUpdate.isDisabled()).toBe(true); await page.locator(".sidebar-issues-button").click(); const updateIssue = page.locator( 'openclaw-sidebar-update-card[data-attention-kind="updateAvailable"]', diff --git a/ui/src/i18n/locales/en.ts b/ui/src/i18n/locales/en.ts index 21e8a342ad79..f2c0e03ada7c 100644 --- a/ui/src/i18n/locales/en.ts +++ b/ui/src/i18n/locales/en.ts @@ -497,11 +497,9 @@ export const en: TranslationMap = { commitsBehind: "{count} commits behind", }, sidebar: { - action: "Update", campaignTarget: "{status} · {target}", updating: "Updating Gateway…", availableTitle: "Update available", - dismissUntilRestartOrVersion: "Hide until next restart or update", availableSummary: "A newer revision is ready.", blockedTitle: "Update blocked", blockedSummary: "The selected revision could not be applied to this checkout.", diff --git a/ui/src/styles/layout.css b/ui/src/styles/layout.css index 1c8dade6119d..b617fa0d91e9 100644 --- a/ui/src/styles/layout.css +++ b/ui/src/styles/layout.css @@ -3906,6 +3906,16 @@ wa-dropdown.sidebar-identity-menu::part(menu) { text-decoration: none; } +.sidebar-footer-build__text { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; +} + +.sidebar-footer-build__update { + color: var(--inbox-attention); +} + /* Dev-checkout branch strip: only rendered when the gateway runs from a git checkout off main. Sits on its own row above the icon bar so the full branch name stays readable without crowding the footer controls. */ diff --git a/ui/src/styles/sidebar-attention-floating.css b/ui/src/styles/sidebar-attention-floating.css new file mode 100644 index 000000000000..24002e3aeceb --- /dev/null +++ b/ui/src/styles/sidebar-attention-floating.css @@ -0,0 +1,54 @@ +/* Floating footer controls clear collapsed chrome and feed their resting width + back into the first-row safe edge. */ +.shell--nav-collapsed:has(openclaw-sidebar-attention.sidebar-attention--floating) { + --shell-attention-width: 40px; +} + +openclaw-sidebar-attention.sidebar-attention--floating { + --sidebar-bg: var(--bg-content, var(--bg)); + + position: fixed; + top: 10px; + left: calc(var(--shell-chrome-controls-inset) + var(--shell-controls-width) + 8px); + z-index: 45; + display: flex; + align-items: center; + gap: var(--shell-chrome-controls-gap); +} + +/* Native navigation hides the web controls. */ +html.openclaw-native-nav openclaw-sidebar-attention.sidebar-attention--floating { + left: calc(var(--shell-chrome-controls-inset) + 6px); +} + +html.openclaw-native-macos openclaw-sidebar-attention.sidebar-attention--floating { + top: 52px; +} + +/* Web-hosted titlebar controls occupy the native row instead of sitting below it. */ +html.openclaw-native-web-chrome openclaw-sidebar-attention.sidebar-attention--floating { + top: calc((var(--openclaw-native-titlebar-height, 50px) - var(--shell-chrome-control-size)) / 2); + left: calc( + var(--shell-native-titlebar-controls-inset) + var(--shell-native-titlebar-controls-width) + + var(--shell-chrome-controls-gap) + ); +} + +:is(.shell--onboarding, .shell--settings) openclaw-sidebar-attention.sidebar-attention--floating { + inset: auto auto max(12px, env(safe-area-inset-bottom)) max(12px, env(safe-area-inset-left)); +} + +/* A stale-client refresh card shares the collapsed overlay strip's row; drop it + below the fixed attention cluster instead of rendering both at the origin. */ +.sidebar-attention--floating ~ .sidebar-update-card--floating .sidebar-update-card { + margin-top: 50px; +} + +/* Footer buttons need opaque chrome when they float over arbitrary page content. */ +:where(openclaw-sidebar-attention.sidebar-attention--floating) .sidebar-issues-button { + border: 1px solid color-mix(in srgb, var(--border) 88%, transparent); + background: color-mix(in srgb, var(--panel) 92%, transparent); + box-shadow: var(--shadow-sm); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); +} diff --git a/ui/src/styles/sidebar-footer-update.css b/ui/src/styles/sidebar-footer-update.css deleted file mode 100644 index f52f5d382f8a..000000000000 --- a/ui/src/styles/sidebar-footer-update.css +++ /dev/null @@ -1,179 +0,0 @@ -/* Floating footer controls clear collapsed chrome and feed their resting width - back into the first-row safe edge. */ -.shell--nav-collapsed:has(openclaw-sidebar-attention.sidebar-attention--floating) { - --shell-attention-width: 40px; -} - -.shell--nav-collapsed:has( - openclaw-sidebar-attention.sidebar-attention--floating .sidebar-footer-update-slot -) { - --shell-attention-width: 72px; -} - -openclaw-sidebar-attention.sidebar-attention--floating { - --sidebar-bg: var(--bg-content, var(--bg)); - - position: fixed; - top: 10px; - left: calc(var(--shell-chrome-controls-inset) + var(--shell-controls-width) + 8px); - z-index: 45; - display: flex; - align-items: center; - gap: var(--shell-chrome-controls-gap); -} - -/* Native navigation hides the web controls. */ -html.openclaw-native-nav openclaw-sidebar-attention.sidebar-attention--floating { - left: calc(var(--shell-chrome-controls-inset) + 6px); -} - -html.openclaw-native-macos openclaw-sidebar-attention.sidebar-attention--floating { - top: 52px; -} - -/* Web-hosted titlebar controls occupy the native row instead of sitting below it. */ -html.openclaw-native-web-chrome openclaw-sidebar-attention.sidebar-attention--floating { - top: calc((var(--openclaw-native-titlebar-height, 50px) - var(--shell-chrome-control-size)) / 2); - left: calc( - var(--shell-native-titlebar-controls-inset) + var(--shell-native-titlebar-controls-width) + - var(--shell-chrome-controls-gap) - ); -} - -:is(.shell--onboarding, .shell--settings) openclaw-sidebar-attention.sidebar-attention--floating { - inset: auto auto max(12px, env(safe-area-inset-bottom)) max(12px, env(safe-area-inset-left)); -} - -/* A stale-client refresh card shares the collapsed overlay strip's row; drop it - below the fixed attention cluster instead of rendering both at the origin. */ -.sidebar-attention--floating ~ .sidebar-update-card--floating .sidebar-update-card { - margin-top: 50px; -} - -/* Footer buttons need opaque chrome when they float over arbitrary page content. */ -:where(openclaw-sidebar-attention.sidebar-attention--floating) .sidebar-issues-button { - border: 1px solid color-mix(in srgb, var(--border) 88%, transparent); - background: color-mix(in srgb, var(--panel) 92%, transparent); - box-shadow: var(--shadow-sm); - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); -} - -.sidebar-footer-bar:has(.sidebar-footer-update-slot) { - padding-inline-end: 80px; -} - -.sidebar-footer-update-slot { - --sidebar-footer-update-size: 32px; - - position: relative; - width: var(--sidebar-footer-update-size); - height: var(--sidebar-footer-update-size); - flex: 0 0 var(--sidebar-footer-update-size); -} - -.sidebar-footer-update { - display: inline-grid; - width: var(--sidebar-footer-update-size); - height: var(--sidebar-footer-update-size); - place-items: center; - padding: 0; - border: 1px solid color-mix(in srgb, var(--inbox-attention) 24%, var(--border)); - border-radius: var(--radius-full); - background: color-mix(in srgb, var(--inbox-attention) 8%, var(--sidebar-bg)); - color: color-mix(in srgb, var(--inbox-attention) 52%, var(--muted)); - cursor: var(--cursor-action); - transition: - border-color var(--duration-fast) ease, - background var(--duration-fast) ease, - color var(--duration-fast) ease; -} - -.sidebar-footer-update:is(:hover, :focus-visible):not(:disabled) { - border-color: color-mix(in srgb, var(--inbox-attention) 42%, var(--border)); - background: color-mix(in srgb, var(--inbox-attention) 14%, var(--sidebar-bg)); - color: var(--inbox-attention); -} - -.sidebar-footer-update:focus-visible { - outline: none; - box-shadow: var(--focus-ring); -} - -.sidebar-footer-update:disabled { - border-color: color-mix(in srgb, var(--muted) 24%, var(--border)); - background: color-mix(in srgb, var(--muted) 8%, var(--sidebar-bg)); - color: var(--muted); - cursor: not-allowed; -} - -.sidebar-footer-update__icon { - display: inline-flex; - width: 18px; - height: 18px; -} - -.sidebar-footer-update__icon svg { - width: 18px; - height: 18px; -} - -.sidebar-footer-update__dismiss { - position: absolute; - z-index: 3; - inset-block-start: -3px; - inset-inline-end: -3px; - display: inline-grid; - width: 16px; - height: 16px; - place-items: center; - padding: 0; - border: 2px solid var(--sidebar-bg); - border-radius: var(--radius-full); - background: var(--text-strong); - color: var(--bg-elevated); - opacity: 0; - pointer-events: none; - transform: scale(0.25); - transition: - opacity var(--duration-fast) ease, - transform var(--duration-fast) ease; -} - -.sidebar-footer-update-slot:is(:hover, :focus-within) .sidebar-footer-update__dismiss { - opacity: 1; - pointer-events: auto; - transform: scale(1); -} - -.sidebar-footer-update__dismiss svg { - width: 9px; -} - -@media (hover: none), (pointer: coarse) { - .sidebar-footer-update:not(:disabled):active { - border-color: color-mix(in srgb, var(--inbox-attention) 42%, var(--border)); - background: color-mix(in srgb, var(--inbox-attention) 14%, var(--sidebar-bg)); - color: var(--inbox-attention); - } - - .sidebar-footer-update__dismiss { - opacity: 1; - pointer-events: auto; - transform: scale(1); - } -} - -.shell--mobile-nav .sidebar-footer-update-slot { - --sidebar-footer-update-size: 40px; -} - -.sidebar-footer-build__text { - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; -} - -.sidebar-footer-build__update { - color: var(--inbox-attention); -}