Files
kandev/apps/web/components/plugins/plugin-slot.tsx
T
Kandev Agent c53f699714 feat: add plugin system with grpc runtime and native ui plugins
- 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>
2026-07-16 17:55:45 +01:00

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>
))}
</>
);
}