123 lines
4.3 KiB
TypeScript
123 lines
4.3 KiB
TypeScript
/**
|
|
* `PluginRuntimeProvider` — bootstraps the plugin runtime once per app session
|
|
* (ticket #43, F1, carnet §1.3): fetches the runtime catalog (already filtered
|
|
* to `enabled && !pendingUninstall` by the backend) and loads every bundle via
|
|
* {@link loadPlugins}, then exposes the resulting {@link PluginRuntimeRegistry}
|
|
* to the rest of the tree (menus in F3, layouts in F4).
|
|
*
|
|
* Mounted once near the app root, inside `<DIProvider>` (it reads gateways via
|
|
* `useGateways()`). Never re-runs within a session — carnet §1.3: disable/
|
|
* uninstall only mask contributions in-session; the actual reload happens on
|
|
* the next app start (a fresh mount of this provider).
|
|
*/
|
|
|
|
import { createContext, useContext, useEffect, useState, type ReactNode } from "react";
|
|
|
|
import { loadPlugins, PluginRuntimeRegistry, type PluginLoadFailure } from "@/plugins/runtime";
|
|
import { useGateways } from "@/app/di";
|
|
|
|
export interface PluginRuntimeContextValue {
|
|
registry: PluginRuntimeRegistry;
|
|
failures: PluginLoadFailure[];
|
|
/** True until the initial catalog fetch + bundle loads have settled. */
|
|
loading: boolean;
|
|
}
|
|
|
|
/**
|
|
* Default value used outside a `<PluginRuntimeProvider>` (e.g. `ProjectsView`
|
|
* rendered directly in tests/Storybook without the full app shell): an empty,
|
|
* already-settled registry rather than a hard requirement to wrap every call
|
|
* site. Plugin contributions are strictly additive, so their absence must
|
|
* never be a reason a surface fails to render.
|
|
*/
|
|
const EMPTY_PLUGIN_RUNTIME: PluginRuntimeContextValue = {
|
|
registry: new PluginRuntimeRegistry(),
|
|
failures: [],
|
|
loading: false,
|
|
};
|
|
|
|
const PluginRuntimeContext = createContext<PluginRuntimeContextValue>(EMPTY_PLUGIN_RUNTIME);
|
|
|
|
function describeError(e: unknown): string {
|
|
if (e && typeof e === "object" && "message" in e) {
|
|
return String((e as { message: unknown }).message);
|
|
}
|
|
return String(e);
|
|
}
|
|
|
|
interface PluginRuntimeProviderProps {
|
|
children: ReactNode;
|
|
/** Test/Storybook escape hatch — skips the gateway fetch and uses this value as-is. */
|
|
value?: PluginRuntimeContextValue;
|
|
}
|
|
|
|
export function PluginRuntimeProvider({ children, value: injected }: PluginRuntimeProviderProps) {
|
|
const gateways = useGateways();
|
|
const [value, setValue] = useState<PluginRuntimeContextValue>(
|
|
injected ?? {
|
|
registry: new PluginRuntimeRegistry(),
|
|
failures: [],
|
|
loading: true,
|
|
},
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (injected) return;
|
|
let cancelled = false;
|
|
gateways.plugin
|
|
.listRuntimeContributions()
|
|
.then((catalog) =>
|
|
loadPlugins(catalog.plugins, {
|
|
project: gateways.project,
|
|
git: gateways.git,
|
|
terminal: gateways.terminal,
|
|
agents: gateways.agent,
|
|
system: gateways.system,
|
|
workState: gateways.workState,
|
|
focusedProject: gateways.focusedProject,
|
|
pluginWorkspace: gateways.pluginWorkspace,
|
|
pluginTask: gateways.pluginTask,
|
|
pluginToolchain: gateways.pluginToolchain,
|
|
pluginEvents: gateways.pluginEvents,
|
|
pluginConfig: gateways.pluginConfig,
|
|
}),
|
|
)
|
|
.then((result) => {
|
|
if (cancelled) return;
|
|
setValue({ registry: result.registry, failures: result.failures, loading: false });
|
|
})
|
|
.catch((e: unknown) => {
|
|
// No plugin gateway / catalog fetch failed: run with zero plugins
|
|
// rather than blocking the app (full-trust plugins are additive).
|
|
if (!cancelled) {
|
|
setValue((prev) => ({
|
|
...prev,
|
|
failures: [
|
|
...prev.failures,
|
|
{ pluginId: "<runtime-catalog>", reason: describeError(e) },
|
|
],
|
|
loading: false,
|
|
}));
|
|
}
|
|
});
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
// Gateways are a stable singleton for the app session (from `useGateways`);
|
|
// re-running on every render would reload every plugin bundle.
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
|
|
return (
|
|
<PluginRuntimeContext.Provider value={value}>{children}</PluginRuntimeContext.Provider>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Reads the loaded plugin runtime registry. Outside a `<PluginRuntimeProvider>`
|
|
* this is the empty registry (see {@link EMPTY_PLUGIN_RUNTIME}), never a throw.
|
|
*/
|
|
export function usePluginRuntime(): PluginRuntimeContextValue {
|
|
return useContext(PluginRuntimeContext);
|
|
}
|