From fabcb068914723df2fe0cb8e72c03379e512744f Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Tue, 17 Mar 2026 16:28:19 -0700 Subject: [PATCH] Fix browser devtools X-close persistence --- Sources/Panels/BrowserPanel.swift | 52 +++++++++++++++++++++++++++ Sources/Panels/BrowserPanelView.swift | 10 ++++++ 2 files changed, 62 insertions(+) diff --git a/Sources/Panels/BrowserPanel.swift b/Sources/Panels/BrowserPanel.swift index 5a214652..f92e7e57 100644 --- a/Sources/Panels/BrowserPanel.swift +++ b/Sources/Panels/BrowserPanel.swift @@ -2152,6 +2152,9 @@ final class BrowserPanel: Panel, ObservableObject { private var pendingDeveloperToolsTransitionTargetVisible: Bool? private var developerToolsTransitionSettleWorkItem: DispatchWorkItem? private let developerToolsTransitionSettleDelay: TimeInterval = 0.15 + private let developerToolsAttachedManualCloseDetectionDelay: TimeInterval = 0.35 + private var developerToolsLastAttachedHostAt: Date? + private var developerToolsLastKnownVisibleAt: Date? private var detachedDeveloperToolsWindowCloseObserver: NSObjectProtocol? private var preferredAttachedDeveloperToolsWidth: CGFloat? private var preferredAttachedDeveloperToolsWidthFraction: CGFloat? @@ -3971,6 +3974,7 @@ extension BrowserPanel { let isVisibleSelector = NSSelectorFromString("isVisible") if inspector.cmuxCallBool(selector: isVisibleSelector) ?? false { developerToolsDetachedOpenGraceDeadline = nil + developerToolsLastKnownVisibleAt = Date() return true } @@ -3980,6 +3984,9 @@ extension BrowserPanel { guard inspector.responds(to: showSelector) else { return false } inspector.cmuxCallVoid(selector: showSelector) let visibleAfterShow = inspector.cmuxCallBool(selector: isVisibleSelector) ?? false + if visibleAfterShow { + developerToolsLastKnownVisibleAt = Date() + } if preferredDeveloperToolsPresentation == .detached { developerToolsDetachedOpenGraceDeadline = visibleAfterShow ? nil @@ -4196,6 +4203,7 @@ extension BrowserPanel { developerToolsDetachedOpenGraceDeadline = nil syncDeveloperToolsPresentationPreferenceFromUI() preferredDeveloperToolsVisible = true + developerToolsLastKnownVisibleAt = Date() cancelDeveloperToolsRestoreRetry() return } @@ -4203,9 +4211,47 @@ extension BrowserPanel { return } preferredDeveloperToolsVisible = false + developerToolsLastKnownVisibleAt = nil cancelDeveloperToolsRestoreRetry() } + func noteDeveloperToolsHostAttached() { + developerToolsLastAttachedHostAt = Date() + developerToolsLastKnownVisibleAt = isDeveloperToolsVisible() ? Date() : nil + } + + @discardableResult + func consumeAttachedDeveloperToolsManualCloseIfNeeded(inspector: NSObject? = nil) -> Bool { + guard preferredDeveloperToolsVisible else { return false } + guard preferredDeveloperToolsPresentation != .detached else { return false } + guard !isDeveloperToolsTransitionInFlight else { return false } + guard webView.superview != nil, webView.window != nil else { return false } + guard let developerToolsLastAttachedHostAt else { return false } + guard Date().timeIntervalSince(developerToolsLastAttachedHostAt) >= developerToolsAttachedManualCloseDetectionDelay else { + return false + } + guard developerToolsLastKnownVisibleAt != nil else { return false } + guard let inspector = inspector ?? webView.cmuxInspectorObject() else { return false } + guard let visible = inspector.cmuxCallBool(selector: NSSelectorFromString("isVisible")) else { return false } + guard !visible else { + developerToolsLastKnownVisibleAt = Date() + return false + } + + preferredDeveloperToolsVisible = false + developerToolsDetachedOpenGraceDeadline = nil + developerToolsLastKnownVisibleAt = nil + forceDeveloperToolsRefreshOnNextAttach = false + cancelDeveloperToolsRestoreRetry() +#if DEBUG + dlog( + "browser.devtools attachedClose.consume panel=\(id.uuidString.prefix(5)) " + + "\(debugDeveloperToolsStateSummary()) \(debugDeveloperToolsGeometrySummary())" + ) +#endif + return true + } + /// Called after WKWebView reattaches to keep inspector stable across split/layout churn. func restoreDeveloperToolsAfterAttachIfNeeded() { guard preferredDeveloperToolsVisible else { @@ -4226,6 +4272,7 @@ extension BrowserPanel { if visible { developerToolsDetachedOpenGraceDeadline = nil syncDeveloperToolsPresentationPreferenceFromUI() + developerToolsLastKnownVisibleAt = Date() #if DEBUG if shouldForceRefresh { dlog("browser.devtools refresh.consumeVisible panel=\(id.uuidString.prefix(5)) \(debugDeveloperToolsStateSummary())") @@ -4249,6 +4296,10 @@ extension BrowserPanel { return } + if consumeAttachedDeveloperToolsManualCloseIfNeeded(inspector: inspector) { + return + } + #if DEBUG if shouldForceRefresh { dlog("browser.devtools refresh.forceShowWhenHidden panel=\(id.uuidString.prefix(5)) \(debugDeveloperToolsStateSummary())") @@ -4264,6 +4315,7 @@ extension BrowserPanel { let visibleAfterShow = inspector.cmuxCallBool(selector: NSSelectorFromString("isVisible")) ?? false if visibleAfterShow { syncDeveloperToolsPresentationPreferenceFromUI() + developerToolsLastKnownVisibleAt = Date() cancelDeveloperToolsRestoreRetry() scheduleDetachedDeveloperToolsWindowDismissal() } else { diff --git a/Sources/Panels/BrowserPanelView.swift b/Sources/Panels/BrowserPanelView.swift index 596820de..2d45cfa9 100644 --- a/Sources/Panels/BrowserPanelView.swift +++ b/Sources/Panels/BrowserPanelView.swift @@ -513,6 +513,14 @@ struct BrowserPanelView: View { refreshSuggestions() } } + .onChange(of: isVisibleInUI) { visibleInUI in + guard !visibleInUI else { return } + // The attached WebKit inspector close button can hide DevTools without + // touching BrowserPanel's persisted intent. Capture the actual inspector + // visibility before the surface detaches so switching away and back + // does not resurrect a manually closed inspector. + panel.syncDeveloperToolsPreferenceFromInspector() + } .onChange(of: isFocused) { focused in #if DEBUG logBrowserFocusState( @@ -5689,6 +5697,7 @@ struct WebViewRepresentable: NSViewRepresentable { coordinator.lastPortalHostId = nil coordinator.lastSynchronizedHostGeometryRevision = 0 if didAttachWebViewToLocalHost { + panel.noteDeveloperToolsHostAttached() panel.restoreDeveloperToolsAfterAttachIfNeeded() webView.needsLayout = true webView.layoutSubtreeIfNeeded() @@ -5702,6 +5711,7 @@ struct WebViewRepresentable: NSViewRepresentable { host.scheduleHostedInspectorDockConfigurationSync(reason: "localInline.update.async") } } else { + panel.consumeAttachedDeveloperToolsManualCloseIfNeeded() host.scheduleHostedInspectorDockConfigurationSync(reason: "localInline.update") }