Clarify plugin authoring and external dev workflow
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
Official TypeScript SDK for Paperclip plugin authors.
|
||||
|
||||
- **Worker SDK:** `@paperclipai/plugin-sdk` — `definePlugin`, context, lifecycle
|
||||
- **UI SDK:** `@paperclipai/plugin-sdk/ui` — React hooks, components, slot props
|
||||
- **UI SDK:** `@paperclipai/plugin-sdk/ui` — React hooks and slot props
|
||||
- **Testing:** `@paperclipai/plugin-sdk/testing` — in-memory host harness
|
||||
- **Bundlers:** `@paperclipai/plugin-sdk/bundlers` — esbuild/rollup presets
|
||||
- **Dev server:** `@paperclipai/plugin-sdk/dev-server` — static UI server + SSE reload
|
||||
@@ -15,10 +15,9 @@ Reference: `doc/plugins/PLUGIN_SPEC.md`
|
||||
| Import | Purpose |
|
||||
|--------|--------|
|
||||
| `@paperclipai/plugin-sdk` | Worker entry: `definePlugin`, `runWorker`, context types, protocol helpers |
|
||||
| `@paperclipai/plugin-sdk/ui` | UI entry: `usePluginData`, `usePluginAction`, `usePluginStream`, `useHostContext`, shared components |
|
||||
| `@paperclipai/plugin-sdk/ui` | UI entry: `usePluginData`, `usePluginAction`, `usePluginStream`, `useHostContext`, slot prop types |
|
||||
| `@paperclipai/plugin-sdk/ui/hooks` | Hooks only |
|
||||
| `@paperclipai/plugin-sdk/ui/types` | UI types and slot prop interfaces |
|
||||
| `@paperclipai/plugin-sdk/ui/components` | `MetricCard`, `StatusBadge`, `Spinner`, `ErrorBoundary`, etc. |
|
||||
| `@paperclipai/plugin-sdk/testing` | `createTestHarness` for unit/integration tests |
|
||||
| `@paperclipai/plugin-sdk/bundlers` | `createPluginBundlerPresets` for worker/manifest/ui builds |
|
||||
| `@paperclipai/plugin-sdk/dev-server` | `startPluginDevServer`, `getUiBuildSnapshot` |
|
||||
@@ -42,10 +41,14 @@ pnpm add @paperclipai/plugin-sdk
|
||||
|
||||
The SDK is stable enough for local development and first-party examples, but the runtime deployment model is still early.
|
||||
|
||||
- Plugin workers and plugin UI should both be treated as trusted code today.
|
||||
- Plugin UI bundles run as same-origin JavaScript inside the main Paperclip app. They can call ordinary Paperclip HTTP APIs with the board session, so manifest capabilities are not a frontend sandbox.
|
||||
- Local-path installs and the repo example plugins are development workflows. They assume the plugin source checkout exists on disk.
|
||||
- For deployed plugins, publish an npm package and install that package into the Paperclip instance at runtime.
|
||||
- The current host runtime expects a writable filesystem, `npm` available at runtime, and network access to the package registry used for plugin installation.
|
||||
- Dynamic plugin install is currently best suited to single-node persistent deployments. Multi-instance cloud deployments still need a shared artifact/distribution model before runtime installs are reliable across nodes.
|
||||
- The host does not currently ship a real shared React component kit for plugins. Build your plugin UI with ordinary React components and CSS.
|
||||
- `ctx.assets` is not part of the supported runtime in this build. Do not depend on asset upload/read APIs yet.
|
||||
|
||||
If you are authoring a plugin for others to deploy, treat npm-packaged installation as the supported path and treat repo-local example installs as a development convenience.
|
||||
|
||||
@@ -97,7 +100,7 @@ runWorker(plugin, import.meta.url);
|
||||
| `onValidateConfig?(config)` | Optional. Return `{ ok, warnings?, errors? }` for settings UI / Test Connection. |
|
||||
| `onWebhook?(input)` | Optional. Handle `POST /api/plugins/:pluginId/webhooks/:endpointKey`; required if webhooks declared. |
|
||||
|
||||
**Context (`ctx`) in setup:** `config`, `events`, `jobs`, `launchers`, `http`, `secrets`, `assets`, `activity`, `state`, `entities`, `projects`, `companies`, `issues`, `agents`, `goals`, `data`, `actions`, `streams`, `tools`, `metrics`, `logger`, `manifest`. All host APIs are capability-gated; declare capabilities in the manifest.
|
||||
**Context (`ctx`) in setup:** `config`, `events`, `jobs`, `launchers`, `http`, `secrets`, `activity`, `state`, `entities`, `projects`, `companies`, `issues`, `agents`, `goals`, `data`, `actions`, `streams`, `tools`, `metrics`, `logger`, `manifest`. Worker-side host APIs are capability-gated; declare capabilities in the manifest.
|
||||
|
||||
**Agents:** `ctx.agents.invoke(agentId, companyId, opts)` for one-shot invocation. `ctx.agents.sessions` for two-way chat: `create`, `list`, `sendMessage` (with streaming `onEvent` callback), `close`. See the [Plugin Authoring Guide](../../doc/plugins/PLUGIN_AUTHORING_GUIDE.md#agent-sessions-two-way-chat) for details.
|
||||
|
||||
@@ -126,7 +129,7 @@ Subscribe in `setup` with `ctx.events.on(name, handler)` or `ctx.events.on(name,
|
||||
|
||||
**Filter (optional):** Pass a second argument to `on()`: `{ projectId?, companyId?, agentId? }` so the host only delivers matching events.
|
||||
|
||||
**Company-scoped delivery:** Events with a `companyId` are only delivered to plugins that are enabled for that company. If a company has disabled a plugin via settings, that plugin's handlers will not receive events belonging to that company. Events without a `companyId` are delivered to all subscribers.
|
||||
**Company context:** Events still carry `companyId` for company-scoped data, but plugin installation and activation are instance-wide in the current runtime.
|
||||
|
||||
## Scheduled (recurring) jobs
|
||||
|
||||
@@ -302,8 +305,6 @@ Declare in `manifest.capabilities`. Grouped by scope:
|
||||
| | `issues.create` |
|
||||
| | `issues.update` |
|
||||
| | `issue.comments.create` |
|
||||
| | `assets.write` |
|
||||
| | `assets.read` |
|
||||
| | `activity.log.write` |
|
||||
| | `metrics.write` |
|
||||
| **Instance** | `instance.settings.register` |
|
||||
@@ -333,14 +334,15 @@ Full list in code: import `PLUGIN_CAPABILITIES` from `@paperclipai/plugin-sdk`.
|
||||
## UI quick start
|
||||
|
||||
```tsx
|
||||
import { usePluginData, usePluginAction, MetricCard } from "@paperclipai/plugin-sdk/ui";
|
||||
import { usePluginData, usePluginAction } from "@paperclipai/plugin-sdk/ui";
|
||||
|
||||
export function DashboardWidget() {
|
||||
const { data } = usePluginData<{ status: string }>("health");
|
||||
const ping = usePluginAction("ping");
|
||||
return (
|
||||
<div>
|
||||
<MetricCard label="Health" value={data?.status ?? "unknown"} />
|
||||
<div style={{ display: "grid", gap: 8 }}>
|
||||
<strong>Health</strong>
|
||||
<div>{data?.status ?? "unknown"}</div>
|
||||
<button onClick={() => void ping()}>Ping</button>
|
||||
</div>
|
||||
);
|
||||
@@ -354,7 +356,7 @@ export function DashboardWidget() {
|
||||
Fetches data from the worker's registered `getData` handler. Re-fetches when `params` changes. Returns `{ data, loading, error, refresh }`.
|
||||
|
||||
```tsx
|
||||
import { usePluginData, Spinner, StatusBadge } from "@paperclipai/plugin-sdk/ui";
|
||||
import { usePluginData } from "@paperclipai/plugin-sdk/ui";
|
||||
|
||||
interface SyncStatus {
|
||||
lastSyncAt: string;
|
||||
@@ -367,12 +369,12 @@ export function SyncStatusWidget({ context }: PluginWidgetProps) {
|
||||
companyId: context.companyId,
|
||||
});
|
||||
|
||||
if (loading) return <Spinner />;
|
||||
if (error) return <StatusBadge label={error.message} status="error" />;
|
||||
if (loading) return <div>Loading…</div>;
|
||||
if (error) return <div>Error: {error.message}</div>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<StatusBadge label={data!.healthy ? "Healthy" : "Unhealthy"} status={data!.healthy ? "ok" : "error"} />
|
||||
<p>Status: {data!.healthy ? "Healthy" : "Unhealthy"}</p>
|
||||
<p>Synced {data!.syncedCount} items</p>
|
||||
<p>Last sync: {data!.lastSyncAt}</p>
|
||||
<button onClick={refresh}>Refresh</button>
|
||||
@@ -465,100 +467,9 @@ export function ChatMessages({ context }: PluginWidgetProps) {
|
||||
|
||||
The SSE connection targets `GET /api/plugins/:pluginId/bridge/stream/:channel?companyId=...`. The host bridge manages the EventSource lifecycle; `close()` terminates the connection.
|
||||
|
||||
### Shared components reference
|
||||
### UI authoring note
|
||||
|
||||
All components are provided by the host at runtime and match the host design tokens. Import from `@paperclipai/plugin-sdk/ui` or `@paperclipai/plugin-sdk/ui/components`.
|
||||
|
||||
#### `MetricCard`
|
||||
|
||||
Displays a single metric value with optional trend and sparkline.
|
||||
|
||||
```tsx
|
||||
<MetricCard label="Issues Synced" value={142} unit="issues" trend={{ direction: "up", percentage: 12 }} />
|
||||
<MetricCard label="API Latency" value="45ms" sparkline={[52, 48, 45, 47, 45]} />
|
||||
```
|
||||
|
||||
#### `StatusBadge`
|
||||
|
||||
Inline status indicator with semantic color.
|
||||
|
||||
```tsx
|
||||
<StatusBadge label="Connected" status="ok" />
|
||||
<StatusBadge label="Rate Limited" status="warning" />
|
||||
<StatusBadge label="Auth Failed" status="error" />
|
||||
```
|
||||
|
||||
#### `DataTable`
|
||||
|
||||
Sortable, paginated table.
|
||||
|
||||
```tsx
|
||||
<DataTable
|
||||
columns={[
|
||||
{ key: "name", header: "Name", sortable: true },
|
||||
{ key: "status", header: "Status", width: "100px" },
|
||||
{ key: "updatedAt", header: "Updated", render: (v) => new Date(v as string).toLocaleDateString() },
|
||||
]}
|
||||
rows={issues}
|
||||
totalCount={totalCount}
|
||||
page={page}
|
||||
pageSize={25}
|
||||
onPageChange={setPage}
|
||||
onSort={(key, dir) => setSortBy({ key, dir })}
|
||||
/>
|
||||
```
|
||||
|
||||
#### `TimeseriesChart`
|
||||
|
||||
Line or bar chart for time-series data.
|
||||
|
||||
```tsx
|
||||
<TimeseriesChart
|
||||
title="Sync Frequency"
|
||||
data={[
|
||||
{ timestamp: "2026-03-01T00:00:00Z", value: 24 },
|
||||
{ timestamp: "2026-03-02T00:00:00Z", value: 31 },
|
||||
{ timestamp: "2026-03-03T00:00:00Z", value: 28 },
|
||||
]}
|
||||
type="bar"
|
||||
yLabel="Syncs"
|
||||
height={250}
|
||||
/>
|
||||
```
|
||||
|
||||
#### `ActionBar`
|
||||
|
||||
Row of action buttons wired to the plugin bridge.
|
||||
|
||||
```tsx
|
||||
<ActionBar
|
||||
actions={[
|
||||
{ label: "Sync Now", actionKey: "sync", variant: "primary" },
|
||||
{ label: "Clear Cache", actionKey: "clear-cache", confirm: true, confirmMessage: "Delete all cached data?" },
|
||||
]}
|
||||
onSuccess={(key) => data.refresh()}
|
||||
onError={(key, err) => console.error(key, err)}
|
||||
/>
|
||||
```
|
||||
|
||||
#### `LogView`, `JsonTree`, `KeyValueList`, `MarkdownBlock`
|
||||
|
||||
```tsx
|
||||
<LogView entries={logEntries} maxHeight="300px" autoScroll />
|
||||
<JsonTree data={debugPayload} defaultExpandDepth={3} />
|
||||
<KeyValueList pairs={[{ label: "Plugin ID", value: pluginId }, { label: "Version", value: "1.2.0" }]} />
|
||||
<MarkdownBlock content="**Bold** text and `code` blocks are supported." />
|
||||
```
|
||||
|
||||
#### `Spinner`, `ErrorBoundary`
|
||||
|
||||
```tsx
|
||||
<Spinner size="lg" label="Loading plugin data..." />
|
||||
|
||||
<ErrorBoundary fallback={<p>Something went wrong.</p>} onError={(err) => console.error(err)}>
|
||||
<MyPluginContent />
|
||||
</ErrorBoundary>
|
||||
```
|
||||
The current host does **not** provide a real shared component library to plugins yet. Use normal React components, your own CSS, or your own small design primitives inside the plugin package.
|
||||
|
||||
### Slot component props
|
||||
|
||||
@@ -579,7 +490,7 @@ Example detail tab with entity context:
|
||||
|
||||
```tsx
|
||||
import type { PluginDetailTabProps } from "@paperclipai/plugin-sdk/ui";
|
||||
import { usePluginData, KeyValueList, Spinner } from "@paperclipai/plugin-sdk/ui";
|
||||
import { usePluginData } from "@paperclipai/plugin-sdk/ui";
|
||||
|
||||
export function AgentMetricsTab({ context }: PluginDetailTabProps) {
|
||||
const { data, loading } = usePluginData<Record<string, string>>("agent-metrics", {
|
||||
@@ -587,13 +498,18 @@ export function AgentMetricsTab({ context }: PluginDetailTabProps) {
|
||||
companyId: context.companyId,
|
||||
});
|
||||
|
||||
if (loading) return <Spinner />;
|
||||
if (loading) return <div>Loading…</div>;
|
||||
if (!data) return <p>No metrics available.</p>;
|
||||
|
||||
return (
|
||||
<KeyValueList
|
||||
pairs={Object.entries(data).map(([label, value]) => ({ label, value }))}
|
||||
/>
|
||||
<dl>
|
||||
{Object.entries(data).map(([label, value]) => (
|
||||
<div key={label}>
|
||||
<dt>{label}</dt>
|
||||
<dd>{value}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
);
|
||||
}
|
||||
```
|
||||
@@ -702,8 +618,6 @@ For short-lived actions, mount a `toolbarButton` and open a plugin-owned modal i
|
||||
```tsx
|
||||
import { useState } from "react";
|
||||
import {
|
||||
ErrorBoundary,
|
||||
Spinner,
|
||||
useHostContext,
|
||||
usePluginAction,
|
||||
} from "@paperclipai/plugin-sdk/ui";
|
||||
@@ -730,7 +644,7 @@ export function SyncToolbarButton() {
|
||||
}
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<>
|
||||
<button type="button" onClick={() => setOpen(true)}>
|
||||
Sync
|
||||
</button>
|
||||
@@ -757,13 +671,13 @@ export function SyncToolbarButton() {
|
||||
Cancel
|
||||
</button>
|
||||
<button type="button" onClick={() => void confirm()} disabled={submitting}>
|
||||
{submitting ? <Spinner size="sm" /> : "Run sync"}
|
||||
{submitting ? "Running…" : "Run sync"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</ErrorBoundary>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -28,10 +28,6 @@
|
||||
"types": "./dist/ui/types.d.ts",
|
||||
"import": "./dist/ui/types.js"
|
||||
},
|
||||
"./ui/components": {
|
||||
"types": "./dist/ui/components.d.ts",
|
||||
"import": "./dist/ui/components.js"
|
||||
},
|
||||
"./testing": {
|
||||
"types": "./dist/testing.d.ts",
|
||||
"import": "./dist/testing.js"
|
||||
@@ -75,10 +71,6 @@
|
||||
"types": "./dist/ui/types.d.ts",
|
||||
"import": "./dist/ui/types.js"
|
||||
},
|
||||
"./ui/components": {
|
||||
"types": "./dist/ui/components.d.ts",
|
||||
"import": "./dist/ui/components.js"
|
||||
},
|
||||
"./testing": {
|
||||
"types": "./dist/testing.d.ts",
|
||||
"import": "./dist/testing.js"
|
||||
|
||||
@@ -62,7 +62,6 @@ export function createPluginBundlerPresets(input: PluginBundlerPresetInput = {})
|
||||
const uiExternal = [
|
||||
"@paperclipai/plugin-sdk/ui",
|
||||
"@paperclipai/plugin-sdk/ui/hooks",
|
||||
"@paperclipai/plugin-sdk/ui/components",
|
||||
"react",
|
||||
"react-dom",
|
||||
"react/jsx-runtime",
|
||||
@@ -84,7 +83,7 @@ export function createPluginBundlerPresets(input: PluginBundlerPresetInput = {})
|
||||
target: "node20",
|
||||
sourcemap,
|
||||
minify,
|
||||
external: ["@paperclipai/plugin-sdk", "@paperclipai/plugin-sdk/ui", "react", "react-dom"],
|
||||
external: ["react", "react-dom"],
|
||||
};
|
||||
|
||||
const esbuildManifest: EsbuildLikeOptions = {
|
||||
@@ -119,7 +118,7 @@ export function createPluginBundlerPresets(input: PluginBundlerPresetInput = {})
|
||||
sourcemap,
|
||||
entryFileNames: "worker.js",
|
||||
},
|
||||
external: ["@paperclipai/plugin-sdk", "react", "react-dom"],
|
||||
external: ["react", "react-dom"],
|
||||
};
|
||||
|
||||
const rollupManifest: RollupLikeConfig = {
|
||||
|
||||
@@ -118,12 +118,6 @@ export interface HostServices {
|
||||
resolve(params: WorkerToHostMethods["secrets.resolve"][0]): Promise<string>;
|
||||
};
|
||||
|
||||
/** Provides `assets.upload`, `assets.getUrl`. */
|
||||
assets: {
|
||||
upload(params: WorkerToHostMethods["assets.upload"][0]): Promise<WorkerToHostMethods["assets.upload"][1]>;
|
||||
getUrl(params: WorkerToHostMethods["assets.getUrl"][0]): Promise<string>;
|
||||
};
|
||||
|
||||
/** Provides `activity.log`. */
|
||||
activity: {
|
||||
log(params: {
|
||||
@@ -274,10 +268,6 @@ const METHOD_CAPABILITY_MAP: Record<WorkerToHostMethodName, PluginCapability | n
|
||||
// Secrets
|
||||
"secrets.resolve": "secrets.read-ref",
|
||||
|
||||
// Assets
|
||||
"assets.upload": "assets.write",
|
||||
"assets.getUrl": "assets.read",
|
||||
|
||||
// Activity
|
||||
"activity.log": "activity.log.write",
|
||||
|
||||
@@ -428,14 +418,6 @@ export function createHostClientHandlers(
|
||||
return services.secrets.resolve(params);
|
||||
}),
|
||||
|
||||
// Assets
|
||||
"assets.upload": gated("assets.upload", async (params) => {
|
||||
return services.assets.upload(params);
|
||||
}),
|
||||
"assets.getUrl": gated("assets.getUrl", async (params) => {
|
||||
return services.assets.getUrl(params);
|
||||
}),
|
||||
|
||||
// Activity
|
||||
"activity.log": gated("activity.log", async (params) => {
|
||||
return services.activity.log(params);
|
||||
|
||||
@@ -164,7 +164,6 @@ export type {
|
||||
PluginLaunchersClient,
|
||||
PluginHttpClient,
|
||||
PluginSecretsClient,
|
||||
PluginAssetsClient,
|
||||
PluginActivityClient,
|
||||
PluginActivityLogEntry,
|
||||
PluginStateClient,
|
||||
|
||||
@@ -495,16 +495,6 @@ export interface WorkerToHostMethods {
|
||||
result: string,
|
||||
];
|
||||
|
||||
// Assets
|
||||
"assets.upload": [
|
||||
params: { filename: string; contentType: string; data: string },
|
||||
result: { assetId: string; url: string },
|
||||
];
|
||||
"assets.getUrl": [
|
||||
params: { assetId: string },
|
||||
result: string,
|
||||
];
|
||||
|
||||
// Activity
|
||||
"activity.log": [
|
||||
params: {
|
||||
|
||||
@@ -136,8 +136,6 @@ export function createTestHarness(options: TestHarnessOptions): TestHarness {
|
||||
const state = new Map<string, unknown>();
|
||||
const entities = new Map<string, PluginEntityRecord>();
|
||||
const entityExternalIndex = new Map<string, string>();
|
||||
const assets = new Map<string, { contentType: string; data: Uint8Array }>();
|
||||
|
||||
const companies = new Map<string, Company>();
|
||||
const projects = new Map<string, Project>();
|
||||
const issues = new Map<string, Issue>();
|
||||
@@ -207,19 +205,6 @@ export function createTestHarness(options: TestHarnessOptions): TestHarness {
|
||||
return `resolved:${secretRef}`;
|
||||
},
|
||||
},
|
||||
assets: {
|
||||
async upload(filename, contentType, data) {
|
||||
requireCapability(manifest, capabilitySet, "assets.write");
|
||||
const assetId = `asset_${randomUUID()}`;
|
||||
assets.set(assetId, { contentType, data: data instanceof Uint8Array ? data : new Uint8Array(data) });
|
||||
return { assetId, url: `memory://assets/${filename}` };
|
||||
},
|
||||
async getUrl(assetId) {
|
||||
requireCapability(manifest, capabilitySet, "assets.read");
|
||||
if (!assets.has(assetId)) throw new Error(`Asset not found: ${assetId}`);
|
||||
return `memory://assets/${assetId}`;
|
||||
},
|
||||
},
|
||||
activity: {
|
||||
async log(entry) {
|
||||
requireCapability(manifest, capabilitySet, "activity.log.write");
|
||||
|
||||
@@ -452,34 +452,6 @@ export interface PluginSecretsClient {
|
||||
resolve(secretRef: string): Promise<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* `ctx.assets` — read and write assets (files, images, etc.).
|
||||
*
|
||||
* `assets.read` capability required for `getUrl()`.
|
||||
* `assets.write` capability required for `upload()`.
|
||||
*
|
||||
* @see PLUGIN_SPEC.md §15.1 — Capabilities: Data Write
|
||||
*/
|
||||
export interface PluginAssetsClient {
|
||||
/**
|
||||
* Upload an asset (e.g. a screenshot or generated file).
|
||||
*
|
||||
* @param filename - Name for the asset file
|
||||
* @param contentType - MIME type
|
||||
* @param data - Raw asset data as a Buffer or Uint8Array
|
||||
* @returns The asset ID and public URL
|
||||
*/
|
||||
upload(filename: string, contentType: string, data: Buffer | Uint8Array): Promise<{ assetId: string; url: string }>;
|
||||
|
||||
/**
|
||||
* Get the public URL for an existing asset by ID.
|
||||
*
|
||||
* @param assetId - Asset identifier
|
||||
* @returns The public URL
|
||||
*/
|
||||
getUrl(assetId: string): Promise<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Input for writing a plugin activity log entry.
|
||||
*
|
||||
@@ -1069,9 +1041,6 @@ export interface PluginContext {
|
||||
/** Resolve secret references. Requires `secrets.read-ref`. */
|
||||
secrets: PluginSecretsClient;
|
||||
|
||||
/** Read and write assets. Requires `assets.read` / `assets.write`. */
|
||||
assets: PluginAssetsClient;
|
||||
|
||||
/** Write activity log entries. Requires `activity.log.write`. */
|
||||
activity: PluginActivityClient;
|
||||
|
||||
|
||||
@@ -29,9 +29,9 @@ import { getSdkUiRuntimeValue } from "./runtime.js";
|
||||
* companyId: context.companyId,
|
||||
* });
|
||||
*
|
||||
* if (loading) return <Spinner />;
|
||||
* if (loading) return <div>Loading…</div>;
|
||||
* if (error) return <div>Error: {error.message}</div>;
|
||||
* return <MetricCard label="Synced Issues" value={data!.syncedCount} />;
|
||||
* return <div>Synced Issues: {data!.syncedCount}</div>;
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
|
||||
@@ -12,14 +12,7 @@
|
||||
* @example
|
||||
* ```tsx
|
||||
* // Plugin UI bundle entry (dist/ui/index.tsx)
|
||||
* import {
|
||||
* usePluginData,
|
||||
* usePluginAction,
|
||||
* useHostContext,
|
||||
* MetricCard,
|
||||
* StatusBadge,
|
||||
* Spinner,
|
||||
* } from "@paperclipai/plugin-sdk/ui";
|
||||
* import { usePluginData, usePluginAction } from "@paperclipai/plugin-sdk/ui";
|
||||
* import type { PluginWidgetProps } from "@paperclipai/plugin-sdk/ui";
|
||||
*
|
||||
* export function DashboardWidget({ context }: PluginWidgetProps) {
|
||||
@@ -28,12 +21,13 @@
|
||||
* });
|
||||
* const resync = usePluginAction("resync");
|
||||
*
|
||||
* if (loading) return <Spinner />;
|
||||
* if (loading) return <div>Loading…</div>;
|
||||
* if (error) return <div>Error: {error.message}</div>;
|
||||
*
|
||||
* return (
|
||||
* <div>
|
||||
* <MetricCard label="Synced Issues" value={data!.syncedCount} />
|
||||
* <div style={{ display: "grid", gap: 8 }}>
|
||||
* <strong>Synced Issues</strong>
|
||||
* <div>{data!.syncedCount}</div>
|
||||
* <button onClick={() => resync({ companyId: context.companyId })}>
|
||||
* Resync Now
|
||||
* </button>
|
||||
@@ -91,40 +85,3 @@ export type {
|
||||
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";
|
||||
|
||||
@@ -456,26 +456,6 @@ export function startWorkerRpcHost(options: WorkerRpcHostOptions): WorkerRpcHost
|
||||
},
|
||||
},
|
||||
|
||||
assets: {
|
||||
async upload(
|
||||
filename: string,
|
||||
contentType: string,
|
||||
data: Buffer | Uint8Array,
|
||||
): Promise<{ assetId: string; url: string }> {
|
||||
// Base64-encode binary data for JSON serialization
|
||||
const base64 = Buffer.from(data).toString("base64");
|
||||
return callHost("assets.upload", {
|
||||
filename,
|
||||
contentType,
|
||||
data: base64,
|
||||
});
|
||||
},
|
||||
|
||||
async getUrl(assetId: string): Promise<string> {
|
||||
return callHost("assets.getUrl", { assetId });
|
||||
},
|
||||
},
|
||||
|
||||
activity: {
|
||||
async log(entry): Promise<void> {
|
||||
await callHost("activity.log", {
|
||||
|
||||
Reference in New Issue
Block a user