Skip to content

devtools resize layout - #1189

Merged
austinywang merged 3 commits into
mainfrom
issue-1183-devtools-resize-layout
Mar 11, 2026
Merged

austinywang merged 3 commits into
mainfrom
issue-1183-devtools-resize-layout

Conversation

@austinywang

@austinywang austinywang commented Mar 11, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • What changed?
  • Why?

Testing

  • How did you test this change?
  • What did you verify manually?

Demo Video

For UI or behavior changes, include a short demo video (GitHub upload, Loom, or other direct link).

  • Video URL or attachment:

Review Trigger (Copy/Paste as PR comment)

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

Checklist

  • I tested the change locally
  • I added or updated tests for behavior changes
  • I updated docs/changelog if needed
  • I requested bot reviews after my latest commit (copy/paste block above or equivalent)
  • All code review bot comments are resolved
  • All human review comments are resolved

Summary by cubic

Stabilizes side-docked DevTools layout and resizing across inline and portal hosting. Persists attached inspector width proportionally, syncs dock side, routes divider interactions to WebKit when possible, and reduces layout churn. Addresses Linear #1183.

  • New Features

    • Track attached vs. detached state; auto-dismiss detached windows when attached is preferred, with a short grace period and a close observer.
    • Persist preferred attached width and its container fraction; inline host reports drag changes back to the panel and restores on host swaps/resizes.
    • Inline host adds a side-dock overlay to manage page/inspector siblings; syncs dock side by querying WI.dockConfiguration and re-parents when bottom-docked.
    • Route divider hit-tests to native WebKit when available; fall back to local handling inside the overlay; minimum inspector width is 1 px.
  • Bug Fixes

    • Correct divider math and anchoring for both sides; clamp to container width and avoid resetting frames when WK companion views exist.
    • Skip re-applies during live drags and when geometry is unchanged; gate by last size and prefer display invalidation to cut thrash.
    • Avoid duplicate portal refresh when sync already adjusted inspector frames; ignore hidden WKInspector views when moving related subviews.

Written for commit 06c5cac. Summary will update on new commits.

Summary by CodeRabbit

  • New Features
    • Remember and restore preferred inspector/devtools widths and docking state for attached side-docks.
  • Bug Fixes
    • More reliable attach/detach handling for developer tools windows and inspector divider drags.
    • Better guard logic to avoid redundant reflows during live drag or sync-adjusted sizing.
  • Performance
    • Reduced unnecessary layout/refresh passes and improved throttling during inspector resize interactions.

@vercel

vercel Bot commented Mar 11, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Mar 11, 2026 7:27pm

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@coderabbitai

coderabbitai Bot commented Mar 11, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Introduces hosted-inspector width tracking, drag-state guards, and size-approximate throttling across portal/host/slot/panel/view layers; adds DevTools presentation state (attached/detached) with grace-period handling, preferred-width persistence APIs, debounced divider reapply, and expanded debug instrumentation.

Changes

Cohort / File(s) Summary
Inspector Portal & Slot/Host Logic
Sources/BrowserWindowPortal.swift, Sources/.../WindowBrowserHostView.swift, Sources/.../WindowBrowserSlotView.swift
Added lastHostedInspectorLayoutBoundsSize, sizeApproximatelyEqual helpers, isHostedInspectorDividerDragActive flag, drag lifecycle updates (mouseDown/Dragged/Up), and reapply/apply divider changes (reapply returns Bool, preferred-width resolution) to avoid redundant reflows during drag/sync.
DevTools Presentation & Panel APIs
Sources/Panels/BrowserPanel.swift
Introduced DeveloperToolsPresentation enum, grace-period/window-close observer handling for detached DevTools, presentation sync helpers, and public APIs recordPreferredAttachedDeveloperToolsWidth and preferredAttachedDeveloperToolsWidthState.
Panel View — Hosted Inspector Docking & Width Persistence
Sources/Panels/BrowserPanelView.swift
Added per-inspector width and width-fraction tracking, width record/resolve helpers, hosted-inspector side-dock container management, debounced scheduling for divider reapply, hit-test/drag routing updates, and callbacks to propagate preferred-width changes to BrowserPanel.
Cross-cutting Debugging & Guards
multiple files above
Added debug logs, early-return guards to avoid double refreshes when drag is active or when sync already applied clamped width, and minor behavioral guards to stabilize invalid drag states.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant Slot as WindowBrowserSlotView
    participant Host as WindowBrowserHostView
    participant Portal as WindowBrowserPortal

    User->>Slot: mouseDown (divider)
    Slot->>Slot: isHostedInspectorDividerDragActive = true

    User->>Slot: mouseDragged
    Slot->>Host: validate drag state
    alt invalid
        Host->>Slot: clear isHostedInspectorDividerDragActive
        Slot->>Slot: return early
    else valid
        Slot->>Portal: request inspector width apply
        Portal->>Portal: sizeApproximatelyEqual(newSize, lastHostedInspectorLayoutBoundsSize)
        alt changed
            Portal->>Host: reapplyHostedInspectorDividerIfNeeded -> true
            Host->>Portal: update frames, notify
        else unchanged
            Portal->>Portal: skip re-layout
        end
    end

    User->>Slot: mouseUp
    Slot->>Slot: isHostedInspectorDividerDragActive = false
