// Renders every plugin bundle registered for a named UI slot (SPEC ยง11.3). // // The host owns a container
and delegates rendering to each bundle's mount // function (framework-agnostic contract), tearing it down on unmount. Slot-specific // data (e.g. the current messageId) is passed as `props` at mount time. import { useEffect, useRef } from "react"; import { loadBundle } from "./bundleLoader"; import { usePluginContext } from "./context"; import { usePluginSlots } from "./manifest"; import type { PluginUnmount } from "./types"; function SlotMount({ pluginName, bundle, slotName, props, }: { pluginName: string; bundle: string; slotName: string; props?: Record; }) { const ref = useRef(null); const ctx = usePluginContext(pluginName); useEffect(() => { let cleanup: PluginUnmount | void; let alive = true; loadBundle(bundle) .then((mount) => { if (alive && ref.current) cleanup = mount(ref.current, ctx, props); }) .catch((err) => console.error(`plugin slot ${slotName} (${pluginName}) failed`, err)); return () => { alive = false; if (typeof cleanup === "function") cleanup(); }; // props is read once at mount; plugins react to later changes via ctx.events. // eslint-disable-next-line react-hooks/exhaustive-deps }, [bundle, ctx]); return
; } export function PluginSlot({ name, props }: { name: string; props?: Record }) { const bindings = usePluginSlots(name); if (bindings.length === 0) return null; return ( <> {bindings.map((b) => ( ))} ); }