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>
152 lines
5.8 KiB
TypeScript
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";
|