Files
IdeaSDK/frontend/src/adapters/http/index.ts
Blomios ac726d075e feat(frontend): système de plugins — runtime, menus, layouts custom (#43)
Lots F1-F4 : runtime de chargement/registre plugin, extension des menus
existants, panneau de gestion des plugins, types de layout custom
(sélecteur, fallback, cellule dédiée) branchés sur le port plugin.
Suite npm typecheck/test verte (947/947).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-22 07:37:11 +02:00

152 lines
5.8 KiB
TypeScript

/**
* 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,
WebPluginGateway,
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(),
plugin: new WebPluginGateway(),
};
}
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";