mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-17 19:20:39 +09:00
Expand plugin host surface (#5205)
## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies > - The plugin system is the extension boundary for optional product capabilities > - Rich plugins need more than a worker entrypoint: they need scoped database storage, local project folders, managed agents/routines, host navigation, and reusable UI components > - The LLM Wiki work exposed those missing host surfaces while keeping plugin code outside the core control plane > - This pull request expands the core plugin host, SDK, server APIs, and UI bridge so plugins can declare and use those surfaces > - The benefit is that future plugins can integrate with Paperclip through documented, validated contracts instead of bespoke server or UI imports ## What Changed - Added plugin-managed database namespaces and migration tracking, including Drizzle schema/migration files and SQL validation for namespace isolation. - Added server support for plugin local folders, managed agents, managed routines, scoped plugin APIs, and plugin operation visibility. - Expanded shared plugin manifest/types/validators and SDK host/testing/UI exports for richer plugin surfaces. - Added reusable UI pieces for file trees, managed routines, resizable sidebars, route sidebars, and plugin bridge initialization. - Updated plugin docs and example plugins to use the expanded host and SDK surface. ## Verification - `pnpm install --frozen-lockfile` - `pnpm run preflight:workspace-links && pnpm exec vitest run packages/shared/src/validators/plugin.test.ts server/src/__tests__/plugin-database.test.ts server/src/__tests__/plugin-local-folders.test.ts server/src/__tests__/plugin-managed-agents.test.ts server/src/__tests__/plugin-managed-routines.test.ts server/src/__tests__/plugin-orchestration-apis.test.ts ui/src/api/plugins.test.ts ui/src/components/FileTree.test.tsx ui/src/components/ResizableSidebarPane.test.tsx ui/src/pages/PluginPage.test.tsx ui/src/plugins/bridge.test.ts` passed: 11 files, 67 tests. - Confirmed this PR changes 89 files and does not include `pnpm-lock.yaml` or `.github/workflows/*`. ## Risks - Medium: this expands plugin host contracts across db/shared/server/ui and includes a new core migration (`0076_useful_elektra.sql`). - The plugin database namespace validator is intentionally restrictive; plugin authors may need follow-up affordances for SQL patterns that remain blocked. - Merge this before the LLM Wiki plugin PR so the plugin can resolve the new SDK and host APIs. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - OpenAI Codex, GPT-5 coding agent, tool-enabled shell/git/GitHub workflow. Context window size was not exposed by the runtime. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] If this change affects the UI, I have included before/after screenshots - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
d6bee62f02
commit
3c73ed26b5
89 changed files with 27516 additions and 914 deletions
|
|
@ -33,7 +33,7 @@ import { PluginSlotMount, PluginSlotOutlet, usePluginSlots } from "@/plugins/slo
|
|||
|
||||
/* ── Top-level tab types ── */
|
||||
|
||||
type ProjectBaseTab = "overview" | "list" | "workspaces" | "configuration" | "budget";
|
||||
type ProjectBaseTab = "overview" | "list" | "plugin-operations" | "workspaces" | "configuration" | "budget";
|
||||
type ProjectPluginTab = `plugin:${string}`;
|
||||
type ProjectTab = ProjectBaseTab | ProjectPluginTab;
|
||||
|
||||
|
|
@ -50,6 +50,7 @@ function resolveProjectTab(pathname: string, projectId: string): ProjectTab | nu
|
|||
if (tab === "configuration") return "configuration";
|
||||
if (tab === "budget") return "budget";
|
||||
if (tab === "issues") return "list";
|
||||
if (tab === "plugin-operations") return "plugin-operations";
|
||||
if (tab === "workspaces") return "workspaces";
|
||||
return null;
|
||||
}
|
||||
|
|
@ -208,6 +209,67 @@ function ProjectIssuesList({ projectId, companyId }: { projectId: string; compan
|
|||
);
|
||||
}
|
||||
|
||||
function ProjectPluginOperationsList({
|
||||
projectId,
|
||||
companyId,
|
||||
pluginKey,
|
||||
}: {
|
||||
projectId: string;
|
||||
companyId: string;
|
||||
pluginKey: string;
|
||||
}) {
|
||||
const queryClient = useQueryClient();
|
||||
const originKindPrefix = `plugin:${pluginKey}`;
|
||||
|
||||
const { data: agents } = useQuery({
|
||||
queryKey: queryKeys.agents.list(companyId),
|
||||
queryFn: () => agentsApi.list(companyId),
|
||||
enabled: !!companyId,
|
||||
});
|
||||
const { data: projects } = useQuery({
|
||||
queryKey: queryKeys.projects.list(companyId),
|
||||
queryFn: () => projectsApi.list(companyId),
|
||||
enabled: !!companyId,
|
||||
});
|
||||
const { data: liveRuns } = useQuery({
|
||||
queryKey: queryKeys.liveRuns(companyId),
|
||||
queryFn: () => heartbeatsApi.liveRunsForCompany(companyId),
|
||||
enabled: !!companyId,
|
||||
refetchInterval: 5000,
|
||||
});
|
||||
const liveIssueIds = useMemo(() => collectLiveIssueIds(liveRuns), [liveRuns]);
|
||||
|
||||
const { data: issues, isLoading, error } = useQuery({
|
||||
queryKey: queryKeys.issues.listPluginOperationsByProject(companyId, projectId, originKindPrefix),
|
||||
queryFn: () => issuesApi.list(companyId, { projectId, originKindPrefix }),
|
||||
enabled: !!companyId && !!projectId,
|
||||
});
|
||||
|
||||
const updateIssue = useMutation({
|
||||
mutationFn: ({ id, data }: { id: string; data: Record<string, unknown> }) =>
|
||||
issuesApi.update(id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.issues.listPluginOperationsByProject(companyId, projectId, originKindPrefix) });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.issues.listByProject(companyId, projectId) });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.issues.list(companyId) });
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<IssuesList
|
||||
issues={issues ?? []}
|
||||
isLoading={isLoading}
|
||||
error={error as Error | null}
|
||||
agents={agents}
|
||||
projects={projects}
|
||||
liveIssueIds={liveIssueIds}
|
||||
projectId={projectId}
|
||||
viewStateKey={`paperclip:project-plugin-operations-view:${pluginKey}`}
|
||||
onUpdateIssue={(id, data) => updateIssue.mutate({ id, data })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Main project page ── */
|
||||
|
||||
export function ProjectDetail() {
|
||||
|
|
@ -390,6 +452,10 @@ export function ProjectDetail() {
|
|||
navigate(`/projects/${canonicalProjectRef}/budget`, { replace: true });
|
||||
return;
|
||||
}
|
||||
if (activeTab === "plugin-operations") {
|
||||
navigate(`/projects/${canonicalProjectRef}/plugin-operations`, { replace: true });
|
||||
return;
|
||||
}
|
||||
if (activeTab === "workspaces") {
|
||||
navigate(`/projects/${canonicalProjectRef}/workspaces`, { replace: true });
|
||||
return;
|
||||
|
|
@ -523,6 +589,9 @@ export function ProjectDetail() {
|
|||
if (cachedTab === "budget") {
|
||||
return <Navigate to={`/projects/${canonicalProjectRef}/budget`} replace />;
|
||||
}
|
||||
if (cachedTab === "plugin-operations" && project?.managedByPlugin) {
|
||||
return <Navigate to={`/projects/${canonicalProjectRef}/plugin-operations`} replace />;
|
||||
}
|
||||
if (cachedTab === "workspaces" && workspaceTabDecisionLoaded && showWorkspacesTab) {
|
||||
return <Navigate to={`/projects/${canonicalProjectRef}/workspaces`} replace />;
|
||||
}
|
||||
|
|
@ -554,6 +623,8 @@ export function ProjectDetail() {
|
|||
navigate(`/projects/${canonicalProjectRef}/workspaces`);
|
||||
} else if (tab === "budget") {
|
||||
navigate(`/projects/${canonicalProjectRef}/budget`);
|
||||
} else if (tab === "plugin-operations") {
|
||||
navigate(`/projects/${canonicalProjectRef}/plugin-operations`);
|
||||
} else if (tab === "configuration") {
|
||||
navigate(`/projects/${canonicalProjectRef}/configuration`);
|
||||
} else {
|
||||
|
|
@ -583,6 +654,12 @@ export function ProjectDetail() {
|
|||
Paused by budget hard stop
|
||||
</div>
|
||||
) : null}
|
||||
{project.managedByPlugin ? (
|
||||
<div className="inline-flex items-center gap-2 rounded-full border border-border bg-muted px-3 py-1 text-[11px] font-medium text-muted-foreground">
|
||||
<span className="h-2 w-2 rounded-full" style={{ backgroundColor: project.color ?? "#6366f1" }} />
|
||||
Managed by {project.managedByPlugin.pluginDisplayName}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -622,6 +699,7 @@ export function ProjectDetail() {
|
|||
items={[
|
||||
{ value: "list", label: "Issues" },
|
||||
{ value: "overview", label: "Overview" },
|
||||
...(project.managedByPlugin ? [{ value: "plugin-operations", label: "Plugin operations" }] : []),
|
||||
...(showWorkspacesTab ? [{ value: "workspaces", label: "Workspaces" }] : []),
|
||||
{ value: "configuration", label: "Configuration" },
|
||||
{ value: "budget", label: "Budget" },
|
||||
|
|
@ -651,6 +729,14 @@ export function ProjectDetail() {
|
|||
<ProjectIssuesList projectId={project.id} companyId={resolvedCompanyId} />
|
||||
)}
|
||||
|
||||
{activeTab === "plugin-operations" && project?.id && resolvedCompanyId && project.managedByPlugin && (
|
||||
<ProjectPluginOperationsList
|
||||
projectId={project.id}
|
||||
companyId={resolvedCompanyId}
|
||||
pluginKey={project.managedByPlugin.pluginKey}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === "workspaces" ? (
|
||||
workspaceTabDecisionLoaded ? (
|
||||
workspaceTabError ? (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue