From 6fa1dd21974a77c4f77a86d4ee3cd4f24015a732 Mon Sep 17 00:00:00 2001 From: Dotta Date: Fri, 13 Mar 2026 23:03:51 -0500 Subject: [PATCH] Add kitchen sink plugin example --- .../2026-03-13-plugin-kitchen-sink-example.md | 699 +++++++ .../plugin-kitchen-sink-example/README.md | 33 + .../plugin-kitchen-sink-example/package.json | 37 + .../scripts/build-ui.mjs | 24 + .../src/constants.ts | 112 ++ .../plugin-kitchen-sink-example/src/index.ts | 2 + .../src/manifest.ts | 290 +++ .../src/ui/index.tsx | 1625 +++++++++++++++++ .../plugin-kitchen-sink-example/src/worker.ts | 1055 +++++++++++ .../plugin-kitchen-sink-example/tsconfig.json | 10 + server/src/routes/plugins.ts | 8 + ui/src/components/CommentThread.tsx | 51 +- ui/src/components/InstanceSidebar.tsx | 29 + ui/src/components/Sidebar.tsx | 21 + ui/src/main.tsx | 9 +- ui/src/pages/IssueDetail.tsx | 81 + ui/src/pages/ProjectDetail.tsx | 34 +- ui/src/plugins/slots.tsx | 4 +- 18 files changed, 4117 insertions(+), 7 deletions(-) create mode 100644 doc/plans/2026-03-13-plugin-kitchen-sink-example.md create mode 100644 packages/plugins/examples/plugin-kitchen-sink-example/README.md create mode 100644 packages/plugins/examples/plugin-kitchen-sink-example/package.json create mode 100644 packages/plugins/examples/plugin-kitchen-sink-example/scripts/build-ui.mjs create mode 100644 packages/plugins/examples/plugin-kitchen-sink-example/src/constants.ts create mode 100644 packages/plugins/examples/plugin-kitchen-sink-example/src/index.ts create mode 100644 packages/plugins/examples/plugin-kitchen-sink-example/src/manifest.ts create mode 100644 packages/plugins/examples/plugin-kitchen-sink-example/src/ui/index.tsx create mode 100644 packages/plugins/examples/plugin-kitchen-sink-example/src/worker.ts create mode 100644 packages/plugins/examples/plugin-kitchen-sink-example/tsconfig.json diff --git a/doc/plans/2026-03-13-plugin-kitchen-sink-example.md b/doc/plans/2026-03-13-plugin-kitchen-sink-example.md new file mode 100644 index 00000000..6a81c5cd --- /dev/null +++ b/doc/plans/2026-03-13-plugin-kitchen-sink-example.md @@ -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. diff --git a/packages/plugins/examples/plugin-kitchen-sink-example/README.md b/packages/plugins/examples/plugin-kitchen-sink-example/README.md new file mode 100644 index 00000000..bfa4ec52 --- /dev/null +++ b/packages/plugins/examples/plugin-kitchen-sink-example/README.md @@ -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. diff --git a/packages/plugins/examples/plugin-kitchen-sink-example/package.json b/packages/plugins/examples/plugin-kitchen-sink-example/package.json new file mode 100644 index 00000000..467ff039 --- /dev/null +++ b/packages/plugins/examples/plugin-kitchen-sink-example/package.json @@ -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" + } +} diff --git a/packages/plugins/examples/plugin-kitchen-sink-example/scripts/build-ui.mjs b/packages/plugins/examples/plugin-kitchen-sink-example/scripts/build-ui.mjs new file mode 100644 index 00000000..5cd75637 --- /dev/null +++ b/packages/plugins/examples/plugin-kitchen-sink-example/scripts/build-ui.mjs @@ -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", +}); diff --git a/packages/plugins/examples/plugin-kitchen-sink-example/src/constants.ts b/packages/plugins/examples/plugin-kitchen-sink-example/src/constants.ts new file mode 100644 index 00000000..4479d4a5 --- /dev/null +++ b/packages/plugins/examples/plugin-kitchen-sink-example/src/constants.ts @@ -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", + }, +}; diff --git a/packages/plugins/examples/plugin-kitchen-sink-example/src/index.ts b/packages/plugins/examples/plugin-kitchen-sink-example/src/index.ts new file mode 100644 index 00000000..f301da5d --- /dev/null +++ b/packages/plugins/examples/plugin-kitchen-sink-example/src/index.ts @@ -0,0 +1,2 @@ +export { default as manifest } from "./manifest.js"; +export { default as worker } from "./worker.js"; diff --git a/packages/plugins/examples/plugin-kitchen-sink-example/src/manifest.ts b/packages/plugins/examples/plugin-kitchen-sink-example/src/manifest.ts new file mode 100644 index 00000000..c7c76109 --- /dev/null +++ b/packages/plugins/examples/plugin-kitchen-sink-example/src/manifest.ts @@ -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; diff --git a/packages/plugins/examples/plugin-kitchen-sink-example/src/ui/index.tsx b/packages/plugins/examples/plugin-kitchen-sink-example/src/ui/index.tsx new file mode 100644 index 00000000..b8fc914c --- /dev/null +++ b/packages/plugins/examples/plugin-kitchen-sink-example/src/ui/index.tsx @@ -0,0 +1,1625 @@ +import { useEffect, useMemo, useState, type CSSProperties, type FormEvent, type ReactNode } from "react"; +import { + useHostContext, + usePluginAction, + usePluginData, + usePluginStream, + type PluginCommentAnnotationProps, + type PluginCommentContextMenuItemProps, + type PluginDetailTabProps, + type PluginPageProps, + type PluginProjectSidebarItemProps, + type PluginSettingsPageProps, + type PluginSidebarProps, + type PluginWidgetProps, +} from "@paperclipai/plugin-sdk/ui"; +import { + DEFAULT_CONFIG, + JOB_KEYS, + PLUGIN_ID, + SAFE_COMMANDS, + SLOT_IDS, + STREAM_CHANNELS, + TOOL_NAMES, + WEBHOOK_KEYS, +} from "../constants.js"; + +type CompanyRecord = { id: string; name: string; issuePrefix?: string | null }; +type ProjectRecord = { id: string; name: string; status?: string; path?: string | null }; +type IssueRecord = { id: string; title: string; status: string; projectId?: string | null }; +type GoalRecord = { id: string; title: string; status: string }; +type AgentRecord = { id: string; name: string; status: string }; + +type OverviewData = { + pluginId: string; + version: string; + capabilities: string[]; + config: Record; + runtimeLaunchers: Array<{ id: string; displayName: string; placementZone: string }>; + recentRecords: Array<{ id: string; source: string; message: string; createdAt: string; level: string; data?: unknown }>; + counts: { + companies: number; + projects: number; + issues: number; + goals: number; + agents: number; + entities: number; + }; + lastJob: unknown; + lastWebhook: unknown; + lastAsset: unknown; + lastProcessResult: unknown; + streamChannels: Record; + safeCommands: Array<{ key: string; label: string; description: string }>; + manifest: { + jobs: Array<{ jobKey: string; displayName: string; schedule?: string }>; + webhooks: Array<{ endpointKey: string; displayName: string }>; + tools: Array<{ name: string; displayName: string; description: string }>; + }; +}; + +type EntityRecord = { + id: string; + entityType: string; + title: string | null; + status: string | null; + scopeKind: string; + scopeId: string | null; + externalId: string | null; + data: unknown; +}; + +type StateValueData = { + scope: { + scopeKind: string; + scopeId?: string; + namespace?: string; + stateKey: string; + }; + value: unknown; +}; + +type PluginConfigData = { + showSidebarEntry?: boolean; + showSidebarPanel?: boolean; + showProjectSidebarItem?: boolean; + showCommentAnnotation?: boolean; + showCommentContextMenuItem?: boolean; + enableWorkspaceDemos?: boolean; + enableProcessDemos?: boolean; +}; + +type CommentContextData = { + commentId: string; + issueId: string; + preview: string; + length: number; + copiedCount: number; +} | null; + +type ProcessResult = { + commandKey: string; + cwd: string; + code: number | null; + stdout: string; + stderr: string; + startedAt: string; + finishedAt: string; +}; + +const layoutStack: CSSProperties = { + display: "grid", + gap: "12px", +}; + +const cardStyle: CSSProperties = { + border: "1px solid var(--border)", + borderRadius: "12px", + padding: "14px", + background: "var(--card, transparent)", +}; + +const subtleCardStyle: CSSProperties = { + border: "1px solid color-mix(in srgb, var(--border) 75%, transparent)", + borderRadius: "10px", + padding: "12px", +}; + +const rowStyle: CSSProperties = { + display: "flex", + flexWrap: "wrap", + alignItems: "center", + gap: "8px", +}; + +const sectionHeaderStyle: CSSProperties = { + display: "flex", + alignItems: "center", + justifyContent: "space-between", + gap: "8px", + marginBottom: "10px", +}; + +const buttonStyle: CSSProperties = { + appearance: "none", + border: "1px solid var(--border)", + borderRadius: "999px", + background: "transparent", + color: "inherit", + padding: "6px 12px", + fontSize: "12px", + cursor: "pointer", +}; + +const primaryButtonStyle: CSSProperties = { + ...buttonStyle, + background: "var(--foreground)", + color: "var(--background)", + borderColor: "var(--foreground)", +}; + +const inputStyle: CSSProperties = { + width: "100%", + border: "1px solid var(--border)", + borderRadius: "8px", + padding: "8px 10px", + background: "transparent", + color: "inherit", + fontSize: "12px", +}; + +const codeStyle: CSSProperties = { + margin: 0, + padding: "10px", + borderRadius: "8px", + border: "1px solid var(--border)", + background: "color-mix(in srgb, var(--muted, #888) 16%, transparent)", + overflowX: "auto", + fontSize: "11px", + lineHeight: 1.45, +}; + +const widgetGridStyle: CSSProperties = { + display: "grid", + gap: "12px", + gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))", +}; + +const widgetStyle: CSSProperties = { + border: "1px solid var(--border)", + borderRadius: "14px", + padding: "14px", + display: "grid", + gap: "8px", + background: "color-mix(in srgb, var(--card, transparent) 72%, transparent)", +}; + +const mutedTextStyle: CSSProperties = { + fontSize: "12px", + opacity: 0.72, + lineHeight: 1.45, +}; + +function hostPath(companyPrefix: string | null | undefined, suffix: string): string { + return companyPrefix ? `/${companyPrefix}${suffix}` : suffix; +} + +function JsonBlock({ value }: { value: unknown }) { + return
{JSON.stringify(value, null, 2)}
; +} + +function Section({ + title, + action, + children, +}: { + title: string; + action?: ReactNode; + children: ReactNode; +}) { + return ( +
+
+ {title} + {action} +
+
{children}
+
+ ); +} + +function Pill({ label }: { label: string }) { + return ( + + {label} + + ); +} + +function MiniWidget({ + title, + eyebrow, + children, +}: { + title: string; + eyebrow?: string; + children: ReactNode; +}) { + return ( +
+ {eyebrow ?
{eyebrow}
: null} + {title} +
{children}
+
+ ); +} + +function MiniList({ + items, + render, + empty, +}: { + items: unknown[]; + render: (item: unknown, index: number) => ReactNode; + empty: string; +}) { + if (items.length === 0) return
{empty}
; + return ( +
+ {items.map((item, index) => ( +
+ {render(item, index)} +
+ ))} +
+ ); +} + +function StatusLine({ label, value }: { label: string; value: ReactNode }) { + return ( +
+ {label} +
{value}
+
+ ); +} + +function usePluginOverview(companyId: string | null) { + return usePluginData("overview", companyId ? { companyId } : {}); +} + +function usePluginConfigData() { + return usePluginData("plugin-config"); +} + +function hostFetchJson(path: string, init?: RequestInit): Promise { + return fetch(path, { + credentials: "include", + headers: { + "content-type": "application/json", + ...(init?.headers ?? {}), + }, + ...init, + }).then(async (response) => { + if (!response.ok) { + const text = await response.text(); + throw new Error(text || `Request failed: ${response.status}`); + } + return await response.json() as T; + }); +} + +function useSettingsConfig() { + const [configJson, setConfigJson] = useState>({ ...DEFAULT_CONFIG }); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + let cancelled = false; + setLoading(true); + hostFetchJson<{ configJson?: Record | null } | null>(`/api/plugins/${PLUGIN_ID}/config`) + .then((result) => { + if (cancelled) return; + setConfigJson({ ...DEFAULT_CONFIG, ...(result?.configJson ?? {}) }); + setError(null); + }) + .catch((nextError) => { + if (cancelled) return; + setError(nextError instanceof Error ? nextError.message : String(nextError)); + }) + .finally(() => { + if (!cancelled) setLoading(false); + }); + return () => { + cancelled = true; + }; + }, []); + + async function save(nextConfig: Record) { + setSaving(true); + try { + await hostFetchJson(`/api/plugins/${PLUGIN_ID}/config`, { + method: "POST", + body: JSON.stringify({ configJson: nextConfig }), + }); + setConfigJson(nextConfig); + setError(null); + } catch (nextError) { + setError(nextError instanceof Error ? nextError.message : String(nextError)); + throw nextError; + } finally { + setSaving(false); + } + } + + return { + configJson, + setConfigJson, + loading, + saving, + error, + save, + }; +} + +function CompactSurfaceSummary({ label, entityType }: { label: string; entityType?: string | null }) { + const context = useHostContext(); + const companyId = context.companyId; + const entityId = context.entityId; + const resolvedEntityType = entityType ?? context.entityType ?? null; + const entityQuery = usePluginData( + "entity-context", + companyId && entityId && resolvedEntityType + ? { companyId, entityId, entityType: resolvedEntityType } + : {}, + ); + const writeMetric = usePluginAction("write-metric"); + + return ( +
+
+ {label} + {resolvedEntityType ? : null} +
+ + + {entityQuery.data ? : null} +
+ ); +} + +function KitchenSinkPageWidgets({ context }: { context: PluginPageProps["context"] }) { + const overview = usePluginOverview(context.companyId); + const emitDemoEvent = usePluginAction("emit-demo-event"); + const startProgressStream = usePluginAction("start-progress-stream"); + const writeMetric = usePluginAction("write-metric"); + const progressStream = usePluginStream<{ step?: number; message?: string }>( + STREAM_CHANNELS.progress, + { companyId: context.companyId ?? undefined }, + ); + + const companyPath = hostPath(context.companyPrefix, `/plugins/${PLUGIN_ID}`); + + return ( +
+ +
+
Companies: {overview.data?.counts.companies ?? 0}
+
Projects: {overview.data?.counts.projects ?? 0}
+
Issues: {overview.data?.counts.issues ?? 0}
+
Agents: {overview.data?.counts.agents ?? 0}
+
+
+ + +
+ + + +
+
+ Recent progress events: {progressStream.events.length} +
+
+ + +
+
Sidebar link and panel
+
Dashboard widget
+
Project link, tab, toolbar button, launcher
+
Issue tab, task view, toolbar button, launcher
+
Comment annotation and comment action
+
+
+ + +
+
Jobs: {overview.data?.manifest.jobs.length ?? 0}
+
Webhooks: {overview.data?.manifest.webhooks.length ?? 0}
+
Tools: {overview.data?.manifest.tools.length ?? 0}
+
Launchers: {overview.data?.runtimeLaunchers.length ?? 0}
+
+
+ + +
+ This updates as you use the worker demos below. +
+ +
+ + +
+ The sidebar entry opens this page directly. Use it as the main kitchen-sink control surface. +
+ + {companyPath} + +
+
+ ); +} + +function KitchenSinkConsole({ context }: { context: { companyId: string | null; companyPrefix?: string | null; projectId?: string | null; entityId?: string | null; entityType?: string | null } }) { + const companyId = context.companyId; + const overview = usePluginOverview(companyId); + const companies = usePluginData("companies"); + const projects = usePluginData("projects", companyId ? { companyId } : {}); + const issues = usePluginData("issues", companyId ? { companyId } : {}); + const goals = usePluginData("goals", companyId ? { companyId } : {}); + const agents = usePluginData("agents", companyId ? { companyId } : {}); + + const [issueTitle, setIssueTitle] = useState("Kitchen Sink demo issue"); + const [goalTitle, setGoalTitle] = useState("Kitchen Sink demo goal"); + const [stateScopeKind, setStateScopeKind] = useState("instance"); + const [stateScopeId, setStateScopeId] = useState(""); + const [stateNamespace, setStateNamespace] = useState(""); + const [stateKey, setStateKey] = useState("demo"); + const [stateValue, setStateValue] = useState("{\"hello\":\"world\"}"); + const [entityType, setEntityType] = useState("demo-record"); + const [entityTitle, setEntityTitle] = useState("Kitchen Sink Entity"); + const [entityScopeKind, setEntityScopeKind] = useState("instance"); + const [entityScopeId, setEntityScopeId] = useState(""); + const [selectedProjectId, setSelectedProjectId] = useState(""); + const [selectedIssueId, setSelectedIssueId] = useState(""); + const [selectedGoalId, setSelectedGoalId] = useState(""); + const [selectedAgentId, setSelectedAgentId] = useState(""); + const [httpUrl, setHttpUrl] = useState(DEFAULT_CONFIG.httpDemoUrl); + const [secretRef, setSecretRef] = useState(""); + const [metricName, setMetricName] = useState("manual"); + const [metricValue, setMetricValue] = useState("1"); + const [assetContent, setAssetContent] = useState("Kitchen Sink asset demo"); + const [workspaceId, setWorkspaceId] = useState(""); + const [workspacePath, setWorkspacePath] = useState(DEFAULT_CONFIG.workspaceScratchFile); + const [workspaceContent, setWorkspaceContent] = useState("Kitchen Sink wrote this file."); + const [commandKey, setCommandKey] = useState(SAFE_COMMANDS[0]?.key ?? "pwd"); + const [toolMessage, setToolMessage] = useState("Hello from the Kitchen Sink tool"); + const [toolOutput, setToolOutput] = useState(null); + const [jobOutput, setJobOutput] = useState(null); + const [webhookOutput, setWebhookOutput] = useState(null); + const [result, setResult] = useState(null); + + const stateQuery = usePluginData("state-value", { + scopeKind: stateScopeKind, + scopeId: stateScopeId || undefined, + namespace: stateNamespace || undefined, + stateKey, + }); + const entityQuery = usePluginData("entities", { + entityType, + scopeKind: entityScopeKind, + scopeId: entityScopeId || undefined, + limit: 25, + }); + const workspaceQuery = usePluginData>( + "workspaces", + companyId && selectedProjectId ? { companyId, projectId: selectedProjectId } : {}, + ); + const progressStream = usePluginStream<{ step: number; total: number; message: string }>( + STREAM_CHANNELS.progress, + companyId ? { companyId } : undefined, + ); + const agentStream = usePluginStream<{ eventType: string; message: string | null }>( + STREAM_CHANNELS.agentChat, + companyId ? { companyId } : undefined, + ); + + const emitDemoEvent = usePluginAction("emit-demo-event"); + const createIssue = usePluginAction("create-issue"); + const advanceIssueStatus = usePluginAction("advance-issue-status"); + const createGoal = usePluginAction("create-goal"); + const advanceGoalStatus = usePluginAction("advance-goal-status"); + const writeScopedState = usePluginAction("write-scoped-state"); + const deleteScopedState = usePluginAction("delete-scoped-state"); + const upsertEntity = usePluginAction("upsert-entity"); + const writeActivity = usePluginAction("write-activity"); + const writeMetric = usePluginAction("write-metric"); + const httpFetch = usePluginAction("http-fetch"); + const resolveSecret = usePluginAction("resolve-secret"); + const createAsset = usePluginAction("create-asset"); + const runProcess = usePluginAction("run-process"); + const readWorkspaceFile = usePluginAction("read-workspace-file"); + const writeWorkspaceScratch = usePluginAction("write-workspace-scratch"); + const startProgressStream = usePluginAction("start-progress-stream"); + const invokeAgent = usePluginAction("invoke-agent"); + const pauseAgent = usePluginAction("pause-agent"); + const resumeAgent = usePluginAction("resume-agent"); + const askAgent = usePluginAction("ask-agent"); + + useEffect(() => { + if (!selectedProjectId && projects.data?.[0]?.id) setSelectedProjectId(projects.data[0].id); + }, [projects.data, selectedProjectId]); + + useEffect(() => { + if (!selectedIssueId && issues.data?.[0]?.id) setSelectedIssueId(issues.data[0].id); + }, [issues.data, selectedIssueId]); + + useEffect(() => { + if (!selectedGoalId && goals.data?.[0]?.id) setSelectedGoalId(goals.data[0].id); + }, [goals.data, selectedGoalId]); + + useEffect(() => { + if (!selectedAgentId && agents.data?.[0]?.id) setSelectedAgentId(agents.data[0].id); + }, [agents.data, selectedAgentId]); + + useEffect(() => { + if (!workspaceId && workspaceQuery.data?.[0]?.id) setWorkspaceId(workspaceQuery.data[0].id); + }, [workspaceId, workspaceQuery.data]); + + const projectRef = selectedProjectId || context.projectId || ""; + + async function refreshAll() { + overview.refresh(); + projects.refresh(); + issues.refresh(); + goals.refresh(); + agents.refresh(); + stateQuery.refresh(); + entityQuery.refresh(); + workspaceQuery.refresh(); + } + + async function executeTool(name: string) { + if (!companyId || !selectedAgentId || !projectRef) { + setToolOutput({ error: "Select a company, project, and agent first." }); + return; + } + try { + const toolName = `${PLUGIN_ID}:${name}`; + const body = + name === TOOL_NAMES.echo + ? { message: toolMessage } + : name === TOOL_NAMES.createIssue + ? { title: issueTitle, description: "Created through the tool dispatcher demo." } + : {}; + const response = await hostFetchJson(`/api/plugins/tools/execute`, { + method: "POST", + body: JSON.stringify({ + tool: toolName, + parameters: body, + runContext: { + agentId: selectedAgentId, + runId: `kitchen-sink-${Date.now()}`, + companyId, + projectId: projectRef, + }, + }), + }); + setToolOutput(response); + await refreshAll(); + } catch (error) { + setToolOutput({ error: error instanceof Error ? error.message : String(error) }); + } + } + + async function fetchJobsAndTrigger() { + try { + const jobsResponse = await hostFetchJson>(`/api/plugins/${PLUGIN_ID}/jobs`); + const job = jobsResponse.find((entry) => entry.jobKey === JOB_KEYS.heartbeat) ?? jobsResponse[0]; + if (!job) { + setJobOutput({ error: "No plugin jobs returned by the host." }); + return; + } + const triggerResult = await hostFetchJson(`/api/plugins/${PLUGIN_ID}/jobs/${job.id}/trigger`, { + method: "POST", + }); + setJobOutput({ jobs: jobsResponse, triggerResult }); + overview.refresh(); + } catch (error) { + setJobOutput({ error: error instanceof Error ? error.message : String(error) }); + } + } + + async function sendWebhook() { + try { + const response = await hostFetchJson(`/api/plugins/${PLUGIN_ID}/webhooks/${WEBHOOK_KEYS.demo}`, { + method: "POST", + body: JSON.stringify({ + source: "kitchen-sink-ui", + sentAt: new Date().toISOString(), + }), + }); + setWebhookOutput(response); + overview.refresh(); + } catch (error) { + setWebhookOutput({ error: error instanceof Error ? error.message : String(error) }); + } + } + + return ( +
+
refreshAll()}>Refresh} + > +
+ + + + {context.entityType ? : null} +
+ {overview.data ? ( + <> +
+ + + + + + +
+ + + ) : ( +
Loading overview…
+ )} +
+ +
+
+ Open plugin page + {projectRef ? ( + + Open project tab + + ) : null} + {selectedIssueId ? ( + + Open selected issue + + ) : null} +
+ +
+ +
+
+
+ Companies + { + const company = item as CompanyRecord; + return
{company.name} ({company.id.slice(0, 8)})
; + }} + /> +
+
+ Projects + { + const project = item as ProjectRecord; + return
{project.name} ({project.status ?? "unknown"})
; + }} + /> +
+
+ Issues + { + const issue = item as IssueRecord; + return
{issue.title} ({issue.status})
; + }} + /> +
+
+ Goals + { + const goal = item as GoalRecord; + return
{goal.title} ({goal.status})
; + }} + /> +
+
+
+ +
+
+
{ + event.preventDefault(); + if (!companyId) return; + void createIssue({ companyId, projectId: selectedProjectId || undefined, title: issueTitle }) + .then((next) => { + setResult(next); + return refreshAll(); + }) + .catch((error) => setResult({ error: error instanceof Error ? error.message : String(error) })); + }} + > + Create issue + setIssueTitle(event.target.value)} /> + +
+
{ + event.preventDefault(); + if (!companyId || !selectedIssueId) return; + void advanceIssueStatus({ companyId, issueId: selectedIssueId, status: "in_review" }) + .then((next) => { + setResult(next); + return refreshAll(); + }) + .catch((error) => setResult({ error: error instanceof Error ? error.message : String(error) })); + }} + > + Advance selected issue + + +
+
{ + event.preventDefault(); + if (!companyId) return; + void createGoal({ companyId, title: goalTitle }) + .then((next) => { + setResult(next); + return refreshAll(); + }) + .catch((error) => setResult({ error: error instanceof Error ? error.message : String(error) })); + }} + > + Create goal + setGoalTitle(event.target.value)} /> + +
+
{ + event.preventDefault(); + if (!companyId || !selectedGoalId) return; + void advanceGoalStatus({ companyId, goalId: selectedGoalId, status: "active" }) + .then((next) => { + setResult(next); + return refreshAll(); + }) + .catch((error) => setResult({ error: error instanceof Error ? error.message : String(error) })); + }} + > + Advance selected goal + + +
+
+
+ +
+
+
{ + event.preventDefault(); + void writeScopedState({ + scopeKind: stateScopeKind, + scopeId: stateScopeId || undefined, + namespace: stateNamespace || undefined, + stateKey, + value: stateValue, + }) + .then((next) => { + setResult(next); + stateQuery.refresh(); + }) + .catch((error) => setResult({ error: error instanceof Error ? error.message : String(error) })); + }} + > + State + setStateScopeKind(event.target.value)} placeholder="scopeKind" /> + setStateScopeId(event.target.value)} placeholder="scopeId (optional)" /> + setStateNamespace(event.target.value)} placeholder="namespace (optional)" /> + setStateKey(event.target.value)} placeholder="stateKey" /> +