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",
|
localPath: "packages/plugins/examples/plugin-file-browser-example",
|
||||||
tag: "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[] {
|
function listBundledPluginExamples(): AvailablePluginExample[] {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { MarkdownEditor, type MarkdownEditorRef, type MentionOption } from "./Ma
|
|||||||
import { StatusBadge } from "./StatusBadge";
|
import { StatusBadge } from "./StatusBadge";
|
||||||
import { AgentIcon } from "./AgentIconPicker";
|
import { AgentIcon } from "./AgentIconPicker";
|
||||||
import { formatDateTime } from "../lib/utils";
|
import { formatDateTime } from "../lib/utils";
|
||||||
|
import { PluginSlotOutlet } from "@/plugins/slots";
|
||||||
|
|
||||||
interface CommentWithRunMeta extends IssueComment {
|
interface CommentWithRunMeta extends IssueComment {
|
||||||
runId?: string | null;
|
runId?: string | null;
|
||||||
@@ -32,6 +33,8 @@ interface CommentReassignment {
|
|||||||
interface CommentThreadProps {
|
interface CommentThreadProps {
|
||||||
comments: CommentWithRunMeta[];
|
comments: CommentWithRunMeta[];
|
||||||
linkedRuns?: LinkedRunItem[];
|
linkedRuns?: LinkedRunItem[];
|
||||||
|
companyId?: string | null;
|
||||||
|
projectId?: string | null;
|
||||||
onAdd: (body: string, reopen?: boolean, reassignment?: CommentReassignment) => Promise<void>;
|
onAdd: (body: string, reopen?: boolean, reassignment?: CommentReassignment) => Promise<void>;
|
||||||
issueStatus?: string;
|
issueStatus?: string;
|
||||||
agentMap?: Map<string, Agent>;
|
agentMap?: Map<string, Agent>;
|
||||||
@@ -118,10 +121,14 @@ type TimelineItem =
|
|||||||
const TimelineList = memo(function TimelineList({
|
const TimelineList = memo(function TimelineList({
|
||||||
timeline,
|
timeline,
|
||||||
agentMap,
|
agentMap,
|
||||||
|
companyId,
|
||||||
|
projectId,
|
||||||
highlightCommentId,
|
highlightCommentId,
|
||||||
}: {
|
}: {
|
||||||
timeline: TimelineItem[];
|
timeline: TimelineItem[];
|
||||||
agentMap?: Map<string, Agent>;
|
agentMap?: Map<string, Agent>;
|
||||||
|
companyId?: string | null;
|
||||||
|
projectId?: string | null;
|
||||||
highlightCommentId?: string | null;
|
highlightCommentId?: string | null;
|
||||||
}) {
|
}) {
|
||||||
if (timeline.length === 0) {
|
if (timeline.length === 0) {
|
||||||
@@ -180,6 +187,22 @@ const TimelineList = memo(function TimelineList({
|
|||||||
<Identity name="You" size="sm" />
|
<Identity name="You" size="sm" />
|
||||||
)}
|
)}
|
||||||
<span className="flex items-center gap-1.5">
|
<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
|
<a
|
||||||
href={`#comment-${comment.id}`}
|
href={`#comment-${comment.id}`}
|
||||||
className="text-xs text-muted-foreground hover:text-foreground hover:underline transition-colors"
|
className="text-xs text-muted-foreground hover:text-foreground hover:underline transition-colors"
|
||||||
@@ -190,6 +213,24 @@ const TimelineList = memo(function TimelineList({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<MarkdownBody className="text-sm">{comment.body}</MarkdownBody>
|
<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 && (
|
{comment.runId && (
|
||||||
<div className="mt-2 pt-2 border-t border-border/60">
|
<div className="mt-2 pt-2 border-t border-border/60">
|
||||||
{comment.runAgentId ? (
|
{comment.runAgentId ? (
|
||||||
@@ -216,6 +257,8 @@ const TimelineList = memo(function TimelineList({
|
|||||||
export function CommentThread({
|
export function CommentThread({
|
||||||
comments,
|
comments,
|
||||||
linkedRuns = [],
|
linkedRuns = [],
|
||||||
|
companyId,
|
||||||
|
projectId,
|
||||||
onAdd,
|
onAdd,
|
||||||
issueStatus,
|
issueStatus,
|
||||||
agentMap,
|
agentMap,
|
||||||
@@ -351,7 +394,13 @@ export function CommentThread({
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h3 className="text-sm font-semibold">Comments & Runs ({timeline.length})</h3>
|
<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}
|
{liveRunSlot}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { Clock3, Puzzle, Settings } from "lucide-react";
|
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";
|
import { SidebarNavItem } from "./SidebarNavItem";
|
||||||
|
|
||||||
export function InstanceSidebar() {
|
export function InstanceSidebar() {
|
||||||
|
const { data: plugins } = useQuery({
|
||||||
|
queryKey: queryKeys.plugins.all,
|
||||||
|
queryFn: () => pluginsApi.list(),
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="w-60 h-full min-h-0 border-r border-border bg-background flex flex-col">
|
<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">
|
<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">
|
<div className="flex flex-col gap-0.5">
|
||||||
<SidebarNavItem to="/instance/settings/heartbeats" label="Heartbeats" icon={Clock3} end />
|
<SidebarNavItem to="/instance/settings/heartbeats" label="Heartbeats" icon={Clock3} end />
|
||||||
<SidebarNavItem to="/instance/settings/plugins" label="Plugins" icon={Puzzle} />
|
<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>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { heartbeatsApi } from "../api/heartbeats";
|
|||||||
import { queryKeys } from "../lib/queryKeys";
|
import { queryKeys } from "../lib/queryKeys";
|
||||||
import { useInboxBadge } from "../hooks/useInboxBadge";
|
import { useInboxBadge } from "../hooks/useInboxBadge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { PluginSlotOutlet } from "@/plugins/slots";
|
||||||
|
|
||||||
export function Sidebar() {
|
export function Sidebar() {
|
||||||
const { openNewIssue } = useDialog();
|
const { openNewIssue } = useDialog();
|
||||||
@@ -38,6 +39,11 @@ export function Sidebar() {
|
|||||||
document.dispatchEvent(new KeyboardEvent("keydown", { key: "k", metaKey: true }));
|
document.dispatchEvent(new KeyboardEvent("keydown", { key: "k", metaKey: true }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pluginContext = {
|
||||||
|
companyId: selectedCompanyId,
|
||||||
|
companyPrefix: selectedCompany?.issuePrefix ?? null,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="w-60 h-full min-h-0 border-r border-border bg-background flex flex-col">
|
<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) */}
|
{/* 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"}
|
badgeTone={inboxBadge.failedRuns > 0 ? "danger" : "default"}
|
||||||
alert={inboxBadge.failedRuns > 0}
|
alert={inboxBadge.failedRuns > 0}
|
||||||
/>
|
/>
|
||||||
|
<PluginSlotOutlet
|
||||||
|
slotTypes={["sidebar"]}
|
||||||
|
context={pluginContext}
|
||||||
|
className="flex flex-col gap-0.5"
|
||||||
|
itemClassName="text-[13px] font-medium"
|
||||||
|
missingBehavior="placeholder"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SidebarSection label="Work">
|
<SidebarSection label="Work">
|
||||||
@@ -97,6 +110,14 @@ export function Sidebar() {
|
|||||||
<SidebarNavItem to="/activity" label="Activity" icon={History} />
|
<SidebarNavItem to="/activity" label="Activity" icon={History} />
|
||||||
<SidebarNavItem to="/company/settings" label="Settings" icon={Settings} />
|
<SidebarNavItem to="/company/settings" label="Settings" icon={Settings} />
|
||||||
</SidebarSection>
|
</SidebarSection>
|
||||||
|
|
||||||
|
<PluginSlotOutlet
|
||||||
|
slotTypes={["sidebarPanel"]}
|
||||||
|
context={pluginContext}
|
||||||
|
className="flex flex-col gap-3"
|
||||||
|
itemClassName="rounded-lg border border-border p-3"
|
||||||
|
missingBehavior="placeholder"
|
||||||
|
/>
|
||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { ToastProvider } from "./context/ToastContext";
|
|||||||
import { ThemeProvider } from "./context/ThemeContext";
|
import { ThemeProvider } from "./context/ThemeContext";
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
import { initPluginBridge } from "./plugins/bridge-init";
|
import { initPluginBridge } from "./plugins/bridge-init";
|
||||||
|
import { PluginLauncherProvider } from "./plugins/launchers";
|
||||||
import "@mdxeditor/editor/style.css";
|
import "@mdxeditor/editor/style.css";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
|
|
||||||
@@ -47,9 +48,11 @@ createRoot(document.getElementById("root")!).render(
|
|||||||
<BreadcrumbProvider>
|
<BreadcrumbProvider>
|
||||||
<SidebarProvider>
|
<SidebarProvider>
|
||||||
<PanelProvider>
|
<PanelProvider>
|
||||||
<DialogProvider>
|
<PluginLauncherProvider>
|
||||||
<App />
|
<DialogProvider>
|
||||||
</DialogProvider>
|
<App />
|
||||||
|
</DialogProvider>
|
||||||
|
</PluginLauncherProvider>
|
||||||
</PanelProvider>
|
</PanelProvider>
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
</BreadcrumbProvider>
|
</BreadcrumbProvider>
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ import { StatusIcon } from "../components/StatusIcon";
|
|||||||
import { PriorityIcon } from "../components/PriorityIcon";
|
import { PriorityIcon } from "../components/PriorityIcon";
|
||||||
import { StatusBadge } from "../components/StatusBadge";
|
import { StatusBadge } from "../components/StatusBadge";
|
||||||
import { Identity } from "../components/Identity";
|
import { Identity } from "../components/Identity";
|
||||||
|
import { PluginSlotMount, PluginSlotOutlet, usePluginSlots } from "@/plugins/slots";
|
||||||
|
import { PluginLauncherOutlet } from "@/plugins/launchers";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover";
|
import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -168,6 +170,7 @@ export function IssueDetail() {
|
|||||||
queryFn: () => issuesApi.get(issueId!),
|
queryFn: () => issuesApi.get(issueId!),
|
||||||
enabled: !!issueId,
|
enabled: !!issueId,
|
||||||
});
|
});
|
||||||
|
const resolvedCompanyId = issue?.companyId ?? selectedCompanyId;
|
||||||
|
|
||||||
const { data: comments } = useQuery({
|
const { data: comments } = useQuery({
|
||||||
queryKey: queryKeys.issues.comments(issueId!),
|
queryKey: queryKeys.issues.comments(issueId!),
|
||||||
@@ -257,6 +260,21 @@ export function IssueDetail() {
|
|||||||
companyId: selectedCompanyId,
|
companyId: selectedCompanyId,
|
||||||
userId: currentUserId,
|
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 agentMap = useMemo(() => {
|
||||||
const map = new Map<string, Agent>();
|
const map = new Map<string, Agent>();
|
||||||
@@ -678,6 +696,47 @@ export function IssueDetail() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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="space-y-3">
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<h3 className="text-sm font-medium text-muted-foreground">Attachments</h3>
|
<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" />
|
<ActivityIcon className="h-3.5 w-3.5" />
|
||||||
Activity
|
Activity
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
{issuePluginTabItems.map((item) => (
|
||||||
|
<TabsTrigger key={item.value} value={item.value}>
|
||||||
|
{item.label}
|
||||||
|
</TabsTrigger>
|
||||||
|
))}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="comments">
|
<TabsContent value="comments">
|
||||||
<CommentThread
|
<CommentThread
|
||||||
comments={commentsWithRunMeta}
|
comments={commentsWithRunMeta}
|
||||||
linkedRuns={timelineRuns}
|
linkedRuns={timelineRuns}
|
||||||
|
companyId={issue.companyId}
|
||||||
|
projectId={issue.projectId}
|
||||||
issueStatus={issue.status}
|
issueStatus={issue.status}
|
||||||
agentMap={agentMap}
|
agentMap={agentMap}
|
||||||
draftKey={`paperclip:issue-comment-draft:${issue.id}`}
|
draftKey={`paperclip:issue-comment-draft:${issue.id}`}
|
||||||
@@ -844,6 +910,21 @@ export function IssueDetail() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</TabsContent>
|
</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>
|
</Tabs>
|
||||||
|
|
||||||
{linkedApprovals && linkedApprovals.length > 0 && (
|
{linkedApprovals && linkedApprovals.length > 0 && (
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ import { PageSkeleton } from "../components/PageSkeleton";
|
|||||||
import { PageTabBar } from "../components/PageTabBar";
|
import { PageTabBar } from "../components/PageTabBar";
|
||||||
import { projectRouteRef, cn } from "../lib/utils";
|
import { projectRouteRef, cn } from "../lib/utils";
|
||||||
import { Tabs } from "@/components/ui/tabs";
|
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 ── */
|
/* ── Top-level tab types ── */
|
||||||
|
|
||||||
@@ -405,6 +406,37 @@ export function ProjectDetail() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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)}>
|
<Tabs value={activeTab ?? "list"} onValueChange={(value) => handleTabChange(value as ProjectTab)}>
|
||||||
<PageTabBar
|
<PageTabBar
|
||||||
items={[
|
items={[
|
||||||
|
|||||||
@@ -257,11 +257,11 @@ function getShimBlobUrl(specifier: "react" | "react-dom" | "react-dom/client" |
|
|||||||
case "sdk-ui":
|
case "sdk-ui":
|
||||||
source = `
|
source = `
|
||||||
const SDK = globalThis.__paperclipPluginBridge__?.sdkUi ?? {};
|
const SDK = globalThis.__paperclipPluginBridge__?.sdkUi ?? {};
|
||||||
const { usePluginData, usePluginAction, useHostContext,
|
const { usePluginData, usePluginAction, useHostContext, usePluginStream,
|
||||||
MetricCard, StatusBadge, DataTable, TimeseriesChart,
|
MetricCard, StatusBadge, DataTable, TimeseriesChart,
|
||||||
MarkdownBlock, KeyValueList, ActionBar, LogView, JsonTree,
|
MarkdownBlock, KeyValueList, ActionBar, LogView, JsonTree,
|
||||||
Spinner, ErrorBoundary } = SDK;
|
Spinner, ErrorBoundary } = SDK;
|
||||||
export { usePluginData, usePluginAction, useHostContext,
|
export { usePluginData, usePluginAction, useHostContext, usePluginStream,
|
||||||
MetricCard, StatusBadge, DataTable, TimeseriesChart,
|
MetricCard, StatusBadge, DataTable, TimeseriesChart,
|
||||||
MarkdownBlock, KeyValueList, ActionBar, LogView, JsonTree,
|
MarkdownBlock, KeyValueList, ActionBar, LogView, JsonTree,
|
||||||
Spinner, ErrorBoundary };
|
Spinner, ErrorBoundary };
|
||||||
|
|||||||
Reference in New Issue
Block a user