Merge a8af6892b94452899cc8808a74837163cc7b8147 into 598f1826d8b2bc969aace2c6459824737667218c

This commit is contained in:
Ryan 2026-03-21 03:15:10 +00:00 committed by GitHub
commit 36f9ca0eca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 65 additions and 0 deletions

View File

@ -99,6 +99,8 @@ export type TalkConfigResponse = TalkConfig & {
export type GatewayControlUiConfig = {
/** If false, the Gateway will not serve the Control UI (default /). */
enabled?: boolean;
/** Custom browser tab title for the Control UI (default: "OpenClaw Control"). */
title?: string;
/** Optional base path prefix for the Control UI (e.g. "/openclaw"). */
basePath?: string;
/** Optional filesystem root for Control UI assets (defaults to dist/control-ui). */

View File

@ -667,6 +667,7 @@ export const OpenClawSchema = z
controlUi: z
.object({
enabled: z.boolean().optional(),
title: z.string().optional(),
basePath: z.string().optional(),
root: z.string().optional(),
allowedOrigins: z.array(z.string()).optional(),

View File

@ -6,4 +6,5 @@ export type ControlUiBootstrapConfig = {
assistantAvatar: string;
assistantAgentId: string;
serverVersion?: string;
title?: string;
};

View File

@ -358,6 +358,7 @@ export function handleControlUiHttpRequest(
assistantAvatar: avatarValue ?? identity.avatar,
assistantAgentId: identity.agentId,
serverVersion: resolveRuntimeServiceVersion(process.env),
title: config?.gateway?.controlUi?.title,
} satisfies ControlUiBootstrapConfig);
return true;
}

View File

@ -63,6 +63,62 @@ describe("loadControlUiBootstrapConfig", () => {
vi.unstubAllGlobals();
});
it("sets document.title when title is present in bootstrap config", async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({
basePath: "",
assistantName: "Ops",
assistantAvatar: "O",
assistantAgentId: "main",
title: "Prod Gateway",
}),
});
vi.stubGlobal("fetch", fetchMock as unknown as typeof fetch);
const state = {
basePath: "",
assistantName: "Assistant",
assistantAvatar: null,
assistantAgentId: null,
serverVersion: null,
};
await loadControlUiBootstrapConfig(state);
expect(document.title).toBe("Prod Gateway");
vi.unstubAllGlobals();
});
it("does not override document.title when title is not set", async () => {
document.title = "OpenClaw Control";
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({
basePath: "",
assistantName: "Ops",
assistantAvatar: "O",
assistantAgentId: "main",
}),
});
vi.stubGlobal("fetch", fetchMock as unknown as typeof fetch);
const state = {
basePath: "",
assistantName: "Assistant",
assistantAvatar: null,
assistantAgentId: null,
serverVersion: null,
};
await loadControlUiBootstrapConfig(state);
expect(document.title).toBe("OpenClaw Control");
vi.unstubAllGlobals();
});
it("normalizes trailing slash basePath for bootstrap fetch path", async () => {
const fetchMock = vi.fn().mockResolvedValue({ ok: false });
vi.stubGlobal("fetch", fetchMock as unknown as typeof fetch);

View File

@ -45,6 +45,9 @@ export async function loadControlUiBootstrapConfig(state: ControlUiBootstrapStat
state.assistantAvatar = normalized.avatar;
state.assistantAgentId = normalized.agentId ?? null;
state.serverVersion = parsed.serverVersion ?? null;
if (parsed.title) {
document.title = parsed.title;
}
} catch {
// Ignore bootstrap failures; UI will update identity after connecting.
}

View File

@ -48,6 +48,7 @@ export default defineConfig({
"ui/src/ui/views/usage-render-details.test.ts",
"ui/src/ui/controllers/agents.test.ts",
"ui/src/ui/controllers/chat.test.ts",
"ui/src/ui/controllers/control-ui-bootstrap.test.ts",
"ui/src/ui/controllers/sessions.test.ts",
"ui/src/ui/app-gateway.sessions.node.test.ts",
],