fix: label browser-local read-only preferences

This commit is contained in:
Shakker
2026-08-06 18:38:30 +01:00
parent fa06a000c5
commit 56ab6ce78e
12 changed files with 116 additions and 3 deletions
+1
View File
@@ -197,6 +197,7 @@ export function renderApplicationShell(host: ShellViewHost) {
terminalAvailable,
catalogOpenTarget: normalizeCatalogOpenTarget(uiSettings.catalogOpenTarget),
canPairDevice: gatewayConnected && (operatorAccess.canAdmin || operatorAccess.canPair),
preferencesBrowserOnly: gatewayConnected && context.runtimeConfig.canPatch === false,
sidebarEntries: navigationSnapshot.sidebarEntries,
workboardBoards: host.sidebarWorkboardSnapshot.boards,
workboardBoardsReady: host.sidebarWorkboardSnapshot.ready,
+1
View File
@@ -32,6 +32,7 @@ export abstract class AppSidebarBase extends OpenClawLightDomContentsElement {
@property({ attribute: false }) terminalAvailable = false;
@property({ attribute: false }) catalogOpenTarget: CatalogOpenTarget = "viewer";
@property({ attribute: false }) canPairDevice = false;
@property({ attribute: false }) preferencesBrowserOnly = false;
@property({ attribute: false }) sessionKey = "";
@property({ attribute: false }) sidebarEntries: readonly string[] = DEFAULT_SIDEBAR_ENTRIES;
@property({ attribute: false }) workboardBoards: readonly SidebarWorkboardBoard[] = [];
@@ -234,6 +234,7 @@ export function renderSidebarMoreMenu(params: SidebarMoreMenuParams) {
type SidebarCustomizeMenuParams = {
position: SidebarMenuPosition | null;
sidebarEntries: readonly string[];
preferencesBrowserOnly: boolean;
isRouteEnabled: (routeId: NavigationRouteId) => boolean;
workboardBoards: readonly SidebarWorkboardBoard[];
workboardRenderers?: SidebarWorkboardRenderers;
@@ -284,6 +285,11 @@ export function renderSidebarCustomizeMenu(params: SidebarCustomizeMenuParams) {
style="position: fixed; left: ${position.x}px; top: ${position.y}px; width: 1px; height: 1px; opacity: 0; pointer-events: none;"
></button>
<div class="sidebar-customize-menu__title">${t("nav.customize")}</div>
${params.preferencesBrowserOnly
? html`<div class="sidebar-customize-menu__provenance" role="note">
${t("quickSettings.personal.browserOnly")}
</div>`
: nothing}
${SIDEBAR_NAV_ROUTES.filter((routeId) => params.isRouteEnabled(routeId)).map((routeId) => {
const visible = params.sidebarEntries.includes(
serializeSidebarEntry({ type: "route", route: routeId }),
@@ -84,6 +84,7 @@ export interface SidebarMenusControllerHost
readonly onUpdateSidebarEntries?: (entries: string[]) => void;
readonly onPreloadRoute?: (routeId: NavigationRouteId) => Promise<void>;
readonly pinnedAgentIds: readonly string[];
readonly preferencesBrowserOnly: boolean;
readonly selectedSessionKeys: ReadonlySet<string>;
readonly sessionData: SessionOrganizerControllerHost["sessionData"] &
Pick<
@@ -104,6 +104,7 @@ export function renderSidebarCustomizeMenuForController(controller: SidebarMenus
return renderSidebarCustomizeMenu({
position,
sidebarEntries: host.sidebarEntries,
preferencesBrowserOnly: host.preferencesBrowserOnly,
isRouteEnabled: (routeId) => controller.isRouteEnabled(routeId),
workboardBoards: host.workboardBoards,
workboardRenderers: host.workboardRenderers,
@@ -487,7 +487,7 @@ describeControlUiE2e("Control UI Appearance defaults mocked Gateway E2E", () =>
}
});
it("keeps read-only edits browser-local across reload and restores the server baseline", async () => {
it("keeps every read-only preference surface browser-local across reload", async () => {
const context = await browser.newContext({
locale: "en-US",
serviceWorkers: "block",
@@ -545,6 +545,65 @@ describeControlUiE2e("Control UI Appearance defaults mocked Gateway E2E", () =>
.not.toContain("Stored in this browser only");
await page.waitForTimeout(100);
expect(await gateway.getRequests("config.patch")).toHaveLength(0);
await page.goto(`${server.baseUrl}chat`);
const viewMenuTrigger = page.locator(".chat-view-menu-trigger");
await viewMenuTrigger.click();
const viewMenu = page.locator("wa-dropdown.chat-view-menu");
await expect
.poll(() => viewMenu.locator(".chat-view-menu__provenance").textContent())
.toContain("Stored in this browser only");
const reasoning = viewMenu.getByRole("menuitemcheckbox", { name: "Reasoning" });
await reasoning.click();
await expect.poll(() => reasoning.getAttribute("aria-checked")).toBe("false");
const sidebar = page.locator("openclaw-app-sidebar");
await sidebar.locator(".sidebar-nav__head-action").click();
await sidebar
.locator("wa-dropdown.sidebar-more-menu")
.getByRole("menuitem", { name: "Edit pinned items" })
.click();
const customizeMenu = sidebar.locator(
"wa-dropdown.sidebar-customize-menu:not(.sidebar-more-menu):not(.sidebar-agent-menu)",
);
await expect
.poll(() => customizeMenu.locator(".sidebar-customize-menu__provenance").textContent())
.toContain("Stored in this browser only");
const tasks = customizeMenu.getByRole("menuitemcheckbox", { name: "Tasks" });
await tasks.click();
await expect.poll(() => tasks.getAttribute("aria-checked")).toBe("true");
await page.waitForTimeout(100);
expect(await gateway.getRequests("config.patch")).toHaveLength(0);
await page.reload();
await viewMenuTrigger.click();
await expect
.poll(() => viewMenu.locator(".chat-view-menu__provenance").textContent())
.toContain("Stored in this browser only");
await expect
.poll(() =>
viewMenu
.getByRole("menuitemcheckbox", { name: "Reasoning" })
.getAttribute("aria-checked"),
)
.toBe("false");
await sidebar.locator(".sidebar-nav__head-action").click();
await sidebar
.locator("wa-dropdown.sidebar-more-menu")
.getByRole("menuitem", { name: "Edit pinned items" })
.click();
await expect
.poll(() => customizeMenu.locator(".sidebar-customize-menu__provenance").textContent())
.toContain("Stored in this browser only");
await expect
.poll(() =>
customizeMenu
.getByRole("menuitemcheckbox", { name: "Tasks" })
.getAttribute("aria-checked"),
)
.toBe("true");
expect(await gateway.getRequests("config.patch")).toHaveLength(0);
} finally {
await context.close();
}
+10
View File
@@ -41,6 +41,7 @@ function createProps(overrides: Record<string, unknown> = {}): ChatControlsProps
stream: null,
},
onboarding: false,
preferencesBrowserOnly: false,
settings: createSettings(),
viewMenuOpen: true,
onSettingsChange: () => undefined,
@@ -101,6 +102,15 @@ describe("chat composer view menu", () => {
);
});
it("labels read-only preference changes as browser-local", () => {
const container = document.createElement("div");
render(renderChatControls(createProps({ preferencesBrowserOnly: true })), container);
expect(container.querySelector(".chat-view-menu__provenance")?.textContent?.trim()).toBe(
t("quickSettings.personal.browserOnly"),
);
});
it("disables the rows and pins display state during onboarding", () => {
const container = document.createElement("div");
const onSettingsChange = vi.fn();
+3
View File
@@ -411,6 +411,9 @@ export class ChatPane extends ChatPaneHeader {
selectedSession,
agentDefaultModel,
mutationAccess: mutationAccess.runtimePatch,
preferencesBrowserOnly:
this.context.runtimeConfig.state.connected &&
this.context.runtimeConfig.canPatch === false,
}),
sessionWorkspace: catalogKey ? undefined : sessionWorkspace,
backgroundTasks: catalogKey ? undefined : backgroundTasks,
@@ -39,8 +39,16 @@ export function renderChatPaneComposerControls(params: {
selectedSession: GatewaySessionRow | undefined;
agentDefaultModel: string | undefined;
mutationAccess: SessionMethodAccess;
preferencesBrowserOnly: boolean;
}) {
const { paneId, state, selectedSession, agentDefaultModel, mutationAccess } = params;
const {
paneId,
state,
selectedSession,
agentDefaultModel,
mutationAccess,
preferencesBrowserOnly,
} = params;
const mutationAllowed = () => mutationAccess.allowed;
return renderChatControls({
paneId,
@@ -74,6 +82,7 @@ export function renderChatPaneComposerControls(params: {
: Promise.resolve(false),
},
onboarding: state.onboarding,
preferencesBrowserOnly,
settings: state.settings,
viewMenuOpen: state.chatViewMenuOpen,
onSettingsChange: state.applySettings,
@@ -1,5 +1,5 @@
// Chat-owned composer display controls: the View menu plus model controls.
import { html } from "lit";
import { html, nothing } from "lit";
import type { UiSettings } from "../../../app/settings.ts";
import { icons } from "../../../components/icons.ts";
import "../../../components/tooltip.ts";
@@ -11,6 +11,7 @@ type ChatControlsProps = {
paneId: string;
model: ChatModelControlsProps;
onboarding: boolean;
preferencesBrowserOnly: boolean;
settings: UiSettings;
viewMenuOpen: boolean;
onSettingsChange: (patch: Partial<UiSettings>) => void;
@@ -105,6 +106,11 @@ export function renderChatControls(props: ChatControlsProps) {
</wa-dropdown-item>
`,
)}
${props.preferencesBrowserOnly
? html`<div class="chat-view-menu__provenance" role="note">
${t("quickSettings.personal.browserOnly")}
</div>`
: nothing}
</wa-dropdown>
</openclaw-tooltip>
</div>
+9
View File
@@ -2569,6 +2569,15 @@ openclaw-chat-video-player {
white-space: nowrap;
}
.chat-view-menu__provenance {
margin: 4px 6px 2px;
padding-top: 7px;
border-top: 1px solid color-mix(in srgb, var(--border) 72%, transparent);
color: var(--muted);
font-size: 11px;
line-height: 1.35;
}
.agent-chat__stt-interim {
padding: 10px 14px 0;
color: var(--muted);
+7
View File
@@ -2346,6 +2346,13 @@ wa-dropdown.sidebar-customize-menu::part(menu) {
color: var(--muted);
}
.sidebar-customize-menu__provenance {
margin: 0 10px 6px;
color: var(--muted);
font-size: 11px;
line-height: 1.35;
}
.sidebar-customize-menu__group-title {
margin: 6px 4px 2px;
padding: 7px 6px 4px;