diff --git a/skills/visualize/SKILL.md b/skills/visualize/SKILL.md index a4a5b8b79a7f..e14b09f97d5f 100644 --- a/skills/visualize/SKILL.md +++ b/skills/visualize/SKILL.md @@ -142,10 +142,14 @@ initialization and outside that host. ## Verify and deliver -Fix reported inline-script syntax errors and call the tool again. Inspect the -rendered result with available browser or device tools: verify libraries and -fonts loaded, important controls work, and content fits the intended width and -theme. For live dashboards, exercise the data read in the actual pinned frame. +Fix reported inline-script syntax errors and call the tool again. Script errors +thrown after an inline widget renders in the Control UI are reported back to this +session. Agent browser tools normally run a separate profile without the viewer's +Control UI session, so opening the chat there reaches sign-in, not the widget; do +not use them to inspect inline widgets. When a browser or device tool can load the +rendered surface itself, verify libraries and fonts loaded, important controls +work, and content fits the intended width and theme. For live dashboards, exercise +the data read in the actual pinned frame. Strict embed mode disables scripts. Report any concrete visual or platform verification gap; successful hosting alone proves neither rendering nor data access. diff --git a/src/agents/sandbox-host.ts b/src/agents/sandbox-host.ts index 05697934d7cb..4838b96866ff 100644 --- a/src/agents/sandbox-host.ts +++ b/src/agents/sandbox-host.ts @@ -1,5 +1,6 @@ import { createHash } from "node:crypto"; import { asOptionalRecord as asRecord } from "@openclaw/normalization-core/record-coerce"; +import { WIDGET_THEME_MESSAGE_TYPE } from "../shared/widget-theme.js"; export type SandboxHostCsp = { connectDomains?: string[]; @@ -316,6 +317,12 @@ function buildSandboxHostProxyHtml(csp?: SandboxHostCsp): string { return; } if (typeof event.data?.method === "string" && event.data.method.startsWith("ui/notifications/sandbox-")) return; + // A frame whose root color-scheme differs from its embedding element gets + // an opaque UA canvas. Follow the host's widget theme mode so this shell + // and the themed widget document both stay transparent. + if (event.data?.type === ${JSON.stringify(WIDGET_THEME_MESSAGE_TYPE)} && (event.data.mode === "light" || event.data.mode === "dark")) { + document.documentElement.style.colorScheme = event.data.mode; + } inner.contentWindow?.postMessage(event.data, "*"); return; } diff --git a/ui/src/e2e/chat-widget-sandbox.e2e.test.ts b/ui/src/e2e/chat-widget-sandbox.e2e.test.ts index 9d72312fee04..c0ddb5fe1726 100644 --- a/ui/src/e2e/chat-widget-sandbox.e2e.test.ts +++ b/ui/src/e2e/chat-widget-sandbox.e2e.test.ts @@ -442,6 +442,15 @@ suite.define(() => { inline.locator("html").evaluate((root) => getComputedStyle(root).colorScheme), ) .toBe("dark"); + // A light proxy between dark documents paints an opaque UA canvas. + await expect + .poll(() => + outer + .contentFrame() + .locator("html") + .evaluate((root) => getComputedStyle(root).colorScheme), + ) + .toBe("dark"); await expect .poll(() => board.locator("html").evaluate((root) => getComputedStyle(root).colorScheme),