feat(frontend): host les fenêtres plugin et l'API publique windows.open

Ajoute la vue window host qui charge le layout plugin dans une fenêtre OS
détachée, expose react/react-dom/jsx-runtime hébergés sous
frontend/public/plugin-host pour que le runtime SDK y résolve React, et
câble services.windows.open() côté runtime plugin (loader/services/registry).

npm test -- --run src/adapters/window.test.ts src/app/ViewWindow.test.tsx
  src/plugins/runtime/loader.test.ts src/plugins/runtime/services.test.ts : 39/39
npm run typecheck : OK
npm run build : OK

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-04 00:39:50 +02:00
parent 551eb09ad2
commit d8df466fba
20 changed files with 678 additions and 10 deletions

View File

@ -162,6 +162,8 @@ import type {
ViewWindowClosed,
ViewWindowSnapshot,
WindowGateway,
PluginLayoutWindowOpenInput,
PluginLayoutWindowOpenResult,
FocusedProject,
FocusedProjectGateway,
WorkStateGateway,
@ -1284,12 +1286,34 @@ class MockRemoteGateway implements RemoteGateway {
export class MockWindowGateway implements WindowGateway {
/** Currently-open detached windows, keyed by `panel`. */
readonly open = new Set<string>();
readonly pluginLayoutWindows = new Map<string, PluginLayoutWindowOpenResult>();
private readonly listeners = new Set<(e: ViewWindowClosed) => void>();
async openViewWindow(panel: string): Promise<void> {
this.open.add(panel);
}
async openPluginLayoutWindow(
input: PluginLayoutWindowOpenInput,
): Promise<PluginLayoutWindowOpenResult> {
const label = `view-plugin-layout-${input.pluginId}.${input.layoutType}`;
const alreadyOpen = this.pluginLayoutWindows.has(label);
const result: PluginLayoutWindowOpenResult = {
label,
url: `index.html?pluginLayout=1&pluginId=${encodeURIComponent(input.pluginId)}&layoutType=${encodeURIComponent(input.layoutType)}`,
alreadyOpen,
providerPluginDisplayName: input.pluginId,
layoutLabel: input.layoutType,
surface: {
pluginId: input.pluginId,
layoutType: input.layoutType,
state: input.state ?? null,
},
};
this.pluginLayoutWindows.set(label, result);
return result;
}
async closeViewWindow(panel: string): Promise<void> {
if (this.open.delete(panel)) {
this.emit({ panel });