fix(control-ui): apply seamColor bootstrap config (#93699)

This commit is contained in:
Goutam Adwant
2026-06-28 22:31:31 -07:00
committed by GitHub
parent 527f20f4b5
commit 5715744ea0
5 changed files with 157 additions and 1 deletions
+1
View File
@@ -20,6 +20,7 @@ export type ControlUiBootstrapConfig = {
embedSandbox?: ControlUiEmbedSandboxMode;
allowExternalEmbedUrls?: boolean;
chatMessageMaxWidth?: string;
seamColor?: string;
/** Resolved `agents.defaults.timeFormat`; "auto" keeps the browser locale default. */
timeFormat?: "auto" | "12" | "24";
};
+6 -1
View File
@@ -47,6 +47,7 @@ describe("handleControlUiHttpRequest", () => {
assistantAgentId: string;
localMediaPreviewRoots?: string[];
chatMessageMaxWidth?: string;
seamColor?: string;
timeFormat?: "auto" | "12" | "24";
};
}
@@ -823,7 +824,10 @@ describe("handleControlUiHttpRequest", () => {
config: {
agents: { defaults: { workspace: tmp, timeFormat: "24" } },
gateway: { controlUi: { chatMessageMaxWidth: "min(1280px, 82%)" } },
ui: { assistant: { name: "</script><script>alert(1)//", avatar: "</script>.png" } },
ui: {
seamColor: "#1A2b3C",
assistant: { name: "</script><script>alert(1)//", avatar: "</script>.png" },
},
},
},
);
@@ -834,6 +838,7 @@ describe("handleControlUiHttpRequest", () => {
expect(parsed.assistantAvatar).toBe("/avatar/main");
expect(parsed.assistantAgentId).toBe("main");
expect(parsed.chatMessageMaxWidth).toBe("min(1280px, 82%)");
expect(parsed.seamColor).toBe("#1A2b3C");
expect(parsed.timeFormat).toBe("24");
expect(Array.isArray(parsed.localMediaPreviewRoots)).toBe(true);
},
+1
View File
@@ -995,6 +995,7 @@ export async function handleControlUiHttpRequest(
: "scripts",
allowExternalEmbedUrls: config?.gateway?.controlUi?.allowExternalEmbedUrls === true,
chatMessageMaxWidth: config?.gateway?.controlUi?.chatMessageMaxWidth,
seamColor: config?.ui?.seamColor,
timeFormat: config?.agents?.defaults?.timeFormat,
} satisfies ControlUiBootstrapConfig);
return true;
@@ -16,6 +16,7 @@ function requireFetchCall(fetchMock: ReturnType<typeof vi.fn>, index = 0) {
describe("loadControlUiBootstrapConfig", () => {
afterEach(() => {
setUiTimeFormatPreference("auto");
document.documentElement.removeAttribute("style");
});
it("threads agents.defaults.timeFormat into the UI hour-cycle preference", async () => {
@@ -108,6 +109,101 @@ describe("loadControlUiBootstrapConfig", () => {
vi.unstubAllGlobals();
});
it("applies configured seamColor to Control UI accent variables", async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({
basePath: "",
assistantName: "Main",
assistantAvatar: "M",
assistantAgentId: "main",
seamColor: "#1A2b3C",
}),
});
vi.stubGlobal("fetch", fetchMock as unknown as typeof fetch);
const state = {
basePath: "",
assistantName: "Assistant",
assistantAvatar: null,
assistantAvatarSource: null,
assistantAvatarStatus: null,
assistantAvatarReason: null,
assistantAgentId: null,
localMediaPreviewRoots: [],
embedSandboxMode: "scripts" as const,
allowExternalEmbedUrls: false,
chatMessageMaxWidth: null,
serverVersion: null,
};
await loadControlUiBootstrapConfig(state);
const rootStyle = document.documentElement.style;
expect(rootStyle.getPropertyValue("--accent")).toBe("#1A2b3C");
expect(rootStyle.getPropertyValue("--ring")).toBe("#1A2b3C");
expect(rootStyle.getPropertyValue("--primary")).toBe("#1A2b3C");
expect(rootStyle.getPropertyValue("--accent-hover")).toBe(
"color-mix(in srgb, var(--accent) 82%, white 18%)",
);
expect(rootStyle.getPropertyValue("--accent-subtle")).toBe(
"color-mix(in srgb, var(--accent) 16%, transparent)",
);
vi.unstubAllGlobals();
});
it("removes server seamColor variables when bootstrap color is missing or invalid", async () => {
const fetchMock = vi
.fn()
.mockResolvedValueOnce({
ok: true,
json: async () => ({
basePath: "",
assistantName: "Main",
assistantAvatar: "M",
assistantAgentId: "main",
seamColor: "00aaee",
}),
})
.mockResolvedValueOnce({
ok: true,
json: async () => ({
basePath: "",
assistantName: "Main",
assistantAvatar: "M",
assistantAgentId: "main",
seamColor: "lobster",
}),
});
vi.stubGlobal("fetch", fetchMock as unknown as typeof fetch);
const state = {
basePath: "",
assistantName: "Assistant",
assistantAvatar: null,
assistantAvatarSource: null,
assistantAvatarStatus: null,
assistantAvatarReason: null,
assistantAgentId: null,
localMediaPreviewRoots: [],
embedSandboxMode: "scripts" as const,
allowExternalEmbedUrls: false,
chatMessageMaxWidth: null,
serverVersion: null,
};
await loadControlUiBootstrapConfig(state);
expect(document.documentElement.style.getPropertyValue("--accent")).toBe("#00aaee");
await loadControlUiBootstrapConfig(state);
expect(document.documentElement.style.getPropertyValue("--accent")).toBe("");
expect(document.documentElement.style.getPropertyValue("--ring")).toBe("");
expect(document.documentElement.style.getPropertyValue("--focus-ring")).toBe("");
vi.unstubAllGlobals();
});
it("can refresh runtime bootstrap settings without clobbering session identity", async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
@@ -12,6 +12,19 @@ import { normalizeAgentId, parseAgentSessionKey } from "../session-key.ts";
import { loadLocalAssistantIdentity } from "../storage.ts";
import { normalizeOptionalString } from "../string-coerce.ts";
const SEAM_COLOR_CSS_VARIABLES = [
"--ring",
"--accent",
"--accent-hover",
"--accent-muted",
"--accent-subtle",
"--accent-glow",
"--primary",
"--focus",
"--focus-ring",
"--focus-glow",
] as const;
export type ControlUiBootstrapState = {
basePath: string;
assistantName: string;
@@ -31,6 +44,45 @@ export type ControlUiBootstrapState = {
password?: string | null;
};
function normalizeSeamColor(value: unknown): string | null {
if (typeof value !== "string") {
return null;
}
const hex = value.trim().replace(/^#/, "");
return /^[0-9a-fA-F]{6}$/.test(hex) ? `#${hex}` : null;
}
function applyControlUiSeamColor(value: unknown) {
if (typeof document === "undefined") {
return;
}
const root = document.documentElement;
const color = normalizeSeamColor(value);
if (!color) {
for (const property of SEAM_COLOR_CSS_VARIABLES) {
root.style.removeProperty(property);
}
return;
}
root.style.setProperty("--ring", color);
root.style.setProperty("--accent", color);
root.style.setProperty("--accent-hover", "color-mix(in srgb, var(--accent) 82%, white 18%)");
root.style.setProperty("--accent-muted", color);
root.style.setProperty("--accent-subtle", "color-mix(in srgb, var(--accent) 16%, transparent)");
root.style.setProperty("--accent-glow", "color-mix(in srgb, var(--accent) 30%, transparent)");
root.style.setProperty("--primary", color);
root.style.setProperty("--focus", "color-mix(in srgb, var(--ring) 22%, transparent)");
root.style.setProperty(
"--focus-ring",
"0 0 0 2px var(--bg), 0 0 0 3px color-mix(in srgb, var(--ring) 80%, transparent)",
);
root.style.setProperty(
"--focus-glow",
"0 0 0 2px var(--bg), 0 0 0 3px var(--ring), 0 0 16px var(--accent-glow)",
);
}
function resolveActiveAgentId(state: ControlUiBootstrapState): string | null {
const sessionAgentId = parseAgentSessionKey(state.sessionKey)?.agentId;
if (sessionAgentId) {
@@ -136,6 +188,7 @@ export async function loadControlUiBootstrapConfig(
typeof parsed.chatMessageMaxWidth === "string" && parsed.chatMessageMaxWidth.trim()
? parsed.chatMessageMaxWidth
: null;
applyControlUiSeamColor(parsed.seamColor);
setUiTimeFormatPreference(parsed.timeFormat);
} catch {
// Ignore bootstrap failures; UI will update identity after connecting.