/** * Web (client/server) adapter set — ticket #13, lot F1. * * `createHttpWsGateways()` is the third gateway implementation, alongside * `createTauriGateways()` (desktop) and `createMockGateways()` (tests/dev). It * wires every UI port to the shared backend core over HTTP request/response and a * single multiplexed WebSocket, exactly behind the *unchanged* ports — no * component is aware of it (selected in `app/di.tsx`). * * Scope reminder (F1): request/response gateways are fully implemented; the WS * PTY/chat streams are a contract-conformant **skeleton** (full round-trip lands * in F3/B5/B6 once a server exists — B3/B4). Desktop-only surfaces (pickFolder, * OS windows, focused-project, remote) fail with an explicit "unsupported on web" * error or an inert stub, never a Tauri call. */ import type { Gateways } from "@/ports"; import { LocalStorageUiPreferencesGateway } from "../uiPreferences"; import { HttpInvoker } from "./httpInvoker"; import { WsLiveClient } from "./wsLiveClient"; import { getWebSession } from "./webSession"; import { setWebLiveClient } from "./webLive"; import { HttpDeviceGateway } from "./deviceGateway"; import { HttpConversationGateway, HttpEmbedderGateway, HttpGitGateway, HttpInputGateway, HttpLayoutGateway, HttpMemoryGateway, HttpModelServerGateway, HttpPermissionGateway, HttpProfileGateway, HttpProjectGateway, HttpSkillGateway, HttpTemplateGateway, HttpWorkStateGateway, } from "./requestResponseGateways"; import { HttpAgentGateway, HttpSystemGateway, HttpTerminalGateway, HttpTicketGateway, } from "./streamGateways"; import { WebDesktopServerGateway, WebFocusedProjectGateway, WebRemoteGateway, WebWindowGateway, } from "./unsupported"; /** Endpoint configuration for the web transport. */ export interface HttpWsGatewaysConfig { /** Backend HTTP base URL (no trailing slash). Defaults to the page origin. */ baseUrl?: string; /** WebSocket base URL. Defaults to the page origin with `http(s)`→`ws(s)`. */ wsUrl?: string; /** Bearer token from pairing (ticket #13 auth). */ token?: string; } /** Derives default `{baseUrl, wsUrl}` from `window.location` when available. */ function resolveEndpoints(config: HttpWsGatewaysConfig): { baseUrl: string; wsUrl: string; } { const origin = typeof window !== "undefined" && window.location ? window.location.origin : "http://localhost"; const baseUrl = config.baseUrl ?? origin; const wsUrl = config.wsUrl ?? baseUrl.replace(/^http(s?):\/\//, (_m, s) => `ws${s}://`); return { baseUrl, wsUrl }; } /** * Builds the full set of web (HTTP+WS) gateways. `config` is optional; endpoints * default to the current page origin so a browser build talks to its own server. * The {@link HttpInvoker} and {@link WsLiveClient} are shared across gateways. */ export function createHttpWsGateways(config: HttpWsGatewaysConfig = {}): Gateways { const { baseUrl, wsUrl } = resolveEndpoints(config); // Route 401s back to the pairing screen via the shared web session (F2/F6). const session = getWebSession(); // `ws` is referenced by the invoker's 401 handler (declared below); the closure // only runs after both are constructed, so the forward reference is safe. let ws: WsLiveClient; const http = new HttpInvoker({ baseUrl, token: config.token, onUnauthorized: () => { // F6: a 401 means the session cookie is gone/revoked. Route back to pairing // AND tear down the live socket so it stops its (now hopeless) reconnect // loop. Re-pairing brings the same client back up cleanly. session.notifyUnauthorized(); ws?.disconnect(); }, }); ws = new WsLiveClient({ wsUrl, token: config.token, // F6: when a WS reconnect fails we cannot read its HTTP status, so probe with // a cheap `health` call. A revoked session answers `401` → the invoker's // `onUnauthorized` above bounces to pairing; a network error is swallowed and // the WS backoff keeps retrying. onReconnectFailed: () => { void http.invoke("health", { request: null }).catch(() => {}); }, }); // Share the live client with the web feature so live surfaces can re-sync on // reconnect (F5). Inert on desktop (never called there). setWebLiveClient(ws); return { system: new HttpSystemGateway(http, ws), agent: new HttpAgentGateway(http, ws), input: new HttpInputGateway(http), terminal: new HttpTerminalGateway(ws), project: new HttpProjectGateway(http), layout: new HttpLayoutGateway(http), git: new HttpGitGateway(http), remote: new WebRemoteGateway(), profile: new HttpProfileGateway(http), modelServer: new HttpModelServerGateway(http), // Desktop-only (#68): the web client is served *by* the embedded server and // must not reconfigure it. desktopServer: new WebDesktopServerGateway(), template: new HttpTemplateGateway(http), skill: new HttpSkillGateway(http), memory: new HttpMemoryGateway(http), embedder: new HttpEmbedderGateway(http), device: new HttpDeviceGateway(http), permission: new HttpPermissionGateway(http), workState: new HttpWorkStateGateway(http), conversation: new HttpConversationGateway(http), ticket: new HttpTicketGateway(http, ws), window: new WebWindowGateway(), focusedProject: new WebFocusedProjectGateway(), // Frontend-owned UI prefs are transport-neutral (localStorage) — reuse as-is. uiPreferences: new LocalStorageUiPreferencesGateway(), }; } export { HttpInvoker } from "./httpInvoker"; export { WsLiveClient } from "./wsLiveClient"; export { WebSession, getWebSession, setWebSessionForTests } from "./webSession"; export { getWebLiveClient, setWebLiveClient, disconnectWebLive } from "./webLive"; export type { ConnectionState } from "./wsLiveClient";