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" />