diff --git a/apps/macos/Sources/Clawdis/MenuContentView.swift b/apps/macos/Sources/Clawdis/MenuContentView.swift index bee233769..448cf5c71 100644 --- a/apps/macos/Sources/Clawdis/MenuContentView.swift +++ b/apps/macos/Sources/Clawdis/MenuContentView.swift @@ -248,7 +248,7 @@ struct MenuContent: View { default: components.scheme = "http" } - components.path = "/ui/" + components.path = "/" components.query = nil guard let url = components.url else { throw NSError(domain: "Dashboard", code: 2, userInfo: [ diff --git a/docs/control-ui.md b/docs/control-ui.md index 35f0f1d91..e8db8707e 100644 --- a/docs/control-ui.md +++ b/docs/control-ui.md @@ -8,7 +8,8 @@ read_when: The Control UI is a small **Vite + Lit** single-page app served by the Gateway under: -- `http://:18789/ui/` +- `http://:18789/` (preferred) +- `http://:18789/ui/` (legacy alias) It speaks **directly to the Gateway WebSocket** on the same port. @@ -48,4 +49,3 @@ pnpm ui:dev ``` Then point the UI at your Gateway WS URL (e.g. `ws://127.0.0.1:18789`). - diff --git a/src/config/config.ts b/src/config/config.ts index adead7b83..4d84d8316 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -102,7 +102,7 @@ export type CanvasHostConfig = { }; export type GatewayControlUiConfig = { - /** If false, the Gateway will not serve the Control UI under /ui/. Default: true. */ + /** If false, the Gateway will not serve the Control UI (/, /ui/). Default: true. */ enabled?: boolean; }; diff --git a/src/gateway/control-ui.ts b/src/gateway/control-ui.ts index 1268b509c..c722de89b 100644 --- a/src/gateway/control-ui.ts +++ b/src/gateway/control-ui.ts @@ -4,6 +4,7 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; const UI_PREFIX = "/ui/"; +const ROOT_PREFIX = "/"; function resolveControlUiRoot(): string | null { const here = path.dirname(fileURLToPath(import.meta.url)); @@ -88,13 +89,11 @@ export function handleControlUiHttpRequest( if (url.pathname === "/ui") { res.statusCode = 302; - res.setHeader("Location", UI_PREFIX); + res.setHeader("Location", "/"); res.end(); return true; } - if (!url.pathname.startsWith(UI_PREFIX)) return false; - const root = resolveControlUiRoot(); if (!root) { res.statusCode = 503; @@ -105,7 +104,12 @@ export function handleControlUiHttpRequest( return true; } - const rel = url.pathname.slice(UI_PREFIX.length); + const rel = (() => { + if (url.pathname === ROOT_PREFIX) return ""; + if (url.pathname.startsWith(UI_PREFIX)) return url.pathname.slice(UI_PREFIX.length); + if (url.pathname.startsWith("/assets/")) return url.pathname.slice(1); + return url.pathname.slice(1); + })(); const requested = rel && !rel.endsWith("/") ? rel : `${rel}index.html`; const fileRel = requested || "index.html"; if (!isSafeRelativePath(fileRel)) { diff --git a/src/gateway/server.ts b/src/gateway/server.ts index 2198c31ac..6e236c680 100644 --- a/src/gateway/server.ts +++ b/src/gateway/server.ts @@ -848,12 +848,6 @@ export async function startGatewayServer( if (String(req.headers.upgrade ?? "").toLowerCase() === "websocket") return; if (controlUiEnabled) { - if (req.url === "/") { - res.statusCode = 302; - res.setHeader("Location", "/ui/"); - res.end(); - return; - } if (handleControlUiHttpRequest(req, res)) return; } diff --git a/ui/tsconfig.json b/ui/tsconfig.json index 9d70bb949..85d70e937 100644 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -5,6 +5,7 @@ "moduleResolution": "Bundler", "lib": ["ES2022", "DOM", "DOM.Iterable"], "strict": true, + "experimentalDecorators": true, "skipLibCheck": true, "types": ["vite/client"], "useDefineForClassFields": false diff --git a/ui/vite.config.ts b/ui/vite.config.ts index 9361e01e3..dbb26cfbd 100644 --- a/ui/vite.config.ts +++ b/ui/vite.config.ts @@ -5,7 +5,7 @@ import { defineConfig } from "vite"; const here = path.dirname(fileURLToPath(import.meta.url)); export default defineConfig({ - base: "/ui/", + base: "/", build: { outDir: path.resolve(here, "../dist/control-ui"), emptyOutDir: true,