diff --git a/apps/web/src/components/Sidebar.logic.test.ts b/apps/web/src/components/Sidebar.logic.test.ts index f9a62c40a511..e06176dfc48e 100644 --- a/apps/web/src/components/Sidebar.logic.test.ts +++ b/apps/web/src/components/Sidebar.logic.test.ts @@ -395,6 +395,18 @@ describe("shouldRecedeSidebarThread", () => { expect(shouldRecedeSidebarThread({ ...input, isActive: true })).toBe(false); expect(shouldRecedeSidebarThread({ ...input, isSelected: true })).toBe(false); }); + + it.each([false, true])("keeps input-required threads prominent with unread=%s", (isUnread) => { + expect( + shouldRecedeSidebarThread({ + status: "input", + isUnread, + isWoke: false, + isActive: false, + isSelected: false, + }), + ).toBe(false); + }); }); describe("createThreadJumpHintVisibilityController", () => { diff --git a/apps/web/src/components/Sidebar.logic.ts b/apps/web/src/components/Sidebar.logic.ts index 50650ed389dc..abb67e65a24e 100644 --- a/apps/web/src/components/Sidebar.logic.ts +++ b/apps/web/src/components/Sidebar.logic.ts @@ -803,9 +803,9 @@ export function shouldRecedeSidebarThread(input: { isActive: boolean; isSelected: boolean; }): boolean { - if (input.isActive || input.isSelected) return false; + if (input.isActive || input.isSelected || input.status === "input") return false; if (input.status === "working" || input.status === "monitoring") return true; - if (input.status === "ready" || input.status === "approval" || input.status === "input") { + if (input.status === "ready" || input.status === "approval") { return !input.isUnread && !input.isWoke; } return false; diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 445ae533c323..792aa3815fcf 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -45,12 +45,15 @@ import { CircleCheckIcon, CircleDashedIcon, ClockIcon, + EyeIcon, FolderIcon, GitBranchIcon, + MessageCircleQuestionIcon, PinIcon, PinOffIcon, PlusIcon, SettingsIcon, + ShieldQuestionIcon, SquarePenIcon, TerminalIcon, Undo2Icon, @@ -1088,8 +1091,6 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { // switching sidebars must not light up every historical thread as unread. const isUnread = hasUnseenCompletion({ ...thread, lastVisitedAt }); const status = resolveSidebarThreadStatus(thread); - const isInFlight = - status === "working" || status === "monitoring" || status === "approval" || status === "input"; // A woken thread reappears at its original position (the sort is // deliberately static), so the pill has to carry the weight. Snoozing is // an explicit act, so the pill clears only when the user re-engages: @@ -1123,35 +1124,32 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { icon: "working" as const, // No shimmer: a label that animates forever is noise in a sidebar // full of them (and repaints every vsync on high-refresh displays). - // Working is a background state, so it rests at the dim end of what - // the old pulse cycled through; only the thread you have open gets - // the label at full strength. - className: cn("text-sky-600 dark:text-sky-400", !props.isActive && "opacity-75"), + className: "text-sky-600 dark:text-sky-400", } : status === "monitoring" ? { // Monitoring is calm background presence, not active progress // (monitoring-pill D6), so it keeps the label at full strength. label: "Monitoring", - icon: null, - className: "text-sky-600 dark:text-sky-400", + icon: "monitoring" as const, + className: "text-foreground dark:text-white", } : status === "approval" ? { label: "Approval", - icon: null, + icon: "approval" as const, className: "text-amber-700 dark:text-amber-300", } : status === "input" ? { label: "Input", - icon: null, + icon: "input" as const, className: "text-indigo-600 dark:text-indigo-300", } : status === "failed" ? { label: "Failed", - icon: null, + icon: "failed" as const, className: "text-red-700 dark:text-red-300", } : isWoke @@ -1396,10 +1394,6 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { : shouldRecede ? "text-sidebar-muted-foreground/75 hover:bg-sidebar-row-hover hover:text-sidebar-foreground" : "bg-transparent text-sidebar-foreground hover:bg-sidebar-row-hover", - isInFlight && - !props.isActive && - !isSelected && - "opacity-70 transition-opacity hover:opacity-100", isFileDragOver && "ring-1 ring-inset ring-primary/70", // The hover tint must not clobber an active/selected row's own surface. isFileDragOver && !props.isActive && !isSelected && "bg-sidebar-row-hover", @@ -1462,7 +1456,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { "truncate", shouldRecede ? "text-secondary-label" - : isUnread || isWoke + : isUnread || isWoke || status === "input" ? "text-foreground" : status === "failed" ? "text-foreground/95" @@ -1472,7 +1466,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { "truncate group-focus-within/sidebar-row:text-foreground group-hover/sidebar-row:text-foreground", shouldRecede ? "text-secondary-label/70" - : props.isActive || isWoke + : props.isActive || isWoke || status === "input" ? "text-foreground" : isUnread ? "text-muted-foreground" @@ -1814,6 +1808,14 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { > {topStatus.icon === "working" ? ( + ) : topStatus.icon === "input" ? ( + + ) : topStatus.icon === "approval" ? ( + + ) : topStatus.icon === "failed" ? ( + + ) : topStatus.icon === "monitoring" ? ( + ) : topStatus.icon === "done" ? ( ) : null}