Add plugin framework and settings UI
This commit is contained in:
310
packages/plugins/sdk/src/ui/components.ts
Normal file
310
packages/plugins/sdk/src/ui/components.ts
Normal file
@@ -0,0 +1,310 @@
|
||||
/**
|
||||
* Shared UI component declarations for plugin frontends.
|
||||
*
|
||||
* These components are exported from `@paperclipai/plugin-sdk/ui` and are
|
||||
* provided by the host at runtime. They match the host's design tokens and
|
||||
* visual language, reducing the boilerplate needed to build consistent plugin UIs.
|
||||
*
|
||||
* **Plugins are not required to use these components.** They exist to reduce
|
||||
* boilerplate and keep visual consistency. A plugin may render entirely custom
|
||||
* UI using any React component library.
|
||||
*
|
||||
* Component implementations are provided by the host — plugin bundles contain
|
||||
* only the type declarations; the runtime implementations are injected via the
|
||||
* host module registry.
|
||||
*
|
||||
* @see PLUGIN_SPEC.md §19.6 — Shared Components In `@paperclipai/plugin-sdk/ui`
|
||||
*/
|
||||
|
||||
import type React from "react";
|
||||
import { renderSdkUiComponent } from "./runtime.js";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Component prop interfaces
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* A trend value that can accompany a metric.
|
||||
* Positive values indicate upward trends; negative values indicate downward trends.
|
||||
*/
|
||||
export interface MetricTrend {
|
||||
/** Direction of the trend. */
|
||||
direction: "up" | "down" | "flat";
|
||||
/** Percentage change value (e.g. `12.5` for 12.5%). */
|
||||
percentage?: number;
|
||||
}
|
||||
|
||||
/** Props for `MetricCard`. */
|
||||
export interface MetricCardProps {
|
||||
/** Short label describing the metric (e.g. `"Synced Issues"`). */
|
||||
label: string;
|
||||
/** The metric value to display. */
|
||||
value: number | string;
|
||||
/** Optional trend indicator. */
|
||||
trend?: MetricTrend;
|
||||
/** Optional sparkline data (array of numbers, latest last). */
|
||||
sparkline?: number[];
|
||||
/** Optional unit suffix (e.g. `"%"`, `"ms"`). */
|
||||
unit?: string;
|
||||
}
|
||||
|
||||
/** Status variants for `StatusBadge`. */
|
||||
export type StatusBadgeVariant = "ok" | "warning" | "error" | "info" | "pending";
|
||||
|
||||
/** Props for `StatusBadge`. */
|
||||
export interface StatusBadgeProps {
|
||||
/** Human-readable label. */
|
||||
label: string;
|
||||
/** Visual variant determining colour. */
|
||||
status: StatusBadgeVariant;
|
||||
}
|
||||
|
||||
/** A single column definition for `DataTable`. */
|
||||
export interface DataTableColumn<T = Record<string, unknown>> {
|
||||
/** Column key, matching a field on the row object. */
|
||||
key: keyof T & string;
|
||||
/** Column header label. */
|
||||
header: string;
|
||||
/** Optional custom cell renderer. */
|
||||
render?: (value: unknown, row: T) => React.ReactNode;
|
||||
/** Whether this column is sortable. */
|
||||
sortable?: boolean;
|
||||
/** CSS width (e.g. `"120px"`, `"20%"`). */
|
||||
width?: string;
|
||||
}
|
||||
|
||||
/** Props for `DataTable`. */
|
||||
export interface DataTableProps<T = Record<string, unknown>> {
|
||||
/** Column definitions. */
|
||||
columns: DataTableColumn<T>[];
|
||||
/** Row data. Each row should have a stable `id` field. */
|
||||
rows: T[];
|
||||
/** Whether the table is currently loading. */
|
||||
loading?: boolean;
|
||||
/** Message shown when `rows` is empty. */
|
||||
emptyMessage?: string;
|
||||
/** Total row count for pagination (if different from `rows.length`). */
|
||||
totalCount?: number;
|
||||
/** Current page (0-based, for pagination). */
|
||||
page?: number;
|
||||
/** Rows per page (for pagination). */
|
||||
pageSize?: number;
|
||||
/** Callback when page changes. */
|
||||
onPageChange?: (page: number) => void;
|
||||
/** Callback when a column header is clicked to sort. */
|
||||
onSort?: (key: string, direction: "asc" | "desc") => void;
|
||||
}
|
||||
|
||||
/** A single data point for `TimeseriesChart`. */
|
||||
export interface TimeseriesDataPoint {
|
||||
/** ISO 8601 timestamp. */
|
||||
timestamp: string;
|
||||
/** Numeric value. */
|
||||
value: number;
|
||||
/** Optional label for the point. */
|
||||
label?: string;
|
||||
}
|
||||
|
||||
/** Props for `TimeseriesChart`. */
|
||||
export interface TimeseriesChartProps {
|
||||
/** Series data. */
|
||||
data: TimeseriesDataPoint[];
|
||||
/** Chart title. */
|
||||
title?: string;
|
||||
/** Y-axis label. */
|
||||
yLabel?: string;
|
||||
/** Chart type. Defaults to `"line"`. */
|
||||
type?: "line" | "bar";
|
||||
/** Height of the chart in pixels. Defaults to `200`. */
|
||||
height?: number;
|
||||
/** Whether the chart is currently loading. */
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
/** Props for `MarkdownBlock`. */
|
||||
export interface MarkdownBlockProps {
|
||||
/** Markdown content to render. */
|
||||
content: string;
|
||||
}
|
||||
|
||||
/** A single key-value pair for `KeyValueList`. */
|
||||
export interface KeyValuePair {
|
||||
/** Label for the key. */
|
||||
label: string;
|
||||
/** Value to display. May be a string, number, or a React node. */
|
||||
value: React.ReactNode;
|
||||
}
|
||||
|
||||
/** Props for `KeyValueList`. */
|
||||
export interface KeyValueListProps {
|
||||
/** Pairs to render in the list. */
|
||||
pairs: KeyValuePair[];
|
||||
}
|
||||
|
||||
/** A single action button for `ActionBar`. */
|
||||
export interface ActionBarItem {
|
||||
/** Button label. */
|
||||
label: string;
|
||||
/** Action key to call via the plugin bridge. */
|
||||
actionKey: string;
|
||||
/** Optional parameters to pass to the action handler. */
|
||||
params?: Record<string, unknown>;
|
||||
/** Button variant. Defaults to `"default"`. */
|
||||
variant?: "default" | "primary" | "destructive";
|
||||
/** Whether to show a confirmation dialog before executing. */
|
||||
confirm?: boolean;
|
||||
/** Text for the confirmation dialog (used when `confirm` is true). */
|
||||
confirmMessage?: string;
|
||||
}
|
||||
|
||||
/** Props for `ActionBar`. */
|
||||
export interface ActionBarProps {
|
||||
/** Action definitions. */
|
||||
actions: ActionBarItem[];
|
||||
/** Called after an action succeeds. Use to trigger data refresh. */
|
||||
onSuccess?: (actionKey: string, result: unknown) => void;
|
||||
/** Called when an action fails. */
|
||||
onError?: (actionKey: string, error: unknown) => void;
|
||||
}
|
||||
|
||||
/** A single log line for `LogView`. */
|
||||
export interface LogViewEntry {
|
||||
/** ISO 8601 timestamp. */
|
||||
timestamp: string;
|
||||
/** Log level. */
|
||||
level: "info" | "warn" | "error" | "debug";
|
||||
/** Log message. */
|
||||
message: string;
|
||||
/** Optional structured metadata. */
|
||||
meta?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/** Props for `LogView`. */
|
||||
export interface LogViewProps {
|
||||
/** Log entries to display. */
|
||||
entries: LogViewEntry[];
|
||||
/** Maximum height of the scrollable container (CSS value). Defaults to `"400px"`. */
|
||||
maxHeight?: string;
|
||||
/** Whether to auto-scroll to the latest entry. */
|
||||
autoScroll?: boolean;
|
||||
/** Whether the log is currently loading. */
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
/** Props for `JsonTree`. */
|
||||
export interface JsonTreeProps {
|
||||
/** The data to render as a collapsible JSON tree. */
|
||||
data: unknown;
|
||||
/** Initial depth to expand. Defaults to `2`. */
|
||||
defaultExpandDepth?: number;
|
||||
}
|
||||
|
||||
/** Props for `Spinner`. */
|
||||
export interface SpinnerProps {
|
||||
/** Size of the spinner. Defaults to `"md"`. */
|
||||
size?: "sm" | "md" | "lg";
|
||||
/** Accessible label for the spinner (used as `aria-label`). */
|
||||
label?: string;
|
||||
}
|
||||
|
||||
/** Props for `ErrorBoundary`. */
|
||||
export interface ErrorBoundaryProps {
|
||||
/** Content to render inside the error boundary. */
|
||||
children: React.ReactNode;
|
||||
/** Optional custom fallback to render when an error is caught. */
|
||||
fallback?: React.ReactNode;
|
||||
/** Called when an error is caught, for logging or reporting. */
|
||||
onError?: (error: Error, info: React.ErrorInfo) => void;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Component declarations (provided by host at runtime)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// These are declared as ambient values so plugin TypeScript code can import
|
||||
// and use them with full type-checking. The host's module registry provides
|
||||
// the concrete React component implementations at bundle load time.
|
||||
|
||||
/**
|
||||
* Displays a single metric with an optional trend indicator and sparkline.
|
||||
*
|
||||
* @see PLUGIN_SPEC.md §19.6 — Shared Components
|
||||
*/
|
||||
function createSdkUiComponent<TProps>(name: string): React.ComponentType<TProps> {
|
||||
return function PaperclipSdkUiComponent(props: TProps) {
|
||||
return renderSdkUiComponent(name, props) as React.ReactNode;
|
||||
};
|
||||
}
|
||||
|
||||
export const MetricCard = createSdkUiComponent<MetricCardProps>("MetricCard");
|
||||
|
||||
/**
|
||||
* Displays an inline status badge (ok / warning / error / info / pending).
|
||||
*
|
||||
* @see PLUGIN_SPEC.md §19.6 — Shared Components
|
||||
*/
|
||||
export const StatusBadge = createSdkUiComponent<StatusBadgeProps>("StatusBadge");
|
||||
|
||||
/**
|
||||
* Sortable, paginated data table.
|
||||
*
|
||||
* @see PLUGIN_SPEC.md §19.6 — Shared Components
|
||||
*/
|
||||
export const DataTable = createSdkUiComponent<DataTableProps>("DataTable");
|
||||
|
||||
/**
|
||||
* Line or bar chart for time-series data.
|
||||
*
|
||||
* @see PLUGIN_SPEC.md §19.6 — Shared Components
|
||||
*/
|
||||
export const TimeseriesChart = createSdkUiComponent<TimeseriesChartProps>("TimeseriesChart");
|
||||
|
||||
/**
|
||||
* Renders Markdown text as HTML.
|
||||
*
|
||||
* @see PLUGIN_SPEC.md §19.6 — Shared Components
|
||||
*/
|
||||
export const MarkdownBlock = createSdkUiComponent<MarkdownBlockProps>("MarkdownBlock");
|
||||
|
||||
/**
|
||||
* Renders a definition-list of label/value pairs.
|
||||
*
|
||||
* @see PLUGIN_SPEC.md §19.6 — Shared Components
|
||||
*/
|
||||
export const KeyValueList = createSdkUiComponent<KeyValueListProps>("KeyValueList");
|
||||
|
||||
/**
|
||||
* Row of action buttons wired to the plugin bridge's `performAction` handlers.
|
||||
*
|
||||
* @see PLUGIN_SPEC.md §19.6 — Shared Components
|
||||
*/
|
||||
export const ActionBar = createSdkUiComponent<ActionBarProps>("ActionBar");
|
||||
|
||||
/**
|
||||
* Scrollable, timestamped log output viewer.
|
||||
*
|
||||
* @see PLUGIN_SPEC.md §19.6 — Shared Components
|
||||
*/
|
||||
export const LogView = createSdkUiComponent<LogViewProps>("LogView");
|
||||
|
||||
/**
|
||||
* Collapsible JSON tree for debugging or raw data inspection.
|
||||
*
|
||||
* @see PLUGIN_SPEC.md §19.6 — Shared Components
|
||||
*/
|
||||
export const JsonTree = createSdkUiComponent<JsonTreeProps>("JsonTree");
|
||||
|
||||
/**
|
||||
* Loading indicator.
|
||||
*
|
||||
* @see PLUGIN_SPEC.md §19.6 — Shared Components
|
||||
*/
|
||||
export const Spinner = createSdkUiComponent<SpinnerProps>("Spinner");
|
||||
|
||||
/**
|
||||
* React error boundary that prevents plugin rendering errors from crashing
|
||||
* the host page.
|
||||
*
|
||||
* @see PLUGIN_SPEC.md §19.7 — Error Propagation Through The Bridge
|
||||
*/
|
||||
export const ErrorBoundary = createSdkUiComponent<ErrorBoundaryProps>("ErrorBoundary");
|
||||
153
packages/plugins/sdk/src/ui/hooks.ts
Normal file
153
packages/plugins/sdk/src/ui/hooks.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import type { PluginDataResult, PluginActionFn, PluginHostContext, PluginStreamResult } from "./types.js";
|
||||
import { getSdkUiRuntimeValue } from "./runtime.js";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// usePluginData
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Fetch data from the plugin worker's registered `getData` handler.
|
||||
*
|
||||
* Calls `ctx.data.register(key, handler)` in the worker and returns the
|
||||
* result as reactive state. Re-fetches when `params` changes.
|
||||
*
|
||||
* @template T The expected shape of the returned data
|
||||
* @param key - The data key matching the handler registered with `ctx.data.register()`
|
||||
* @param params - Optional parameters forwarded to the handler
|
||||
* @returns `PluginDataResult<T>` with `data`, `loading`, `error`, and `refresh`
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* function SyncWidget({ context }: PluginWidgetProps) {
|
||||
* const { data, loading, error } = usePluginData<SyncHealth>("sync-health", {
|
||||
* companyId: context.companyId,
|
||||
* });
|
||||
*
|
||||
* if (loading) return <Spinner />;
|
||||
* if (error) return <div>Error: {error.message}</div>;
|
||||
* return <MetricCard label="Synced Issues" value={data!.syncedCount} />;
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @see PLUGIN_SPEC.md §13.8 — `getData`
|
||||
* @see PLUGIN_SPEC.md §19.7 — Error Propagation Through The Bridge
|
||||
*/
|
||||
export function usePluginData<T = unknown>(
|
||||
key: string,
|
||||
params?: Record<string, unknown>,
|
||||
): PluginDataResult<T> {
|
||||
const impl = getSdkUiRuntimeValue<
|
||||
(nextKey: string, nextParams?: Record<string, unknown>) => PluginDataResult<T>
|
||||
>("usePluginData");
|
||||
return impl(key, params);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// usePluginAction
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Get a callable function that invokes the plugin worker's registered
|
||||
* `performAction` handler.
|
||||
*
|
||||
* The returned function is async and throws a `PluginBridgeError` on failure.
|
||||
*
|
||||
* @param key - The action key matching the handler registered with `ctx.actions.register()`
|
||||
* @returns An async function that sends the action to the worker and resolves with the result
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* function ResyncButton({ context }: PluginWidgetProps) {
|
||||
* const resync = usePluginAction("resync");
|
||||
* const [error, setError] = useState<string | null>(null);
|
||||
*
|
||||
* async function handleClick() {
|
||||
* try {
|
||||
* await resync({ companyId: context.companyId });
|
||||
* } catch (err) {
|
||||
* setError((err as PluginBridgeError).message);
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* return <button onClick={handleClick}>Resync Now</button>;
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @see PLUGIN_SPEC.md §13.9 — `performAction`
|
||||
* @see PLUGIN_SPEC.md §19.7 — Error Propagation Through The Bridge
|
||||
*/
|
||||
export function usePluginAction(key: string): PluginActionFn {
|
||||
const impl = getSdkUiRuntimeValue<(nextKey: string) => PluginActionFn>("usePluginAction");
|
||||
return impl(key);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// useHostContext
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Read the current host context (active company, project, entity, user).
|
||||
*
|
||||
* Use this to know which context the plugin component is being rendered in
|
||||
* so you can scope data requests and actions accordingly.
|
||||
*
|
||||
* @returns The current `PluginHostContext`
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* function IssueTab() {
|
||||
* const { companyId, entityId } = useHostContext();
|
||||
* const { data } = usePluginData("linear-link", { issueId: entityId });
|
||||
* return <div>{data?.linearIssueUrl}</div>;
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @see PLUGIN_SPEC.md §19 — UI Extension Model
|
||||
*/
|
||||
export function useHostContext(): PluginHostContext {
|
||||
const impl = getSdkUiRuntimeValue<() => PluginHostContext>("useHostContext");
|
||||
return impl();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// usePluginStream
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Subscribe to a real-time event stream pushed from the plugin worker.
|
||||
*
|
||||
* Opens an SSE connection to `GET /api/plugins/:pluginId/bridge/stream/:channel`
|
||||
* and accumulates events as they arrive. The worker pushes events using
|
||||
* `ctx.streams.emit(channel, event)`.
|
||||
*
|
||||
* @template T The expected shape of each streamed event
|
||||
* @param channel - The stream channel name (must match what the worker uses in `ctx.streams.emit`)
|
||||
* @param options - Optional configuration for the stream
|
||||
* @returns `PluginStreamResult<T>` with `events`, `lastEvent`, connection status, and `close()`
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* function ChatMessages() {
|
||||
* const { events, connected, close } = usePluginStream<ChatToken>("chat-stream");
|
||||
*
|
||||
* return (
|
||||
* <div>
|
||||
* {events.map((e, i) => <span key={i}>{e.text}</span>)}
|
||||
* {connected && <span className="pulse" />}
|
||||
* <button onClick={close}>Stop</button>
|
||||
* </div>
|
||||
* );
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @see PLUGIN_SPEC.md §19.8 — Real-Time Streaming
|
||||
*/
|
||||
export function usePluginStream<T = unknown>(
|
||||
channel: string,
|
||||
options?: { companyId?: string },
|
||||
): PluginStreamResult<T> {
|
||||
const impl = getSdkUiRuntimeValue<
|
||||
(nextChannel: string, nextOptions?: { companyId?: string }) => PluginStreamResult<T>
|
||||
>("usePluginStream");
|
||||
return impl(channel, options);
|
||||
}
|
||||
125
packages/plugins/sdk/src/ui/index.ts
Normal file
125
packages/plugins/sdk/src/ui/index.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* `@paperclipai/plugin-sdk/ui` — Paperclip plugin UI SDK.
|
||||
*
|
||||
* Import this subpath from plugin UI bundles (React components that run in
|
||||
* the host frontend). Do **not** import this from plugin worker code.
|
||||
*
|
||||
* The worker-side SDK is available from `@paperclipai/plugin-sdk` (root).
|
||||
*
|
||||
* @see PLUGIN_SPEC.md §19.0.1 — Plugin UI SDK
|
||||
* @see PLUGIN_SPEC.md §29.2 — SDK Versioning
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* // Plugin UI bundle entry (dist/ui/index.tsx)
|
||||
* import {
|
||||
* usePluginData,
|
||||
* usePluginAction,
|
||||
* useHostContext,
|
||||
* MetricCard,
|
||||
* StatusBadge,
|
||||
* Spinner,
|
||||
* } from "@paperclipai/plugin-sdk/ui";
|
||||
* import type { PluginWidgetProps } from "@paperclipai/plugin-sdk/ui";
|
||||
*
|
||||
* export function DashboardWidget({ context }: PluginWidgetProps) {
|
||||
* const { data, loading, error } = usePluginData("sync-health", {
|
||||
* companyId: context.companyId,
|
||||
* });
|
||||
* const resync = usePluginAction("resync");
|
||||
*
|
||||
* if (loading) return <Spinner />;
|
||||
* if (error) return <div>Error: {error.message}</div>;
|
||||
*
|
||||
* return (
|
||||
* <div>
|
||||
* <MetricCard label="Synced Issues" value={data!.syncedCount} />
|
||||
* <button onClick={() => resync({ companyId: context.companyId })}>
|
||||
* Resync Now
|
||||
* </button>
|
||||
* </div>
|
||||
* );
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
|
||||
/**
|
||||
* Bridge hooks for plugin UI components to communicate with the plugin worker.
|
||||
*
|
||||
* - `usePluginData(key, params)` — fetch data from the worker's `getData` handler
|
||||
* - `usePluginAction(key)` — get a callable that invokes the worker's `performAction` handler
|
||||
* - `useHostContext()` — read the current active company, project, entity, and user IDs
|
||||
* - `usePluginStream(channel)` — subscribe to real-time SSE events from the worker
|
||||
*/
|
||||
export {
|
||||
usePluginData,
|
||||
usePluginAction,
|
||||
useHostContext,
|
||||
usePluginStream,
|
||||
} from "./hooks.js";
|
||||
|
||||
// Bridge error and host context types
|
||||
export type {
|
||||
PluginBridgeError,
|
||||
PluginBridgeErrorCode,
|
||||
PluginHostContext,
|
||||
PluginModalBoundsRequest,
|
||||
PluginRenderCloseEvent,
|
||||
PluginRenderCloseHandler,
|
||||
PluginRenderCloseLifecycle,
|
||||
PluginRenderEnvironmentContext,
|
||||
PluginLauncherBounds,
|
||||
PluginLauncherRenderEnvironment,
|
||||
PluginDataResult,
|
||||
PluginActionFn,
|
||||
PluginStreamResult,
|
||||
} from "./types.js";
|
||||
|
||||
// Slot component prop interfaces
|
||||
export type {
|
||||
PluginPageProps,
|
||||
PluginWidgetProps,
|
||||
PluginDetailTabProps,
|
||||
PluginSidebarProps,
|
||||
PluginProjectSidebarItemProps,
|
||||
PluginCommentAnnotationProps,
|
||||
PluginCommentContextMenuItemProps,
|
||||
PluginSettingsPageProps,
|
||||
} from "./types.js";
|
||||
|
||||
// Shared UI components
|
||||
export {
|
||||
MetricCard,
|
||||
StatusBadge,
|
||||
DataTable,
|
||||
TimeseriesChart,
|
||||
MarkdownBlock,
|
||||
KeyValueList,
|
||||
ActionBar,
|
||||
LogView,
|
||||
JsonTree,
|
||||
Spinner,
|
||||
ErrorBoundary,
|
||||
} from "./components.js";
|
||||
|
||||
// Shared component prop types (for plugin authors who need to extend them)
|
||||
export type {
|
||||
MetricCardProps,
|
||||
MetricTrend,
|
||||
StatusBadgeProps,
|
||||
StatusBadgeVariant,
|
||||
DataTableProps,
|
||||
DataTableColumn,
|
||||
TimeseriesChartProps,
|
||||
TimeseriesDataPoint,
|
||||
MarkdownBlockProps,
|
||||
KeyValueListProps,
|
||||
KeyValuePair,
|
||||
ActionBarProps,
|
||||
ActionBarItem,
|
||||
LogViewProps,
|
||||
LogViewEntry,
|
||||
JsonTreeProps,
|
||||
SpinnerProps,
|
||||
ErrorBoundaryProps,
|
||||
} from "./components.js";
|
||||
51
packages/plugins/sdk/src/ui/runtime.ts
Normal file
51
packages/plugins/sdk/src/ui/runtime.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
type PluginBridgeRegistry = {
|
||||
react?: {
|
||||
createElement?: (type: unknown, props?: Record<string, unknown> | null) => unknown;
|
||||
} | null;
|
||||
sdkUi?: Record<string, unknown> | null;
|
||||
};
|
||||
|
||||
type GlobalBridge = typeof globalThis & {
|
||||
__paperclipPluginBridge__?: PluginBridgeRegistry;
|
||||
};
|
||||
|
||||
function getBridgeRegistry(): PluginBridgeRegistry | undefined {
|
||||
return (globalThis as GlobalBridge).__paperclipPluginBridge__;
|
||||
}
|
||||
|
||||
function missingBridgeValueError(name: string): Error {
|
||||
return new Error(
|
||||
`Paperclip plugin UI runtime is not initialized for "${name}". ` +
|
||||
'Ensure the host loaded the plugin bridge before rendering this UI module.',
|
||||
);
|
||||
}
|
||||
|
||||
export function getSdkUiRuntimeValue<T>(name: string): T {
|
||||
const value = getBridgeRegistry()?.sdkUi?.[name];
|
||||
if (value === undefined) {
|
||||
throw missingBridgeValueError(name);
|
||||
}
|
||||
return value as T;
|
||||
}
|
||||
|
||||
export function renderSdkUiComponent<TProps>(
|
||||
name: string,
|
||||
props: TProps,
|
||||
): unknown {
|
||||
const registry = getBridgeRegistry();
|
||||
const component = registry?.sdkUi?.[name];
|
||||
if (component === undefined) {
|
||||
throw missingBridgeValueError(name);
|
||||
}
|
||||
|
||||
const createElement = registry?.react?.createElement;
|
||||
if (typeof createElement === "function") {
|
||||
return createElement(component, props as Record<string, unknown>);
|
||||
}
|
||||
|
||||
if (typeof component === "function") {
|
||||
return component(props);
|
||||
}
|
||||
|
||||
throw new Error(`Paperclip plugin UI component "${name}" is not callable`);
|
||||
}
|
||||
358
packages/plugins/sdk/src/ui/types.ts
Normal file
358
packages/plugins/sdk/src/ui/types.ts
Normal file
@@ -0,0 +1,358 @@
|
||||
/**
|
||||
* Paperclip plugin UI SDK — types for plugin frontend components.
|
||||
*
|
||||
* Plugin UI bundles import from `@paperclipai/plugin-sdk/ui`. This subpath
|
||||
* provides the bridge hooks, component prop interfaces, and error types that
|
||||
* plugin React components use to communicate with the host.
|
||||
*
|
||||
* Plugin UI bundles are loaded as ES modules into designated extension slots.
|
||||
* All communication with the plugin worker goes through the host bridge — plugin
|
||||
* components must NOT access host internals or call host APIs directly.
|
||||
*
|
||||
* @see PLUGIN_SPEC.md §19 — UI Extension Model
|
||||
* @see PLUGIN_SPEC.md §19.0.1 — Plugin UI SDK
|
||||
* @see PLUGIN_SPEC.md §29.2 — SDK Versioning
|
||||
*/
|
||||
|
||||
import type {
|
||||
PluginBridgeErrorCode,
|
||||
PluginLauncherBounds,
|
||||
PluginLauncherRenderEnvironment,
|
||||
} from "@paperclipai/shared";
|
||||
import type {
|
||||
PluginLauncherRenderContextSnapshot,
|
||||
PluginModalBoundsRequest,
|
||||
PluginRenderCloseEvent,
|
||||
} from "../protocol.js";
|
||||
|
||||
// Re-export PluginBridgeErrorCode for plugin UI authors
|
||||
export type {
|
||||
PluginBridgeErrorCode,
|
||||
PluginLauncherBounds,
|
||||
PluginLauncherRenderEnvironment,
|
||||
} from "@paperclipai/shared";
|
||||
export type {
|
||||
PluginLauncherRenderContextSnapshot,
|
||||
PluginModalBoundsRequest,
|
||||
PluginRenderCloseEvent,
|
||||
} from "../protocol.js";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Bridge error
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Structured error returned by the bridge when a UI → worker call fails.
|
||||
*
|
||||
* Plugin components receive this in `usePluginData()` as the `error` field
|
||||
* and may encounter it as a thrown value from `usePluginAction()`.
|
||||
*
|
||||
* Error codes:
|
||||
* - `WORKER_UNAVAILABLE` — plugin worker is not running
|
||||
* - `CAPABILITY_DENIED` — plugin lacks the required capability
|
||||
* - `WORKER_ERROR` — worker returned an error from its handler
|
||||
* - `TIMEOUT` — worker did not respond within the configured timeout
|
||||
* - `UNKNOWN` — unexpected bridge-level failure
|
||||
*
|
||||
* @see PLUGIN_SPEC.md §19.7 — Error Propagation Through The Bridge
|
||||
*/
|
||||
export interface PluginBridgeError {
|
||||
/** Machine-readable error code. */
|
||||
code: PluginBridgeErrorCode;
|
||||
/** Human-readable error message. */
|
||||
message: string;
|
||||
/**
|
||||
* Original error details from the worker, if available.
|
||||
* Only present when `code === "WORKER_ERROR"`.
|
||||
*/
|
||||
details?: unknown;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Host context available to all plugin components
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Read-only host context passed to every plugin component via `useHostContext()`.
|
||||
*
|
||||
* Plugin components use this to know which company, project, or entity is
|
||||
* currently active so they can scope their data requests accordingly.
|
||||
*
|
||||
* @see PLUGIN_SPEC.md §19 — UI Extension Model
|
||||
*/
|
||||
export interface PluginHostContext {
|
||||
/** UUID of the currently active company, if any. */
|
||||
companyId: string | null;
|
||||
/** URL prefix for the current company (e.g. `"my-company"`). */
|
||||
companyPrefix: string | null;
|
||||
/** UUID of the currently active project, if any. */
|
||||
projectId: string | null;
|
||||
/** UUID of the current entity (for detail tab contexts), if any. */
|
||||
entityId: string | null;
|
||||
/** Type of the current entity (e.g. `"issue"`, `"agent"`). */
|
||||
entityType: string | null;
|
||||
/**
|
||||
* UUID of the parent entity when rendering nested slots.
|
||||
* For `commentAnnotation` slots this is the issue ID containing the comment.
|
||||
*/
|
||||
parentEntityId?: string | null;
|
||||
/** UUID of the current authenticated user. */
|
||||
userId: string | null;
|
||||
/** Runtime metadata for the host container currently rendering this plugin UI. */
|
||||
renderEnvironment?: PluginRenderEnvironmentContext | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Async-capable callback invoked during a host-managed close lifecycle.
|
||||
*/
|
||||
export type PluginRenderCloseHandler = (
|
||||
event: PluginRenderCloseEvent,
|
||||
) => void | Promise<void>;
|
||||
|
||||
/**
|
||||
* Close lifecycle hooks available when the plugin UI is rendered inside a
|
||||
* host-managed launcher environment.
|
||||
*/
|
||||
export interface PluginRenderCloseLifecycle {
|
||||
/** Register a callback before the host closes the current environment. */
|
||||
onBeforeClose?(handler: PluginRenderCloseHandler): () => void;
|
||||
/** Register a callback after the host closes the current environment. */
|
||||
onClose?(handler: PluginRenderCloseHandler): () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runtime information about the host container currently rendering a plugin UI.
|
||||
*/
|
||||
export interface PluginRenderEnvironmentContext
|
||||
extends PluginLauncherRenderContextSnapshot {
|
||||
/** Optional host callback for requesting new bounds while a modal is open. */
|
||||
requestModalBounds?(request: PluginModalBoundsRequest): Promise<void>;
|
||||
/** Optional close lifecycle callbacks for host-managed overlays. */
|
||||
closeLifecycle?: PluginRenderCloseLifecycle | null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Slot component prop interfaces
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Props passed to a plugin page component.
|
||||
*
|
||||
* A page is a full-page extension at `/plugins/:pluginId` or `/:company/plugins/:pluginId`.
|
||||
*
|
||||
* @see PLUGIN_SPEC.md §19.1 — Global Operator Routes
|
||||
* @see PLUGIN_SPEC.md §19.2 — Company-Context Routes
|
||||
*/
|
||||
export interface PluginPageProps {
|
||||
/** The current host context. */
|
||||
context: PluginHostContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Props passed to a plugin dashboard widget component.
|
||||
*
|
||||
* A dashboard widget is rendered as a card or section on the main dashboard.
|
||||
*
|
||||
* @see PLUGIN_SPEC.md §19.4 — Dashboard Widgets
|
||||
*/
|
||||
export interface PluginWidgetProps {
|
||||
/** The current host context. */
|
||||
context: PluginHostContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Props passed to a plugin detail tab component.
|
||||
*
|
||||
* A detail tab is rendered as an additional tab on a project, issue, agent,
|
||||
* goal, or run detail page.
|
||||
*
|
||||
* @see PLUGIN_SPEC.md §19.3 — Detail Tabs
|
||||
*/
|
||||
export interface PluginDetailTabProps {
|
||||
/** The current host context, always including `entityId` and `entityType`. */
|
||||
context: PluginHostContext & {
|
||||
entityId: string;
|
||||
entityType: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Props passed to a plugin sidebar component.
|
||||
*
|
||||
* A sidebar entry adds a link or section to the application sidebar.
|
||||
*
|
||||
* @see PLUGIN_SPEC.md §19.5 — Sidebar Entries
|
||||
*/
|
||||
export interface PluginSidebarProps {
|
||||
/** The current host context. */
|
||||
context: PluginHostContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Props passed to a plugin project sidebar item component.
|
||||
*
|
||||
* A project sidebar item is rendered **once per project** under that project's
|
||||
* row in the sidebar Projects list. The host passes the current project's id
|
||||
* in `context.entityId` and `context.entityType` is `"project"`.
|
||||
*
|
||||
* Use this slot to add a link (e.g. "Files", "Linear Sync") that navigates to
|
||||
* the project detail with a plugin tab selected: `/projects/:projectRef?tab=plugin:key:slotId`.
|
||||
*
|
||||
* @see PLUGIN_SPEC.md §19.5.1 — Project sidebar items
|
||||
*/
|
||||
export interface PluginProjectSidebarItemProps {
|
||||
/** Host context plus entityId (project id) and entityType "project". */
|
||||
context: PluginHostContext & {
|
||||
entityId: string;
|
||||
entityType: "project";
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Props passed to a plugin comment annotation component.
|
||||
*
|
||||
* A comment annotation is rendered below each individual comment in the
|
||||
* issue detail timeline. The host passes the comment ID as `entityId`
|
||||
* and `"comment"` as `entityType`, plus the parent issue ID as
|
||||
* `parentEntityId` so the plugin can scope data fetches to both.
|
||||
*
|
||||
* Use this slot to augment comments with parsed file links, sentiment
|
||||
* badges, inline actions, or any per-comment metadata.
|
||||
*
|
||||
* @see PLUGIN_SPEC.md §19.6 — Comment Annotations
|
||||
*/
|
||||
export interface PluginCommentAnnotationProps {
|
||||
/** Host context with comment and parent issue identifiers. */
|
||||
context: PluginHostContext & {
|
||||
/** UUID of the comment being annotated. */
|
||||
entityId: string;
|
||||
/** Always `"comment"` for comment annotation slots. */
|
||||
entityType: "comment";
|
||||
/** UUID of the parent issue containing this comment. */
|
||||
parentEntityId: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Props passed to a plugin comment context menu item component.
|
||||
*
|
||||
* A comment context menu item is rendered in a "more" dropdown menu on
|
||||
* each comment in the issue detail timeline. The host passes the comment
|
||||
* ID as `entityId` and `"comment"` as `entityType`, plus the parent
|
||||
* issue ID as `parentEntityId`.
|
||||
*
|
||||
* Use this slot to add per-comment actions such as "Create sub-issue from
|
||||
* comment", "Translate", "Flag for review", or any custom plugin action.
|
||||
*
|
||||
* @see PLUGIN_SPEC.md §19.7 — Comment Context Menu Items
|
||||
*/
|
||||
export interface PluginCommentContextMenuItemProps {
|
||||
/** Host context with comment and parent issue identifiers. */
|
||||
context: PluginHostContext & {
|
||||
/** UUID of the comment this menu item acts on. */
|
||||
entityId: string;
|
||||
/** Always `"comment"` for comment context menu item slots. */
|
||||
entityType: "comment";
|
||||
/** UUID of the parent issue containing this comment. */
|
||||
parentEntityId: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Props passed to a plugin settings page component.
|
||||
*
|
||||
* Overrides the auto-generated JSON Schema form when the plugin declares
|
||||
* a `settingsPage` UI slot. The component is responsible for reading and
|
||||
* writing config through the bridge.
|
||||
*
|
||||
* @see PLUGIN_SPEC.md §19.8 — Plugin Settings UI
|
||||
*/
|
||||
export interface PluginSettingsPageProps {
|
||||
/** The current host context. */
|
||||
context: PluginHostContext;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// usePluginData hook return type
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Return value of `usePluginData(key, params)`.
|
||||
*
|
||||
* Mirrors a standard async data-fetching hook pattern:
|
||||
* exactly one of `data` or `error` is non-null at any time (unless `loading`).
|
||||
*
|
||||
* @template T The type of the data returned by the worker handler
|
||||
*
|
||||
* @see PLUGIN_SPEC.md §19.7 — Error Propagation Through The Bridge
|
||||
*/
|
||||
export interface PluginDataResult<T = unknown> {
|
||||
/** The data returned by the worker's `getData` handler. `null` while loading or on error. */
|
||||
data: T | null;
|
||||
/** `true` while the initial request or a refresh is in flight. */
|
||||
loading: boolean;
|
||||
/** Bridge error if the request failed. `null` on success or while loading. */
|
||||
error: PluginBridgeError | null;
|
||||
/**
|
||||
* Manually trigger a data refresh.
|
||||
* Useful for poll-based updates or post-action refreshes.
|
||||
*/
|
||||
refresh(): void;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// usePluginAction hook return type
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// usePluginStream hook return type
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Return value of `usePluginStream<T>(channel)`.
|
||||
*
|
||||
* Provides a growing array of events pushed from the plugin worker via SSE,
|
||||
* plus connection status metadata.
|
||||
*
|
||||
* @template T The type of each event emitted by the worker
|
||||
*
|
||||
* @see PLUGIN_SPEC.md §19.8 — Real-Time Streaming
|
||||
*/
|
||||
export interface PluginStreamResult<T = unknown> {
|
||||
/** All events received so far, in arrival order. */
|
||||
events: T[];
|
||||
/** The most recently received event, or `null` if none yet. */
|
||||
lastEvent: T | null;
|
||||
/** `true` while the SSE connection is being established. */
|
||||
connecting: boolean;
|
||||
/** `true` once the SSE connection is open and receiving events. */
|
||||
connected: boolean;
|
||||
/** Error if the SSE connection failed or was interrupted. `null` otherwise. */
|
||||
error: Error | null;
|
||||
/** Close the SSE connection and stop receiving events. */
|
||||
close(): void;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// usePluginAction hook return type
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Return value of `usePluginAction(key)`.
|
||||
*
|
||||
* Returns an async function that, when called, sends an action request
|
||||
* to the worker's `performAction` handler and returns the result.
|
||||
*
|
||||
* On failure, the async function throws a `PluginBridgeError`.
|
||||
*
|
||||
* @see PLUGIN_SPEC.md §19.7 — Error Propagation Through The Bridge
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const resync = usePluginAction("resync");
|
||||
* <button onClick={() => resync({ companyId }).catch(err => console.error(err))}>
|
||||
* Resync Now
|
||||
* </button>
|
||||
* ```
|
||||
*/
|
||||
export type PluginActionFn = (params?: Record<string, unknown>) => Promise<unknown>;
|
||||
Reference in New Issue
Block a user