improve(ui): fold the sidebar update action into the Inbox (#129977)

* 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.
This commit is contained in:
Peter Steinberger
2026-08-26 01:12:29 -07:00
committed by GitHub
parent e05dae2bb5
commit 81fb7e69c4
16 changed files with 100 additions and 441 deletions
+8 -1
View File
@@ -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"
]
@@ -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
@@ -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(),
});
}
@@ -69,7 +69,6 @@ type SidebarAttentionElement = HTMLElement & {
context: ApplicationContext;
updateComplete: Promise<boolean>;
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", () => {
+2 -50
View File
@@ -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}
</button>
${updateEntry
? html`<span class="sidebar-footer-update-slot">
<button
type="button"
class="sidebar-footer-update"
aria-label=${t("updates.sidebar.availableTitle")}
?disabled=${updateState.busy || !updateState.actionable || !updateState.canUpdate}
@click=${this.startUpdate}
>
<span class="sidebar-footer-update__icon" aria-hidden="true"
>${updateState.busy ? icons.refresh : icons.download}</span
>
</button>
${updateDismissal
? html`<openclaw-tooltip
class="sidebar-hover-tooltip"
.content=${t("updates.sidebar.dismissUntilRestartOrVersion")}
.delay=${600}
.closeDelay=${300}
>
<button
type="button"
class="sidebar-footer-update__dismiss"
aria-label=${t("updates.sidebar.dismissUntilRestartOrVersion")}
@click=${() => this.dismiss(updateDismissal)}
>
${icons.x}
</button>
</openclaw-tooltip>`
: nothing}
</span>`
: nothing}
${this.panelOpen && this.panelRenderer
? this.panelRenderer({
context: this.context,
-1
View File
@@ -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,
@@ -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);
@@ -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();
@@ -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);
+1 -1
View File
@@ -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);
+2 -2
View File
@@ -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");
},
+19 -111
View File
@@ -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"]',
-2
View File
@@ -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.",
+10
View File
@@ -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. */
@@ -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);
}
-179
View File
@@ -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);
}