50bff3b274
## Thinking Path > - Paperclip is the open source control plane people use to manage AI agents, work, and company context. > - The board UI sidebar is the main way operators keep orientation across companies, projects, agents, issues, and settings. > - The existing fixed expanded sidebar competes with route-specific navigation, especially company settings and plugin routes that bring their own contextual sidebar. > - A collapsible primary rail preserves global navigation while giving contextual pages more horizontal room. > - This pull request adds a persisted collapsed rail, hover/focus peek, keyboard toggle, and a secondary sidebar takeover model for settings and plugin `routeSidebar` surfaces. > - The benefit is a denser board shell that keeps the app rail available without replacing it when a route needs its own navigation. ## Linked Issues or Issue Description Paperclip issue: PAP-10638 Create collapsible sidebar branch. Related GitHub PR found during duplicate search: #3838 (`feat/collapsible-sidebar`) covers a similar sidebar area but is a different head branch and implementation. This PR intentionally packages the work from `PAP-10638-collapsable-sidebar` into one reviewable branch. Problem description: The board shell needs a first-class collapsed sidebar mode. Contextual surfaces such as company settings and plugin route sidebars should not replace the global app sidebar; they should collapse the app sidebar to a rail and render their contextual navigation beside it. ## What Changed - Added desktop collapsed/sidebar-peek state to `SidebarContext`, including persisted user pins, route collapse requests, and forced collapse for secondary-sidebar routes. - Replaced the old resizable sidebar pane with `SidebarShell`, which supports a fixed 64px rail, persisted expanded width, keyboard/pointer resizing, and hover/focus peek overlay behavior. - Updated `Sidebar`, sidebar nav items, project/agent sections, badges, and account/company menu presentation for expanded, collapsed, and peeking states. - Added `RequestCollapsedSidebar` and `SecondarySidebar` so routes and plugin `routeSidebar` slots can request contextual sidebar layouts without replacing the primary app sidebar. - Wired company settings and plugin route sidebars into the secondary-pane takeover model. - Added focused Vitest coverage for sidebar state precedence, shell sizing, nav item rail rendering, keyboard shortcuts, layout takeover behavior, and route collapse requests. - Updated plugin authoring docs/spec references for route sidebar behavior. ## Verification Targeted local verification passed: ```sh NODE_ENV=test pnpm run preflight:workspace-links && NODE_ENV=test pnpm exec vitest run ui/src/context/SidebarContext.test.tsx ui/src/components/SidebarShell.test.tsx ui/src/components/Sidebar.test.tsx ui/src/components/Layout.test.tsx ui/src/components/RequestCollapsedSidebar.test.tsx ui/src/components/SidebarNavItem.test.tsx ui/src/components/SidebarAgents.test.tsx ui/src/components/SidebarProjects.test.tsx ui/src/components/KeyboardShortcutsCheatsheet.test.tsx ui/src/hooks/useKeyboardShortcuts.test.tsx ``` Result: 10 test files passed, 88 tests passed. Additional follow-up verification passed after review fixes: ```sh NODE_ENV=test pnpm run preflight:workspace-links && NODE_ENV=test pnpm exec vitest run ui/src/components/Layout.test.tsx ui/src/context/SidebarContext.test.tsx && pnpm --filter /ui typecheck ``` Result: 2 test files passed, 28 tests passed, and UI typecheck passed. Latest PR-head remote checks: Paperclip PR workflow, Snyk, Socket, and Greptile are green; commitperclip `review` is cancelled in its security-gate step after filing a non-blocking neutral `security-review` check. Notes: - A direct run without `NODE_ENV=test` loads React's production build in this workspace, where `act` is unavailable; the command above matches the repo stable runner's test environment. - I did not run Playwright/browser e2e or full workspace build/typecheck in this PR-creation heartbeat. - QA screenshots are attached in https://github.com/paperclipai/paperclip/pull/7824#issuecomment-4661968387 for expanded, collapsed rail, hover peek, and settings secondary-sidebar states. ## Risks - Medium UI layout risk: this changes the board shell and primary sidebar composition across many routes. - Local storage migration risk is low: new collapsed state uses a new key and existing width storage remains scoped to the sidebar width. - Plugin route risk: plugin `routeSidebar` slots now render as secondary panes on desktop, so plugin authors should confirm their route sidebar content fits a 240px contextual pane. - Mobile risk appears low because mobile keeps the drawer model and gates collapsed/peek behavior to desktop. > 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 coding agent based on GPT-5, with local shell/git/GitHub CLI tool use. Exact service-side model identifier and context window were not exposed in this 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 searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [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 - [ ] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Paperclip <noreply@paperclip.ing>
579 lines
21 KiB
Markdown
579 lines
21 KiB
Markdown
# Plugin Authoring Guide
|
|
|
|
This guide describes the current, implemented way to create a Paperclip plugin in this repo.
|
|
|
|
It is intentionally narrower than [PLUGIN_SPEC.md](./PLUGIN_SPEC.md). The spec includes future ideas; this guide only covers the alpha surface that exists now.
|
|
|
|
> **New to plugins?** Start with the short [Local Plugin Development guide](./LOCAL_PLUGIN_DEVELOPMENT.md) — it walks the CLI happy path (`plugin init` → `pnpm dev` → `plugin install <path>`) end to end. Come back here for the full manifest surface, worker capabilities, and UI components.
|
|
|
|
## Current reality
|
|
|
|
- Treat plugin workers and plugin UI as trusted code.
|
|
- Plugin UI runs as same-origin JavaScript inside the main Paperclip app.
|
|
- Worker-side host APIs are capability-gated.
|
|
- Plugin UI is not sandboxed by manifest capabilities.
|
|
- Plugin database migrations are restricted to a host-derived plugin namespace.
|
|
- Plugin-managed surfaces are first-class records (agents, projects, routines, and
|
|
skills) rather than private plugin-only state.
|
|
- Plugin-owned JSON API routes must be declared in the manifest and are mounted
|
|
only under `/api/plugins/:pluginId/api/*`.
|
|
- 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
|
|
|
|
Use the CLI scaffold command:
|
|
|
|
```bash
|
|
paperclipai plugin init @yourscope/plugin-name --output /absolute/path/to/plugin-repos
|
|
```
|
|
|
|
That creates `<output>/plugin-name/` with:
|
|
|
|
- `src/manifest.ts`
|
|
- `src/worker.ts`
|
|
- `src/ui/index.tsx`
|
|
- `tests/plugin.spec.ts`
|
|
- `esbuild.config.mjs`
|
|
- `rollup.config.mjs`
|
|
|
|
Inside this monorepo, the scaffold uses `workspace:*` for `@paperclipai/plugin-sdk`.
|
|
|
|
Outside this monorepo, the scaffold snapshots `@paperclipai/plugin-sdk` from the local Paperclip checkout into a `.paperclip-sdk/` tarball so you can build and test a plugin without publishing anything to npm first. Pass `--sdk-path /absolute/path/to/paperclip/packages/plugins/sdk` if you have more than one Paperclip checkout.
|
|
|
|
## Local development workflow
|
|
|
|
See the short [Local Plugin Development guide](./LOCAL_PLUGIN_DEVELOPMENT.md) for the full happy path (`pnpm dev` → `paperclipai plugin install <absolute-path>` → `paperclipai plugin list`) and reload semantics.
|
|
|
|
Minimum verification from the generated plugin folder:
|
|
|
|
```bash
|
|
pnpm install
|
|
pnpm typecheck
|
|
pnpm test
|
|
pnpm build
|
|
```
|
|
|
|
## Supported alpha surface
|
|
|
|
Worker:
|
|
|
|
- config
|
|
- events
|
|
- jobs
|
|
- launchers
|
|
- http
|
|
- secrets
|
|
- activity
|
|
- state
|
|
- database namespace via `ctx.db`
|
|
- scoped JSON API routes declared with `apiRoutes`
|
|
- entities
|
|
- projects, project workspaces, and plugin-managed projects
|
|
- companies
|
|
- issues, comments, namespaced `plugin:<pluginKey>` origins, blocker relations, checkout assertions, assignment wakeups, and orchestration summaries
|
|
- agents, plugin-managed agents, and agent sessions
|
|
- plugin-managed routines
|
|
- plugin-managed skills
|
|
- goals
|
|
- data/actions
|
|
- streams
|
|
- tools
|
|
- metrics
|
|
- logger
|
|
|
|
### Plugin database declarations
|
|
|
|
First-party or otherwise trusted orchestration plugins can declare:
|
|
|
|
```ts
|
|
database: {
|
|
migrationsDir: "migrations",
|
|
coreReadTables: ["issues"],
|
|
}
|
|
```
|
|
|
|
Required capabilities are `database.namespace.migrate` and
|
|
`database.namespace.read`; add `database.namespace.write` for runtime mutations.
|
|
The host derives `ctx.db.namespace`, runs SQL files in filename order before the
|
|
worker starts, records checksums in `plugin_migrations`, and rejects changed
|
|
already-applied migrations.
|
|
|
|
Migration SQL may create or alter objects only inside `ctx.db.namespace`. It may
|
|
reference whitelisted `public` core tables for foreign keys or read-only views,
|
|
but may not mutate/alter/drop/truncate public tables, create extensions,
|
|
triggers, untrusted languages, or runtime multi-statement SQL. Runtime
|
|
`ctx.db.query()` is restricted to `SELECT`; runtime `ctx.db.execute()` is
|
|
restricted to namespace-local `INSERT`, `UPDATE`, and `DELETE`.
|
|
|
|
### Scoped plugin API routes
|
|
|
|
Plugins can expose JSON-only routes under their own namespace:
|
|
|
|
```ts
|
|
apiRoutes: [
|
|
{
|
|
routeKey: "initialize",
|
|
method: "POST",
|
|
path: "/issues/:issueId/smoke",
|
|
auth: "board-or-agent",
|
|
capability: "api.routes.register",
|
|
checkoutPolicy: "required-for-agent-in-progress",
|
|
companyResolution: { from: "issue", param: "issueId" },
|
|
},
|
|
]
|
|
```
|
|
|
|
The host resolves the plugin, checks that it is ready, enforces
|
|
`api.routes.register`, matches the declared method/path, resolves company access,
|
|
and applies checkout policy before dispatching to the worker's `onApiRequest`
|
|
handler. The worker receives sanitized headers, route params, query, parsed JSON
|
|
body, actor context, and company id. Do not use plugin routes to claim core
|
|
paths; they always remain under `/api/plugins/:pluginId/api/*`.
|
|
|
|
## Managed Paperclip resources
|
|
|
|
Plugins that provide durable Paperclip business objects should declare them in
|
|
the manifest and let the host create or relink the actual records per company.
|
|
Do this for plugin-owned agents, projects, routines, and skills.
|
|
Do not hide long-lived work behind private plugin state when it should be visible
|
|
to the board, scoped to a company, audited, budgeted, and assigned like normal
|
|
Paperclip work.
|
|
|
|
Content-oriented plugins, such as LLM Wiki-style ingestion or durable knowledge
|
|
systems, should use the same pattern: managed projects for operation issues,
|
|
managed agents plus managed skills for LLM work, and managed routines for
|
|
ingest, lint, refresh, or maintenance runs.
|
|
|
|
Use these surfaces:
|
|
|
|
- Managed agents: declare top-level `agents[]` and require
|
|
`agents.managed`. Use this when the plugin provides a named worker the board
|
|
should see in the org, budget, pause, invoke, and inspect. Managed agents are
|
|
normal Paperclip agents with plugin ownership metadata, not background plugin
|
|
workers.
|
|
- Managed projects: declare top-level `projects[]` and require
|
|
`projects.managed`. Use this when the plugin needs a stable company-scoped
|
|
project for its issues, routines, or workspace-oriented UI. Keep plugin work
|
|
in a project instead of scattering generated issues across unrelated projects.
|
|
- Managed routines: declare top-level `routines[]` and require
|
|
`routines.managed`. Use this for scheduled, webhook, or manually triggered
|
|
jobs that should create visible Paperclip issues. Prefer managed routines over
|
|
plugin `jobs[]` for recurring business work; plugin jobs are for plugin
|
|
runtime maintenance that does not need a board-visible task trail.
|
|
- Managed skills: declare top-level `skills[]` and require `skills.managed`.
|
|
Use this for reusable plugin capabilities that should be surfaced to operators and
|
|
synced into Paperclip managed agents.
|
|
|
|
Managed resources are resolved by stable plugin keys, not hardcoded database
|
|
ids. In a worker action or data handler, call `ctx.agents.managed.reconcile()`,
|
|
`ctx.projects.managed.reconcile()`, `ctx.routines.managed.reconcile()`, and
|
|
`ctx.skills.managed.reconcile()` for
|
|
the current `companyId`. `reconcile()` creates the missing resource, relinks a
|
|
recoverable binding, or returns the existing resource. `reset()` reapplies the
|
|
manifest defaults when the operator wants to restore the plugin's suggested
|
|
configuration.
|
|
|
|
Declare dependencies between managed resources with refs. A routine can point
|
|
at a managed agent through `assigneeRef` and at a managed project through
|
|
`projectRef`. Reconcile the referenced agent and project before reconciling the
|
|
routine; if a ref is still missing, the routine resolution reports
|
|
`missing_refs` instead of guessing.
|
|
|
|
```ts
|
|
import type { PaperclipPluginManifestV1 } from "@paperclipai/plugin-sdk";
|
|
|
|
const manifest: PaperclipPluginManifestV1 = {
|
|
id: "example.research-plugin",
|
|
apiVersion: 1,
|
|
version: "0.1.0",
|
|
displayName: "Research Plugin",
|
|
description: "Creates a managed research agent and scheduled research routine.",
|
|
author: "Example",
|
|
categories: ["automation"],
|
|
capabilities: [
|
|
"agents.managed",
|
|
"projects.managed",
|
|
"routines.managed",
|
|
"skills.managed",
|
|
"instance.settings.register",
|
|
],
|
|
entrypoints: {
|
|
worker: "./dist/worker.js",
|
|
ui: "./dist/ui",
|
|
},
|
|
agents: [
|
|
{
|
|
agentKey: "researcher",
|
|
displayName: "Researcher",
|
|
role: "research",
|
|
title: "Research Agent",
|
|
capabilities: "Runs recurring research briefs for this company.",
|
|
adapterPreference: ["codex_local", "claude_local", "process"],
|
|
instructions: {
|
|
content: "Follow the Paperclip heartbeat and produce concise research briefs.",
|
|
},
|
|
},
|
|
],
|
|
projects: [
|
|
{
|
|
projectKey: "research",
|
|
displayName: "Research",
|
|
description: "Recurring research work created by the Research Plugin.",
|
|
status: "in_progress",
|
|
},
|
|
],
|
|
routines: [
|
|
{
|
|
routineKey: "weekly-brief",
|
|
title: "Weekly research brief",
|
|
description: "Create a short research brief for the board.",
|
|
assigneeRef: { resourceKind: "agent", resourceKey: "researcher" },
|
|
projectRef: { resourceKind: "project", resourceKey: "research" },
|
|
priority: "medium",
|
|
triggers: [
|
|
{
|
|
kind: "schedule",
|
|
label: "Monday morning",
|
|
cronExpression: "0 9 * * 1",
|
|
timezone: "America/Chicago",
|
|
enabled: false,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
skills: [
|
|
{
|
|
skillKey: "weekly-brief-skills",
|
|
displayName: "Weekly Briefer",
|
|
description: "Reusable skill for the managed research workflow.",
|
|
},
|
|
],
|
|
ui: {
|
|
slots: [
|
|
{
|
|
type: "settingsPage",
|
|
id: "settings",
|
|
displayName: "Research",
|
|
exportName: "SettingsPage",
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
export default manifest;
|
|
```
|
|
|
|
In the worker, expose a small setup action or settings-page action that
|
|
reconciles the resources for the selected company:
|
|
|
|
```ts
|
|
import { definePlugin } from "@paperclipai/plugin-sdk";
|
|
|
|
export default definePlugin({
|
|
setup(ctx) {
|
|
ctx.actions.register("setup-company", async (params) => {
|
|
const companyId = String(params.companyId ?? "");
|
|
if (!companyId) throw new Error("companyId is required");
|
|
|
|
const project = await ctx.projects.managed.reconcile("research", companyId);
|
|
const agent = await ctx.agents.managed.reconcile("researcher", companyId);
|
|
const routine = await ctx.routines.managed.reconcile("weekly-brief", companyId);
|
|
const skill = await ctx.skills.managed.reconcile("weekly-brief-skills", companyId);
|
|
|
|
return { project, agent, routine, skill };
|
|
});
|
|
},
|
|
});
|
|
```
|
|
|
|
Authoring rules:
|
|
|
|
- Keep keys stable once published. Renaming `agentKey`, `projectKey`,
|
|
`routineKey`, or `skillKey` creates a new managed resource from the host's
|
|
point of view.
|
|
- Use managed agents for plugin-provided labor. Use `ctx.agents.invoke()` or
|
|
`ctx.agents.sessions` only after you have a real agent id, either selected by
|
|
the operator or resolved from `ctx.agents.managed`.
|
|
- Use managed routines for recurring or externally triggered work that should
|
|
produce tasks. Schedule, webhook, and API triggers are visible routine
|
|
triggers, and each run has the normal Paperclip issue/audit trail.
|
|
- Use managed skills for reusable operator-visible capabilities that are shared
|
|
by managed agents. Reconcile skill declarations by `skillKey` and keep the
|
|
declared skill markdown and files in sync with agent behavior.
|
|
- Use managed projects to keep plugin-generated work organized and to give
|
|
project-scoped plugin UI a stable home. For filesystem access inside a
|
|
project, still resolve project workspaces through `ctx.projects`.
|
|
- Keep defaults conservative. Managed declarations are suggestions owned by the
|
|
plugin, but the resulting resources are normal Paperclip records that the
|
|
operator can inspect, pause, and adjust.
|
|
|
|
UI:
|
|
|
|
- `usePluginData`
|
|
- `usePluginAction`
|
|
- `usePluginStream`
|
|
- `usePluginToast`
|
|
- `useHostContext`
|
|
- typed slot props from `@paperclipai/plugin-sdk/ui`
|
|
|
|
Mount surfaces currently wired in the host include:
|
|
|
|
- `page`
|
|
- `settingsPage`
|
|
- `dashboardWidget`
|
|
- `sidebar`
|
|
- `routeSidebar`
|
|
- `sidebarPanel`
|
|
- `detailTab`
|
|
- `taskDetailView`
|
|
- `projectSidebarItem`
|
|
- `globalToolbarButton`
|
|
- `toolbarButton`
|
|
- `contextMenuItem`
|
|
- `commentAnnotation`
|
|
- `commentContextMenuItem`
|
|
|
|
### `routeSidebar` and the app sidebar
|
|
|
|
A `routeSidebar` slot gives a plugin page route its own contextual navigation.
|
|
It **coexists** with the main app sidebar rather than replacing it: while your
|
|
route is active the host collapses the app `<Sidebar/>` to its 64px icon rail
|
|
(still hover/peek-able) and renders your sidebar in a second pane, yielding
|
|
`[ app rail ][ your sidebar ][ content ]`.
|
|
|
|
Because the host drives this collapse, a plugin should **not** mount
|
|
`RequestCollapsedSidebar` or otherwise try to collapse the app sidebar itself —
|
|
doing so is redundant and fights the host. While your route is active the app
|
|
rail is forced collapsed (its expand toggle is hidden), overriding any user pin
|
|
— a secondary sidebar always collapses the primary. This force never changes the
|
|
user's saved expanded/collapsed preference, so the host restores exactly what
|
|
the user chose as soon as they navigate away.
|
|
|
|
## 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.
|
|
|
|
Prefer shared components for common Paperclip UX patterns to reduce drift and
|
|
deprecation risk, especially for task/assignment flows and routine or sidebar-like
|
|
plugin screens.
|
|
|
|
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:
|
|
|
|
```text
|
|
/:companyPrefix/<routePath>
|
|
```
|
|
|
|
Rules:
|
|
|
|
- `routePath` must be a single lowercase slug
|
|
- it cannot collide with reserved host routes
|
|
- it cannot duplicate another installed plugin page route
|
|
|
|
## Publishing guidance
|
|
|
|
- Use npm packages as the deployment artifact.
|
|
- Treat repo-local example installs as a development workflow only.
|
|
- Prefer keeping plugin UI self-contained inside the package.
|
|
- Do not rely on host design-system components or undocumented app internals.
|
|
- GitHub repository installs are not a first-class workflow today. For local development, use a checked-out local path. For production, publish to npm or a private npm-compatible registry.
|
|
|
|
## Verification before handoff
|
|
|
|
At minimum:
|
|
|
|
```bash
|
|
pnpm --filter <your-plugin-package> typecheck
|
|
pnpm --filter <your-plugin-package> test
|
|
pnpm --filter <your-plugin-package> build
|
|
```
|
|
|
|
If you changed host integration too, also run:
|
|
|
|
```bash
|
|
pnpm -r typecheck
|
|
pnpm test:run
|
|
pnpm build
|
|
```
|