Skip to content

Fix Browser DevTools host reconciliation#4980

Open
lawrencecchen wants to merge 7 commits into
mainfrom
issue-2933-browser-devtools
Open

Fix Browser DevTools host reconciliation#4980
lawrencecchen wants to merge 7 commits into
mainfrom
issue-2933-browser-devtools

Conversation

@lawrencecchen
Copy link
Copy Markdown
Contributor

@lawrencecchen lawrencecchen commented May 29, 2026

Fixes #2933

Summary:

  • Route detached DevTools closes through the owning inspector frontend instead of the window title.
  • Reconcile DevTools visibility after local inline and portal host updates instead of unconditionally restoring.
  • Preserve forced visible refreshes through visible layout transitions while stable host hides record close intent.

Tests:

  • PASS: ./scripts/reload.sh --tag dev2933
  • ATTEMPTED: AWS cmux-unit selected BrowserDeveloperToolsVisibilityPersistenceTests. The app-host test runner crashes in XCTest memory checker on WebKit-backed DevTools teardown before a clean summary on macOS 15.7.4.

View with Codesmith Autofix with Codesmith
Need help on this PR? Tag @codesmith with what you need. Autofix is disabled.


Note

Medium Risk
Touches WebKit inspector visibility, window close interception, and portal/local-inline hosting paths where incorrect state could reopen DevTools or leave stray windows; scope is browser-panel–local with added regression tests.

Overview
DevTools close routing and host reconciliation are reworked so panel ownership and lifecycle state drive behavior instead of window titles or unconditional restore.

Detached closes no longer depend on a Web Inspector title prefix: AppDelegate forwards close actions to browser panels, which match detached windows via the owning inspector frontend (detachedDeveloperToolsWindowBelongsToPanel). Stray detached windows are dismissed only when they belong to that panel, avoiding closing another panel’s detached inspector.

A DeveloperToolsLifecyclePhase (hidden / opening / visible / closing / restoring) centralizes visible intent, pending opens during host churn, and when a stable host hide counts as a manual close. reconcileDeveloperToolsAfterHostUpdate replaces always calling restore after local-inline and portal host updates: restore when attaching or visibility changes; record manual close when DevTools were visible, the host is stable, and WebKit reports hidden; otherwise sync preference while preserving intent when detached or mid-transition.

BrowserPanelView passes host-attach and visibility deltas into that reconcile path instead of restoreDeveloperToolsAfterAttachIfNeeded alone.

Reviewed by Cursor Bugbot for commit f23fc90. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Fixes DevTools close routing and host reconciliation in Browser panels. Centralizes lifecycle so pending opens survive host churn, while stable host hides are treated as manual closes and other panels’ detached windows aren’t touched.

  • Bug Fixes
    • Route detached DevTools close through the owning inspector frontend; AppDelegate forwards actions and panels determine ownership (no window title heuristics).
    • Centralize DevTools lifecycle (hidden/opening/visible/restoring/closing) to preserve visible intent and allow forced refresh during in‑flight transitions; drive hosting and visibility sync off this state.
    • Replace unconditional restore with reconcileDeveloperToolsAfterHostUpdate(...): restore on attach or host visibility change; treat stable host hides as a manual close; preserve intent while detached or mid‑transition.
    • Scope detached window detection and dismissal to the owning panel; never close another panel’s detached inspector window.
    • Add tests for title‑less detached close routing, not closing other panels’ detached windows on cleanup, pending attached open surviving hidden inspector sync during host churn, and respecting manual close on local inline host updates.

Written for commit f23fc90. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes

    • Improved developer tools lifecycle so detached DevTools windows close reliably and manual closes are honored without affecting unrelated inspector windows.
    • Preserved DevTools visibility intent during host updates to avoid unexpected reopenings.
    • Refined host/portal binding logic to more safely reconcile DevTools state across hosting changes.
  • Tests

    • Added regression tests covering DevTools visibility persistence, detached inspector close behavior, and hosting churn scenarios.

Review Change Stack

@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@vercel
Copy link
Copy Markdown

vercel Bot commented May 29, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment May 29, 2026 7:26am
cmux-staging Building Building Preview, Comment May 29, 2026 7:26am

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 29, 2026

