Add kitchen sink plugin example
This commit is contained in:
699
doc/plans/2026-03-13-plugin-kitchen-sink-example.md
Normal file
699
doc/plans/2026-03-13-plugin-kitchen-sink-example.md
Normal file
@@ -0,0 +1,699 @@
|
||||
# Kitchen Sink Plugin Plan
|
||||
|
||||
## Goal
|
||||
|
||||
Add a new first-party example plugin, `Kitchen Sink (Example)`, that demonstrates every currently implemented Paperclip plugin API surface in one place.
|
||||
|
||||
This plugin is meant to be:
|
||||
|
||||
- a living reference implementation for contributors
|
||||
- a manual test harness for the plugin runtime
|
||||
- a discoverable demo of what plugins can actually do today
|
||||
|
||||
It is not meant to be a polished end-user product plugin.
|
||||
|
||||
## Why
|
||||
|
||||
The current plugin system has a real API surface, but it is spread across:
|
||||
|
||||
- SDK docs
|
||||
- SDK types
|
||||
- plugin spec prose
|
||||
- two example plugins that each show only a narrow slice
|
||||
|
||||
That makes it hard to answer basic questions like:
|
||||
|
||||
- what can plugins render?
|
||||
- what can plugin workers actually do?
|
||||
- which surfaces are real versus aspirational?
|
||||
- how should a new plugin be structured in this repo?
|
||||
|
||||
The kitchen-sink plugin should answer those questions by example.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
The plugin is successful if a contributor can install it and, without reading the SDK first, discover and exercise the current plugin runtime surface area from inside Paperclip.
|
||||
|
||||
Concretely:
|
||||
|
||||
- it installs from the bundled examples list
|
||||
- it exposes at least one demo for every implemented worker API surface
|
||||
- it exposes at least one demo for every host-mounted UI surface
|
||||
- it clearly labels local-only / trusted-only demos
|
||||
- it is safe enough for local development by default
|
||||
- it doubles as a regression harness for plugin runtime changes
|
||||
|
||||
## Constraints
|
||||
|
||||
- Keep it instance-installed, not company-installed.
|
||||
- Treat this as a trusted/local example plugin.
|
||||
- Do not rely on cloud-safe runtime assumptions.
|
||||
- Avoid destructive defaults.
|
||||
- Avoid irreversible mutations unless they are clearly labeled and easy to undo.
|
||||
|
||||
## Source Of Truth For This Plan
|
||||
|
||||
This plan is based on the currently implemented SDK/types/runtime, not only the long-horizon spec.
|
||||
|
||||
Primary references:
|
||||
|
||||
- `packages/plugins/sdk/README.md`
|
||||
- `packages/plugins/sdk/src/types.ts`
|
||||
- `packages/plugins/sdk/src/ui/types.ts`
|
||||
- `packages/shared/src/constants.ts`
|
||||
- `packages/shared/src/types/plugin.ts`
|
||||
|
||||
## Current Surface Inventory
|
||||
|
||||
### Worker/runtime APIs to demonstrate
|
||||
|
||||
These are the concrete `ctx` clients currently exposed by the SDK:
|
||||
|
||||
- `ctx.config`
|
||||
- `ctx.events`
|
||||
- `ctx.jobs`
|
||||
- `ctx.launchers`
|
||||
- `ctx.http`
|
||||
- `ctx.secrets`
|
||||
- `ctx.assets`
|
||||
- `ctx.activity`
|
||||
- `ctx.state`
|
||||
- `ctx.entities`
|
||||
- `ctx.projects`
|
||||
- `ctx.companies`
|
||||
- `ctx.issues`
|
||||
- `ctx.agents`
|
||||
- `ctx.goals`
|
||||
- `ctx.data`
|
||||
- `ctx.actions`
|
||||
- `ctx.streams`
|
||||
- `ctx.tools`
|
||||
- `ctx.metrics`
|
||||
- `ctx.logger`
|
||||
|
||||
### UI surfaces to demonstrate
|
||||
|
||||
Surfaces defined in the SDK:
|
||||
|
||||
- `page`
|
||||
- `settingsPage`
|
||||
- `dashboardWidget`
|
||||
- `sidebar`
|
||||
- `sidebarPanel`
|
||||
- `detailTab`
|
||||
- `taskDetailView`
|
||||
- `projectSidebarItem`
|
||||
- `toolbarButton`
|
||||
- `contextMenuItem`
|
||||
- `commentAnnotation`
|
||||
- `commentContextMenuItem`
|
||||
|
||||
### Current host confidence
|
||||
|
||||
Confirmed or strongly indicated as mounted in the current app:
|
||||
|
||||
- `page`
|
||||
- `settingsPage`
|
||||
- `dashboardWidget`
|
||||
- `detailTab`
|
||||
- `projectSidebarItem`
|
||||
- comment surfaces
|
||||
- launcher infrastructure
|
||||
|
||||
Need explicit validation before claiming full demo coverage:
|
||||
|
||||
- `sidebar`
|
||||
- `sidebarPanel`
|
||||
- `taskDetailView`
|
||||
- `toolbarButton` as direct slot, distinct from launcher placement
|
||||
- `contextMenuItem` as direct slot, distinct from comment menu and launcher placement
|
||||
|
||||
The implementation should keep a small validation checklist for these before we call the plugin "complete".
|
||||
|
||||
## Plugin Concept
|
||||
|
||||
The plugin should be named:
|
||||
|
||||
- display name: `Kitchen Sink (Example)`
|
||||
- package: `@paperclipai/plugin-kitchen-sink-example`
|
||||
- plugin id: `paperclip.kitchen-sink-example` or `paperclip-kitchen-sink-example`
|
||||
|
||||
Recommendation: use `paperclip-kitchen-sink-example` to match current in-repo example naming style.
|
||||
|
||||
Category mix:
|
||||
|
||||
- `ui`
|
||||
- `automation`
|
||||
- `workspace`
|
||||
- `connector`
|
||||
|
||||
That is intentionally broad because the point is coverage.
|
||||
|
||||
## UX Shape
|
||||
|
||||
The plugin should have one main full-page demo console plus smaller satellites on other surfaces.
|
||||
|
||||
### 1. Plugin page
|
||||
|
||||
Primary route: the plugin `page` surface should be the central dashboard for all demos.
|
||||
|
||||
Recommended page sections:
|
||||
|
||||
- `Overview`
|
||||
- what this plugin demonstrates
|
||||
- current capabilities granted
|
||||
- current host context
|
||||
- `UI Surfaces`
|
||||
- links explaining where each other surface should appear
|
||||
- `Data + Actions`
|
||||
- buttons and forms for bridge-driven worker demos
|
||||
- `Events + Streams`
|
||||
- emit event
|
||||
- watch event log
|
||||
- stream demo output
|
||||
- `Paperclip Domain APIs`
|
||||
- companies
|
||||
- projects/workspaces
|
||||
- issues
|
||||
- goals
|
||||
- agents
|
||||
- `Local Workspace + Process`
|
||||
- file listing
|
||||
- file read/write scratch area
|
||||
- child process demo
|
||||
- `Jobs + Webhooks + Tools`
|
||||
- job status
|
||||
- webhook URL and recent deliveries
|
||||
- declared tools
|
||||
- `State + Entities + Assets`
|
||||
- scoped state editor
|
||||
- plugin entity inspector
|
||||
- upload/generated asset demo
|
||||
- `Observability`
|
||||
- metrics written
|
||||
- activity log samples
|
||||
- latest worker logs
|
||||
|
||||
### 2. Dashboard widget
|
||||
|
||||
A compact widget on the main dashboard should show:
|
||||
|
||||
- plugin health
|
||||
- count of demos exercised
|
||||
- recent event/stream activity
|
||||
- shortcut to the full plugin page
|
||||
|
||||
### 3. Project sidebar item
|
||||
|
||||
Add a `Kitchen Sink` link under each project that deep-links into a project-scoped plugin tab.
|
||||
|
||||
### 4. Detail tabs
|
||||
|
||||
Use detail tabs to demonstrate entity-context rendering on:
|
||||
|
||||
- `project`
|
||||
- `issue`
|
||||
- `agent`
|
||||
- `goal`
|
||||
|
||||
Each tab should show:
|
||||
|
||||
- the host context it received
|
||||
- the relevant entity fetch via worker bridge
|
||||
- one small action scoped to that entity
|
||||
|
||||
### 5. Comment surfaces
|
||||
|
||||
Use issue comment demos to prove comment-specific extension points:
|
||||
|
||||
- `commentAnnotation`
|
||||
- render parsed metadata below each comment
|
||||
- show comment id, issue id, and a small derived status
|
||||
- `commentContextMenuItem`
|
||||
- add a menu action like `Copy Context To Kitchen Sink`
|
||||
- action writes a plugin entity or state record for later inspection
|
||||
|
||||
### 6. Settings page
|
||||
|
||||
Custom `settingsPage` should be intentionally simple and operational:
|
||||
|
||||
- `About`
|
||||
- `Danger / Trust Model`
|
||||
- demo toggles
|
||||
- local process defaults
|
||||
- workspace scratch-path behavior
|
||||
- secret reference inputs
|
||||
- event/job/webhook sample config
|
||||
|
||||
This plugin should also keep the generic plugin settings `Status` tab useful by writing health, logs, and metrics.
|
||||
|
||||
## Feature Matrix
|
||||
|
||||
Each implemented worker API should have a visible demo.
|
||||
|
||||
### `ctx.config`
|
||||
|
||||
Demo:
|
||||
|
||||
- read live config
|
||||
- show config JSON
|
||||
- react to config changes without restart where possible
|
||||
|
||||
### `ctx.events`
|
||||
|
||||
Demos:
|
||||
|
||||
- emit a plugin event
|
||||
- subscribe to plugin events
|
||||
- subscribe to a core Paperclip event such as `issue.created`
|
||||
- show recent received events in a timeline
|
||||
|
||||
### `ctx.jobs`
|
||||
|
||||
Demos:
|
||||
|
||||
- one scheduled heartbeat-style demo job
|
||||
- one manual run button from the UI if host supports manual job trigger
|
||||
- show last run result and timestamps
|
||||
|
||||
### `ctx.launchers`
|
||||
|
||||
Demos:
|
||||
|
||||
- declare launchers in manifest
|
||||
- optionally register one runtime launcher from the worker
|
||||
- show launcher metadata on the plugin page
|
||||
|
||||
### `ctx.http`
|
||||
|
||||
Demo:
|
||||
|
||||
- make a simple outbound GET request to a safe endpoint
|
||||
- show status code, latency, and JSON result
|
||||
|
||||
Recommendation: default to a Paperclip-local endpoint or a stable public echo endpoint to avoid flaky docs.
|
||||
|
||||
### `ctx.secrets`
|
||||
|
||||
Demo:
|
||||
|
||||
- operator enters a secret reference in config
|
||||
- plugin resolves it on demand
|
||||
- UI only shows masked result length / success status, never raw secret
|
||||
|
||||
### `ctx.assets`
|
||||
|
||||
Demos:
|
||||
|
||||
- generate a text asset from the UI
|
||||
- optionally upload a tiny JSON blob or screenshot-like text file
|
||||
- show returned asset URL
|
||||
|
||||
### `ctx.activity`
|
||||
|
||||
Demo:
|
||||
|
||||
- button to write a plugin activity log entry against current company/entity
|
||||
|
||||
### `ctx.state`
|
||||
|
||||
Demos:
|
||||
|
||||
- instance-scoped state
|
||||
- company-scoped state
|
||||
- project-scoped state
|
||||
- issue-scoped state
|
||||
- delete/reset controls
|
||||
|
||||
Use a small state inspector/editor on the plugin page.
|
||||
|
||||
### `ctx.entities`
|
||||
|
||||
Demos:
|
||||
|
||||
- create plugin-owned sample records
|
||||
- list/filter them
|
||||
- show one realistic use case such as "copied comments" or "demo sync records"
|
||||
|
||||
### `ctx.projects`
|
||||
|
||||
Demos:
|
||||
|
||||
- list projects
|
||||
- list project workspaces
|
||||
- resolve primary workspace
|
||||
- resolve workspace for issue
|
||||
|
||||
### `ctx.companies`
|
||||
|
||||
Demo:
|
||||
|
||||
- list companies and show current selected company
|
||||
|
||||
### `ctx.issues`
|
||||
|
||||
Demos:
|
||||
|
||||
- list issues in current company
|
||||
- create issue
|
||||
- update issue status/title
|
||||
- list comments
|
||||
- create comment
|
||||
|
||||
### `ctx.agents`
|
||||
|
||||
Demos:
|
||||
|
||||
- list agents
|
||||
- invoke one agent with a test prompt
|
||||
- pause/resume where safe
|
||||
|
||||
Agent mutation controls should be behind an explicit warning.
|
||||
|
||||
### `ctx.agents.sessions`
|
||||
|
||||
Demos:
|
||||
|
||||
- create agent chat session
|
||||
- send message
|
||||
- stream events back to the UI
|
||||
- close session
|
||||
|
||||
This is a strong candidate for the best "wow" demo on the plugin page.
|
||||
|
||||
### `ctx.goals`
|
||||
|
||||
Demos:
|
||||
|
||||
- list goals
|
||||
- create goal
|
||||
- update status/title
|
||||
|
||||
### `ctx.data`
|
||||
|
||||
Use throughout the plugin for all read-side bridge demos.
|
||||
|
||||
### `ctx.actions`
|
||||
|
||||
Use throughout the plugin for all mutation-side bridge demos.
|
||||
|
||||
### `ctx.streams`
|
||||
|
||||
Demos:
|
||||
|
||||
- live event log stream
|
||||
- token-style stream from an agent session relay
|
||||
- fake progress stream for a long-running action
|
||||
|
||||
### `ctx.tools`
|
||||
|
||||
Demos:
|
||||
|
||||
- declare 2-3 simple agent tools
|
||||
- tool 1: echo/diagnostics
|
||||
- tool 2: project/workspace summary
|
||||
- tool 3: create issue or write plugin state
|
||||
|
||||
The plugin page should list declared tools and show example input payloads.
|
||||
|
||||
### `ctx.metrics`
|
||||
|
||||
Demo:
|
||||
|
||||
- write a sample metric on each major demo action
|
||||
- surface a small recent metrics table in the plugin page
|
||||
|
||||
### `ctx.logger`
|
||||
|
||||
Demo:
|
||||
|
||||
- every action logs structured entries
|
||||
- plugin settings `Status` page then doubles as the log viewer
|
||||
|
||||
## Local Workspace And Process Demos
|
||||
|
||||
The plugin SDK intentionally leaves file/process operations to the plugin itself once it has workspace metadata.
|
||||
|
||||
The kitchen-sink plugin should demonstrate that explicitly.
|
||||
|
||||
### Workspace demos
|
||||
|
||||
- list files from a selected workspace
|
||||
- read a file
|
||||
- write to a plugin-owned scratch file
|
||||
- optionally search files with `rg` if available
|
||||
|
||||
### Process demos
|
||||
|
||||
- run a short-lived command like `pwd`, `ls`, or `git status`
|
||||
- stream stdout/stderr back to UI
|
||||
- show exit code and timing
|
||||
|
||||
Important safeguards:
|
||||
|
||||
- default commands must be read-only
|
||||
- no shell interpolation from arbitrary free-form input in v1
|
||||
- provide a curated command list or a strongly validated command form
|
||||
- clearly label this area as local-only and trusted-only
|
||||
|
||||
## Proposed Manifest Coverage
|
||||
|
||||
The plugin should aim to declare:
|
||||
|
||||
- `page`
|
||||
- `settingsPage`
|
||||
- `dashboardWidget`
|
||||
- `detailTab` for `project`, `issue`, `agent`, `goal`
|
||||
- `projectSidebarItem`
|
||||
- `commentAnnotation`
|
||||
- `commentContextMenuItem`
|
||||
|
||||
Then, after host validation, add if supported:
|
||||
|
||||
- `sidebar`
|
||||
- `sidebarPanel`
|
||||
- `taskDetailView`
|
||||
- `toolbarButton`
|
||||
- `contextMenuItem`
|
||||
|
||||
It should also declare one or more `ui.launchers` entries to exercise launcher behavior independently of slot rendering.
|
||||
|
||||
## Proposed Package Layout
|
||||
|
||||
New package:
|
||||
|
||||
- `packages/plugins/examples/plugin-kitchen-sink-example/`
|
||||
|
||||
Expected files:
|
||||
|
||||
- `package.json`
|
||||
- `README.md`
|
||||
- `tsconfig.json`
|
||||
- `src/index.ts`
|
||||
- `src/manifest.ts`
|
||||
- `src/worker.ts`
|
||||
- `src/ui/index.tsx`
|
||||
- `src/ui/components/...`
|
||||
- `src/ui/hooks/...`
|
||||
- `src/lib/...`
|
||||
- optional `scripts/build-ui.mjs` if UI bundling needs esbuild
|
||||
|
||||
## Proposed Internal Architecture
|
||||
|
||||
### Worker modules
|
||||
|
||||
Recommended split:
|
||||
|
||||
- `src/worker.ts`
|
||||
- plugin definition and wiring
|
||||
- `src/worker/data.ts`
|
||||
- `ctx.data.register(...)`
|
||||
- `src/worker/actions.ts`
|
||||
- `ctx.actions.register(...)`
|
||||
- `src/worker/events.ts`
|
||||
- event subscriptions and event log buffer
|
||||
- `src/worker/jobs.ts`
|
||||
- scheduled job handlers
|
||||
- `src/worker/tools.ts`
|
||||
- tool declarations and handlers
|
||||
- `src/worker/local-runtime.ts`
|
||||
- file/process demos
|
||||
- `src/worker/demo-store.ts`
|
||||
- helpers for state/entities/assets/metrics
|
||||
|
||||
### UI modules
|
||||
|
||||
Recommended split:
|
||||
|
||||
- `src/ui/index.tsx`
|
||||
- exported slot components
|
||||
- `src/ui/page/KitchenSinkPage.tsx`
|
||||
- `src/ui/settings/KitchenSinkSettingsPage.tsx`
|
||||
- `src/ui/widgets/KitchenSinkDashboardWidget.tsx`
|
||||
- `src/ui/tabs/ProjectKitchenSinkTab.tsx`
|
||||
- `src/ui/tabs/IssueKitchenSinkTab.tsx`
|
||||
- `src/ui/tabs/AgentKitchenSinkTab.tsx`
|
||||
- `src/ui/tabs/GoalKitchenSinkTab.tsx`
|
||||
- `src/ui/comments/KitchenSinkCommentAnnotation.tsx`
|
||||
- `src/ui/comments/KitchenSinkCommentMenuItem.tsx`
|
||||
- `src/ui/shared/...`
|
||||
|
||||
## Configuration Schema
|
||||
|
||||
The plugin should have a substantial but understandable `instanceConfigSchema`.
|
||||
|
||||
Recommended config fields:
|
||||
|
||||
- `enableDangerousDemos`
|
||||
- `enableWorkspaceDemos`
|
||||
- `enableProcessDemos`
|
||||
- `showSidebarEntry`
|
||||
- `showSidebarPanel`
|
||||
- `showProjectSidebarItem`
|
||||
- `showCommentAnnotation`
|
||||
- `showCommentContextMenuItem`
|
||||
- `showToolbarLauncher`
|
||||
- `defaultDemoCompanyId` optional
|
||||
- `secretRefExample`
|
||||
- `httpDemoUrl`
|
||||
- `processAllowedCommands`
|
||||
- `workspaceScratchSubdir`
|
||||
|
||||
Defaults should keep risky behavior off.
|
||||
|
||||
## Safety Defaults
|
||||
|
||||
Default posture:
|
||||
|
||||
- UI and read-only demos on
|
||||
- mutating domain demos on but explicitly labeled
|
||||
- process demos off by default
|
||||
- no arbitrary shell input by default
|
||||
- no raw secret rendering ever
|
||||
|
||||
## Phased Build Plan
|
||||
|
||||
### Phase 1: Core plugin skeleton
|
||||
|
||||
- scaffold package
|
||||
- add manifest, worker, UI entrypoints
|
||||
- add README
|
||||
- make it appear in bundled examples list
|
||||
|
||||
### Phase 2: Core, confirmed UI surfaces
|
||||
|
||||
- plugin page
|
||||
- settings page
|
||||
- dashboard widget
|
||||
- project sidebar item
|
||||
- detail tabs
|
||||
|
||||
### Phase 3: Core worker APIs
|
||||
|
||||
- config
|
||||
- state
|
||||
- entities
|
||||
- companies/projects/issues/goals
|
||||
- data/actions
|
||||
- metrics/logger/activity
|
||||
|
||||
### Phase 4: Real-time and automation APIs
|
||||
|
||||
- streams
|
||||
- events
|
||||
- jobs
|
||||
- webhooks
|
||||
- agent sessions
|
||||
- tools
|
||||
|
||||
### Phase 5: Local trusted runtime demos
|
||||
|
||||
- workspace file demos
|
||||
- child process demos
|
||||
- guarded by config
|
||||
|
||||
### Phase 6: Secondary UI surfaces
|
||||
|
||||
- comment annotation
|
||||
- comment context menu item
|
||||
- launchers
|
||||
|
||||
### Phase 7: Validation-only surfaces
|
||||
|
||||
Validate whether the current host truly mounts:
|
||||
|
||||
- `sidebar`
|
||||
- `sidebarPanel`
|
||||
- `taskDetailView`
|
||||
- direct-slot `toolbarButton`
|
||||
- direct-slot `contextMenuItem`
|
||||
|
||||
If mounted, add demos.
|
||||
If not mounted, document them as SDK-defined but host-pending.
|
||||
|
||||
## Documentation Deliverables
|
||||
|
||||
The plugin should ship with a README that includes:
|
||||
|
||||
- what it demonstrates
|
||||
- which surfaces are local-only
|
||||
- how to install it
|
||||
- where each UI surface should appear
|
||||
- a mapping from demo card to SDK API
|
||||
|
||||
It should also be referenced from plugin docs as the "reference everything plugin".
|
||||
|
||||
## Testing And Verification
|
||||
|
||||
Minimum verification:
|
||||
|
||||
- package typecheck/build
|
||||
- install from bundled example list
|
||||
- page loads
|
||||
- widget appears
|
||||
- project tab appears
|
||||
- comment surfaces render
|
||||
- settings page loads
|
||||
- key actions succeed
|
||||
|
||||
Recommended manual checklist:
|
||||
|
||||
- create issue from plugin
|
||||
- create goal from plugin
|
||||
- emit and receive plugin event
|
||||
- stream action output
|
||||
- open agent session and receive streamed reply
|
||||
- upload an asset
|
||||
- write plugin activity log
|
||||
- run a safe local process demo
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. Should the process demo remain curated-command-only in the first pass?
|
||||
Recommendation: yes.
|
||||
|
||||
2. Should the plugin create throwaway "kitchen sink demo" issues/goals automatically?
|
||||
Recommendation: no. Make creation explicit.
|
||||
|
||||
3. Should we expose unsupported-but-typed surfaces in the UI even if host mounting is not wired?
|
||||
Recommendation: yes, but label them as `SDK-defined / host validation pending`.
|
||||
|
||||
4. Should agent mutation demos include pause/resume by default?
|
||||
Recommendation: probably yes, but behind a warning block.
|
||||
|
||||
5. Should this plugin be treated as a supported regression harness in CI later?
|
||||
Recommendation: yes. Long term, this should be the plugin-runtime smoke test package.
|
||||
|
||||
## Recommended Next Step
|
||||
|
||||
If this plan looks right, the next implementation pass should start by building only:
|
||||
|
||||
- package skeleton
|
||||
- page
|
||||
- settings page
|
||||
- dashboard widget
|
||||
- one project detail tab
|
||||
- one issue detail tab
|
||||
- the basic worker/action/data/state/event scaffolding
|
||||
|
||||
That is enough to lock the architecture before filling in every demo surface.
|
||||
@@ -0,0 +1,33 @@
|
||||
# @paperclipai/plugin-kitchen-sink-example
|
||||
|
||||
Kitchen Sink is the first-party reference plugin that demonstrates nearly the full currently implemented Paperclip plugin surface in one package.
|
||||
|
||||
It is intentionally broad:
|
||||
|
||||
- full plugin page
|
||||
- dashboard widget
|
||||
- project and issue surfaces
|
||||
- comment surfaces
|
||||
- sidebar surfaces
|
||||
- settings page
|
||||
- worker bridge data/actions
|
||||
- events, jobs, webhooks, tools, streams
|
||||
- state, entities, assets, metrics, activity
|
||||
- local workspace and process demos
|
||||
|
||||
This plugin is for local development, contributor onboarding, and runtime regression testing. It is not meant as a production plugin template to ship unchanged.
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
pnpm --filter @paperclipai/plugin-kitchen-sink-example build
|
||||
pnpm paperclipai plugin install ./packages/plugins/examples/plugin-kitchen-sink-example
|
||||
```
|
||||
|
||||
Or install it from the Paperclip plugin manager as a bundled example once this repo is built.
|
||||
|
||||
## Notes
|
||||
|
||||
- Local workspace and process demos are trusted-only and default to safe, curated commands.
|
||||
- The plugin settings page lets you toggle optional demo surfaces and local runtime behavior.
|
||||
- Some SDK-defined host surfaces still depend on the Paperclip host wiring them visibly; this package aims to exercise the currently mounted ones and make the rest obvious.
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "@paperclipai/plugin-kitchen-sink-example",
|
||||
"version": "0.1.0",
|
||||
"description": "Reference plugin that demonstrates the full Paperclip plugin surface area in one package",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"paperclipPlugin": {
|
||||
"manifest": "./dist/manifest.js",
|
||||
"worker": "./dist/worker.js",
|
||||
"ui": "./dist/ui/"
|
||||
},
|
||||
"scripts": {
|
||||
"prebuild": "node ../../../../scripts/ensure-plugin-build-deps.mjs",
|
||||
"build": "tsc && node ./scripts/build-ui.mjs",
|
||||
"clean": "rm -rf dist",
|
||||
"typecheck": "pnpm --filter @paperclipai/plugin-sdk build && tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@paperclipai/plugin-sdk": "workspace:*",
|
||||
"@paperclipai/shared": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.27.3",
|
||||
"@types/node": "^24.6.0",
|
||||
"@types/react": "^19.0.8",
|
||||
"@types/react-dom": "^19.0.3",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"typescript": "^5.7.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import esbuild from "esbuild";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const packageRoot = path.resolve(__dirname, "..");
|
||||
|
||||
await esbuild.build({
|
||||
entryPoints: [path.join(packageRoot, "src/ui/index.tsx")],
|
||||
outfile: path.join(packageRoot, "dist/ui/index.js"),
|
||||
bundle: true,
|
||||
format: "esm",
|
||||
platform: "browser",
|
||||
target: ["es2022"],
|
||||
sourcemap: true,
|
||||
external: [
|
||||
"react",
|
||||
"react-dom",
|
||||
"react/jsx-runtime",
|
||||
"@paperclipai/plugin-sdk/ui",
|
||||
],
|
||||
logLevel: "info",
|
||||
});
|
||||
@@ -0,0 +1,112 @@
|
||||
import type { PluginLauncherRegistration } from "@paperclipai/plugin-sdk";
|
||||
|
||||
export const PLUGIN_ID = "paperclip-kitchen-sink-example";
|
||||
export const PLUGIN_VERSION = "0.1.0";
|
||||
|
||||
export const SLOT_IDS = {
|
||||
page: "kitchen-sink-page",
|
||||
settingsPage: "kitchen-sink-settings-page",
|
||||
dashboardWidget: "kitchen-sink-dashboard-widget",
|
||||
sidebar: "kitchen-sink-sidebar-link",
|
||||
sidebarPanel: "kitchen-sink-sidebar-panel",
|
||||
projectSidebarItem: "kitchen-sink-project-link",
|
||||
projectTab: "kitchen-sink-project-tab",
|
||||
issueTab: "kitchen-sink-issue-tab",
|
||||
taskDetailView: "kitchen-sink-task-detail",
|
||||
toolbarButton: "kitchen-sink-toolbar-action",
|
||||
contextMenuItem: "kitchen-sink-context-action",
|
||||
commentAnnotation: "kitchen-sink-comment-annotation",
|
||||
commentContextMenuItem: "kitchen-sink-comment-action",
|
||||
} as const;
|
||||
|
||||
export const EXPORT_NAMES = {
|
||||
page: "KitchenSinkPage",
|
||||
settingsPage: "KitchenSinkSettingsPage",
|
||||
dashboardWidget: "KitchenSinkDashboardWidget",
|
||||
sidebar: "KitchenSinkSidebarLink",
|
||||
sidebarPanel: "KitchenSinkSidebarPanel",
|
||||
projectSidebarItem: "KitchenSinkProjectSidebarItem",
|
||||
projectTab: "KitchenSinkProjectTab",
|
||||
issueTab: "KitchenSinkIssueTab",
|
||||
taskDetailView: "KitchenSinkTaskDetailView",
|
||||
toolbarButton: "KitchenSinkToolbarButton",
|
||||
contextMenuItem: "KitchenSinkContextMenuItem",
|
||||
commentAnnotation: "KitchenSinkCommentAnnotation",
|
||||
commentContextMenuItem: "KitchenSinkCommentContextMenuItem",
|
||||
launcherModal: "KitchenSinkLauncherModal",
|
||||
} as const;
|
||||
|
||||
export const JOB_KEYS = {
|
||||
heartbeat: "demo-heartbeat",
|
||||
} as const;
|
||||
|
||||
export const WEBHOOK_KEYS = {
|
||||
demo: "demo-ingest",
|
||||
} as const;
|
||||
|
||||
export const TOOL_NAMES = {
|
||||
echo: "echo",
|
||||
companySummary: "company-summary",
|
||||
createIssue: "create-issue",
|
||||
} as const;
|
||||
|
||||
export const STREAM_CHANNELS = {
|
||||
progress: "progress",
|
||||
agentChat: "agent-chat",
|
||||
} as const;
|
||||
|
||||
export const SAFE_COMMANDS = [
|
||||
{
|
||||
key: "pwd",
|
||||
label: "Print workspace path",
|
||||
command: "pwd",
|
||||
args: [] as string[],
|
||||
description: "Prints the current workspace directory.",
|
||||
},
|
||||
{
|
||||
key: "ls",
|
||||
label: "List workspace files",
|
||||
command: "ls",
|
||||
args: ["-la"] as string[],
|
||||
description: "Lists files in the selected workspace.",
|
||||
},
|
||||
{
|
||||
key: "git-status",
|
||||
label: "Git status",
|
||||
command: "git",
|
||||
args: ["status", "--short", "--branch"] as string[],
|
||||
description: "Shows git status for the selected workspace.",
|
||||
},
|
||||
] as const;
|
||||
|
||||
export type SafeCommandKey = (typeof SAFE_COMMANDS)[number]["key"];
|
||||
|
||||
export const DEFAULT_CONFIG = {
|
||||
showSidebarEntry: true,
|
||||
showSidebarPanel: true,
|
||||
showProjectSidebarItem: true,
|
||||
showCommentAnnotation: true,
|
||||
showCommentContextMenuItem: true,
|
||||
enableWorkspaceDemos: true,
|
||||
enableProcessDemos: false,
|
||||
secretRefExample: "",
|
||||
httpDemoUrl: "https://httpbin.org/anything",
|
||||
allowedCommands: SAFE_COMMANDS.map((command) => command.key),
|
||||
workspaceScratchFile: ".paperclip-kitchen-sink-demo.txt",
|
||||
} as const;
|
||||
|
||||
export const RUNTIME_LAUNCHER: PluginLauncherRegistration = {
|
||||
id: "kitchen-sink-runtime-launcher",
|
||||
displayName: "Kitchen Sink Modal",
|
||||
description: "Demonstrates runtime launcher registration from the worker.",
|
||||
placementZone: "toolbarButton",
|
||||
entityTypes: ["project", "issue"],
|
||||
action: {
|
||||
type: "openModal",
|
||||
target: EXPORT_NAMES.launcherModal,
|
||||
},
|
||||
render: {
|
||||
environment: "hostOverlay",
|
||||
bounds: "wide",
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as manifest } from "./manifest.js";
|
||||
export { default as worker } from "./worker.js";
|
||||
@@ -0,0 +1,290 @@
|
||||
import type { PaperclipPluginManifestV1 } from "@paperclipai/plugin-sdk";
|
||||
import {
|
||||
DEFAULT_CONFIG,
|
||||
EXPORT_NAMES,
|
||||
JOB_KEYS,
|
||||
PLUGIN_ID,
|
||||
PLUGIN_VERSION,
|
||||
SLOT_IDS,
|
||||
TOOL_NAMES,
|
||||
WEBHOOK_KEYS,
|
||||
} from "./constants.js";
|
||||
|
||||
const manifest: PaperclipPluginManifestV1 = {
|
||||
id: PLUGIN_ID,
|
||||
apiVersion: 1,
|
||||
version: PLUGIN_VERSION,
|
||||
displayName: "Kitchen Sink (Example)",
|
||||
description: "Reference plugin that demonstrates the current Paperclip plugin API surface, UI surfaces, bridge actions, events, jobs, webhooks, tools, local workspace access, and runtime diagnostics in one place.",
|
||||
author: "Paperclip",
|
||||
categories: ["ui", "automation", "workspace", "connector"],
|
||||
capabilities: [
|
||||
"companies.read",
|
||||
"projects.read",
|
||||
"project.workspaces.read",
|
||||
"issues.read",
|
||||
"issues.create",
|
||||
"issues.update",
|
||||
"issue.comments.read",
|
||||
"issue.comments.create",
|
||||
"agents.read",
|
||||
"agents.pause",
|
||||
"agents.resume",
|
||||
"agents.invoke",
|
||||
"agent.sessions.create",
|
||||
"agent.sessions.list",
|
||||
"agent.sessions.send",
|
||||
"agent.sessions.close",
|
||||
"goals.read",
|
||||
"goals.create",
|
||||
"goals.update",
|
||||
"assets.write",
|
||||
"assets.read",
|
||||
"activity.log.write",
|
||||
"metrics.write",
|
||||
"plugin.state.read",
|
||||
"plugin.state.write",
|
||||
"events.subscribe",
|
||||
"events.emit",
|
||||
"jobs.schedule",
|
||||
"webhooks.receive",
|
||||
"http.outbound",
|
||||
"secrets.read-ref",
|
||||
"agent.tools.register",
|
||||
"instance.settings.register",
|
||||
"ui.sidebar.register",
|
||||
"ui.page.register",
|
||||
"ui.detailTab.register",
|
||||
"ui.dashboardWidget.register",
|
||||
"ui.commentAnnotation.register",
|
||||
"ui.action.register",
|
||||
],
|
||||
entrypoints: {
|
||||
worker: "./dist/worker.js",
|
||||
ui: "./dist/ui",
|
||||
},
|
||||
instanceConfigSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
showSidebarEntry: {
|
||||
type: "boolean",
|
||||
title: "Show Sidebar Entry",
|
||||
default: DEFAULT_CONFIG.showSidebarEntry,
|
||||
},
|
||||
showSidebarPanel: {
|
||||
type: "boolean",
|
||||
title: "Show Sidebar Panel",
|
||||
default: DEFAULT_CONFIG.showSidebarPanel,
|
||||
},
|
||||
showProjectSidebarItem: {
|
||||
type: "boolean",
|
||||
title: "Show Project Sidebar Item",
|
||||
default: DEFAULT_CONFIG.showProjectSidebarItem,
|
||||
},
|
||||
showCommentAnnotation: {
|
||||
type: "boolean",
|
||||
title: "Show Comment Annotation",
|
||||
default: DEFAULT_CONFIG.showCommentAnnotation,
|
||||
},
|
||||
showCommentContextMenuItem: {
|
||||
type: "boolean",
|
||||
title: "Show Comment Action",
|
||||
default: DEFAULT_CONFIG.showCommentContextMenuItem,
|
||||
},
|
||||
enableWorkspaceDemos: {
|
||||
type: "boolean",
|
||||
title: "Enable Workspace Demos",
|
||||
default: DEFAULT_CONFIG.enableWorkspaceDemos,
|
||||
},
|
||||
enableProcessDemos: {
|
||||
type: "boolean",
|
||||
title: "Enable Process Demos",
|
||||
default: DEFAULT_CONFIG.enableProcessDemos,
|
||||
description: "Allows curated local child-process demos in project workspaces.",
|
||||
},
|
||||
secretRefExample: {
|
||||
type: "string",
|
||||
title: "Secret Reference Example",
|
||||
default: DEFAULT_CONFIG.secretRefExample,
|
||||
},
|
||||
httpDemoUrl: {
|
||||
type: "string",
|
||||
title: "HTTP Demo URL",
|
||||
default: DEFAULT_CONFIG.httpDemoUrl,
|
||||
},
|
||||
allowedCommands: {
|
||||
type: "array",
|
||||
title: "Allowed Process Commands",
|
||||
items: {
|
||||
type: "string",
|
||||
enum: DEFAULT_CONFIG.allowedCommands,
|
||||
},
|
||||
default: DEFAULT_CONFIG.allowedCommands,
|
||||
},
|
||||
workspaceScratchFile: {
|
||||
type: "string",
|
||||
title: "Workspace Scratch File",
|
||||
default: DEFAULT_CONFIG.workspaceScratchFile,
|
||||
},
|
||||
},
|
||||
},
|
||||
jobs: [
|
||||
{
|
||||
jobKey: JOB_KEYS.heartbeat,
|
||||
displayName: "Demo Heartbeat",
|
||||
description: "Periodic demo job that records plugin runtime activity.",
|
||||
schedule: "*/15 * * * *",
|
||||
},
|
||||
],
|
||||
webhooks: [
|
||||
{
|
||||
endpointKey: WEBHOOK_KEYS.demo,
|
||||
displayName: "Demo Ingest",
|
||||
description: "Accepts arbitrary webhook payloads and records the latest delivery in plugin state.",
|
||||
},
|
||||
],
|
||||
tools: [
|
||||
{
|
||||
name: TOOL_NAMES.echo,
|
||||
displayName: "Kitchen Sink Echo",
|
||||
description: "Returns the provided message and the current run context.",
|
||||
parametersSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
message: { type: "string" },
|
||||
},
|
||||
required: ["message"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: TOOL_NAMES.companySummary,
|
||||
displayName: "Kitchen Sink Company Summary",
|
||||
description: "Summarizes the current company using the Paperclip domain APIs.",
|
||||
parametersSchema: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: TOOL_NAMES.createIssue,
|
||||
displayName: "Kitchen Sink Create Issue",
|
||||
description: "Creates an issue in the current project from an agent tool call.",
|
||||
parametersSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
title: { type: "string" },
|
||||
description: { type: "string" },
|
||||
},
|
||||
required: ["title"],
|
||||
},
|
||||
},
|
||||
],
|
||||
ui: {
|
||||
slots: [
|
||||
{
|
||||
type: "page",
|
||||
id: SLOT_IDS.page,
|
||||
displayName: "Kitchen Sink",
|
||||
exportName: EXPORT_NAMES.page,
|
||||
},
|
||||
{
|
||||
type: "settingsPage",
|
||||
id: SLOT_IDS.settingsPage,
|
||||
displayName: "Kitchen Sink Settings",
|
||||
exportName: EXPORT_NAMES.settingsPage,
|
||||
},
|
||||
{
|
||||
type: "dashboardWidget",
|
||||
id: SLOT_IDS.dashboardWidget,
|
||||
displayName: "Kitchen Sink",
|
||||
exportName: EXPORT_NAMES.dashboardWidget,
|
||||
},
|
||||
{
|
||||
type: "sidebar",
|
||||
id: SLOT_IDS.sidebar,
|
||||
displayName: "Kitchen Sink",
|
||||
exportName: EXPORT_NAMES.sidebar,
|
||||
},
|
||||
{
|
||||
type: "sidebarPanel",
|
||||
id: SLOT_IDS.sidebarPanel,
|
||||
displayName: "Kitchen Sink Panel",
|
||||
exportName: EXPORT_NAMES.sidebarPanel,
|
||||
},
|
||||
{
|
||||
type: "projectSidebarItem",
|
||||
id: SLOT_IDS.projectSidebarItem,
|
||||
displayName: "Kitchen Sink",
|
||||
exportName: EXPORT_NAMES.projectSidebarItem,
|
||||
entityTypes: ["project"],
|
||||
},
|
||||
{
|
||||
type: "detailTab",
|
||||
id: SLOT_IDS.projectTab,
|
||||
displayName: "Kitchen Sink",
|
||||
exportName: EXPORT_NAMES.projectTab,
|
||||
entityTypes: ["project"],
|
||||
},
|
||||
{
|
||||
type: "detailTab",
|
||||
id: SLOT_IDS.issueTab,
|
||||
displayName: "Kitchen Sink",
|
||||
exportName: EXPORT_NAMES.issueTab,
|
||||
entityTypes: ["issue"],
|
||||
},
|
||||
{
|
||||
type: "taskDetailView",
|
||||
id: SLOT_IDS.taskDetailView,
|
||||
displayName: "Kitchen Sink Task View",
|
||||
exportName: EXPORT_NAMES.taskDetailView,
|
||||
entityTypes: ["issue"],
|
||||
},
|
||||
{
|
||||
type: "toolbarButton",
|
||||
id: SLOT_IDS.toolbarButton,
|
||||
displayName: "Kitchen Sink Action",
|
||||
exportName: EXPORT_NAMES.toolbarButton,
|
||||
entityTypes: ["project", "issue"],
|
||||
},
|
||||
{
|
||||
type: "contextMenuItem",
|
||||
id: SLOT_IDS.contextMenuItem,
|
||||
displayName: "Kitchen Sink Context",
|
||||
exportName: EXPORT_NAMES.contextMenuItem,
|
||||
entityTypes: ["project", "issue"],
|
||||
},
|
||||
{
|
||||
type: "commentAnnotation",
|
||||
id: SLOT_IDS.commentAnnotation,
|
||||
displayName: "Kitchen Sink Comment Annotation",
|
||||
exportName: EXPORT_NAMES.commentAnnotation,
|
||||
entityTypes: ["comment"],
|
||||
},
|
||||
{
|
||||
type: "commentContextMenuItem",
|
||||
id: SLOT_IDS.commentContextMenuItem,
|
||||
displayName: "Kitchen Sink Comment Action",
|
||||
exportName: EXPORT_NAMES.commentContextMenuItem,
|
||||
entityTypes: ["comment"],
|
||||
},
|
||||
],
|
||||
launchers: [
|
||||
{
|
||||
id: "kitchen-sink-launcher",
|
||||
displayName: "Kitchen Sink Modal",
|
||||
placementZone: "toolbarButton",
|
||||
entityTypes: ["project", "issue"],
|
||||
action: {
|
||||
type: "openModal",
|
||||
target: EXPORT_NAMES.launcherModal,
|
||||
},
|
||||
render: {
|
||||
environment: "hostOverlay",
|
||||
bounds: "wide",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default manifest;
|
||||
File diff suppressed because it is too large
Load Diff
1055
packages/plugins/examples/plugin-kitchen-sink-example/src/worker.ts
Normal file
1055
packages/plugins/examples/plugin-kitchen-sink-example/src/worker.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"lib": ["ES2023", "DOM"],
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -131,6 +131,14 @@ const BUNDLED_PLUGIN_EXAMPLES: AvailablePluginExample[] = [
|
||||
localPath: "packages/plugins/examples/plugin-file-browser-example",
|
||||
tag: "example",
|
||||
},
|
||||
{
|
||||
packageName: "@paperclipai/plugin-kitchen-sink-example",
|
||||
pluginKey: "paperclip-kitchen-sink-example",
|
||||
displayName: "Kitchen Sink (Example)",
|
||||
description: "Reference plugin that demonstrates the current Paperclip plugin API surface, bridge flows, UI extension surfaces, jobs, webhooks, tools, streams, and trusted local workspace/process demos.",
|
||||
localPath: "packages/plugins/examples/plugin-kitchen-sink-example",
|
||||
tag: "example",
|
||||
},
|
||||
];
|
||||
|
||||
function listBundledPluginExamples(): AvailablePluginExample[] {
|
||||
|
||||
@@ -10,6 +10,7 @@ import { MarkdownEditor, type MarkdownEditorRef, type MentionOption } from "./Ma
|
||||
import { StatusBadge } from "./StatusBadge";
|
||||
import { AgentIcon } from "./AgentIconPicker";
|
||||
import { formatDateTime } from "../lib/utils";
|
||||
import { PluginSlotOutlet } from "@/plugins/slots";
|
||||
|
||||
interface CommentWithRunMeta extends IssueComment {
|
||||
runId?: string | null;
|
||||
@@ -32,6 +33,8 @@ interface CommentReassignment {
|
||||
interface CommentThreadProps {
|
||||
comments: CommentWithRunMeta[];
|
||||
linkedRuns?: LinkedRunItem[];
|
||||
companyId?: string | null;
|
||||
projectId?: string | null;
|
||||
onAdd: (body: string, reopen?: boolean, reassignment?: CommentReassignment) => Promise<void>;
|
||||
issueStatus?: string;
|
||||
agentMap?: Map<string, Agent>;
|
||||
@@ -118,10 +121,14 @@ type TimelineItem =
|
||||
const TimelineList = memo(function TimelineList({
|
||||
timeline,
|
||||
agentMap,
|
||||
companyId,
|
||||
projectId,
|
||||
highlightCommentId,
|
||||
}: {
|
||||
timeline: TimelineItem[];
|
||||
agentMap?: Map<string, Agent>;
|
||||
companyId?: string | null;
|
||||
projectId?: string | null;
|
||||
highlightCommentId?: string | null;
|
||||
}) {
|
||||
if (timeline.length === 0) {
|
||||
@@ -180,6 +187,22 @@ const TimelineList = memo(function TimelineList({
|
||||
<Identity name="You" size="sm" />
|
||||
)}
|
||||
<span className="flex items-center gap-1.5">
|
||||
{companyId ? (
|
||||
<PluginSlotOutlet
|
||||
slotTypes={["commentContextMenuItem"]}
|
||||
entityType="comment"
|
||||
context={{
|
||||
companyId,
|
||||
projectId: projectId ?? null,
|
||||
entityId: comment.id,
|
||||
entityType: "comment",
|
||||
parentEntityId: comment.issueId,
|
||||
}}
|
||||
className="flex flex-wrap items-center gap-1.5"
|
||||
itemClassName="inline-flex"
|
||||
missingBehavior="placeholder"
|
||||
/>
|
||||
) : null}
|
||||
<a
|
||||
href={`#comment-${comment.id}`}
|
||||
className="text-xs text-muted-foreground hover:text-foreground hover:underline transition-colors"
|
||||
@@ -190,6 +213,24 @@ const TimelineList = memo(function TimelineList({
|
||||
</span>
|
||||
</div>
|
||||
<MarkdownBody className="text-sm">{comment.body}</MarkdownBody>
|
||||
{companyId ? (
|
||||
<div className="mt-2 space-y-2">
|
||||
<PluginSlotOutlet
|
||||
slotTypes={["commentAnnotation"]}
|
||||
entityType="comment"
|
||||
context={{
|
||||
companyId,
|
||||
projectId: projectId ?? null,
|
||||
entityId: comment.id,
|
||||
entityType: "comment",
|
||||
parentEntityId: comment.issueId,
|
||||
}}
|
||||
className="space-y-2"
|
||||
itemClassName="rounded-md"
|
||||
missingBehavior="placeholder"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{comment.runId && (
|
||||
<div className="mt-2 pt-2 border-t border-border/60">
|
||||
{comment.runAgentId ? (
|
||||
@@ -216,6 +257,8 @@ const TimelineList = memo(function TimelineList({
|
||||
export function CommentThread({
|
||||
comments,
|
||||
linkedRuns = [],
|
||||
companyId,
|
||||
projectId,
|
||||
onAdd,
|
||||
issueStatus,
|
||||
agentMap,
|
||||
@@ -351,7 +394,13 @@ export function CommentThread({
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-semibold">Comments & Runs ({timeline.length})</h3>
|
||||
|
||||
<TimelineList timeline={timeline} agentMap={agentMap} highlightCommentId={highlightCommentId} />
|
||||
<TimelineList
|
||||
timeline={timeline}
|
||||
agentMap={agentMap}
|
||||
companyId={companyId}
|
||||
projectId={projectId}
|
||||
highlightCommentId={highlightCommentId}
|
||||
/>
|
||||
|
||||
{liveRunSlot}
|
||||
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Clock3, Puzzle, Settings } from "lucide-react";
|
||||
import { NavLink } from "@/lib/router";
|
||||
import { pluginsApi } from "@/api/plugins";
|
||||
import { queryKeys } from "@/lib/queryKeys";
|
||||
import { SidebarNavItem } from "./SidebarNavItem";
|
||||
|
||||
export function InstanceSidebar() {
|
||||
const { data: plugins } = useQuery({
|
||||
queryKey: queryKeys.plugins.all,
|
||||
queryFn: () => pluginsApi.list(),
|
||||
});
|
||||
|
||||
return (
|
||||
<aside className="w-60 h-full min-h-0 border-r border-border bg-background flex flex-col">
|
||||
<div className="flex items-center gap-2 px-3 h-12 shrink-0">
|
||||
@@ -15,6 +24,26 @@ export function InstanceSidebar() {
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<SidebarNavItem to="/instance/settings/heartbeats" label="Heartbeats" icon={Clock3} end />
|
||||
<SidebarNavItem to="/instance/settings/plugins" label="Plugins" icon={Puzzle} />
|
||||
{(plugins ?? []).length > 0 ? (
|
||||
<div className="ml-4 mt-1 flex flex-col gap-0.5 border-l border-border/70 pl-3">
|
||||
{(plugins ?? []).map((plugin) => (
|
||||
<NavLink
|
||||
key={plugin.id}
|
||||
to={`/instance/settings/plugins/${plugin.id}`}
|
||||
className={({ isActive }) =>
|
||||
[
|
||||
"rounded-md px-2 py-1.5 text-xs transition-colors",
|
||||
isActive
|
||||
? "bg-accent text-foreground"
|
||||
: "text-muted-foreground hover:bg-accent/50 hover:text-foreground",
|
||||
].join(" ")
|
||||
}
|
||||
>
|
||||
{plugin.manifestJson.displayName ?? plugin.packageName}
|
||||
</NavLink>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
@@ -21,6 +21,7 @@ import { heartbeatsApi } from "../api/heartbeats";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import { useInboxBadge } from "../hooks/useInboxBadge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { PluginSlotOutlet } from "@/plugins/slots";
|
||||
|
||||
export function Sidebar() {
|
||||
const { openNewIssue } = useDialog();
|
||||
@@ -38,6 +39,11 @@ export function Sidebar() {
|
||||
document.dispatchEvent(new KeyboardEvent("keydown", { key: "k", metaKey: true }));
|
||||
}
|
||||
|
||||
const pluginContext = {
|
||||
companyId: selectedCompanyId,
|
||||
companyPrefix: selectedCompany?.issuePrefix ?? null,
|
||||
};
|
||||
|
||||
return (
|
||||
<aside className="w-60 h-full min-h-0 border-r border-border bg-background flex flex-col">
|
||||
{/* Top bar: Company name (bold) + Search — aligned with top sections (no visible border) */}
|
||||
@@ -80,6 +86,13 @@ export function Sidebar() {
|
||||
badgeTone={inboxBadge.failedRuns > 0 ? "danger" : "default"}
|
||||
alert={inboxBadge.failedRuns > 0}
|
||||
/>
|
||||
<PluginSlotOutlet
|
||||
slotTypes={["sidebar"]}
|
||||
context={pluginContext}
|
||||
className="flex flex-col gap-0.5"
|
||||
itemClassName="text-[13px] font-medium"
|
||||
missingBehavior="placeholder"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SidebarSection label="Work">
|
||||
@@ -97,6 +110,14 @@ export function Sidebar() {
|
||||
<SidebarNavItem to="/activity" label="Activity" icon={History} />
|
||||
<SidebarNavItem to="/company/settings" label="Settings" icon={Settings} />
|
||||
</SidebarSection>
|
||||
|
||||
<PluginSlotOutlet
|
||||
slotTypes={["sidebarPanel"]}
|
||||
context={pluginContext}
|
||||
className="flex flex-col gap-3"
|
||||
itemClassName="rounded-lg border border-border p-3"
|
||||
missingBehavior="placeholder"
|
||||
/>
|
||||
</nav>
|
||||
</aside>
|
||||
);
|
||||
|
||||
@@ -15,6 +15,7 @@ import { ToastProvider } from "./context/ToastContext";
|
||||
import { ThemeProvider } from "./context/ThemeContext";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import { initPluginBridge } from "./plugins/bridge-init";
|
||||
import { PluginLauncherProvider } from "./plugins/launchers";
|
||||
import "@mdxeditor/editor/style.css";
|
||||
import "./index.css";
|
||||
|
||||
@@ -47,9 +48,11 @@ createRoot(document.getElementById("root")!).render(
|
||||
<BreadcrumbProvider>
|
||||
<SidebarProvider>
|
||||
<PanelProvider>
|
||||
<DialogProvider>
|
||||
<App />
|
||||
</DialogProvider>
|
||||
<PluginLauncherProvider>
|
||||
<DialogProvider>
|
||||
<App />
|
||||
</DialogProvider>
|
||||
</PluginLauncherProvider>
|
||||
</PanelProvider>
|
||||
</SidebarProvider>
|
||||
</BreadcrumbProvider>
|
||||
|
||||
@@ -24,6 +24,8 @@ import { StatusIcon } from "../components/StatusIcon";
|
||||
import { PriorityIcon } from "../components/PriorityIcon";
|
||||
import { StatusBadge } from "../components/StatusBadge";
|
||||
import { Identity } from "../components/Identity";
|
||||
import { PluginSlotMount, PluginSlotOutlet, usePluginSlots } from "@/plugins/slots";
|
||||
import { PluginLauncherOutlet } from "@/plugins/launchers";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -168,6 +170,7 @@ export function IssueDetail() {
|
||||
queryFn: () => issuesApi.get(issueId!),
|
||||
enabled: !!issueId,
|
||||
});
|
||||
const resolvedCompanyId = issue?.companyId ?? selectedCompanyId;
|
||||
|
||||
const { data: comments } = useQuery({
|
||||
queryKey: queryKeys.issues.comments(issueId!),
|
||||
@@ -257,6 +260,21 @@ export function IssueDetail() {
|
||||
companyId: selectedCompanyId,
|
||||
userId: currentUserId,
|
||||
});
|
||||
const { slots: issuePluginDetailSlots } = usePluginSlots({
|
||||
slotTypes: ["detailTab"],
|
||||
entityType: "issue",
|
||||
companyId: resolvedCompanyId,
|
||||
enabled: !!resolvedCompanyId,
|
||||
});
|
||||
const issuePluginTabItems = useMemo(
|
||||
() => issuePluginDetailSlots.map((slot) => ({
|
||||
value: `plugin:${slot.pluginKey}:${slot.id}`,
|
||||
label: slot.displayName,
|
||||
slot,
|
||||
})),
|
||||
[issuePluginDetailSlots],
|
||||
);
|
||||
const activePluginTab = issuePluginTabItems.find((item) => item.value === detailTab) ?? null;
|
||||
|
||||
const agentMap = useMemo(() => {
|
||||
const map = new Map<string, Agent>();
|
||||
@@ -678,6 +696,47 @@ export function IssueDetail() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<PluginSlotOutlet
|
||||
slotTypes={["toolbarButton", "contextMenuItem"]}
|
||||
entityType="issue"
|
||||
context={{
|
||||
companyId: issue.companyId,
|
||||
projectId: issue.projectId ?? null,
|
||||
entityId: issue.id,
|
||||
entityType: "issue",
|
||||
}}
|
||||
className="flex flex-wrap gap-2"
|
||||
itemClassName="inline-flex"
|
||||
missingBehavior="placeholder"
|
||||
/>
|
||||
|
||||
<PluginLauncherOutlet
|
||||
placementZones={["toolbarButton"]}
|
||||
entityType="issue"
|
||||
context={{
|
||||
companyId: issue.companyId,
|
||||
projectId: issue.projectId ?? null,
|
||||
entityId: issue.id,
|
||||
entityType: "issue",
|
||||
}}
|
||||
className="flex flex-wrap gap-2"
|
||||
itemClassName="inline-flex"
|
||||
/>
|
||||
|
||||
<PluginSlotOutlet
|
||||
slotTypes={["taskDetailView"]}
|
||||
entityType="issue"
|
||||
context={{
|
||||
companyId: issue.companyId,
|
||||
projectId: issue.projectId ?? null,
|
||||
entityId: issue.id,
|
||||
entityType: "issue",
|
||||
}}
|
||||
className="space-y-3"
|
||||
itemClassName="rounded-lg border border-border p-3"
|
||||
missingBehavior="placeholder"
|
||||
/>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<h3 className="text-sm font-medium text-muted-foreground">Attachments</h3>
|
||||
@@ -766,12 +825,19 @@ export function IssueDetail() {
|
||||
<ActivityIcon className="h-3.5 w-3.5" />
|
||||
Activity
|
||||
</TabsTrigger>
|
||||
{issuePluginTabItems.map((item) => (
|
||||
<TabsTrigger key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="comments">
|
||||
<CommentThread
|
||||
comments={commentsWithRunMeta}
|
||||
linkedRuns={timelineRuns}
|
||||
companyId={issue.companyId}
|
||||
projectId={issue.projectId}
|
||||
issueStatus={issue.status}
|
||||
agentMap={agentMap}
|
||||
draftKey={`paperclip:issue-comment-draft:${issue.id}`}
|
||||
@@ -844,6 +910,21 @@ export function IssueDetail() {
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
{activePluginTab && (
|
||||
<TabsContent value={activePluginTab.value}>
|
||||
<PluginSlotMount
|
||||
slot={activePluginTab.slot}
|
||||
context={{
|
||||
companyId: issue.companyId,
|
||||
projectId: issue.projectId ?? null,
|
||||
entityId: issue.id,
|
||||
entityType: "issue",
|
||||
}}
|
||||
missingBehavior="placeholder"
|
||||
/>
|
||||
</TabsContent>
|
||||
)}
|
||||
</Tabs>
|
||||
|
||||
{linkedApprovals && linkedApprovals.length > 0 && (
|
||||
|
||||
@@ -19,7 +19,8 @@ import { PageSkeleton } from "../components/PageSkeleton";
|
||||
import { PageTabBar } from "../components/PageTabBar";
|
||||
import { projectRouteRef, cn } from "../lib/utils";
|
||||
import { Tabs } from "@/components/ui/tabs";
|
||||
import { PluginSlotMount, usePluginSlots } from "@/plugins/slots";
|
||||
import { PluginLauncherOutlet } from "@/plugins/launchers";
|
||||
import { PluginSlotMount, PluginSlotOutlet, usePluginSlots } from "@/plugins/slots";
|
||||
|
||||
/* ── Top-level tab types ── */
|
||||
|
||||
@@ -405,6 +406,37 @@ export function ProjectDetail() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<PluginSlotOutlet
|
||||
slotTypes={["toolbarButton", "contextMenuItem"]}
|
||||
entityType="project"
|
||||
context={{
|
||||
companyId: resolvedCompanyId ?? null,
|
||||
companyPrefix: companyPrefix ?? null,
|
||||
projectId: project.id,
|
||||
projectRef: canonicalProjectRef,
|
||||
entityId: project.id,
|
||||
entityType: "project",
|
||||
}}
|
||||
className="flex flex-wrap gap-2"
|
||||
itemClassName="inline-flex"
|
||||
missingBehavior="placeholder"
|
||||
/>
|
||||
|
||||
<PluginLauncherOutlet
|
||||
placementZones={["toolbarButton"]}
|
||||
entityType="project"
|
||||
context={{
|
||||
companyId: resolvedCompanyId ?? null,
|
||||
companyPrefix: companyPrefix ?? null,
|
||||
projectId: project.id,
|
||||
projectRef: canonicalProjectRef,
|
||||
entityId: project.id,
|
||||
entityType: "project",
|
||||
}}
|
||||
className="flex flex-wrap gap-2"
|
||||
itemClassName="inline-flex"
|
||||
/>
|
||||
|
||||
<Tabs value={activeTab ?? "list"} onValueChange={(value) => handleTabChange(value as ProjectTab)}>
|
||||
<PageTabBar
|
||||
items={[
|
||||
|
||||
@@ -257,11 +257,11 @@ function getShimBlobUrl(specifier: "react" | "react-dom" | "react-dom/client" |
|
||||
case "sdk-ui":
|
||||
source = `
|
||||
const SDK = globalThis.__paperclipPluginBridge__?.sdkUi ?? {};
|
||||
const { usePluginData, usePluginAction, useHostContext,
|
||||
const { usePluginData, usePluginAction, useHostContext, usePluginStream,
|
||||
MetricCard, StatusBadge, DataTable, TimeseriesChart,
|
||||
MarkdownBlock, KeyValueList, ActionBar, LogView, JsonTree,
|
||||
Spinner, ErrorBoundary } = SDK;
|
||||
export { usePluginData, usePluginAction, useHostContext,
|
||||
export { usePluginData, usePluginAction, useHostContext, usePluginStream,
|
||||
MetricCard, StatusBadge, DataTable, TimeseriesChart,
|
||||
MarkdownBlock, KeyValueList, ActionBar, LogView, JsonTree,
|
||||
Spinner, ErrorBoundary };
|
||||
|
||||
Reference in New Issue
Block a user