feat(webchat): auto-start at root

This commit is contained in:
Peter Steinberger
2025-12-09 21:07:53 +00:00
parent 3ee3f7e30b
commit bd41cf377a
7 changed files with 43 additions and 10 deletions

View File

@@ -14,7 +14,7 @@ The macOS menu bar app opens the gateways 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 its 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.

View File

@@ -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=<key>``{ 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:<port>/` (legacy `/webchat/` still works).
- `GET /webchat/info?session=<key>` (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.

View File

@@ -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" }),
);
});
});

View File

@@ -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}/`),
);
}
});

View File

@@ -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()

View File

@@ -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<GatewayServer> {
);
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") {

View File

@@ -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;