@@ -190,6 +190,13 @@ enum WorktrunkSidebarListMode: String {
190190}
191191
192192final class WorktrunkStore : ObservableObject {
193+ private enum RefreshAllTrigger {
194+ case sidebarAppear
195+ case repositoryAdded
196+ case worktrunkInstalled
197+ case manual
198+ }
199+
193200 struct Repository : Identifiable , Codable , Hashable {
194201 var id : UUID
195202 var path : String
@@ -306,7 +313,12 @@ final class WorktrunkStore: ObservableObject {
306313 private var lastAppQuitTimestamp : Date ?
307314 private var sidebarModelRevisionCounter : Int = 0
308315 private var refreshAllTask : Task < Void , Never > ?
316+ private var refreshAllTaskGeneration : UInt64 = 0
309317 private var refreshAllNeedsRerun : Bool = false
318+ private var lastRefreshAllCompletedAt : Date = . distantPast
319+ private let sidebarAppearRefreshInterval : TimeInterval = 20
320+ private let repoListRefreshConcurrency : Int = 4
321+ private let gitTrackingRefreshConcurrency : Int = 8
310322
311323 init ( ) {
312324 load ( )
@@ -527,7 +539,7 @@ final class WorktrunkStore: ObservableObject {
527539 save ( )
528540 rebuildSidebarSnapshot ( )
529541 bumpSidebarModelRevision ( )
530- Task { await refreshAll ( ) }
542+ Task { await refreshAll ( trigger : . repositoryAdded ) }
531543 }
532544
533545 func removeRepository( id: UUID ) {
@@ -541,23 +553,55 @@ final class WorktrunkStore: ObservableObject {
541553 }
542554
543555 func refreshAll( ) async {
556+ await refreshAll ( trigger: . manual)
557+ }
558+
559+ func refreshForSidebarAppearIfNeeded( ) async {
560+ await refreshAll ( trigger: . sidebarAppear)
561+ }
562+
563+ private func refreshAll( trigger: RefreshAllTrigger ) async {
564+ if trigger == . sidebarAppear, shouldSkipSidebarAppearRefresh ( ) {
565+ return
566+ }
567+
568+ let shouldScheduleRerun = shouldScheduleRefreshRerun ( for: trigger)
544569 if let existing = refreshAllTask {
545- refreshAllNeedsRerun = true
570+ let observedGeneration = refreshAllTaskGeneration
571+ if shouldScheduleRerun {
572+ refreshAllNeedsRerun = true
573+ }
546574 await existing. value
547- if refreshAllNeedsRerun {
548- refreshAllNeedsRerun = false
549- await refreshAll ( )
575+
576+ // Existing task may be complete but still stored here until its creator resumes.
577+ if refreshAllTaskGeneration == observedGeneration {
578+ refreshAllTask = nil
579+ }
580+
581+ if shouldScheduleRerun, refreshAllNeedsRerun, refreshAllTask == nil {
582+ await startRefreshAllTask ( )
550583 }
551584 return
552585 }
553586
587+ await startRefreshAllTask ( )
588+ }
589+
590+ private func startRefreshAllTask( ) async {
591+ guard refreshAllTask == nil else { return }
592+
593+ refreshAllTaskGeneration &+= 1
594+ let generation = refreshAllTaskGeneration
554595 let task = Task { [ weak self] in
555596 guard let self else { return }
556597 await self . refreshAllBatchedLoop ( )
557598 }
558599 refreshAllTask = task
559600 await task. value
560- refreshAllTask = nil
601+
602+ if refreshAllTaskGeneration == generation {
603+ refreshAllTask = nil
604+ }
561605 }
562606
563607 private struct RefreshListResult {
@@ -577,10 +621,27 @@ final class WorktrunkStore: ObservableObject {
577621 await refreshAllBatchedOnce ( )
578622 } while refreshAllNeedsRerun
579623 await MainActor . run {
624+ lastRefreshAllCompletedAt = Date ( )
580625 isRefreshing = false
581626 }
582627 }
583628
629+ private func shouldSkipSidebarAppearRefresh( ) -> Bool {
630+ if refreshAllTask != nil {
631+ return false
632+ }
633+ return Date ( ) . timeIntervalSince ( lastRefreshAllCompletedAt) < sidebarAppearRefreshInterval
634+ }
635+
636+ private func shouldScheduleRefreshRerun( for trigger: RefreshAllTrigger ) -> Bool {
637+ switch trigger {
638+ case . sidebarAppear:
639+ return false
640+ case . repositoryAdded, . worktrunkInstalled, . manual:
641+ return true
642+ }
643+ }
644+
584645 private func refreshAllBatchedOnce( ) async {
585646 let repoSnapshot = await MainActor . run { repositories }
586647 let previousByRepoID = await MainActor . run {
@@ -597,7 +658,12 @@ final class WorktrunkStore: ObservableObject {
597658 results. reserveCapacity ( repoSnapshot. count)
598659
599660 await withTaskGroup ( of: RefreshListResult . self) { group in
600- for repo in repoSnapshot {
661+ var nextRepoIndex = 0
662+
663+ func enqueueNextRepo( ) {
664+ guard nextRepoIndex < repoSnapshot. count else { return }
665+ let repo = repoSnapshot [ nextRepoIndex]
666+ nextRepoIndex += 1
601667 let previous = previousByRepoID [ repo. id] ?? ( hadExisting: false , paths: Set < String > ( ) )
602668 group. addTask { [ self ] in
603669 do {
@@ -624,8 +690,13 @@ final class WorktrunkStore: ObservableObject {
624690 }
625691 }
626692
627- for await result in group {
693+ for _ in 0 ..< min ( repoListRefreshConcurrency, repoSnapshot. count) {
694+ enqueueNextRepo ( )
695+ }
696+
697+ while let result = await group. next ( ) {
628698 results. append ( result)
699+ enqueueNextRepo ( )
629700 }
630701 }
631702
@@ -678,8 +749,9 @@ final class WorktrunkStore: ObservableObject {
678749 repositories. flatMap { worktreesByRepositoryID [ $0. id] ?? [ ] }
679750 }
680751
681- await refreshGitTracking ( for: allWorktrees, removing: allPreviousPaths)
682- await refreshSessions ( )
752+ async let trackingRefresh : Void = refreshGitTracking ( for: allWorktrees, removing: allPreviousPaths)
753+ async let sessionsRefresh : Void = refreshSessions ( )
754+ _ = await ( trackingRefresh, sessionsRefresh)
683755 }
684756
685757 private func decodeWorktrees( repoID: UUID , data: Data ) throws -> [ Worktree ] {
@@ -818,7 +890,7 @@ final class WorktrunkStore: ObservableObject {
818890 errorMessage = nil
819891 needsWorktrunkInstall = false
820892 }
821- await refreshAll ( )
893+ await refreshAll ( trigger : . worktrunkInstalled )
822894 return true
823895 } catch {
824896 await MainActor . run {
@@ -1001,17 +1073,27 @@ final class WorktrunkStore: ObservableObject {
10011073 var results : [ String : GitTracking ] = [ : ]
10021074
10031075 await withTaskGroup ( of: ( String, GitTracking? ) . self) { group in
1004- for worktree in worktrees {
1076+ var nextWorktreeIndex = 0
1077+
1078+ func enqueueNextWorktree( ) {
1079+ guard nextWorktreeIndex < worktrees. count else { return }
1080+ let worktree = worktrees [ nextWorktreeIndex]
1081+ nextWorktreeIndex += 1
10051082 group. addTask { [ self ] in
10061083 let tracking = try ? await getGitTracking ( worktreePath: worktree. path)
10071084 return ( worktree. path, tracking)
10081085 }
10091086 }
10101087
1011- for await (path, tracking) in group {
1088+ for _ in 0 ..< min ( gitTrackingRefreshConcurrency, worktrees. count) {
1089+ enqueueNextWorktree ( )
1090+ }
1091+
1092+ while let ( path, tracking) = await group. next ( ) {
10121093 if let tracking {
10131094 results [ path] = tracking
10141095 }
1096+ enqueueNextWorktree ( )
10151097 }
10161098 }
10171099
@@ -1037,6 +1119,9 @@ final class WorktrunkStore: ObservableObject {
10371119 let output = try await runGitStatus ( worktreePath: worktreePath)
10381120 if output. isEmpty { return nil }
10391121 var parsed = parseGitStatusOutput ( output)
1122+ if parsed. tracking. totalChangesCount == 0 {
1123+ return parsed. tracking
1124+ }
10401125
10411126 let ( unstagedAdds, unstagedDeletes) = ( try ? await runGitNumstat (
10421127 worktreePath: worktreePath,
0 commit comments