From 661e8d098677968d896a36dcf96b6151eb52d62a Mon Sep 17 00:00:00 2001 From: khoi <6994441+khoi@users.noreply.github.com> Date: Thu, 19 Feb 2026 10:29:59 +0700 Subject: [PATCH] Add sidebar multi-select bulk actions and resolve build warnings --- .../Clients/Workspace/WorkspaceClient.swift | 2 +- supacode/Domain/OpenWorktreeAction.swift | 7 +- .../Reducer/RepositoriesFeature.swift | 59 ++++++++++ .../Views/ArchivedWorktreeRowView.swift | 7 +- .../Views/RepositorySectionView.swift | 2 + .../Repositories/Views/SidebarListView.swift | 85 ++++++++++---- .../Repositories/Views/SidebarView.swift | 106 +++++++++++++++--- .../Repositories/Views/WorktreeRow.swift | 7 +- .../Repositories/Views/WorktreeRowsView.swift | 3 +- .../Views/WindowFocusObserverView.swift | 14 ++- .../Ghostty/GhosttySurfaceView.swift | 28 +++-- 11 files changed, 260 insertions(+), 60 deletions(-) diff --git a/supacode/Clients/Workspace/WorkspaceClient.swift b/supacode/Clients/Workspace/WorkspaceClient.swift index 9890eb31..4aa814f4 100644 --- a/supacode/Clients/Workspace/WorkspaceClient.swift +++ b/supacode/Clients/Workspace/WorkspaceClient.swift @@ -6,7 +6,7 @@ struct WorkspaceClient { @MainActor @Sendable ( _ action: OpenWorktreeAction, _ worktree: Worktree, - _ onError: @escaping @MainActor (OpenActionError) -> Void + _ onError: @escaping @MainActor @Sendable (OpenActionError) -> Void ) -> Void } diff --git a/supacode/Domain/OpenWorktreeAction.swift b/supacode/Domain/OpenWorktreeAction.swift index af8bddf4..305cbd59 100644 --- a/supacode/Domain/OpenWorktreeAction.swift +++ b/supacode/Domain/OpenWorktreeAction.swift @@ -229,7 +229,8 @@ enum OpenWorktreeAction: CaseIterable, Identifiable { defaultPriority.first(where: \.isInstalled) ?? .finder } - func perform(with worktree: Worktree, onError: @escaping (OpenActionError) -> Void) { + func perform(with worktree: Worktree, onError: @escaping @MainActor @Sendable (OpenActionError) -> Void) { + let actionTitle = title switch self { case .editor: return @@ -261,7 +262,7 @@ enum OpenWorktreeAction: CaseIterable, Identifiable { Task { @MainActor in onError( OpenActionError( - title: "Unable to open in \(self.title)", + title: "Unable to open in \(actionTitle)", message: error.localizedDescription ) ) @@ -293,7 +294,7 @@ enum OpenWorktreeAction: CaseIterable, Identifiable { Task { @MainActor in onError( OpenActionError( - title: "Unable to open in \(self.title)", + title: "Unable to open in \(actionTitle)", message: error.localizedDescription ) ) diff --git a/supacode/Features/Repositories/Reducer/RepositoriesFeature.swift b/supacode/Features/Repositories/Reducer/RepositoriesFeature.swift index 0429e853..e80e3c62 100644 --- a/supacode/Features/Repositories/Reducer/RepositoriesFeature.swift +++ b/supacode/Features/Repositories/Reducer/RepositoriesFeature.swift @@ -141,6 +141,7 @@ struct RepositoriesFeature { case consumeSetupScript(Worktree.ID) case consumeTerminalFocus(Worktree.ID) case requestArchiveWorktree(Worktree.ID, Repository.ID) + case requestArchiveWorktrees([ArchiveWorktreeTarget]) case archiveWorktreeConfirmed(Worktree.ID, Repository.ID) case unarchiveWorktree(Worktree.ID) case requestDeleteWorktree(Worktree.ID, Repository.ID) @@ -194,6 +195,11 @@ struct RepositoriesFeature { let repositoryID: Repository.ID } + struct ArchiveWorktreeTarget: Equatable { + let worktreeID: Worktree.ID + let repositoryID: Repository.ID + } + private struct ApplyRepositoriesResult { let didPrunePinned: Bool let didPruneRepositoryOrder: Bool @@ -208,6 +214,7 @@ struct RepositoriesFeature { enum Alert: Equatable { case confirmArchiveWorktree(Worktree.ID, Repository.ID) + case confirmArchiveWorktrees([ArchiveWorktreeTarget]) case confirmDeleteWorktree(Worktree.ID, Repository.ID) case confirmDeleteWorktrees([DeleteWorktreeTarget]) case confirmRemoveRepository(Repository.ID) @@ -853,9 +860,58 @@ struct RepositoriesFeature { } return .none + case .requestArchiveWorktrees(let targets): + var validTargets: [ArchiveWorktreeTarget] = [] + var seenWorktreeIDs: Set = [] + for target in targets { + guard seenWorktreeIDs.insert(target.worktreeID).inserted else { continue } + if state.removingRepositoryIDs.contains(target.repositoryID) { + continue + } + guard let repository = state.repositories[id: target.repositoryID], + let worktree = repository.worktrees[id: target.worktreeID] + else { + continue + } + if state.isMainWorktree(worktree) + || state.deletingWorktreeIDs.contains(worktree.id) + || state.isWorktreeArchived(worktree.id) + { + continue + } + validTargets.append(target) + } + guard !validTargets.isEmpty else { + return .none + } + if validTargets.count == 1, let target = validTargets.first { + return .send(.requestArchiveWorktree(target.worktreeID, target.repositoryID)) + } + let count = validTargets.count + state.alert = AlertState { + TextState("Archive \(count) worktrees?") + } actions: { + ButtonState(role: .destructive, action: .confirmArchiveWorktrees(validTargets)) { + TextState("Archive \(count) (⌘↩)") + } + ButtonState(role: .cancel) { + TextState("Cancel") + } + } message: { + TextState("Archive \(count) worktrees?") + } + return .none + case .alert(.presented(.confirmArchiveWorktree(let worktreeID, let repositoryID))): return .send(.archiveWorktreeConfirmed(worktreeID, repositoryID)) + case .alert(.presented(.confirmArchiveWorktrees(let targets))): + return .merge( + targets.map { target in + .send(.archiveWorktreeConfirmed(target.worktreeID, target.repositoryID)) + } + ) + case .archiveWorktreeConfirmed(let worktreeID, let repositoryID): guard let repository = state.repositories[id: repositoryID], let worktree = repository.worktrees[id: worktreeID] @@ -2484,6 +2540,9 @@ extension RepositoriesFeature.State { if case .confirmArchiveWorktree(let worktreeID, let repositoryID)? = button.action.action { return .confirmArchiveWorktree(worktreeID, repositoryID) } + if case .confirmArchiveWorktrees(let targets)? = button.action.action { + return .confirmArchiveWorktrees(targets) + } if case .confirmDeleteWorktree(let worktreeID, let repositoryID)? = button.action.action { return .confirmDeleteWorktree(worktreeID, repositoryID) } diff --git a/supacode/Features/Repositories/Views/ArchivedWorktreeRowView.swift b/supacode/Features/Repositories/Views/ArchivedWorktreeRowView.swift index dac5d4e5..befce50e 100644 --- a/supacode/Features/Repositories/Views/ArchivedWorktreeRowView.swift +++ b/supacode/Features/Repositories/Views/ArchivedWorktreeRowView.swift @@ -13,6 +13,7 @@ struct ArchivedWorktreeRowView: View { pullRequest: info?.pullRequest ) let deleteShortcut = KeyboardShortcut(.delete, modifiers: [.command, .shift]).display + let bodyFontAscender = NSFont.preferredFont(forTextStyle: .body).ascender VStack(alignment: .leading, spacing: 2) { HStack(alignment: .firstTextBaseline, spacing: 8) { Image(systemName: "archivebox") @@ -21,7 +22,7 @@ struct ArchivedWorktreeRowView: View { .accessibilityHidden(true) .frame(width: 16, height: 16) .alignmentGuide(.firstTextBaseline) { _ in - bodyFont.ascender + bodyFontAscender } Text(worktree.name) .font(.body) @@ -62,10 +63,6 @@ struct ArchivedWorktreeRowView: View { .frame(height: rowHeight, alignment: .center) } - private var bodyFont: NSFont { - NSFont.preferredFont(forTextStyle: .body) - } - private var rowHeight: CGFloat { 50 } diff --git a/supacode/Features/Repositories/Views/RepositorySectionView.swift b/supacode/Features/Repositories/Views/RepositorySectionView.swift index c22910e3..11bbcb3f 100644 --- a/supacode/Features/Repositories/Views/RepositorySectionView.swift +++ b/supacode/Features/Repositories/Views/RepositorySectionView.swift @@ -6,6 +6,7 @@ struct RepositorySectionView: View { let showsTopSeparator: Bool let isDragActive: Bool let hotkeyRows: [WorktreeRowModel] + let selectedWorktreeIDs: Set @Binding var expandedRepoIDs: Set @Bindable var store: StoreOf let terminalManager: WorktreeTerminalManager @@ -124,6 +125,7 @@ struct RepositorySectionView: View { repository: repository, isExpanded: isExpanded, hotkeyRows: hotkeyRows, + selectedWorktreeIDs: selectedWorktreeIDs, store: store, terminalManager: terminalManager ) diff --git a/supacode/Features/Repositories/Views/SidebarListView.swift b/supacode/Features/Repositories/Views/SidebarListView.swift index 4eb92783..7f73ea8b 100644 --- a/supacode/Features/Repositories/Views/SidebarListView.swift +++ b/supacode/Features/Repositories/Views/SidebarListView.swift @@ -4,42 +4,83 @@ import SwiftUI struct SidebarListView: View { @Bindable var store: StoreOf @Binding var expandedRepoIDs: Set + @Binding var sidebarSelections: Set let terminalManager: WorktreeTerminalManager @State private var isDragActive = false var body: some View { - let selection = Binding( + let state = store.state + let hotkeyRows = state.orderedWorktreeRows(includingRepositoryIDs: expandedRepoIDs) + let orderedRoots = state.orderedRepositoryRoots() + let selectedWorktreeIDs = Set(sidebarSelections.compactMap(\.worktreeID)) + let selection = Binding>( get: { - if store.isShowingArchivedWorktrees { - return .archivedWorktrees + var nextSelections = sidebarSelections + if state.isShowingArchivedWorktrees { + nextSelections = [.archivedWorktrees] + } else { + nextSelections.remove(.archivedWorktrees) + if let selectedWorktreeID = state.selectedWorktreeID { + nextSelections.insert(.worktree(selectedWorktreeID)) + } } - return store.selectedWorktreeID.map(SidebarSelection.worktree) + return nextSelections }, set: { newValue in - switch newValue { - case .archivedWorktrees: - store.send(.selectArchivedWorktrees) - case .worktree(let id): - store.send(.selectWorktree(id, focusTerminal: true)) - case .repository(let id): - guard let repo = store.state.repositories[id: id], - !store.state.isRemovingRepository(repo) - else { return } + var nextSelections = newValue + let repositorySelections: [Repository.ID] = nextSelections.compactMap { selection in + guard case .repository(let repositoryID) = selection else { return nil } + return repositoryID + } + if !repositorySelections.isEmpty { withAnimation(.easeOut(duration: 0.2)) { - if expandedRepoIDs.contains(id) { - expandedRepoIDs.remove(id) - } else { - expandedRepoIDs.insert(id) + for repositoryID in repositorySelections { + guard let repository = store.state.repositories[id: repositoryID], + !store.state.isRemovingRepository(repository) + else { + continue + } + if expandedRepoIDs.contains(repositoryID) { + expandedRepoIDs.remove(repositoryID) + } else { + expandedRepoIDs.insert(repositoryID) + } } } - case nil: + nextSelections = Set( + nextSelections.filter { + if case .repository = $0 { + return false + } + return true + }) + } + + if nextSelections.contains(.archivedWorktrees) { + sidebarSelections = [.archivedWorktrees] + store.send(.selectArchivedWorktrees) + return + } + + let worktreeIDs = Set(nextSelections.compactMap(\.worktreeID)) + guard !worktreeIDs.isEmpty else { + if !repositorySelections.isEmpty { + return + } + sidebarSelections = [] store.send(.selectWorktree(nil)) + return } + sidebarSelections = Set(worktreeIDs.map(SidebarSelection.worktree)) + if let selectedWorktreeID = state.selectedWorktreeID, worktreeIDs.contains(selectedWorktreeID) { + return + } + let nextPrimarySelection = + hotkeyRows.map(\.id).first(where: worktreeIDs.contains) + ?? worktreeIDs.first + store.send(.selectWorktree(nextPrimarySelection, focusTerminal: true)) } ) - let state = store.state - let hotkeyRows = state.orderedWorktreeRows(includingRepositoryIDs: expandedRepoIDs) - let orderedRoots = state.orderedRepositoryRoots() let repositoriesByID = Dictionary(uniqueKeysWithValues: store.repositories.map { ($0.id, $0) }) List(selection: selection) { if orderedRoots.isEmpty { @@ -50,6 +91,7 @@ struct SidebarListView: View { showsTopSeparator: index > 0, isDragActive: isDragActive, hotkeyRows: hotkeyRows, + selectedWorktreeIDs: selectedWorktreeIDs, expandedRepoIDs: $expandedRepoIDs, store: store, terminalManager: terminalManager @@ -99,6 +141,7 @@ struct SidebarListView: View { showsTopSeparator: index > 0, isDragActive: isDragActive, hotkeyRows: hotkeyRows, + selectedWorktreeIDs: selectedWorktreeIDs, expandedRepoIDs: $expandedRepoIDs, store: store, terminalManager: terminalManager diff --git a/supacode/Features/Repositories/Views/SidebarView.swift b/supacode/Features/Repositories/Views/SidebarView.swift index 7a4910c1..c91dac1a 100644 --- a/supacode/Features/Repositories/Views/SidebarView.swift +++ b/supacode/Features/Repositories/Views/SidebarView.swift @@ -6,6 +6,7 @@ struct SidebarView: View { @Bindable var store: StoreOf let terminalManager: WorktreeTerminalManager @Shared(.appStorage("sidebarCollapsedRepositoryIDs")) private var collapsedRepositoryIDs: [Repository.ID] = [] + @State private var sidebarSelections: Set = [] var body: some View { let state = store.state @@ -26,6 +27,9 @@ struct SidebarView: View { ) let visibleHotkeyRows = state.orderedWorktreeRows(includingRepositoryIDs: expandedRepoIDs) let selectedRow = state.selectedRow(for: state.selectedWorktreeID) + let selectedWorktreeIDs = Set(sidebarSelections.compactMap(\.worktreeID)) + let selectedRows = visibleHotkeyRows.filter { selectedWorktreeIDs.contains($0.id) } + let effectiveSelectedRows = selectedRows.isEmpty ? (selectedRow.map { [$0] } ?? []) : selectedRows let confirmWorktreeAction: (() -> Void)? = { guard let alert = state.confirmWorktreeAlert else { return nil } return { @@ -33,27 +37,103 @@ struct SidebarView: View { } }() let archiveWorktreeAction: (() -> Void)? = { - guard let selectedRow, selectedRow.isRemovable, !selectedRow.isMainWorktree else { return nil } + let targets = + effectiveSelectedRows + .filter { $0.isRemovable && !$0.isMainWorktree && !$0.isDeleting } + .map { + RepositoriesFeature.ArchiveWorktreeTarget( + worktreeID: $0.id, + repositoryID: $0.repositoryID + ) + } + guard !targets.isEmpty else { return nil } return { - store.send(.requestArchiveWorktree(selectedRow.id, selectedRow.repositoryID)) + if targets.count == 1, let target = targets.first { + store.send(.requestArchiveWorktree(target.worktreeID, target.repositoryID)) + } else { + store.send(.requestArchiveWorktrees(targets)) + } } }() let deleteWorktreeAction: (() -> Void)? = { - guard let selectedRow, selectedRow.isRemovable else { return nil } + let targets = + effectiveSelectedRows + .filter { $0.isRemovable && !$0.isDeleting } + .map { + RepositoriesFeature.DeleteWorktreeTarget( + worktreeID: $0.id, + repositoryID: $0.repositoryID + ) + } + guard !targets.isEmpty else { return nil } return { - store.send(.requestDeleteWorktree(selectedRow.id, selectedRow.repositoryID)) + if targets.count == 1, let target = targets.first { + store.send(.requestDeleteWorktree(target.worktreeID, target.repositoryID)) + } else { + store.send(.requestDeleteWorktrees(targets)) + } } }() - SidebarListView(store: store, expandedRepoIDs: expandedRepoIDsBinding, terminalManager: terminalManager) - .focusedSceneValue(\.confirmWorktreeAction, confirmWorktreeAction) - .focusedSceneValue(\.archiveWorktreeAction, archiveWorktreeAction) - .focusedSceneValue(\.deleteWorktreeAction, deleteWorktreeAction) - .focusedSceneValue(\.visibleHotkeyWorktreeRows, visibleHotkeyRows) - .onChange(of: repositoryIDs) { _, newValue in - let collapsed = Set(collapsedRepositoryIDs).intersection(newValue) - $collapsedRepositoryIDs.withLock { - $0 = Array(collapsed).sorted() + SidebarListView( + store: store, + expandedRepoIDs: expandedRepoIDsBinding, + sidebarSelections: $sidebarSelections, + terminalManager: terminalManager + ) + .focusedSceneValue(\.confirmWorktreeAction, confirmWorktreeAction) + .focusedSceneValue(\.archiveWorktreeAction, archiveWorktreeAction) + .focusedSceneValue(\.deleteWorktreeAction, deleteWorktreeAction) + .focusedSceneValue(\.visibleHotkeyWorktreeRows, visibleHotkeyRows) + .onAppear { + sidebarSelections = normalizedSidebarSelections( + current: sidebarSelections, + state: state, + visibleWorktreeIDs: Set(visibleHotkeyRows.map(\.id)) + ) + } + .onChange(of: state.selection) { _, _ in + sidebarSelections = normalizedSidebarSelections( + current: sidebarSelections, + state: state, + visibleWorktreeIDs: Set(visibleHotkeyRows.map(\.id)) + ) + } + .onChange(of: visibleHotkeyRows.map(\.id)) { _, _ in + sidebarSelections = normalizedSidebarSelections( + current: sidebarSelections, + state: state, + visibleWorktreeIDs: Set(visibleHotkeyRows.map(\.id)) + ) + } + .onChange(of: repositoryIDs) { _, newValue in + let collapsed = Set(collapsedRepositoryIDs).intersection(newValue) + $collapsedRepositoryIDs.withLock { + $0 = Array(collapsed).sorted() + } + } + } + + private func normalizedSidebarSelections( + current: Set, + state: RepositoriesFeature.State, + visibleWorktreeIDs: Set + ) -> Set { + if state.isShowingArchivedWorktrees { + return [.archivedWorktrees] + } + var normalized = Set( + current.compactMap { selection -> SidebarSelection? in + guard let worktreeID = selection.worktreeID, + visibleWorktreeIDs.contains(worktreeID) + else { + return nil } + return .worktree(worktreeID) } + ) + if let selectedWorktreeID = state.selectedWorktreeID { + normalized.insert(.worktree(selectedWorktreeID)) + } + return normalized } } diff --git a/supacode/Features/Repositories/Views/WorktreeRow.swift b/supacode/Features/Repositories/Views/WorktreeRow.swift index f37ea139..47990028 100644 --- a/supacode/Features/Repositories/Views/WorktreeRow.swift +++ b/supacode/Features/Repositories/Views/WorktreeRow.swift @@ -36,6 +36,7 @@ struct WorktreeRow: View { let showsPullRequestTag = display.pullRequest != nil && display.pullRequestBadgeStyle != nil let nameColor = colorScheme == .dark ? Color.white : Color.primary let detailText = worktreeName.isEmpty ? name : worktreeName + let bodyFontAscender = NSFont.preferredFont(forTextStyle: .body).ascender VStack(alignment: .leading, spacing: 2) { HStack(alignment: .firstTextBaseline, spacing: 6) { ZStack { @@ -64,7 +65,7 @@ struct WorktreeRow: View { } .frame(width: 16, height: 16) .alignmentGuide(.firstTextBaseline) { _ in - bodyFont.ascender + bodyFontAscender } Text(name) .font(.body) @@ -130,10 +131,6 @@ struct WorktreeRow: View { return PullRequestMergeReadiness(pullRequest: pullRequest) } - private var bodyFont: NSFont { - NSFont.preferredFont(forTextStyle: .body) - } - private var worktreeRowHeight: CGFloat { 42 } diff --git a/supacode/Features/Repositories/Views/WorktreeRowsView.swift b/supacode/Features/Repositories/Views/WorktreeRowsView.swift index 3c87d816..eeb6987d 100644 --- a/supacode/Features/Repositories/Views/WorktreeRowsView.swift +++ b/supacode/Features/Repositories/Views/WorktreeRowsView.swift @@ -6,6 +6,7 @@ struct WorktreeRowsView: View { let repository: Repository let isExpanded: Bool let hotkeyRows: [WorktreeRowModel] + let selectedWorktreeIDs: Set @Bindable var store: StoreOf let terminalManager: WorktreeTerminalManager @Environment(CommandKeyObserver.self) private var commandKeyObserver @@ -177,7 +178,7 @@ struct WorktreeRowsView: View { } private func worktreeRowView(_ row: WorktreeRowModel, config: WorktreeRowViewConfig) -> some View { - let isSelected = row.id == store.state.selectedWorktreeID + let isSelected = selectedWorktreeIDs.contains(row.id) let taskStatus = terminalManager.taskStatus(for: row.id) let isRunScriptRunning = terminalManager.isRunScriptRunning(for: row.id) return WorktreeRow( diff --git a/supacode/Features/Terminal/Views/WindowFocusObserverView.swift b/supacode/Features/Terminal/Views/WindowFocusObserverView.swift index 3e21b448..4a64246c 100644 --- a/supacode/Features/Terminal/Views/WindowFocusObserverView.swift +++ b/supacode/Features/Terminal/Views/WindowFocusObserverView.swift @@ -59,7 +59,9 @@ final class WindowFocusObserverNSView: NSView { object: window, queue: .main ) { [weak self] _ in - self?.emitActivityIfNeeded() + Task { @MainActor [weak self] in + self?.emitActivityIfNeeded() + } }) observers.append( center.addObserver( @@ -67,7 +69,9 @@ final class WindowFocusObserverNSView: NSView { object: window, queue: .main ) { [weak self] _ in - self?.emitActivityIfNeeded() + Task { @MainActor [weak self] in + self?.emitActivityIfNeeded() + } }) observers.append( center.addObserver( @@ -75,7 +79,9 @@ final class WindowFocusObserverNSView: NSView { object: window, queue: .main ) { [weak self] _ in - self?.emitActivityIfNeeded() + Task { @MainActor [weak self] in + self?.emitActivityIfNeeded() + } }) emitActivityIfNeeded(force: true) } @@ -97,7 +103,7 @@ final class WindowFocusObserverNSView: NSView { observers.removeAll() } - deinit { + isolated deinit { clearObservers() } } diff --git a/supacode/Infrastructure/Ghostty/GhosttySurfaceView.swift b/supacode/Infrastructure/Ghostty/GhosttySurfaceView.swift index 5daf5b4a..14f5d714 100644 --- a/supacode/Infrastructure/Ghostty/GhosttySurfaceView.swift +++ b/supacode/Infrastructure/Ghostty/GhosttySurfaceView.swift @@ -177,7 +177,9 @@ final class GhosttySurfaceView: NSView, Identifiable { object: window, queue: .main ) { [weak self] _ in - self?.windowDidChangeScreen() + Task { @MainActor [weak self] in + self?.windowDidChangeScreen() + } }) } @@ -1392,7 +1394,9 @@ final class GhosttySurfaceScrollView: NSView { object: scrollView.contentView, queue: .main ) { [weak self] _ in - self?.handleScrollChange() + Task { @MainActor [weak self] in + self?.handleScrollChange() + } }) observers.append( @@ -1401,7 +1405,9 @@ final class GhosttySurfaceScrollView: NSView { object: scrollView, queue: .main ) { [weak self] _ in - self?.isLiveScrolling = true + Task { @MainActor [weak self] in + self?.isLiveScrolling = true + } }) observers.append( @@ -1410,7 +1416,9 @@ final class GhosttySurfaceScrollView: NSView { object: scrollView, queue: .main ) { [weak self] _ in - self?.isLiveScrolling = false + Task { @MainActor [weak self] in + self?.isLiveScrolling = false + } }) observers.append( @@ -1419,7 +1427,9 @@ final class GhosttySurfaceScrollView: NSView { object: scrollView, queue: .main ) { [weak self] _ in - self?.handleLiveScroll() + Task { @MainActor [weak self] in + self?.handleLiveScroll() + } }) observers.append( @@ -1428,7 +1438,9 @@ final class GhosttySurfaceScrollView: NSView { object: nil, queue: nil ) { [weak self] _ in - self?.handleScrollerStyleChange() + Task { @MainActor [weak self] in + self?.handleScrollerStyleChange() + } }) observers.append( @@ -1437,7 +1449,9 @@ final class GhosttySurfaceScrollView: NSView { object: nil, queue: .main ) { [weak self] _ in - self?.refreshAppearance() + Task { @MainActor [weak self] in + self?.refreshAppearance() + } }) } -- 2.51.2