feat(frontend): système de plugins — runtime, menus, layouts custom (#43)
Lots F1-F4 : runtime de chargement/registre plugin, extension des menus existants, panneau de gestion des plugins, types de layout custom (sélecteur, fallback, cellule dédiée) branchés sur le port plugin. Suite npm typecheck/test verte (947/947). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@ -38,6 +38,13 @@ import type {
|
||||
PairedDevice,
|
||||
PairingCode,
|
||||
PermissionSet,
|
||||
PluginAdmin,
|
||||
PluginContributionSummary,
|
||||
PluginInstallResult,
|
||||
PluginLifecycleState,
|
||||
PluginReview,
|
||||
PluginRuntimeContributionCatalog,
|
||||
PluginUninstallResult,
|
||||
Project,
|
||||
ProjectMcpToolPermissions,
|
||||
ProjectPermissions,
|
||||
@ -88,8 +95,10 @@ import type {
|
||||
ProfileGateway,
|
||||
ProjectGateway,
|
||||
PermissionGateway,
|
||||
PluginGateway,
|
||||
ReattachResult,
|
||||
RemoteGateway,
|
||||
ReviewPluginPackageInput,
|
||||
SkillGateway,
|
||||
StoppedLiveAgent,
|
||||
SystemGateway,
|
||||
@ -184,6 +193,11 @@ export class MockSystemGateway implements SystemGateway {
|
||||
return "/home/user/mock-project";
|
||||
}
|
||||
|
||||
/** Returns a deterministic fake path — never opens a native dialog. */
|
||||
async pickArchiveFile(): Promise<string | null> {
|
||||
return "/home/user/mock-plugin.ideaplug";
|
||||
}
|
||||
|
||||
private exitGuardListeners = new Set<(state: AppExitWorkGuardState) => void>();
|
||||
/** Count of `confirmAppExit()` calls, for test assertions. */
|
||||
confirmAppExitCallCount = 0;
|
||||
@ -2923,6 +2937,114 @@ function mostRestrictive(
|
||||
return rank[agent] >= rank[fallback] ? agent : fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* In-memory plugin store (ticket #43, F1). Mirrors the carnet contract closely
|
||||
* enough to develop/test F1-F4 without the backend (B1-B4, landing in
|
||||
* parallel): review before commit, install/enable/disable/uninstall, and a
|
||||
* runtime catalog that only ever lists `enabled && !pendingUninstall` plugins.
|
||||
*/
|
||||
export class MockPluginGateway implements PluginGateway {
|
||||
private plugins: PluginAdmin[] = [];
|
||||
private seq = 0;
|
||||
|
||||
private summaryFor(_pluginId: string): PluginContributionSummary {
|
||||
return { topLevelMenus: 0, menuItems: 0, layouts: 0, mcpServers: 0 };
|
||||
}
|
||||
|
||||
/** Test/dev helper: seed a plugin directly, bypassing install. */
|
||||
_seedPlugin(plugin: PluginAdmin): void {
|
||||
this.plugins.push(plugin);
|
||||
}
|
||||
|
||||
async listPlugins(): Promise<PluginAdmin[]> {
|
||||
return structuredClone(this.plugins);
|
||||
}
|
||||
|
||||
async reviewPackage(input: ReviewPluginPackageInput): Promise<PluginReview> {
|
||||
this.seq += 1;
|
||||
const label = input.path.split("/").pop() ?? input.path;
|
||||
return {
|
||||
id: `mock.plugin.${this.seq}`,
|
||||
displayName: label.replace(/\.(ideaplug|zip|vsix)$/i, ""),
|
||||
publisher: "Mock Publisher",
|
||||
version: "0.1.0",
|
||||
description: `Reviewed from ${input.sourceKind}: ${input.path}`,
|
||||
trustLevel: "full",
|
||||
contributionSummary: this.summaryFor(`mock.plugin.${this.seq}`),
|
||||
issues: [],
|
||||
installable: true,
|
||||
};
|
||||
}
|
||||
|
||||
private async install(
|
||||
sourceKind: "archive" | "directory",
|
||||
path: string,
|
||||
): Promise<PluginInstallResult> {
|
||||
this.seq += 1;
|
||||
const label = path.split("/").pop() ?? path;
|
||||
const plugin: PluginAdmin = {
|
||||
id: `mock.plugin.${this.seq}`,
|
||||
displayName: label.replace(/\.(ideaplug|zip|vsix)$/i, ""),
|
||||
publisher: "Mock Publisher",
|
||||
version: "0.1.0",
|
||||
sourceKind,
|
||||
sourceLabel: path,
|
||||
lifecycleState: "enabled",
|
||||
enabled: true,
|
||||
pendingUninstall: false,
|
||||
restartRequired: true,
|
||||
trustLevel: "full",
|
||||
contributionSummary: this.summaryFor(`mock.plugin.${this.seq}`),
|
||||
};
|
||||
this.plugins.push(plugin);
|
||||
return { plugin: structuredClone(plugin), restartRequired: true };
|
||||
}
|
||||
|
||||
installFromArchive(path: string): Promise<PluginInstallResult> {
|
||||
return this.install("archive", path);
|
||||
}
|
||||
|
||||
installFromDirectory(path: string): Promise<PluginInstallResult> {
|
||||
return this.install("directory", path);
|
||||
}
|
||||
|
||||
async setEnabled(pluginId: string, enabled: boolean): Promise<PluginAdmin> {
|
||||
const plugin = this.plugins.find((p) => p.id === pluginId);
|
||||
if (!plugin) {
|
||||
const err: GatewayError = { code: "NOT_FOUND", message: `plugin ${pluginId} not found` };
|
||||
throw err;
|
||||
}
|
||||
const state: PluginLifecycleState = enabled ? "enabled" : "disabled";
|
||||
plugin.enabled = enabled;
|
||||
plugin.lifecycleState = state;
|
||||
plugin.restartRequired = true;
|
||||
return structuredClone(plugin);
|
||||
}
|
||||
|
||||
async uninstall(pluginId: string): Promise<PluginUninstallResult> {
|
||||
const idx = this.plugins.findIndex((p) => p.id === pluginId);
|
||||
if (idx === -1) {
|
||||
const err: GatewayError = { code: "NOT_FOUND", message: `plugin ${pluginId} not found` };
|
||||
throw err;
|
||||
}
|
||||
this.plugins.splice(idx, 1);
|
||||
return { pluginId, restartRequired: true };
|
||||
}
|
||||
|
||||
async listRuntimeContributions(): Promise<PluginRuntimeContributionCatalog> {
|
||||
// Only enabled, non-pending-uninstall plugins are loadable at bootstrap
|
||||
// (carnet §1.3) — the mock has no bundle to actually import, so this
|
||||
// starts empty; tests seed `plugins` on `PluginRuntimeContributionCatalog`
|
||||
// directly via a `MockPluginGateway` subclass/test double when a loader
|
||||
// round-trip is needed.
|
||||
return { plugins: [] };
|
||||
}
|
||||
|
||||
async openPluginsFolder(_pluginId?: string): Promise<void> {
|
||||
// No filesystem in the mock — no-op.
|
||||
}
|
||||
}
|
||||
|
||||
/** Builds the full set of mock gateways. */
|
||||
export function createMockGateways(): Gateways {
|
||||
const agentGateway = new MockAgentGateway();
|
||||
@ -2951,6 +3073,7 @@ export function createMockGateways(): Gateways {
|
||||
window: new MockWindowGateway(),
|
||||
focusedProject: new MockFocusedProjectGateway(),
|
||||
uiPreferences: new MockUiPreferencesGateway(),
|
||||
plugin: new MockPluginGateway(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user