diff --git a/packages/web/src/components/DirectTerminal.tsx b/packages/web/src/components/DirectTerminal.tsx index e5e8c11b1..ab32dc4ee 100644 --- a/packages/web/src/components/DirectTerminal.tsx +++ b/packages/web/src/components/DirectTerminal.tsx @@ -21,6 +21,40 @@ interface DirectTerminalProps { height?: string; } +interface DirectTerminalLocation { + protocol: string; + hostname: string; + host: string; + port: string; +} + +interface DirectTerminalWsUrlOptions { + location: DirectTerminalLocation; + sessionId: string; + proxyWsPath?: string; + directTerminalPort?: string; +} + +export function buildDirectTerminalWsUrl({ + location, + sessionId, + proxyWsPath, + directTerminalPort, +}: DirectTerminalWsUrlOptions): string { + const protocol = location.protocol === "https:" ? "wss:" : "ws:"; + if (proxyWsPath) { + // Path-based proxy uses host so non-standard ports are preserved. + return `${protocol}//${location.host}${proxyWsPath}?session=${encodeURIComponent(sessionId)}`; + } + + if (location.port === "" || location.port === "443" || location.port === "80") { + return `${protocol}//${location.hostname}/ao-terminal-ws?session=${encodeURIComponent(sessionId)}`; + } + + const port = directTerminalPort ?? "14801"; + return `${protocol}//${location.hostname}:${port}/ws?session=${encodeURIComponent(sessionId)}`; +} + /** * Direct xterm.js terminal with native WebSocket connection. * Implements Extended Device Attributes (XDA) handler to enable @@ -186,22 +220,12 @@ export function DirectTerminal({ // WebSocket URL (stable across reconnects) // When accessed via reverse proxy (HTTPS on standard port), use path-based // WebSocket endpoint instead of direct port access. - const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; - const hostname = window.location.hostname; - const host = window.location.host; - const proxyWsPath = process.env.NEXT_PUBLIC_TERMINAL_WS_PATH; - let wsUrl: string; - if (proxyWsPath) { - // Path-based proxy (e.g. /ao-terminal-ws) - wsUrl = `${protocol}//${host}${proxyWsPath}?session=${encodeURIComponent(sessionId)}`; - } else if (window.location.port === "" || window.location.port === "443" || window.location.port === "80") { - // Behind reverse proxy on standard port — use path-based endpoint - wsUrl = `${protocol}//${hostname}/ao-terminal-ws?session=${encodeURIComponent(sessionId)}`; - } else { - // Direct access (dev mode) — use direct port - const port = process.env.NEXT_PUBLIC_DIRECT_TERMINAL_PORT ?? "14801"; - wsUrl = `${protocol}//${hostname}:${port}/ws?session=${encodeURIComponent(sessionId)}`; - } + const wsUrl = buildDirectTerminalWsUrl({ + location: window.location, + sessionId, + proxyWsPath: process.env.NEXT_PUBLIC_TERMINAL_WS_PATH, + directTerminalPort: process.env.NEXT_PUBLIC_DIRECT_TERMINAL_PORT, + }); // ── Preserve selection while terminal receives output ──────── // xterm.js clears the selection on every terminal.write(). We diff --git a/packages/web/src/components/__tests__/DirectTerminal.test.ts b/packages/web/src/components/__tests__/DirectTerminal.test.ts new file mode 100644 index 000000000..20ff16756 --- /dev/null +++ b/packages/web/src/components/__tests__/DirectTerminal.test.ts @@ -0,0 +1,63 @@ +import { describe, it, expect } from "vitest"; +import { buildDirectTerminalWsUrl } from "@/components/DirectTerminal"; + +describe("buildDirectTerminalWsUrl", () => { + it("keeps non-standard port when proxy path override is set", () => { + const wsUrl = buildDirectTerminalWsUrl({ + location: { + protocol: "https:", + hostname: "example.com", + host: "example.com:8443", + port: "8443", + }, + sessionId: "session-1", + proxyWsPath: "/ao-terminal-ws", + }); + + expect(wsUrl).toBe("wss://example.com:8443/ao-terminal-ws?session=session-1"); + }); + + it("uses proxy path without port when default port is used", () => { + const wsUrl = buildDirectTerminalWsUrl({ + location: { + protocol: "https:", + hostname: "example.com", + host: "example.com", + port: "", + }, + sessionId: "session-2", + proxyWsPath: "/ao-terminal-ws", + }); + + expect(wsUrl).toBe("wss://example.com/ao-terminal-ws?session=session-2"); + }); + + it("uses default path-based endpoint on standard ports when no proxy override is set", () => { + const wsUrl = buildDirectTerminalWsUrl({ + location: { + protocol: "https:", + hostname: "example.com", + host: "example.com", + port: "443", + }, + sessionId: "session-3", + }); + + expect(wsUrl).toBe("wss://example.com/ao-terminal-ws?session=session-3"); + }); + + it("uses direct terminal port on non-standard ports when no proxy override is set", () => { + const wsUrl = buildDirectTerminalWsUrl({ + location: { + protocol: "http:", + hostname: "localhost", + host: "localhost:3000", + port: "3000", + }, + sessionId: "session-4", + directTerminalPort: "14888", + }); + + expect(wsUrl).toBe("ws://localhost:14888/ws?session=session-4"); + }); +});