📝 Walkthrough

Walkthrough

Refactors Developer Tools lifecycle into explicit phases, centralizes host-update reconciliation, tightens detached DevTools window detection and close routing, updates host-binding logic in both local-inline and window-portal flows, and adds regression tests for close and host-update scenarios.

Changes

DevTools Lifecycle & Host Update Reconciliation

Layer / File(s) Summary
Detached Inspector Close Routing & Window Identification
Sources/AppDelegate.swift, Sources/Panels/BrowserPanel.swift, cmuxTests/BrowserConfigTests.swift
Removes AppDelegate's detached-inspector guard, introduces isDetachedDeveloperToolsWindow/panel-owned detection, tightens stray-detached dismissal to panel-owned windows, and adds tests for empty-title __close and debug dismissal behavior.
DeveloperToolsLifecyclePhase & Helpers
Sources/Panels/BrowserPanel.swift
Adds DeveloperToolsLifecyclePhase, stores lifecycle state, and centralizes mark-visible/hidden/pending-visible helpers and debug reporting.
Reconciliation, Restore, and Manual-Close Handling
Sources/Panels/BrowserPanel.swift
Reworks restore/retry logic and stable-host reconciliation; adds recordDeveloperToolsManualCloseDuringStableHostUpdate() and reconcileDeveloperToolsAfterHostUpdate(...); tightens manual-close consumption and transition scheduling.
Local Inline Host Update DevTools Reconciliation
Sources/Panels/BrowserPanelView.swift
Snapshots prior DevTools visibility for local-inline host updates and replaces attach+restore calls with reconcileDeveloperToolsAfterHostUpdate(...); updates reveal computation to use the snapshot.
Window Portal Host Update & Rebinding Logic
Sources/Panels/BrowserPanelView.swift
Snapshots DevTools visibility for portal updates, explicitly computes portal reattach/visibility/z-priority deltas, simplifies rebind triggers, and integrates reconciliation after portal bind.
Regression Tests for Close & Update Scenarios
cmuxTests/BrowserConfigTests.swift
Adds tests validating detached inspector __close with empty title, that detached DevTools dismissal doesn't close unrelated inspectors, pending attached-open survival across host churn, and respecting manual close during local-inline host updates.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • manaflow-ai/cmux#4887: Modifies BrowserDeveloperToolsVisibilityPersistenceTests with overlapping test-suite changes.
  • manaflow-ai/cmux#4182: Related changes to AppDelegate and BrowserPanel close/teardown routing.
  • manaflow-ai/cmux#4244: Related developer-tools visibility/manual-close handling adjustments in BrowserPanel.swift.

Suggested reviewers

  • Ari4ka

Poem

🐇 A tiny rabbit hops to the pane,
Quietly closing a wayward window's reign.
Phases now guard when the inspector may peep,
Hosts rebind kindly and let manual closes keep—
Hooray for a calmer DevTools domain!


Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore

