Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions Sources/BrowserWindowPortal.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ private var cmuxWindowBrowserPortalKey: UInt8 = 0
private var cmuxWindowBrowserPortalCloseObserverKey: UInt8 = 0
private var cmuxBrowserSearchOverlayPanelIdAssociationKey: UInt8 = 0
private var cmuxBrowserPortalNeedsRenderingStateReattachKey: UInt8 = 0
private var cmuxWindowInteractiveSplitDividerDragKey: UInt8 = 0

#if DEBUG
private func browserPortalDebugToken(_ view: NSView?) -> String {
Expand Down Expand Up @@ -44,6 +45,35 @@ private extension NSResponder {
}
}

private extension NSWindow {
var browserPortalHasInteractiveSplitDividerDrag: Bool {
get {
let isActive =
(objc_getAssociatedObject(self, &cmuxWindowInteractiveSplitDividerDragKey) as? NSNumber)?
.boolValue ?? false
guard isActive else { return false }
guard (NSEvent.pressedMouseButtons & 1) != 0 else {
objc_setAssociatedObject(
self,
&cmuxWindowInteractiveSplitDividerDragKey,
NSNumber(value: false),
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
)
return false
}
return true
}
set {
objc_setAssociatedObject(
self,
&cmuxWindowInteractiveSplitDividerDragKey,
NSNumber(value: newValue),
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
)
}
}
}

