/** * Desktop-only surfaces that have no web equivalent in V1 (ticket #13, lot F1): * the native folder picker, OS view-windows and the main↔detached focused-project * channel, plus SSH/WSL remote. On the web client these must fail with a clear, * explicit error rather than reaching for Tauri (which is absent). * * The B0 inventory classified these as `desktop-only`; F1 does NOT implement them * for web. If/when the product wants an equivalent (e.g. a server-side folder * browser to replace `pickFolder`), it gets its own lot. */ import type { EmbeddedServerStatus, GatewayError, PluginAdmin, PluginCommandTask, PluginConfigDocument, PluginConfigDocumentWriteResult, PluginEventBatch, PluginEventSubscription, PluginInstallResult, PluginToolchainDiagnostic, PluginProjectStructure, PluginReview, PluginRuntimeContributionCatalog, PluginUninstallResult, PluginWorkspaceBinaryFile, PluginWorkspaceDirectoryListing, PluginWorkspaceStat, PluginWorkspaceTextFile, ServerExposurePreview, ServerExposureSettings, Unsubscribe, } from "@/domain"; import type { DesktopServerGateway, FocusedProject, FocusedProjectGateway, PluginConfigDocumentReadInput, PluginConfigDocumentUpdateInput, PluginConfigGateway, PluginEventGateway, PluginEventPollInput, PluginEventSubscribeInput, PluginEventUnsubscribeInput, PluginGateway, PluginProjectStructureQuery, PluginRunCommandInput, PluginTaskGateway, PluginTaskStatusInput, PluginToolchainDiagnosticRequest, PluginToolchainGateway, PluginWorkspaceGateway, PluginWorkspacePathInput, PluginWorkspaceWriteBinaryInput, PluginWorkspaceWriteTextInput, RemoteGateway, ReviewPluginPackageInput, ViewWindowClosed, ViewWindowSnapshot, WindowGateway, } from "@/ports"; /** Throws a stable, explicit "unsupported on web" {@link GatewayError}. */ export function unsupportedOnWeb(what: string): never { const err: GatewayError = { code: "UNSUPPORTED_ON_WEB", message: `${what} is not available in the web client (desktop-only).`, }; throw err; } /** Web stub: OS view-windows are desktop-only (no `WebviewWindow` on the web). */ export class WebWindowGateway implements WindowGateway { openViewWindow(): Promise { return unsupportedOnWeb("Detaching a panel into an OS window"); } closeViewWindow(): Promise { return unsupportedOnWeb("Closing a detached OS window"); } listOpenViewWindows(): Promise { // Best-effort by contract (callers treat rejection as "no reconciliation"), // but there are simply never detached OS windows on web → empty list. return Promise.resolve([]); } onViewWindowClosed( _handler: (event: ViewWindowClosed) => void, ): Promise { // No OS windows ⇒ nothing ever fires; return a no-op unsubscribe. return Promise.resolve(() => {}); } } /** * Web stub for the focused-project channel. With no detached windows on web the * channel has no cross-window consumer; F1 keeps it inert (no publish, no focus). * A future multi-tab web build could back this with `BroadcastChannel`. */ export class WebFocusedProjectGateway implements FocusedProjectGateway { setFocusedProject(_project: FocusedProject | null): Promise { // No-op: nothing reads the channel on web V1. return Promise.resolve(); } getFocusedProject(): Promise { return Promise.resolve(null); } onFocusedProjectChanged( _handler: (project: FocusedProject | null) => void, ): Promise { return Promise.resolve(() => {}); } } /** Web stub: SSH/WSL remote connection is desktop-only in V1. */ export class WebRemoteGateway implements RemoteGateway { connect(): Promise { return unsupportedOnWeb("Remote (SSH/WSL) connection"); } } /** * Web stub: the embedded server is desktop-only (ticket #68). A web client is * *served by* this very server, so letting it reconfigure or stop the server * would let a remote device saw off the branch it sits on. The desktop app owns * this surface; every call fails explicitly rather than reaching for Tauri. */ export class WebDesktopServerGateway implements DesktopServerGateway { // `async` on purpose: these must *reject* rather than throw synchronously, so // callers using `.then(ok, err)` (not just `await`) still see the failure. async getExposureSettings(): Promise { return unsupportedOnWeb("Embedded server settings"); } async saveExposureSettings(_settings: ServerExposureSettings): Promise { return unsupportedOnWeb("Embedded server settings"); } async previewExposure( _settings: ServerExposureSettings, ): Promise { return unsupportedOnWeb("Embedded server settings"); } async status(): Promise { return unsupportedOnWeb("Embedded server status"); } async start(): Promise { return unsupportedOnWeb("Starting the embedded server"); } async stop(): Promise { return unsupportedOnWeb("Stopping the embedded server"); } onStatusChanged( _handler: (status: EmbeddedServerStatus) => void, ): Promise { // Never fires on web; a no-op unsubscribe keeps callers' teardown honest. return Promise.resolve(() => {}); } } /** * Web stub: the plugin system (#43) installs/loads full-trust local ESM * bundles from the desktop filesystem — no server-side equivalent in V1. The * web client must not manage or load plugins on behalf of the desktop app. */ export class WebPluginGateway implements PluginGateway { async listPlugins(): Promise { return unsupportedOnWeb("Plugin management"); } async reviewPackage(_input: ReviewPluginPackageInput): Promise { return unsupportedOnWeb("Plugin management"); } async installFromArchive(_path: string): Promise { return unsupportedOnWeb("Plugin management"); } async installFromDirectory(_path: string): Promise { return unsupportedOnWeb("Plugin management"); } async setEnabled(_pluginId: string, _enabled: boolean): Promise { return unsupportedOnWeb("Plugin management"); } async uninstall(_pluginId: string): Promise { return unsupportedOnWeb("Plugin management"); } async listRuntimeContributions(): Promise { // No plugin bundles are ever loaded on the web client — an empty catalog // lets the bootstrap loader run unconditionally without a transport check. return { plugins: [] }; } async openPluginsFolder(_pluginId?: string): Promise { return unsupportedOnWeb("Plugin management"); } } /** Web stub: public plugin workspace services are only meaningful where plugins run. */ export class WebPluginWorkspaceGateway implements PluginWorkspaceGateway { async readText(_input: PluginWorkspacePathInput): Promise { return unsupportedOnWeb("Plugin workspace access"); } async readBinary(_input: PluginWorkspacePathInput): Promise { return unsupportedOnWeb("Plugin workspace access"); } async writeText(_input: PluginWorkspaceWriteTextInput): Promise { return unsupportedOnWeb("Plugin workspace access"); } async writeBinary(_input: PluginWorkspaceWriteBinaryInput): Promise { return unsupportedOnWeb("Plugin workspace access"); } async listDir(_input: PluginWorkspacePathInput): Promise { return unsupportedOnWeb("Plugin workspace access"); } async stat(_input: PluginWorkspacePathInput): Promise { return unsupportedOnWeb("Plugin workspace access"); } async queryProjectStructure( _input: PluginProjectStructureQuery, ): Promise { return unsupportedOnWeb("Plugin workspace access"); } } /** Web stub: plugin command tasks are desktop-hosted in this runtime. */ export class WebPluginTaskGateway implements PluginTaskGateway { async runCommand(_input: PluginRunCommandInput): Promise { return unsupportedOnWeb("Plugin command tasks"); } async getStatus(_input: PluginTaskStatusInput): Promise { return unsupportedOnWeb("Plugin command tasks"); } } /** Web stub: plugin toolchain diagnostics run on the desktop host. */ export class WebPluginToolchainGateway implements PluginToolchainGateway { async diagnose( _input: PluginToolchainDiagnosticRequest, ): Promise { return unsupportedOnWeb("Plugin toolchain diagnostics"); } } /** Web stub: plugin public events are sourced from the desktop host. */ export class WebPluginEventGateway implements PluginEventGateway { async subscribe(_input: PluginEventSubscribeInput): Promise { return unsupportedOnWeb("Plugin public events"); } async poll(_input: PluginEventPollInput): Promise { return unsupportedOnWeb("Plugin public events"); } async unsubscribe(_input: PluginEventUnsubscribeInput): Promise { return unsupportedOnWeb("Plugin public events"); } } /** Web stub: plugin config documents are read/written by the desktop host. */ export class WebPluginConfigGateway implements PluginConfigGateway { async readDocument(_input: PluginConfigDocumentReadInput): Promise { return unsupportedOnWeb("Plugin structured config documents"); } async updateDocument( _input: PluginConfigDocumentUpdateInput, ): Promise { return unsupportedOnWeb("Plugin structured config documents"); } }