Files
kandev/apps/web/components/plugins/plugin-slot.tsx
T
João Salavisa 30e6cba810 feat(plugins): add owner-scoped plugin-settings slot for inline plugin UI (#1836)
* feat(plugins): add plugin-settings slot to per-plugin settings page

Add a named "plugin-settings" host slot so an installed plugin can render
its own inline UI (e.g. a live integration/health status card) on its
per-plugin settings page (Settings > Plugins > <plugin>), between the
schema-driven settings form and the manifest card.

The slot renders on every plugin's detail page, so the host forwards the
current plugin's identity via slotProps ({ pluginId, status }); registered
components must gate on slotProps.pluginId to avoid rendering on another
plugin's page. Documented in PLUGIN-API.md and mirrored in types.ts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(plugins): make plugin-settings slot owner-scoped in the host

Instead of forcing every plugin author to early-return unless
slotProps.pluginId matches its own id, the host now filters the
"plugin-settings" slot by the owning plugin. The registry already records
the owning pluginId per registration, so PluginSlot gains an optional
ownerPluginId prop backed by a new getSlotComponentsForPlugin() getter,
and the plugin-detail page passes the current plugin's id.

A plugin's settings card now renders only on its own settings page, with
no gating boilerplate in the plugin. Docs/types updated to match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(plugins): add plugin-settings slot usage mockups

Add illustrative mockups of the plugin-settings slot rendered on a plugin's
settings page (Provider Usage health card) plus a strip of possible usages
(healthy, degraded, and a generic integration), referenced from PLUGIN-API.md.
Clearly labeled as mockups, not real product screenshots.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(plugins): reset error boundary per owner in owner-scoped slots

Address review feedback on PR #1836:

- The PluginErrorBoundary key was `${name}-${index}`, identical across
  plugins for an owner-scoped slot. Navigating /settings/plugins/A -> B in
  the same settings shell (without remounting PluginSlot) reused A's errored
  boundary and silently hid B's healthy card. Include ownerPluginId in the
  key so each owner gets an independent boundary; add a regression test.
- Trim the verbose multi-line slot comment in plugin-detail to one line per
  the repo's comment convention.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(plugins): document plugin-settings slot; drop mockup binaries

- Public docs: add the owner-scoped `plugin-settings` slot to the Named
  slots table and a "Plugin settings page" section in plugins-authoring.md,
  and mention inline status cards in plugins.md's per-plugin settings.
- Remove the illustrative mockup PNGs (and their PLUGIN-API.md embeds) that
  were committed to the branch — image binaries should not merge into main;
  PR screenshots belong on a throwaway media ref instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(plugins): condense owner-key boundary comment to one line

Address review nit — the repo prefers one short comment line.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 11:53:43 +01:00

48 lines
1.8 KiB
TypeScript

"use client";
import { usePluginRegistry } from "@/lib/plugins/registry";
import type { SlotComponent } from "@/lib/plugins/types";
import { PluginErrorBoundary } from "./plugin-error-boundary";
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;
/**
* When set, render only the components registered by this plugin. Used by
* owner-scoped slots (e.g. "plugin-settings" on a plugin's own settings
* page) so the host isolates by owner and plugin authors don't have to gate
* on the current plugin id themselves.
*/
ownerPluginId?: string;
};
/**
* 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. Pass
* `ownerPluginId` to restrict rendering to that plugin's own components.
*/
export function PluginSlot({ name, slotProps, ownerPluginId }: PluginSlotProps) {
const registry = usePluginRegistry();
const components = ownerPluginId
? registry.getSlotComponentsForPlugin(name, ownerPluginId)
: registry.getSlotComponents(name);
if (components.length === 0) return null;
// Key by owner too, so navigating A -> B resets the boundary instead of reusing A's error (hiding B).
const keyBase = ownerPluginId ? `${name}-${ownerPluginId}` : name;
return (
<>
{components.map((SlotComponentImpl: SlotComponent, index) => (
<PluginErrorBoundary key={`${keyBase}-${index}`} context={`slot "${name}" component`}>
<SlotComponentImpl slotProps={slotProps} />
</PluginErrorBoundary>
))}
</>
);
}