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:
@ -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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user