From 5d0de3499d3cd135443db0ee3bc7755a804318bc Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Wed, 8 Jul 2026 07:19:40 -0500 Subject: [PATCH] [codex] Fix collapsed starred project indentation (#9215) ## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - The board sidebar is a high-frequency navigation surface for companies, projects, and work queues. > - Starred projects render as child rows under Projects in the expanded sidebar. > - The collapsed rail should align every nav icon in the same rail column. > - The starred-project child indent was still applied in the collapsed rail, which pushed the project glyph out of alignment. > - This pull request keeps the expanded hierarchy indent while removing it only for the collapsed rail. > - The benefit is a cleaner collapsed sidebar without changing expanded sidebar hierarchy. ## Linked Issues or Issue Description No public GitHub issue exists. ## What happened? In the collapsed sidebar rail, starred project rows kept the expanded child indentation. That pushed the project glyph out of alignment with the rest of the collapsed sidebar icons. ## Expected behavior Collapsed starred project icons should align with the other sidebar rail icons while the expanded sidebar should keep the child-row indentation under Projects. ## Steps to reproduce 1. Open Paperclip with at least one starred project. 2. Collapse the sidebar into rail mode. 3. Compare the starred project glyph position with the other collapsed sidebar glyphs. ## Paperclip version or commit Reproduced on the PR base before this branch; fixed on commit e57d14343d28f4da2cb8d93847aa328529cd01e3 with CI cleanup on commit 12aafe8959351826038baf0c1e401fb44913c67c. ## Deployment mode Local dev (`pnpm dev`). ## What Changed - Applies the starred-project left padding only when the sidebar is not in rail mode. - Adds a regression test covering expanded and collapsed starred-project rendering. - Makes the heartbeat worktree suppression test cleanup tolerate late heartbeat run events before deleting heartbeat runs. ## Verification - Passed: `pnpm exec vitest run ui/src/components/SidebarStarredProjects.test.tsx` - Passed: `pnpm exec vitest run server/src/__tests__/heartbeat-worktree-suppression.test.ts` - Passed: PR #9215 latest-head GitHub checks on commit `12aafe8959351826038baf0c1e401fb44913c67c` - Passed: Greptile 5/5 with no inline comments or unresolved review threads on commit `12aafe8959351826038baf0c1e401fb44913c67c` - Known existing baseline failure: `pnpm check:token-gates` reports violations in `ui/src/components/ActivityCharts.tsx` and `ui/src/components/IssueRecoveryActionCard.tsx`, which this PR does not touch. ## Risks Low risk. The UI change adjusts one conditional class on starred project links and preserves the expanded sidebar layout. The server test change is cleanup-only and does not alter production behavior. ## Model Used OpenAI GPT-5 Codex coding agent with local command execution and repository editing tools. ## 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 searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip --- .../heartbeat-worktree-suppression.test.ts | 21 ++++++++++++-- .../SidebarStarredProjects.test.tsx | 28 +++++++++++++++++++ ui/src/components/SidebarStarredProjects.tsx | 3 +- 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/server/src/__tests__/heartbeat-worktree-suppression.test.ts b/server/src/__tests__/heartbeat-worktree-suppression.test.ts index 71d9255eff..bb1e23c944 100644 --- a/server/src/__tests__/heartbeat-worktree-suppression.test.ts +++ b/server/src/__tests__/heartbeat-worktree-suppression.test.ts @@ -36,19 +36,36 @@ describeEmbeddedPostgres("heartbeat worktree suppression", () => { let db!: ReturnType; let tempDb: Awaited> | null = null; + function isHeartbeatRunEventFkError(error: unknown) { + const message = error instanceof Error ? `${error.message} ${String(error.cause ?? "")}` : String(error); + return message.includes("heartbeat_run_events_run_id_heartbeat_runs_id_fk"); + } + + async function deleteHeartbeatRunsWithEvents() { + for (let attempt = 0; attempt < 5; attempt += 1) { + await db.delete(heartbeatRunEvents); + try { + await db.delete(heartbeatRuns); + return; + } catch (error) { + if (!isHeartbeatRunEventFkError(error) || attempt === 4) throw error; + await new Promise((resolve) => setTimeout(resolve, 25)); + } + } + } + beforeAll(async () => { tempDb = await startEmbeddedPostgresTestDatabase("heartbeat-worktree-suppression-"); db = createDb(tempDb.connectionString); }, 20_000); afterEach(async () => { - await db.delete(heartbeatRunEvents); await db.delete(issueComments); await db.delete(issueDocuments); await db.delete(documentRevisions); await db.delete(documents); await db.delete(activityLog); - await db.delete(heartbeatRuns); + await deleteHeartbeatRunsWithEvents(); await db.delete(agentWakeupRequests); await db.delete(issues); await db.delete(agentRuntimeState); diff --git a/ui/src/components/SidebarStarredProjects.test.tsx b/ui/src/components/SidebarStarredProjects.test.tsx index 501da6a2d3..e8088f4f46 100644 --- a/ui/src/components/SidebarStarredProjects.test.tsx +++ b/ui/src/components/SidebarStarredProjects.test.tsx @@ -112,6 +112,10 @@ function projectLinkLabels(container: HTMLElement) { .filter(Boolean); } +function projectLink(container: HTMLElement, projectRef: string) { + return container.querySelector(`a[href="/projects/${projectRef}/issues"]`) as HTMLAnchorElement | null; +} + describe("SidebarStarredProjects", () => { let container: HTMLDivElement; let root: ReturnType | null; @@ -176,6 +180,30 @@ describe("SidebarStarredProjects", () => { expect(document.body.querySelector('button[aria-label="Unstar Bravo"]')).not.toBeNull(); }); + it("keeps starred projects indented only outside the collapsed rail", async () => { + mockProjectsApi.list.mockResolvedValue([ + makeProject({ id: "project-a", name: "Alpha", urlKey: "alpha" }), + ]); + memberships = { ...memberships, starredProjectIds: ["project-a"] }; + + await render(); + + expect(projectLink(container, "alpha")?.className).toContain("pl-8"); + + await act(async () => root?.unmount()); + root = null; + container.innerHTML = ""; + queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } } }); + mockSidebarState.collapsed = true; + + await render(); + + const railProjectLink = projectLink(container, "alpha"); + expect(railProjectLink?.className).not.toContain("pl-8"); + const nameSpan = Array.from(container.querySelectorAll("span")).find((el) => el.textContent === "Alpha"); + expect(nameSpan?.className).toContain("w-0"); + }); + it("renders nothing when no projects are starred", async () => { mockProjectsApi.list.mockResolvedValue([makeProject({ id: "project-a", name: "Alpha" })]); diff --git a/ui/src/components/SidebarStarredProjects.tsx b/ui/src/components/SidebarStarredProjects.tsx index 1b0925666e..f6e8711b17 100644 --- a/ui/src/components/SidebarStarredProjects.tsx +++ b/ui/src/components/SidebarStarredProjects.tsx @@ -121,7 +121,8 @@ export function SidebarStarredProjects() { if (isMobile) setSidebarOpen(false); }} className={cn( - "flex min-w-0 flex-1 items-center gap-2.5 px-3 py-1.5 pl-8 pointer-coarse:py-1 pr-8 text-(length:--text-compact) font-medium transition-colors", + "flex min-w-0 flex-1 items-center gap-2.5 px-3 py-1.5 pointer-coarse:py-1 pr-8 text-(length:--text-compact) font-medium transition-colors", + !rail && "pl-8", isActive ? "bg-accent text-foreground" : "text-foreground/80 hover:bg-accent/50 hover:text-foreground",