c53f699714
- go plugin backends ship as release tarballs (per-platform binaries, checksums) that kandev installs from url/upload or filesystem sideload+sync, then spawns and supervises via hashicorp/go-plugin (unix socket / loopback tcp+automtls) - strict typed grpc contract (kandev.plugin.v1): DeliverEvent/InvokeTool/HandleWebhook plus a capability-gated host service (state, secrets, emit); author sdk in pkg/pluginsdk; packaging cli in cmd/plugin-pack - native frontend bundles: window.registerKandevPlugin + registry/host loader with dynamic routes, nav items, slots and ws handlers; plugins settings page with install dialog, sync button and enable/disable/uninstall - gated behind the plugins feature flag (prod off, dev/e2e on); covered by unit and integration tests plus playwright e2e against a real packaged fixture plugin Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
73 lines
2.0 KiB
TypeScript
73 lines
2.0 KiB
TypeScript
"use client";
|
|
|
|
import { Component, type ErrorInfo, type ReactNode } from "react";
|
|
import { usePluginRegistry } from "@/lib/plugins/registry";
|
|
import type { SlotComponent } from "@/lib/plugins/types";
|
|
|
|
type PluginSlotErrorBoundaryProps = {
|
|
slotName: string;
|
|
children: ReactNode;
|
|
};
|
|
|
|
type PluginSlotErrorBoundaryState = {
|
|
error: Error | null;
|
|
};
|
|
|
|
/**
|
|
* Isolates a single plugin slot component. Plugin code runs in-process (see
|
|
* PLUGIN-API.md security posture) — a throw anywhere in one plugin's slot
|
|
* render must not tear down the host surface or other plugins' components.
|
|
*/
|
|
class PluginSlotErrorBoundary extends Component<
|
|
PluginSlotErrorBoundaryProps,
|
|
PluginSlotErrorBoundaryState
|
|
> {
|
|
state: PluginSlotErrorBoundaryState = { error: null };
|
|
|
|
static getDerivedStateFromError(error: Error): PluginSlotErrorBoundaryState {
|
|
return { error };
|
|
}
|
|
|
|
componentDidCatch(error: Error, info: ErrorInfo): void {
|
|
console.error(
|
|
`[plugins] slot "${this.props.slotName}" component threw`,
|
|
error,
|
|
info.componentStack,
|
|
);
|
|
}
|
|
|
|
render(): ReactNode {
|
|
if (this.state.error) return null;
|
|
return this.props.children;
|
|
}
|
|
}
|
|
|
|
export type PluginSlotProps = {
|
|
/** Named slot to render — see PLUGIN-API.md for the initial set of slot names. */
|
|
name: string;
|
|
/** Forwarded to each registered component as `slotProps`. */
|
|
slotProps?: unknown;
|
|
};
|
|
|
|
/**
|
|
* Renders every plugin component registered for the named slot
|
|
* (`registry.registerComponent(name, Component)`), each isolated behind its
|
|
* own error boundary so one broken plugin can't break the host surface.
|
|
*/
|
|
export function PluginSlot({ name, slotProps }: PluginSlotProps) {
|
|
const registry = usePluginRegistry();
|
|
const components = registry.getSlotComponents(name);
|
|
|
|
if (components.length === 0) return null;
|
|
|
|
return (
|
|
<>
|
|
{components.map((SlotComponentImpl: SlotComponent, index) => (
|
|
<PluginSlotErrorBoundary key={`${name}-${index}`} slotName={name}>
|
|
<SlotComponentImpl slotProps={slotProps} />
|
|
</PluginSlotErrorBoundary>
|
|
))}
|
|
</>
|
|
);
|
|
}
|