From 1ae7fcb478e87b30db422fcd1a4a3bebf185d7cd Mon Sep 17 00:00:00 2001 From: arzafran Date: Fri, 30 Oct 2026 13:06:18 -0300 Subject: [PATCH 1/2] fix: keep the sidebar from sticking mid-slide when toggled quickly (INC-423) Wait for the slide to visually finish before ending the portal resize, and measure the traffic lights only once the sidebar has settled. --- Sources/ContentView.swift | 5 +++++ Sources/Update/UpdateTitlebarAccessory.swift | 10 ++++++++++ Sources/VerticalTabsSidebar.swift | 13 +++++++++++-- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index 89202895..3efebe5f 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -731,6 +731,11 @@ struct ContentView: View { updateSidebarResizerBandState() syncTrafficLightInset() } + // The portal stops following SwiftUI frames when the slide ends; sync it to the final one. + .onReceive(NotificationCenter.default.publisher(for: .programaSidebarToggleDidSettle, object: sidebarState)) { _ in + guard let observedWindow else { return } + TerminalWindowPortalRegistry.scheduleExternalGeometrySynchronize(for: observedWindow) + } .onChange(of: sidebarMatchTerminalBackground) { guard sidebarState.isVisible, sidebarBlendMode == SidebarBlendModeOption.withinWindow.rawValue else { return } diff --git a/Sources/Update/UpdateTitlebarAccessory.swift b/Sources/Update/UpdateTitlebarAccessory.swift index 28d84987..42b4bc4c 100644 --- a/Sources/Update/UpdateTitlebarAccessory.swift +++ b/Sources/Update/UpdateTitlebarAccessory.swift @@ -743,6 +743,14 @@ private final class TrafficLightsTrailingEdgeView: NSView { self?.scheduleMeasure() } } + // Measurements are skipped while the sidebar slides, so measure once it settles. + windowObservers.append( + NotificationCenter.default.addObserver( + forName: .programaSidebarToggleDidSettle, object: nil, queue: .main + ) { [weak self] _ in + self?.scheduleMeasure() + } + ) scheduleMeasure() } @@ -764,6 +772,8 @@ private final class TrafficLightsTrailingEdgeView: NSView { DispatchQueue.main.async { [weak self] in guard let self else { return } self.measurePending = false + // Mid-slide frames carry the transition offset; the settle notification re-measures. + guard !TerminalWindowPortalRegistry.isInteractiveGeometryResizeActive else { return } self.onChange?(self.measureZoomButtonMaxX()) } } diff --git a/Sources/VerticalTabsSidebar.swift b/Sources/VerticalTabsSidebar.swift index 0eec2ab9..89f08953 100644 --- a/Sources/VerticalTabsSidebar.swift +++ b/Sources/VerticalTabsSidebar.swift @@ -6,6 +6,9 @@ import SwiftUI extension Notification.Name { static let programaSidebarVisibilityDidChange = Notification.Name("programaSidebarVisibilityDidChange") + /// Posted with the `SidebarState` as object when its show/hide animation finishes. + static let programaSidebarToggleDidSettle = + Notification.Name("programaSidebarToggleDidSettle") } final class SidebarState: ObservableObject { @@ -28,10 +31,16 @@ final class SidebarState: ObservableObject { // The terminal is an AppKit portal; marking the toggle as an interactive resize makes // it follow the animated SwiftUI frame every tick, as it does for a sidebar drag. TerminalWindowPortalRegistry.beginInteractiveGeometryResize() - withAnimation(reduceMotion ? .easeOut(duration: 0.15) : .snappy(duration: 0.24)) { + // `.removed` waits for the visual end: the default (logically complete) fires as soon as + // another toggle retargets the slide, which ended the portal resize mid-motion. + withAnimation( + reduceMotion ? .easeOut(duration: 0.15) : .snappy(duration: 0.24), + completionCriteria: .removed + ) { isVisible.toggle() - } completion: { + } completion: { [weak self] in TerminalWindowPortalRegistry.endInteractiveGeometryResize() + NotificationCenter.default.post(name: .programaSidebarToggleDidSettle, object: self) } } } From 20aab76491f1ac7609659ca802329aa2c7810702 Mon Sep 17 00:00:00 2001 From: arzafran Date: Fri, 30 Oct 2026 13:08:41 -0300 Subject: [PATCH 2/2] fix: release the sidebar's pending resize marks if its window goes away mid-slide (INC-423) --- Sources/VerticalTabsSidebar.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Sources/VerticalTabsSidebar.swift b/Sources/VerticalTabsSidebar.swift index 89f08953..63f0f574 100644 --- a/Sources/VerticalTabsSidebar.swift +++ b/Sources/VerticalTabsSidebar.swift @@ -19,6 +19,17 @@ final class SidebarState: ObservableObject { } } @Published var persistedWidth: CGFloat + /// Slides whose completion has not run; released in deinit so a window closed + /// mid-slide cannot leave the global portal resize counter raised. + private var pendingSlides = 0 + + deinit { + let pending = pendingSlides + guard pending > 0 else { return } + Task { @MainActor in + for _ in 0..