private extension WKWebView {
private var browserPortalNeedsRenderingStateReattach: Bool {
get {
Expand Down Expand Up @@ -2057,6 +2087,13 @@ final class WindowBrowserSlotView: NSView {
final class WindowBrowserPortal: NSObject {
private static let transientRecoveryRetryBudget: Int = 12

private static func dividerHitRectContains(_ point: NSPoint, rect: NSRect) -> Bool {
point.x >= rect.minX &&
point.x <= rect.maxX &&
point.y >= rect.minY &&
point.y <= rect.maxY
}

private weak var window: NSWindow?
private let hostView = WindowBrowserHostView(frame: .zero)
private weak var installedContainerView: NSView?
Expand Down Expand Up @@ -2119,7 +2156,60 @@ final class WindowBrowserPortal: NSObject {
return !isInteractiveSplitDividerDrag(in: window)
}

private static func noteInteractiveSplitDividerDragIfNeeded(
_ splitView: NSSplitView,
window: NSWindow,
hostView: WindowBrowserHostView
) {
guard splitView.window === window else { return }
guard !splitView.isDescendant(of: hostView) else { return }
guard (NSEvent.pressedMouseButtons & 1) != 0 else { return }
guard let event = NSApp.currentEvent else { return }
let now = ProcessInfo.processInfo.systemUptime
guard (now - event.timestamp) < 0.1 else { return }
guard event.window === window else { return }
switch event.type {
case .leftMouseDown, .leftMouseDragged:
break
default:
return
}
guard splitView.arrangedSubviews.count >= 2 else { return }

let location = splitView.convert(event.locationInWindow, from: nil)
let first = splitView.arrangedSubviews[0].frame
let second = splitView.arrangedSubviews[1].frame
let thickness = splitView.dividerThickness
let dividerRect: NSRect

if splitView.isVertical {
guard first.width > 1, second.width > 1 else { return }
dividerRect = NSRect(
x: max(0, first.maxX),
y: 0,
width: thickness,
height: splitView.bounds.height
)
} else {
guard first.height > 1, second.height > 1 else { return }
dividerRect = NSRect(
x: 0,
y: max(0, first.maxY),
width: splitView.bounds.width,
height: thickness
)
}

let hitRect = dividerRect.insetBy(dx: -5, dy: -5)
if dividerHitRectContains(location, rect: hitRect) {
window.browserPortalHasInteractiveSplitDividerDrag = true
}
Comment on lines +2177 to +2206

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Divider latch detection only checks the first divider pair.

Line 2173–2174 only inspects arrangedSubviews[0]/[1], so drags on later dividers in a 3+ pane NSSplitView won’t set the latch flag. That leaves a path for redundant external geometry sync during active drag.

Suggested fix
-        guard splitView.arrangedSubviews.count >= 2 else { return }
-
-        let location = splitView.convert(event.locationInWindow, from: nil)
-        let first = splitView.arrangedSubviews[0].frame
-        let second = splitView.arrangedSubviews[1].frame
-        let thickness = splitView.dividerThickness
-        let dividerRect: NSRect
-
-        if splitView.isVertical {
-            guard first.width > 1, second.width > 1 else { return }
-            dividerRect = NSRect(
-                x: max(0, first.maxX),
-                y: 0,
-                width: thickness,
-                height: splitView.bounds.height
-            )
-        } else {
-            guard first.height > 1, second.height > 1 else { return }
-            dividerRect = NSRect(
-                x: 0,
-                y: max(0, first.maxY),
-                width: splitView.bounds.width,
-                height: thickness
-            )
-        }
-
-        if dividerRect.insetBy(dx: -4, dy: -4).contains(location) {
-            window.browserPortalHasInteractiveSplitDividerDrag = true
-        }
+        let location = splitView.convert(event.locationInWindow, from: nil)
+        let thickness = splitView.dividerThickness
+        let dividerCount = max(0, splitView.arrangedSubviews.count - 1)
+        guard dividerCount > 0 else { return }
+
+        for dividerIndex in 0..<dividerCount {
+            let first = splitView.arrangedSubviews[dividerIndex].frame
+            let second = splitView.arrangedSubviews[dividerIndex + 1].frame
+            let dividerRect: NSRect
+
+            if splitView.isVertical {
+                guard first.width > 1 || second.width > 1 else { continue }
+                dividerRect = NSRect(
+                    x: max(0, first.maxX),
+                    y: 0,
+                    width: thickness,
+                    height: splitView.bounds.height
+                )
+            } else {
+                guard first.height > 1 || second.height > 1 else { continue }
+                dividerRect = NSRect(
+                    x: 0,
+                    y: max(0, first.maxY),
+                    width: splitView.bounds.width,
+                    height: thickness
+                )
+            }
+
+            if dividerRect.insetBy(dx: -4, dy: -4).contains(location) {
+                window.browserPortalHasInteractiveSplitDividerDrag = true
+                return
+            }
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
guard splitView.arrangedSubviews.count >= 2 else { return }
let location = splitView.convert(event.locationInWindow, from: nil)
let first = splitView.arrangedSubviews[0].frame
let second = splitView.arrangedSubviews[1].frame
let thickness = splitView.dividerThickness
let dividerRect: NSRect
if splitView.isVertical {
guard first.width > 1, second.width > 1 else { return }
dividerRect = NSRect(
x: max(0, first.maxX),
y: 0,
width: thickness,
height: splitView.bounds.height
)
} else {
guard first.height > 1, second.height > 1 else { return }
dividerRect = NSRect(
x: 0,
y: max(0, first.maxY),
width: splitView.bounds.width,
height: thickness
)
}
if dividerRect.insetBy(dx: -4, dy: -4).contains(location) {
window.browserPortalHasInteractiveSplitDividerDrag = true
}
let location = splitView.convert(event.locationInWindow, from: nil)
let thickness = splitView.dividerThickness
let dividerCount = max(0, splitView.arrangedSubviews.count - 1)
guard dividerCount > 0 else { return }
for dividerIndex in 0..<dividerCount {
let first = splitView.arrangedSubviews[dividerIndex].frame
let second = splitView.arrangedSubviews[dividerIndex + 1].frame
let dividerRect: NSRect
if splitView.isVertical {
guard first.width > 1 || second.width > 1 else { continue }
dividerRect = NSRect(
x: max(0, first.maxX),
y: 0,
width: thickness,
height: splitView.bounds.height
)
} else {
guard first.height > 1 || second.height > 1 else { continue }
dividerRect = NSRect(
x: 0,
y: max(0, first.maxY),
width: splitView.bounds.width,
height: thickness
)
}
if dividerRect.insetBy(dx: -4, dy: -4).contains(location) {
window.browserPortalHasInteractiveSplitDividerDrag = true
return
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/BrowserWindowPortal.swift` around lines 2170 - 2198, The current
detection only checks the first arrangedSubviews pair and misses later dividers;
update the logic in the hit-test block that uses splitView, arrangedSubviews and
dividerThickness to iterate adjacent subview pairs (0/1, 1/2, …) compute each
dividerRect (respecting splitView.isVertical vs horizontal and using
first.maxX/first.maxY and second frames) and test location against each
dividerRect.insetBy(dx: -4, dy: -4); if any contains(location) set
window.browserPortalHasInteractiveSplitDividerDrag = true (and return/exit
early) so drags on any divider latch correctly.

}

private static func isInteractiveSplitDividerDrag(in window: NSWindow) -> Bool {
if window.browserPortalHasInteractiveSplitDividerDrag {
return true
}
guard (NSEvent.pressedMouseButtons & 1) != 0 else { return false }
guard let event = NSApp.currentEvent else { return false }
let now = ProcessInfo.processInfo.systemUptime
Expand Down Expand Up @@ -2155,6 +2245,22 @@ final class WindowBrowserPortal: NSObject {
self?.scheduleExternalGeometrySynchronize()
}
})
geometryObservers.append(center.addObserver(
forName: NSSplitView.willResizeSubviewsNotification,
object: nil,
queue: .main
) { [weak self] notification in
MainActor.assumeIsolated {
guard let self,
let splitView = notification.object as? NSSplitView,
let window = self.window else { return }
Self.noteInteractiveSplitDividerDragIfNeeded(
splitView,
window: window,
hostView: self.hostView
)
}
})
geometryObservers.append(center.addObserver(
forName: NSSplitView.didResizeSubviewsNotification,
object: nil,
Expand Down
14 changes: 14 additions & 0 deletions Sources/TabManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1503,6 +1503,20 @@ class TabManager: ObservableObject {
}
}

private func scheduleInitialWorkspaceGitMetadataRefresh(
workspaceId: UUID,
panelId: UUID,
directory: String
) {
scheduleWorkspaceGitMetadataRefresh(
workspaceId: workspaceId,
panelId: panelId,
directory: directory,
delays: Self.initialWorkspaceGitProbeDelays,
reason: "initial"
)
}

private func scheduleWorkspaceGitMetadataRefresh(
workspaceId: UUID,
panelId: UUID,
Expand Down
4 changes: 2 additions & 2 deletions cmuxTests/TabManagerUnitTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -710,8 +710,8 @@ final class TabManagerPullRequestProbeTests: XCTestCase {
return
}

workspace.updatePanelDirectory(panelId: panelId, directory: repoURL.path)
workspace.updatePanelGitBranch(panelId: panelId, branch: "feature/sidebar-pr", isDirty: false)
manager.updateSurfaceDirectory(tabId: workspace.id, surfaceId: panelId, directory: repoURL.path)
manager.updateSurfaceGitBranch(tabId: workspace.id, surfaceId: panelId, branch: "feature/sidebar-pr", isDirty: false)
workspace.updatePanelPullRequest(
panelId: panelId,
number: 1052,
Expand Down
2 changes: 1 addition & 1 deletion vendor/bonsplit
Loading