From 3b2a1c30dec851a0ad160d8c56119874faf73e17 Mon Sep 17 00:00:00 2001 From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Date: Wed, 11 Mar 2026 18:10:12 -0700 Subject: [PATCH 1/2] Add browser slot responder crash regression test --- cmuxTests/CmuxWebViewKeyEquivalentTests.swift | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/cmuxTests/CmuxWebViewKeyEquivalentTests.swift b/cmuxTests/CmuxWebViewKeyEquivalentTests.swift index 3db83f10..51b1afb1 100644 --- a/cmuxTests/CmuxWebViewKeyEquivalentTests.swift +++ b/cmuxTests/CmuxWebViewKeyEquivalentTests.swift @@ -2434,7 +2434,9 @@ final class BrowserSessionHistoryRestoreTests: XCTestCase { @MainActor final class BrowserDeveloperToolsVisibilityPersistenceTests: XCTestCase { - private final class WKInspectorProbeView: NSView {} + private final class WKInspectorProbeView: NSView { + override var acceptsFirstResponder: Bool { true } + } private final class FakeInspector: NSObject { private(set) var attachCount = 0 @@ -11515,6 +11517,46 @@ final class BrowserWindowPortalLifecycleTests: XCTestCase { ) } + func testHidingBrowserSlotYieldsOwnedInspectorFirstResponder() { + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 520, height: 320), + styleMask: [.titled, .closable], + backing: .buffered, + defer: false + ) + defer { window.orderOut(nil) } + realizeWindowLayout(window) + + guard let contentView = window.contentView else { + XCTFail("Expected content view") + return + } + + let slot = WindowBrowserSlotView(frame: NSRect(x: 40, y: 24, width: 260, height: 180)) + contentView.addSubview(slot) + + let inspectorContainer = NSView(frame: slot.bounds) + inspectorContainer.autoresizingMask = [.width, .height] + let inspectorView = WKInspectorProbeView(frame: inspectorContainer.bounds) + inspectorView.autoresizingMask = [.width, .height] + inspectorContainer.addSubview(inspectorView) + slot.addSubview(inspectorContainer) + contentView.layoutSubtreeIfNeeded() + + XCTAssertTrue( + window.makeFirstResponder(inspectorView), + "Precondition failed: inspector probe should become first responder" + ) + XCTAssertTrue(window.firstResponder === inspectorView) + + slot.isHidden = true + + XCTAssertNil( + window.firstResponder, + "Hiding a browser slot should yield any owned inspector responder before it goes off-screen" + ) + } + func testHiddenPortalSyncDoesNotStealLocallyHostedDevToolsWebViewDuringResize() { let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 520, height: 320), From fce5545675accd158167102f1722db9c1cc09908 Mon Sep 17 00:00:00 2001 From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Date: Wed, 11 Mar 2026 18:10:27 -0700 Subject: [PATCH 2/2] Yield hidden browser slot responders --- Sources/BrowserWindowPortal.swift | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/Sources/BrowserWindowPortal.swift b/Sources/BrowserWindowPortal.swift index 200a9ef0..92ba0fe0 100644 --- a/Sources/BrowserWindowPortal.swift +++ b/Sources/BrowserWindowPortal.swift @@ -1530,6 +1530,12 @@ final class BrowserPaneDropTargetView: NSView { final class WindowBrowserSlotView: NSView { override var isOpaque: Bool { false } + override var isHidden: Bool { + didSet { + guard isHidden, !oldValue, let window else { return } + yieldOwnedFirstResponderIfNeeded(in: window, reason: "slotHidden") + } + } private let paneDropTargetView = BrowserPaneDropTargetView(frame: .zero) private let dropZoneOverlayView = BrowserDropZoneOverlayView(frame: .zero) private var searchOverlayHostingView: NSHostingView? @@ -1571,6 +1577,13 @@ final class WindowBrowserSlotView: NSView { nil } + override func viewWillMove(toWindow newWindow: NSWindow?) { + if newWindow == nil, let currentWindow = window { + yieldOwnedFirstResponderIfNeeded(in: currentWindow, reason: "slotWillLeaveWindow") + } + super.viewWillMove(toWindow: newWindow) + } + override func layout() { super.layout() paneDropTargetView.frame = bounds @@ -1739,6 +1752,23 @@ final class WindowBrowserSlotView: NSView { return window.makeFirstResponder(nil) } + @discardableResult + private func yieldOwnedFirstResponderIfNeeded(in window: NSWindow, reason: String) -> Bool { + guard let firstResponder = window.firstResponder, + let owningView = firstResponder.browserPortalOwningView, + owningView === self || owningView.isDescendant(of: self) else { + return false + } +#if DEBUG + dlog( + "browser.slot.firstResponder.yield reason=\(reason) " + + "slot=\(browserPortalDebugToken(self)) " + + "responder=\(String(describing: type(of: firstResponder)))" + ) +#endif + return window.makeFirstResponder(nil) + } + func pinHostedWebView(_ webView: WKWebView) { guard webView.superview === self else { return }