Skip to content
Open
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
3 changes: 3 additions & 0 deletions Sources/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ final class MainWindowHostingView<Content: View>: NSHostingView<Content> {
override var safeAreaInsets: NSEdgeInsets { NSEdgeInsetsZero }
override var safeAreaRect: NSRect { bounds }
override var safeAreaLayoutGuide: NSLayoutGuide { zeroSafeAreaLayoutGuide }
// Minimal mode pulls interactive SwiftUI chrome into the titlebar band.
// Keep the root host non-draggable so workspace and pane tabs receive clicks.
override var mouseDownCanMoveWindow: Bool { false }

required init(rootView: Content) {
super.init(rootView: rootView)
Expand Down
100 changes: 100 additions & 0 deletions Sources/BrowserWindowPortal.swift
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,9 @@ final class WindowBrowserHostView: NSView {
#endif
return nil
}
if shouldPassThroughToPaneTabBar(at: point, eventType: NSApp.currentEvent?.type) {
return nil
}
if sidebarPassThrough {
#if DEBUG
debugLogPointerRouting(
Expand Down Expand Up @@ -695,6 +698,35 @@ final class WindowBrowserHostView: NSView {
return windowPoint.y >= interactionBandMinY
}

private func shouldPassThroughToPaneTabBar(
at point: NSPoint,
eventType: NSEvent.EventType?
) -> Bool {
switch eventType {
case .leftMouseDown, .leftMouseUp,
.rightMouseDown, .rightMouseUp,
.otherMouseDown, .otherMouseUp:
break
default:
return false
}

let windowPoint = convert(point, to: nil)
let registryHit = window.map {
BonsplitTabBarHitRegionRegistry.containsWindowPoint(windowPoint, in: $0)
} ?? false
let result = registryHit || Self.hasUnderlyingBonsplitTabBarBackground(at: windowPoint, below: self)
#if DEBUG
if eventType == .leftMouseDown {
dlog(
"portal.brwsr.passThroughTabBar wp=\(Int(windowPoint.x)),\(Int(windowPoint.y)) " +
"registry=\(registryHit ? 1 : 0) result=\(result ? 1 : 0)"
)
}
#endif
return result
}
Comment on lines +701 to +728
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 | 🟡 Minor

Hover still won’t reach pane tabs.

Because Line 705 only whitelists mouse down/up events, split-pane and left-pane tab bars under the browser portal still won’t receive move/enter/exit/cursor updates. Click/right-click will work again, but hover highlight and pointer-state changes stay intercepted by the host.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/BrowserWindowPortal.swift` around lines 701 - 728, The switch in
shouldPassThroughToPaneTabBar currently only allows mouse down/up types so hover
and pointer updates are blocked; update the eventType whitelist in
shouldPassThroughToPaneTabBar to also include .mouseMoved, .mouseEntered,
.mouseExited, and .cursorUpdate (and optionally the dragged variants if desired)
so move/enter/exit/cursor updates are treated like clicks and can pass through
to the pane tab bar; keep the rest of the logic (windowPoint conversion,
BonsplitTabBarHitRegionRegistry check and debug logging) unchanged.


private func shouldPassThroughToSidebarResizer(at point: NSPoint) -> Bool {
let dividerHit = splitDividerHit(at: point)
let hostedInspectorHit = dividerHit == nil ? hostedInspectorDividerHit(at: point) : nil
Expand Down Expand Up @@ -1222,6 +1254,74 @@ final class WindowBrowserHostView: NSView {
}
}

private static func hasBonsplitTabBarBackground(at windowPoint: NSPoint, in view: NSView) -> Bool {
guard !view.isHidden, view.alphaValue > 0 else { return false }

let className = NSStringFromClass(type(of: view))
if className.contains("TabBarBackgroundNSView") {
let pointInView = view.convert(windowPoint, from: nil)
let inside = view.bounds.contains(pointInView)
#if DEBUG
let frameInWindow = view.convert(view.bounds, to: nil)
dlog(
"portal.brwsr.bg.found cls=\(className) " +
"boundsInWin=\(Int(frameInWindow.minX)),\(Int(frameInWindow.minY)) " +
"\(Int(frameInWindow.width))x\(Int(frameInWindow.height)) " +
"wp=\(Int(windowPoint.x)),\(Int(windowPoint.y)) inside=\(inside ? 1 : 0)"
)
#endif
if inside {
return true
}
}

for subview in view.subviews.reversed() {
if hasBonsplitTabBarBackground(at: windowPoint, in: subview) {
return true
}
}

return false
}

private static func hasUnderlyingBonsplitTabBarBackground(
at windowPoint: NSPoint,
below portalHost: NSView
) -> Bool {
if let container = portalHost.superview,
let hostIndex = container.subviews.firstIndex(of: portalHost) {
for sibling in container.subviews[..<hostIndex].reversed() {
guard !sibling.isHidden, sibling.alphaValue > 0 else { continue }
// Minimal mode lets the pane tab strip render into the titlebar band,
// so the immediate sibling container may not contain the click even
// though a descendant tab-bar backing view does.
if hasBonsplitTabBarBackground(at: windowPoint, in: sibling) {
return true
}
}
return false
}

guard let window = portalHost.window,
let rootView = window.contentView else {
return false
}
return hasBonsplitTabBarBackground(at: windowPoint, in: rootView)
}

#if DEBUG
static func hasBonsplitTabBarBackgroundForTesting(at windowPoint: NSPoint, in view: NSView) -> Bool {
hasBonsplitTabBarBackground(at: windowPoint, in: view)
}

static func hasUnderlyingBonsplitTabBarBackgroundForTesting(
at windowPoint: NSPoint,
below portalHost: NSView
) -> Bool {
hasUnderlyingBonsplitTabBarBackground(at: windowPoint, below: portalHost)
}
#endif

}

private final class BrowserDropZoneOverlayView: NSView {
Expand Down
124 changes: 121 additions & 3 deletions Sources/TerminalWindowPortal.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import AppKit
import ObjectiveC
#if DEBUG
import Bonsplit
#endif
import ObjectiveC

private var cmuxWindowTerminalPortalKey: UInt8 = 0
private var cmuxWindowTerminalPortalCloseObserverKey: UInt8 = 0
Expand Down Expand Up @@ -146,6 +144,16 @@ final class WindowTerminalHostView: NSView {
}

if isPointerEvent {
if shouldPassThroughToTitlebar(at: point) {
clearActiveDividerCursor(restoreArrow: false)
return nil
}

if shouldPassThroughToPaneTabBar(at: point, eventType: currentEvent?.type) {
clearActiveDividerCursor(restoreArrow: false)
return nil
}

if shouldPassThroughToSidebarResizer(at: point) {
clearActiveDividerCursor(restoreArrow: false)
return nil
Expand Down Expand Up @@ -199,6 +207,48 @@ final class WindowTerminalHostView: NSView {
return hitView === self ? nil : hitView
}

private func shouldPassThroughToTitlebar(at point: NSPoint) -> Bool {
guard let window else { return false }

// Window-level terminal portals sit above SwiftUI. In minimal mode the
// pane tab strip is intentionally rendered in the titlebar band, so
// terminal hosts must never claim hits there.
let windowPoint = convert(point, to: nil)
let nativeTitlebarHeight = window.frame.height - window.contentLayoutRect.height
let customTitlebarBandHeight = max(28, min(72, nativeTitlebarHeight))
let interactionBandMinY = window.contentLayoutRect.maxY - customTitlebarBandHeight - 0.5
return windowPoint.y >= interactionBandMinY
}

private func shouldPassThroughToPaneTabBar(
at point: NSPoint,
eventType: NSEvent.EventType?
) -> Bool {
switch eventType {
case .leftMouseDown, .leftMouseUp,
.rightMouseDown, .rightMouseUp,
.otherMouseDown, .otherMouseUp:
break
default:
return false
}

let windowPoint = convert(point, to: nil)
let registryHit = window.map {
BonsplitTabBarHitRegionRegistry.containsWindowPoint(windowPoint, in: $0)
} ?? false
let result = registryHit || Self.hasUnderlyingBonsplitTabBarBackground(at: windowPoint, below: self)
#if DEBUG
if eventType == .leftMouseDown {
dlog(
"portal.term.passThroughTabBar wp=\(Int(windowPoint.x)),\(Int(windowPoint.y)) " +
"registry=\(registryHit ? 1 : 0) result=\(result ? 1 : 0)"
)
}
#endif
return result
}
Comment on lines +223 to +250
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 | 🟡 Minor

Pane-tab hover is still blocked here.

Line 227 only passes mouse down/up through to the underlying Bonsplit tab bar. That restores click/right-click, but hover/enter/exit/cursor updates for split-pane and left-pane tabs will still get swallowed by the terminal portal host.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/TerminalWindowPortal.swift` around lines 223 - 250, The method
shouldPassThroughToPaneTabBar currently only allows mouse down/up events to pass
through, which still swallows hover and cursor events; update the switch on
eventType in shouldPassThroughToPaneTabBar to also allow hover-related event
types (at minimum .mouseMoved, .mouseEntered, .mouseExited, and .cursorUpdate,
and optionally .leftMouseDragged/.rightMouseDragged if relevant) to fall through
to the passthrough logic so registry checks
(BonsplitTabBarHitRegionRegistry.containsWindowPoint) and
Self.hasUnderlyingBonsplitTabBarBackground are used for those events too; keep
the existing registryHit/result computation and debug logging unchanged.


private func shouldPassThroughToSidebarResizer(at point: NSPoint) -> Bool {
// The sidebar resizer handle is implemented in SwiftUI. When terminals
// are portal-hosted, this AppKit host can otherwise sit above the handle
Expand Down Expand Up @@ -382,6 +432,74 @@ final class WindowTerminalHostView: NSView {
}
}

private static func hasBonsplitTabBarBackground(at windowPoint: NSPoint, in view: NSView) -> Bool {
guard !view.isHidden, view.alphaValue > 0 else { return false }

let className = NSStringFromClass(type(of: view))
if className.contains("TabBarBackgroundNSView") {
let pointInView = view.convert(windowPoint, from: nil)
let inside = view.bounds.contains(pointInView)
#if DEBUG
let frameInWindow = view.convert(view.bounds, to: nil)
dlog(
"portal.term.bg.found cls=\(className) " +
"boundsInWin=\(Int(frameInWindow.minX)),\(Int(frameInWindow.minY)) " +
"\(Int(frameInWindow.width))x\(Int(frameInWindow.height)) " +
"wp=\(Int(windowPoint.x)),\(Int(windowPoint.y)) inside=\(inside ? 1 : 0)"
)
#endif
if inside {
return true
}
}

for subview in view.subviews.reversed() {
if hasBonsplitTabBarBackground(at: windowPoint, in: subview) {
return true
}
}

return false
}

private static func hasUnderlyingBonsplitTabBarBackground(
at windowPoint: NSPoint,
below portalHost: NSView
) -> Bool {
if let container = portalHost.superview,
let hostIndex = container.subviews.firstIndex(of: portalHost) {
for sibling in container.subviews[..<hostIndex].reversed() {
guard !sibling.isHidden, sibling.alphaValue > 0 else { continue }
// Minimal mode lets the pane tab strip render into the titlebar band,
// so the immediate sibling container may not contain the click even
// though a descendant tab-bar backing view does.
if hasBonsplitTabBarBackground(at: windowPoint, in: sibling) {
return true
}
}
return false
}

guard let window = portalHost.window,
let rootView = window.contentView else {
return false
}
return hasBonsplitTabBarBackground(at: windowPoint, in: rootView)
}

#if DEBUG
static func hasBonsplitTabBarBackgroundForTesting(at windowPoint: NSPoint, in view: NSView) -> Bool {
hasBonsplitTabBarBackground(at: windowPoint, in: view)
}

static func hasUnderlyingBonsplitTabBarBackgroundForTesting(
at windowPoint: NSPoint,
below portalHost: NSView
) -> Bool {
hasUnderlyingBonsplitTabBarBackground(at: windowPoint, below: portalHost)
}
#endif
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Duplicated tab bar detection logic across portal classes

Low Severity

hasBonsplitTabBarBackground, hasUnderlyingBonsplitTabBarBackground, shouldPassThroughToPaneTabBar, and their testing wrappers are copy-pasted across WindowTerminalHostView and WindowBrowserHostView with identical logic (only debug log prefixes differ). A bug fix applied to one will likely be missed in the other. Since these are static methods operating on plain NSView hierarchies with no dependency on the host class, they could live in a shared extension or utility.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e2e96fe. Configure here.


#if DEBUG
private func logDragRouteDecision(
passThrough: Bool,
Expand Down
Loading
Loading