mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-14 01:50:39 +09:00
## Thinking Path > - Paperclip gives operators a workspace diff plugin so they can inspect agent changes before review > - The diff view needs reliable base-ref defaults and controls that stay usable while scrolling large diffs > - The working branch mixed those plugin improvements with unrelated server and cloud work > - Keeping the workspace diff plugin changes isolated makes them easy to test and review > - This pull request polishes the workspace diff plugin controls, base-ref behavior, and sticky headers > - The benefit is a more predictable diff review surface for agent workspaces ## What Changed - Fixed workspace diff default base-ref resolution. - Improved split/unified and working-tree/against-ref pane controls. - Made workspace diff headers stay sticky while scrolling. - Added a review screenshot at `screenshots/PAP-9841-workspace-diff.png`. ## Verification - `pnpm install --frozen-lockfile --ignore-scripts` - `pnpm --filter @paperclipai/plugin-sdk build` - `pnpm --filter @paperclipai/plugin-workspace-diff exec vitest run tests/plugin.spec.ts` - Result: 9 tests passed. ## Risks - UI-only plugin branch with low data risk. - The default base-ref inference should be reviewed against unusual worktree/upstream combinations. > 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-based coding agent with local shell/git/tool use. Exact hosted model ID and context-window size are not exposed by the local Paperclip adapter 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>
108 lines
3.8 KiB
TypeScript
108 lines
3.8 KiB
TypeScript
import { definePlugin, runWorker, type PluginContext } from "@paperclipai/plugin-sdk";
|
|
import { workspaceDiffQuerySchema } from "./contracts.js";
|
|
import { workspaceDiffService } from "./workspace-diff.js";
|
|
|
|
const PLUGIN_NAME = "workspace-diff";
|
|
|
|
function readString(value: unknown): string {
|
|
return typeof value === "string" ? value.trim() : "";
|
|
}
|
|
|
|
function readOptionalString(value: unknown): string | null {
|
|
const trimmed = readString(value);
|
|
return trimmed || null;
|
|
}
|
|
|
|
export function resolveDefaultBaseRef(input: {
|
|
workspaceBaseRef?: unknown;
|
|
projectWorkspaceDefaultRef?: unknown;
|
|
projectWorkspaceRepoRef?: unknown;
|
|
}): string | null {
|
|
return readOptionalString(input.workspaceBaseRef)
|
|
?? readOptionalString(input.projectWorkspaceDefaultRef)
|
|
?? readOptionalString(input.projectWorkspaceRepoRef);
|
|
}
|
|
|
|
async function resolveProjectWorkspaceDefaultBaseRef(input: {
|
|
ctx: PluginContext;
|
|
projectId: string;
|
|
companyId: string;
|
|
projectWorkspaceId?: string | null;
|
|
}): Promise<string | null> {
|
|
if (!input.projectId) return null;
|
|
const workspaces = await input.ctx.projects.listWorkspaces(input.projectId, input.companyId);
|
|
const projectWorkspace = input.projectWorkspaceId
|
|
? workspaces.find((candidate) => candidate.id === input.projectWorkspaceId)
|
|
: workspaces.find((candidate) => candidate.isPrimary) ?? workspaces[0] ?? null;
|
|
return projectWorkspace
|
|
? resolveDefaultBaseRef({
|
|
projectWorkspaceDefaultRef: projectWorkspace.defaultRef,
|
|
projectWorkspaceRepoRef: projectWorkspace.repoRef,
|
|
})
|
|
: null;
|
|
}
|
|
|
|
const plugin = definePlugin({
|
|
async setup(ctx) {
|
|
ctx.logger.info(`${PLUGIN_NAME} plugin setup`);
|
|
const workspaceDiff = workspaceDiffService();
|
|
|
|
ctx.data.register("workspace-diff", async (params: Record<string, unknown>) => {
|
|
const workspaceId = readString(params.workspaceId);
|
|
const companyId = readString(params.companyId);
|
|
if (!workspaceId || !companyId) {
|
|
throw new Error("workspaceId and companyId are required");
|
|
}
|
|
|
|
if (params.entityType === "project_workspace") {
|
|
const projectId = readString(params.projectId);
|
|
if (!projectId) {
|
|
throw new Error("projectId is required for project workspace diffs");
|
|
}
|
|
const workspaces = await ctx.projects.listWorkspaces(projectId, companyId);
|
|
const workspace = workspaces.find((candidate) => candidate.id === workspaceId);
|
|
if (!workspace) {
|
|
throw new Error("Workspace not found");
|
|
}
|
|
return workspaceDiff.getDiff({
|
|
id: workspace.id,
|
|
companyId,
|
|
cwd: workspace.path,
|
|
baseRef: resolveDefaultBaseRef({
|
|
projectWorkspaceDefaultRef: workspace.defaultRef,
|
|
projectWorkspaceRepoRef: workspace.repoRef,
|
|
}),
|
|
}, workspaceDiffQuerySchema.parse(params));
|
|
}
|
|
|
|
const workspace = await ctx.executionWorkspaces.get(workspaceId, companyId);
|
|
if (!workspace) {
|
|
throw new Error("Workspace not found");
|
|
}
|
|
let projectWorkspaceDefaultBaseRef: string | null = null;
|
|
if (!readOptionalString(workspace.baseRef)) {
|
|
projectWorkspaceDefaultBaseRef = await resolveProjectWorkspaceDefaultBaseRef({
|
|
ctx,
|
|
projectId: workspace.projectId || readString(params.projectId),
|
|
companyId,
|
|
projectWorkspaceId: workspace.projectWorkspaceId,
|
|
});
|
|
}
|
|
|
|
return workspaceDiff.getDiff({
|
|
...workspace,
|
|
baseRef: resolveDefaultBaseRef({
|
|
workspaceBaseRef: workspace.baseRef,
|
|
projectWorkspaceDefaultRef: projectWorkspaceDefaultBaseRef,
|
|
}),
|
|
}, workspaceDiffQuerySchema.parse(params));
|
|
});
|
|
},
|
|
|
|
async onHealth() {
|
|
return { status: "ok", message: `${PLUGIN_NAME} ready` };
|
|
},
|
|
});
|
|
|
|
export default plugin;
|
|
runWorker(plugin, import.meta.url);
|