import { useEffect, useMemo, useState, type CSSProperties, type FormEvent, type ReactNode } from "react"; import { useHostContext, usePluginAction, usePluginData, usePluginStream, usePluginToast, 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, PAGE_ROUTE, PLUGIN_ID, SAFE_COMMANDS, SLOT_IDS, STREAM_CHANNELS, TOOL_NAMES, WEBHOOK_KEYS, } from "../constants.js"; import { AsciiArtAnimation } from "./AsciiArtAnimation.js"; type CompanyRecord = { id: string; name: string; issuePrefix?: string | null; status?: 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 HostIssueRecord = { id: string; title: string; status: string; priority?: string | null; createdAt?: string; }; type HostHeartbeatRunRecord = { id: string; status: string; invocationSource?: string | null; triggerDetail?: string | null; createdAt?: string; startedAt?: string | null; finishedAt?: string | null; agentId?: string | null; }; type HostLiveRunRecord = HostHeartbeatRunRecord & { agentName?: string | null; issueId?: string | null; }; 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; 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)", }; function toneButtonStyle(tone: "success" | "warn" | "info"): CSSProperties { if (tone === "success") { return { ...buttonStyle, background: "color-mix(in srgb, #16a34a 18%, transparent)", borderColor: "color-mix(in srgb, #16a34a 60%, var(--border))", color: "#86efac", }; } if (tone === "warn") { return { ...buttonStyle, background: "color-mix(in srgb, #d97706 18%, transparent)", borderColor: "color-mix(in srgb, #d97706 60%, var(--border))", color: "#fcd34d", }; } return { ...buttonStyle, background: "color-mix(in srgb, #2563eb 18%, transparent)", borderColor: "color-mix(in srgb, #2563eb 60%, var(--border))", color: "#93c5fd", }; } 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 pluginPagePath(companyPrefix: string | null | undefined): string { return hostPath(companyPrefix, `/${PAGE_ROUTE}`); } function getErrorMessage(error: unknown): string { return error instanceof Error ? error.message : String(error); } function getObjectString(value: unknown, key: string): string | null { if (!value || typeof value !== "object") return null; const next = (value as Record)[key]; return typeof next === "string" ? next : null; } function getObjectNumber(value: unknown, key: string): number | null { if (!value || typeof value !== "object") return null; const next = (value as Record)[key]; return typeof next === "number" && Number.isFinite(next) ? next : null; } function isKitchenSinkDemoCompany(company: CompanyRecord): boolean { return company.name.startsWith("Kitchen Sink Demo"); } 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 PaginatedDomainCard({ title, items, totalCount, empty, onLoadMore, render, }: { title: string; items: unknown[]; totalCount: number | null; empty: string; onLoadMore: () => void; render: (item: unknown, index: number) => ReactNode; }) { const hasMore = totalCount !== null ? items.length < totalCount : false; return (
{title} {totalCount !== null ? {items.length} / {totalCount} : null}
{hasMore ? (
) : null}
); } 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}
This surface demo shows the host context for the current mount point. The metric button records a demo counter so you can verify plugin metrics wiring from a contextual surface.
{entityQuery.data ? : null}
); } function KitchenSinkPageWidgets({ context }: { context: PluginPageProps["context"] }) { const overview = usePluginOverview(context.companyId); const toast = usePluginToast(); 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 [quickActionStatus, setQuickActionStatus] = useState<{ title: string; body: string; tone: "info" | "success" | "warn" | "error"; } | null>(null); useEffect(() => { const latest = progressStream.events.at(-1); if (!latest) return; setQuickActionStatus({ title: "Progress stream update", body: latest.message ?? `Step ${latest.step ?? "?"}`, tone: "info", }); }, [progressStream.events]); 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}
{quickActionStatus ? (
{quickActionStatus.title}
{quickActionStatus.body}
) : null} {progressStream.events.length > 0 ? ( ) : null}
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.
); } function KitchenSinkIssueCrudDemo({ context }: { context: PluginPageProps["context"] }) { const toast = usePluginToast(); const [issues, setIssues] = useState([]); const [drafts, setDrafts] = useState>({}); const [createTitle, setCreateTitle] = useState("Kitchen Sink demo issue"); const [createDescription, setCreateDescription] = useState("Created from the Kitchen Sink embedded page."); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); async function loadIssues() { if (!context.companyId) return; setLoading(true); try { const result = await hostFetchJson(`/api/companies/${context.companyId}/issues`); const nextIssues = result.slice(0, 8); setIssues(nextIssues); setDrafts( Object.fromEntries( nextIssues.map((issue) => [issue.id, { title: issue.title, status: issue.status }]), ), ); setError(null); } catch (nextError) { setError(getErrorMessage(nextError)); } finally { setLoading(false); } } useEffect(() => { void loadIssues(); }, [context.companyId]); async function handleCreate() { if (!context.companyId || !createTitle.trim()) return; try { await hostFetchJson(`/api/companies/${context.companyId}/issues`, { method: "POST", body: JSON.stringify({ title: createTitle.trim(), description: createDescription.trim() || undefined, status: "todo", priority: "medium", }), }); toast({ title: "Issue created", body: createTitle.trim(), tone: "success" }); setCreateTitle("Kitchen Sink demo issue"); setCreateDescription("Created from the Kitchen Sink embedded page."); await loadIssues(); } catch (nextError) { toast({ title: "Issue create failed", body: getErrorMessage(nextError), tone: "error" }); } } async function handleSave(issueId: string) { const draft = drafts[issueId]; if (!draft) return; try { await hostFetchJson(`/api/issues/${issueId}`, { method: "PATCH", body: JSON.stringify({ title: draft.title.trim(), status: draft.status, }), }); toast({ title: "Issue updated", body: draft.title.trim(), tone: "success" }); await loadIssues(); } catch (nextError) { toast({ title: "Issue update failed", body: getErrorMessage(nextError), tone: "error" }); } } async function handleDelete(issueId: string) { try { await hostFetchJson(`/api/issues/${issueId}`, { method: "DELETE" }); toast({ title: "Issue deleted", tone: "info" }); await loadIssues(); } catch (nextError) { toast({ title: "Issue delete failed", body: getErrorMessage(nextError), tone: "error" }); } } return (
This is a regular embedded React page inside Paperclip calling the board API directly. It creates, updates, and deletes issues for the current company.
{!context.companyId ? (
Select a company to use issue demos.
) : ( <>
setCreateTitle(event.target.value)} placeholder="Issue title" /> setCreateDescription(event.target.value)} placeholder="Issue description" />
{loading ?
Loading issues…
: null} {error ?
{error}
: null}
{issues.map((issue) => { const draft = drafts[issue.id] ?? { title: issue.title, status: issue.status }; return (
setDrafts((current) => ({ ...current, [issue.id]: { ...draft, title: event.target.value }, }))} />
); })} {!loading && issues.length === 0 ?
No issues yet for this company.
: null}
)}
); } function KitchenSinkCompanyCrudDemo({ context }: { context: PluginPageProps["context"] }) { const toast = usePluginToast(); const [companies, setCompanies] = useState([]); const [drafts, setDrafts] = useState>({}); const [newCompanyName, setNewCompanyName] = useState(`Kitchen Sink Demo ${new Date().toLocaleTimeString()}`); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); async function loadCompanies() { setLoading(true); try { const result = await hostFetchJson>("/api/companies"); setCompanies(result); setDrafts( Object.fromEntries( result.map((company) => [company.id, { name: company.name, status: company.status ?? "active" }]), ), ); setError(null); } catch (nextError) { setError(getErrorMessage(nextError)); } finally { setLoading(false); } } useEffect(() => { void loadCompanies(); }, []); async function handleCreate() { const trimmed = newCompanyName.trim(); if (!trimmed) return; const name = trimmed.startsWith("Kitchen Sink Demo") ? trimmed : `Kitchen Sink Demo ${trimmed}`; try { await hostFetchJson("/api/companies", { method: "POST", body: JSON.stringify({ name, description: "Created from the Kitchen Sink example plugin page.", }), }); toast({ title: "Demo company created", body: name, tone: "success" }); setNewCompanyName(`Kitchen Sink Demo ${Date.now()}`); await loadCompanies(); } catch (nextError) { toast({ title: "Company create failed", body: getErrorMessage(nextError), tone: "error" }); } } async function handleSave(companyId: string) { const draft = drafts[companyId]; if (!draft) return; try { await hostFetchJson(`/api/companies/${companyId}`, { method: "PATCH", body: JSON.stringify({ name: draft.name.trim(), status: draft.status, }), }); toast({ title: "Company updated", body: draft.name.trim(), tone: "success" }); await loadCompanies(); } catch (nextError) { toast({ title: "Company update failed", body: getErrorMessage(nextError), tone: "error" }); } } async function handleDelete(company: CompanyRecord) { try { await hostFetchJson(`/api/companies/${company.id}`, { method: "DELETE" }); toast({ title: "Demo company deleted", body: company.name, tone: "info" }); await loadCompanies(); } catch (nextError) { toast({ title: "Company delete failed", body: getErrorMessage(nextError), tone: "error" }); } } const currentCompany = companies.find((company) => company.id === context.companyId) ?? null; const demoCompanies = companies.filter(isKitchenSinkDemoCompany); return (
The worker SDK currently exposes company reads. This page shows a pragmatic embedded-app pattern for broader board actions by calling the host REST API directly.
Current Company {currentCompany ? : null}
{currentCompany?.name ?? "No current company selected"}
setNewCompanyName(event.target.value)} placeholder="Kitchen Sink Demo Company" />
{loading ?
Loading companies…
: null} {error ?
{error}
: null}
{demoCompanies.map((company) => { const draft = drafts[company.id] ?? { name: company.name, status: "active" }; const isCurrent = company.id === context.companyId; return (
setDrafts((current) => ({ ...current, [company.id]: { ...draft, name: event.target.value }, }))} />
{isCurrent ?
Current company cannot be deleted from this demo.
: null}
); })} {!loading && demoCompanies.length === 0 ? (
No demo companies yet. Create one above and manage it from this page.
) : null}
); } function KitchenSinkTopRow({ context }: { context: PluginPageProps["context"] }) { return (
Plugins can host their own React page and behave like a native company page. Kitchen Sink now uses this route as a practical demo app, then keeps the lower-level worker console below for the rest of the SDK surface.
The company sidebar entry opens this route directly, so the plugin feels like a first-class company page instead of a settings subpage.
{pluginPagePath(context.companyPrefix)}
This is the same Paperclip ASCII treatment used in onboarding, copied into the example plugin so the package stays self-contained.
); } function KitchenSinkStorageDemo({ context }: { context: PluginPageProps["context"] }) { const toast = usePluginToast(); const stateKey = "revenue_clicker"; const revenueState = usePluginData( "state-value", context.companyId ? { scopeKind: "company", scopeId: context.companyId, stateKey } : {}, ); const writeScopedState = usePluginAction("write-scoped-state"); const deleteScopedState = usePluginAction("delete-scoped-state"); const currentValue = useMemo(() => { const raw = revenueState.data?.value; if (typeof raw === "number") return raw; const parsed = Number(raw ?? 0); return Number.isFinite(parsed) ? parsed : 0; }, [revenueState.data?.value]); async function adjust(delta: number) { if (!context.companyId) return; try { await writeScopedState({ scopeKind: "company", scopeId: context.companyId, stateKey, value: currentValue + delta, }); revenueState.refresh(); } catch (nextError) { toast({ title: "Storage write failed", body: getErrorMessage(nextError), tone: "error" }); } } async function reset() { if (!context.companyId) return; try { await deleteScopedState({ scopeKind: "company", scopeId: context.companyId, stateKey, }); toast({ title: "Revenue counter reset", tone: "info" }); revenueState.refresh(); } catch (nextError) { toast({ title: "Storage reset failed", body: getErrorMessage(nextError), tone: "error" }); } } return (
This clicker persists into plugin-scoped company storage. A real revenue plugin could store counters, sync cursors, or cached external IDs the same way.
{!context.companyId ? (
Select a company to use company-scoped plugin storage.
) : ( <>
{currentValue}
Stored at `company/{context.companyId}/{stateKey}`
{[-10, -1, 1, 10].map((delta) => ( ))}
)}
); } function KitchenSinkHostIntegrationDemo({ context }: { context: PluginPageProps["context"] }) { const [liveRuns, setLiveRuns] = useState([]); const [recentRuns, setRecentRuns] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); async function loadRuns() { if (!context.companyId) return; setLoading(true); try { const [nextLiveRuns, nextRecentRuns] = await Promise.all([ hostFetchJson(`/api/companies/${context.companyId}/live-runs?minCount=5`), hostFetchJson(`/api/companies/${context.companyId}/heartbeat-runs?limit=5`), ]); setLiveRuns(nextLiveRuns); setRecentRuns(nextRecentRuns); setError(null); } catch (nextError) { setError(getErrorMessage(nextError)); } finally { setLoading(false); } } useEffect(() => { void loadRuns(); }, [context.companyId]); return (
Plugin pages can feel like native Paperclip pages. This section demonstrates host toasts, company-scoped routing, and reading live heartbeat data from the embedded page.
Company Route
This page is mounted as a real company route instead of living only under `/plugins/:pluginId`.
{!context.companyId ? (
Select a company to read run data.
) : (
Live Runs
{loading ?
Loading run data…
: null} {error ?
{error}
: null} { const run = item as HostLiveRunRecord; return (
{run.status} {run.agentName ? : null}
{run.id}
{run.agentId ? ( Open run ) : null}
); }} />
Recent Heartbeats { const run = item as HostHeartbeatRunRecord; return (
{run.status} {run.invocationSource ? : null}
{run.id}
); }} />
)}
); } function KitchenSinkEmbeddedApp({ context }: { context: PluginPageProps["context"] }) { return (
); } 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 [companiesLimit, setCompaniesLimit] = useState(20); const [projectsLimit, setProjectsLimit] = useState(20); const [issuesLimit, setIssuesLimit] = useState(20); const [goalsLimit, setGoalsLimit] = useState(20); const companies = usePluginData("companies", { limit: companiesLimit }); const projects = usePluginData("projects", companyId ? { companyId, limit: projectsLimit } : {}); const issues = usePluginData("issues", companyId ? { companyId, limit: issuesLimit } : {}); const goals = usePluginData("goals", companyId ? { companyId, limit: goalsLimit } : {}); 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 [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 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(() => { setProjectsLimit(20); setIssuesLimit(20); setGoalsLimit(20); }, [companyId]); 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}
setCompaniesLimit((current) => current + 20)} render={(item) => { const company = item as CompanyRecord; return
{company.name} ({company.id.slice(0, 8)})
; }} /> setProjectsLimit((current) => current + 20)} render={(item) => { const project = item as ProjectRecord; return
{project.name} ({project.status ?? "unknown"})
; }} /> setIssuesLimit((current) => current + 20)} render={(item) => { const issue = item as IssueRecord; return
{issue.title} ({issue.status})
; }} /> setGoalsLimit((current) => current + 20)} render={(item) => { 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" />