fix(ui): fix web UI after tsdown migration and typing changes

This commit is contained in:
Gustavo Madeira Santana
2026-02-03 13:56:20 -05:00
parent 1c4db91593
commit 5935c4d23d
24 changed files with 499 additions and 43 deletions

View File

@@ -1,8 +1,8 @@
import type { IncomingMessage, ServerResponse } from "node:http";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import type { OpenClawConfig } from "../config/config.js";
import { resolveControlUiRootSync } from "../infra/control-ui-assets.js";
import { DEFAULT_ASSISTANT_IDENTITY, resolveAssistantIdentity } from "./assistant-identity.js";
import {
buildControlUiAvatarUrl,
@@ -17,34 +17,13 @@ export type ControlUiRequestOptions = {
basePath?: string;
config?: OpenClawConfig;
agentId?: string;
root?: ControlUiRootState;
};
function resolveControlUiRoot(): string | null {
const here = path.dirname(fileURLToPath(import.meta.url));
const execDir = (() => {
try {
return path.dirname(fs.realpathSync(process.execPath));
} catch {
return null;
}
})();
const candidates = [
// Packaged app: control-ui lives alongside the executable.
execDir ? path.resolve(execDir, "control-ui") : null,
// Running from dist: dist/gateway/control-ui.js -> dist/control-ui
path.resolve(here, "../control-ui"),
// Running from source: src/gateway/control-ui.ts -> dist/control-ui
path.resolve(here, "../../dist/control-ui"),
// Fallback to cwd (dev)
path.resolve(process.cwd(), "dist", "control-ui"),
].filter((dir): dir is string => Boolean(dir));
for (const dir of candidates) {
if (fs.existsSync(path.join(dir, "index.html"))) {
return dir;
}
}
return null;
}
export type ControlUiRootState =
| { kind: "resolved"; path: string }
| { kind: "invalid"; path: string }
| { kind: "missing" };
function contentTypeForExt(ext: string): string {
switch (ext) {
@@ -288,7 +267,32 @@ export function handleControlUiHttpRequest(
}
}
const root = resolveControlUiRoot();
const rootState = opts?.root;
if (rootState?.kind === "invalid") {
res.statusCode = 503;
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end(
`Control UI assets not found at ${rootState.path}. Build them with \`pnpm ui:build\` (auto-installs UI deps), or update gateway.controlUi.root.`,
);
return true;
}
if (rootState?.kind === "missing") {
res.statusCode = 503;
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end(
"Control UI assets not found. Build them with `pnpm ui:build` (auto-installs UI deps), or run `pnpm ui:dev` during development.",
);
return true;
}
const root =
rootState?.kind === "resolved"
? rootState.path
: resolveControlUiRootSync({
moduleUrl: import.meta.url,
argv1: process.argv[1],
cwd: process.cwd(),
});
if (!root) {
res.statusCode = 503;
res.setHeader("Content-Type", "text/plain; charset=utf-8");

View File

@@ -13,7 +13,11 @@ import { resolveAgentAvatar } from "../agents/identity-avatar.js";
import { handleA2uiHttpRequest } from "../canvas-host/a2ui.js";
import { loadConfig } from "../config/config.js";
import { handleSlackHttpRequest } from "../slack/http/index.js";
import { handleControlUiAvatarRequest, handleControlUiHttpRequest } from "./control-ui.js";
import {
handleControlUiAvatarRequest,
handleControlUiHttpRequest,
type ControlUiRootState,
} from "./control-ui.js";
import { applyHookMappings } from "./hooks-mapping.js";
import {
extractHookToken,
@@ -206,6 +210,7 @@ export function createGatewayHttpServer(opts: {
canvasHost: CanvasHostHandler | null;
controlUiEnabled: boolean;
controlUiBasePath: string;
controlUiRoot?: ControlUiRootState;
openAiChatCompletionsEnabled: boolean;
openResponsesEnabled: boolean;
openResponsesConfig?: import("../config/types.gateway.js").GatewayHttpResponsesConfig;
@@ -218,6 +223,7 @@ export function createGatewayHttpServer(opts: {
canvasHost,
controlUiEnabled,
controlUiBasePath,
controlUiRoot,
openAiChatCompletionsEnabled,
openResponsesEnabled,
openResponsesConfig,
@@ -301,6 +307,7 @@ export function createGatewayHttpServer(opts: {
handleControlUiHttpRequest(req, res, {
basePath: controlUiBasePath,
config: configSnapshot,
root: controlUiRoot,
})
) {
return;

View File

@@ -20,6 +20,7 @@ export type GatewayRuntimeConfig = {
openResponsesEnabled: boolean;
openResponsesConfig?: import("../config/types.gateway.js").GatewayHttpResponsesConfig;
controlUiBasePath: string;
controlUiRoot?: string;
resolvedAuth: ResolvedGatewayAuth;
authMode: ResolvedGatewayAuth["mode"];
tailscaleConfig: GatewayTailscaleConfig;
@@ -51,6 +52,11 @@ export async function resolveGatewayRuntimeConfig(params: {
const openResponsesConfig = params.cfg.gateway?.http?.endpoints?.responses;
const openResponsesEnabled = params.openResponsesEnabled ?? openResponsesConfig?.enabled ?? false;
const controlUiBasePath = normalizeControlUiBasePath(params.cfg.gateway?.controlUi?.basePath);
const controlUiRootRaw = params.cfg.gateway?.controlUi?.root;
const controlUiRoot =
typeof controlUiRootRaw === "string" && controlUiRootRaw.trim().length > 0
? controlUiRootRaw.trim()
: undefined;
const authBase = params.cfg.gateway?.auth ?? {};
const authOverrides = params.auth ?? {};
const authConfig = {
@@ -103,6 +109,7 @@ export async function resolveGatewayRuntimeConfig(params: {
? { ...openResponsesConfig, enabled: openResponsesEnabled }
: undefined,
controlUiBasePath,
controlUiRoot,
resolvedAuth,
authMode,
tailscaleConfig,

View File

@@ -6,6 +6,7 @@ import type { PluginRegistry } from "../plugins/registry.js";
import type { RuntimeEnv } from "../runtime.js";
import type { ResolvedGatewayAuth } from "./auth.js";
import type { ChatAbortControllerEntry } from "./chat-abort.js";
import type { ControlUiRootState } from "./control-ui.js";
import type { HooksConfigResolved } from "./hooks.js";
import type { DedupeEntry } from "./server-shared.js";
import type { GatewayTlsRuntime } from "./server/tls.js";
@@ -27,6 +28,7 @@ export async function createGatewayRuntimeState(params: {
port: number;
controlUiEnabled: boolean;
controlUiBasePath: string;
controlUiRoot?: ControlUiRootState;
openAiChatCompletionsEnabled: boolean;
openResponsesEnabled: boolean;
openResponsesConfig?: import("../config/types.gateway.js").GatewayHttpResponsesConfig;
@@ -112,6 +114,7 @@ export async function createGatewayRuntimeState(params: {
canvasHost,
controlUiEnabled: params.controlUiEnabled,
controlUiBasePath: params.controlUiBasePath,
controlUiRoot: params.controlUiRoot,
openAiChatCompletionsEnabled: params.openAiChatCompletionsEnabled,
openResponsesEnabled: params.openResponsesEnabled,
openResponsesConfig: params.openResponsesConfig,

View File

@@ -1,6 +1,8 @@
import path from "node:path";
import type { CanvasHostServer } from "../canvas-host/server.js";
import type { PluginServicesHandle } from "../plugins/services.js";
import type { RuntimeEnv } from "../runtime.js";
import type { ControlUiRootState } from "./control-ui.js";
import type { startBrowserControlServerIfEnabled } from "./server-browser.js";
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
import { registerSkillsChangeListener } from "../agents/skills/refresh.js";
@@ -18,6 +20,11 @@ import {
} from "../config/config.js";
import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js";
import { clearAgentRunContext, onAgentEvent } from "../infra/agent-events.js";
import {
ensureControlUiAssetsBuilt,
resolveControlUiRootOverrideSync,
resolveControlUiRootSync,
} from "../infra/control-ui-assets.js";
import { isDiagnosticsEnabled } from "../infra/diagnostic-events.js";
import { logAcceptedEnvOption } from "../infra/env.js";
import { createExecApprovalForwarder } from "../infra/exec-approval-forwarder.js";
@@ -87,6 +94,7 @@ const logReload = log.child("reload");
const logHooks = log.child("hooks");
const logPlugins = log.child("plugins");
const logWsControl = log.child("ws");
const gatewayRuntime = runtimeForLogger(log);
const canvasRuntime = runtimeForLogger(logCanvas);
export type GatewayServer = {
@@ -253,6 +261,7 @@ export async function startGatewayServer(
openResponsesEnabled,
openResponsesConfig,
controlUiBasePath,
controlUiRoot: controlUiRootOverride,
resolvedAuth,
tailscaleConfig,
tailscaleMode,
@@ -260,6 +269,38 @@ export async function startGatewayServer(
let hooksConfig = runtimeConfig.hooksConfig;
const canvasHostEnabled = runtimeConfig.canvasHostEnabled;
let controlUiRootState: ControlUiRootState | undefined;
if (controlUiRootOverride) {
const resolvedOverride = resolveControlUiRootOverrideSync(controlUiRootOverride);
const resolvedOverridePath = path.resolve(controlUiRootOverride);
controlUiRootState = resolvedOverride
? { kind: "resolved", path: resolvedOverride }
: { kind: "invalid", path: resolvedOverridePath };
if (!resolvedOverride) {
log.warn(`gateway: controlUi.root not found at ${resolvedOverridePath}`);
}
} else if (controlUiEnabled) {
let resolvedRoot = resolveControlUiRootSync({
moduleUrl: import.meta.url,
argv1: process.argv[1],
cwd: process.cwd(),
});
if (!resolvedRoot) {
const ensureResult = await ensureControlUiAssetsBuilt(gatewayRuntime);
if (!ensureResult.ok && ensureResult.message) {
log.warn(`gateway: ${ensureResult.message}`);
}
resolvedRoot = resolveControlUiRootSync({
moduleUrl: import.meta.url,
argv1: process.argv[1],
cwd: process.cwd(),
});
}
controlUiRootState = resolvedRoot
? { kind: "resolved", path: resolvedRoot }
: { kind: "missing" };
}
const wizardRunner = opts.wizardRunner ?? runOnboardingWizard;
const { wizardSessions, findRunningWizard, purgeWizardSession } = createWizardSessionTracker();
@@ -291,6 +332,7 @@ export async function startGatewayServer(
port,
controlUiEnabled,
controlUiBasePath,
controlUiRoot: controlUiRootState,
openAiChatCompletionsEnabled,
openResponsesEnabled,
openResponsesConfig,