feat(frontend): adapter web HTTP+WebSocket derrière les ports (#13)

Lot F1 du chantier server/client mode : nouvel adaptateur web branché
derrière les ports d'invocation et de flux live, permettant au frontend de
dialoguer avec le backend via HTTP + WebSocket en mode client/serveur. Le
mode desktop (Tauri IPC) reste inchangé.

- frontend/src/adapters/http : invoker HTTP, client live WebSocket, gateways
  request/response et stream, frames, garde unsupported (7 fichiers + 2 tests).
- frontend/src/app : câblage DI (di.tsx) et son test, typage vite-env.d.ts.

Validé : build vert, garde no-direct-invoke verte, 724 tests verts,
desktop inchangé.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-15 12:50:38 +02:00
parent c8fef2a76a
commit e0cdb4aa56
12 changed files with 1783 additions and 5 deletions

View File

@ -0,0 +1,110 @@
/**
* 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 {
HttpConversationGateway,
HttpEmbedderGateway,
HttpGitGateway,
HttpInputGateway,
HttpLayoutGateway,
HttpMemoryGateway,
HttpModelServerGateway,
HttpPermissionGateway,
HttpProfileGateway,
HttpProjectGateway,
HttpSkillGateway,
HttpTemplateGateway,
HttpWorkStateGateway,
} from "./requestResponseGateways";
import {
HttpAgentGateway,
HttpSystemGateway,
HttpTerminalGateway,
HttpTicketGateway,
} from "./streamGateways";
import {
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);
const http = new HttpInvoker({ baseUrl, token: config.token });
const ws = new WsLiveClient({ wsUrl, token: config.token });
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),
template: new HttpTemplateGateway(http),
skill: new HttpSkillGateway(http),
memory: new HttpMemoryGateway(http),
embedder: new HttpEmbedderGateway(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";