From cbaffd2cb409df21e8409905a2a924ec5fdf05b9 Mon Sep 17 00:00:00 2001
From: neversettle <41864816+neversettle17-101@users.noreply.github.com>
Date: Sun, 5 Jul 2026 18:40:45 +0530
Subject: [PATCH] fix: simplify kanban pr footer (#2374)
---
.github/workflows/frontend-nightly.yml | 2 +-
.../integration/pr-hydration.test.tsx | 101 ++++++++++---
.../OrchestratorReplacementDialog.tsx | 9 +-
.../components/ProjectSettingsForm.test.tsx | 6 +-
.../src/renderer/components/SessionsBoard.tsx | 143 +++++++++++-------
frontend/src/renderer/components/Sidebar.tsx | 8 +-
.../renderer/hooks/useWorkspaceQuery.test.tsx | 7 +-
.../src/renderer/lib/restart-orchestrator.ts | 5 +-
frontend/src/renderer/types/workspace.test.ts | 3 +-
frontend/src/renderer/types/workspace.ts | 3 +-
10 files changed, 204 insertions(+), 83 deletions(-)
diff --git a/.github/workflows/frontend-nightly.yml b/.github/workflows/frontend-nightly.yml
index 7ad149c8f..35ec830fe 100644
--- a/.github/workflows/frontend-nightly.yml
+++ b/.github/workflows/frontend-nightly.yml
@@ -11,7 +11,7 @@ on:
workflow_dispatch:
inputs:
important:
- description: 'Mark this nightly as an important update (escalates the in-app restart prompt). Retro-flag an already-published nightly by re-running only the publish-feed job with this input set.'
+ description: "Mark this nightly as an important update (escalates the in-app restart prompt). Retro-flag an already-published nightly by re-running only the publish-feed job with this input set."
type: boolean
default: false
diff --git a/frontend/src/renderer/__tests__/integration/pr-hydration.test.tsx b/frontend/src/renderer/__tests__/integration/pr-hydration.test.tsx
index 4411716be..88b7088cb 100644
--- a/frontend/src/renderer/__tests__/integration/pr-hydration.test.tsx
+++ b/frontend/src/renderer/__tests__/integration/pr-hydration.test.tsx
@@ -38,20 +38,10 @@ function respondWithProjectAndPRs() {
projectId: "proj-1",
displayName: "fix the bug",
harness: "claude-code",
- status: "pr_open",
+ status: "draft",
isTerminated: false,
updatedAt: "2026-06-10T16:15:04Z",
prs: [
- {
- number: 278,
- state: "open",
- url: "https://github.com/aoagents/ReverbCode/pull/278",
- ci: "passing",
- review: "approved",
- mergeability: "clean",
- reviewComments: false,
- updatedAt: "2026-06-10T16:15:04Z",
- },
{
number: 279,
state: "draft",
@@ -62,6 +52,46 @@ function respondWithProjectAndPRs() {
reviewComments: false,
updatedAt: "2026-06-10T16:20:04Z",
},
+ {
+ number: 278,
+ state: "open",
+ url: "https://github.com/aoagents/ReverbCode/pull/278",
+ ci: "passing",
+ review: "review_required",
+ mergeability: "clean",
+ reviewComments: false,
+ updatedAt: "2026-06-10T16:15:04Z",
+ },
+ {
+ number: 280,
+ state: "open",
+ url: "https://github.com/aoagents/ReverbCode/issues/280",
+ ci: "passing",
+ review: "approved",
+ mergeability: "clean",
+ reviewComments: false,
+ updatedAt: "2026-06-10T16:25:04Z",
+ },
+ {
+ number: 281,
+ state: "merged",
+ url: "https://github.com/aoagents/ReverbCode/pull/281",
+ ci: "passing",
+ review: "approved",
+ mergeability: "mergeable",
+ reviewComments: false,
+ updatedAt: "2026-06-10T16:30:04Z",
+ },
+ {
+ number: 282,
+ state: "closed",
+ url: "https://github.com/aoagents/ReverbCode/pull/282",
+ ci: "passing",
+ review: "approved",
+ mergeability: "unknown",
+ reviewComments: false,
+ updatedAt: "2026-06-10T16:35:04Z",
+ },
],
},
],
@@ -179,11 +209,40 @@ beforeEach(() => {
});
describe("PR hydration for a normal project (#251)", () => {
- it("renders every session PR on the Board card instead of 'no PR yet'", async () => {
+ it("renders Board card PR numbers with lifecycle statuses only instead of 'no PR yet'", async () => {
renderWithProviders();
- expect(await screen.findByText("PR #278 · open")).toBeInTheDocument();
- expect(screen.getByText("PR #279 · draft")).toBeInTheDocument();
+ expect(await screen.findByRole("link", { name: "#278" })).toHaveAttribute(
+ "href",
+ "https://github.com/aoagents/ReverbCode/pull/278",
+ );
+ expect(screen.getByText("open")).toBeInTheDocument();
+ expect(screen.getByRole("link", { name: "#279" })).toHaveAttribute(
+ "href",
+ "https://github.com/aoagents/ReverbCode/pull/279",
+ );
+ expect(screen.getByRole("link", { name: "#280" })).toHaveAttribute(
+ "href",
+ "https://github.com/aoagents/ReverbCode/pull/280",
+ );
+ expect(screen.getByRole("link", { name: "#281" })).toHaveAttribute(
+ "href",
+ "https://github.com/aoagents/ReverbCode/pull/281",
+ );
+ expect(screen.getByRole("link", { name: "#282" })).toHaveAttribute(
+ "href",
+ "https://github.com/aoagents/ReverbCode/pull/282",
+ );
+ expect(screen.getByLabelText("#278, #280 open")).toBeInTheDocument();
+ expect(screen.getByLabelText("#279 draft")).toBeInTheDocument();
+ expect(screen.getByLabelText("#281 merged")).toBeInTheDocument();
+ expect(screen.getByLabelText("#282 closed")).toBeInTheDocument();
+ expect(screen.getByText("draft")).toBeInTheDocument();
+ expect(screen.getByText("merged")).toHaveClass("text-accent");
+ expect(screen.getByText("closed")).toHaveClass("text-error");
+ expect(screen.queryByText("review pending")).not.toBeInTheDocument();
+ expect(screen.queryByText("CI")).not.toBeInTheDocument();
+ expect(screen.queryByText("Needs attention")).not.toBeInTheDocument();
expect(screen.queryByText("no PR yet")).not.toBeInTheDocument();
});
@@ -191,14 +250,13 @@ describe("PR hydration for a normal project (#251)", () => {
respondWithAttentionPR();
renderWithProviders();
- expect(await screen.findByRole("link", { name: "PR" })).toHaveAttribute(
+ expect(await screen.findByRole("link", { name: "#278" })).toHaveAttribute(
"href",
"https://github.com/aoagents/ReverbCode/pull/278",
);
- expect(screen.getByRole("link", { name: "conflicts" })).toHaveAttribute(
- "href",
- "https://github.com/aoagents/ReverbCode/pull/278/conflicts",
- );
+ expect(screen.getByText("open")).toBeInTheDocument();
+ expect(screen.queryByText("changes requested")).not.toBeInTheDocument();
+ expect(screen.queryByRole("link", { name: "conflicts" })).not.toBeInTheDocument();
});
it("lists every session PR on the PR page instead of being empty", async () => {
@@ -206,7 +264,10 @@ describe("PR hydration for a normal project (#251)", () => {
expect(await screen.findByText("#278")).toBeInTheDocument();
expect(screen.getByText("#279")).toBeInTheDocument();
+ expect(screen.getByText("#280")).toBeInTheDocument();
+ expect(screen.getByText("#281")).toBeInTheDocument();
+ expect(screen.getByText("#282")).toBeInTheDocument();
expect(screen.queryByText("No open pull requests.")).not.toBeInTheDocument();
- expect(screen.getAllByText("fix the bug")).toHaveLength(2);
+ expect(screen.getAllByText("fix the bug")).toHaveLength(5);
});
});
diff --git a/frontend/src/renderer/components/OrchestratorReplacementDialog.tsx b/frontend/src/renderer/components/OrchestratorReplacementDialog.tsx
index c9f3817e6..8c342a789 100644
--- a/frontend/src/renderer/components/OrchestratorReplacementDialog.tsx
+++ b/frontend/src/renderer/components/OrchestratorReplacementDialog.tsx
@@ -41,13 +41,18 @@ export function OrchestratorReplacementDialog({
- Orchestrator replacement failed
+
+ Orchestrator replacement failed
+
{error ?? "The project orchestrator could not be replaced."}
-
>
) : undefined;
@@ -314,73 +319,103 @@ function SessionCard({ session, onOpen }: { session: WorkspaceSession; onOpen: (
onOpen();
};
return (
-
-
-
-
- {badge.label}
-
- {issueId && (
-
- {issueId}
+
+
+
+
+
+ {badge.label}
- )}
-
- {agentLabel(session.provider)}
-
+ {issueId && (
+
+ {issueId}
+
+ )}
+
+ {agentLabel(session.provider)}
+
+
+
+ {session.title}
+
+ {showBranch &&
{branch}
}
event.stopPropagation()}
>
- {session.title}
-
- {showBranch &&
{branch}
}
-
- {prSummaries.length > 0 ? (
-
- {prSummaries.map((prSummary, index) => (
-
0 && "border-t border-border pt-2")}
- key={prSummary.number}
- pr={prSummary}
- />
+ {prSummaries.length === 0 ? (
+ "no PR yet"
+ ) : (
+
+ {groupPRsByLifecycle(prSummaries).map((group) => (
+
))}
- ) : (
- "no PR yet"
)}
);
}
-function BoardPRSummary({ className, pr }: { className?: string; pr: SessionPRSummary }) {
- const diffSummary = prDiffSummary(pr);
+type BoardPRLifecycleStatus = { label: "closed" | "open" | "draft" | "merged"; className: string };
+type BoardPRGroup = { status: BoardPRLifecycleStatus; prs: SessionPRSummary[] };
+
+function BoardPRGroup({ group }: { group: BoardPRGroup }) {
return (
-
-
- PR #{pr.number} · {pr.state}
-
- {diffSummary ?
{diffSummary} : null}
-
-
-
+
`#${pr.number}`).join(", ")} ${group.status.label}`}
+ className="inline-flex min-w-0 flex-wrap items-center gap-x-1.5 gap-y-1"
+ >
+ PR
+ {group.prs.map((pr, index) => (
+
+
+ #{pr.number}
+
+ {index < group.prs.length - 1 ? "," : null}
+
+ ))}
+ {group.status.label}
+
);
}
+function groupPRsByLifecycle(prs: SessionPRSummary[]): BoardPRGroup[] {
+ const groups = new Map
();
+ for (const pr of prs) {
+ const status = prLifecycleStatus(pr);
+ const group = groups.get(status.label);
+ if (group) {
+ group.prs.push(pr);
+ } else {
+ groups.set(status.label, { status, prs: [pr] });
+ }
+ }
+ return Array.from(groups.values());
+}
+
+function prLifecycleStatus(pr: SessionPRSummary): BoardPRLifecycleStatus {
+ if (pr.state === "draft") return { label: "draft", className: "text-passive" };
+ if (pr.state === "merged") return { label: "merged", className: "text-accent" };
+ if (pr.state === "closed") return { label: "closed", className: "text-error" };
+ return { label: "open", className: "text-success" };
+}
+
function sameLabel(a: string, b: string): boolean {
const normalize = (value: string) =>
value
diff --git a/frontend/src/renderer/components/Sidebar.tsx b/frontend/src/renderer/components/Sidebar.tsx
index 09923768c..d405fa7c3 100644
--- a/frontend/src/renderer/components/Sidebar.tsx
+++ b/frontend/src/renderer/components/Sidebar.tsx
@@ -557,7 +557,13 @@ function ProjectItem({
- {isProjectRestarting ? "Restarting…" : isSpawning ? "Spawning…" : orchestrator ? "Orchestrator" : "Spawn orchestrator"}
+ {isProjectRestarting
+ ? "Restarting…"
+ : isSpawning
+ ? "Spawning…"
+ : orchestrator
+ ? "Orchestrator"
+ : "Spawn orchestrator"}
diff --git a/frontend/src/renderer/hooks/useWorkspaceQuery.test.tsx b/frontend/src/renderer/hooks/useWorkspaceQuery.test.tsx
index 60fc3c4e8..9e21bfa96 100644
--- a/frontend/src/renderer/hooks/useWorkspaceQuery.test.tsx
+++ b/frontend/src/renderer/hooks/useWorkspaceQuery.test.tsx
@@ -101,7 +101,12 @@ describe("useWorkspaceQuery", () => {
await waitFor(() => expect(result.current.isSuccess).toBe(true));
const [workspace] = result.current.data ?? [];
- expect(workspace).toMatchObject({ id: "proj-1", name: "my-app", path: "/home/me/my-app", orchestratorAgent: "codex" });
+ expect(workspace).toMatchObject({
+ id: "proj-1",
+ name: "my-app",
+ path: "/home/me/my-app",
+ orchestratorAgent: "codex",
+ });
expect(workspace.sessions).toHaveLength(2);
expect(workspace.sessions[0]).toMatchObject({
id: "sess-1",
diff --git a/frontend/src/renderer/lib/restart-orchestrator.ts b/frontend/src/renderer/lib/restart-orchestrator.ts
index 0d41f7981..d921a4c80 100644
--- a/frontend/src/renderer/lib/restart-orchestrator.ts
+++ b/frontend/src/renderer/lib/restart-orchestrator.ts
@@ -44,7 +44,10 @@ export async function restartProjectOrchestrator({
});
} catch (error) {
await refreshWorkspaceState(queryClient);
- setOrchestratorReplacementError(projectId, error instanceof Error ? error.message : "Could not replace orchestrator");
+ setOrchestratorReplacementError(
+ projectId,
+ error instanceof Error ? error.message : "Could not replace orchestrator",
+ );
onError?.(error);
} finally {
setProjectRestarting(projectId, false);
diff --git a/frontend/src/renderer/types/workspace.test.ts b/frontend/src/renderer/types/workspace.test.ts
index 1f9021da6..236f4fa47 100644
--- a/frontend/src/renderer/types/workspace.test.ts
+++ b/frontend/src/renderer/types/workspace.test.ts
@@ -239,7 +239,8 @@ describe("orchestratorHealth", () => {
}),
).toEqual({
state: "duplicates",
- message: "Multiple orchestrators are active. The newest one is used; stale ones will be cleaned up on daemon reconcile.",
+ message:
+ "Multiple orchestrators are active. The newest one is used; stale ones will be cleaned up on daemon reconcile.",
});
expect(
diff --git a/frontend/src/renderer/types/workspace.ts b/frontend/src/renderer/types/workspace.ts
index 9d0ad4587..683478286 100644
--- a/frontend/src/renderer/types/workspace.ts
+++ b/frontend/src/renderer/types/workspace.ts
@@ -386,7 +386,8 @@ export function orchestratorHealth(workspace: WorkspaceSummary, restarting = fal
if (active.length > 1) {
return {
state: "duplicates",
- message: "Multiple orchestrators are active. The newest one is used; stale ones will be cleaned up on daemon reconcile.",
+ message:
+ "Multiple orchestrators are active. The newest one is used; stale ones will be cleaned up on daemon reconcile.",
};
}
const orchestrator = newestActiveOrchestrator(workspace.sessions);