forked from farhoodlabs/paperclip
[codex] Bundle local branch fixes from PAP-10032 (#6604)
## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies. > - This branch accumulated multiple already-tested control-plane, adapter runtime, invite, workspace, plugin, and UI quality fixes on the primary Paperclip checkout. > - `origin/master` advanced while those commits were still local, so the branch needed to be preserved and reconciled before review. > - Splitting the branch commit-by-commit against the new base produced overlapping conflicts with recently merged upstream PRs. > - This pull request keeps the remaining branch as one standalone PR because the final diff is 38 files after removing screenshot artifacts, under Greptile's 100-file cap, and can be merged independently after review. > - The benefit is that none of the local work is lost, the branch is now based on current `origin/master`, and reviewers can evaluate the reconciled changes in one place. ## What Changed - Merged the local accumulated branch with current `origin/master` and resolved the invite-flow overlaps from the newer upstream companies query helper. - Preserved the local fixes for invite existing-member behavior, invite link copy fallback, reusable workspace selection, worktree auth, static SPA fallback, markdown wrapping, plugin slot registration, cloud upstream UX/server polish, project sorting, and related tests. - Removed screenshot artifacts from the PR per review request. - Kept the PR under the requested file limit: 38 files changed, with no `pnpm-lock.yaml` or `.github/workflows/*` changes. ## Verification - `NODE_ENV=test pnpm exec vitest run ui/src/pages/CompanyInvites.test.tsx ui/src/pages/InviteLanding.test.tsx ui/src/pages/Projects.test.tsx ui/src/plugins/slots.test.ts ui/src/components/MarkdownBody.test.tsx server/src/__tests__/invite-accept-existing-member.test.ts server/src/__tests__/static-index-html.test.ts server/src/__tests__/execution-workspaces-service.test.ts server/src/__tests__/better-auth.test.ts server/src/__tests__/worktree-config.test.ts` - `NODE_ENV=test pnpm --filter @paperclipai/ui typecheck` - `NODE_ENV=test pnpm --filter @paperclipai/server typecheck` - Confirmed `git diff --name-only origin/master...HEAD | wc -l` is `38`. - Confirmed no PR diff entries match `pnpm-lock.yaml`, `.github/workflows/*`, or `screenshots/*`. ## Risks - Medium review risk because this is a bundled rescue PR rather than several narrow feature PRs. - Invite flow and company cache behavior overlapped with newer upstream changes; the merge resolution intentionally keeps the shared `companiesListQueryOptions` helper while preserving local existing-member invite behavior. - Visual review evidence is no longer attached in-repo because screenshots were removed from this PR per review request. > 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 repository tool access, terminal execution, and git/GitHub CLI operations. ## 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] UI screenshots were intentionally removed from this PR per review request - [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> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: CodexCoder <codexcoder@paperclip.local>
This commit is contained in:
@@ -103,6 +103,7 @@ type UpstreamTransferManifest = {
|
||||
idempotencyKey: string;
|
||||
generatedAt: string;
|
||||
entityCount: number;
|
||||
perEntityTypeCounts: Record<string, number>;
|
||||
entities: UpstreamTransferEntityRecord[];
|
||||
chunks: Array<Omit<LocalUpstreamExportChunk, "payload"> & { manifestHash: NormalizedSha256 }>;
|
||||
warnings: UpstreamTransferWarning[];
|
||||
@@ -914,6 +915,7 @@ function buildLocalUpstreamExportBundle(input: {
|
||||
idempotencyKey: input.idempotencyKey,
|
||||
generatedAt: new Date(0).toISOString(),
|
||||
entityCount: entities.length,
|
||||
perEntityTypeCounts: countEntityTypesForManifest(entities),
|
||||
entities: entities.map((entity) => entity.record),
|
||||
chunks: chunksWithoutManifestHash.map(({ payload: _payload, ...chunk }) => chunk),
|
||||
warnings: input.warnings ?? [],
|
||||
@@ -931,6 +933,15 @@ function buildLocalUpstreamExportBundle(input: {
|
||||
};
|
||||
}
|
||||
|
||||
function countEntityTypesForManifest(entities: LocalUpstreamExportEntity[]): Record<string, number> {
|
||||
const counts: Record<string, number> = {};
|
||||
for (const entity of entities) {
|
||||
const entityType = entity.record.key.sourceEntityType;
|
||||
counts[entityType] = (counts[entityType] ?? 0) + 1;
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
function buildLocalChunks(entities: LocalUpstreamExportEntity[], maxEntitiesPerChunk: number): LocalUpstreamExportChunk[] {
|
||||
if (!Number.isInteger(maxEntitiesPerChunk) || maxEntitiesPerChunk < 1) {
|
||||
throw new Error("maxEntitiesPerChunk must be a positive integer");
|
||||
|
||||
@@ -2,7 +2,7 @@ import { execFile } from "node:child_process";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { and, desc, eq, inArray } from "drizzle-orm";
|
||||
import { and, desc, eq, inArray, isNull } from "drizzle-orm";
|
||||
import type { Db } from "@paperclipai/db";
|
||||
import { executionWorkspaces, issues, projects, projectWorkspaces, workspaceRuntimeServices } from "@paperclipai/db";
|
||||
import type {
|
||||
@@ -344,12 +344,18 @@ function toExecutionWorkspace(
|
||||
};
|
||||
}
|
||||
|
||||
function toExecutionWorkspaceSummary(row: Pick<ExecutionWorkspaceRow, "id" | "name" | "mode" | "projectWorkspaceId">): ExecutionWorkspaceSummary {
|
||||
function toExecutionWorkspaceSummary(
|
||||
row: Pick<ExecutionWorkspaceRow, "id" | "name" | "mode" | "status" | "cwd" | "branchName" | "projectWorkspaceId" | "lastUsedAt">,
|
||||
): ExecutionWorkspaceSummary {
|
||||
return {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
mode: row.mode as ExecutionWorkspaceSummary["mode"],
|
||||
status: row.status as ExecutionWorkspaceSummary["status"],
|
||||
cwd: row.cwd ?? null,
|
||||
branchName: row.branchName ?? null,
|
||||
projectWorkspaceId: row.projectWorkspaceId ?? null,
|
||||
lastUsedAt: row.lastUsedAt,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -412,6 +418,8 @@ export function executionWorkspaceService(db: Db) {
|
||||
}
|
||||
if (filters?.reuseEligible) {
|
||||
conditions.push(inArray(executionWorkspaces.status, ["active", "idle", "in_review"]));
|
||||
conditions.push(isNull(executionWorkspaces.closedAt));
|
||||
conditions.push(inArray(executionWorkspaces.mode, ["isolated_workspace", "operator_branch", "adapter_managed", "cloud_sandbox"]));
|
||||
}
|
||||
return conditions;
|
||||
}
|
||||
@@ -452,7 +460,11 @@ export function executionWorkspaceService(db: Db) {
|
||||
id: executionWorkspaces.id,
|
||||
name: executionWorkspaces.name,
|
||||
mode: executionWorkspaces.mode,
|
||||
status: executionWorkspaces.status,
|
||||
cwd: executionWorkspaces.cwd,
|
||||
branchName: executionWorkspaces.branchName,
|
||||
projectWorkspaceId: executionWorkspaces.projectWorkspaceId,
|
||||
lastUsedAt: executionWorkspaces.lastUsedAt,
|
||||
})
|
||||
.from(executionWorkspaces)
|
||||
.where(and(...conditions))
|
||||
|
||||
@@ -559,7 +559,9 @@ export function createPluginWorkerHandle(
|
||||
(message as { paperclipInvocationId?: unknown }).paperclipInvocationId,
|
||||
);
|
||||
if (!invocationId) {
|
||||
return activeInvocations.size > 0 ? { invalidInvocationScope: true } : {};
|
||||
const hasActiveInvocation = activeInvocations.size > 0 ||
|
||||
Array.from(pendingRequests.values()).some((pending) => pending.invocationId);
|
||||
return hasActiveInvocation ? { invalidInvocationScope: true } : {};
|
||||
}
|
||||
const entry = activeInvocations.get(invocationId);
|
||||
if (!entry) return { invalidInvocationScope: true };
|
||||
|
||||
Reference in New Issue
Block a user