[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:
Dotta
2026-05-25 07:25:26 -05:00
committed by GitHub
parent 96f0279e08
commit ece8a51e22
38 changed files with 1715 additions and 134 deletions
+80 -4
View File
@@ -1,5 +1,6 @@
import { useEffect, useMemo } from "react";
import { useEffect, useMemo, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import type { Project } from "@paperclipai/shared";
import { projectsApi } from "../api/projects";
import { useCompany } from "../context/CompanyContext";
import { useDialogActions } from "../context/DialogContext";
@@ -11,12 +12,67 @@ import { EmptyState } from "../components/EmptyState";
import { PageSkeleton } from "../components/PageSkeleton";
import { formatDate, projectUrl } from "../lib/utils";
import { Button } from "@/components/ui/button";
import { Hexagon, Plus } from "lucide-react";
import { ArrowUpDown, Hexagon, Plus } from "lucide-react";
type ProjectSortMode = "name" | "updated" | "targetDate" | "status";
const PROJECT_SORT_LABELS: Record<ProjectSortMode, string> = {
name: "Name",
updated: "Recently updated",
targetDate: "Target date",
status: "Status",
};
const PROJECT_STATUS_RANK: Record<Project["status"], number> = {
in_progress: 0,
planned: 1,
backlog: 2,
completed: 3,
cancelled: 4,
};
function projectTime(project: Project, field: "createdAt" | "updatedAt"): number {
const value = project[field];
const time = value instanceof Date ? value.getTime() : new Date(value).getTime();
return Number.isFinite(time) ? time : 0;
}
function compareProjectNames(left: Project, right: Project): number {
const nameDiff = left.name.localeCompare(right.name, undefined, { sensitivity: "base" });
return nameDiff !== 0 ? nameDiff : left.id.localeCompare(right.id);
}
function compareTargetDates(left: Project, right: Project): number {
if (!left.targetDate && !right.targetDate) return compareProjectNames(left, right);
if (!left.targetDate) return 1;
if (!right.targetDate) return -1;
const dateDiff = left.targetDate.localeCompare(right.targetDate);
return dateDiff !== 0 ? dateDiff : compareProjectNames(left, right);
}
function sortProjects(projects: Project[], sortMode: ProjectSortMode): Project[] {
return [...projects].sort((left, right) => {
if (sortMode === "updated") {
const updatedDiff = projectTime(right, "updatedAt") - projectTime(left, "updatedAt");
return updatedDiff !== 0 ? updatedDiff : compareProjectNames(left, right);
}
if (sortMode === "targetDate") {
return compareTargetDates(left, right);
}
if (sortMode === "status") {
const statusDiff = PROJECT_STATUS_RANK[left.status] - PROJECT_STATUS_RANK[right.status];
return statusDiff !== 0 ? statusDiff : compareProjectNames(left, right);
}
return compareProjectNames(left, right);
});
}
export function Projects() {
const { selectedCompanyId } = useCompany();
const { openNewProject } = useDialogActions();
const { setBreadcrumbs } = useBreadcrumbs();
const [sortMode, setSortMode] = useState<ProjectSortMode>("name");
useEffect(() => {
setBreadcrumbs([{ label: "Projects" }]);
@@ -31,6 +87,10 @@ export function Projects() {
() => (allProjects ?? []).filter((p) => !p.archivedAt),
[allProjects],
);
const sortedProjects = useMemo(
() => sortProjects(projects, sortMode),
[projects, sortMode],
);
if (!selectedCompanyId) {
return <EmptyState icon={Hexagon} message="Select a company to view projects." />;
@@ -42,7 +102,22 @@ export function Projects() {
return (
<div className="space-y-4">
<div className="flex items-center justify-end">
<div className="flex flex-wrap items-center justify-between gap-3">
<label className="flex items-center gap-2 text-sm text-muted-foreground">
<ArrowUpDown className="h-4 w-4" />
<span>Sort</span>
<select
className="rounded-md border border-border bg-background px-2.5 py-1.5 text-sm text-foreground outline-none transition-colors hover:bg-accent/50 focus-visible:ring-2 focus-visible:ring-ring"
value={sortMode}
onChange={(event) => setSortMode(event.target.value as ProjectSortMode)}
>
{(Object.keys(PROJECT_SORT_LABELS) as ProjectSortMode[]).map((value) => (
<option key={value} value={value}>
{PROJECT_SORT_LABELS[value]}
</option>
))}
</select>
</label>
<Button size="sm" variant="outline" onClick={openNewProject}>
<Plus className="h-4 w-4 mr-1" />
Add Project
@@ -62,11 +137,12 @@ export function Projects() {
{projects.length > 0 && (
<div className="border border-border">
{projects.map((project) => (
{sortedProjects.map((project) => (
<EntityRow
key={project.id}
title={project.name}
subtitle={project.description ?? undefined}
reserveSubtitleSpace
to={projectUrl(project)}
trailing={
<div className="flex items-center gap-3">