From be9734651cc383874755b4b8830a2324133b49fc Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 28 Mar 2026 23:21:00 -0700 Subject: [PATCH 1/2] Show hidden thread status in sidebar - Track hidden threads in sidebar thread grouping - Reuse thread status pills for visible and collapsed threads - Cover hidden-thread visibility in logic tests --- apps/web/src/components/Sidebar.logic.test.ts | 4 + apps/web/src/components/Sidebar.logic.ts | 5 ++ apps/web/src/components/Sidebar.tsx | 78 ++++++++++++------- 3 files changed, 59 insertions(+), 28 deletions(-) diff --git a/apps/web/src/components/Sidebar.logic.test.ts b/apps/web/src/components/Sidebar.logic.test.ts index b54ec1cb93..d7f93f371d 100644 --- a/apps/web/src/components/Sidebar.logic.test.ts +++ b/apps/web/src/components/Sidebar.logic.test.ts @@ -429,6 +429,9 @@ describe("getVisibleThreadsForProject", () => { ThreadId.makeUnsafe("thread-6"), ThreadId.makeUnsafe("thread-8"), ]); + expect(result.hiddenThreads.map((thread) => thread.id)).toEqual([ + ThreadId.makeUnsafe("thread-7"), + ]); }); it("returns all threads when the list is expanded", () => { @@ -449,6 +452,7 @@ describe("getVisibleThreadsForProject", () => { expect(result.visibleThreads.map((thread) => thread.id)).toEqual( threads.map((thread) => thread.id), ); + expect(result.hiddenThreads).toEqual([]); }); }); diff --git a/apps/web/src/components/Sidebar.logic.ts b/apps/web/src/components/Sidebar.logic.ts index 1e0871e0d2..f911775446 100644 --- a/apps/web/src/components/Sidebar.logic.ts +++ b/apps/web/src/components/Sidebar.logic.ts @@ -244,6 +244,7 @@ export function getVisibleThreadsForProject(input: { previewLimit: number; }): { hasHiddenThreads: boolean; + hiddenThreads: Thread[]; visibleThreads: Thread[]; } { const { activeThreadId, isThreadListExpanded, previewLimit, threads } = input; @@ -252,6 +253,7 @@ export function getVisibleThreadsForProject(input: { if (!hasHiddenThreads || isThreadListExpanded) { return { hasHiddenThreads, + hiddenThreads: [], visibleThreads: [...threads], }; } @@ -260,6 +262,7 @@ export function getVisibleThreadsForProject(input: { if (!activeThreadId || previewThreads.some((thread) => thread.id === activeThreadId)) { return { hasHiddenThreads: true, + hiddenThreads: threads.slice(previewLimit), visibleThreads: previewThreads, }; } @@ -268,6 +271,7 @@ export function getVisibleThreadsForProject(input: { if (!activeThread) { return { hasHiddenThreads: true, + hiddenThreads: threads.slice(previewLimit), visibleThreads: previewThreads, }; } @@ -276,6 +280,7 @@ export function getVisibleThreadsForProject(input: { return { hasHiddenThreads: true, + hiddenThreads: threads.filter((thread) => !visibleThreadIds.has(thread.id)), visibleThreads: threads.filter((thread) => visibleThreadIds.has(thread.id)), }; } diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 100d0e3f47..8dc2470771 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -151,6 +151,26 @@ interface PrStatusIndicator { type ThreadPr = GitStatusResult["pr"]; +function ThreadStatusLabel({ + status, +}: { + status: NonNullable>; +}) { + return ( + + + {status.label} + + ); +} + function terminalStatusFromRunningIds( runningTerminalIds: string[], ): TerminalStatusIndicator | null { @@ -1023,14 +1043,18 @@ export default function Sidebar() { visibleThreads.filter((thread) => thread.projectId === project.id), appSettings.sidebarThreadSortOrder, ); - const projectStatus = resolveProjectStatusIndicator( - projectThreads.map((thread) => + const threadStatuses = new Map( + projectThreads.map((thread) => [ + thread.id, resolveThreadStatusPill({ thread, hasPendingApprovals: derivePendingApprovals(thread.activities).length > 0, hasPendingUserInput: derivePendingUserInputs(thread.activities).length > 0, }), - ), + ]), + ); + const projectStatus = resolveProjectStatusIndicator( + projectThreads.map((thread) => threadStatuses.get(thread.id) ?? null), ); const activeThreadId = routeThreadId ?? undefined; const isThreadListExpanded = expandedThreadListsByProject.has(project.id); @@ -1039,13 +1063,19 @@ export default function Sidebar() { ? (projectThreads.find((thread) => thread.id === activeThreadId) ?? null) : null; const shouldShowThreadPanel = project.expanded || pinnedCollapsedThread !== null; - const { hasHiddenThreads, visibleThreads: visibleProjectThreads } = - getVisibleThreadsForProject({ - threads: projectThreads, - activeThreadId, - isThreadListExpanded, - previewLimit: THREAD_PREVIEW_LIMIT, - }); + const { + hasHiddenThreads, + hiddenThreads, + visibleThreads: visibleProjectThreads, + } = getVisibleThreadsForProject({ + threads: projectThreads, + activeThreadId, + isThreadListExpanded, + previewLimit: THREAD_PREVIEW_LIMIT, + }); + const hiddenThreadStatus = resolveProjectStatusIndicator( + hiddenThreads.map((thread) => threadStatuses.get(thread.id) ?? null), + ); const orderedProjectThreadIds = projectThreads.map((thread) => thread.id); const renderedThreads = pinnedCollapsedThread ? [pinnedCollapsedThread] @@ -1053,10 +1083,12 @@ export default function Sidebar() { return { hasHiddenThreads, + hiddenThreadStatus, orderedProjectThreadIds, project, projectStatus, projectThreads, + threadStatuses, renderedThreads, shouldShowThreadPanel, isThreadListExpanded, @@ -1198,10 +1230,12 @@ export default function Sidebar() { ) { const { hasHiddenThreads, + hiddenThreadStatus, orderedProjectThreadIds, project, projectStatus, projectThreads, + threadStatuses, renderedThreads, shouldShowThreadPanel, isThreadListExpanded, @@ -1213,11 +1247,7 @@ export default function Sidebar() { const jumpLabel = threadJumpLabelById.get(thread.id) ?? null; const isThreadRunning = thread.session?.status === "running" && thread.session.activeTurnId != null; - const threadStatus = resolveThreadStatusPill({ - thread, - hasPendingApprovals: derivePendingApprovals(thread.activities).length > 0, - hasPendingUserInput: derivePendingUserInputs(thread.activities).length > 0, - }); + const threadStatus = threadStatuses.get(thread.id) ?? null; const prStatus = prStatusIndicator(prByThreadId.get(thread.id) ?? null); const terminalStatus = terminalStatusFromRunningIds( selectThreadTerminalState(terminalStateByThreadId, thread.id).runningTerminalIds, @@ -1291,18 +1321,7 @@ export default function Sidebar() { {prStatus.tooltip} )} - {threadStatus && ( - - - {threadStatus.label} - - )} + {threadStatus && } {renamingThreadId === thread.id ? ( { @@ -1548,7 +1567,10 @@ export default function Sidebar() { expandThreadListForProject(project.id); }} > - Show more + + Show more + {hiddenThreadStatus && } + )} From d21cd8fac3ddfc76a6ca706dac71cbd23c1b37df Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 28 Mar 2026 23:25:58 -0700 Subject: [PATCH 2/2] Show hidden thread status in compact sidebar state - Render the hidden-thread status as a compact icon in the sidebar - Keep the status accessible with label text and tooltip --- apps/web/src/components/Sidebar.tsx | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 8dc2470771..02aa4fb827 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -153,9 +153,27 @@ type ThreadPr = GitStatusResult["pr"]; function ThreadStatusLabel({ status, + compact = false, }: { status: NonNullable>; + compact?: boolean; }) { + if (compact) { + return ( + + + {status.label} + + ); + } + return ( - + + {hiddenThreadStatus && } Show more - {hiddenThreadStatus && }