authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-03-29 11:47:17-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-03-29 11:47:17-07:00
logd5f2333173f6b01e38d6488dd7a2f157b479195f
tree91dec5419d0e4b10a3b4f2a878364d6abf7c59e8
parent40e7dbdae9cbb4b5b9cfdb0c8b9672709034b810
signature Signed by PGP key B5690EEEBB952194

Show hidden thread status in sidebar (#1517)


3 files changed, 77 insertions(+), 28 deletions(-)

apps/web/src/components/Sidebar.logic.test.ts+4
...@@ -429,6 +429,9 @@ describe("getVisibleThreadsForProject", () => {...@@ -429,6 +429,9 @@ describe("getVisibleThreadsForProject", () => {
429 ThreadId.makeUnsafe("thread-6"),429 ThreadId.makeUnsafe("thread-6"),
430 ThreadId.makeUnsafe("thread-8"),430 ThreadId.makeUnsafe("thread-8"),
431 ]);431 ]);
432 expect(result.hiddenThreads.map((thread) => thread.id)).toEqual([
433 ThreadId.makeUnsafe("thread-7"),
434 ]);
432 });435 });
433436
434 it("returns all threads when the list is expanded", () => {437 it("returns all threads when the list is expanded", () => {
...@@ -449,6 +452,7 @@ describe("getVisibleThreadsForProject", () => {...@@ -449,6 +452,7 @@ describe("getVisibleThreadsForProject", () => {
449 expect(result.visibleThreads.map((thread) => thread.id)).toEqual(452 expect(result.visibleThreads.map((thread) => thread.id)).toEqual(
450 threads.map((thread) => thread.id),453 threads.map((thread) => thread.id),
451 );454 );
455 expect(result.hiddenThreads).toEqual([]);
452 });456 });
453});457});
454458
apps/web/src/components/Sidebar.logic.ts+5
...@@ -244,6 +244,7 @@ export function getVisibleThreadsForProject(input: {...@@ -244,6 +244,7 @@ export function getVisibleThreadsForProject(input: {
244 previewLimit: number;244 previewLimit: number;
245}): {245}): {
246 hasHiddenThreads: boolean;246 hasHiddenThreads: boolean;
247 hiddenThreads: Thread[];
247 visibleThreads: Thread[];248 visibleThreads: Thread[];
248} {249} {
249 const { activeThreadId, isThreadListExpanded, previewLimit, threads } = input;250 const { activeThreadId, isThreadListExpanded, previewLimit, threads } = input;
...@@ -252,6 +253,7 @@ export function getVisibleThreadsForProject(input: {...@@ -252,6 +253,7 @@ export function getVisibleThreadsForProject(input: {
252 if (!hasHiddenThreads || isThreadListExpanded) {253 if (!hasHiddenThreads || isThreadListExpanded) {
253 return {254 return {
254 hasHiddenThreads,255 hasHiddenThreads,
256 hiddenThreads: [],
255 visibleThreads: [...threads],257 visibleThreads: [...threads],
256 };258 };
257 }259 }
...@@ -260,6 +262,7 @@ export function getVisibleThreadsForProject(input: {...@@ -260,6 +262,7 @@ export function getVisibleThreadsForProject(input: {
260 if (!activeThreadId || previewThreads.some((thread) => thread.id === activeThreadId)) {262 if (!activeThreadId || previewThreads.some((thread) => thread.id === activeThreadId)) {
261 return {263 return {
262 hasHiddenThreads: true,264 hasHiddenThreads: true,
265 hiddenThreads: threads.slice(previewLimit),
263 visibleThreads: previewThreads,266 visibleThreads: previewThreads,
264 };267 };
265 }268 }
...@@ -268,6 +271,7 @@ export function getVisibleThreadsForProject(input: {...@@ -268,6 +271,7 @@ export function getVisibleThreadsForProject(input: {
268 if (!activeThread) {271 if (!activeThread) {
269 return {272 return {
270 hasHiddenThreads: true,273 hasHiddenThreads: true,
274 hiddenThreads: threads.slice(previewLimit),
271 visibleThreads: previewThreads,275 visibleThreads: previewThreads,
272 };276 };
273 }277 }
...@@ -276,6 +280,7 @@ export function getVisibleThreadsForProject(input: {...@@ -276,6 +280,7 @@ export function getVisibleThreadsForProject(input: {
276280
277 return {281 return {
278 hasHiddenThreads: true,282 hasHiddenThreads: true,
283 hiddenThreads: threads.filter((thread) => !visibleThreadIds.has(thread.id)),
279 visibleThreads: threads.filter((thread) => visibleThreadIds.has(thread.id)),284 visibleThreads: threads.filter((thread) => visibleThreadIds.has(thread.id)),
280 };285 };
281}286}
apps/web/src/components/Sidebar.tsx+68-28
...@@ -151,6 +151,44 @@ interface PrStatusIndicator {...@@ -151,6 +151,44 @@ interface PrStatusIndicator {
151151
152type ThreadPr = GitStatusResult["pr"];152type ThreadPr = GitStatusResult["pr"];
153153
154function ThreadStatusLabel({
155 status,
156 compact = false,
157}: {
158 status: NonNullable<ReturnType<typeof resolveThreadStatusPill>>;
159 compact?: boolean;
160}) {
161 if (compact) {
162 return (
163 <span
164 title={status.label}
165 className={`inline-flex size-3.5 shrink-0 items-center justify-center ${status.colorClass}`}
166 >
167 <span
168 className={`size-[9px] rounded-full ${status.dotClass} ${
169 status.pulse ? "animate-pulse" : ""
170 }`}
171 />
172 <span className="sr-only">{status.label}</span>
173 </span>
174 );
175 }
176
177 return (
178 <span
179 title={status.label}
180 className={`inline-flex items-center gap-1 text-[10px] ${status.colorClass}`}
181 >
182 <span
183 className={`h-1.5 w-1.5 rounded-full ${status.dotClass} ${
184 status.pulse ? "animate-pulse" : ""
185 }`}
186 />
187 <span className="hidden md:inline">{status.label}</span>
188 </span>
189 );
190}
191
154function terminalStatusFromRunningIds(192function terminalStatusFromRunningIds(
155 runningTerminalIds: string[],193 runningTerminalIds: string[],
156): TerminalStatusIndicator | null {194): TerminalStatusIndicator | null {
...@@ -1023,14 +1061,18 @@ export default function Sidebar() {...@@ -1023,14 +1061,18 @@ export default function Sidebar() {
1023 visibleThreads.filter((thread) => thread.projectId === project.id),1061 visibleThreads.filter((thread) => thread.projectId === project.id),
1024 appSettings.sidebarThreadSortOrder,1062 appSettings.sidebarThreadSortOrder,
1025 );1063 );
1026 const projectStatus = resolveProjectStatusIndicator(1064 const threadStatuses = new Map(
1027 projectThreads.map((thread) =>1065 projectThreads.map((thread) => [
1066 thread.id,
1028 resolveThreadStatusPill({1067 resolveThreadStatusPill({
1029 thread,1068 thread,
1030 hasPendingApprovals: derivePendingApprovals(thread.activities).length > 0,1069 hasPendingApprovals: derivePendingApprovals(thread.activities).length > 0,
1031 hasPendingUserInput: derivePendingUserInputs(thread.activities).length > 0,1070 hasPendingUserInput: derivePendingUserInputs(thread.activities).length > 0,
1032 }),1071 }),
1033 ),1072 ]),
1073 );
1074 const projectStatus = resolveProjectStatusIndicator(
1075 projectThreads.map((thread) => threadStatuses.get(thread.id) ?? null),
1034 );1076 );
1035 const activeThreadId = routeThreadId ?? undefined;1077 const activeThreadId = routeThreadId ?? undefined;
1036 const isThreadListExpanded = expandedThreadListsByProject.has(project.id);1078 const isThreadListExpanded = expandedThreadListsByProject.has(project.id);
...@@ -1039,13 +1081,19 @@ export default function Sidebar() {...@@ -1039,13 +1081,19 @@ export default function Sidebar() {
1039 ? (projectThreads.find((thread) => thread.id === activeThreadId) ?? null)1081 ? (projectThreads.find((thread) => thread.id === activeThreadId) ?? null)
1040 : null;1082 : null;
1041 const shouldShowThreadPanel = project.expanded || pinnedCollapsedThread !== null;1083 const shouldShowThreadPanel = project.expanded || pinnedCollapsedThread !== null;
1042 const { hasHiddenThreads, visibleThreads: visibleProjectThreads } =1084 const {
1043 getVisibleThreadsForProject({1085 hasHiddenThreads,
1044 threads: projectThreads,1086 hiddenThreads,
1045 activeThreadId,1087 visibleThreads: visibleProjectThreads,
1046 isThreadListExpanded,1088 } = getVisibleThreadsForProject({
1047 previewLimit: THREAD_PREVIEW_LIMIT,1089 threads: projectThreads,
1048 });1090 activeThreadId,
1091 isThreadListExpanded,
1092 previewLimit: THREAD_PREVIEW_LIMIT,
1093 });
1094 const hiddenThreadStatus = resolveProjectStatusIndicator(
1095 hiddenThreads.map((thread) => threadStatuses.get(thread.id) ?? null),
1096 );
1049 const orderedProjectThreadIds = projectThreads.map((thread) => thread.id);1097 const orderedProjectThreadIds = projectThreads.map((thread) => thread.id);
1050 const renderedThreads = pinnedCollapsedThread1098 const renderedThreads = pinnedCollapsedThread
1051 ? [pinnedCollapsedThread]1099 ? [pinnedCollapsedThread]
...@@ -1053,10 +1101,12 @@ export default function Sidebar() {...@@ -1053,10 +1101,12 @@ export default function Sidebar() {
10531101
1054 return {1102 return {
1055 hasHiddenThreads,1103 hasHiddenThreads,
1104 hiddenThreadStatus,
1056 orderedProjectThreadIds,1105 orderedProjectThreadIds,
1057 project,1106 project,
1058 projectStatus,1107 projectStatus,
1059 projectThreads,1108 projectThreads,
1109 threadStatuses,
1060 renderedThreads,1110 renderedThreads,
1061 shouldShowThreadPanel,1111 shouldShowThreadPanel,
1062 isThreadListExpanded,1112 isThreadListExpanded,
...@@ -1198,10 +1248,12 @@ export default function Sidebar() {...@@ -1198,10 +1248,12 @@ export default function Sidebar() {
1198 ) {1248 ) {
1199 const {1249 const {
1200 hasHiddenThreads,1250 hasHiddenThreads,
1251 hiddenThreadStatus,
1201 orderedProjectThreadIds,1252 orderedProjectThreadIds,
1202 project,1253 project,
1203 projectStatus,1254 projectStatus,
1204 projectThreads,1255 projectThreads,
1256 threadStatuses,
1205 renderedThreads,1257 renderedThreads,
1206 shouldShowThreadPanel,1258 shouldShowThreadPanel,
1207 isThreadListExpanded,1259 isThreadListExpanded,
...@@ -1213,11 +1265,7 @@ export default function Sidebar() {...@@ -1213,11 +1265,7 @@ export default function Sidebar() {
1213 const jumpLabel = threadJumpLabelById.get(thread.id) ?? null;1265 const jumpLabel = threadJumpLabelById.get(thread.id) ?? null;
1214 const isThreadRunning =1266 const isThreadRunning =
1215 thread.session?.status === "running" && thread.session.activeTurnId != null;1267 thread.session?.status === "running" && thread.session.activeTurnId != null;
1216 const threadStatus = resolveThreadStatusPill({1268 const threadStatus = threadStatuses.get(thread.id) ?? null;
1217 thread,
1218 hasPendingApprovals: derivePendingApprovals(thread.activities).length > 0,
1219 hasPendingUserInput: derivePendingUserInputs(thread.activities).length > 0,
1220 });
1221 const prStatus = prStatusIndicator(prByThreadId.get(thread.id) ?? null);1269 const prStatus = prStatusIndicator(prByThreadId.get(thread.id) ?? null);
1222 const terminalStatus = terminalStatusFromRunningIds(1270 const terminalStatus = terminalStatusFromRunningIds(
1223 selectThreadTerminalState(terminalStateByThreadId, thread.id).runningTerminalIds,1271 selectThreadTerminalState(terminalStateByThreadId, thread.id).runningTerminalIds,
...@@ -1291,18 +1339,7 @@ export default function Sidebar() {...@@ -1291,18 +1339,7 @@ export default function Sidebar() {
1291 <TooltipPopup side="top">{prStatus.tooltip}</TooltipPopup>1339 <TooltipPopup side="top">{prStatus.tooltip}</TooltipPopup>
1292 </Tooltip>1340 </Tooltip>
1293 )}1341 )}
1294 {threadStatus && (1342 {threadStatus && <ThreadStatusLabel status={threadStatus} />}
1295 <span
1296 className={`inline-flex items-center gap-1 text-[10px] ${threadStatus.colorClass}`}
1297 >
1298 <span
1299 className={`h-1.5 w-1.5 rounded-full ${threadStatus.dotClass} ${
1300 threadStatus.pulse ? "animate-pulse" : ""
1301 }`}
1302 />
1303 <span className="hidden md:inline">{threadStatus.label}</span>
1304 </span>
1305 )}
1306 {renamingThreadId === thread.id ? (1343 {renamingThreadId === thread.id ? (
1307 <input1344 <input
1308 ref={(el) => {1345 ref={(el) => {
...@@ -1548,7 +1585,10 @@ export default function Sidebar() {...@@ -1548,7 +1585,10 @@ export default function Sidebar() {
1548 expandThreadListForProject(project.id);1585 expandThreadListForProject(project.id);
1549 }}1586 }}
1550 >1587 >
1551 <span>Show more</span>1588 <span className="flex min-w-0 flex-1 items-center gap-2">
1589 {hiddenThreadStatus && <ThreadStatusLabel status={hiddenThreadStatus} compact />}
1590 <span>Show more</span>
1591 </span>
1552 </SidebarMenuSubButton>1592 </SidebarMenuSubButton>
1553 </SidebarMenuSubItem>1593 </SidebarMenuSubItem>
1554 )}1594 )}