From bd41cf377a1302316d06b2aa696e9ea42843a6ad Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 9 Dec 2025 21:07:53 +0000 Subject: [PATCH] feat(webchat): auto-start at root --- docs/mac/webchat.md | 2 +- docs/webchat.md | 7 ++++--- src/cli/program.test.ts | 2 +- src/cli/program.ts | 25 ++++++++++++++++++++++--- src/gateway/server.test.ts | 3 +++ src/gateway/server.ts | 8 ++++++++ src/webchat/server.ts | 6 ++++-- 7 files changed, 43 insertions(+), 10 deletions(-) diff --git a/docs/mac/webchat.md b/docs/mac/webchat.md index c2a045aae..75a9b495d 100644 --- a/docs/mac/webchat.md +++ b/docs/mac/webchat.md @@ -14,7 +14,7 @@ The macOS menu bar app opens the gateway’s loopback web chat server in a WKWeb - WK logs: navigation lifecycle, readyState, js location, and JS errors/unhandled rejections are mirrored to OSLog for easier diagnosis. ## How it’s wired -- Assets: `apps/macos/Sources/Clawdis/Resources/WebChat/` contains the `pi-web-ui` dist plus a local import map pointing at bundled vendor modules and a tiny `pi-ai` stub. Everything is served from the gateway at `/webchat/*`. +- Assets: `apps/macos/Sources/Clawdis/Resources/WebChat/` contains the `pi-web-ui` dist plus a local import map pointing at bundled vendor modules and a tiny `pi-ai` stub. Everything is served from the gateway at `/` (legacy `/webchat/*` still works). - Bridge: none. The web UI calls `/webchat/rpc` directly; Swift no longer proxies messages. RPC is handled in-process inside the gateway (no CLI spawn/PATH dependency). - Session: always primary; multiple transports (WhatsApp/Telegram/Desktop) share the same session key so context is unified. diff --git a/docs/webchat.md b/docs/webchat.md index f368fd1e5..cc1a8ee0a 100644 --- a/docs/webchat.md +++ b/docs/webchat.md @@ -15,9 +15,10 @@ Updated: 2025-12-09 - `webchat.gatewayPort` config can point at a non-default Gateway port if needed. ## Endpoints -- `GET /webchat/info?session=` → `{ port, sessionId, initialMessages, basePath }` plus history from the Gateway session store. -- `GET /webchat/*` → static assets. -- `POST /webchat/rpc` → proxies a chat/agent action through the Gateway connection and returns `{ ok, payloads?, error? }`. +- UI is now served at the root: `http://127.0.0.1:/` (legacy `/webchat/` still works). +- `GET /webchat/info?session=` (alias `/info`) → `{ port, sessionId, initialMessages, basePath }` plus history from the Gateway session store. +- `GET /` (or `/webchat/*`) → static assets. +- `POST /webchat/rpc` (alias `/rpc`) → proxies a chat/agent action through the Gateway connection and returns `{ ok, payloads?, error? }`. ## How it connects - On startup, the WebChat server dials the Gateway WebSocket and performs the mandatory `hello` handshake; the `hello-ok` snapshot seeds presence + health immediately. diff --git a/src/cli/program.test.ts b/src/cli/program.test.ts index 239a21cf2..56cffd215 100644 --- a/src/cli/program.test.ts +++ b/src/cli/program.test.ts @@ -56,7 +56,7 @@ describe("cli program", () => { await program.parseAsync(["webchat", "--json"], { from: "user" }); expect(startWebChatServer).toHaveBeenCalled(); expect(runtime.log).toHaveBeenCalledWith( - JSON.stringify({ port: 18788, basePath: "/webchat/", host: "127.0.0.1" }), + JSON.stringify({ port: 18788, basePath: "/", host: "127.0.0.1" }), ); }); }); diff --git a/src/cli/program.ts b/src/cli/program.ts index 98856e634..b6cba2b4c 100644 --- a/src/cli/program.ts +++ b/src/cli/program.ts @@ -13,7 +13,10 @@ import { loginWeb, logoutWeb } from "../provider-web.js"; import { runRpcLoop } from "../rpc/loop.js"; import { defaultRuntime } from "../runtime.js"; import { VERSION } from "../version.js"; -import { startWebChatServer } from "../webchat/server.js"; +import { + ensureWebChatServerFromConfig, + startWebChatServer, +} from "../webchat/server.js"; import { createDefaultDeps } from "./deps.js"; import { forceFreePort, @@ -282,6 +285,22 @@ Examples: } try { await startGatewayServer(port); + try { + const webchat = await ensureWebChatServerFromConfig({ + gatewayUrl: `ws://127.0.0.1:${port}`, + }); + if (webchat) { + defaultRuntime.log( + info( + `webchat listening on http://127.0.0.1:${webchat.port}/`, + ), + ); + } else { + defaultRuntime.log(info("webchat disabled via config")); + } + } catch (webchatErr) { + defaultRuntime.error(`WebChat failed to start: ${String(webchatErr)}`); + } } catch (err) { if (err instanceof GatewayLockError) { defaultRuntime.error(`Gateway failed to start: ${err.message}`); @@ -588,14 +607,14 @@ Shows token usage per session when the agent reports it; set inbound.reply.agent const server = await startWebChatServer(port); const payload = { port: server.port, - basePath: "/webchat/", + basePath: "/", host: "127.0.0.1", }; if (opts.json) { defaultRuntime.log(JSON.stringify(payload)); } else { defaultRuntime.log( - info(`webchat listening on http://127.0.0.1:${server.port}/webchat/`), + info(`webchat listening on http://127.0.0.1:${server.port}/`), ); } }); diff --git a/src/gateway/server.test.ts b/src/gateway/server.test.ts index 449d50917..b54ee5f40 100644 --- a/src/gateway/server.test.ts +++ b/src/gateway/server.test.ts @@ -10,6 +10,9 @@ vi.mock("../commands/health.js", () => ({ vi.mock("../commands/status.js", () => ({ getStatusSummary: vi.fn().mockResolvedValue({ ok: true }), })); +vi.mock("../webchat/server.js", () => ({ + ensureWebChatServerFromConfig: vi.fn().mockResolvedValue(null), +})); vi.mock("../web/outbound.js", () => ({ sendMessageWhatsApp: vi .fn() diff --git a/src/gateway/server.ts b/src/gateway/server.ts index 581d4c653..144f8f080 100644 --- a/src/gateway/server.ts +++ b/src/gateway/server.ts @@ -22,6 +22,7 @@ import { defaultRuntime } from "../runtime.js"; import { monitorTelegramProvider } from "../telegram/monitor.js"; import { sendMessageTelegram } from "../telegram/send.js"; import { sendMessageWhatsApp } from "../web/outbound.js"; +import { ensureWebChatServerFromConfig } from "../webchat/server.js"; import { ErrorCodes, type ErrorShape, @@ -730,6 +731,13 @@ export async function startGatewayServer(port = 18789): Promise { ); defaultRuntime.log(`gateway log file: ${getResolvedLoggerSettings().file}`); + // Start loopback WebChat server (unless disabled via config). + void ensureWebChatServerFromConfig({ + gatewayUrl: `ws://127.0.0.1:${port}`, + }).catch((err) => { + logError(`gateway: webchat failed to start: ${String(err)}`); + }); + // Launch configured providers (WhatsApp Web, Telegram) so gateway replies via the // surface the message came from. Tests can opt out via CLAWDIS_SKIP_PROVIDERS. if (process.env.CLAWDIS_SKIP_PROVIDERS !== "1") { diff --git a/src/webchat/server.ts b/src/webchat/server.ts index 9750d3b32..d24dc9570 100644 --- a/src/webchat/server.ts +++ b/src/webchat/server.ts @@ -557,12 +557,14 @@ export function __broadcastGatewayEventForTests( broadcastAll({ type: "gateway-event", event, payload }); } -export async function ensureWebChatServerFromConfig() { +export async function ensureWebChatServerFromConfig(opts?: { + gatewayUrl?: string; +}) { const cfg = loadConfig(); if (cfg.webchat?.enabled === false) return null; const port = cfg.webchat?.port ?? WEBCHAT_DEFAULT_PORT; try { - return await startWebChatServer(port); + return await startWebChatServer(port, opts?.gatewayUrl); } catch (err) { logDebug(`webchat server failed to start: ${String(err)}`); throw err;