[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
e57d14343d with CI cleanup on commit
12aafe8959.

## 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 <noreply@paperclip.ing>
This commit is contained in:
Dotta 2026-07-08 07:19:40 -05:00 committed by GitHub
parent 555391fed7
commit 5d0de3499d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 49 additions and 3 deletions

View File

@ -36,19 +36,36 @@ describeEmbeddedPostgres("heartbeat worktree suppression", () => {
let db!: ReturnType<typeof createDb>;
let tempDb: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | 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);

View File

@ -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<typeof createRoot> | 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" })]);

View File

@ -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",