mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:40:44 +00:00
fix(control-ui): allow configured chat message width
Adds validated gateway.controlUi.chatMessageMaxWidth support for grouped Control UI chat messages, carries it through the Gateway bootstrap payload into UI state, applies it as a CSS custom property, and documents the setting while preserving the existing default width.
Fixes #67935.
Validation:
- Targeted config, gateway, and Control UI tests passed locally.
- Config schema/docs checks passed.
- Testbox changed-file gate passed.
- GitHub CI and security checks are green on cea25a4ca9.
This commit is contained in:
@@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Control UI: allow deployments to configure grouped chat message max-width with a validated `gateway.controlUi.chatMessageMaxWidth` setting instead of patching bundled CSS after upgrades. Fixes #67935. Thanks @xiew4589-lang.
|
||||
- Control UI/sessions: bound the default Sessions tab query to recent activity and fewer rows, avoiding expensive full-history loads while keeping filters editable. Fixes #76050. (#76051) Thanks @Neomail2.
|
||||
- Plugins/doctor: repair missing configured provider and channel plugins from ClawHub before npm fallback, preserving ClawPack metadata in the install record. Thanks @vincentkoc.
|
||||
- Gateway/channels: cap startup fanout at four channel/account handoffs and recover from Bonjour ciao self-probe races, reducing Windows startup stalls with many Telegram accounts. Fixes #75687.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
f3a0cf57605c6c25ce162080d2631c0256018c2ec128383d521153f65e69a699 config-baseline.json
|
||||
711b933e8748fe220d4be1bcc7df74503ab9c5973e967839302b8c5c773ecebf config-baseline.core.json
|
||||
cf956c5e58ec0e36cf47708b0cd42fa34b1f39d0da951de343be0ba6e5b28168 config-baseline.json
|
||||
057e444dfc78472bac172d9d8a7bd9c9a40f9ca4755268307cfcbd7e87a4d932 config-baseline.core.json
|
||||
a2a949a99f5cc5960d4d7ae0159b6b48c4d6b1f813be67cda196457ab2f88034 config-baseline.channel.json
|
||||
fffe0e74eab92a88c3c57952a70bc932438ce3a7f5f9982688437f2cdaee0bcb config-baseline.plugin.json
|
||||
|
||||
@@ -366,6 +366,7 @@ See [Plugins](/tools/plugin).
|
||||
// root: "dist/control-ui",
|
||||
// embedSandbox: "scripts", // strict | scripts | trusted
|
||||
// allowExternalEmbedUrls: false, // dangerous: allow absolute external http(s) embed URLs
|
||||
// chatMessageMaxWidth: "min(1280px, 82%)", // optional grouped chat message max-width
|
||||
// allowedOrigins: ["https://control.example.com"], // required for non-loopback Control UI
|
||||
// dangerouslyAllowHostHeaderOriginFallback: false, // dangerous Host-header origin fallback mode
|
||||
// allowInsecureAuth: false,
|
||||
@@ -427,6 +428,7 @@ See [Plugins](/tools/plugin).
|
||||
lock out a different origin.
|
||||
- `tailscale.mode`: `serve` (tailnet only, loopback bind) or `funnel` (public, requires auth).
|
||||
- `controlUi.allowedOrigins`: explicit browser-origin allowlist for Gateway WebSocket connects. Required when browser clients are expected from non-loopback origins.
|
||||
- `controlUi.chatMessageMaxWidth`: optional max-width for grouped Control UI chat messages. Accepts constrained CSS width values such as `960px`, `82%`, `min(1280px, 82%)`, and `calc(100% - 2rem)`.
|
||||
- `controlUi.dangerouslyAllowHostHeaderOriginFallback`: dangerous mode that enables Host-header origin fallback for deployments that intentionally rely on Host-header origin policy.
|
||||
- `remote.transport`: `ssh` (default) or `direct` (ws/wss). For `direct`, `remote.url` must be `ws://` or `wss://`.
|
||||
- `OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1`: client-side process-environment
|
||||
|
||||
@@ -247,6 +247,22 @@ Use `trusted` only when the embedded document genuinely needs same-origin behavi
|
||||
|
||||
Absolute external `http(s)` embed URLs stay blocked by default. If you intentionally want `[embed url="https://..."]` to load third-party pages, set `gateway.controlUi.allowExternalEmbedUrls: true`.
|
||||
|
||||
## Chat message width
|
||||
|
||||
Grouped chat messages use a readable default max-width. Wide-monitor deployments can override it without patching bundled CSS by setting `gateway.controlUi.chatMessageMaxWidth`:
|
||||
|
||||
```json5
|
||||
{
|
||||
gateway: {
|
||||
controlUi: {
|
||||
chatMessageMaxWidth: "min(1280px, 82%)",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The value is validated before it reaches the browser. Supported values include plain lengths and percentages such as `960px` or `82%`, plus constrained `min(...)`, `max(...)`, `clamp(...)`, `calc(...)`, and `fit-content(...)` width expressions.
|
||||
|
||||
## Tailnet access (recommended)
|
||||
|
||||
<Tabs>
|
||||
|
||||
@@ -296,6 +296,52 @@ describe("gateway.controlUi.allowExternalEmbedUrls", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("gateway.controlUi.chatMessageMaxWidth", () => {
|
||||
it("accepts constrained CSS width values", () => {
|
||||
for (const value of ["960px", "82%", "min(1280px, 82%)", "calc(100% - 2rem)"]) {
|
||||
const result = OpenClawSchema.safeParse({
|
||||
gateway: {
|
||||
controlUi: {
|
||||
chatMessageMaxWidth: value,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.gateway?.controlUi?.chatMessageMaxWidth).toBe(value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("normalizes whitespace around the width value", () => {
|
||||
const result = OpenClawSchema.safeParse({
|
||||
gateway: {
|
||||
controlUi: {
|
||||
chatMessageMaxWidth: " min(1280px, 82%) ",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.gateway?.controlUi?.chatMessageMaxWidth).toBe("min(1280px, 82%)");
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects arbitrary CSS injection", () => {
|
||||
for (const value of ["url(https://example.com/x)", "960px; color: red", "var(--x)"]) {
|
||||
const result = OpenClawSchema.safeParse({
|
||||
gateway: {
|
||||
controlUi: {
|
||||
chatMessageMaxWidth: value,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("plugins.entries.*.hooks", () => {
|
||||
it.each([true, false])("accepts allowConversationAccess=%s", (allowConversationAccess) => {
|
||||
const result = OpenClawSchema.safeParse({
|
||||
|
||||
60
src/config/control-ui-css.ts
Normal file
60
src/config/control-ui-css.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
const CSS_WIDTH_KEYWORDS = new Set(["none", "min-content", "max-content"]);
|
||||
const CSS_WIDTH_FUNCTIONS = new Set(["calc", "clamp", "fit-content", "max", "min"]);
|
||||
const CSS_WIDTH_UNITS = new Set(["ch", "em", "rem", "vh", "vmax", "vmin", "vw", "px"]);
|
||||
const CSS_WIDTH_ALLOWED_CHARS = /^[0-9A-Za-z.%+\-*/(),\s]+$/;
|
||||
const CSS_WIDTH_IDENTIFIER_RE = /[A-Za-z][A-Za-z0-9-]*/g;
|
||||
const CSS_WIDTH_SIMPLE_RE = /^(?:\d+(?:\.\d+)?|\.\d+)(?:px|rem|em|ch|vw|vh|vmin|vmax|%)$/i;
|
||||
const CSS_WIDTH_MAX_LENGTH = 96;
|
||||
|
||||
function hasBalancedParentheses(value: string): boolean {
|
||||
let depth = 0;
|
||||
for (const char of value) {
|
||||
if (char === "(") {
|
||||
depth++;
|
||||
} else if (char === ")") {
|
||||
depth--;
|
||||
if (depth < 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return depth === 0;
|
||||
}
|
||||
|
||||
function hasAllowedIdentifiers(value: string): boolean {
|
||||
for (const match of value.matchAll(CSS_WIDTH_IDENTIFIER_RE)) {
|
||||
const identifier = match[0].toLowerCase();
|
||||
if (
|
||||
!CSS_WIDTH_FUNCTIONS.has(identifier) &&
|
||||
!CSS_WIDTH_KEYWORDS.has(identifier) &&
|
||||
!CSS_WIDTH_UNITS.has(identifier)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function normalizeControlUiChatMessageMaxWidth(value: string): string {
|
||||
return value.trim().replace(/\s+/g, " ");
|
||||
}
|
||||
|
||||
export function isValidControlUiChatMessageMaxWidth(value: string): boolean {
|
||||
const normalized = normalizeControlUiChatMessageMaxWidth(value);
|
||||
if (normalized.length === 0 || normalized.length > CSS_WIDTH_MAX_LENGTH) {
|
||||
return false;
|
||||
}
|
||||
if (CSS_WIDTH_KEYWORDS.has(normalized.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
if (CSS_WIDTH_SIMPLE_RE.test(normalized)) {
|
||||
return true;
|
||||
}
|
||||
if (!CSS_WIDTH_ALLOWED_CHARS.test(normalized)) {
|
||||
return false;
|
||||
}
|
||||
if (!hasBalancedParentheses(normalized) || !hasAllowedIdentifiers(normalized)) {
|
||||
return false;
|
||||
}
|
||||
return /^(?:calc|clamp|fit-content|max|min)\(.+\)$/i.test(normalized);
|
||||
}
|
||||
@@ -22293,6 +22293,11 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
|
||||
description:
|
||||
"DANGEROUS toggle that allows hosted embeds to load absolute external http(s) URLs. Keep this off unless your Control UI intentionally embeds trusted third-party pages; hosted /__openclaw__/canvas and /__openclaw__/a2ui documents do not need it.",
|
||||
},
|
||||
chatMessageMaxWidth: {
|
||||
title: "Control UI Chat Message Max Width",
|
||||
description:
|
||||
'Optional CSS max-width for grouped Control UI chat messages, for example "960px", "82%", or "min(1280px, 82%)". Values are validated against a constrained width grammar before reaching the browser.',
|
||||
},
|
||||
allowedOrigins: {
|
||||
type: "array",
|
||||
items: {
|
||||
@@ -25988,6 +25993,11 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
|
||||
help: "DANGEROUS toggle that allows hosted embeds to load absolute external http(s) URLs. Keep this off unless your Control UI intentionally embeds trusted third-party pages; hosted /__openclaw__/canvas and /__openclaw__/a2ui documents do not need it.",
|
||||
tags: ["security", "access", "network", "advanced"],
|
||||
},
|
||||
"gateway.controlUi.chatMessageMaxWidth": {
|
||||
label: "Control UI Chat Message Max Width",
|
||||
help: 'Optional CSS max-width for grouped Control UI chat messages, for example "960px", "82%", or "min(1280px, 82%)". Values are validated against a constrained width grammar before reaching the browser.',
|
||||
tags: ["advanced"],
|
||||
},
|
||||
"gateway.controlUi.allowedOrigins": {
|
||||
label: "Control UI Allowed Origins",
|
||||
help: 'Allowed browser origins for Control UI/WebChat websocket connections (full origins only, e.g. https://control.example.com). Required for non-loopback Control UI deployments unless dangerous Host-header fallback is explicitly enabled. Setting ["*"] means allow any browser origin and should be avoided outside tightly controlled local testing.',
|
||||
|
||||
@@ -458,6 +458,8 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
'Iframe sandbox policy for hosted Control UI embeds. "strict" disables scripts, "scripts" allows interactive embeds while keeping origin isolation (default), and "trusted" adds `allow-same-origin` for same-site documents that intentionally need stronger privileges.',
|
||||
"gateway.controlUi.allowExternalEmbedUrls":
|
||||
"DANGEROUS toggle that allows hosted embeds to load absolute external http(s) URLs. Keep this off unless your Control UI intentionally embeds trusted third-party pages; hosted /__openclaw__/canvas and /__openclaw__/a2ui documents do not need it.",
|
||||
"gateway.controlUi.chatMessageMaxWidth":
|
||||
'Optional CSS max-width for grouped Control UI chat messages, for example "960px", "82%", or "min(1280px, 82%)". Values are validated against a constrained width grammar before reaching the browser.',
|
||||
"gateway.controlUi.allowedOrigins":
|
||||
'Allowed browser origins for Control UI/WebChat websocket connections (full origins only, e.g. https://control.example.com). Required for non-loopback Control UI deployments unless dangerous Host-header fallback is explicitly enabled. Setting ["*"] means allow any browser origin and should be avoided outside tightly controlled local testing.',
|
||||
"gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback":
|
||||
|
||||
@@ -308,6 +308,7 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
"gateway.controlUi.root": "Control UI Assets Root",
|
||||
"gateway.controlUi.embedSandbox": "Control UI Embed Sandbox Mode",
|
||||
"gateway.controlUi.allowExternalEmbedUrls": "Allow External Control UI Embed URLs",
|
||||
"gateway.controlUi.chatMessageMaxWidth": "Control UI Chat Message Max Width",
|
||||
"gateway.controlUi.allowedOrigins": "Control UI Allowed Origins",
|
||||
"gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback":
|
||||
"Dangerously Allow Host-Header Origin Fallback",
|
||||
|
||||
@@ -45,6 +45,7 @@ const TAG_OVERRIDES: Record<string, ConfigTag[]> = {
|
||||
"gateway.push.apns.relay.baseUrl": ["network", "advanced"],
|
||||
"gateway.controlUi.embedSandbox": ["security", "access", "advanced"],
|
||||
"gateway.controlUi.allowExternalEmbedUrls": ["security", "access", "network", "advanced"],
|
||||
"gateway.controlUi.chatMessageMaxWidth": ["advanced"],
|
||||
"gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback": [
|
||||
"security",
|
||||
"access",
|
||||
|
||||
@@ -99,6 +99,8 @@ export type GatewayControlUiConfig = {
|
||||
* Default off; prefer hosted /__openclaw__/canvas or /__openclaw__/a2ui content.
|
||||
*/
|
||||
allowExternalEmbedUrls?: boolean;
|
||||
/** Optional max-width for grouped Control UI chat messages (default: min(900px, 68%)). */
|
||||
chatMessageMaxWidth?: string;
|
||||
/** Allowed browser origins for Control UI/WebChat websocket connections. */
|
||||
allowedOrigins?: string[];
|
||||
/**
|
||||
|
||||
@@ -5,6 +5,10 @@ import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
normalizeStringifiedOptionalString,
|
||||
} from "../shared/string-coerce.js";
|
||||
import {
|
||||
isValidControlUiChatMessageMaxWidth,
|
||||
normalizeControlUiChatMessageMaxWidth,
|
||||
} from "./control-ui-css.js";
|
||||
import {
|
||||
SilentReplyPolicyConfigSchema,
|
||||
SilentReplyRewriteConfigSchema,
|
||||
@@ -784,6 +788,14 @@ export const OpenClawSchema = z
|
||||
.union([z.literal("strict"), z.literal("scripts"), z.literal("trusted")])
|
||||
.optional(),
|
||||
allowExternalEmbedUrls: z.boolean().optional(),
|
||||
chatMessageMaxWidth: z
|
||||
.string()
|
||||
.transform((value) => normalizeControlUiChatMessageMaxWidth(value))
|
||||
.refine((value) => isValidControlUiChatMessageMaxWidth(value), {
|
||||
message:
|
||||
"Expected a CSS width value such as 960px, 82%, min(1280px, 82%), or calc(100% - 2rem)",
|
||||
})
|
||||
.optional(),
|
||||
allowedOrigins: z.array(z.string()).optional(),
|
||||
dangerouslyAllowHostHeaderOriginFallback: z.boolean().optional(),
|
||||
allowInsecureAuth: z.boolean().optional(),
|
||||
|
||||
@@ -14,4 +14,5 @@ export type ControlUiBootstrapConfig = {
|
||||
localMediaPreviewRoots?: string[];
|
||||
embedSandbox?: ControlUiEmbedSandboxMode;
|
||||
allowExternalEmbedUrls?: boolean;
|
||||
chatMessageMaxWidth?: string;
|
||||
};
|
||||
|
||||
@@ -37,6 +37,7 @@ describe("handleControlUiHttpRequest", () => {
|
||||
assistantAvatar: string;
|
||||
assistantAgentId: string;
|
||||
localMediaPreviewRoots?: string[];
|
||||
chatMessageMaxWidth?: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -594,6 +595,7 @@ describe("handleControlUiHttpRequest", () => {
|
||||
root: { kind: "resolved", path: tmp },
|
||||
config: {
|
||||
agents: { defaults: { workspace: tmp } },
|
||||
gateway: { controlUi: { chatMessageMaxWidth: "min(1280px, 82%)" } },
|
||||
ui: { assistant: { name: "</script><script>alert(1)//", avatar: "</script>.png" } },
|
||||
},
|
||||
},
|
||||
@@ -604,6 +606,7 @@ describe("handleControlUiHttpRequest", () => {
|
||||
expect(parsed.assistantName).toBe("</script><script>alert(1)//");
|
||||
expect(parsed.assistantAvatar).toBe("/avatar/main");
|
||||
expect(parsed.assistantAgentId).toBe("main");
|
||||
expect(parsed.chatMessageMaxWidth).toBe("min(1280px, 82%)");
|
||||
expect(Array.isArray(parsed.localMediaPreviewRoots)).toBe(true);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -805,6 +805,7 @@ export async function handleControlUiHttpRequest(
|
||||
? "strict"
|
||||
: "scripts",
|
||||
allowExternalEmbedUrls: config?.gateway?.controlUi?.allowExternalEmbedUrls === true,
|
||||
chatMessageMaxWidth: config?.gateway?.controlUi?.chatMessageMaxWidth,
|
||||
} satisfies ControlUiBootstrapConfig);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
gap: 2px;
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
max-width: min(900px, 68%);
|
||||
max-width: var(--chat-message-max-width, min(900px, 68%));
|
||||
align-items: flex-start;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,15 @@ function readLayoutCss(): string {
|
||||
return readFileSync(cssPath!, "utf8");
|
||||
}
|
||||
|
||||
function readGroupedChatCss(): string {
|
||||
const cssPath = [
|
||||
resolve(process.cwd(), "ui/src/styles/chat/grouped.css"),
|
||||
resolve(process.cwd(), "..", "ui/src/styles/chat/grouped.css"),
|
||||
].find((candidate) => existsSync(candidate));
|
||||
expect(cssPath).toBeTruthy();
|
||||
return readFileSync(cssPath!, "utf8");
|
||||
}
|
||||
|
||||
describe("chat header responsive mobile styles", () => {
|
||||
it("keeps the chat header and session controls from clipping on narrow widths", () => {
|
||||
const css = readMobileCss();
|
||||
@@ -46,3 +55,11 @@ describe("sidebar menu trigger styles", () => {
|
||||
expect(css).toContain("display: none;");
|
||||
});
|
||||
});
|
||||
|
||||
describe("grouped chat width styles", () => {
|
||||
it("uses the config-fed CSS variable with the current fallback", () => {
|
||||
const css = readGroupedChatCss();
|
||||
|
||||
expect(css).toContain("max-width: var(--chat-message-max-width, min(900px, 68%));");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { html } from "lit";
|
||||
import { html, render } from "lit";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { AppViewState } from "./app-view-state.ts";
|
||||
import type { QuickSettingsProps } from "./views/config-quick.ts";
|
||||
@@ -89,6 +89,7 @@ function createState(overrides: Partial<AppViewState> = {}): AppViewState {
|
||||
localMediaPreviewRoots: [],
|
||||
embedSandboxMode: "scripts",
|
||||
allowExternalEmbedUrls: false,
|
||||
chatMessageMaxWidth: null,
|
||||
sessionKey: "main",
|
||||
chatLoading: false,
|
||||
chatSending: false,
|
||||
@@ -227,4 +228,16 @@ describe("renderApp assistant avatar routing", () => {
|
||||
expect(quickSettingsProps.current?.assistantAvatarReason).toBeNull();
|
||||
expect(quickSettingsProps.current?.assistantAvatarOverride).toBe(dataUrl);
|
||||
});
|
||||
|
||||
it("applies the configured chat message width as a shell CSS variable", () => {
|
||||
const container = document.createElement("div");
|
||||
|
||||
render(
|
||||
renderApp(createState({ tab: "chat", chatMessageMaxWidth: "min(1280px, 82%)" })),
|
||||
container,
|
||||
);
|
||||
|
||||
const shell = container.querySelector<HTMLElement>(".shell");
|
||||
expect(shell?.style.getPropertyValue("--chat-message-max-width")).toBe("min(1280px, 82%)");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { html, nothing } from "lit";
|
||||
import { styleMap } from "lit/directives/style-map.js";
|
||||
import { t } from "../i18n/index.ts";
|
||||
import { getSafeLocalStorage } from "../local-storage.ts";
|
||||
import { refreshChat } from "./app-chat.ts";
|
||||
@@ -1329,6 +1330,9 @@ export function renderApp(state: AppViewState) {
|
||||
: ""} ${navCollapsed ? "shell--nav-collapsed" : ""} ${navDrawerOpen
|
||||
? "shell--nav-drawer-open"
|
||||
: ""} ${state.onboarding ? "shell--onboarding" : ""}"
|
||||
style=${styleMap(
|
||||
state.chatMessageMaxWidth ? { "--chat-message-max-width": state.chatMessageMaxWidth } : {},
|
||||
)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -84,6 +84,7 @@ export type AppViewState = {
|
||||
localMediaPreviewRoots: string[];
|
||||
embedSandboxMode: EmbedSandboxMode;
|
||||
allowExternalEmbedUrls: boolean;
|
||||
chatMessageMaxWidth?: string | null;
|
||||
sessionKey: string;
|
||||
chatLoading: boolean;
|
||||
chatSending: boolean;
|
||||
|
||||
@@ -189,6 +189,7 @@ export class OpenClawApp extends LitElement {
|
||||
@state() localMediaPreviewRoots: string[] = [];
|
||||
@state() embedSandboxMode: "strict" | "scripts" | "trusted" = "scripts";
|
||||
@state() allowExternalEmbedUrls = false;
|
||||
@state() chatMessageMaxWidth: string | null = null;
|
||||
@state() serverVersion: string | null = null;
|
||||
|
||||
@state() sessionKey = this.settings.sessionKey;
|
||||
|
||||
@@ -20,6 +20,7 @@ describe("loadControlUiBootstrapConfig", () => {
|
||||
localMediaPreviewRoots: ["/tmp/openclaw"],
|
||||
embedSandbox: "scripts",
|
||||
allowExternalEmbedUrls: true,
|
||||
chatMessageMaxWidth: "min(1280px, 82%)",
|
||||
}),
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchMock as unknown as typeof fetch);
|
||||
@@ -35,6 +36,7 @@ describe("loadControlUiBootstrapConfig", () => {
|
||||
localMediaPreviewRoots: [],
|
||||
embedSandboxMode: "scripts" as const,
|
||||
allowExternalEmbedUrls: false,
|
||||
chatMessageMaxWidth: null,
|
||||
serverVersion: null,
|
||||
};
|
||||
|
||||
@@ -54,6 +56,7 @@ describe("loadControlUiBootstrapConfig", () => {
|
||||
expect(state.localMediaPreviewRoots).toEqual(["/tmp/openclaw"]);
|
||||
expect(state.embedSandboxMode).toBe("scripts");
|
||||
expect(state.allowExternalEmbedUrls).toBe(true);
|
||||
expect(state.chatMessageMaxWidth).toBe("min(1280px, 82%)");
|
||||
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
@@ -22,6 +22,7 @@ export type ControlUiBootstrapState = {
|
||||
localMediaPreviewRoots: string[];
|
||||
embedSandboxMode: ControlUiEmbedSandboxMode;
|
||||
allowExternalEmbedUrls: boolean;
|
||||
chatMessageMaxWidth?: string | null;
|
||||
sessionKey?: string | null;
|
||||
hello?: { auth?: { deviceToken?: string | null } | null } | null;
|
||||
settings?: { token?: string | null } | null;
|
||||
@@ -130,6 +131,10 @@ export async function loadControlUiBootstrapConfig(
|
||||
? "strict"
|
||||
: "scripts";
|
||||
state.allowExternalEmbedUrls = parsed.allowExternalEmbedUrls === true;
|
||||
state.chatMessageMaxWidth =
|
||||
typeof parsed.chatMessageMaxWidth === "string" && parsed.chatMessageMaxWidth.trim()
|
||||
? parsed.chatMessageMaxWidth
|
||||
: null;
|
||||
} catch {
|
||||
// Ignore bootstrap failures; UI will update identity after connecting.
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user