From 6236d6e17feade2a54133f5d499c5db30301e7b5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Jun 2026 04:23:15 +0000 Subject: [PATCH 1/6] chore: plan vercel output directory fix --- web/tsconfig.tsbuildinfo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/tsconfig.tsbuildinfo b/web/tsconfig.tsbuildinfo index 9acdc09..eecbec0 100644 --- a/web/tsconfig.tsbuildinfo +++ b/web/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/App.tsx","./src/main.tsx","./src/ws.ts","./src/pages/Activity.tsx","./src/pages/Chat.tsx","./src/pages/Datasets.tsx","./src/pages/EmbedSearch.tsx","./src/pages/Evals.tsx","./src/pages/FineTune.tsx","./src/pages/Mcp.tsx","./src/pages/Metrics.tsx","./src/pages/Overview.tsx","./src/pages/Prompts.tsx","./src/pages/Providers.tsx","./src/pages/Sandbox.tsx","./src/pages/Search.tsx","./src/pages/SessionDetail.tsx","./src/pages/Sessions.tsx","./src/pages/Templates.tsx","./src/pages/Terminal.tsx","./src/pages/WhatsApp.tsx"],"errors":true,"version":"6.0.2"} \ No newline at end of file +{"root":["./src/App.tsx","./src/main.tsx","./src/ws.ts","./src/pages/Activity.tsx","./src/pages/Chat.tsx","./src/pages/Datasets.tsx","./src/pages/EmbedSearch.tsx","./src/pages/Evals.tsx","./src/pages/FineTune.tsx","./src/pages/Mcp.tsx","./src/pages/Metrics.tsx","./src/pages/Overview.tsx","./src/pages/Prompts.tsx","./src/pages/Providers.tsx","./src/pages/Sandbox.tsx","./src/pages/Search.tsx","./src/pages/SessionDetail.tsx","./src/pages/Sessions.tsx","./src/pages/Templates.tsx","./src/pages/Terminal.tsx","./src/pages/WhatsApp.tsx"],"version":"5.9.3"} \ No newline at end of file From 210ffc9b9315b7a346d7c3747a67afec27303b76 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Jun 2026 04:23:24 +0000 Subject: [PATCH 2/6] fix: configure vercel frontend output directory --- vercel.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 vercel.json diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..cc09120 --- /dev/null +++ b/vercel.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "installCommand": "npm install --prefix web", + "buildCommand": "cd web && npm run build", + "outputDirectory": "web/dist" +} From ec023388e100effe03bf8422d10985645c0da13c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Jun 2026 04:38:48 +0000 Subject: [PATCH 3/6] fix: human-readable WS error, auto-reconnect, Overview re-fetches on reconnect --- web/src/pages/Overview.tsx | 20 +++++++++++++++----- web/src/ws.ts | 19 +++++++++++++++++-- 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/web/src/pages/Overview.tsx b/web/src/pages/Overview.tsx index 8345d63..406ba04 100644 --- a/web/src/pages/Overview.tsx +++ b/web/src/pages/Overview.tsx @@ -1,5 +1,5 @@ -import { useEffect, useState } from "react"; -import { call, onEvent } from "../ws.ts"; +import { useEffect, useState, useCallback } from "react"; +import { call, onEvent, onStatus } from "../ws.ts"; import { Link } from "react-router-dom"; interface Stats { @@ -121,7 +121,8 @@ export default function Overview() { const [recentTokens, setRecentTokens] = useState<{ ts: number }[]>([]); const [error, setError] = useState(null); - useEffect(() => { + const fetchData = useCallback(() => { + setError(null); call("sessions.stats").then(setStats).catch((e: Error) => setError(e.message)); call<{ sessions: Session[] }>("sessions.list", { limit: 5 }) .then((r) => setRecentSessions(r.sessions)) @@ -164,6 +165,15 @@ export default function Overview() { ); setLive((prev) => ({ ...prev, totalOutputToday: todayTotal })); }).catch(() => {}); + }, []); + + useEffect(() => { + fetchData(); + + // Re-fetch whenever the gate reconnects + const offStatus = onStatus((connected) => { + if (connected) fetchData(); + }); // Real-time token stream tracking const offEvent = onEvent((event, payload) => { @@ -195,8 +205,8 @@ export default function Overview() { setRecentTokens((prev) => prev.filter((t) => now - t.ts < 5_000)); }, 1000); - return () => { offEvent(); clearInterval(ticker); }; - }, []); + return () => { offStatus(); offEvent(); clearInterval(ticker); }; + }, [fetchData]); const totalTokens = stats ? stats.totalInputTokens + stats.totalOutputTokens : 0; const onlineCount = providers.filter((p) => p.ok).length; diff --git a/web/src/ws.ts b/web/src/ws.ts index 8149ef8..73a304b 100644 --- a/web/src/ws.ts +++ b/web/src/ws.ts @@ -5,6 +5,9 @@ type StatusHandler = (connected: boolean) => void; let ws: WebSocket | null = null; let connectPromise: Promise | null = null; let isConnected = false; +let reconnectTimer: ReturnType | null = null; +let reconnectAttempt = 0; +const MAX_RECONNECT_DELAY_MS = 30_000; const pending = new Map void; reject: (e: Error) => void }>(); const eventHandlers = new Set(); const rawHandlers = new Set(); @@ -24,6 +27,16 @@ function notifyStatus(connected: boolean) { for (const h of statusHandlers) h(connected); } +function scheduleReconnect() { + if (reconnectTimer !== null) return; + const delay = Math.min(1_000 * 2 ** reconnectAttempt, MAX_RECONNECT_DELAY_MS); + reconnectAttempt += 1; + reconnectTimer = setTimeout(() => { + reconnectTimer = null; + connect().catch(() => { }); + }, delay); +} + function connect(): Promise { if (connectPromise) return connectPromise; @@ -32,6 +45,7 @@ function connect(): Promise { socket.onopen = () => { ws = socket; + reconnectAttempt = 0; notifyStatus(true); resolve(socket); }; @@ -44,11 +58,12 @@ function connect(): Promise { r(new Error("WebSocket closed")); } pending.clear(); + scheduleReconnect(); }; - socket.onerror = (e) => { + socket.onerror = () => { connectPromise = null; - reject(new Error("WebSocket error: " + String(e))); + reject(new Error("Could not connect to gate server")); }; socket.onmessage = (ev) => { From 68f91faadd134e8fac541fff131a053fcd3015c7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Jun 2026 04:39:45 +0000 Subject: [PATCH 4/6] =?UTF-8?q?fix:=20address=20code=20review=20=E2=80=94?= =?UTF-8?q?=20close=20code=20guard,=20reconnect=20comment,=20error=20comme?= =?UTF-8?q?nt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/pages/Overview.tsx | 2 ++ web/src/ws.ts | 9 +++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/web/src/pages/Overview.tsx b/web/src/pages/Overview.tsx index 406ba04..0794e7a 100644 --- a/web/src/pages/Overview.tsx +++ b/web/src/pages/Overview.tsx @@ -122,6 +122,8 @@ export default function Overview() { const [error, setError] = useState(null); const fetchData = useCallback(() => { + // Clear any prior error so the UI reflects the freshest attempt. + // Individual call failures below will set a new error if they occur. setError(null); call("sessions.stats").then(setStats).catch((e: Error) => setError(e.message)); call<{ sessions: Session[] }>("sessions.list", { limit: 5 }) diff --git a/web/src/ws.ts b/web/src/ws.ts index 73a304b..bd7c571 100644 --- a/web/src/ws.ts +++ b/web/src/ws.ts @@ -33,6 +33,8 @@ function scheduleReconnect() { reconnectAttempt += 1; reconnectTimer = setTimeout(() => { reconnectTimer = null; + // Errors are expected while the server is unavailable; scheduleReconnect will be + // called again via onclose if the attempt fails. connect().catch(() => { }); }, delay); } @@ -50,7 +52,7 @@ function connect(): Promise { resolve(socket); }; - socket.onclose = () => { + socket.onclose = (ev) => { ws = null; connectPromise = null; notifyStatus(false); @@ -58,7 +60,10 @@ function connect(): Promise { r(new Error("WebSocket closed")); } pending.clear(); - scheduleReconnect(); + // 1000 = normal closure, 1001 = going away (page unload) — no reconnect needed. + if (ev.code !== 1000 && ev.code !== 1001) { + scheduleReconnect(); + } }; socket.onerror = () => { From b909cb1e1d8631fb0e0dba7ca6afc5c23d35df96 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Jun 2026 05:06:55 +0000 Subject: [PATCH 5/6] feat: add Gateway page with server controls and gate.status RPC handler --- src/gate/protocol.ts | 20 ++++ src/gate/server.ts | 6 +- web/src/App.tsx | 5 + web/src/pages/Gateway.tsx | 229 +++++++++++++++++++++++++++++++++++++ web/src/pages/Terminal.tsx | 6 + 5 files changed, 265 insertions(+), 1 deletion(-) create mode 100644 web/src/pages/Gateway.tsx diff --git a/src/gate/protocol.ts b/src/gate/protocol.ts index b7c42c2..2b263ef 100644 --- a/src/gate/protocol.ts +++ b/src/gate/protocol.ts @@ -81,8 +81,14 @@ export interface ProtocolContext { runtime: ChatRuntime; /** Broadcast a frame to all connected WebSocket clients. */ broadcast: (frame: Record) => void; + /** Returns the current number of connected WebSocket clients. */ + getClientCount: () => number; + /** The TCP port the gate server is listening on. */ + serverPort: number; } +const SERVER_START_TIME = Date.now(); + // --------------------------------------------------------------------------- // Router // --------------------------------------------------------------------------- @@ -1194,6 +1200,20 @@ async function handleRequest(ctx: ProtocolContext, req: RequestFrameT): Promise< return ok(req.id, { messages: whatsappMessages.slice(0, limit) }); } + // ── gate ────────────────────────────────────────────────────────────────── + case "gate.status": { + const uptimeSec = (Date.now() - SERVER_START_TIME) / 1000; + return ok(req.id, { + uptime: uptimeSec, + port: ctx.serverPort, + connectedClients: ctx.getClientCount(), + pid: process.pid, + nodeVersion: process.version, + platform: process.platform, + startedAt: SERVER_START_TIME, + }); + } + // ── legacy ──────────────────────────────────────────────────────────────── case "agent.echo": { const input = requireStr(req.id, p.input, "input"); diff --git a/src/gate/server.ts b/src/gate/server.ts index 8aaf53f..0dbd2e1 100644 --- a/src/gate/server.ts +++ b/src/gate/server.ts @@ -60,7 +60,11 @@ export async function startJclawGate(options: JclawGateOptions) { } wss.on("connection", (socket) => { - handleWsConnection({ socket, sessions, plugins, runtime, broadcast }); + handleWsConnection({ + socket, sessions, plugins, runtime, broadcast, + getClientCount: () => wss.clients.size, + serverPort: options.port, + }); }); app.get("/health", (_req, res) => { diff --git a/web/src/App.tsx b/web/src/App.tsx index c3ba708..9a6766e 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -7,6 +7,7 @@ import SessionDetail from "./pages/SessionDetail.tsx"; import Prompts from "./pages/Prompts.tsx"; import Templates from "./pages/Templates.tsx"; import Providers from "./pages/Providers.tsx"; +import Gateway from "./pages/Gateway.tsx"; import Search from "./pages/Search.tsx"; import Chat from "./pages/Chat.tsx"; import Terminal from "./pages/Terminal.tsx"; @@ -102,6 +103,9 @@ export default function App() {
config
+ "nav-item" + (isActive ? " active" : "")} onClick={closeSidebar}> + Gateway + "nav-item" + (isActive ? " active" : "")} onClick={closeSidebar}> Providers @@ -139,6 +143,7 @@ export default function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/web/src/pages/Gateway.tsx b/web/src/pages/Gateway.tsx new file mode 100644 index 0000000..b6677ac --- /dev/null +++ b/web/src/pages/Gateway.tsx @@ -0,0 +1,229 @@ +import { useEffect, useState, useCallback } from "react"; +import { call, onStatus, getStatus } from "../ws.ts"; + +interface GateStatus { + uptime: number; + port: number; + connectedClients: number; + pid: number; + nodeVersion: string; + platform: string; + startedAt: number; +} + +interface PingResult { + name: string; + displayName?: string; + ok: boolean; + latencyMs: number | null; + error?: string; +} + +function fmtUptime(sec: number): string { + const s = Math.floor(sec); + const d = Math.floor(s / 86400); + const h = Math.floor((s % 86400) / 3600); + const m = Math.floor((s % 3600) / 60); + const parts: string[] = []; + if (d > 0) parts.push(`${d}d`); + if (h > 0) parts.push(`${h}h`); + if (m > 0) parts.push(`${m}m`); + parts.push(`${s % 60}s`); + return parts.join(" "); +} + +function fmtTs(ms: number): string { + return new Date(ms).toLocaleString(undefined, { + dateStyle: "medium", + timeStyle: "medium", + }); +} + +function StatCard({ label, value, sub, accent }: { + label: string; value: string; sub?: string; accent?: string; +}) { + const color = accent ?? "var(--accent)"; + return ( +
+
+ {label} +
+
+ {value} +
+ {sub && ( +
+ {sub} +
+ )} +
+ ); +} + +export default function Gateway() { + const [status, setStatus] = useState(null); + const [pings, setPings] = useState([]); + const [loading, setLoading] = useState(true); + const [pinging, setPinging] = useState(false); + const [error, setError] = useState(null); + const [connected, setConnected] = useState(getStatus()); + const [lastRefresh, setLastRefresh] = useState(null); + const [pingMs, setPingMs] = useState(null); + + const fetchStatus = useCallback(async () => { + try { + const [s, p] = await Promise.all([ + call("gate.status"), + call<{ providers: PingResult[] }>("providers.ping").then((r) => r.providers), + ]); + setStatus(s); + setPings(p); + setLastRefresh(Date.now()); + setError(null); + } catch (e: unknown) { + setError(String(e)); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchStatus(); + const interval = setInterval(fetchStatus, 10_000); + const unsub = onStatus((s) => { + setConnected(s); + if (s) fetchStatus(); + }); + return () => { + clearInterval(interval); + unsub(); + }; + }, [fetchStatus]); + + async function doPing() { + setPinging(true); + const t0 = Date.now(); + try { + await call("ping"); + setPingMs(Date.now() - t0); + } catch { + setPingMs(null); + } finally { + setPinging(false); + } + } + + if (loading) return
Loading gateway status...
; + + return ( +
+
Gateway
+ + {/* Connection banner */} +
+ + + {connected ? "GATE CONNECTED" : "GATE DISCONNECTED — reconnecting..."} + + {lastRefresh && ( + + last refresh {new Date(lastRefresh).toLocaleTimeString()} + + )} +
+ + {error &&
{error}
} + + {/* Stat cards */} + {status && ( +
+ + + 0 ? "var(--green)" : "var(--muted)"} + /> + + + +
+ )} + + {/* Quick controls */} +
+ Quick Controls +
+
+ + + {pingMs !== null && ( + + {pingMs}ms round-trip + + )} +
+ + {/* Provider health */} +
+ Provider Health +
+ {pings.length === 0 ? ( +
No provider results yet.
+ ) : ( +
+ {pings.map((p) => ( +
+ + + {(p.displayName ?? p.name).toUpperCase()} + + + {p.ok + ? `online — ${p.latencyMs}ms` + : `offline${p.error ? ` — ${p.error}` : ""}`} + +
+ ))} +
+ )} +
+ ); +} diff --git a/web/src/pages/Terminal.tsx b/web/src/pages/Terminal.tsx index db5ab6a..383aaf1 100644 --- a/web/src/pages/Terminal.tsx +++ b/web/src/pages/Terminal.tsx @@ -23,6 +23,9 @@ const ALIASES: Record "prompts.list": { method: "prompts.list" }, templates: { method: "templates.list" }, "templates.list": { method: "templates.list" }, + gateway: { method: "gate.status" }, + "gate.status": { method: "gate.status" }, + "gate.clients": { method: "gate.status" }, }; const HELP_TEXT = `JCLAW TERMINAL — available commands: @@ -37,6 +40,9 @@ const HELP_TEXT = `JCLAW TERMINAL — available commands: stats aggregate session stats prompts list saved prompts templates list saved templates + gateway gate server status (uptime, port, clients) + gate.status gate server status (uptime, port, clients) + gate.clients connected client count clear clear this terminal help show this message From 96979809d65740c6a7530c27d5386b0887811d3d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Jun 2026 05:08:31 +0000 Subject: [PATCH 6/6] fix: rename gate.clients to gate.info and guard latencyMs null in Gateway --- web/src/pages/Gateway.tsx | 2 +- web/src/pages/Terminal.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/web/src/pages/Gateway.tsx b/web/src/pages/Gateway.tsx index b6677ac..e708b98 100644 --- a/web/src/pages/Gateway.tsx +++ b/web/src/pages/Gateway.tsx @@ -217,7 +217,7 @@ export default function Gateway() { color: p.ok ? "var(--green)" : "var(--muted)", }}> {p.ok - ? `online — ${p.latencyMs}ms` + ? `online — ${p.latencyMs ?? "N/A"}ms` : `offline${p.error ? ` — ${p.error}` : ""}`} diff --git a/web/src/pages/Terminal.tsx b/web/src/pages/Terminal.tsx index 383aaf1..417b413 100644 --- a/web/src/pages/Terminal.tsx +++ b/web/src/pages/Terminal.tsx @@ -25,7 +25,7 @@ const ALIASES: Record "templates.list": { method: "templates.list" }, gateway: { method: "gate.status" }, "gate.status": { method: "gate.status" }, - "gate.clients": { method: "gate.status" }, + "gate.info": { method: "gate.status" }, }; const HELP_TEXT = `JCLAW TERMINAL — available commands: @@ -42,7 +42,7 @@ const HELP_TEXT = `JCLAW TERMINAL — available commands: templates list saved templates gateway gate server status (uptime, port, clients) gate.status gate server status (uptime, port, clients) - gate.clients connected client count + gate.info gate server info (alias for gate.status) clear clear this terminal help show this message