Files
IdeA/frontend/src/adapters/http/unsupported.ts
Blomios bf33e58508 feat(application,backend,infrastructure,app-tauri,frontend): contributions UI/runtime des plugins limitées aux projets liés — #321 (QA verte)
Les contributions runtime des plugins (menus, slash commands, layouts,
surfaces) ne sont exposées que dans les projets liés au plugin.

- Définition du lien: ProjectPluginSettingsStore par racine projet
  (domaine) — une contribution runtime-active n'entre dans le catalogue
  que si elle est explicitement activée pour le projet courant; hors
  projet focalisé, catalogue vide
- Application: ListPluginRuntimeContributions.execute prend un
  project_id optionnel, charge projet + settings et filtre le registre
  par activation projet; tests fake store/settings (catalogue vide sans
  projet, activation, layout, slash commands)
- Backend/app-tauri: plugin_list_runtime_contributions et slash
  commands list/execute acceptent projectId optionnel (DTO camelCase,
  défaut serde), BackendCore recâblé avec store_port et
  project_plugin_settings_store; test DTO de scope optionnel
- Frontend: PluginGateway.listRuntimeContributions(projectId) (ports,
  tauri, http/unsupported, mock alignés); PluginRuntimeProvider
  re-scope le catalogue au changement de focus projet (vidage du
  registre précédent, disposition des bundles, requêtes obsolètes
  ignorées par projectId/révision); PluginLayoutCellView ignore les
  références de layout devenues hors périmètre via onChooseAnotherLayout
  et ne rend rien pendant le rechargement (aucune fuite inter-projets)
- Tests: plugin_install_load (infrastructure), dto slash commands,
  plugin.test, PluginRuntimeProvider.test, PluginLayoutCellView.test

QA verte sur feature/321-plugin-contributions-linked-projects. Aucun push.
2026-09-11 17:46:03 +02:00

311 lines
11 KiB
TypeScript

