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

@ -18,6 +18,7 @@ import type {
PluginWorkspaceGateway,
ProjectGateway,
TerminalGateway,
WindowGateway,
WorkStateGateway,
} from "@/ports";
@ -28,6 +29,7 @@ export interface PluginServices {
events: EventService;
config: ConfigDocumentService;
terminal: TerminalService;
windows: WindowService;
}
export interface WorkspaceProject {
@ -370,9 +372,32 @@ export interface TerminalService {
close(sessionId: string): Promise<void>;
}
export interface OpenPluginWindowOptions {
layoutType: string;
state?: JsonValue;
}
export interface PluginWindow {
label: string;
url: string;
alreadyOpen: boolean;
providerPluginDisplayName: string;
layoutLabel: string;
surface: {
pluginId: string;
layoutType: string;
state: JsonValue;
};
}
export interface WindowService {
open(options: OpenPluginWindowOptions): Promise<PluginWindow>;
}
interface PluginServiceGatewaySet {
project: ProjectGateway;
terminal: TerminalGateway;
window: WindowGateway;
workState: WorkStateGateway;
focusedProject: FocusedProjectGateway;
pluginWorkspace: PluginWorkspaceGateway;
@ -382,6 +407,11 @@ interface PluginServiceGatewaySet {
pluginConfig: PluginConfigGateway;
}
export interface PluginServiceScope {
pluginId?: string;
declaredLayoutTypes?: ReadonlySet<string>;
}
const DEFAULT_ROWS = 24;
const DEFAULT_COLS = 80;
const DEFAULT_EVENT_POLL_INTERVAL_MS = 1000;
@ -462,7 +492,10 @@ function workspacePathMatches(watchedPath: string, eventPath: string): boolean {
);
}
export function createPluginServices(gateways: PluginServiceGatewaySet): PluginServices {
export function createPluginServices(
gateways: PluginServiceGatewaySet,
scope: PluginServiceScope = {},
): PluginServices {
async function currentProject(): Promise<WorkspaceProject | null> {
const focused = await gateways.focusedProject.getFocusedProject();
return focused ? toWorkspaceProject(focused) : null;
@ -724,5 +757,28 @@ export function createPluginServices(gateways: PluginServiceGatewaySet): PluginS
},
};
return { workspace, tasks, tooling, events, config, terminal };
const windows: WindowService = {
async open(options) {
const layoutType = options.layoutType.trim();
if (!layoutType) {
throw new Error("layoutType is required");
}
if (scope.declaredLayoutTypes && !scope.declaredLayoutTypes.has(layoutType)) {
throw new Error(
`plugin "${scope.pluginId ?? "<unknown>"}" cannot open undeclared layout "${layoutType}"`,
);
}
const pluginId = scope.pluginId;
if (!pluginId) {
throw new Error("plugin id is required to open a plugin layout window");
}
return gateways.window.openPluginLayoutWindow({
pluginId,
layoutType,
state: options.state ?? null,
});
},
};
return { workspace, tasks, tooling, events, config, terminal, windows };
}