From dd57cfb6c150b8201680b8e8930de0403d71856c Mon Sep 17 00:00:00 2001 From: "Vyctor H. Brzezowski" Date: Wed, 12 Aug 2026 02:18:05 -0300 Subject: [PATCH] fix(ui): show catalog labels in hidden section settings (#122320) * test(ui): reproduce hidden catalog id labels * fix(ui): label hidden session catalogs * test(ui): capture hidden catalog label evidence * test(ui): harden hidden catalog label proof --- ui/src/e2e/sidebar-customization.e2e.test.ts | 79 +++++++++++++++++++ ui/src/pages/config/config-page.ts | 46 ++++++++++- .../config/view-appearance-preferences.ts | 2 +- ui/src/pages/config/view-types.ts | 1 + ui/src/pages/config/view.browser.test.ts | 21 +++-- 5 files changed, 140 insertions(+), 9 deletions(-) diff --git a/ui/src/e2e/sidebar-customization.e2e.test.ts b/ui/src/e2e/sidebar-customization.e2e.test.ts index 3c2a2d2e6a68..62e2845bb9c4 100644 --- a/ui/src/e2e/sidebar-customization.e2e.test.ts +++ b/ui/src/e2e/sidebar-customization.e2e.test.ts @@ -20,6 +20,7 @@ const suite = createControlUiE2eSuite({ }); const captureUiProofEnabled = process.env.OPENCLAW_CAPTURE_UI_PROOF === "1"; +const hiddenSessionCatalogsStorageKey = "openclaw:sidebar:sessions:hidden-catalogs"; const uiProofArtifactDir = path.join( process.cwd(), ".artifacts", @@ -93,6 +94,19 @@ async function holdUiProof(page: Page, durationMs = 600) { } } +async function setThemeMode(page: Page, mode: "dark" | "light") { + await page.emulateMedia({ colorScheme: mode }); + await page.evaluate((nextMode) => { + const root = document.documentElement; + root.dataset.themeMode = nextMode; + root.dataset.themeResolved = nextMode; + root.classList.toggle("wa-light", nextMode === "light"); + root.classList.toggle("wa-dark", nextMode === "dark"); + root.style.colorScheme = nextMode; + }, mode); + await expect.poll(() => page.locator("html").getAttribute("data-theme-mode")).toBe(mode); +} + async function openSidebarTestPage() { const context = await suite.browser.newContext({ locale: "en-US", @@ -107,6 +121,71 @@ async function openSidebarTestPage() { } suite.define(() => { + it("uses catalog labels in the hidden-section recovery rows", async () => { + const context = await suite.browser.newContext({ + locale: "en-US", + serviceWorkers: "block", + viewport: { height: 900, width: 1440 }, + }); + const page = await context.newPage(); + await page.addInitScript(({ key, value }) => localStorage.setItem(key, JSON.stringify(value)), { + key: hiddenSessionCatalogsStorageKey, + value: ["claude", "offline-catalog"], + }); + const gateway = await installMockGateway(page, { + featureMethods: ["sessions.catalog.list"], + methodResponses: { + "sessions.catalog.list": { + catalogs: [ + { + id: "claude", + label: "Claude Code", + capabilities: { continueSession: true, archive: false }, + hosts: [], + }, + ], + }, + }, + }); + + try { + await page.goto(`${suite.server.baseUrl}settings/appearance`); + await waitForControlUiSettingsTakeover(page); + await gateway.waitForRequest("sessions.catalog.list"); + const sidebarSettings = page.locator("#settings-appearance-sidebar"); + await sidebarSettings.getByRole("heading", { name: "Hidden session sections" }).waitFor(); + const recovery = sidebarSettings.locator(".settings-group", { hasText: "offline-catalog" }); + const row = recovery.locator(".settings-row", { hasText: "Claude Code" }); + await expect.poll(() => recovery.textContent()).toContain("Claude Code"); + await expect.poll(() => recovery.textContent()).toContain("offline-catalog"); + expect(await recovery.getByText("claude", { exact: true }).count()).toBe(0); + + if (captureUiProofEnabled) { + await mkdir(uiProofArtifactDir, { recursive: true }); + await recovery.scrollIntoViewIfNeeded(); + for (const theme of ["light", "dark"] as const) { + await setThemeMode(page, theme); + await page.screenshot({ + animations: "disabled", + path: path.join(uiProofArtifactDir, `after-${theme}-context.png`), + }); + await recovery.screenshot({ + animations: "disabled", + path: path.join(uiProofArtifactDir, `after-${theme}-rows.png`), + }); + } + } + + await row.getByRole("button", { name: "Show" }).click(); + await expect.poll(() => row.count()).toBe(0); + expect( + await page.evaluate((key) => localStorage.getItem(key), hiddenSessionCatalogsStorageKey), + ).toBe('["offline-catalog"]'); + } finally { + await context.close(); + } + }); + it("pins routes, restores defaults, and persists navigation state across reloads", async () => { if (captureUiProofEnabled) { await mkdir(uiProofArtifactDir, { recursive: true }); diff --git a/ui/src/pages/config/config-page.ts b/ui/src/pages/config/config-page.ts index dc4e8eb2f3da..e6c0cf7438bc 100644 --- a/ui/src/pages/config/config-page.ts +++ b/ui/src/pages/config/config-page.ts @@ -4,7 +4,10 @@ import { initialState, Task, TaskStatus } from "@lit/task"; import { asNullableRecord as asConfigRecord } from "@openclaw/normalization-core/record-coerce"; import { html, nothing, type PropertyValues } from "lit"; import { property, state } from "lit/decorators.js"; -import type { SystemInfoResult } from "../../../../packages/gateway-protocol/src/index.js"; +import type { + SessionsCatalogListResult, + SystemInfoResult, +} from "../../../../packages/gateway-protocol/src/index.js"; import type { GatewayBrowserClient } from "../../api/gateway.ts"; import type { ModelCatalogEntry } from "../../api/types.ts"; import { titleForRoute } from "../../app-navigation.ts"; @@ -114,6 +117,7 @@ const MOVED_SECTION_ROUTES: Record = new Map(); function defaultConfigSelection(pageId: ConfigPageId): ConfigSelection { switch (pageId) { @@ -338,6 +342,42 @@ export class ConfigPage extends OpenClawLightDomElement { } }, }); + private readonly hiddenSessionCatalogLabelsTask = new Task(this, { + args: () => { + const gateway = this.context?.gateway.snapshot; + const hiddenCatalogIds = [...this.hiddenSessionCatalogIds].toSorted(); + const client = + this.pageId === "appearance" && + hiddenCatalogIds.length > 0 && + canCallGatewayMethod(gateway, "sessions.catalog.list", "operator.read") + ? gateway?.client + : null; + return [ + client, + this.context?.agentSelection.state.selectedId ?? null, + hiddenCatalogIds.join("\0"), + ] as const; + }, + task: async ([client, agentId], { signal }) => { + if (!client) { + return EMPTY_SESSION_CATALOG_LABELS; + } + try { + const result = await client.request( + "sessions.catalog.list", + { + ...(agentId ? { agentId } : {}), + limitPerHost: 1, + }, + { signal }, + ); + return new Map(result.catalogs.map((catalog) => [catalog.id, catalog.label])); + } catch { + // Recovery must remain available when catalog discovery is unsupported or offline. + return EMPTY_SESSION_CATALOG_LABELS; + } + }, + }); private pendingRouteTargetId: string | null = null; private readonly subscriptions = new SubscriptionsController(this) .watch( @@ -1170,6 +1210,10 @@ export class ConfigPage extends OpenClawLightDomElement { this.settings.sidebarLiveActivity ?? UI_APPEARANCE_DEFAULTS.sidebarLiveActivity, setSidebarLiveActivity: (enabled) => this.setSetting("sidebarLiveActivity", enabled), hiddenSessionCatalogIds: this.hiddenSessionCatalogIds, + hiddenSessionCatalogLabels: + this.hiddenSessionCatalogLabelsTask.status === TaskStatus.COMPLETE + ? (this.hiddenSessionCatalogLabelsTask.value ?? EMPTY_SESSION_CATALOG_LABELS) + : EMPTY_SESSION_CATALOG_LABELS, setSessionCatalogHidden: setStoredSessionCatalogHidden, chatMessageMaxWidth: this.settings.chatMessageMaxWidth, setChatMessageMaxWidth: (value) => this.setSetting("chatMessageMaxWidth", value), diff --git a/ui/src/pages/config/view-appearance-preferences.ts b/ui/src/pages/config/view-appearance-preferences.ts index 7100e66e4842..ab931478f78e 100644 --- a/ui/src/pages/config/view-appearance-preferences.ts +++ b/ui/src/pages/config/view-appearance-preferences.ts @@ -486,7 +486,7 @@ export function renderSidebarPreferencesSection(props: ConfigProps) {
${hiddenCatalogIds.map((catalogId) => renderSettingsRow({ - title: catalogId, + title: props.hiddenSessionCatalogLabels.get(catalogId) ?? catalogId, description: t("quickSettings.personal.browserOnly"), control: html`