mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
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
This commit is contained in:
committed by
GitHub
parent
0295b7ab54
commit
dd57cfb6c1
@@ -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 });
|
||||
|
||||
@@ -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<string, { routeId: RouteId; keepSection: bool
|
||||
};
|
||||
|
||||
const SESSION_OBSERVER_STATUS_POLL_INTERVAL_MS = 10_000;
|
||||
const EMPTY_SESSION_CATALOG_LABELS: ReadonlyMap<string, string> = 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<SessionsCatalogListResult>(
|
||||
"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),
|
||||
|
||||
@@ -486,7 +486,7 @@ export function renderSidebarPreferencesSection(props: ConfigProps) {
|
||||
<div class="settings-group">
|
||||
${hiddenCatalogIds.map((catalogId) =>
|
||||
renderSettingsRow({
|
||||
title: catalogId,
|
||||
title: props.hiddenSessionCatalogLabels.get(catalogId) ?? catalogId,
|
||||
description: t("quickSettings.personal.browserOnly"),
|
||||
control: html`<button
|
||||
type="button"
|
||||
|
||||
@@ -128,6 +128,7 @@ export type ConfigProps = {
|
||||
sidebarLiveActivity: boolean;
|
||||
setSidebarLiveActivity: (enabled: boolean) => void;
|
||||
hiddenSessionCatalogIds: ReadonlySet<string>;
|
||||
hiddenSessionCatalogLabels: ReadonlyMap<string, string>;
|
||||
setSessionCatalogHidden: (catalogId: string, hidden: boolean) => void;
|
||||
chatMessageMaxWidth?: string;
|
||||
setChatMessageMaxWidth: (value: string | undefined) => void;
|
||||
|
||||
@@ -85,6 +85,7 @@ describe("config view", () => {
|
||||
sidebarLiveActivity: true,
|
||||
setSidebarLiveActivity: vi.fn(),
|
||||
hiddenSessionCatalogIds: new Set<string>(),
|
||||
hiddenSessionCatalogLabels: new Map<string, string>(),
|
||||
setSessionCatalogHidden: vi.fn(),
|
||||
chatMessageMaxWidth: undefined,
|
||||
setChatMessageMaxWidth: vi.fn(),
|
||||
@@ -2129,26 +2130,32 @@ describe("config view", () => {
|
||||
expect(setSidebarLiveActivity).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it("lists hidden session sections and offers to show them", () => {
|
||||
it("labels hidden session sections from the catalog and keeps ids as the fallback", () => {
|
||||
const setSessionCatalogHidden = vi.fn();
|
||||
const { container } = renderConfigView({
|
||||
activeSection: "__appearance__",
|
||||
includeSections: ["__appearance__"],
|
||||
hiddenSessionCatalogIds: new Set(["codex"]),
|
||||
hiddenSessionCatalogIds: new Set(["claude", "offline-catalog"]),
|
||||
hiddenSessionCatalogLabels: new Map([["claude", "Claude Code"]]),
|
||||
setSessionCatalogHidden,
|
||||
});
|
||||
|
||||
const heading = Array.from(container.querySelectorAll("h3")).find(
|
||||
(candidate) => candidate.textContent?.trim() === "Hidden session sections",
|
||||
);
|
||||
const row = Array.from(container.querySelectorAll<HTMLElement>(".settings-row")).find(
|
||||
const labeledRow = Array.from(container.querySelectorAll<HTMLElement>(".settings-row")).find(
|
||||
(candidate) =>
|
||||
candidate.querySelector(".settings-row__title")?.textContent?.trim() === "codex",
|
||||
candidate.querySelector(".settings-row__title")?.textContent?.trim() === "Claude Code",
|
||||
);
|
||||
const fallbackRow = Array.from(container.querySelectorAll<HTMLElement>(".settings-row")).find(
|
||||
(candidate) =>
|
||||
candidate.querySelector(".settings-row__title")?.textContent?.trim() === "offline-catalog",
|
||||
);
|
||||
expect(heading).toBeDefined();
|
||||
expect(row).toBeDefined();
|
||||
row?.querySelector<HTMLButtonElement>("button")?.click();
|
||||
expect(setSessionCatalogHidden).toHaveBeenCalledWith("codex", false);
|
||||
expect(labeledRow).toBeDefined();
|
||||
expect(fallbackRow).toBeDefined();
|
||||
labeledRow?.querySelector<HTMLButtonElement>("button")?.click();
|
||||
expect(setSessionCatalogHidden).toHaveBeenCalledWith("claude", false);
|
||||
});
|
||||
|
||||
it("uses rich Lobsterdex lore tooltips and opens the full collection", () => {
|
||||
|
||||
Reference in New Issue
Block a user