22 ArrowLeftIcon ,
33 ArrowUpDownIcon ,
44 ChevronRightIcon ,
5+ FoldVerticalIcon ,
56 FolderIcon ,
67 GitPullRequestIcon ,
78 PlusIcon ,
@@ -93,6 +94,7 @@ import {
9394 getFallbackThreadIdAfterDelete ,
9495 getVisibleThreadsForProject ,
9596 resolveProjectStatusIndicator ,
97+ collectSidebarNonIdleProjectIds ,
9698 resolveSidebarNewThreadEnvMode ,
9799 resolveThreadRowClassName ,
98100 resolveThreadStatusPill ,
@@ -130,12 +132,6 @@ function formatRelativeTime(iso: string): string {
130132 return `${ Math . floor ( hours / 24 ) } d ago` ;
131133}
132134
133- interface TerminalStatusIndicator {
134- label : "Terminal process running" ;
135- colorClass : string ;
136- pulse : boolean ;
137- }
138-
139135interface PrStatusIndicator {
140136 label : "PR open" | "PR closed" | "PR merged" ;
141137 colorClass : string ;
@@ -145,19 +141,6 @@ interface PrStatusIndicator {
145141
146142type ThreadPr = GitStatusResult [ "pr" ] ;
147143
148- function terminalStatusFromRunningIds (
149- runningTerminalIds : string [ ] ,
150- ) : TerminalStatusIndicator | null {
151- if ( runningTerminalIds . length === 0 ) {
152- return null ;
153- }
154- return {
155- label : "Terminal process running" ,
156- colorClass : "text-teal-600 dark:text-teal-300/90" ,
157- pulse : true ,
158- } ;
159- }
160-
161144function prStatusIndicator ( pr : ThreadPr ) : PrStatusIndicator | null {
162145 if ( ! pr ) return null ;
163146
@@ -365,6 +348,7 @@ export default function Sidebar() {
365348 const projects = useStore ( ( store ) => store . projects ) ;
366349 const threads = useStore ( ( store ) => store . threads ) ;
367350 const markThreadUnread = useStore ( ( store ) => store . markThreadUnread ) ;
351+ const setProjectExpanded = useStore ( ( store ) => store . setProjectExpanded ) ;
368352 const toggleProject = useStore ( ( store ) => store . toggleProject ) ;
369353 const reorderProjects = useStore ( ( store ) => store . reorderProjects ) ;
370354 const clearComposerDraftForThread = useComposerDraftStore ( ( store ) => store . clearDraftThread ) ;
@@ -471,6 +455,51 @@ export default function Sidebar() {
471455 return map ;
472456 } , [ threadGitStatusCwds , threadGitStatusQueries , threadGitTargets ] ) ;
473457
458+ const threadStatusById = useMemo ( ( ) => {
459+ const map = new Map < ThreadId , ReturnType < typeof resolveThreadStatusPill > > ( ) ;
460+ for ( const thread of threads ) {
461+ map . set (
462+ thread . id ,
463+ resolveThreadStatusPill ( {
464+ thread,
465+ hasPendingApprovals : derivePendingApprovals ( thread . activities ) . length > 0 ,
466+ hasPendingUserInput : derivePendingUserInputs ( thread . activities ) . length > 0 ,
467+ } ) ,
468+ ) ;
469+ }
470+ return map ;
471+ } , [ threads ] ) ;
472+
473+ const runningTerminalThreadIds = useMemo ( ( ) => {
474+ const ids = new Set < ThreadId > ( ) ;
475+ for ( const thread of threads ) {
476+ if ( selectThreadTerminalState ( terminalStateByThreadId , thread . id ) . runningTerminalIds . length ) {
477+ ids . add ( thread . id ) ;
478+ }
479+ }
480+ return ids ;
481+ } , [ terminalStateByThreadId , threads ] ) ;
482+
483+ const activeThread = routeThreadId
484+ ? threads . find ( ( thread ) => thread . id === routeThreadId )
485+ : undefined ;
486+ const activeDraftThread = useComposerDraftStore ( ( store ) =>
487+ routeThreadId ? store . draftThreadsByThreadId [ routeThreadId ] : undefined ,
488+ ) ;
489+ const activeProjectId = activeThread ?. projectId ?? activeDraftThread ?. projectId ?? null ;
490+
491+ // Currently active project and projects with a thread status or running terminal
492+ const nonIdleProjectIds = useMemo (
493+ ( ) =>
494+ collectSidebarNonIdleProjectIds ( {
495+ activeProjectId,
496+ threads,
497+ threadStatusById,
498+ runningTerminalThreadIds,
499+ } ) ,
500+ [ activeProjectId , runningTerminalThreadIds , threadStatusById , threads ] ,
501+ ) ;
502+
474503 const openPrLink = useCallback ( ( event : React . MouseEvent < HTMLElement > , prUrl : string ) => {
475504 event . preventDefault ( ) ;
476505 event . stopPropagation ( ) ;
@@ -1113,13 +1142,7 @@ export default function Sidebar() {
11131142 appSettings . sidebarThreadSortOrder ,
11141143 ) ;
11151144 const projectStatus = resolveProjectStatusIndicator (
1116- projectThreads . map ( ( thread ) =>
1117- resolveThreadStatusPill ( {
1118- thread,
1119- hasPendingApprovals : derivePendingApprovals ( thread . activities ) . length > 0 ,
1120- hasPendingUserInput : derivePendingUserInputs ( thread . activities ) . length > 0 ,
1121- } ) ,
1122- ) ,
1145+ projectThreads . map ( ( thread ) => threadStatusById . get ( thread . id ) ?? null ) ,
11231146 ) ;
11241147 const activeThreadId = routeThreadId ?? undefined ;
11251148 const isThreadListExpanded = expandedThreadListsByProject . has ( project . id ) ;
@@ -1140,15 +1163,15 @@ export default function Sidebar() {
11401163 const isActive = routeThreadId === thread . id ;
11411164 const isSelected = selectedThreadIds . has ( thread . id ) ;
11421165 const isHighlighted = isActive || isSelected ;
1143- const threadStatus = resolveThreadStatusPill ( {
1144- thread,
1145- hasPendingApprovals : derivePendingApprovals ( thread . activities ) . length > 0 ,
1146- hasPendingUserInput : derivePendingUserInputs ( thread . activities ) . length > 0 ,
1147- } ) ;
1166+ const threadStatus = threadStatusById . get ( thread . id ) ?? null ;
11481167 const prStatus = prStatusIndicator ( prByThreadId . get ( thread . id ) ?? null ) ;
1149- const terminalStatus = terminalStatusFromRunningIds (
1150- selectThreadTerminalState ( terminalStateByThreadId , thread . id ) . runningTerminalIds ,
1151- ) ;
1168+ const terminalStatus = runningTerminalThreadIds . has ( thread . id )
1169+ ? {
1170+ label : "Terminal process running" ,
1171+ colorClass : "text-teal-600 dark:text-teal-300/90" ,
1172+ pulse : true ,
1173+ }
1174+ : null ;
11521175
11531176 return (
11541177 < SidebarMenuSubItem key = { thread . id } className = "w-full" data-thread-item >
@@ -1447,6 +1470,15 @@ export default function Sidebar() {
14471470 [ toggleProject ] ,
14481471 ) ;
14491472
1473+ const handleCollapseIdleProjects = useCallback ( ( ) => {
1474+ for ( const project of projects ) {
1475+ if ( ! project . expanded || nonIdleProjectIds . has ( project . id ) ) {
1476+ continue ;
1477+ }
1478+ setProjectExpanded ( project . id , false ) ;
1479+ }
1480+ } , [ projects , nonIdleProjectIds , setProjectExpanded ] ) ;
1481+
14501482 useEffect ( ( ) => {
14511483 const onMouseDown = ( event : globalThis . MouseEvent ) => {
14521484 if ( selectedThreadIds . size === 0 ) return ;
@@ -1687,7 +1719,7 @@ export default function Sidebar() {
16871719 < span className = "text-[10px] font-medium uppercase tracking-wider text-muted-foreground/60" >
16881720 Projects
16891721 </ span >
1690- < div className = "flex items-center gap-1 " >
1722+ < div className = "flex items-center gap-0.5 " >
16911723 < ProjectSortMenu
16921724 projectSortOrder = { appSettings . sidebarProjectSortOrder }
16931725 threadSortOrder = { appSettings . sidebarThreadSortOrder }
@@ -1698,6 +1730,21 @@ export default function Sidebar() {
16981730 updateSettings ( { sidebarThreadSortOrder : sortOrder } ) ;
16991731 } }
17001732 />
1733+ < Tooltip >
1734+ < TooltipTrigger
1735+ render = {
1736+ < button
1737+ type = "button"
1738+ aria-label = "Collapse idle projects"
1739+ className = "inline-flex size-5 cursor-pointer items-center justify-center rounded-md text-muted-foreground/60 transition-colors hover:bg-accent hover:text-foreground"
1740+ onClick = { handleCollapseIdleProjects }
1741+ />
1742+ }
1743+ >
1744+ < FoldVerticalIcon className = "size-3.5" />
1745+ </ TooltipTrigger >
1746+ < TooltipPopup side = "top" > Collapse idle projects</ TooltipPopup >
1747+ </ Tooltip >
17011748 < Tooltip >
17021749 < TooltipTrigger
17031750 render = {
0 commit comments