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", () => {
429429 ThreadId.makeUnsafe("thread-6"),
430430 ThreadId.makeUnsafe("thread-8"),
431431 ]);
432 expect(result.hiddenThreads.map((thread) => thread.id)).toEqual([
433 ThreadId.makeUnsafe("thread-7"),
434 ]);
432435 });
433436
434437 it("returns all threads when the list is expanded", () => {
......@@ -449,6 +452,7 @@ describe("getVisibleThreadsForProject", () => {
449452 expect(result.visibleThreads.map((thread) => thread.id)).toEqual(
450453 threads.map((thread) => thread.id),
451454 );
455 expect(result.hiddenThreads).toEqual([]);
452456 });
453457});
454458
apps/web/src/components/Sidebar.logic.ts+5
......@@ -244,6 +244,7 @@ export function getVisibleThreadsForProject(input: {
244244 previewLimit: number;
245245}): {
246246 hasHiddenThreads: boolean;
247 hiddenThreads: Thread[];
247248 visibleThreads: Thread[];
248249} {
249250 const { activeThreadId, isThreadListExpanded, previewLimit, threads } = input;
......@@ -252,6 +253,7 @@ export function getVisibleThreadsForProject(input: {
252253 if (!hasHiddenThreads || isThreadListExpanded) {
253254 return {
254255 hasHiddenThreads,
256 hiddenThreads: [],
255257 visibleThreads: [...threads],
256258 };
257259 }
......@@ -260,6 +262,7 @@ export function getVisibleThreadsForProject(input: {
260262 if (!activeThreadId || previewThreads.some((thread) => thread.id === activeThreadId)) {
261263 return {
262264 hasHiddenThreads: true,
265 hiddenThreads: threads.slice(previewLimit),
263266 visibleThreads: previewThreads,
264267 };
265268 }
......@@ -268,6 +271,7 @@ export function getVisibleThreadsForProject(input: {
268271 if (!activeThread) {
269272 return {
270273 hasHiddenThreads: true,
274 hiddenThreads: threads.slice(previewLimit),
271275 visibleThreads: previewThreads,
272276 };
273277 }
......@@ -276,6 +280,7 @@ export function getVisibleThreadsForProject(input: {
276280
277281 return {
278282 hasHiddenThreads: true,
283 hiddenThreads: threads.filter((thread) => !visibleThreadIds.has(thread.id)),
279284 visibleThreads: threads.filter((thread) => visibleThreadIds.has(thread.id)),
280285 };
281286}
apps/web/src/components/Sidebar.tsx+68-28
......@@ -151,6 +151,44 @@ interface PrStatusIndicator {
151151
152152type 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
154192function terminalStatusFromRunningIds(
155193 runningTerminalIds: string[],
156194): TerminalStatusIndicator | null {
......@@ -1023,14 +1061,18 @@ export default function Sidebar() {
10231061 visibleThreads.filter((thread) => thread.projectId === project.id),
10241062 appSettings.sidebarThreadSortOrder,
10251063 );
1026 const projectStatus = resolveProjectStatusIndicator(
1027 projectThreads.map((thread) =>
1064 const threadStatuses = new Map(
1065 projectThreads.map((thread) => [
1066 thread.id,
10281067 resolveThreadStatusPill({
10291068 thread,
10301069 hasPendingApprovals: derivePendingApprovals(thread.activities).length > 0,
10311070 hasPendingUserInput: derivePendingUserInputs(thread.activities).length > 0,
10321071 }),
1033 ),
1072 ]),
1073 );
1074 const projectStatus = resolveProjectStatusIndicator(
1075 projectThreads.map((thread) => threadStatuses.get(thread.id) ?? null),
10341076 );
10351077 const activeThreadId = routeThreadId ?? undefined;
10361078 const isThreadListExpanded = expandedThreadListsByProject.has(project.id);
......@@ -1039,13 +1081,19 @@ export default function Sidebar() {
10391081 ? (projectThreads.find((thread) => thread.id === activeThreadId) ?? null)
10401082 : null;
10411083 const shouldShowThreadPanel = project.expanded || pinnedCollapsedThread !== null;
1042 const { hasHiddenThreads, visibleThreads: visibleProjectThreads } =
1043 getVisibleThreadsForProject({
1044 threads: projectThreads,
1045 activeThreadId,
1046 isThreadListExpanded,
1047 previewLimit: THREAD_PREVIEW_LIMIT,
1048 });
1084 const {
1085 hasHiddenThreads,
1086 hiddenThreads,
1087 visibleThreads: visibleProjectThreads,
1088 } = getVisibleThreadsForProject({
1089 threads: projectThreads,
1090 activeThreadId,
1091 isThreadListExpanded,
1092 previewLimit: THREAD_PREVIEW_LIMIT,
1093 });
1094 const hiddenThreadStatus = resolveProjectStatusIndicator(
1095 hiddenThreads.map((thread) => threadStatuses.get(thread.id) ?? null),
1096 );
10491097 const orderedProjectThreadIds = projectThreads.map((thread) => thread.id);
10501098 const renderedThreads = pinnedCollapsedThread
10511099 ? [pinnedCollapsedThread]
......@@ -1053,10 +1101,12 @@ export default function Sidebar() {
10531101
10541102 return {
10551103 hasHiddenThreads,
1104 hiddenThreadStatus,
10561105 orderedProjectThreadIds,
10571106 project,
10581107 projectStatus,
10591108 projectThreads,
1109 threadStatuses,
10601110 renderedThreads,
10611111 shouldShowThreadPanel,
10621112 isThreadListExpanded,
......@@ -1198,10 +1248,12 @@ export default function Sidebar() {
11981248 ) {
11991249 const {
12001250 hasHiddenThreads,
1251 hiddenThreadStatus,
12011252 orderedProjectThreadIds,
12021253 project,
12031254 projectStatus,
12041255 projectThreads,
1256 threadStatuses,
12051257 renderedThreads,
12061258 shouldShowThreadPanel,
12071259 isThreadListExpanded,
......@@ -1213,11 +1265,7 @@ export default function Sidebar() {
12131265 const jumpLabel = threadJumpLabelById.get(thread.id) ?? null;
12141266 const isThreadRunning =
12151267 thread.session?.status === "running" && thread.session.activeTurnId != null;
1216 const threadStatus = resolveThreadStatusPill({
1217 thread,
1218 hasPendingApprovals: derivePendingApprovals(thread.activities).length > 0,
1219 hasPendingUserInput: derivePendingUserInputs(thread.activities).length > 0,
1220 });
1268 const threadStatus = threadStatuses.get(thread.id) ?? null;
12211269 const prStatus = prStatusIndicator(prByThreadId.get(thread.id) ?? null);
12221270 const terminalStatus = terminalStatusFromRunningIds(
12231271 selectThreadTerminalState(terminalStateByThreadId, thread.id).runningTerminalIds,
......@@ -1291,18 +1339,7 @@ export default function Sidebar() {
12911339 <TooltipPopup side="top">{prStatus.tooltip}</TooltipPopup>
12921340 </Tooltip>
12931341 )}
1294 {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 )}
1342 {threadStatus && <ThreadStatusLabel status={threadStatus} />}
13061343 {renamingThreadId === thread.id ? (
13071344 <input
13081345 ref={(el) => {
......@@ -1548,7 +1585,10 @@ export default function Sidebar() {
15481585 expandThreadListForProject(project.id);
15491586 }}
15501587 >
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>
15521592 </SidebarMenuSubButton>
15531593 </SidebarMenuSubItem>
15541594 )}