mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-14 01:50:39 +09:00
Expand plugin host surface (#5205)
## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies > - The plugin system is the extension boundary for optional product capabilities > - Rich plugins need more than a worker entrypoint: they need scoped database storage, local project folders, managed agents/routines, host navigation, and reusable UI components > - The LLM Wiki work exposed those missing host surfaces while keeping plugin code outside the core control plane > - This pull request expands the core plugin host, SDK, server APIs, and UI bridge so plugins can declare and use those surfaces > - The benefit is that future plugins can integrate with Paperclip through documented, validated contracts instead of bespoke server or UI imports ## What Changed - Added plugin-managed database namespaces and migration tracking, including Drizzle schema/migration files and SQL validation for namespace isolation. - Added server support for plugin local folders, managed agents, managed routines, scoped plugin APIs, and plugin operation visibility. - Expanded shared plugin manifest/types/validators and SDK host/testing/UI exports for richer plugin surfaces. - Added reusable UI pieces for file trees, managed routines, resizable sidebars, route sidebars, and plugin bridge initialization. - Updated plugin docs and example plugins to use the expanded host and SDK surface. ## Verification - `pnpm install --frozen-lockfile` - `pnpm run preflight:workspace-links && pnpm exec vitest run packages/shared/src/validators/plugin.test.ts server/src/__tests__/plugin-database.test.ts server/src/__tests__/plugin-local-folders.test.ts server/src/__tests__/plugin-managed-agents.test.ts server/src/__tests__/plugin-managed-routines.test.ts server/src/__tests__/plugin-orchestration-apis.test.ts ui/src/api/plugins.test.ts ui/src/components/FileTree.test.tsx ui/src/components/ResizableSidebarPane.test.tsx ui/src/pages/PluginPage.test.tsx ui/src/plugins/bridge.test.ts` passed: 11 files, 67 tests. - Confirmed this PR changes 89 files and does not include `pnpm-lock.yaml` or `.github/workflows/*`. ## Risks - Medium: this expands plugin host contracts across db/shared/server/ui and includes a new core migration (`0076_useful_elektra.sql`). - The plugin database namespace validator is intentionally restrictive; plugin authors may need follow-up affordances for SQL patterns that remain blocked. - Merge this before the LLM Wiki plugin PR so the plugin can resolve the new SDK and host APIs. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - OpenAI Codex, GPT-5 coding agent, tool-enabled shell/git/GitHub workflow. Context window size was not exposed by the runtime. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] If this change affects the UI, I have included before/after screenshots - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
d6bee62f02
commit
3c73ed26b5
89 changed files with 27516 additions and 914 deletions
|
|
@ -13,7 +13,9 @@ It is intentionally narrower than [PLUGIN_SPEC.md](./PLUGIN_SPEC.md). The spec i
|
|||
- Plugin database migrations are restricted to a host-derived plugin namespace.
|
||||
- Plugin-owned JSON API routes must be declared in the manifest and are mounted
|
||||
only under `/api/plugins/:pluginId/api/*`.
|
||||
- There is no host-provided shared React component kit for plugins yet.
|
||||
- The host provides a small shared React component kit through
|
||||
`@paperclipai/plugin-sdk/ui`; use it for common Paperclip controls before
|
||||
building custom versions.
|
||||
- `ctx.assets` is not supported in the current runtime.
|
||||
|
||||
## Scaffold a plugin
|
||||
|
|
@ -168,6 +170,187 @@ Mount surfaces currently wired in the host include:
|
|||
- `commentAnnotation`
|
||||
- `commentContextMenuItem`
|
||||
|
||||
## Shared host components
|
||||
|
||||
Use shared components from `@paperclipai/plugin-sdk/ui` when the plugin needs a
|
||||
Paperclip-native control. The host owns the implementation, so plugins inherit
|
||||
the board's current styling, ordering, recent selections, and dark-mode behavior
|
||||
without importing `ui/src` internals.
|
||||
|
||||
Currently exposed components include:
|
||||
|
||||
- `MarkdownBlock` and `MarkdownEditor` for rendered and editable markdown.
|
||||
- `FileTree` for serializable file and directory trees.
|
||||
- `IssuesList` for a native company-scoped issue table.
|
||||
- `AssigneePicker` for the same agent/user selector used in the new issue pane.
|
||||
Use the controlled `value` format `agent:<id>`, `user:<id>`, or `""`.
|
||||
- `ProjectPicker` for the same project selector used in the new issue pane.
|
||||
Use the controlled project id value, or `""` for no project.
|
||||
- `ManagedRoutinesList` for plugin-owned routine settings pages.
|
||||
|
||||
```tsx
|
||||
import { AssigneePicker, ProjectPicker } from "@paperclipai/plugin-sdk/ui";
|
||||
|
||||
export function PluginAssignmentControls({ companyId }: { companyId: string }) {
|
||||
const [assignee, setAssignee] = useState("");
|
||||
const [projectId, setProjectId] = useState("");
|
||||
|
||||
return (
|
||||
<>
|
||||
<AssigneePicker
|
||||
companyId={companyId}
|
||||
value={assignee}
|
||||
onChange={(value) => setAssignee(value)}
|
||||
/>
|
||||
<ProjectPicker
|
||||
companyId={companyId}
|
||||
value={projectId}
|
||||
onChange={setProjectId}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## File and path UI
|
||||
|
||||
Plugin UI often needs to render a file tree, accept a folder path, or browse a
|
||||
project workspace. There are three different surfaces for that, and they map to
|
||||
different trust and data-flow boundaries. Pick the surface that matches the
|
||||
data the plugin actually has.
|
||||
|
||||
### When to use the shared `FileTree`
|
||||
|
||||
Use `FileTree` from `@paperclipai/plugin-sdk/ui` whenever the plugin only needs
|
||||
to render a serializable file/directory list and react to selection or
|
||||
expand/collapse. The host owns the implementation, so plugin UI inherits the
|
||||
board's icons, indent, focus ring, and dark-mode styling without importing host
|
||||
internals.
|
||||
|
||||
```tsx
|
||||
import {
|
||||
FileTree,
|
||||
type FileTreeNode,
|
||||
} from "@paperclipai/plugin-sdk/ui";
|
||||
|
||||
const nodes: FileTreeNode[] = [
|
||||
{ name: "AGENTS.md", path: "AGENTS.md", kind: "file", children: [] },
|
||||
{
|
||||
name: "wiki",
|
||||
path: "wiki",
|
||||
kind: "dir",
|
||||
children: [
|
||||
{ name: "index.md", path: "wiki/index.md", kind: "file", children: [] },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export function WikiTree() {
|
||||
const [expanded, setExpanded] = useState<Set<string>>(() => new Set(["wiki"]));
|
||||
const [selected, setSelected] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<FileTree
|
||||
nodes={nodes}
|
||||
selectedFile={selected}
|
||||
expandedPaths={expanded}
|
||||
onSelectFile={(path) => setSelected(path)}
|
||||
onToggleDir={(path) =>
|
||||
setExpanded((current) => {
|
||||
const next = new Set(current);
|
||||
next.has(path) ? next.delete(path) : next.add(path);
|
||||
return next;
|
||||
})
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Good fits:
|
||||
|
||||
- LLM Wiki page navigation in `packages/plugins/plugin-llm-wiki` builds a
|
||||
`FileTreeNode[]` from worker query results and renders it through `FileTree`.
|
||||
- The example `plugin-file-browser-example` lazily fetches a directory's
|
||||
children through a `loadFileList` action when `onToggleDir` fires, then
|
||||
merges the children into the local tree state — letting the shared component
|
||||
handle rendering and selection.
|
||||
|
||||
Boundary rules:
|
||||
|
||||
- Keep the prop surface serializable (`nodes`, `expandedPaths`, `checkedPaths`,
|
||||
`fileBadges`, `fileTones`). Do not pass arbitrary render functions across the
|
||||
plugin/host boundary in v1; the supported escape hatches are
|
||||
`fileBadges` (status pill keyed by path) and `fileTones` (row tone keyed by
|
||||
path).
|
||||
- Do not import the host's `FileTree.tsx` or any `ui/src/*` module. The SDK
|
||||
declaration is the only supported import path for plugin UI.
|
||||
- The shared `FileTree` is for rendering and selection. Plugin-specific editors,
|
||||
ingest flows, query forms, and lint runs stay inside the plugin and do not
|
||||
belong as `FileTree` props.
|
||||
|
||||
### When to declare `localFolders`
|
||||
|
||||
When the plugin needs operator-configured filesystem roots — typically for
|
||||
trusted local plugins like wiki tooling — declare `localFolders[]` on the
|
||||
manifest and add the `local.folders` capability. The host renders a settings
|
||||
surface for the operator to set the absolute path, validates the path
|
||||
server-side (containment, symlinks, required files/directories), and exposes
|
||||
`ctx.localFolders.readText()` and `ctx.localFolders.writeTextAtomic()` in the
|
||||
worker.
|
||||
|
||||
```ts
|
||||
export const manifest = {
|
||||
capabilities: ["local.folders"],
|
||||
localFolders: [
|
||||
{
|
||||
folderKey: "content-root",
|
||||
displayName: "Content root",
|
||||
access: "readWrite",
|
||||
requiredDirectories: ["sources", "pages"],
|
||||
requiredFiles: ["schema.md"],
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
Use this when:
|
||||
|
||||
- The data lives outside any project workspace.
|
||||
- Reads and writes need company-scoped configuration.
|
||||
- The operator picks the path once in plugin settings and the worker resolves
|
||||
files relative to that root.
|
||||
|
||||
Do not use `localFolders` to grant the UI direct browser-side access to the
|
||||
filesystem — there is no such capability. The browser still goes through the
|
||||
worker via `getData` / `performAction`, and the worker only exposes paths it
|
||||
chose to expose.
|
||||
|
||||
### When to keep worker-mediated project workspace browsing
|
||||
|
||||
When the data lives inside an existing project workspace, keep the browsing
|
||||
flow worker-mediated:
|
||||
|
||||
- The worker uses `ctx.projects.listWorkspaces()` to resolve the workspace
|
||||
path, then reads its filesystem with normal Node APIs.
|
||||
- The plugin UI calls a `getData` handler for the root listing and an action
|
||||
for lazy children, then renders them through `FileTree`.
|
||||
- The worker is the only side that touches the disk. The browser receives a
|
||||
serializable tree and never sees raw absolute paths it can replay.
|
||||
|
||||
The example `plugin-file-browser-example` is the reference for this pattern:
|
||||
the worker registers `fileList` (data) and `loadFileList` (action) over the
|
||||
same handler, and the UI uses the action for on-toggle directory loading so the
|
||||
shared `FileTree` stays the rendering surface.
|
||||
|
||||
### Mixing surfaces
|
||||
|
||||
A single plugin can use more than one of these. The LLM Wiki uses
|
||||
`localFolders` for its content root, then renders the resulting page list
|
||||
through `FileTree`. The file browser example uses `ctx.projects.listWorkspaces`
|
||||
to pick a workspace and renders its on-disk tree through `FileTree` with lazy
|
||||
loading. Pick the boundary per data source, not per plugin.
|
||||
|
||||
## Company routes
|
||||
|
||||
Plugins may declare a `page` slot with `routePath` to own a company route like:
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ Current limitations to keep in mind:
|
|||
- Published npm packages are the intended install artifact for deployed plugins.
|
||||
- The repo example plugins under `packages/plugins/examples/` are development conveniences. They work from a source checkout and should not be assumed to exist in a generic published build unless they are explicitly shipped with that build.
|
||||
- Dynamic plugin install is not yet cloud-ready for horizontally scaled or ephemeral deployments. There is no shared artifact store, install coordination, or cross-node distribution layer yet.
|
||||
- The current runtime does not yet ship a real host-provided plugin UI component kit, and it does not support plugin asset uploads/reads. Treat those as future-scope ideas in this spec, not current implementation promises.
|
||||
- The current runtime ships a small host-provided plugin UI component kit through `@paperclipai/plugin-sdk/ui`, but does not support plugin asset uploads/reads yet. Treat plugin asset APIs as future-scope ideas, not current implementation promises.
|
||||
- Scoped plugin API routes are JSON-only and must be declared in `apiRoutes`.
|
||||
They mount under `/api/plugins/:pluginId/api/*`; plugins cannot shadow core
|
||||
API routes.
|
||||
|
|
@ -976,13 +976,23 @@ export function DashboardWidget({ context }: PluginWidgetProps) {
|
|||
|
||||
The SDK includes a `ui` subpath export that plugin frontends import. This subpath provides:
|
||||
|
||||
- **Bridge hooks**: `usePluginData(key, params)`, `usePluginAction(key)`, `useHostContext()`
|
||||
- **Bridge hooks**: `usePluginData(key, params)`, `usePluginAction(key)`, `useHostContext()`, `useHostNavigation()`
|
||||
- **Design tokens**: colors, spacing, typography, shadows matching the host theme
|
||||
- **Shared components**: `MetricCard`, `StatusBadge`, `DataTable`, `LogView`, `ActionBar`, `Spinner`, etc.
|
||||
- **Type definitions**: `PluginPageProps`, `PluginWidgetProps`, `PluginDetailTabProps`
|
||||
|
||||
Plugins are encouraged but not required to use the shared components. A plugin may render entirely custom UI as long as it communicates through the bridge.
|
||||
|
||||
`useHostNavigation()` is the supported way for plugin UI to navigate to
|
||||
Paperclip-internal pages. It exposes `resolveHref(to)`, `navigate(to,
|
||||
options?)`, and `linkProps(to, options?)`. Plugin links should prefer
|
||||
`linkProps()` so anchors keep real `href` values for copy-link, modifier-click,
|
||||
middle-click, and open-in-new-tab behavior while plain left-clicks route through
|
||||
the host SPA router. The host resolves company-scoped paths against the active
|
||||
company prefix without double-prefixing already-prefixed paths. Plugin UI should
|
||||
not use raw same-origin `href`s or `window.location.assign()` for internal
|
||||
Paperclip navigation because those can force a full document reload.
|
||||
|
||||
### 19.0.2 Bundle Isolation
|
||||
|
||||
Plugin UI bundles are loaded as standard ES modules, not iframed. This gives plugins full rendering performance and access to the host's design tokens.
|
||||
|
|
@ -1062,6 +1072,11 @@ The host SDK ships shared components that plugins can import to quickly build UI
|
|||
| `LogView` | Scrollable log output with timestamps | Webhook deliveries, job output, process logs |
|
||||
| `JsonTree` | Collapsible JSON tree for debugging | Raw API responses, plugin state inspection |
|
||||
| `Spinner` | Loading indicator | Data fetch states |
|
||||
| `FileTree` | Host-styled file/directory tree | Wiki pages, workspace files, import previews |
|
||||
| `IssuesList` | Host issue list | Plugin pages that need a native issue view |
|
||||
| `AssigneePicker` | Host assignee picker for agents and board users | Creating issues, assigning routines, filtering work |
|
||||
| `ProjectPicker` | Host project picker | Creating issues, scoping dashboards, filtering work |
|
||||
| `ManagedRoutinesList` | Host routine list | Plugin settings pages that manage routines |
|
||||
|
||||
Plugins may also use entirely custom components. The shared components exist to reduce boilerplate and keep visual consistency, not to limit what plugins can render.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue