native macOS codings agent orchestrator
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add sidebar repository expand toggle

+164
+101
supacode/Features/Repositories/Views/SidebarListView.swift
··· 2 2 import SwiftUI 3 3 4 4 struct SidebarListView: View { 5 + enum RepositoryListHeaderAction: Equatable { 6 + case expandAll 7 + case collapseAll 8 + 9 + var title: String { 10 + switch self { 11 + case .expandAll: 12 + return "Expand All" 13 + case .collapseAll: 14 + return "Collapse All" 15 + } 16 + } 17 + 18 + var systemImageName: String { 19 + "chevron.right" 20 + } 21 + 22 + var rotation: Angle { 23 + switch self { 24 + case .expandAll: 25 + return .zero 26 + case .collapseAll: 27 + return .degrees(90) 28 + } 29 + } 30 + } 31 + 5 32 @Bindable var store: StoreOf<RepositoriesFeature> 6 33 @Binding var expandedRepoIDs: Set<Repository.ID> 7 34 @Binding var sidebarSelections: Set<SidebarSelection> ··· 13 40 let state = store.state 14 41 let hotkeyRows = state.orderedWorktreeRows(includingRepositoryIDs: expandedRepoIDs) 15 42 let orderedRoots = state.orderedRepositoryRoots() 43 + let expandableRepositoryIDs = Self.expandableRepositoryIDs(in: state.repositories) 44 + let repositoryListHeaderAction = Self.repositoryListHeaderAction( 45 + expandedRepoIDs: expandedRepoIDs, 46 + expandableRepositoryIDs: expandableRepositoryIDs 47 + ) 48 + let visibleRepositoryCount = orderedRoots.isEmpty ? state.repositories.count : orderedRoots.count 49 + let showsRepositoryListHeader = Self.showsRepositoryListHeader(repositoryCount: visibleRepositoryCount) 16 50 let selectedWorktreeIDs = Set(sidebarSelections.compactMap(\.worktreeID)) 17 51 let selection = Binding<Set<SidebarSelection>>( 18 52 get: { ··· 102 136 103 137 ScrollViewReader { scrollProxy in 104 138 List(selection: selection) { 139 + if showsRepositoryListHeader { 140 + repositoryListHeader( 141 + action: repositoryListHeaderAction, 142 + expandableRepositoryIDs: expandableRepositoryIDs 143 + ) 144 + .listRowInsets(EdgeInsets()) 145 + } 146 + 105 147 if orderedRoots.isEmpty { 106 148 let repositories = store.repositories 107 149 ForEach(Array(repositories.enumerated()), id: \.element.id) { index, repository in ··· 240 282 } 241 283 } 242 284 285 + private func repositoryListHeader( 286 + action: RepositoryListHeaderAction, 287 + expandableRepositoryIDs: Set<Repository.ID> 288 + ) -> some View { 289 + HStack(spacing: 8) { 290 + Text("Repositories") 291 + .font(.caption) 292 + .foregroundStyle(.tertiary) 293 + .frame(maxWidth: .infinity, alignment: .leading) 294 + if !expandableRepositoryIDs.isEmpty { 295 + Button { 296 + withAnimation(.easeOut(duration: 0.2)) { 297 + switch action { 298 + case .expandAll: 299 + expandedRepoIDs.formUnion(expandableRepositoryIDs) 300 + case .collapseAll: 301 + expandedRepoIDs.subtract(expandableRepositoryIDs) 302 + } 303 + } 304 + } label: { 305 + Label(action.title, systemImage: action.systemImageName) 306 + .labelStyle(.iconOnly) 307 + .frame(width: 20, height: 20) 308 + .rotationEffect(action.rotation) 309 + .contentShape(.rect) 310 + } 311 + .buttonStyle(.plain) 312 + .foregroundStyle(.secondary) 313 + .help(action.title) 314 + } 315 + } 316 + .frame(maxWidth: .infinity, minHeight: 26, alignment: .center) 317 + .padding(.top, 8) 318 + .padding(.bottom, 4) 319 + } 320 + 243 321 @MainActor 244 322 private func revealPendingSidebarWorktree( 245 323 _ pendingSidebarReveal: PendingSidebarReveal?, ··· 254 332 scrollProxy.scrollTo(pendingSidebarReveal.worktreeID, anchor: .center) 255 333 } 256 334 store.send(.consumePendingSidebarReveal(pendingSidebarReveal.id)) 335 + } 336 + 337 + static func expandableRepositoryIDs<Repositories: Sequence>( 338 + in repositories: Repositories 339 + ) -> Set<Repository.ID> where Repositories.Element == Repository { 340 + Set( 341 + repositories 342 + .filter(\.capabilities.supportsWorktrees) 343 + .map(\.id) 344 + ) 345 + } 346 + 347 + static func repositoryListHeaderAction( 348 + expandedRepoIDs: Set<Repository.ID>, 349 + expandableRepositoryIDs: Set<Repository.ID> 350 + ) -> RepositoryListHeaderAction { 351 + !expandedRepoIDs.isDisjoint(with: expandableRepositoryIDs) 352 + ? .collapseAll 353 + : .expandAll 354 + } 355 + 356 + static func showsRepositoryListHeader(repositoryCount: Int) -> Bool { 357 + repositoryCount > 10 257 358 } 258 359 } 259 360
+63
supacodeTests/RepositorySectionViewTests.swift
··· 6 6 7 7 @MainActor 8 8 struct RepositorySectionViewTests { 9 + @Test func sidebarHeaderActionCollapsesWhenAnyExpandableRepositoryIsOpen() { 10 + let gitRepository = Repository( 11 + id: "/tmp/git", 12 + rootURL: URL(fileURLWithPath: "/tmp/git"), 13 + name: "git", 14 + kind: .git, 15 + worktrees: [] 16 + ) 17 + let plainRepository = Repository( 18 + id: "/tmp/plain", 19 + rootURL: URL(fileURLWithPath: "/tmp/plain"), 20 + name: "plain", 21 + kind: .plain, 22 + worktrees: [] 23 + ) 24 + let expandableIDs = SidebarListView.expandableRepositoryIDs( 25 + in: [gitRepository, plainRepository] 26 + ) 27 + 28 + #expect(expandableIDs == [gitRepository.id]) 29 + #expect( 30 + SidebarListView.repositoryListHeaderAction( 31 + expandedRepoIDs: [], 32 + expandableRepositoryIDs: [] 33 + ) 34 + == .expandAll 35 + ) 36 + #expect( 37 + SidebarListView.repositoryListHeaderAction( 38 + expandedRepoIDs: [], 39 + expandableRepositoryIDs: expandableIDs 40 + ) 41 + == .expandAll 42 + ) 43 + #expect( 44 + SidebarListView.repositoryListHeaderAction( 45 + expandedRepoIDs: [gitRepository.id], 46 + expandableRepositoryIDs: expandableIDs 47 + ) 48 + == .collapseAll 49 + ) 50 + #expect( 51 + SidebarListView.repositoryListHeaderAction( 52 + expandedRepoIDs: [gitRepository.id, plainRepository.id], 53 + expandableRepositoryIDs: expandableIDs 54 + ) 55 + == .collapseAll 56 + ) 57 + #expect( 58 + SidebarListView.repositoryListHeaderAction( 59 + expandedRepoIDs: [plainRepository.id], 60 + expandableRepositoryIDs: expandableIDs 61 + ) 62 + == .expandAll 63 + ) 64 + } 65 + 66 + @Test func sidebarHeaderOnlyShowsForLongRepositoryLists() { 67 + #expect(!SidebarListView.showsRepositoryListHeader(repositoryCount: 0)) 68 + #expect(!SidebarListView.showsRepositoryListHeader(repositoryCount: 10)) 69 + #expect(SidebarListView.showsRepositoryListHeader(repositoryCount: 11)) 70 + } 71 + 9 72 @Test func openTabCountForGitRepositorySumsAllWorktrees() { 10 73 let manager = WorktreeTerminalManager(runtime: GhosttyRuntime()) 11 74 let repositoryRootURL = URL(fileURLWithPath: "/tmp/repo")