❌ Failed checks (3 errors, 1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Cmux Swift Blocking Runtime ❌ Error Main-thread blocking NSCondition.wait() in CmuxDiffViewerURLSchemeHandler.stopSchemeTask (line 2785) violates swift-blocking-runtime.md; called from UIActor-isolated WKURLSchemeHandler method. Replace NSCondition.wait() with async signaling via completion callback or Task.cancel() to signal work completion instead of blocking main thread.
Cmux Swift Logging ❌ Error 19 unguarded NSLog calls added in production code: 15 in BrowserPanel.swift, 4 in AppDelegate.swift, violating swift-logging.md which requires debug logs guarded by #if DEBUG. Wrap NSLog statements in #if DEBUG/#endif guards or use Logger/cmuxDebugLog for production diagnostics per swift-logging.md.
Cmux Architecture Rethink ❌ Error PR adds timing delays (0.35s) to detect manual close and uses lifecycle phase as side-state gating, creating split UI lifecycle ownership between phases and inspector visibility checks. Replace delay-based detection with direct invariant: manual close allowed only when phase=visible AND inspector hidden. Consolidate the two state owners into one predicate.
Docstring Coverage ⚠️ Warning Docstring coverage is 6.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The PR description includes a summary, testing approach, and notes. However, it lacks several required template sections: no demo video (UI/behavior change provided), missing explicit checklist items, and incomplete test verification details. Add missing sections: include a demo video URL or attachment, complete all checklist items with clear confirmations, and clarify test results (AWS test failure context and local test details).
✅ Passed checks (13 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Fix Browser DevTools host reconciliation' accurately reflects the main change in this PR, which introduces a new reconciliation mechanism for DevTools host updates.
Linked Issues check ✅ Passed The PR directly addresses issue #2933 by fixing DevTools close routing, host reconciliation, and lifecycle management to prevent reopening after manual close and improve inspector behavior.
Out of Scope Changes check ✅ Passed All changes are scoped to DevTools lifecycle management and host reconciliation in BrowserPanel, BrowserPanelView, AppDelegate, and related tests—directly addressing the linked issue objectives.
Cmux Swift Actor Isolation ✅ Passed Changes confined to @MainActor classes/SwiftUI View types. New enum is private within @MainActor. No implicit models, unsafe Sendable, or background-thread UI access violations found.
Cmux No Hacky Sleeps ✅ Passed PR modifies only Swift code. Check scope is TypeScript/JavaScript/shell/build-scripts only; Swift delays covered by "swift-blocking-runtime" check.
Cmux Algorithmic Complexity ✅ Passed No algorithmic complexity violations: NSApp.windows is small/fixed, no nested rescans, panel inspection on user-action only with O(1) checks, no repeated sorting/filtering in hot paths.
Cmux Swift Concurrency ✅ Passed PR introduces DispatchQueue.main.asyncAfter for DevTools lifecycle (stored+cancelled) and temporary Combine subscriptions observing existing @Published properties, not new app state.
Cmux Swift @Concurrent ✅ Passed All async functions follow concurrency rules: nonisolated detaches properly; network I/O in actor; MainActor async correctly isolated. No @concurrent annotation issues detected.
Cmux Swift File And Package Boundaries ✅ Passed PR is a focused DevTools lifecycle bug fix with +162 net lines to BrowserPanel (below 250-line threshold), no new mixed responsibilities, and refactored state management with clear extraction path.
Cmux User-Facing Error Privacy ✅ Passed PR uses proper String(localized:) for all user-facing messages; no unlocalized error messages, vendor names, provider details, or sensitive info exposed in user text.
Cmux Full Internationalization ✅ Passed PR adds only debug-only code changes and internal refactoring with no new user-facing strings. All existing localized strings remain properly localized. No Info.plist or string catalog changes.
Cmux Swiftui State Layout ✅ Passed No new @Observable/@Published/@StateObject patterns; GeometryReader for measurement only; ForEach uses value snapshots; state mutations in lifecycle handlers only.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed No new user-visible windows created; existing cmux.browserBackgroundPreload window is in IGNORED_IDENTIFIERS; changes route detached DevTools close actions through existing panels only.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-2933-browser-devtools

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 1bdb357. Configure here.

Comment thread Sources/Panels/BrowserPanel.swift
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 29, 2026

Greptile Summary

This PR fixes two related DevTools bugs: detached inspector-window close actions are now routed through the owning panel's inspector frontend (not a "Web Inspector" title prefix), and host updates no longer unconditionally restore DevTools — a new DeveloperToolsLifecyclePhase state machine drives reconcileDeveloperToolsAfterHostUpdate so that stable hides are treated as manual closes while pending opens survive host churn.

  • AppDelegate / close routing: removes the isDetachedInspectorWindow title guard so any close action on a window that the panel owns is handled correctly, regardless of window title.
  • BrowserPanel lifecycle: introduces DeveloperToolsLifecyclePhase (hidden / opening / visible / closing / restoring) and replaces scattered developerToolsDetachedOpenGraceDeadline / developerToolsLastKnownVisibleAt assignments with semantic lifecycle marks; adds reconcileDeveloperToolsAfterHostUpdate called from both the local-inline and portal hosting paths.
  • Tests: three new regression tests cover title-less detached close routing, pending attach surviving hidden-inspector sync during host churn, and stable local-inline host updates respecting a manual close.

Confidence Score: 4/5

Safe to merge; the lifecycle state machine is internally consistent and the detached-close routing fix is well-scoped.

The reconcile logic correctly gates manual-close recording behind allowsHostHiddenManualClose (phase .visible only) and !isDeveloperToolsTransitionInFlight, so in-flight transitions cannot be silently killed. The one concern is that markDeveloperToolsLifecycleVisible() has no guard on preferredDeveloperToolsVisible, leaving a narrow window where phase.preservesVisibleIntent = true while preferred = false could spuriously enable the local-inline host path — the state is representable even if no current call site produces it.

Sources/Panels/BrowserPanel.swift — effectiveDeveloperToolsVisibilityIntent() and shouldUseLocalInlineDeveloperToolsHosting() dual-intent; cmuxTests/BrowserConfigTests.swift — 50 ms RunLoop budgets in testLocalInlineHostUpdateRespectsManualDeveloperToolsClose.

Important Files Changed

Filename Overview
Sources/AppDelegate.swift Removes BrowserPanel.isDetachedInspectorWindow title-prefix guard from the window close routing path; panels now decide ownership themselves.
Sources/Panels/BrowserPanel.swift Adds DeveloperToolsLifecyclePhase enum and reconcileDeveloperToolsAfterHostUpdate; replaces ad-hoc date/deadline vars with semantic lifecycle marks. Core logic looks sound, but visible-intent is now split across two orthogonal boolean sources.
Sources/Panels/BrowserPanelView.swift Replaces unconditional restoreDeveloperToolsAfterAttachIfNeeded calls with reconcileDeveloperToolsAfterHostUpdate in both local-inline and portal paths; captures pre-update visibility snapshot correctly at the top of each update function.
cmuxTests/BrowserConfigTests.swift Adds three new regression tests. testLocalInlineHostUpdateRespectsManualDeveloperToolsClose uses 50ms RunLoop waits that are tighter than the established 500ms waitForDeveloperToolsTransitions() helper, raising a flakiness concern under CI stress.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[SwiftUI updateNSView] --> B{Local inline or portal?}
    B -- local inline --> C[capture wasDeveloperToolsVisibleBeforeHostUpdate]
    B -- portal --> D[capture wasDeveloperToolsVisibleBeforeHostUpdate]
    C --> E[reconcileDeveloperToolsAfterHostUpdate]
    D --> F{portalHostAccepted & window != nil?}
    F -- yes --> E
    F -- no --> G[skip reconcile]
    E --> H{webView attached to window?}
    H -- no --> I[syncDeveloperToolsPreferenceFromInspector preserveIntent=true]
    H -- yes --> J[noteDeveloperToolsHostAttached]
    J --> K[compute hasPendingRestore & shouldRestore]
    K --> L{wasVisible & stable host & phase==.visible & no transition & !hasPendingRestore?}
    L -- yes --> M[recordManualClose preferred=false phase=.hidden]
    L -- no --> N{shouldRestore?}
    N -- yes --> O[restoreDeveloperToolsAfterAttachIfNeeded]
    N -- no --> P[syncDeveloperToolsPreferenceFromInspector]
    O --> Q{inspector visible?}
    Q -- yes --> R[markLifecycleVisible]
    Q -- no --> S[markLifecyclePendingVisible scheduleDeveloperToolsRestoreRetry]
    style M fill:#f99,stroke:#c00
    style R fill:#9f9,stroke:#090
    style I fill:#ff9,stroke:#990
Loading

Reviews (2): Last reviewed commit: "fix: scope detached devtools windows to ..." | Re-trigger Greptile

Comment on lines +6638 to +6642
if wasVisibleBeforeHostUpdate,
!isDeveloperToolsVisible(),
!didAttachHost,
!didChangeHostVisibility,
!hasPendingRestore {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Manual-close heuristic fires during in-flight opening transitions

hasPendingRestore covers forceDeveloperToolsRefreshOnNextAttach and developerToolsRestoreRetryWorkItem, but not isDeveloperToolsTransitionInFlight (developerToolsTransitionSettleWorkItem != nil). If a stable SwiftUI host re-render lands in the narrow window between enqueueDeveloperToolsVisibilityTransition(to: true) being called and the inspector confirming isVisible == true, the combination of wasVisibleBeforeHostUpdate = true and !isDeveloperToolsVisible() will trigger recordDeveloperToolsManualCloseDuringStableHostUpdate(), cancelling the settle work item and setting preferredDeveloperToolsVisible = false — effectively killing an in-flight reveal that the user initiated.

Suggested change
if wasVisibleBeforeHostUpdate,
!isDeveloperToolsVisible(),
!didAttachHost,
!didChangeHostVisibility,
!hasPendingRestore {
if wasVisibleBeforeHostUpdate,
!isDeveloperToolsVisible(),
!didAttachHost,
!didChangeHostVisibility,
!hasPendingRestore,
!isDeveloperToolsTransitionInFlight {

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/Panels/BrowserPanel.swift`:
- Around line 6160-6173: The per-panel detached DevTools enumeration is too
broad because isDetachedDeveloperToolsWindow(_:) falls back to
Self.isDetachedInspectorWindow(_:), causing detachedDeveloperToolsWindows() to
pick up other panels' inspectors; change the logic so the panel only considers
windows that satisfy detachedDeveloperToolsWindowBelongsToPanel(_:). Concretely,
remove or stop using the Self.isDetachedInspectorWindow(window) fallback in
isDetachedDeveloperToolsWindow(_:) (or, alternatively, change
detachedDeveloperToolsWindows() to filter solely with
detachedDeveloperToolsWindowBelongsToPanel(_:)) so only this panel's detached
developer tools are returned.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9db83708-3283-44de-ba0e-fb5887bac2ea

📥 Commits

Reviewing files that changed from the base of the PR and between d7b5b14 and 1bdb357.

📒 Files selected for processing (4)
  • Sources/AppDelegate.swift
  • Sources/Panels/BrowserPanel.swift
  • Sources/Panels/BrowserPanelView.swift
  • cmuxTests/BrowserConfigTests.swift

Comment thread Sources/Panels/BrowserPanel.swift
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/Panels/BrowserPanel.swift`:
- Around line 6454-6472: The current logic finalizes a queued hide immediately
when isDeveloperToolsVisible() is false even if an earlier show() is still
in-flight; update the guard branches (the block using pendingTargetVisible,
isDeveloperToolsVisible(), preferredDeveloperToolsVisible and the calls to
markDeveloperToolsLifecycleHidden/Visible/PendingVisible and
performDeveloperToolsVisibilityTransition) to first check for any in-flight
show/open operation (e.g. an "inFlightShow"/"isOpeningDeveloperTools"/lifecycle
state indicating pending visible) and if such an in-flight show exists, do not
mark lifecycle hidden nor finalize the queued hide—either preserve or re-queue
the pending hide (keep lifecycle pending) so the earlier show completion can
take effect; otherwise proceed with the existing calls to
markDeveloperToolsLifecycleHidden/Visible or
performDeveloperToolsVisibilityTransition as before.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: d9daa2f3-2671-4d2e-a409-ead279125323

📥 Commits

Reviewing files that changed from the base of the PR and between 1bdb357 and c222554.

📒 Files selected for processing (2)
  • Sources/Panels/BrowserPanel.swift
  • cmuxTests/BrowserConfigTests.swift

Comment thread Sources/Panels/BrowserPanel.swift
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
Sources/Panels/BrowserPanel.swift (1)

6679-6710: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Queued hide can still lose to an earlier async open.

Line 6708 avoids classifying one host-update path as a manual close, but the earlier race is still reachable: if hide is queued during an in-flight show() and finishDeveloperToolsTransition() runs while the inspector is still hidden, the code finalizes .hidden without ever applying the queued conceal. A late completion of the earlier show() can still reopen DevTools after the user just closed them. Keep the hide pending until the open settles, or explicitly conceal once the inspector eventually surfaces.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: d0118b1f-9494-4be9-80b0-9bc01f5efa3f

📥 Commits

Reviewing files that changed from the base of the PR and between c222554 and f23fc90.

📒 Files selected for processing (2)
  • Sources/Panels/BrowserPanel.swift
  • cmuxTests/BrowserConfigTests.swift

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Browser DevTools: multiple rendering and interaction bugs

1 participant