mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
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:
committed by
GitHub
parent
e05dae2bb5
commit
81fb7e69c4
@@ -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", () => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
},
|
||||
|
||||
@@ -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"]',
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user