authorgravatar for maria@kuuro.netmaria <maria@kuuro.net> 2026-09-12 18:38:07-03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-12 18:38:07-03:00
log03e135577cbf861c2948981601c070dab038ea11
tree1625618af5c9f2ac166874a6f9cbbb563844d408
parentcfeaca41ae27bdf2c203158d378c87c7308fea2a
signature Signed by PGP key B5690EEEBB952194

fix(web): complete thread status icons and keep input threads prominent (#11461)

Co-authored-by: maria-rcks <254055478+maria-rcks@users.noreply.github.com>

3 files changed, 33 insertions(+), 19 deletions(-)

apps/web/src/components/Sidebar.logic.test.ts+12
...@@ -395,6 +395,18 @@ describe("shouldRecedeSidebarThread", () => {...@@ -395,6 +395,18 @@ describe("shouldRecedeSidebarThread", () => {
395 expect(shouldRecedeSidebarThread({ ...input, isActive: true })).toBe(false);395 expect(shouldRecedeSidebarThread({ ...input, isActive: true })).toBe(false);
396 expect(shouldRecedeSidebarThread({ ...input, isSelected: true })).toBe(false);396 expect(shouldRecedeSidebarThread({ ...input, isSelected: true })).toBe(false);
397 });397 });
398
399 it.each([false, true])("keeps input-required threads prominent with unread=%s", (isUnread) => {
400 expect(
401 shouldRecedeSidebarThread({
402 status: "input",
403 isUnread,
404 isWoke: false,
405 isActive: false,
406 isSelected: false,
407 }),
408 ).toBe(false);
409 });
398});410});
399411
400describe("createThreadJumpHintVisibilityController", () => {412describe("createThreadJumpHintVisibilityController", () => {
apps/web/src/components/Sidebar.logic.ts+2-2
...@@ -803,9 +803,9 @@ export function shouldRecedeSidebarThread(input: {...@@ -803,9 +803,9 @@ export function shouldRecedeSidebarThread(input: {
803 isActive: boolean;803 isActive: boolean;
804 isSelected: boolean;804 isSelected: boolean;
805}): boolean {805}): boolean {
806 if (input.isActive || input.isSelected) return false;806 if (input.isActive || input.isSelected || input.status === "input") return false;
807 if (input.status === "working" || input.status === "monitoring") return true;807 if (input.status === "working" || input.status === "monitoring") return true;
808 if (input.status === "ready" || input.status === "approval" || input.status === "input") {808 if (input.status === "ready" || input.status === "approval") {
809 return !input.isUnread && !input.isWoke;809 return !input.isUnread && !input.isWoke;
810 }810 }
811 return false;811 return false;
apps/web/src/components/Sidebar.tsx+19-17
...@@ -45,12 +45,15 @@ import {...@@ -45,12 +45,15 @@ import {
45 CircleCheckIcon,45 CircleCheckIcon,
46 CircleDashedIcon,46 CircleDashedIcon,
47 ClockIcon,47 ClockIcon,
48 EyeIcon,
48 FolderIcon,49 FolderIcon,
49 GitBranchIcon,50 GitBranchIcon,
51 MessageCircleQuestionIcon,
50 PinIcon,52 PinIcon,
51 PinOffIcon,53 PinOffIcon,
52 PlusIcon,54 PlusIcon,
53 SettingsIcon,55 SettingsIcon,
56 ShieldQuestionIcon,
54 SquarePenIcon,57 SquarePenIcon,
55 TerminalIcon,58 TerminalIcon,
56 Undo2Icon,59 Undo2Icon,
...@@ -1088,8 +1091,6 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {...@@ -1088,8 +1091,6 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
1088 // switching sidebars must not light up every historical thread as unread.1091 // switching sidebars must not light up every historical thread as unread.
1089 const isUnread = hasUnseenCompletion({ ...thread, lastVisitedAt });1092 const isUnread = hasUnseenCompletion({ ...thread, lastVisitedAt });
1090 const status = resolveSidebarThreadStatus(thread);1093 const status = resolveSidebarThreadStatus(thread);
1091 const isInFlight =
1092 status === "working" || status === "monitoring" || status === "approval" || status === "input";
1093 // A woken thread reappears at its original position (the sort is1094 // A woken thread reappears at its original position (the sort is
1094 // deliberately static), so the pill has to carry the weight. Snoozing is1095 // deliberately static), so the pill has to carry the weight. Snoozing is
1095 // an explicit act, so the pill clears only when the user re-engages:1096 // an explicit act, so the pill clears only when the user re-engages:
...@@ -1123,35 +1124,32 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {...@@ -1123,35 +1124,32 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
1123 icon: "working" as const,1124 icon: "working" as const,
1124 // No shimmer: a label that animates forever is noise in a sidebar1125 // No shimmer: a label that animates forever is noise in a sidebar
1125 // full of them (and repaints every vsync on high-refresh displays).1126 // full of them (and repaints every vsync on high-refresh displays).
1126 // Working is a background state, so it rests at the dim end of what1127 className: "text-sky-600 dark:text-sky-400",
1127 // the old pulse cycled through; only the thread you have open gets
1128 // the label at full strength.
1129 className: cn("text-sky-600 dark:text-sky-400", !props.isActive && "opacity-75"),
1130 }1128 }
1131 : status === "monitoring"1129 : status === "monitoring"
1132 ? {1130 ? {
1133 // Monitoring is calm background presence, not active progress1131 // Monitoring is calm background presence, not active progress
1134 // (monitoring-pill D6), so it keeps the label at full strength.1132 // (monitoring-pill D6), so it keeps the label at full strength.
1135 label: "Monitoring",1133 label: "Monitoring",
1136 icon: null,1134 icon: "monitoring" as const,
1137 className: "text-sky-600 dark:text-sky-400",1135 className: "text-foreground dark:text-white",
1138 }1136 }
1139 : status === "approval"1137 : status === "approval"
1140 ? {1138 ? {
1141 label: "Approval",1139 label: "Approval",
1142 icon: null,1140 icon: "approval" as const,
1143 className: "text-amber-700 dark:text-amber-300",1141 className: "text-amber-700 dark:text-amber-300",
1144 }1142 }
1145 : status === "input"1143 : status === "input"
1146 ? {1144 ? {
1147 label: "Input",1145 label: "Input",
1148 icon: null,1146 icon: "input" as const,
1149 className: "text-indigo-600 dark:text-indigo-300",1147 className: "text-indigo-600 dark:text-indigo-300",
1150 }1148 }
1151 : status === "failed"1149 : status === "failed"
1152 ? {1150 ? {
1153 label: "Failed",1151 label: "Failed",
1154 icon: null,1152 icon: "failed" as const,
1155 className: "text-red-700 dark:text-red-300",1153 className: "text-red-700 dark:text-red-300",
1156 }1154 }
1157 : isWoke1155 : isWoke
...@@ -1396,10 +1394,6 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {...@@ -1396,10 +1394,6 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
1396 : shouldRecede1394 : shouldRecede
1397 ? "text-sidebar-muted-foreground/75 hover:bg-sidebar-row-hover hover:text-sidebar-foreground"1395 ? "text-sidebar-muted-foreground/75 hover:bg-sidebar-row-hover hover:text-sidebar-foreground"
1398 : "bg-transparent text-sidebar-foreground hover:bg-sidebar-row-hover",1396 : "bg-transparent text-sidebar-foreground hover:bg-sidebar-row-hover",
1399 isInFlight &&
1400 !props.isActive &&
1401 !isSelected &&
1402 "opacity-70 transition-opacity hover:opacity-100",
1403 isFileDragOver && "ring-1 ring-inset ring-primary/70",1397 isFileDragOver && "ring-1 ring-inset ring-primary/70",
1404 // The hover tint must not clobber an active/selected row's own surface.1398 // The hover tint must not clobber an active/selected row's own surface.
1405 isFileDragOver && !props.isActive && !isSelected && "bg-sidebar-row-hover",1399 isFileDragOver && !props.isActive && !isSelected && "bg-sidebar-row-hover",
...@@ -1462,7 +1456,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {...@@ -1462,7 +1456,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
1462 "truncate",1456 "truncate",
1463 shouldRecede1457 shouldRecede
1464 ? "text-secondary-label"1458 ? "text-secondary-label"
1465 : isUnread || isWoke1459 : isUnread || isWoke || status === "input"
1466 ? "text-foreground"1460 ? "text-foreground"
1467 : status === "failed"1461 : status === "failed"
1468 ? "text-foreground/95"1462 ? "text-foreground/95"
...@@ -1472,7 +1466,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {...@@ -1472,7 +1466,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
1472 "truncate group-focus-within/sidebar-row:text-foreground group-hover/sidebar-row:text-foreground",1466 "truncate group-focus-within/sidebar-row:text-foreground group-hover/sidebar-row:text-foreground",
1473 shouldRecede1467 shouldRecede
1474 ? "text-secondary-label/70"1468 ? "text-secondary-label/70"
1475 : props.isActive || isWoke1469 : props.isActive || isWoke || status === "input"
1476 ? "text-foreground"1470 ? "text-foreground"
1477 : isUnread1471 : isUnread
1478 ? "text-muted-foreground"1472 ? "text-muted-foreground"
...@@ -1814,6 +1808,14 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {...@@ -1814,6 +1808,14 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
1814 >1808 >
1815 {topStatus.icon === "working" ? (1809 {topStatus.icon === "working" ? (
1816 <CircleDashedIcon aria-hidden className="size-4 shrink-0" />1810 <CircleDashedIcon aria-hidden className="size-4 shrink-0" />
1811 ) : topStatus.icon === "input" ? (
1812 <MessageCircleQuestionIcon aria-hidden className="size-4 shrink-0" />
1813 ) : topStatus.icon === "approval" ? (
1814 <ShieldQuestionIcon aria-hidden className="size-4 shrink-0" />
1815 ) : topStatus.icon === "failed" ? (
1816 <CircleAlertIcon aria-hidden className="size-4 shrink-0" />
1817 ) : topStatus.icon === "monitoring" ? (
1818 <EyeIcon aria-hidden className="size-4 shrink-0" />
1817 ) : topStatus.icon === "done" ? (1819 ) : topStatus.icon === "done" ? (
1818 <CircleCheckIcon aria-hidden className="size-4 shrink-0" />1820 <CircleCheckIcon aria-hidden className="size-4 shrink-0" />
1819 ) : null}1821 ) : null}