Skip to content

Reconcile Dock portal bindings at reparent time instead of on focus - #7786

Merged
austinywang merged 4 commits into
mainfrom
issue-7780-dock-reparent-blank
Jul 10, 2026
Merged

austinywang merged 4 commits into
mainfrom
issue-7780-dock-reparent-blank

Conversation

@austinywang

@austinywang austinywang commented Jul 10, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #7780. Fixes #7305.

Bug

Moving surfaces (terminals AND browsers) between the main area and the right-sidebar Dock intermittently left them blank or invisible until the Dock gained focus — across both directions, plain drops, and drops into splits.

Root cause

Terminal and browser content views are window-level portal-hosted; a portal bind silently no-ops when the target anchor is momentarily off-window during reparent churn (TerminalWindowPortalRegistry.bind / BrowserWindowPortalRegistry.bind both guard on anchorView.window). The main split area self-heals after topology changes via Workspace's event-driven reconcile passes (reconcileTerminalGeometryPass, reconcileBrowserPortalVisibilityForCurrentRenderedLayout), but the Dock had no reconciler at all:

  • terminals only got a synchronous reattach-token bump at attach time (Fix Dock terminal reattach on move #7055's fix), which can itself run before the churn settles;
  • browsers had no show path whatsoever — DockSplitStore.applyVisibility only implemented the browser hide side.

The only reliable recovery was the SwiftUI re-render caused by rightSidebarOwnsInputFocus flipping — which is exactly why blanked surfaces came back when the Dock gained focus. Prior point fixes (#7054, #5435, #7529) each patched one trigger of that re-render without closing the underlying gap.

Fix

Give the Dock the same authoritative portal reconciliation the main area already has:

  • New Sources/DockSplitStore+PortalReconcile.swift: idempotent Dock portal reconcile pass for terminals and browsers, mirroring the Workspace idiom — coalesced zero-delay first attempt, follow-ups re-armed by the structural notifications that fire when reparent churn settles (.terminalSurfaceHostedViewDidMoveToWindow, .terminalPortalVisibilityDidChange, .browserPortalRegistryDidChange, .terminalSurfaceDidBecomeReady), wall-clock stall backoff (10ms→250ms), and a 2s hard deadline that tears down observers. Healthy state is a pure-check no-op (no portal mutations, no refresh, no registry posts).
  • Browser show parity in DockSplitStore.applyVisibility: a visible Dock browser now marks its portal entry visible and schedules recovery only when the binding is actually damaged.
  • Reconcile scheduled from every Dock mutation path: attach (including rollback attaches), internal pane drops/drag-to-splits (performPortalPaneDrop), bonsplit didMoveTab/didSplitPane, Dock reveal, zoom toggles.
  • Cross-container moves reconcile both sides: moveSurfaceIntoDock schedules the destination Dock; moveDockSurfaceToWorkspace / moveDockSurfaceToNewWorkspace schedule the source Dock and run the destination workspace's terminal + browser reconcile after the attach/split legs.

No focus-semantics changes; recovery no longer depends on the Dock gaining focus in any path.

Commits (two-commit regression policy)

  1. aac686c — failing regression tests only (browser attach-into-visible-Dock and Dock reveal). CI is expected red on this commit.
  2. 2661432 — the fix + reconciler-specific coverage (stale terminal bind without focus, unbound browser imperative bind, healthy-browser no-op, move-path scheduling both directions).

Verification

  • Deterministic: 7 new tests in cmuxTests/DockPortalReconcileTests.swift (wired into pbxproj; scripts/lint-pbxproj-test-wiring.sh passes).
  • python3 scripts/swift_file_length_budget.py passes; no budget TSV touched; new files are 292/327 lines; no new warnings.
  • Tagged Debug build succeeded (-derivedDataPath /tmp/cmux-fable-7780).
  • Manually verified vs deterministic: the interactive drag-and-drop repro paths (dragging surfaces into/out of the Dock, including into splits, with the Dock unfocused) are inherently interactive and are NOT covered by the unit tests beyond the model-level move-path assertions above; they are being verified visually on a cloud macOS VM and via dogfood. The blank-until-focus mode itself is covered deterministically by the red tests in commit 1.
  • Localization audit: no user-facing strings added or changed (debug log strings only).

🤖 Generated with Claude Code


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


Note

Medium Risk
Touches window-level portal registries and many Dock mutation paths; incorrect reconcile timing could cause flicker or duplicate binds, but behavior mirrors an existing Workspace pattern and is covered by new tests.

Overview
Fixes Dock terminals and browsers staying blank after moves or layout changes until the Dock gained focus, by adding event-driven portal reconciliation parallel to the main workspace.

New DockSplitStore+PortalReconcile.swift introduces scheduleDockPortalReconcile / reconcileDockPortalPass: coalesced attempts, notification-driven wakeups (terminal hosted-view moves, portal visibility, browser registry), exponential backoff, and a 2s timeout. Visible panels get terminal reattach/geometry refresh and imperative browser portal bind/sync when anchors are ready.

applyVisibility now shows Dock browsers (registry visibility + reconcile when binding is damaged), not only hide.

Reconcile is hooked from Dock topology paths: attach/detach, pane drops, tab move/split, selection, zoom, and AppDelegate cross-container moves (moveSurfaceIntoDock; workspace moves also run destination workspace terminal/browser reconcile and schedule source Dock reconcile).

browserPanel(owning:in:) moves to DockSplitStore+PaneFocus.swift. DockPortalReconcileTests and a small BrowserConfigTests timeout failure style tweak round out the change.

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


Summary by cubic

Fixes Dock surfaces going blank after moves or reveal by reconciling terminal and browser portal bindings during reparenting instead of on focus. Restores portal visibility immediately and makes recovery deterministic. Fixes #7780 and #7305.

  • Bug Fixes
    • Added DockSplitStore+PortalReconcile.swift: idempotent, event‑driven reconcile for terminals and browsers with coalesced attempts, structural wakeups, short backoff, and a 2s timeout.
    • Implemented browser show parity in DockSplitStore.applyVisibility; marks BrowserWindowPortalRegistry entries visible and only schedules recovery when the binding is damaged.
    • Scheduled reconcile on all Dock topology changes and cross‑container moves (attach/rollback, pane drops/splits, tab move/split, selection changes, reveal, zoom).
    • Made reconcile state store‑owned: DockSplitStore.dockPortalReconcileState; removed debug seams and moved focus‑ownership browserPanel(owning:in:) to DockSplitStore+PaneFocus.swift.
    • Stabilized tests: BrowserSessionHistoryRestoreTests.waitUntil now aborts via continueAfterFailure = false instead of throwing to avoid false shard failures.

Written for commit 023cd5e. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes
    • Improved reliability of docking and workspace transfers for terminal and browser surfaces.
    • Dock “portal” visibility now stays consistent through focus changes, zoom toggles, pane splits, and tab moves.
    • Better recovery for stale or missing terminal/browser portal bindings, including reattachment and geometry refresh when needed.
    • Moving surfaces into new docks/workspaces now triggers stronger portal and layout reconciliation to reduce transient inconsistencies.
  • Tests
    • Added coverage for dock portal reconciliation behavior and scheduling.

austinywang and others added 2 commits July 9, 2026 19:01
Two regression tests for the class of bug where surfaces moved into or
revealed in the Dock stay blank until the Dock gains focus:

- dockBrowserAttachIntoVisibleDockShowsPortal: attaching a live browser
  panel into a visible Dock must mark its window-portal entry visible at
  attach time. Fails today because DockSplitStore.applyVisibility only
  implements the browser hide side; the show side is a no-op.
- dockBrowserRevealRestoresPortalVisibility: hiding and re-revealing the
  Dock must restore the browser portal entry's visibility. Fails today
  for the same reason (the #5435 family, browser flavor).

CI is expected to go red on this commit; the fix lands in the next
commit per the repo's two-commit regression policy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes #7780. Fixes #7305.

Root cause: terminal and browser content views are window-level
portal-hosted, and a portal bind silently no-ops when the target anchor
is momentarily off-window during reparent churn (both registries guard
on anchorView.window). The main split area self-heals after topology
changes via Workspace's event-driven reconcile passes, but the Dock had
no reconciler at all: terminals only got a synchronous reattach-token
bump at attach time (which can itself race the churn), and browsers had
no show path whatsoever — DockSplitStore.applyVisibility only
implemented the browser hide side. The only reliable recovery was the
SwiftUI re-render caused by rightSidebarOwnsInputFocus flipping, which
is why blanked surfaces came back exactly when the Dock gained focus.

Fix:
- New DockSplitStore+PortalReconcile.swift: an idempotent Dock portal
  reconcile pass covering terminals and browsers, mirroring the
  Workspace reconcile idiom — coalesced zero-delay first attempt,
  follow-ups re-armed by the structural notifications that fire when
  reparent churn settles, wall-clock stall backoff, and a hard deadline
  that tears down observers. Healthy state is a pure-check no-op.
- Browser show parity in DockSplitStore.applyVisibility: a visible
  Dock browser now marks its portal entry visible and schedules
  recovery only when the binding is actually damaged.
- Reconcile scheduling from every Dock mutation path: attach (including
  rollback attaches), internal pane drops and drag-to-splits, bonsplit
  didMoveTab/didSplitPane, Dock reveal, and zoom toggles.
- Cross-container moves reconcile both sides: moveSurfaceIntoDock
  schedules the destination Dock; moveDockSurfaceToWorkspace and
  moveDockSurfaceToNewWorkspace schedule the source Dock and run the
  destination workspace's terminal and browser reconcile after the
  attach/split legs.

The regression tests from the previous commit now pass; additional
coverage exercises the reconciler directly (stale terminal bind without
focus, unbound browser imperative bind, healthy-browser no-op, and
move-path scheduling in both directions).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 10, 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 Jul 10, 2026 7:33am
cmux-staging Building Building Preview, Comment Jul 10, 2026 7:33am

@coderabbitai

coderabbitai Bot commented Jul 10, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Dock portal reconciliation now repairs terminal and browser visibility, bindings, geometry, and refresh state after Dock mutations and structural events. Surface moves, focus changes, pane operations, and visibility updates schedule reconciliation, with retry handling and regression tests.

Changes

Dock portal reconciliation

Layer / File(s) Summary
Reconciliation scheduler and pass
Sources/DockSplitStore+PortalReconcile.swift, Sources/DockSplitStore.swift
Adds scheduled, retried reconciliation for terminal and browser portals, including readiness checks, geometry updates, visibility, binding, refresh, and stored reconciliation state.
Dock mutation and visibility triggers
Sources/AppDelegate+DockSurfaceMove.swift, Sources/DockSplitStore+PaneFocus.swift, Sources/DockSplitStore+PortalDrop.swift, Sources/DockSplitStore+SurfaceTransfer.swift, Sources/DockSplitStore.swift
Schedules reconciliation after surface moves, attachments, pane changes, tab moves, portal drops, focus changes, and browser visibility updates; relocates browser focus-intent lookup into the pane-focus extension.
Regression coverage and project wiring
cmuxTests/DockPortalReconcileTests.swift, cmuxTests/BrowserConfigTests.swift, cmux.xcodeproj/project.pbxproj
Adds browser and terminal reconciliation tests, updates provisional-navigation test timeout handling, and registers the implementation and test files in Xcode.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DockSplitStore
  participant TerminalWindowPortalRegistry
  participant BrowserWindowPortalRegistry
  DockSplitStore->>DockSplitStore: Schedule reconciliation after Dock mutation
  DockSplitStore->>DockSplitStore: Flush layouts and inspect panels
  DockSplitStore->>TerminalWindowPortalRegistry: Reattach and reconcile terminal portal
  DockSplitStore->>BrowserWindowPortalRegistry: Bind and update browser portal
  DockSplitStore->>DockSplitStore: Retry until portal readiness is complete
Loading

Possibly related PRs


Important

Pre-merge checks failed

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

❌ Failed checks (4 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Swift Blocking Runtime ❌ Error Production Sources/DockSplitStore+PortalReconcile.swift adds DispatchQueue.main.asyncAfter timeout/backoff scheduling, which the rule forbids in non-test Swift. Replace delayed dispatch with a real signal/callback/notification-driven state transition or actor-owned scheduler; keep any timing-only logic test-only.
Cmux Algorithmic Complexity ❌ Error Sources/DockSplitStore+PortalReconcile.swift:149-155 scans panels, then each panel rescanes surfaceIdToPanelId and all panes/tabs via panelIsSelectedInVisibleDockPane/paneId(forPanelId), creating a... Cache panel→pane/tab membership or pass the selected pane/tab once so the reconcile loop stays a single pass over panels without repeated rescans.
Cmux Swift Concurrency ❌ Error New production code adds DispatchWorkItem + DispatchQueue.main.asyncAfter retry/backoff logic in DockSplitStore+PortalReconcile, a legacy async workflow outside a callback boundary. Replace the GCD timer/retry loop with a stored MainActor Task/actor-based scheduler (e.g. Task.sleep/Clock) tied to dockPortalReconcileState cancellation.
Cmux Architecture Rethink ❌ Error Adds DockPortalReconcileState with NotificationCenter observers and asyncAfter backoff to paper over reparent/focus races, which the rules forbid. Move portal visibility/reattach ownership into one deterministic lifecycle path derived from panel/window attachment, removing timer/observer-based repair logic.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (20 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR implements event-driven Dock portal reconciliation, covers browser and terminal reparenting, and adds regression tests matching #7780 and #7305.
Out of Scope Changes check ✅ Passed The added test timeout change and file moves are support work for the Dock reconcile feature and do not appear unrelated to the stated objectives.
Cmux Swift Actor Isolation ✅ Passed New Dock portal reconcile code stays @MainActor; callers are main-actor methods, and no new Sendable/shared-mutable isolation issue was introduced.
Cmux Browser Automation Off-Main ✅ Passed PR only touched Dock portal reconciliation/tests; browser.wait/eval/screenshot/cookie commands are already worker-routed and policy-tested elsewhere.
Cmux Expensive Synchronous Load ✅ Passed The Dock changes only add portal-reconcile scheduling/UI state; no touched path calls RestorableAgentSessionIndex.load, SharedLiveAgentIndex.shared, or large JSON/JSONL parsing.
Cmux Cache Substitution Correctness ✅ Passed No persistence/history/undo/snapshot path was changed; portal snapshots are only used for transient UI reconciliation and are freshness-checked with reconcile observers/fallbacks.
Cmux No Hacky Sleeps ✅ Passed Diff only changes a Swift test file; the no-hacky-sleeps rule is scoped to non-Swift runtime scripts, so it doesn’t apply here.
Cmux Swift @Concurrent ✅ Passed All touched async code is @MainActor/UI-bound; no new nonisolated async, invalid @concurrent, or heavy background helper calls were introduced.
Cmux Swift File And Package Boundaries ✅ Passed Touched files are small-to-medium app/UI glue; the only new prod file is 265 lines and the 880-line store only got incidental changes, so no boundary rule violation.
Cmux Swiftpm Lockfiles ✅ Passed PR only adds Swift sources and pbxproj file entries; no .gitignore, Package.resolved, workflow, or SwiftPM package-reference diffs were present.
Cmux Swift Logging ✅ Passed No added print/debugPrint/dump/NSLog/Logger usage in the touched diff; changed app files only add reconciliation calls, and tests add no runtime logging.
Cmux User-Facing Error Privacy ✅ Passed New production strings are internal reconcile reasons and generic UI labels; no added user-facing errors/alerts expose vendor names, secrets, or raw messages.
Cmux Full Internationalization ✅ Passed HEAD only changes a test helper in BrowserConfigTests.swift; no production/user-facing text, catalogs, or Info.plist files were changed.
Cmux Swiftui State Layout ✅ Passed No new ObservableObject/@Published/GeometryReader/lazy-row or render-time mutation was added; the touched Dock code is @Observable model/AppKit-bridge logic, which the rule allows.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed No production NSWindow/NSPanel/WindowGroup changes were added; the only new window is a test-only fixture, which the rule allows.
Cmux Source Artifacts ✅ Passed All changed paths are intentional source/test/project files; no logs, temp dirs, caches, build output, or other artifacts were added.
Cmux No Test Or Debug Seam In Production Source ✅ Passed No new debug/test-only seam was added in Sources; the new reconcile helpers are used by production paths, and the only ForTesting seam is pre-existing and untouched.
Cmux No Ambient Global State ✅ Passed No new ambient global state: all new behavior is in DockSplitStore/AppDelegate extensions, and reconcile state is owned by DockSplitStore via an instance property.
Title check ✅ Passed The title clearly summarizes the main change: reconciling Dock portal bindings during reparenting instead of relying on focus.
Description check ✅ Passed The description thoroughly explains the bug, root cause, fix, and verification, though the template’s demo video, review trigger, and checklist sections are omitted.
✨ 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-7780-dock-reparent-blank

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.

@greptile-apps

greptile-apps Bot commented Jul 10, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds Dock portal reconciliation so moved Dock surfaces recover without relying on focus. The main changes are:

  • A Dock-owned reconciler for terminal and browser portal bindings.
  • Browser show-side visibility updates for visible Dock panels.
  • Reconcile scheduling across Dock attach, drop, split, selection, zoom, and cross-container move paths.
  • Tests for Dock browser and terminal portal recovery, plus Xcode project wiring.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
Sources/DockSplitStore+PortalReconcile.swift Adds the Dock portal reconcile state machine, observers, retry scheduling, and terminal/browser recovery pass.
Sources/DockSplitStore.swift Adds store-owned reconcile state on DockSplitStore.
Sources/DockSplitStore+PaneFocus.swift Adds browser show-side portal visibility updates and schedules reconciliation from Dock focus, selection, split, and zoom paths.
Sources/AppDelegate+DockSurfaceMove.swift Schedules Dock and workspace reconciliation around surface moves into and out of the Dock.
cmuxTests/DockPortalReconcileTests.swift Adds tests for Dock browser reveal, terminal stale binding recovery, healthy browser no-op behavior, and move-path scheduling.
cmux.xcodeproj/project.pbxproj Wires the new reconciler source file and Dock portal tests into the Xcode project.

Reviews (3): Last reviewed commit: "Abort browser waitUntil timeouts without..." | Re-trigger Greptile

Comment on lines +74 to +84
private func refreshDockPortalReconcileTimeout() {
let state = dockPortalReconcileState
state.timeoutWorkItem?.cancel()
let workItem = DispatchWorkItem { [weak self] in
self?.clearDockPortalReconcile()
}
state.timeoutWorkItem = workItem
DispatchQueue.main.asyncAfter(
deadline: .now() + Self.dockPortalReconcileTimeout,
execute: workItem
)

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 Timeout Drops Recovery Signals

When a Dock portal anchor or hosted view becomes ready after this fixed 2-second window, clearDockPortalReconcile() removes the observers and invalidates pending attempts before the real move-to-window or registry-change notification arrives. A slow reparent, busy main thread, or delayed WebKit/terminal attach can then leave the Dock surface blank until another unrelated Dock mutation schedules a fresh reconcile.

Rule Used: Flag new blocking or timing-based synchronization ... (source)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

By design, matching the repo's sanctioned mechanism: the 2s deadline is byte-for-byte the bound Workspace.beginEventDrivenLayoutFollowUp uses for the identical class of race in the main split area (Sources/Workspace.swift, event-driven layout follow-up). The deadline is a leak/wakeup backstop, not the primary signal — recovery is driven by the four structural notifications that fire when SwiftUI actually commits the reparent, and the browser/terminal view-side onDidMoveToWindow bind paths remain the first line of recovery independent of this reconciler. Any subsequent Dock mutation (attach, drop, split, selection, reveal, zoom) re-arms a fresh chain with a fresh deadline, so an anchor that becomes ready later than 2s without any of those signals is the same residual the main area already accepts. Keeping observers armed indefinitely is what the bound exists to prevent.

Comment on lines +108 to +123
private func scheduleDockPortalReconcileAttempt() {
let state = dockPortalReconcileState
guard state.timeoutWorkItem != nil else { return }
guard !state.attemptScheduled else { return }

state.attemptScheduled = true
let delay = dockPortalReconcileBackoffDelay()
let version = state.attemptVersion
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
guard let self else { return }
let state = self.dockPortalReconcileState
guard state.attemptVersion == version,
state.timeoutWorkItem != nil else { return }
state.attemptScheduled = false
self.attemptDockPortalReconcile()
}

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 Portal Readiness Is Polled

When dockBrowserPortalAnchorReady or the terminal bounds/surface checks stay false, this loop retries with DispatchQueue.main.asyncAfter rather than waiting only for the structural notifications that prove readiness changed. That can both keep doing repeated main-thread layout/portal work while nothing changed and still miss recovery once the hard timeout clears the observers.

Rule Used: Flag new blocking or timing-based synchronization ... (source)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The loop is event-driven first: the four structural notifications (terminalSurfaceHostedViewDidMoveToWindow, terminalPortalVisibilityDidChange, browserPortalRegistryDidChange, terminalSurfaceDidBecomeReady) immediately re-arm a zero-delay attempt when readiness actually changes. The 10ms→250ms stall backoff only spaces retries while the anchor is not yet ready — that path performs no registry mutations and posts nothing, so it cannot spin — and it mirrors Workspace.beginEventDrivenLayoutFollowUp's stall backoff exactly, bounded by the same 2s deadline. A purely notification-driven loop would hang forever on the one case this exists to repair: a bind dropped because the commit produced no observable notification.

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.

That addresses the concern. The critical insight — that the silent-drop case is precisely the one that produces no notification — justifies having a backoff path rather than relying on notifications alone. And with no registry mutations on the stall path, the cost is just a few cheap readiness checks until the 2s deadline fires.

Withdrawn.

Comment on lines +276 to +291
#if DEBUG
extension DockSplitStore {
func debugDockPortalReconcileScheduleCountForTesting() -> Int {
dockPortalReconcileState.scheduledRequestCount
}

func debugResetDockPortalReconcileStateForTesting() {
clearDockPortalReconcile()
dockPortalReconcileState.scheduledRequestCount = 0
}

@discardableResult
func debugReconcileDockPortalsForTesting(reason: String = "dock.portal.test") -> Bool {
reconcileDockPortalPass(reason: reason)
}
}

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 Production Source Test Seam

These #if DEBUG ForTesting methods expose and drive private reconciler state from a file under Sources/, and the new tests call them to reset schedule state, count requests, and run the reconcile pass. The repo rule keeps this kind of test-only surface out of production Swift; the tests should reach internal state through @testable import or a test-support target instead.

Rule Used: Do not add new test/debug seams (ForTesting-styl... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in a26c14a: the #if DEBUG debug…ForTesting extension is removed per .github/review-bot-rules/no-test-debug-seam-in-production-source.md. Tests now call the internal reconcileDockPortalPass(reason:) / clearDockPortalReconcile() directly and read the store-owned dockPortalReconcileState via @testable import (the rule's canonical fix).

Comment on lines +26 to +33
private var dockPortalReconcileState: DockPortalReconcileState {
if let state = objc_getAssociatedObject(self, &dockPortalReconcileStateKey) as? DockPortalReconcileState {
return state
}
let state = DockPortalReconcileState()
objc_setAssociatedObject(self, &dockPortalReconcileStateKey, state, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
return state
}

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.

P2 Associated Object Requires ObjC Runtime

scheduleDockPortalReconcile reaches this property on DockSplitStore, but the inspected declaration is a pure Swift final class DockSplitStore: BonsplitDelegate, not an NSObject subclass. If that protocol does not make the class Objective-C-compatible in every app build, the first reconcile schedule passes a non-ObjC object to objc_getAssociatedObject/objc_setAssociatedObject, which can crash the Dock move/reveal path instead of recovering the portal.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Resolved by a26c14a — the associated-object pattern is gone entirely; the state is now a declared stored property on DockSplitStore, so no ObjC-runtime dependency remains.

@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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Sources/DockSplitStore+PaneFocus.swift (1)

92-100: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Route Bonsplit selection callbacks through this scheduled path.

Lines 119-129 still call applyDockSelection directly, bypassing this new scheduling path. A user selecting or focusing a stale terminal portal therefore only receives the lightweight visibility update, not the full reconcile pass. Have both delegate callbacks call applyFocusedDockSelection() (or a shared helper that schedules afterward).

Proposed fix
 func splitTabBar(_ controller: BonsplitController, didSelectTab tab: Bonsplit.Tab, inPane pane: PaneID) {
-    applyDockSelection(tabId: tab.id, inPane: pane)
+    applyFocusedDockSelection()
 }

 func splitTabBar(_ controller: BonsplitController, didFocusPane pane: PaneID) {
-    guard let tab = controller.selectedTab(inPane: pane) else {
-        applyVisibilityToAllPanels()
-        return
-    }
-    applyDockSelection(tabId: tab.id, inPane: pane)
+    applyFocusedDockSelection()
 }

As per coding guidelines, use one shared action path rather than wiring behavior separately across surfaces.

🤖 Prompt for 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.

In `@Sources/DockSplitStore`+PaneFocus.swift around lines 92 - 100, Route both
Bonsplit selection/focus delegate callbacks through applyFocusedDockSelection()
instead of calling applyDockSelection directly. Update the callbacks around the
existing direct calls so they use this shared scheduled path, ensuring stale
terminal portal selections trigger the full dock portal reconcile.

Source: Coding guidelines

🤖 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/DockSplitStore`+PortalReconcile.swift:
- Around line 276-292: Remove the DEBUG-only DockSplitStore testing extension
and its methods debugDockPortalReconcileScheduleCountForTesting(),
debugResetDockPortalReconcileStateForTesting(), and
debugReconcileDockPortalsForTesting(). Update tests to use the existing internal
reconcileDockPortalPass(reason:) via `@testable` import and assert portal-registry
outcomes directly.
- Around line 4-31: Move DockPortalReconcileState ownership into DockSplitStore:
remove the mutable file-global dockPortalReconcileStateKey and Objective-C
association logic, declare the reconciliation state as an injected/store-owned
property initialized by DockSplitStore’s constructor, and update the
dockPortalReconcileState accessor and all reconciliation methods to use that
property directly.
- Around line 74-84: The portal repair logic in
refreshDockPortalReconcileTimeout and the related retry/layout methods relies on
delayed dispatch, exponential retries, and forced polling. Remove the timeout
and all DispatchQueue.main.asyncAfter-based coordination, retry counters, and
layout polling; instead, trigger one coalesced reconciliation pass from scoped
attachment, layout, and registry-completion lifecycle events, using the existing
portal reconciliation state and clearDockPortalReconcile symbols.

---

Outside diff comments:
In `@Sources/DockSplitStore`+PaneFocus.swift:
- Around line 92-100: Route both Bonsplit selection/focus delegate callbacks
through applyFocusedDockSelection() instead of calling applyDockSelection
directly. Update the callbacks around the existing direct calls so they use this
shared scheduled path, ensuring stale terminal portal selections trigger the
full dock portal reconcile.
🪄 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: b7005de3-0b47-4ff7-a4ab-ed866f84b30f

📥 Commits

Reviewing files that changed from the base of the PR and between 98b86ec and 2661432.

📒 Files selected for processing (7)
  • Sources/AppDelegate+DockSurfaceMove.swift
  • Sources/DockSplitStore+PaneFocus.swift
  • Sources/DockSplitStore+PortalDrop.swift
  • Sources/DockSplitStore+PortalReconcile.swift
  • Sources/DockSplitStore+SurfaceTransfer.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/DockPortalReconcileTests.swift

Comment thread Sources/DockSplitStore+PortalReconcile.swift Outdated
Comment thread Sources/DockSplitStore+PortalReconcile.swift
Comment thread Sources/DockSplitStore+PortalReconcile.swift Outdated
austinywang and others added 2 commits July 9, 2026 19:37
- Replace the Objective-C associated-object state (and its top-level
  association key) with a declared DockSplitStore-owned
  dockPortalReconcileState property, per the no-ambient-global-state
  review rule. DockSplitStore.swift stays under its length budget by
  relocating the focus-ownership browserPanel(owning:in:) lookup to
  DockSplitStore+PaneFocus.swift.
- Remove the #if DEBUG debug…ForTesting extension, per the
  no-test-debug-seam-in-production-source rule. Tests now drive the
  internal reconcileDockPortalPass(reason:)/clearDockPortalReconcile()
  API and read the store-owned state directly via @testable import.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BrowserSessionHistoryRestoreTests' waitUntil recorded an XCTFail and
then also threw BrowserTestTimeout. The thrown error is tallied by
XCTest as an "unexpected" failure, which is the one class the app-host
unit-test gate does not tolerate — so whenever this pre-existing timeout
(it reproduces identically on main; see runs 29062349460 and
29063767894, BrowserConfigTests.swift:2667) lands in the final test-run
segment, the whole shard goes red. On main it is usually masked because
an earlier app-host crash restarts the run and the gate parses only the
last summary; this PR's added test file re-packs the shards and put the
suite in a segment that completes cleanly.

Abort via continueAfterFailure = false instead: the timeout is still
recorded as a visible test failure and still stops the test at the same
point, but is no longer misclassified as a crash-grade failure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Preview – cmux — 023cd5e6 Deployed Jul 10, 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

1 participant