native macOS codings agent orchestrator
6
fork

Configure Feed

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

Merge pull request #159 from onevcat/fix/sidebar-ui-compact

fix: compact sidebar layout with previews

authored by

Wei Wang and committed by
GitHub
6b05f50e 757c9e05

+216 -25
+11
supacode/Features/Repositories/Views/RepoHeaderRow.swift
··· 36 36 } 37 37 } 38 38 } 39 + 40 + // MARK: - Previews 41 + 42 + #Preview("RepoHeaderRow") { 43 + VStack(alignment: .leading, spacing: 12) { 44 + RepoHeaderRow(name: "supacode", isRemoving: false, tabCount: 3) 45 + RepoHeaderRow(name: "ghostty", isRemoving: false, tabCount: 0) 46 + RepoHeaderRow(name: "removing-repo", isRemoving: true, tabCount: 1) 47 + } 48 + .padding() 49 + }
+4 -3
supacode/Features/Repositories/Views/RepositorySectionView.swift
··· 160 160 } 161 161 .frame(maxWidth: .infinity) 162 162 .frame(height: headerCellHeight, alignment: .center) 163 + .padding(.top, showsTopSeparator ? 4 : 0) 163 164 .contentShape(.interaction, .rect) 164 165 .background { 165 166 if Self.debugHeaderLayers { ··· 172 173 } 173 174 } 174 175 .overlay(alignment: .top) { 175 - if showsTopSeparator && !isPlainFolderSelected { 176 + if showsTopSeparator && !isPlainFolderSelected && Self.debugHeaderLayers { 176 177 Rectangle() 177 - .fill(Self.debugHeaderLayers ? .blue : .secondary) 178 + .fill(.blue) 178 179 .frame(height: 1) 179 180 .frame(maxWidth: .infinity) 180 181 .accessibilityHidden(true) ··· 220 221 } 221 222 222 223 private var headerCellHeight: CGFloat { 223 - 34 224 + 26 224 225 } 225 226 226 227 static func openTabCount(
+81
supacode/Features/Repositories/Views/SidebarListView.swift
··· 223 223 } 224 224 } 225 225 } 226 + 227 + // MARK: - Previews 228 + 229 + #if DEBUG 230 + @MainActor 231 + private struct SidebarLayoutPreview: View { 232 + @State private var expandedRepoIDs: Set<Repository.ID> 233 + @State private var sidebarSelections: Set<SidebarSelection> = [] 234 + private let store: StoreOf<RepositoriesFeature> 235 + private let terminalManager: WorktreeTerminalManager = .preview 236 + 237 + init() { 238 + let state = Self.mockState 239 + _expandedRepoIDs = State(initialValue: Set(state.repositories.map(\.id))) 240 + store = Store(initialState: state) { EmptyReducer() } 241 + } 242 + 243 + var body: some View { 244 + SidebarListView( 245 + store: store, 246 + expandedRepoIDs: $expandedRepoIDs, 247 + sidebarSelections: $sidebarSelections, 248 + terminalManager: terminalManager 249 + ) 250 + .environment(CommandKeyObserver()) 251 + .frame(width: 280, height: 500) 252 + } 253 + 254 + private static var mockState: RepositoriesFeature.State { 255 + let repo1Root = URL(fileURLWithPath: "/tmp/supacode") 256 + let repo1Worktrees: IdentifiedArrayOf<Worktree> = [ 257 + Worktree( 258 + id: repo1Root.path, name: "main", detail: ".", 259 + workingDirectory: repo1Root, repositoryRootURL: repo1Root 260 + ), 261 + Worktree( 262 + id: "/tmp/wt/sidebar", name: "feature/sidebar-redesign", detail: "/tmp/wt/sidebar", 263 + workingDirectory: URL(fileURLWithPath: "/tmp/wt/sidebar"), repositoryRootURL: repo1Root 264 + ), 265 + Worktree( 266 + id: "/tmp/wt/auth", name: "feature/auth", detail: "/tmp/wt/auth", 267 + workingDirectory: URL(fileURLWithPath: "/tmp/wt/auth"), repositoryRootURL: repo1Root 268 + ), 269 + Worktree( 270 + id: "/tmp/wt/crash", name: "fix/crash", detail: "/tmp/wt/crash", 271 + workingDirectory: URL(fileURLWithPath: "/tmp/wt/crash"), repositoryRootURL: repo1Root 272 + ), 273 + ] 274 + let repo1 = Repository( 275 + id: repo1Root.path, rootURL: repo1Root, name: "supacode", worktrees: repo1Worktrees 276 + ) 277 + 278 + let repo2Root = URL(fileURLWithPath: "/tmp/ghostty") 279 + let repo2Worktrees: IdentifiedArrayOf<Worktree> = [ 280 + Worktree( 281 + id: repo2Root.path, name: "main", detail: ".", 282 + workingDirectory: repo2Root, repositoryRootURL: repo2Root 283 + ), 284 + Worktree( 285 + id: "/tmp/wt/renderer", name: "feature/renderer", detail: "/tmp/wt/renderer", 286 + workingDirectory: URL(fileURLWithPath: "/tmp/wt/renderer"), repositoryRootURL: repo2Root 287 + ), 288 + ] 289 + let repo2 = Repository( 290 + id: repo2Root.path, rootURL: repo2Root, name: "ghostty", worktrees: repo2Worktrees 291 + ) 292 + 293 + var state = RepositoriesFeature.State() 294 + state.repositories = [repo1, repo2] 295 + state.pinnedWorktreeIDs = ["/tmp/wt/auth"] 296 + state.worktreeInfoByID = [ 297 + "/tmp/wt/sidebar": WorktreeInfoEntry(addedLines: 120, removedLines: 45, pullRequest: nil), 298 + ] 299 + return state 300 + } 301 + } 302 + 303 + #Preview("Sidebar Layout") { 304 + SidebarLayoutPreview() 305 + } 306 + #endif
+102 -19
supacode/Features/Repositories/Views/WorktreeRow.swift
··· 80 80 .help("Run script active") 81 81 .accessibilityLabel("Run script active") 82 82 } 83 - if hasChangeCounts, let displayAddedLines, let displayRemovedLines { 84 - Button { 85 - onDiffTap?() 86 - } label: { 87 - WorktreeRowChangeCountView( 88 - addedLines: displayAddedLines, 89 - removedLines: displayRemovedLines, 90 - isSelected: isSelected, 91 - ) 92 - } 93 - .buttonStyle(.plain) 94 - .help( 95 - AppShortcuts.helpText( 96 - title: "Show Diff", 97 - commandID: AppShortcuts.CommandID.showDiff, 98 - in: resolvedKeybindings 99 - )) 100 - } 101 83 if isHovered { 102 84 Button { 103 85 pinAction?() ··· 121 103 .help("Archive Worktree") 122 104 .disabled(archiveAction == nil) 123 105 } 106 + if hasChangeCounts, let displayAddedLines, let displayRemovedLines { 107 + Button { 108 + onDiffTap?() 109 + } label: { 110 + WorktreeRowChangeCountView( 111 + addedLines: displayAddedLines, 112 + removedLines: displayRemovedLines, 113 + isSelected: isSelected, 114 + ) 115 + } 116 + .buttonStyle(.plain) 117 + .help( 118 + AppShortcuts.helpText( 119 + title: "Show Diff", 120 + commandID: AppShortcuts.CommandID.showDiff, 121 + in: resolvedKeybindings 122 + )) 123 + } 124 124 } 125 125 WorktreeRowInfoView( 126 126 worktreeName: detailText, ··· 146 146 } 147 147 148 148 private var worktreeRowHeight: CGFloat { 149 - 42 149 + 36 150 150 } 151 151 } 152 152 ··· 208 208 } 209 209 } 210 210 211 + // MARK: - Previews 212 + 213 + @MainActor 214 + private struct WorktreeRowPreview: View { 215 + @State private var hoveredID: String? 216 + 217 + var body: some View { 218 + List { 219 + row(id: "main", name: "main", worktreeName: "Default", isMainWorktree: true) 220 + row( 221 + id: "diff", name: "feature/sidebar-redesign", worktreeName: "sidebar-redesign", 222 + addedLines: 120, removedLines: 45 223 + ) 224 + row(id: "pinned", name: "feature/pinned-branch", worktreeName: "pinned-branch", isPinned: true) 225 + row(id: "running", name: "feature/auth-flow", worktreeName: "auth-flow", taskStatus: .running) 226 + row(id: "loading", name: "creating-worktree...", worktreeName: "Setting up", isLoading: true) 227 + row(id: "notif", name: "feature/notifications", worktreeName: "notifications", showsNotificationIndicator: true) 228 + row(id: "script", name: "feature/run-script", worktreeName: "run-script", isRunScriptRunning: true) 229 + row(id: "hint", name: "feature/shortcuts", worktreeName: "shortcuts", shortcutHint: "⌘1") 230 + row(id: "selected", name: "feature/selected", worktreeName: "selected", isSelected: true) 231 + } 232 + .listStyle(.sidebar) 233 + .scrollIndicators(.never) 234 + .frame(width: 280, height: 550) 235 + } 236 + 237 + private func row( 238 + id: String, 239 + name: String, 240 + worktreeName: String, 241 + isPinned: Bool = false, 242 + isMainWorktree: Bool = false, 243 + isLoading: Bool = false, 244 + taskStatus: WorktreeTaskStatus? = nil, 245 + isRunScriptRunning: Bool = false, 246 + showsNotificationIndicator: Bool = false, 247 + addedLines: Int? = nil, 248 + removedLines: Int? = nil, 249 + isSelected: Bool = false, 250 + shortcutHint: String? = nil 251 + ) -> some View { 252 + let info: WorktreeInfoEntry? = 253 + if let addedLines, let removedLines { 254 + WorktreeInfoEntry(addedLines: addedLines, removedLines: removedLines, pullRequest: nil) 255 + } else { 256 + nil 257 + } 258 + let isHovered = hoveredID == id 259 + return WorktreeRow( 260 + name: name, 261 + worktreeName: worktreeName, 262 + info: info, 263 + showsPullRequestInfo: false, 264 + isHovered: isHovered, 265 + isPinned: isPinned, 266 + isMainWorktree: isMainWorktree, 267 + isLoading: isLoading, 268 + taskStatus: taskStatus, 269 + isRunScriptRunning: isRunScriptRunning, 270 + showsNotificationIndicator: showsNotificationIndicator, 271 + notifications: [], 272 + onFocusNotification: { _ in }, 273 + shortcutHint: shortcutHint, 274 + pinAction: {}, 275 + isSelected: isSelected, 276 + archiveAction: {}, 277 + onDiffTap: addedLines != nil ? {} : nil 278 + ) 279 + .listRowInsets(EdgeInsets()) 280 + .listRowSeparator(.hidden) 281 + .onHover { hovering in 282 + hoveredID = hovering ? id : nil 283 + } 284 + } 285 + } 286 + 287 + #Preview("WorktreeRow") { 288 + WorktreeRowPreview() 289 + } 290 + 291 + // MARK: - Subviews 292 + 211 293 private struct WorktreeRowChangeCountView: View { 212 294 let addedLines: Int 213 295 let removedLines: Int ··· 219 301 .foregroundStyle(.green) 220 302 Text("-\(removedLines)") 221 303 .foregroundStyle(.red) 304 + .baselineOffset(-1) 222 305 } 223 306 .font(.caption) 224 307 .lineLimit(1)
+18 -3
supacode/Features/Terminal/BusinessLogic/WorktreeTerminalManager.swift
··· 8 8 @MainActor 9 9 @Observable 10 10 final class WorktreeTerminalManager { 11 - private let runtime: GhosttyRuntime 11 + private let runtime: GhosttyRuntime? 12 12 private let layoutPersistence: TerminalLayoutPersistenceClient 13 13 private var states: [Worktree.ID: WorktreeTerminalState] = [:] 14 14 private var notificationsEnabled = true ··· 189 189 } 190 190 let runSetupScript = runSetupScriptIfNew() 191 191 let state = WorktreeTerminalState( 192 - runtime: runtime, 192 + runtime: runtime!, 193 193 worktree: worktree, 194 194 runSetupScript: runSetupScript, 195 195 defaultFontSize: preferredFontSize ··· 361 361 } 362 362 363 363 func surfaceBackgroundOpacity() -> Double { 364 - runtime.backgroundOpacity() 364 + runtime?.backgroundOpacity() ?? 1.0 365 365 } 366 366 367 367 func syncPreferredFontSize(from worktreeID: Worktree.ID) { ··· 524 524 terminalLogger.info("[LayoutRestore] apply: successfully restored \(restoredStates.count) worktree(s)") 525 525 return true 526 526 } 527 + 528 + #if DEBUG 529 + /// Inert instance for SwiftUI previews — no GhosttyRuntime, all reads return defaults. 530 + static let preview: WorktreeTerminalManager = { 531 + let manager = WorktreeTerminalManager(preview: ()) 532 + return manager 533 + }() 534 + 535 + private init(preview: Void) { 536 + self.runtime = nil 537 + self.layoutPersistence = .liveValue 538 + self.preferredFontSize = nil 539 + self.baselineFontSize = 13 540 + } 541 + #endif 527 542 }