/**
* 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,
JsonValue,
PluginAdmin,
PluginCommandTask,
PluginConfigDocument,
PluginConfigDocumentWriteResult,
PluginEventBatch,
PluginEventSubscription,
PluginInstallResult,
PluginToolchainDiagnostic,
PluginProjectStructure,
PluginReview,
PluginRuntimeContributionCatalog,
ProjectPluginSettings,
ProjectPluginSettingsView,
PluginUninstallResult,
PluginWorkspaceBinaryFile,
PluginWorkspaceDirectoryListing,
PluginWorkspaceStat,
PluginWorkspaceTextFile,
ServerExposurePreview,
ServerExposureSettings,
Unsubscribe,
} from "@/domain";
import type {
DesktopServerGateway,
FocusedProject,
FocusedProjectGateway,
PluginConfigDocumentReadInput,
PluginConfigDocumentUpdateInput,
PluginConfigGateway,
PluginEventGateway,
PluginEventPollInput,
PluginEventSubscribeInput,
PluginEventUnsubscribeInput,
PluginGateway,
MutateAgentPluginSkillInput,
MutateAgentPluginToolInput,
PluginStorageGateway,
PluginStorageGetInput,
PluginStorageSetInput,
PluginProjectStructureQuery,
PluginRunCommandInput,
PluginTaskGateway,
PluginTaskStatusInput,
PluginToolchainDiagnosticRequest,
PluginToolchainGateway,
PluginWorkspaceGateway,
PluginWorkspacePathInput,
PluginWorkspaceWriteBinaryInput,
PluginWorkspaceWriteTextInput,
RemoteGateway,
ReviewPluginPackageInput,
SetProjectPluginEnabledInput,
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<void> {
return unsupportedOnWeb("Detaching a panel into an OS window");
}
openPluginLayoutWindow(): Promise<never> {
return unsupportedOnWeb("Opening a plugin layout in an OS window");
}
closeViewWindow(): Promise<void> {
return unsupportedOnWeb("Closing a detached OS window");
}
listOpenViewWindows(): Promise<ViewWindowSnapshot[]> {
// 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<Unsubscribe> {
// 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<void> {
// No-op: nothing reads the channel on web V1.
return Promise.resolve();
}
getFocusedProject(): Promise<FocusedProject | null> {
return Promise.resolve(null);
}
onFocusedProjectChanged(
_handler: (project: FocusedProject | null) => void,
): Promise<Unsubscribe> {
return Promise.resolve(() => {});
}
}
/** Web stub: SSH/WSL remote connection is desktop-only in V1. */
export class WebRemoteGateway implements RemoteGateway {
connect(): Promise<void> {
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<ServerExposureSettings> {
return unsupportedOnWeb("Embedded server settings");
}
async saveExposureSettings(_settings: ServerExposureSettings): Promise<void> {
return unsupportedOnWeb("Embedded server settings");
}
async previewExposure(
_settings: ServerExposureSettings,
): Promise<ServerExposurePreview> {
return unsupportedOnWeb("Embedded server settings");
}
async status(): Promise<EmbeddedServerStatus> {
return unsupportedOnWeb("Embedded server status");
}
async start(): Promise<EmbeddedServerStatus> {
return unsupportedOnWeb("Starting the embedded server");
}
async stop(): Promise<EmbeddedServerStatus> {
return unsupportedOnWeb("Stopping the embedded server");
}
onStatusChanged(
_handler: (status: EmbeddedServerStatus) => void,
): Promise<Unsubscribe> {
// 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<PluginAdmin[]> {
return unsupportedOnWeb("Plugin management");
}
async reviewPackage(_input: ReviewPluginPackageInput): Promise<PluginReview> {
return unsupportedOnWeb("Plugin management");
}
async installFromArchive(_path: string): Promise<PluginInstallResult> {
return unsupportedOnWeb("Plugin management");
}
async installFromDirectory(_path: string): Promise<PluginInstallResult> {
return unsupportedOnWeb("Plugin management");
}
async setEnabled(_pluginId: string, _enabled: boolean): Promise<PluginAdmin> {
return unsupportedOnWeb("Plugin management");
}
async uninstall(_pluginId: string): Promise<PluginUninstallResult> {
return unsupportedOnWeb("Plugin management");
}
async listRuntimeContributions(_projectId?: string): Promise<PluginRuntimeContributionCatalog> {
// 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 listProjectSettings(_projectId: string): Promise<ProjectPluginSettingsView> {
return unsupportedOnWeb("Project plugin settings");
}
async setProjectEnabled(_input: SetProjectPluginEnabledInput): Promise<ProjectPluginSettings> {
return unsupportedOnWeb("Project plugin settings");
}
async assignAgentSkill(_input: MutateAgentPluginSkillInput): Promise<ProjectPluginSettings> {
return unsupportedOnWeb("Project plugin settings");
}
async unassignAgentSkill(_input: MutateAgentPluginSkillInput): Promise<ProjectPluginSettings> {
return unsupportedOnWeb("Project plugin settings");
}
async assignAgentTool(_input: MutateAgentPluginToolInput): Promise<ProjectPluginSettings> {
return unsupportedOnWeb("Project plugin settings");
}
async unassignAgentTool(_input: MutateAgentPluginToolInput): Promise<ProjectPluginSettings> {
return unsupportedOnWeb("Project plugin settings");
}
async openPluginsFolder(_pluginId?: string): Promise<void> {
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<PluginWorkspaceTextFile> {
return unsupportedOnWeb("Plugin workspace access");
}
async readBinary(_input: PluginWorkspacePathInput): Promise<PluginWorkspaceBinaryFile> {
return unsupportedOnWeb("Plugin workspace access");
}
async writeText(_input: PluginWorkspaceWriteTextInput): Promise<void> {
return unsupportedOnWeb("Plugin workspace access");
}
async writeBinary(_input: PluginWorkspaceWriteBinaryInput): Promise<void> {
return unsupportedOnWeb("Plugin workspace access");
}
async listDir(_input: PluginWorkspacePathInput): Promise<PluginWorkspaceDirectoryListing> {
return unsupportedOnWeb("Plugin workspace access");
}
async stat(_input: PluginWorkspacePathInput): Promise<PluginWorkspaceStat> {
return unsupportedOnWeb("Plugin workspace access");
}
async queryProjectStructure(
_input: PluginProjectStructureQuery,
): Promise<PluginProjectStructure> {
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<PluginCommandTask> {
return unsupportedOnWeb("Plugin command tasks");
}
async getStatus(_input: PluginTaskStatusInput): Promise<PluginCommandTask | null> {
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<PluginToolchainDiagnostic> {
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<PluginEventSubscription> {
return unsupportedOnWeb("Plugin public events");
}
async poll(_input: PluginEventPollInput): Promise<PluginEventBatch> {
return unsupportedOnWeb("Plugin public events");
}
async unsubscribe(_input: PluginEventUnsubscribeInput): Promise<PluginEventSubscription> {
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<PluginConfigDocument> {
return unsupportedOnWeb("Plugin structured config documents");
}
async updateDocument(
_input: PluginConfigDocumentUpdateInput,
): Promise<PluginConfigDocumentWriteResult> {
return unsupportedOnWeb("Plugin structured config documents");
}
}
/** Web stub: plugin-owned storage is owned by the desktop host app-data. */
export class WebPluginStorageGateway implements PluginStorageGateway {
async get(_input: PluginStorageGetInput): Promise<JsonValue | null> {
return unsupportedOnWeb("Plugin storage");
}
async set(_input: PluginStorageSetInput): Promise<void> {
return unsupportedOnWeb("Plugin storage");
}
async delete(_input: PluginStorageGetInput): Promise<boolean> {
return unsupportedOnWeb("Plugin storage");
}
}