fix(ui): propagate user accent to Talk Mode and widget frames (#128577)

talk.config forwarded only the operator ui.seamColor, so a user-selected
ui.prefs.accent never reached the native Talk overlay; it now wins with
the same precedence the Control UI applies (regression test fails
pre-fix). The widget-theme observer now explicitly watches the <html>
style attribute that carries the inline accent override instead of
relying on incidental data-theme re-sets.
This commit is contained in:
Peter Steinberger
2026-08-24 00:16:06 -07:00
committed by GitHub
parent 538791a34c
commit 4dc083dc29
4 changed files with 34 additions and 3 deletions
+20
View File
@@ -1118,6 +1118,26 @@ describe("talk.config handler", () => {
},
);
it("prefers the user accent over the operator seam color", async () => {
markTalkOwnerCold("tts");
const runtimeConfig = createTalkConfig("healthy-talk-key");
mocks.getSpeechProvider.mockReturnValue({ id: "acme" });
mocks.readConfigFileSnapshot.mockResolvedValue({
config: { ...runtimeConfig, ui: { seamColor: "#123456", prefs: { accent: "#52c99a" } } },
});
const respond = vi.fn();
await callTalkHandler("talk.config", {
params: {},
client: { connect: { scopes: ["operator.read"] } },
respond,
context: { getRuntimeConfig: () => runtimeConfig },
});
expect(respond.mock.calls[0]?.[0]).toBe(true);
expect(respond.mock.calls[0]?.[1]?.config?.ui).toEqual({ seamColor: "#52c99a" });
});
it("projects the runtime realtime transport when source config is invalid", async () => {
mocks.readConfigFileSnapshot.mockResolvedValue({
path: "/tmp/openclaw.json",
+3 -1
View File
@@ -802,7 +802,9 @@ export const talkHandlers: GatewayRequestHandlers = {
configPayload.session = { mainKey: sessionMainKey };
}
const seamColor = snapshot.config.ui?.seamColor;
// User accent wins over the operator seam color, matching Control UI
// precedence (ui.prefs.accent -> ui.seamColor -> theme default).
const seamColor = snapshot.config.ui?.prefs?.accent ?? snapshot.config.ui?.seamColor;
if (typeof seamColor === "string") {
configPayload.ui = { seamColor };
}
+3 -1
View File
@@ -94,6 +94,8 @@ export function installWidgetThemeObserver(): void {
}
}).observe(root, {
attributes: true,
attributeFilter: ["data-theme", "data-theme-mode"],
// "style" carries the accent override (inline custom properties on <html>);
// without it a user accent change would only reach frames incidentally.
attributeFilter: ["data-theme", "data-theme-mode", "style"],
});
}
@@ -130,7 +130,7 @@ describe("widget theme bridge", () => {
document.documentElement,
{
attributes: true,
attributeFilter: ["data-theme", "data-theme-mode"],
attributeFilter: ["data-theme", "data-theme-mode", "style"],
},
);
FakeMutationObserver.instances[0]?.trigger({
@@ -139,5 +139,12 @@ describe("widget theme bridge", () => {
expect(chatPost).toHaveBeenCalledOnce();
expect(boardPost).toHaveBeenCalledOnce();
expect(unrelatedPost).not.toHaveBeenCalled();
// Accent overrides land as inline style mutations on <html>.
FakeMutationObserver.instances[0]?.trigger({
attributeName: "style",
} as MutationRecord);
expect(chatPost).toHaveBeenCalledTimes(2);
expect(boardPost).toHaveBeenCalledTimes(2);
expect(unrelatedPost).not.toHaveBeenCalled();
});
});