fix(ui): keep limited-access banner clear of shell controls (#123128)

* fix(ui): keep limited-access banner clear of shell controls

* fix(ui): avoid native notice gutters
This commit is contained in:
Peter Steinberger
2026-08-13 04:40:26 -07:00
committed by GitHub
parent baf85e6868
commit 449ca204a6
4 changed files with 115 additions and 10 deletions
+1 -3
View File
@@ -59,7 +59,6 @@ const SCOPE_UPGRADE_BANNER_ELEMENT = {
function renderScopeUpgradeBanner(
host: ShellViewHost,
snapshot: ApplicationContext["gateway"]["snapshot"],
chromeOffset: boolean,
) {
const state = readScopeUpgradeAvailability(snapshot);
if (state.phase === "hidden") {
@@ -80,7 +79,6 @@ function renderScopeUpgradeBanner(
return html`<openclaw-device-scope-upgrade-banner
.props=${{
snapshot,
chromeOffset,
}}
></openclaw-device-scope-upgrade-banner>`;
}
@@ -506,7 +504,7 @@ export function renderApplicationShell(host: ShellViewHost) {
: ""} ${activeRoute === "workboard" ? "content--workboard" : ""}"
.tabIndex=${-1}
>
${renderScopeUpgradeBanner(host, gatewaySnapshot, !settingsTakeover && !mobileNavLayout)}
${renderScopeUpgradeBanner(host, gatewaySnapshot)}
${gatewaySnapshot.hello?.deviceAuthMigration?.pending === true
? // The migration banner is registered by a rare-flow dynamic import after first render.
customElements.get("openclaw-device-auth-migration-banner")
@@ -130,7 +130,6 @@ export class ScopeUpgradeController {
type ScopeUpgradeBannerProps = {
snapshot: ApplicationGatewaySnapshot;
chromeOffset: boolean;
};
class ScopeUpgradeBanner extends OpenClawLightDomContentsElement {
@@ -186,7 +185,6 @@ class ScopeUpgradeBanner extends OpenClawLightDomContentsElement {
class="callout ${state.phase === "error" || state.phase === "rejected"
? "danger"
: "warn"} callout--action"
style=${props.chromeOffset ? "margin-left: 72px" : nothing}
role="status"
>
<span class="callout__content">${text}</span>
+89 -1
View File
@@ -4,6 +4,7 @@ import { chromium, type Browser, type BrowserContext, type Page } from "playwrig
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
import {
canRunPlaywrightChromium,
controlUiBundledSettingsStorageKey,
installMockGateway,
resolvePlaywrightChromiumExecutablePath,
startControlUiE2eServer,
@@ -29,9 +30,24 @@ const SCOPE_UPGRADE_METHODS = [
"device.scopes.requestUpgrade",
"device.scopes.waitUpgrade",
] as const;
const HIDDEN_WEB_CHROME_HOSTS = [
{ collapsed: false, label: "native web chrome", rootClass: "openclaw-native-web-chrome" },
{ collapsed: true, label: "collapsed native navigation", rootClass: "openclaw-native-nav" },
] as const;
const MANUAL_UPGRADE_GUIDANCE =
"This browser has limited access. Manage it with openclaw devices on the Gateway or from Devices on an admin browser.";
type BoundingBox = { x: number; y: number; width: number; height: number };
function boxesIntersect(left: BoundingBox, right: BoundingBox): boolean {
return !(
left.x + left.width <= right.x ||
right.x + right.width <= left.x ||
left.y + left.height <= right.y ||
right.y + right.height <= left.y
);
}
let browser: Browser;
let server: ControlUiE2eServer;
const openContexts = new Set<BrowserContext>();
@@ -122,6 +138,10 @@ describeControlUiE2e("Control UI live device scope upgrade", () => {
await page.getByRole("button", { name: "Request admin" }).waitFor();
await page.locator("#new-session-project-trigger").click();
const projectPopover = page.locator("wa-popover.new-session-page__project-popover");
await expect
.poll(() => projectPopover.evaluate((element) => element === document.activeElement))
.toBe(true);
const browse = page.getByRole("button", { name: "Browse folders" });
await expect.poll(() => browse.isDisabled()).toBe(true);
await browse.focus();
@@ -187,13 +207,81 @@ describeControlUiE2e("Control UI live device scope upgrade", () => {
});
await page.goto(`${server.baseUrl}chat`);
await page.getByText(MANUAL_UPGRADE_GUIDANCE, { exact: true }).waitFor();
const guidance = page.getByText(MANUAL_UPGRADE_GUIDANCE, { exact: true });
await guidance.waitFor();
const guidanceBox = await guidance.boundingBox();
const chromeControls = page.locator(".shell-chrome-controls__button");
expect(guidanceBox).not.toBeNull();
expect(await chromeControls.count()).toBe(2);
for (let index = 0; index < (await chromeControls.count()); index += 1) {
const control = chromeControls.nth(index);
const controlBox = await control.boundingBox();
expect(controlBox).not.toBeNull();
if (guidanceBox && controlBox) {
const label = await control.getAttribute("aria-label");
expect(
boxesIntersect(guidanceBox, controlBox),
`guidance ${JSON.stringify(guidanceBox)} intersects ${label} ${JSON.stringify(controlBox)}`,
).toBe(false);
}
}
expect(await page.getByRole("button", { name: "Request admin" }).count()).toBe(0);
expect(await gateway.getRequests("device.scopes.requestUpgrade")).toHaveLength(0);
},
);
it.each(HIDDEN_WEB_CHROME_HOSTS)(
"keeps manual guidance at the normal content inset with $label",
async ({ collapsed, rootClass }) => {
const context = await createContext();
await context.addInitScript(
({ hostClass, settingsKey, startCollapsed }) => {
localStorage.setItem(settingsKey, JSON.stringify({ navCollapsed: startCollapsed }));
const stamp = () =>
document.documentElement.classList.add("openclaw-native-macos", hostClass);
if (document.documentElement) {
stamp();
} else {
document.addEventListener("DOMContentLoaded", stamp);
}
},
{
hostClass: rootClass,
settingsKey: controlUiBundledSettingsStorageKey(server.baseUrl),
startCollapsed: collapsed,
},
);
const page = await context.newPage();
await installMockGateway(page, {
featureMethods: ["chat.metadata", "chat.startup", "device.scopes.requestUpgrade"],
operatorScopes: LIMITED_SCOPES,
});
await page.goto(`${server.baseUrl}chat`);
const guidance = page.getByText(MANUAL_UPGRADE_GUIDANCE, { exact: true });
await guidance.waitFor();
if (collapsed) {
await expect
.poll(() => page.locator(".shell").getAttribute("class"))
.toContain("shell--nav-collapsed");
}
await expect.poll(() => page.locator(".shell-chrome-controls").isVisible()).toBe(false);
const contentBox = await page.locator(".content").boundingBox();
const calloutBox = await page.locator("openclaw-update-banner .callout").boundingBox();
const contentInset = await page
.locator(".content")
.evaluate((element) => Number.parseFloat(getComputedStyle(element).paddingLeft));
expect(contentBox).not.toBeNull();
expect(calloutBox).not.toBeNull();
if (contentBox && calloutBox) {
expect(calloutBox.x).toBe(contentBox.x + contentInset);
}
},
);
it("does not misreport limited Custodian access as an outdated Gateway", async () => {
const context = await createContext();
const page = await context.newPage();
+25 -4
View File
@@ -15,8 +15,9 @@
var(--shell-chrome-control-size) + var(--shell-chrome-controls-gap) +
var(--shell-chrome-controls-gap)
);
--shell-floating-update-card-left: calc(
var(--shell-chrome-controls-inset) + var(--shell-chrome-controls-collapsed-width) + 8px
--shell-chrome-safe-area-left: calc(
var(--shell-chrome-controls-inset) + var(--shell-chrome-controls-collapsed-width) -
var(--shell-chrome-control-size) - var(--shell-chrome-controls-gap) + 8px
);
/* Desktop has no topbar row — the sidebar owns navigation. Narrow viewports
(layout.mobile.css) and the chat split toolbar restore the row height. */
@@ -73,6 +74,9 @@
.shell--nav-collapsed {
grid-template-columns: 0 minmax(0, 1fr);
--shell-nav-width: 0px;
--shell-chrome-safe-area-left: calc(
var(--shell-chrome-controls-inset) + var(--shell-chrome-controls-collapsed-width) + 8px
);
}
/* Drawer shells keep the nav mounted offscreen; desktop collapse removes it. */
@@ -136,6 +140,23 @@
display: none;
}
/* Reserve notice space only while the corresponding in-page chrome remains visible. */
:is(
html:not(.openclaw-native-nav, .openclaw-native-web-chrome)
.shell:not(.shell--mobile-nav, .shell--settings),
html.openclaw-native-nav:not(.openclaw-native-web-chrome)
.shell:not(.shell--mobile-nav, .shell--nav-collapsed, .shell--settings)
)
.content
> :is(
openclaw-update-banner,
openclaw-device-scope-upgrade-banner,
openclaw-device-auth-migration-banner
)
.callout {
margin-left: var(--shell-chrome-safe-area-left);
}
html.openclaw-native-macos .shell-chrome-controls {
top: 52px;
}
@@ -1150,8 +1171,8 @@ openclaw-settings-save-indicator {
/* Clear the collapsed three-button chrome cluster plus an 8px breathing gap. */
.shell:not(.shell--mobile-nav) .sidebar-update-card--floating .sidebar-update-card {
width: min(360px, calc(100% - var(--shell-floating-update-card-left) - 16px));
margin-left: var(--shell-floating-update-card-left);
width: min(360px, calc(100% - var(--shell-chrome-safe-area-left) - 16px));
margin-left: var(--shell-chrome-safe-area-left);
}
/* Native hosts hide the web cluster; onboarding never mounts it. */