Address workspace diff review findings

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-05-15 13:28:59 -05:00
1 parent 4c98a35b8c
commit af5f646e51
12 files changed
+68 -39

No files matched your search

+31 -20
View File
@@ -62,6 +62,13 @@ jobs:
pnpm install --lockfile-only --ignore-scripts --no-frozen-lockfile
fi
- name: Upload resolved lockfile
uses: actions/upload-artifact@v4
with:
name: pr-pnpm-lock-${{ github.run_id }}
path: pnpm-lock.yaml
retention-days: 1
verify:
needs: [policy]
runs-on: ubuntu-latest
@@ -82,12 +89,13 @@ jobs:
node-version: 24
cache: pnpm
- name: Use policy-resolved lockfile
uses: actions/download-artifact@v4
with:
name: pr-pnpm-lock-${{ github.run_id }}
- name: Install dependencies
run: |
pnpm install --frozen-lockfile || {
echo "Frozen lockfile install failed; retrying without frozen lockfile because PRs do not commit pnpm-lock.yaml."
pnpm install --no-frozen-lockfile
}
run: pnpm install --frozen-lockfile
- name: Typecheck workspaces whose build scripts skip TypeScript
run: pnpm run typecheck:build-gaps
@@ -138,12 +146,13 @@ jobs:
node-version: 24
cache: pnpm
- name: Use policy-resolved lockfile
uses: actions/download-artifact@v4
with:
name: pr-pnpm-lock-${{ github.run_id }}
- name: Install dependencies
run: |
pnpm install --frozen-lockfile || {
echo "Frozen lockfile install failed; retrying without frozen lockfile because PRs do not commit pnpm-lock.yaml."
pnpm install --no-frozen-lockfile
}
run: pnpm install --frozen-lockfile
- name: Run serialized server test shard
run: pnpm test:run:serialized -- --shard-index ${{ matrix.shard_index }} --shard-count ${{ matrix.shard_count }}
@@ -169,12 +178,13 @@ jobs:
node-version: 24
cache: pnpm
- name: Use policy-resolved lockfile
uses: actions/download-artifact@v4
with:
name: pr-pnpm-lock-${{ github.run_id }}
- name: Install dependencies
run: |
pnpm install --frozen-lockfile || {
echo "Frozen lockfile install failed; retrying without frozen lockfile because PRs do not commit pnpm-lock.yaml."
pnpm install --no-frozen-lockfile
}
run: pnpm install --frozen-lockfile
# `release.sh` always executes its Step 2/7 workspace build, even when
# `--skip-verify` bypasses the initial verification gate.
@@ -204,12 +214,13 @@ jobs:
node-version: 24
cache: pnpm
- name: Use policy-resolved lockfile
uses: actions/download-artifact@v4
with:
name: pr-pnpm-lock-${{ github.run_id }}
- name: Install dependencies
run: |
pnpm install --frozen-lockfile || {
echo "Frozen lockfile install failed; retrying without frozen lockfile because PRs do not commit pnpm-lock.yaml."
pnpm install --no-frozen-lockfile
}
run: pnpm install --frozen-lockfile
- name: Install Playwright
run: npx playwright install --with-deps chromium
@@ -14,6 +14,7 @@ const manifest: PaperclipPluginManifestV1 = {
capabilities: [
"ui.detailTab.register",
"execution.workspaces.read",
"project.workspaces.read",
],
entrypoints: {
worker: "./dist/worker.js",
@@ -47,8 +47,6 @@ export function diffResponse(overrides: Partial<WorkspaceDiffResponse> = {}): Wo
return {
workspaceId: "11111111-1111-4111-8111-111111111111",
companyId: "22222222-2222-4222-8222-222222222222",
repoRoot: "/tmp/paperclip",
cwd: "/tmp/paperclip",
view: "working-tree",
baseRef: null,
headSha: null,
@@ -8,6 +8,7 @@ describe("workspace diff plugin", () => {
it("declares workspace Changes tabs and host diff capability", () => {
expect(manifest.capabilities).toContain("ui.detailTab.register");
expect(manifest.capabilities).toContain("execution.workspaces.read");
expect(manifest.capabilities).toContain("project.workspaces.read");
expect(manifest.ui?.slots).toContainEqual(expect.objectContaining({
type: "detailTab",
displayName: "Changes",
@@ -88,8 +88,6 @@ export interface WorkspaceDiffStats {
export interface WorkspaceDiffResponse {
workspaceId: string;
companyId: string;
repoRoot: string;
cwd: string;
view: WorkspaceDiffView;
baseRef: string | null;
headSha: string | null;
@@ -20,8 +20,6 @@ describe("workspace diff validators", () => {
const response = workspaceDiffResponseSchema.parse({
workspaceId: "11111111-1111-4111-8111-111111111111",
companyId: "22222222-2222-4222-8222-222222222222",
repoRoot: "/tmp/repo",
cwd: "/tmp/repo",
view: "working-tree",
baseRef: null,
headSha: "abc123",
@@ -117,8 +117,6 @@ export const workspaceDiffStatsSchema = z.object({
export const workspaceDiffResponseSchema = z.object({
workspaceId: z.string().uuid(),
companyId: z.string().uuid(),
repoRoot: z.string(),
cwd: z.string(),
view: workspaceDiffViewSchema,
baseRef: z.string().nullable(),
headSha: z.string().nullable(),
@@ -91,6 +91,7 @@ describe("workspaceDiffService", () => {
await fs.rm(path.join(repoRoot, "delete-me.txt"));
await fs.writeFile(path.join(repoRoot, "binary.bin"), Buffer.from([0, 1, 2, 3, 4, 5]));
await fs.writeFile(path.join(repoRoot, "untracked.txt"), "brand new\n", "utf8");
await fs.writeFile(path.join(repoRoot, "empty-untracked.txt"), "", "utf8");
await fs.writeFile(path.join(repoRoot, "oversized.txt"), "x".repeat(WORKSPACE_DIFF_CAPS.maxFileBytes + 1), "utf8");
const diff = await workspaceDiffService().getDiff(createWorkspace(repoRoot), workingTreeQuery());
@@ -126,6 +127,13 @@ describe("workspaceDiffService", () => {
additions: 1,
});
expect(byPath.get("untracked.txt")?.patches[0]?.patch).toContain("+brand new");
expect(byPath.get("empty-untracked.txt")?.patches[0]?.patch).toBe([
"diff --git a/empty-untracked.txt b/empty-untracked.txt",
"new file mode 100644",
"--- /dev/null",
"+++ b/empty-untracked.txt",
"",
].join("\n"));
expect(byPath.get("binary.bin")).toMatchObject({
binary: true,
unstaged: true,
@@ -571,6 +571,11 @@ export function buildHostServices(
*/
const ensurePluginAvailableForCompany = async (_companyId: string) => {};
const ensureDeclaredCapability = (capability: import("@paperclipai/shared").PluginCapability) => {
if (!options.manifest || options.manifest.capabilities.includes(capability)) return;
throw new Error(`Missing required capability: ${capability}`);
};
const getLocalFolderDeclaration = (folderKey: string) =>
requireLocalFolderDeclaration(options.manifest?.localFolders, folderKey);
@@ -1227,6 +1232,7 @@ export function buildHostServices(
if (!projectWorkspace) {
throw new Error("Workspace not found");
}
ensureDeclaredCapability("project.workspaces.read");
return workspaceDiff.getDiff({
id: projectWorkspace.id,
companyId: projectWorkspace.companyId,
+5 -8
View File
@@ -442,17 +442,16 @@ function countAddedLines(content: string) {
function buildUntrackedPatch(filePath: string, content: string) {
const lines = content.length === 0 ? [] : content.split("\n");
if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
const hunkLines = lines.map((line) => `+${line}`).join("\n");
const lineCount = countAddedLines(content);
return [
const header = [
`diff --git a/${filePath} b/${filePath}`,
"new file mode 100644",
"--- /dev/null",
`+++ b/${filePath}`,
`@@ -0,0 +1,${lineCount} @@`,
hunkLines,
"",
].join("\n");
];
if (lineCount === 0) return `${header.join("\n")}\n`;
const hunkLines = lines.map((line) => `+${line}`).join("\n");
return [...header, `@@ -0,0 +1,${lineCount} @@`, hunkLines, ""].join("\n");
}
async function buildUntrackedFilePatch(input: {
@@ -674,8 +673,6 @@ export function workspaceDiffService() {
return {
workspaceId: workspace.id,
companyId: workspace.companyId,
repoRoot,
cwd,
view: query.view,
baseRef,
headSha: await resolveHeadSha(cwd),
+13 -1
View File
@@ -593,7 +593,11 @@ export function ExecutionWorkspaceDetail() {
[project, workspace?.projectWorkspaceId],
);
const { slots: workspacePluginDetailSlots } = usePluginSlots({
const {
slots: workspacePluginDetailSlots,
isLoading: workspacePluginDetailSlotsLoading,
errorMessage: workspacePluginDetailSlotsError,
} = usePluginSlots({
slotTypes: ["detailTab"],
entityType: "execution_workspace",
companyId: workspace?.companyId ?? null,
@@ -1183,6 +1187,14 @@ export function ExecutionWorkspaceDetail() {
context={pluginSlotContext}
missingBehavior="placeholder"
/>
) : isExecutionWorkspacePluginTab(activeTab) && workspacePluginDetailSlotsLoading ? (
<Card>
<CardContent className="py-6 text-sm text-muted-foreground">Loading workspace plugin...</CardContent>
</Card>
) : isExecutionWorkspacePluginTab(activeTab) && workspacePluginDetailSlotsError ? (
<Card>
<CardContent className="py-6 text-sm text-destructive">{workspacePluginDetailSlotsError}</CardContent>
</Card>
) : (
<ExecutionWorkspaceRoutinesList
workspace={workspace}
+3 -2
View File
@@ -52,7 +52,7 @@ function upstreamBaseRef(workspace: ProjectWorkspace) {
if (workspace.repoRef) {
return workspace.repoRef.startsWith("origin/") ? workspace.repoRef : `origin/${workspace.repoRef.replace(/^refs\/heads\//, "")}`;
}
return "origin/master";
return null;
}
const SOURCE_TYPE_OPTIONS: Array<{ value: ProjectWorkspaceSourceType; label: string; description: string }> = [
@@ -431,8 +431,9 @@ export function ProjectWorkspaceDetail() {
const params = new URLSearchParams({
tab: "changes",
diffView: "head",
baseRef: upstreamBaseRef(workspace),
});
const baseRef = upstreamBaseRef(workspace);
if (baseRef) params.set("baseRef", baseRef);
navigate(`${workspacePath}?${params.toString()}`);
return;
}