feat(plugins): load activation scope from plugin manifest

Plugins can now declare activationScope ("app" | "project") in their
manifest; loader/runtime honor it to defer activation of project-scoped
plugins until a project is focused instead of activating everything at
app bootstrap. Bumps sdk/IdeaSDK to the commit that adds the field.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-03 14:44:08 +02:00
parent 863d9b7277
commit e2da2d911e
16 changed files with 513 additions and 34 deletions

View File

@ -76,9 +76,16 @@ export interface PluginLoadFailure {
reason: string;
}
export interface PluginLoadPending {
pluginId: string;
displayName: string;
reason: string;
}
export interface PluginLoadResult {
registry: PluginRuntimeRegistry;
failures: PluginLoadFailure[];
pending: PluginLoadPending[];
}
export interface PluginLoadOptions {
@ -190,6 +197,20 @@ function hasCapability(entry: PluginRuntimePlugin, capability: string): boolean
return arrayOrEmpty<string>(objectOrEmpty(entry).capabilities).includes(capability);
}
function activationScope(entry: PluginRuntimePlugin): "app" | "project" {
return objectOrEmpty(entry).activationScope === "project" ? "project" : "app";
}
function pendingForProjectFocus(entry: PluginRuntimePlugin): PluginLoadPending {
const entryObject = objectOrEmpty(entry);
const pluginId = safePluginId(entry);
return {
pluginId,
displayName: nonEmptyString(entryObject.displayName) ?? pluginId,
reason: "En attente d'un projet actif.",
};
}
async function disposeAll(disposables: Disposable[], activation?: void | PluginActivation): Promise<void> {
try {
await activation?.dispose?.();
@ -324,13 +345,20 @@ export async function loadPlugins(
): Promise<PluginLoadResult> {
const registry = new PluginRuntimeRegistry();
const failures: PluginLoadFailure[] = [];
const pending: PluginLoadPending[] = [];
const resolvedOptions: Required<PluginLoadOptions> = {
timeoutMs: options.timeoutMs ?? DEFAULT_PLUGIN_LOAD_TIMEOUT_MS,
};
const entries = Array.isArray(catalogPlugins) ? catalogPlugins : [];
const focus = await gateways.focusedProject?.getFocusedProject?.();
const loadableEntries = entries.filter((entry) => {
if (activationScope(entry) !== "project" || focus) return true;
pending.push(pendingForProjectFocus(entry));
return false;
});
const results = await Promise.all(
entries.map((entry) => loadOne(entry, gateways, resolvedOptions)),
loadableEntries.map((entry) => loadOne(entry, gateways, resolvedOptions)),
);
for (const result of results) {
if ("failure" in result) {
@ -345,9 +373,9 @@ export async function loadPlugins(
if (entries.length > 0) {
console.info(
`[plugins] load complete loaded=${registry.list().length} failed=${failures.length}`,
`[plugins] load complete loaded=${registry.list().length} failed=${failures.length} pending=${pending.length}`,
);
}
return { registry, failures };
return { registry, failures, pending };
}