Loading
sequenceDiagram
    participant UI as DevTools UI
    participant Panel as BrowserPanel
    participant View as BrowserPanelView
    participant Observer as Window Close Observer

    UI->>Panel: revealDeveloperTools(inspector)
    Panel->>Panel: syncDeveloperToolsPresentationPreferenceFromUI()
    alt presentation -> detached
        Panel->>Observer: installDetachedDeveloperToolsWindowCloseObserver()
        Observer->>Panel: window closed -> schedule dismissal
    else presentation -> attached
        Panel->>View: prepareDeveloperToolsForRevealIfNeeded()
        View->>View: recordPreferredAttachedDeveloperToolsWidth(...)
    end

    UI->>Panel: toggleDeveloperTools()
    Panel->>Panel: setPreferredDeveloperToolsPresentation(...)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

Poem

🐰 I nibbled at pixels, nudged bounds just right,

Drag flags set true through the long debug night,
Widths remembered, reflows kept few,
Docked tools learned the fraction and grew,
Now frames skip dance when nothing's new.

🚥 Pre-merge checks | ❌ 3

❌ Failed checks (2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is an unfilled template with placeholder sections and empty content under Summary and Testing; auto-generated content is present but author provided no substantive information. Fill in the Summary (what changed and why), Testing (how tested and verified manually), and Demo Video sections; check the checklist items indicating completion of required tasks.
Docstring Coverage ⚠️ Warning Docstring coverage is 3.90% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'devtools resize layout' is vague and generic, failing to clearly convey the main change or purpose. Use a more descriptive title like 'Stabilize side-docked DevTools layout and resizing' or 'Fix DevTools resize throttling and drag state handling' to clarify the main objective.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch issue-1183-devtools-resize-layout

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (6)
Sources/Panels/BrowserPanel.swift (1)

4132-4144: Consider extracting shared inspector view helpers.

The visibleDescendants(in:) and isInspectorView(_:) methods are duplicated across BrowserPanel.swift, BrowserWindowPortal.swift, and BrowserPanelView.swift. While encapsulation justifies some duplication, consolidating these into a shared internal utility (e.g., InspectorViewHelpers) would reduce maintenance overhead if the detection logic needs to evolve.

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

In `@Sources/Panels/BrowserPanel.swift` around lines 4132 - 4144, The duplicated
logic in visibleDescendants(in:) and isInspectorView(_:) should be consolidated
into a single internal helper type (e.g., InspectorViewHelpers) with static
methods visibleDescendants(in:) and isInspectorView(_:), move the existing
implementations there, make it internal, and update all call sites in
BrowserPanel, BrowserWindowPortal, and BrowserPanelView to call
InspectorViewHelpers.visibleDescendants(in:) and
InspectorViewHelpers.isInspectorView(_:) so future changes live in one place.
Sources/Panels/BrowserPanelView.swift (2)

4007-4013: Simplified mouse event handlers delegate to super.

The custom hosted inspector divider drag handling has been removed. Both mouseDown and mouseDragged now simply call super, delegating event handling to WebKit's native inspector implementation. This aligns with the commit message about routing inspector hits to native WebKit.

Note that hostedInspectorDividerDrag state and related drag computation code (e.g., HostedInspectorDividerDragState) appear to be unused after this change. Consider removing the dead code if native WebKit drag handling is the intended long-term approach.

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

In `@Sources/Panels/BrowserPanelView.swift` around lines 4007 - 4013,
mouseDown(with:) and mouseDragged(with:) were simplified to call super, but
leftover state and helper code for the custom inspector divider drag (e.g., the
hostedInspectorDividerDrag property, HostedInspectorDividerDragState type, and
any functions that compute drag/resize for the hosted inspector) are now unused
and should be removed; locate and delete the HostedInspectorDividerDragState
declaration, the hostedInspectorDividerDrag property/variable, and any methods
referenced only by the old drag handling (search for symbols like
hostedInspectorDividerDrag, HostedInspectorDividerDragState,
computeHostedInspectorDrag, or similar) and remove their usages and tests, or
mark them as deprecated if you need to preserve behavior temporarily.

4187-4196: Debounced divider reapply scheduling.

The scheduleHostedInspectorDividerReapply method properly cancels pending work before scheduling new work, providing effective debouncing. The method name says "Reapply" but calls captureHostedInspectorPreferredWidthFromCurrentLayout - this is intentional as it captures the current WebKit-managed layout state rather than applying a stored preference.

Consider renaming to scheduleHostedInspectorWidthCapture for clarity, though this is a minor nit.

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

In `@Sources/Panels/BrowserPanelView.swift` around lines 4187 - 4196, Rename the
misleading method scheduleHostedInspectorDividerReapply to
scheduleHostedInspectorWidthCapture to match its actual behavior (it captures
WebKit-managed layout via captureHostedInspectorPreferredWidthFromCurrentLayout
rather than reapplying a stored divider), and update all references/usages
(calls, delegates, unit tests, and any documentation/comments) to the new name
while preserving the current implementation and semantics (including
hostedInspectorReapplyWorkItem handling and DispatchWorkItem logic); also update
any related comment text to reflect “width capture” wording.
Sources/BrowserWindowPortal.swift (3)

170-171: Consider removing the unused minimumInspectorWidth parameter.

The parameter is explicitly marked as unused (_: CGFloat) and always passed as 0 at the call site (line 966). If minimum width enforcement is no longer needed, consider removing this parameter to clean up the API surface.

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

In `@Sources/BrowserWindowPortal.swift` around lines 170 - 171, The
initializer/method currently takes an unused parameter named
minimumInspectorWidth (_: CGFloat); remove this parameter from the signature
(and any internal placeholder) and delete its argument at all call sites (e.g.,
the call that passes 0), then update any related documentation/comments and
tests that referenced minimumInspectorWidth to reflect the simplified API (look
for the parameter name minimumInspectorWidth and the function/initializer that
includes inspectorFrame: NSRect to locate the code).

987-987: Minor: String-based detection for live drag state.

The isLiveDrag flag is determined by comparing reason == "drag". This is somewhat fragile as it depends on the exact string passed from callers. Consider using a dedicated boolean parameter for more explicit control.

♻️ Alternative approach
 private func applyHostedInspectorDividerWidth(
     _ preferredWidth: CGFloat,
     to hit: HostedInspectorDividerHit,
-    reason: String
+    reason: String,
+    isLiveDrag: Bool = false
 ) -> (pageFrame: NSRect, inspectorFrame: NSRect) {
     ...
-    let isLiveDrag = reason == "drag"
     ...
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/BrowserWindowPortal.swift` at line 987, Replace the fragile string
check for live-drag with an explicit boolean parameter: stop deriving isLiveDrag
via reason == "drag" and instead add a Bool parameter (e.g., isLiveDrag or
isDragging) to the BrowserWindowPortal method that currently accepts reason and
defines isLiveDrag, update the method body to use that boolean, and update all
callers to pass the new flag (or provide an overload/optional default to
preserve compatibility). Ensure you remove the string comparison and any callers
that relied on "drag" as a sentinel and update tests/usages accordingly.

1081-1084: Consider extracting duplicate sizeApproximatelyEqual helpers to a shared utility.

The sizeApproximatelyEqual helper is duplicated identically in both WindowBrowserHostView (lines 1081-1084) and WindowBrowserSlotView (lines 1609-1612). Similarly, rectApproximatelyEqual is duplicated across multiple classes in this file and in BrowserPanel.swift.

Consider extracting these to a shared extension or utility function to reduce duplication.

♻️ Suggested extraction
// Could be added as a private extension at file scope or in a shared utilities file
private extension NSSize {
    func approximatelyEquals(_ other: NSSize, epsilon: CGFloat = 0.5) -> Bool {
        abs(width - other.width) <= epsilon &&
            abs(height - other.height) <= epsilon
    }
}

private extension NSRect {
    func approximatelyEquals(_ other: NSRect, epsilon: CGFloat = 0.5) -> Bool {
        abs(origin.x - other.origin.x) <= epsilon &&
            abs(origin.y - other.origin.y) <= epsilon &&
            abs(size.width - other.size.width) <= epsilon &&
            abs(size.height - other.size.height) <= epsilon
    }
}

Also applies to: 1609-1612

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

In `@Sources/BrowserWindowPortal.swift` around lines 1081 - 1084, The duplicate
helpers sizeApproximatelyEqual and rectApproximatelyEqual (used in
WindowBrowserHostView, WindowBrowserSlotView and BrowserPanel) should be
extracted into a single shared utility — e.g. create private extensions on
NSSize and NSRect (or a small Utilities file) that add approximatelyEquals(_:,
epsilon:) methods and replace all occurrences of sizeApproximatelyEqual and
rectApproximatelyEqual to call these extensions; update callers in
WindowBrowserHostView and WindowBrowserSlotView (and BrowserPanel) to use the
new methods to remove duplication and keep the same epsilon semantics.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@Sources/BrowserWindowPortal.swift`:
- Around line 170-171: The initializer/method currently takes an unused
parameter named minimumInspectorWidth (_: CGFloat); remove this parameter from
the signature (and any internal placeholder) and delete its argument at all call
sites (e.g., the call that passes 0), then update any related
documentation/comments and tests that referenced minimumInspectorWidth to
reflect the simplified API (look for the parameter name minimumInspectorWidth
and the function/initializer that includes inspectorFrame: NSRect to locate the
code).
- Line 987: Replace the fragile string check for live-drag with an explicit
boolean parameter: stop deriving isLiveDrag via reason == "drag" and instead add
a Bool parameter (e.g., isLiveDrag or isDragging) to the BrowserWindowPortal
method that currently accepts reason and defines isLiveDrag, update the method
body to use that boolean, and update all callers to pass the new flag (or
provide an overload/optional default to preserve compatibility). Ensure you
remove the string comparison and any callers that relied on "drag" as a sentinel
and update tests/usages accordingly.
- Around line 1081-1084: The duplicate helpers sizeApproximatelyEqual and
rectApproximatelyEqual (used in WindowBrowserHostView, WindowBrowserSlotView and
BrowserPanel) should be extracted into a single shared utility — e.g. create
private extensions on NSSize and NSRect (or a small Utilities file) that add
approximatelyEquals(_:, epsilon:) methods and replace all occurrences of
sizeApproximatelyEqual and rectApproximatelyEqual to call these extensions;
update callers in WindowBrowserHostView and WindowBrowserSlotView (and
BrowserPanel) to use the new methods to remove duplication and keep the same
epsilon semantics.

In `@Sources/Panels/BrowserPanel.swift`:
- Around line 4132-4144: The duplicated logic in visibleDescendants(in:) and
isInspectorView(_:) should be consolidated into a single internal helper type
(e.g., InspectorViewHelpers) with static methods visibleDescendants(in:) and
isInspectorView(_:), move the existing implementations there, make it internal,
and update all call sites in BrowserPanel, BrowserWindowPortal, and
BrowserPanelView to call InspectorViewHelpers.visibleDescendants(in:) and
InspectorViewHelpers.isInspectorView(_:) so future changes live in one place.

In `@Sources/Panels/BrowserPanelView.swift`:
- Around line 4007-4013: mouseDown(with:) and mouseDragged(with:) were
simplified to call super, but leftover state and helper code for the custom
inspector divider drag (e.g., the hostedInspectorDividerDrag property,
HostedInspectorDividerDragState type, and any functions that compute drag/resize
for the hosted inspector) are now unused and should be removed; locate and
delete the HostedInspectorDividerDragState declaration, the
hostedInspectorDividerDrag property/variable, and any methods referenced only by
the old drag handling (search for symbols like hostedInspectorDividerDrag,
HostedInspectorDividerDragState, computeHostedInspectorDrag, or similar) and
remove their usages and tests, or mark them as deprecated if you need to
preserve behavior temporarily.
- Around line 4187-4196: Rename the misleading method
scheduleHostedInspectorDividerReapply to scheduleHostedInspectorWidthCapture to
match its actual behavior (it captures WebKit-managed layout via
captureHostedInspectorPreferredWidthFromCurrentLayout rather than reapplying a
stored divider), and update all references/usages (calls, delegates, unit tests,
and any documentation/comments) to the new name while preserving the current
implementation and semantics (including hostedInspectorReapplyWorkItem handling
and DispatchWorkItem logic); also update any related comment text to reflect
“width capture” wording.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7eba51bb-f500-4180-a798-4959e8a190a7

📥 Commits

Reviewing files that changed from the base of the PR and between 00587ed and 8b66628.

📒 Files selected for processing (3)
  • Sources/BrowserWindowPortal.swift
  • Sources/Panels/BrowserPanel.swift
  • Sources/Panels/BrowserPanelView.swift

@greptile-apps

greptile-apps Bot commented Mar 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR refactors the devtools (WebKit inspector) resize layout across three files, addressing proportional resizing on window resize, drag-handling consolidation, and attached vs. detached presentation tracking.

Key behavioral changes:

  • resizedFrames now derives page/inspector frames from containerBounds rather than the current pageFrame, fixing cases where the inspector could not take the full container width
  • Inspector preferred width is now stored as both an absolute value and a fraction of container width, so the split proportionally rescales when the window is resized
  • Layout reapplication is debounced in both WindowBrowserHostView and WindowBrowserSlotView via lastHostedInspectorLayoutBoundsSize, preventing redundant passes when bounds haven't changed
  • Drag handling for the inline-hosted inspector is centralized in WindowBrowserHostView (removed from HostContainerView), and isHostedInspectorDividerDragActive guards prevent portal syncs from clobbering an in-progress drag
  • A new DeveloperToolsPresentation enum (.unknown, .attached, .detached) tracks user intent, allowing shouldUseLocalInlineDeveloperToolsHosting to correctly return false when devtools are detached to a separate window
  • A grace period (developerToolsDetachedOpenGracePeriod = 0.35s) prevents premature state changes while a detached devtools window is still opening
  • Hit-test fallback for the inspector divider now correctly returns hostedInspectorHit.inspectorView instead of self, enabling proper native event delivery to the inspector panel
  • Hidden/zero-size WKInspector subviews are now filtered out during the WebKit-related-subview transfer, preventing ghost inspector views from interfering with layout detection

Issues found:

  • isLiveDrag is declared outside #if DEBUG but only consumed inside it in both BrowserWindowPortal.swift and BrowserPanelView.swift, causing compiler warnings in non-debug builds
  • The minimumInspectorWidth _: CGFloat parameter in resizedFrames is silently discarded via _; all callers pass 0 so there is no functional impact, but the API is misleading
  • preferredAttachedDeveloperToolsWidthState() (BrowserPanel.swift) and setPreferredHostedInspectorWidth(width:widthFraction:) (BrowserPanelView.swift) are introduced but have no call-sites in the current codebase — likely scaffolding for a follow-up PR, worth a // TODO: comment

Confidence Score: 4/5

  • This PR is safe to merge; changes are self-contained AppKit layout logic with no data persistence or security implications.
  • The logic changes are well-scoped to inspector resize/layout code, the behavioral fixes (container-based frame math, proportional fraction tracking, drag-active guard) are correct, and the overall refactoring reduces duplication. Minor issues — unused isLiveDrag variable causing release-build warnings, a silently-ignored function parameter, and two dead-code functions — lower the score slightly but do not block shipping.
  • Sources/Panels/BrowserPanel.swift — the orphaned preferredAttachedDeveloperToolsWidthState() / setPreferredHostedInspectorWidth pair should be confirmed as intentional scaffolding or wired up before this path is relied upon for width restoration.

Important Files Changed

Filename Overview
Sources/BrowserWindowPortal.swift Major refactor of the hosted inspector resize logic: resizedFrames now derives frames from containerBounds instead of pageFrame, inspector width preference is stored as both an absolute value and a fraction for proportional resize on window resize, drag-active guard added to prevent layout conflicts, and layout reapplication is debounced via lastHostedInspectorLayoutBoundsSize. Minor issue: isLiveDrag is declared outside #if DEBUG but only consumed inside it, causing a compiler warning in non-debug builds.
Sources/Panels/BrowserPanel.swift Adds DeveloperToolsPresentation enum (.unknown, .attached, .detached) and associated tracking to correctly distinguish between attached and detached devtools states. Replaces the previous attachDeveloperToolsIfSupported/isDeveloperToolsAttached pattern. Adds a grace period for detached-window open settling, a window-close observer for auto-clearing detached intent, and a new recordPreferredAttachedDeveloperToolsWidth + preferredAttachedDeveloperToolsWidthState API. The latter function is currently never called from anywhere in the codebase — potentially dead code or incomplete infrastructure.
Sources/Panels/BrowserPanelView.swift Local inline devtools hosting (HostContainerView) refactored: drag handling removed and delegated to WindowBrowserHostView, inspector-width preference now tracks fractions, layout reapplication debounced, captureHostedInspectorPreferredWidthFromCurrentLayout replaces reapplyHostedInspectorDividerIfNeeded (reads current state rather than imposing preferred width). Hit test fallback now correctly returns the inspector view instead of self. Two newly added functions — setPreferredHostedInspectorWidth and the onPreferredHostedInspectorWidthChanged pathway — appear to have no call-site consumers in the current codebase.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[User drags inspector divider] --> B[WindowBrowserHostView\nmouseDown / mouseDragged]
    B --> C[Set isHostedInspectorDividerDragActive = true\non WindowBrowserSlotView]
    C --> D[clampedDividerX\nrespects minimumInspectorWidth]
    D --> E[recordPreferredHostedInspectorWidth\nstores absolute + fraction]
    E --> F[applyHostedInspectorDividerWidth\nresizedFrames from containerBounds]
    F --> G[Set needsDisplay on\npage + inspector + container + slot]
    G --> H[mouseUp → isHostedInspectorDividerDragActive = false]

    H --> I{Portal sync\ntriggered?}
    I -- drag still active --> J[refreshHostedWebViewPresentation\nSKIPPED]
    I -- drag complete --> K[reapplyHostedInspectorDividerIfNeeded\nuses resolvedPreferredHostedInspectorWidth\nfraction × containerBounds.width]

    L[Window resize / layout] --> M{bounds size changed?}
    M -- No change ε=0.5 --> N[SKIP reapply]
    M -- Changed --> O[captureHostedInspectorPreferredWidthFromCurrentLayout\nreads current inspector frame,\nupdates stored fraction]

    P[DevTools toggle] --> Q{Current presentation?}
    Q -- unknown --> R[prepareDeveloperToolsForRevealIfNeeded\nattach inspector]
    Q -- attached --> S[scheduleDetachedWindowDismissal\nif shouldDismiss]
    Q -- detached --> T[skip dismissal,\nset grace deadline]
    R --> U[syncDeveloperToolsPresentationPreferenceFromUI\nupdate .attached / .detached]
    S --> U
    T --> U
Loading

Last reviewed commit: 8b66628

hit.inspectorView.needsLayout = true
hit.containerView.needsLayout = true
hit.slotView.needsLayout = true
let isLiveDrag = reason == "drag"

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.

isLiveDrag unused in non-debug builds

isLiveDrag is declared outside the #if DEBUG block but is only referenced inside it. In non-debug (release) builds the compiler will emit an "immutable value 'isLiveDrag' was never used" warning. The same pattern exists in BrowserPanelView.swift at line 4280.

Move the declaration inside the conditional compilation block to eliminate the warning:

Suggested change
let isLiveDrag = reason == "drag"
#if DEBUG
let isLiveDrag = reason == "drag"
dlog(

Then adjust the closing #endif accordingly to remove the separate dlog block.

Comment on lines +171 to 172
minimumInspectorWidth _: CGFloat
) -> (pageFrame: NSRect, inspectorFrame: NSRect) {

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.

minimumInspectorWidth parameter is silently discarded

The parameter minimumInspectorWidth _: CGFloat is accepted by the function signature but the wildcard _ makes it immediately inaccessible — all logic has been rewritten to ignore it entirely. Both current call-sites pass 0, so there is no functional impact, but this leaves the API misleading: callers see a minimumInspectorWidth argument they can tune, while the implementation ignores it.

Consider one of:

  • Removing the parameter from the signature entirely (and updating the two call-sites that pass 0)
  • Or re-introducing the minimum-width clamping logic so the parameter has the expected effect
Suggested change
minimumInspectorWidth _: CGFloat
) -> (pageFrame: NSRect, inspectorFrame: NSRect) {
minimumInspectorWidth _: CGFloat = 0

(Providing a default at least makes it clearer the value is unused, though removing it outright is cleaner.)

Comment on lines +3243 to 3245
func preferredAttachedDeveloperToolsWidthState() -> (width: CGFloat?, widthFraction: CGFloat?) {
(preferredAttachedDeveloperToolsWidth, preferredAttachedDeveloperToolsWidthFraction)
}

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.

preferredAttachedDeveloperToolsWidthState() is never called

This function is introduced in this PR but has no call-site anywhere in the codebase — a search of the entire Sources/ tree returns only this definition. The companion method setPreferredHostedInspectorWidth(width:widthFraction:) in HostContainerView (BrowserPanelView.swift:3663) is similarly orphaned.

If this is scaffolding for a follow-up PR, adding a // TODO: comment would make the intent explicit. If it was meant to be wired up in this PR (e.g., to restore the inspector width when re-entering inline hosting mode), the restoration path appears to be missing.

hit.inspectorView.needsLayout = true
hit.containerView.needsLayout = true
needsLayout = true
let isLiveDrag = reason == "drag"

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.

isLiveDrag unused in non-debug builds

Same issue as BrowserWindowPortal.swift line 987: isLiveDrag is declared in the main function body but is only referenced inside the #if DEBUG block below. In release builds the compiler will warn about the unused binding.

Suggested change
let isLiveDrag = reason == "drag"
#if DEBUG
let isLiveDrag = reason == "drag"

(Shift the declaration into the debug block, removing the standalone #if DEBUG line that opens it.)

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No issues found across 3 files

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Sources/Panels/BrowserPanelView.swift">

<violation number="1" location="Sources/Panels/BrowserPanelView.swift:4039">
P2: Guard the async dock-configuration callback against stale frontend responses; otherwise an in-flight `evaluateJavaScript` result can reapply outdated dock state after the host switches modes.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

hostedInspectorFrontendWebView.evaluateJavaScript(
"typeof WI === 'undefined' ? null : WI.dockConfiguration"
) { [weak self] result, _ in
self?.applyHostedInspectorDockConfiguration(result as? String, reason: reason)

@cubic-dev-ai cubic-dev-ai Bot Mar 11, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Guard the async dock-configuration callback against stale frontend responses; otherwise an in-flight evaluateJavaScript result can reapply outdated dock state after the host switches modes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/Panels/BrowserPanelView.swift, line 4039:

<comment>Guard the async dock-configuration callback against stale frontend responses; otherwise an in-flight `evaluateJavaScript` result can reapply outdated dock state after the host switches modes.</comment>

<file context>
@@ -3877,12 +3927,159 @@ struct WebViewRepresentable: NSViewRepresentable {
+            hostedInspectorFrontendWebView.evaluateJavaScript(
+                "typeof WI === 'undefined' ? null : WI.dockConfiguration"
+            ) { [weak self] result, _ in
+                self?.applyHostedInspectorDockConfiguration(result as? String, reason: reason)
+            }
+        }
</file context>
Suggested change
self?.applyHostedInspectorDockConfiguration(result as? String, reason: reason)
guard let self, self.hostedInspectorFrontendWebView === hostedInspectorFrontendWebView else { return }
self.applyHostedInspectorDockConfiguration(result as? String, reason: reason)
Fix with Cubic

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
Sources/Panels/BrowserPanel.swift (2)

2900-2929: Consider using Task { @mainactor in } instead of MainActor.assumeIsolated.

The observer callback uses MainActor.assumeIsolated at line 2909. While the notification is delivered on .main queue (ensuring main thread execution), MainActor.assumeIsolated is a Swift concurrency primitive that asserts MainActor isolation at runtime. In release builds, if the assumption is violated, this can lead to undefined behavior.

Since you're already deferring subsequent work via DispatchQueue.main.async, consider wrapping the isDetachedInspectorWindow check similarly for consistency:

♻️ Suggested approach
         detachedDeveloperToolsWindowCloseObserver = NotificationCenter.default.addObserver(
             forName: NSWindow.willCloseNotification,
             object: nil,
             queue: .main
         ) { [weak self] notification in
             guard let self,
                   let window = notification.object as? NSWindow else { return }
-            let isDetachedInspectorWindow = MainActor.assumeIsolated {
-                Self.isDetachedInspectorWindow(window)
-            }
-            guard isDetachedInspectorWindow else { return }
             DispatchQueue.main.async { [weak self] in
                 guard let self else { return }
+                guard Self.isDetachedInspectorWindow(window) else { return }
                 guard self.preferredDeveloperToolsPresentation == .detached else { return }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Panels/BrowserPanel.swift` around lines 2900 - 2929, The use of
MainActor.assumeIsolated inside installDetachedDeveloperToolsWindowCloseObserver
should be replaced with a safe MainActor-bound check; locate the notification
closure that calls Self.isDetachedInspectorWindow(window) and either perform
that call inside a Task { `@MainActor` in ... } or move the call into the existing
DispatchQueue.main.async block (before checking
preferredDeveloperToolsPresentation/visible) so it runs on the main actor
without assuming isolation; update the closure to use Task { `@MainActor` in let
isDetached = Self.isDetachedInspectorWindow(window) ... } or equivalent so you
remove MainActor.assumeIsolated and ensure main-actor safety for
isDetachedInspectorWindow.

4165-4184: Visibility change from private extension to extension exposes private WebKit API helpers.

The WKWebView extension is now non-private, exposing cmuxInspectorObject() and cmuxInspectorFrontendWebView() to other files. Per the AI summary, this aligns with cross-file dependencies for hosted inspector layout.

Consider documenting that these methods rely on private WebKit selectors (_inspector, inspectorWebView) which could change or break in future macOS/WebKit releases:

📝 Suggested documentation
 extension WKWebView {
+    /// Access the private WKInspector object.
+    /// - Warning: Relies on private WebKit selector `_inspector` which may change in future releases.
     func cmuxInspectorObject() -> NSObject? {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Panels/BrowserPanel.swift` around lines 4165 - 4184, The extension
exposing cmuxInspectorObject() and cmuxInspectorFrontendWebView() uses private
WebKit selectors and should not be public; change the extension back to a
file-private or private extension (so cmuxInspectorObject and
cmuxInspectorFrontendWebView are not visible across files) and add a short doc
comment above these methods noting they rely on private selectors "_inspector"
and "inspectorWebView" which may break in future WebKit/macOS releases; ensure
you keep the selector lookup logic (NSSelectorFromString,
perform(_:).takeUnretainedValue()) in the same functions so only visibility and
the explanatory comment are changed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/Panels/BrowserPanelView.swift`:
- Around line 3627-3628: The restore/capture/hit-test logic must use the new 1
px minimum constant so the inspector can be recovered; update any comparisons
and clamps that reference zero or ignore width==1 to use
hostedInspectorDividerHitExpansion and minimumHostedInspectorWidth (instead of
minimumInspectorWidth: 0) and ensure equality is handled (e.g. treat width >=
minimumHostedInspectorWidth as visible, capture widths of exactly
minimumHostedInspectorWidth, and clamp restored widths with max(storedWidth,
minimumHostedInspectorWidth)). Adjust the restore, capture and hit-test code
paths that currently use 0 or exclusive checks so they consistently use
minimumHostedInspectorWidth and the hit expansion constant.
- Around line 3702-3704: The evaluateJavaScript completion handlers can run
after hostedInspectorFrontendWebView has been cleared/replaced, causing stale
dock state to be applied; to fix, capture the current
hostedInspectorFrontendWebView into a local constant (e.g., let currentWebView =
hostedInspectorFrontendWebView) immediately before calling evaluateJavaScript in
the methods that schedule JS evaluation, and inside the completion handler check
that hostedInspectorFrontendWebView === currentWebView (or that currentWebView
is non-nil and still valid) before calling
applyHostedInspectorDockConfiguration(...); apply this same guard wherever
evaluateJavaScript completion can call applyHostedInspectorDockConfiguration
(refer to prepareForWindowPortalHosting, setHostedInspectorFrontendWebView, and
the calls that invoke applyHostedInspectorDockConfiguration) so late callbacks
are ignored if the frontend/host changed.

---

Nitpick comments:
In `@Sources/Panels/BrowserPanel.swift`:
- Around line 2900-2929: The use of MainActor.assumeIsolated inside
installDetachedDeveloperToolsWindowCloseObserver should be replaced with a safe
MainActor-bound check; locate the notification closure that calls
Self.isDetachedInspectorWindow(window) and either perform that call inside a
Task { `@MainActor` in ... } or move the call into the existing
DispatchQueue.main.async block (before checking
preferredDeveloperToolsPresentation/visible) so it runs on the main actor
without assuming isolation; update the closure to use Task { `@MainActor` in let
isDetached = Self.isDetachedInspectorWindow(window) ... } or equivalent so you
remove MainActor.assumeIsolated and ensure main-actor safety for
isDetachedInspectorWindow.
- Around line 4165-4184: The extension exposing cmuxInspectorObject() and
cmuxInspectorFrontendWebView() uses private WebKit selectors and should not be
public; change the extension back to a file-private or private extension (so
cmuxInspectorObject and cmuxInspectorFrontendWebView are not visible across
files) and add a short doc comment above these methods noting they rely on
private selectors "_inspector" and "inspectorWebView" which may break in future
WebKit/macOS releases; ensure you keep the selector lookup logic
(NSSelectorFromString, perform(_:).takeUnretainedValue()) in the same functions
so only visibility and the explanatory comment are changed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5e86be82-4421-4647-a4ba-aa86b977eb05

📥 Commits

Reviewing files that changed from the base of the PR and between 8b66628 and 06c5cac.

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

Comment on lines +3627 to +3628
private static let hostedInspectorDividerHitExpansion: CGFloat = 10
private static let minimumHostedInspectorWidth: CGFloat = 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Align the restore/capture/hit-test thresholds with the new 1 px minimum.

The drag path clamps to 1 px, but the restore path still reapplies with minimumInspectorWidth: 0, the capture path ignores widths at exactly 1 px, and hit-testing stops treating the inspector as visible once it reaches 1 px. That makes a minimized inspector hard or impossible to recover after a resize/reattach, and it can also snap back to an older stored width.

Suggested fix
-            guard inspectorWidth > 1 else { return }
+            guard inspectorWidth >= Self.minimumHostedInspectorWidth else { return }

             let nextFrames = hit.dockSide.resizedFrames(
                 preferredWidth: preferredWidth,
                 in: containerBounds,
                 pageFrame: hit.pageView.frame,
                 inspectorFrame: hit.inspectorView.frame,
-                minimumInspectorWidth: 0
+                minimumInspectorWidth: Self.minimumHostedInspectorWidth
             )

         fileprivate static func isVisibleHostedInspectorCandidate(_ view: NSView) -> Bool {
             !view.isHidden &&
                 view.alphaValue > 0 &&
-                view.frame.width > 1 &&
+                view.frame.width >= Self.minimumHostedInspectorWidth &&
                 view.frame.height > 1
         }

Also applies to: 4595-4597, 4644-4650, 4710-4714

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

In `@Sources/Panels/BrowserPanelView.swift` around lines 3627 - 3628, The
restore/capture/hit-test logic must use the new 1 px minimum constant so the
inspector can be recovered; update any comparisons and clamps that reference
zero or ignore width==1 to use hostedInspectorDividerHitExpansion and
minimumHostedInspectorWidth (instead of minimumInspectorWidth: 0) and ensure
equality is handled (e.g. treat width >= minimumHostedInspectorWidth as visible,
capture widths of exactly minimumHostedInspectorWidth, and clamp restored widths
with max(storedWidth, minimumHostedInspectorWidth)). Adjust the restore, capture
and hit-test code paths that currently use 0 or exclusive checks so they
consistently use minimumHostedInspectorWidth and the hit expansion constant.

Comment on lines +3702 to +3704
func setHostedInspectorFrontendWebView(_ webView: WKWebView?) {
hostedInspectorFrontendWebView = webView
}

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

Ignore stale dock-sync callbacks after the frontend/host changes.

prepareForWindowPortalHosting() cancels the queued work item, but it does not cancel an evaluateJavaScript that has already started. A late completion from the previous inspector frontend can still call applyHostedInspectorDockConfiguration(...) after hostedInspectorFrontendWebView was cleared or replaced, which can reparent/layout the local host using stale dock state.

Suggested fix
+        private var hostedInspectorDockConfigurationGeneration: UInt64 = 0
+
         func setHostedInspectorFrontendWebView(_ webView: WKWebView?) {
             hostedInspectorFrontendWebView = webView
+            hostedInspectorDockConfigurationGeneration &+= 1
         }

         private func syncHostedInspectorDockConfiguration(reason: String) {
             hostedInspectorDockConfigurationSyncWorkItem = nil
             guard let hostedInspectorFrontendWebView else { return }
+            let generation = hostedInspectorDockConfigurationGeneration
             hostedInspectorFrontendWebView.evaluateJavaScript(
                 "typeof WI === 'undefined' ? null : WI.dockConfiguration"
-            ) { [weak self] result, _ in
-                self?.applyHostedInspectorDockConfiguration(result as? String, reason: reason)
+            ) { [weak self, weak hostedInspectorFrontendWebView] result, _ in
+                guard let self,
+                      generation == self.hostedInspectorDockConfigurationGeneration,
+                      hostedInspectorFrontendWebView === self.hostedInspectorFrontendWebView else { return }
+                self.applyHostedInspectorDockConfiguration(result as? String, reason: reason)
             }
         }

Also applies to: 3857-3862, 4033-4040, 4043-4074

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

In `@Sources/Panels/BrowserPanelView.swift` around lines 3702 - 3704, The
evaluateJavaScript completion handlers can run after
hostedInspectorFrontendWebView has been cleared/replaced, causing stale dock
state to be applied; to fix, capture the current hostedInspectorFrontendWebView
into a local constant (e.g., let currentWebView =
hostedInspectorFrontendWebView) immediately before calling evaluateJavaScript in
the methods that schedule JS evaluation, and inside the completion handler check
that hostedInspectorFrontendWebView === currentWebView (or that currentWebView
is non-nil and still valid) before calling
applyHostedInspectorDockConfiguration(...); apply this same guard wherever
evaluateJavaScript completion can call applyHostedInspectorDockConfiguration
(refer to prepareForWindowPortalHosting, setHostedInspectorFrontendWebView, and
the calls that invoke applyHostedInspectorDockConfiguration) so late callbacks
are ignored if the frontend/host changed.

This branch was successfully deployed

1 active deployment
Preview — 06c5cac4 Deployed Mar 11, 2026 by vercel[bot]
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.

1 participant