From 3d3ab5a6be863f10dfe2c6d5bb3a2853135fb7cf Mon Sep 17 00:00:00 2001 From: alimaazamat Date: Mon, 15 Jun 2026 14:44:40 -0700 Subject: [PATCH 1/3] Fix WebSocket connection in dev mode Vite's http-proxy cannot relay Bun.serve's WebSocket upgrade (101) response, so the /ws proxy silently times out and terminals never connect under `bun run dev` (broken since the initial commit). Connect the terminal WebSocket directly to the backend in dev instead of through the proxy, and remove the non-functional /ws proxy entry. Production/single-server mode is unchanged (the backend serves the client, so window.location.host already points at it). The backend port is configurable via VITE_BACKEND_PORT (defaults 6968). --- client/src/components/Terminal.tsx | 8 +++++++- client/vite.config.ts | 7 +++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/client/src/components/Terminal.tsx b/client/src/components/Terminal.tsx index 9ea1059..48ce559 100644 --- a/client/src/components/Terminal.tsx +++ b/client/src/components/Terminal.tsx @@ -85,7 +85,13 @@ export function Terminal({ sessionId, color, nodeId }: TerminalProps) { // Connect WebSocket with small delay to allow session to be ready const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; - const wsUrl = `${protocol}//${window.location.host}/ws?sessionId=${sessionId}`; + // In dev, Vite's proxy can't relay Bun's WebSocket upgrade response, so connect + // directly to the backend. In production (single-server) the backend serves the + // client itself, so the current host already points at the backend. + const wsHost = import.meta.env.DEV + ? `${window.location.hostname}:${import.meta.env.VITE_BACKEND_PORT ?? 6968}` + : window.location.host; + const wsUrl = `${protocol}//${wsHost}/ws?sessionId=${sessionId}`; let ws: WebSocket | null = null; let isFirstMessage = true; diff --git a/client/vite.config.ts b/client/vite.config.ts index 7bd0bd4..d09d6a7 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -10,10 +10,9 @@ export default defineConfig({ target: "http://localhost:6968", changeOrigin: true, }, - "/ws": { - target: "ws://localhost:6968", - ws: true, - }, + // NOTE: /ws is intentionally not proxied. Vite's http-proxy cannot relay + // Bun.serve's WebSocket upgrade response, so the client connects directly + // to the backend in dev (see Terminal.tsx). }, }, build: { From 028aae77bf88c7f4533889f4b53340e7392adf8e Mon Sep 17 00:00:00 2001 From: alimaazamat Date: Mon, 15 Jun 2026 17:03:18 -0700 Subject: [PATCH 2/3] Force ws:// for direct backend connection in dev In dev the client connects directly to the plain-HTTP Bun backend, which has no TLS listener. Deriving the WebSocket protocol from the page meant an HTTPS-served dev page produced a wss:// URL that failed the TLS handshake. Always use ws:// for the direct dev connection; production still mirrors the page protocol and reuses the current host. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- client/src/components/Terminal.tsx | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/client/src/components/Terminal.tsx b/client/src/components/Terminal.tsx index 48ce559..0b1de5c 100644 --- a/client/src/components/Terminal.tsx +++ b/client/src/components/Terminal.tsx @@ -83,15 +83,16 @@ export function Terminal({ sessionId, color, nodeId }: TerminalProps) { xtermRef.current = term; fitAddonRef.current = fitAddon; - // Connect WebSocket with small delay to allow session to be ready - const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; + // Connect WebSocket with small delay to allow session to be ready. // In dev, Vite's proxy can't relay Bun's WebSocket upgrade response, so connect - // directly to the backend. In production (single-server) the backend serves the - // client itself, so the current host already points at the backend. - const wsHost = import.meta.env.DEV - ? `${window.location.hostname}:${import.meta.env.VITE_BACKEND_PORT ?? 6968}` - : window.location.host; - const wsUrl = `${protocol}//${wsHost}/ws?sessionId=${sessionId}`; + // directly to the backend. The backend serves plain HTTP/WebSocket with no TLS, + // so always use ws:// even when the dev page is served over HTTPS. In production + // (single-server) the backend serves the client itself, so mirror the page's + // protocol and reuse the current host. + const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; + const wsUrl = import.meta.env.DEV + ? `ws://${window.location.hostname}:${import.meta.env.VITE_BACKEND_PORT ?? 6968}/ws?sessionId=${sessionId}` + : `${protocol}//${window.location.host}/ws?sessionId=${sessionId}`; let ws: WebSocket | null = null; let isFirstMessage = true; From 9076c4bdb6f2e08cd77306b130d926881ae47db5 Mon Sep 17 00:00:00 2001 From: alimaazamat Date: Mon, 15 Jun 2026 17:16:04 -0700 Subject: [PATCH 3/3] Mirror page protocol for dev WebSocket URL Forcing ws:// would be blocked by the browser's mixed-content policy when the dev page is served over HTTPS. Mirror the page protocol instead: the default HTTP dev page still yields ws:// (works against the plain backend), while an HTTPS dev page yields wss:// so the browser does not block it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- client/src/components/Terminal.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/client/src/components/Terminal.tsx b/client/src/components/Terminal.tsx index 0b1de5c..eda7d1f 100644 --- a/client/src/components/Terminal.tsx +++ b/client/src/components/Terminal.tsx @@ -84,14 +84,17 @@ export function Terminal({ sessionId, color, nodeId }: TerminalProps) { fitAddonRef.current = fitAddon; // Connect WebSocket with small delay to allow session to be ready. + // Always mirror the page's protocol (ws for http, wss for https) so the + // browser never blocks the connection under its mixed-content policy. // In dev, Vite's proxy can't relay Bun's WebSocket upgrade response, so connect - // directly to the backend. The backend serves plain HTTP/WebSocket with no TLS, - // so always use ws:// even when the dev page is served over HTTPS. In production - // (single-server) the backend serves the client itself, so mirror the page's - // protocol and reuse the current host. + // directly to the backend's host/port; the default dev page is served over plain + // HTTP, yielding ws://. (Serving the dev page over HTTPS requires the backend to + // be reachable over TLS, since the browser forbids ws:// from an https:// origin.) + // In production (single-server) the backend serves the client itself, so reuse + // the current host. const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; const wsUrl = import.meta.env.DEV - ? `ws://${window.location.hostname}:${import.meta.env.VITE_BACKEND_PORT ?? 6968}/ws?sessionId=${sessionId}` + ? `${protocol}//${window.location.hostname}:${import.meta.env.VITE_BACKEND_PORT ?? 6968}/ws?sessionId=${sessionId}` : `${protocol}//${window.location.host}/ws?sessionId=${sessionId}`; let ws: WebSocket | null = null;