Skip to content

Commit 20e79bd

Browse files
authored
Merge branch 'main' into fix/provider-health-banner-wrong-provider
2 parents 72d0d03 + bc12421 commit 20e79bd

3 files changed

Lines changed: 111 additions & 6 deletions

File tree

apps/web/src/components/Sidebar.logic.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
22

33
import {
44
hasUnseenCompletion,
5+
resolveProjectStatusIndicator,
56
resolveSidebarNewThreadEnvMode,
67
resolveThreadRowClassName,
78
resolveThreadStatusPill,
@@ -230,3 +231,53 @@ describe("resolveThreadRowClassName", () => {
230231
expect(className).toContain("hover:bg-accent");
231232
});
232233
});
234+
235+
describe("resolveProjectStatusIndicator", () => {
236+
it("returns null when no threads have a notable status", () => {
237+
expect(resolveProjectStatusIndicator([null, null])).toBeNull();
238+
});
239+
240+
it("surfaces the highest-priority actionable state across project threads", () => {
241+
expect(
242+
resolveProjectStatusIndicator([
243+
{
244+
label: "Completed",
245+
colorClass: "text-emerald-600",
246+
dotClass: "bg-emerald-500",
247+
pulse: false,
248+
},
249+
{
250+
label: "Pending Approval",
251+
colorClass: "text-amber-600",
252+
dotClass: "bg-amber-500",
253+
pulse: false,
254+
},
255+
{
256+
label: "Working",
257+
colorClass: "text-sky-600",
258+
dotClass: "bg-sky-500",
259+
pulse: true,
260+
},
261+
]),
262+
).toMatchObject({ label: "Pending Approval", dotClass: "bg-amber-500" });
263+
});
264+
265+
it("prefers plan-ready over completed when no stronger action is needed", () => {
266+
expect(
267+
resolveProjectStatusIndicator([
268+
{
269+
label: "Completed",
270+
colorClass: "text-emerald-600",
271+
dotClass: "bg-emerald-500",
272+
pulse: false,
273+
},
274+
{
275+
label: "Plan Ready",
276+
colorClass: "text-violet-600",
277+
dotClass: "bg-violet-500",
278+
pulse: false,
279+
},
280+
]),
281+
).toMatchObject({ label: "Plan Ready", dotClass: "bg-violet-500" });
282+
});
283+
});

apps/web/src/components/Sidebar.logic.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,15 @@ export interface ThreadStatusPill {
2222
pulse: boolean;
2323
}
2424

25+
const THREAD_STATUS_PRIORITY: Record<ThreadStatusPill["label"], number> = {
26+
"Pending Approval": 5,
27+
"Awaiting Input": 4,
28+
Working: 3,
29+
Connecting: 3,
30+
"Plan Ready": 2,
31+
Completed: 1,
32+
};
33+
2534
type ThreadStatusInput = Pick<
2635
Thread,
2736
"interactionMode" | "latestTurn" | "lastVisitedAt" | "proposedPlans" | "session"
@@ -151,3 +160,21 @@ export function resolveThreadStatusPill(input: {
151160

152161
return null;
153162
}
163+
164+
export function resolveProjectStatusIndicator(
165+
statuses: ReadonlyArray<ThreadStatusPill | null>,
166+
): ThreadStatusPill | null {
167+
let highestPriorityStatus: ThreadStatusPill | null = null;
168+
169+
for (const status of statuses) {
170+
if (status === null) continue;
171+
if (
172+
highestPriorityStatus === null ||
173+
THREAD_STATUS_PRIORITY[status.label] > THREAD_STATUS_PRIORITY[highestPriorityStatus.label]
174+
) {
175+
highestPriorityStatus = status;
176+
}
177+
}
178+
179+
return highestPriorityStatus;
180+
}

apps/web/src/components/Sidebar.tsx

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ import { useThreadSelectionStore } from "../threadSelectionStore";
8484
import { formatWorktreePathForDisplay, getOrphanedWorktreePathForThread } from "../worktreeCleanup";
8585
import { isNonEmpty as isNonEmptyString } from "effect/String";
8686
import {
87+
resolveProjectStatusIndicator,
8788
resolveSidebarNewThreadEnvMode,
8889
resolveThreadRowClassName,
8990
resolveThreadStatusPill,
@@ -1340,13 +1341,22 @@ export default function Sidebar() {
13401341
if (byDate !== 0) return byDate;
13411342
return b.id.localeCompare(a.id);
13421343
});
1344+
const projectStatus = resolveProjectStatusIndicator(
1345+
projectThreads.map((thread) =>
1346+
resolveThreadStatusPill({
1347+
thread,
1348+
hasPendingApprovals: derivePendingApprovals(thread.activities).length > 0,
1349+
hasPendingUserInput: derivePendingUserInputs(thread.activities).length > 0,
1350+
}),
1351+
),
1352+
);
13431353
const isThreadListExpanded = expandedThreadListsByProject.has(project.id);
13441354
const hasHiddenThreads = projectThreads.length > THREAD_PREVIEW_LIMIT;
13451355
const visibleThreads =
13461356
hasHiddenThreads && !isThreadListExpanded
13471357
? projectThreads.slice(0, THREAD_PREVIEW_LIMIT)
13481358
: projectThreads;
1349-
const orderedProjectThreadIds = projectThreads.map((t) => t.id);
1359+
const orderedProjectThreadIds = projectThreads.map((thread) => thread.id);
13501360

13511361
return (
13521362
<SortableProjectItem key={project.id} projectId={project.id}>
@@ -1369,11 +1379,28 @@ export default function Sidebar() {
13691379
});
13701380
}}
13711381
>
1372-
<ChevronRightIcon
1373-
className={`-ml-0.5 size-3.5 shrink-0 text-muted-foreground/70 transition-transform duration-150 ${
1374-
project.expanded ? "rotate-90" : ""
1375-
}`}
1376-
/>
1382+
{!project.expanded && projectStatus ? (
1383+
<span
1384+
aria-hidden="true"
1385+
title={projectStatus.label}
1386+
className={`-ml-0.5 relative inline-flex size-3.5 shrink-0 items-center justify-center ${projectStatus.colorClass}`}
1387+
>
1388+
<span className="absolute inset-0 flex items-center justify-center transition-opacity duration-150 group-hover/project-header:opacity-0">
1389+
<span
1390+
className={`size-[9px] rounded-full ${projectStatus.dotClass} ${
1391+
projectStatus.pulse ? "animate-pulse" : ""
1392+
}`}
1393+
/>
1394+
</span>
1395+
<ChevronRightIcon className="absolute inset-0 m-auto size-3.5 text-muted-foreground/70 opacity-0 transition-opacity duration-150 group-hover/project-header:opacity-100" />
1396+
</span>
1397+
) : (
1398+
<ChevronRightIcon
1399+
className={`-ml-0.5 size-3.5 shrink-0 text-muted-foreground/70 transition-transform duration-150 ${
1400+
project.expanded ? "rotate-90" : ""
1401+
}`}
1402+
/>
1403+
)}
13771404
<ProjectFavicon cwd={project.cwd} />
13781405
<span className="flex-1 truncate text-xs font-medium text-foreground/90">
13791406
{project.name}

0 commit comments

Comments
 (0)