Skip to content

Prevent detached replacements from displacing rearmed Dock portal hosts - #8310

Merged
austinywang merged 8 commits into
mainfrom
fix-dock-blank-after-pane-moves
Jul 17, 2026
Merged

austinywang merged 8 commits into
mainfrom
fix-dock-blank-after-pane-moves

Conversation

@austinywang

@austinywang austinywang commented Jul 17, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Reproduces the Dock portal lifecycle race where SwiftUI dismantles a visible terminal host, rearms its lease, and briefly creates a detached zero-sized replacement.
  • Preserves the live host as portal authority until a distinct replacement is attached to a window and has usable bounds.
  • Allows that same replacement to claim normally as soon as it becomes usable.

Why this is a draft

An earlier tagged dogfood flow also exposed stale browser placement and duplicate hover targets during pane dragging. Those are not claimed as fixed by this terminal lease change. The PR stays draft until fresh terminal/browser drag dogfood confirms this patch is isolated and does not reproduce those behaviors.

Regression sequence

  1. 04936dbad9 — adds the failing behavioral regression test only.
  2. f7e08c0353 — rejects ownership acquisition by a distinct detached/tiny host until it becomes usable.

The test-only commit was pushed first and the manual CI workflow was dispatched. Current origin/main blocks macOS tests earlier in workflow-guard-tests because WorkspaceTodoNotificationRegressionTests.swift is not wired into the cmuxTests target, so CI could not record the intended regression failure. Run: https://github.com/manaflow-ai/cmux/actions/runs/29552840750

Verification

  • Regression-only local Dock portal suite: 13 tests executed; only the new detached-replacement case failed, on both unauthorized ownership and displaced-host assertions.
  • Fixed HEAD rebased onto current origin/main: 13 tests in DockPortalReconcileTests passed.
  • ./scripts/reload.sh --tag detached-replacement-portal succeeded on f7e08c0353.
  • git diff --check passed.
  • No user-facing strings changed; localization catalogs are unaffected.

Dogfood checklist before ready for review

  • Repeated terminal pane moves into, within, and out of the Dock.
  • Repeated browser pane moves over the same targets.
  • Confirm exactly one drop-target overlay is visible during drag.
  • Confirm browser portals move with their pane and terminal portals never go blank.

Summary by CodeRabbit

  • Bug Fixes
    • Improved portal-host claiming and replacement to prevent detached or undersized hosts from taking over an active portal.
    • Enhanced ownership reconciliation during detach/rearm and browser detach/attach, including correct same-pane refresh behavior.
  • Tests
    • Added new Dock and browser reconciliation tests covering detached vs attached claiming and lease selection outcomes.
  • Refactor
    • Centralized portal-host lease usability and replacement decisioning into a shared policy, applied across terminal surfaces and the browser panel.

@coderabbitai

coderabbitai Bot commented Jul 17, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Portal host claiming and replacement now use a shared PortalHostLeasePolicy for area, usability, and ownership decisions. Terminal and browser flows consume the policy, while reconciliation tests cover detached and attached Dock and browser replacements.

Changes

Portal host lease policy

Layer / File(s) Summary
Centralize lease policy rules
Packages/macOS/CmuxTerminalCore/..., Packages/macOS/CmuxTerminal/..., Sources/Panels/BrowserPanel.swift
PortalHostLeasePolicy computes lease area, determines usability from attachment and minimum area, and evaluates replacement eligibility.
Apply policy to terminal claims
Packages/macOS/CmuxTerminal/...
TerminalSurface.claimPortalHost constructs leases through the policy, delegates replacement decisions, and records policy-derived skip causes.
Apply policy to browser claims
Sources/Panels/BrowserPanel.swift
Browser claims use policy-based usability and replacement handling, preserve portal locks, track instance serials, and update forced/cause logging.
Validate detached and attached replacements
cmuxTests/DockPortalReconcileTests.swift
Tests verify detached Dock and browser replacements cannot displace live hosts, while attached replacements can claim after the live owner is released.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Host as Portal host
  participant Claim as claimPortalHost
  participant Policy as PortalHostLeasePolicy
  participant Lease as Active portal lease
  Host->>Claim: Submit bounds and ownership
  Claim->>Policy: Calculate area and evaluate usability
  Policy-->>Claim: Return lease metrics and replacement decision
  Claim->>Lease: Preserve or update active host lease
Loading

Possibly related PRs

Suggested reviewers: lawrencecchen, azooz2003-bit

🚥 Pre-merge checks | ✅ 25
✅ Passed checks (25 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed PASS: The patch keeps portal-host logic on @MainActor UI classes, and the new PortalHostLeasePolicy is a pure Sendable value type with no shared mutable state.
Cmux Swift Blocking Runtime ✅ Passed The diff only refactors portal lease policy and adds tests; no new waits, sleeps, semaphores, sync dispatch, or NSLock-based synchronization were introduced.
Cmux Browser Automation Off-Main ✅ Passed Diff only changes BrowserPanel portal-host lease logic; TerminalController and ControlCommandExecutionPolicy are untouched, so no browser socket automation routing violation appears.
Cmux Expensive Synchronous Load ✅ Passed BrowserPanel.claimPortalHost only adds PortalHostLeasePolicy checks; no agent-history/session loads, JSON parsing, or other expensive sync work appears in the diff.
Cmux Cache Substitution Correctness ✅ Passed The new lease policy and lock are transient portal-claim state, not persistence/history/undo/snapshot caches, and cold/stale cases are revalidated or cleared.
Cmux No Hacky Sleeps ✅ Passed PASS: The diff is Swift-only; the rule scopes out Swift, and no non-Swift runtime scripts were added or modified.
Cmux Algorithmic Complexity ✅ Passed BrowserPanel’s new claim logic is constant-time predicate work; no new scans, sorts, joins, or per-target rescans over scalable collections.
Cmux Swift Concurrency ✅ Passed PR only refactors synchronous portal-lease logic and adds tests; no new DispatchQueue, Task, Combine app-state, or completion-handler APIs were introduced.
Cmux Swift @Concurrent ✅ Passed No changed Swift code adds invalid @concurrent or nonisolated async; the touched portal-lease paths and tests are synchronous/UI-bound.
Cmux Swift Package Boundaries ✅ Passed Reusable lease-area/usability/replacement logic was extracted to CmuxTerminalCore; BrowserPanel and TerminalSurface now only compose surface-specific state and logging.
Cmux Swiftpm Lockfiles ✅ Passed No Package.swift, Package.resolved, .gitignore, Xcode, or workflow files changed; diff is source/test only.
Cmux Swift Logging ✅ Passed PASS: The only Swift diff adds lease logic in BrowserPanel; no new or changed print/debugPrint/dump/NSLog/Logger usage, and the existing DEBUG cmuxDebugLog is allowed.
Cmux User-Facing Error Privacy ✅ Passed Patch only adjusts internal portal-lease logic and DEBUG logs; no user-facing error/alert/output copy or sensitive details were added.
Cmux Full Internationalization ✅ Passed No production user-facing copy was added; changes are logic/tests plus DEBUG-only logs and test reasons, all allowed by the i18n rule.
Cmux Swiftui State Layout ✅ Passed The diff only adds 5 lines inside BrowserPanel.claimPortalHost; no new @Observable/@published state, GeometryReader, lazy rows, or render-time state writes were introduced.
Cmux Architecture Rethink ✅ Passed The only new code refreshes existing lock state on same-host reclaim; it adds no sleeps, polling, delayed dispatch, or split ownership.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed The PR only changes portal-lease logic and test fixtures; it adds no new standalone NSWindow/NSPanel/WindowGroup code or cmux.* identifier assignments.
Cmux Source Artifacts ✅ Passed All changed paths are intentional source/test files; no logs, screenshots, caches, temp, build output, or other artifacts appear in the diff.
Cmux No Test Or Debug Seam In Production Source ✅ Passed PASS: Sources/Panels/BrowserPanel.swift only adds lease-policy claim logic and DEBUG logging; no new debug/test-only accessor or seam-shaped member was added.
Cmux No Ambient Global State ✅ Passed Diff adds a constructable PortalHostLeasePolicy and local policy uses; production files only remove ambient helpers, with no new file-scope funcs, mutable vars, or singletons.
Title check ✅ Passed The title accurately captures the main change: preventing detached replacements from displacing Dock portal hosts.
Description check ✅ Passed The description covers summary, rationale, regression sequence, and verification, but omits some template sections like Demo Video and Review Trigger.
✨ 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 fix-dock-blank-after-pane-moves

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.

@austinywang
austinywang marked this pull request as ready for review July 17, 2026 04:05
@greptile-apps

greptile-apps Bot commented Jul 17, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a SwiftUI Dock portal lifecycle race where a detached, zero-sized replacement host could displace the still-live rearmed host, leaving the terminal surface blank. It introduces PortalHostLeasePolicy in CmuxTerminalCore to centralise the usability and replacement rules, then adopts it in both TerminalSurface+PortalLease and BrowserPanel, replacing the previously duplicated helpers.

  • PortalHostLeasePolicy.shouldReplace now gates all cross-host replacements (including cross-pane) on isUsable(candidate), so a detached or zero-area representable can never steal portal authority until it is window-attached with meaningful bounds.
  • BrowserPanel drops its local PortalHostLease duplicate, adopts the shared type, and adds a new no-current-lease guard so a pending distinct replacement also blocks an unusable first-time claimant.
  • Two new regression tests in DockPortalReconcileTests cover the terminal detach/rearm sequence and the browser detach/attach lifecycle end-to-end.

Confidence Score: 5/5

Safe to merge after the dogfood checklist passes; the change tightens portal ownership and cannot produce a worse outcome than the existing blank-portal bug.

The core invariant is cleanly expressed in one place and verified by new targeted tests. No production logic was added that introduces new state, side channels, or timing dependencies.

No files require special attention beyond verifying the dogfood results for cross-pane terminal and browser pane drags.

Important Files Changed

Filename Overview
Packages/macOS/CmuxTerminalCore/Sources/CmuxTerminalCore/SurfaceValues/PortalHostLeasePolicy.swift New shared policy struct that centralizes portal-host usability and replacement rules across terminal and browser surfaces; clean, well-documented, Sendable, and correctly gates all cross-host replacements on candidate usability.
Packages/macOS/CmuxTerminal/Sources/CmuxTerminal/Surface/TerminalSurface+PortalLease.swift Removes the duplicated usability helpers and delegates to PortalHostLeasePolicy; the newerSamePaneHostReady and newerModelOwnerReady flags lose their nextUsable && guard (dead code noted in prior review comments since shouldReplace gates on isUsable first) and the cross-pane hand-off comment is stale (both called out in prior comments).
Sources/Panels/BrowserPanel.swift Removes local PortalHostLease duplicate and adopts the shared CmuxTerminalCore type; browser now hardcodes instanceSerial: 0 since it has no serial tracking — the field is unused by shouldReplace but reads misleadingly; the new no-current-lease usability guard and same-host-refresh pending-replacement consumption are correct.
cmuxTests/DockPortalReconcileTests.swift Adds two well-structured regression tests: one for the terminal Dock portal detached-replacement race and one for the browser detach/attach lifecycle; both cover the new blocking behavior and the subsequent usable-host success path.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A([claimPortalHost called]) --> B{activePortalHostLease exists?}
    B -- No --> C{consumesPendingDistinctReplacement?}
    C -- Yes --> D{isUsable candidate?}
    D -- No --> E([return false / skip])
    D -- Yes --> F[consume pending\nclear + lock host]
    F --> G([set lease / return true])
    C -- No --> G

    B -- Yes --> H{current.hostId == hostId?\nself-refresh}
    H -- Yes --> I{pendingDistinct for this pane\n&& isUsable?}
    I -- Yes --> J[consume pending\nset lock]
    J --> K([update lease / return true])
    I -- No --> K

    H -- No --> L[compute\nallowsSamePaneReplacement]
    L --> M[leasePolicy.shouldReplace]
    M --> N{isUsable candidate?}
    N -- No --> O([return false\ncause=detachedOrTiny])
    N -- Yes --> P{cross-pane?\ncurrent.paneId != candidate.paneId}
    P -- Yes --> Q([replace / return true])
    P -- No --> R{!isUsable current\nOR allowsSamePane?}
    R -- Yes --> Q
    R -- No --> S([return false\ncause=ownerPreferred])
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A([claimPortalHost called]) --> B{activePortalHostLease exists?}
    B -- No --> C{consumesPendingDistinctReplacement?}
    C -- Yes --> D{isUsable candidate?}
    D -- No --> E([return false / skip])
    D -- Yes --> F[consume pending\nclear + lock host]
    F --> G([set lease / return true])
    C -- No --> G

    B -- Yes --> H{current.hostId == hostId?\nself-refresh}
    H -- Yes --> I{pendingDistinct for this pane\n&& isUsable?}
    I -- Yes --> J[consume pending\nset lock]
    J --> K([update lease / return true])
    I -- No --> K

    H -- No --> L[compute\nallowsSamePaneReplacement]
    L --> M[leasePolicy.shouldReplace]
    M --> N{isUsable candidate?}
    N -- No --> O([return false\ncause=detachedOrTiny])
    N -- Yes --> P{cross-pane?\ncurrent.paneId != candidate.paneId}
    P -- Yes --> Q([replace / return true])
    P -- No --> R{!isUsable current\nOR allowsSamePane?}
    R -- Yes --> Q
    R -- No --> S([return false\ncause=ownerPreferred])
Loading

Reviews (3): Last reviewed commit: "fix: consume surviving browser portal ha..." | Re-trigger Greptile

@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: 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 3452-3458: Update the shouldReplace ownership-change path to clear
pendingDistinctPortalHostReplacementPaneId after every successful distinct
replacement. Capture whether the replacement consumed the pending request before
clearing it, and set lockedPortalHost only for that consumed-request case;
preserve the existing unlock behavior for matching non-forced locks.
🪄 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: 41a18ce5-99d2-4c01-9442-4487a3983ba2

📥 Commits

Reviewing files that changed from the base of the PR and between f7e08c0 and dab8d33.

📒 Files selected for processing (4)
  • Packages/macOS/CmuxTerminal/Sources/CmuxTerminal/Surface/TerminalSurface+PortalLease.swift
  • Packages/macOS/CmuxTerminalCore/Sources/CmuxTerminalCore/SurfaceValues/PortalHostLeasePolicy.swift
  • Sources/Panels/BrowserPanel.swift
  • cmuxTests/DockPortalReconcileTests.swift

Comment thread Sources/Panels/BrowserPanel.swift

@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.

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

3452-3458: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Clear superseded pending replacement state after every successful ownership change.

A non-forced replacement currently leaves pendingDistinctPortalHostReplacementPaneId intact if it doesn't match the pending pane ID. If ownership later returns to that pending pane, the stale value can authorize an unrelated host. Clear the pending transition whenever an ownership change succeeds, then set the lock only when that replacement consumed the pending request.

  • Sources/Panels/BrowserPanel.swift#L3452-L3458: Clear the pending state unconditionally before checking consumesPendingDistinctReplacement on the replacement path.
  • Sources/Panels/BrowserPanel.swift#L3500-L3504: Clear the pending state unconditionally on the vacant-lease claim path as well.

This is a regression of a previously flagged issue that was introduced during the policy refactor. As per path instructions, portal ownership must remain derived from one authoritative, structured source without stale fallback state.

Proposed fixes

Sources/Panels/BrowserPanel.swift#L3452-L3458:

             if shouldReplace {
-                if consumesPendingDistinctReplacement {
-                    pendingDistinctPortalHostReplacementPaneId = nil
-                    lockedPortalHost = PortalHostLock(hostId: hostId, paneId: paneId.id)
-                } else if lockedPortalHost?.hostId == current.hostId &&
-                    lockedPortalHost?.paneId == current.paneId {
-                    lockedPortalHost = nil
-                }
+                pendingDistinctPortalHostReplacementPaneId = nil
+                if consumesPendingDistinctReplacement {
+                    lockedPortalHost = PortalHostLock(hostId: hostId, paneId: paneId.id)
+                } else if lockedPortalHost?.hostId == current.hostId &&
+                    lockedPortalHost?.paneId == current.paneId {
+                    lockedPortalHost = nil
+                }

Sources/Panels/BrowserPanel.swift#L3500-L3504:

-        if consumesPendingDistinctReplacement {
-            pendingDistinctPortalHostReplacementPaneId = nil
-            lockedPortalHost = PortalHostLock(hostId: hostId, paneId: paneId.id)
-        }
+        pendingDistinctPortalHostReplacementPaneId = nil
+        if consumesPendingDistinctReplacement {
+            lockedPortalHost = PortalHostLock(hostId: hostId, paneId: paneId.id)
+        }
         activePortalHostLease = next
🤖 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/Panels/BrowserPanel.swift` around lines 3452 - 3458, In the
ownership-change logic around the replacement path at
Sources/Panels/BrowserPanel.swift lines 3452-3458, clear
pendingDistinctPortalHostReplacementPaneId unconditionally before evaluating
consumesPendingDistinctReplacement, then set lockedPortalHost only when that
replacement consumed the pending request. Apply the same unconditional
pending-state clear on the vacant-lease claim path at
Sources/Panels/BrowserPanel.swift lines 3500-3504; retain the existing ownership
and lock behavior otherwise.

Source: Path instructions

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

Duplicate comments:
In `@Sources/Panels/BrowserPanel.swift`:
- Around line 3452-3458: In the ownership-change logic around the replacement
path at Sources/Panels/BrowserPanel.swift lines 3452-3458, clear
pendingDistinctPortalHostReplacementPaneId unconditionally before evaluating
consumesPendingDistinctReplacement, then set lockedPortalHost only when that
replacement consumed the pending request. Apply the same unconditional
pending-state clear on the vacant-lease claim path at
Sources/Panels/BrowserPanel.swift lines 3500-3504; retain the existing ownership
and lock behavior otherwise.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 57a0ed0f-979e-4146-8a96-574cfb48f936

📥 Commits

Reviewing files that changed from the base of the PR and between dab8d33 and e07fdc6.

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

@austinywang
austinywang merged commit c9f2d8c into main Jul 17, 2026
6 checks passed
azooz2003-bit pushed a commit that referenced this pull request Jul 18, 2026
…est heal

- Rows re-wrap continuously during divider/window resize: per width tick
  the visible pure-AppKit rows re-measure at the live width (manual frame
  math, bounded by viewport size); heightOfRow falls back to a
  content-matched entry at another width mid-drag, and the settle pass
  forces a full re-measure even when the drag ends where it started.
- Double-click rename: the single-click action fires for both clicks, so
  click 2's queued coalesced selection landed after the rename field took
  the field editor, re-activated the workspace, and end-editing committed
  the untouched title. didDoubleClickTableRow now drops the queued
  selection before beginning the edit, and logs whether the field took
  first responder.
- DEBUG probes across the rename/color paint chain (write, snapshot
  refresh, table apply, title paint, geometry-resize gate transitions) to
  localize the still-reported reactivity delay with evidence.
- Heal main: DockPortalReconcileTests calls
  preparePortalHostReplacementIfOwned without the instanceSerial that
  #8310 added (merged during the
  CI-advisory week); all four unit-test shards fail to compile on main.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
azooz2003-bit added a commit that referenced this pull request Jul 18, 2026
…cing, behavior tests (#8390)

* Lawrence Sidebar round 2, batch A: popover refresh, one-pass diff, explicit resize signal, metadata/markdown toggles

- Checklist popover refreshes while open: the row's configure pass now
  forwards fresh models into an open popover (update rebuilds content
  and resizes) instead of showing creation-time items until reopen.
- One equivalence pass per table apply: the height cache reuses the
  controller's reconfigure diff (skippingEquivalenceCheckAt) instead of
  re-running row equality over all 128 rows a second time.
- Explicit resize-completion: the portal registry posts
  cmuxInteractiveGeometryResizeDidEnd from its single end path (tracker,
  legacy gesture, and cursor failsafe all funnel there); the table
  re-measures immediately on it. The 120ms trailing task remains only as
  a fallback for width churn without an end signal (window live resize).
- Metadata show-more/less and markdown show-details toggles (legacy
  parity, same localized keys): expansion state is container-owned and
  flows through the model so heights re-measure via the normal apply.

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

* Add SidebarLayoutModel + width applier wrappers (unwired scaffolding)

Canonical width storage outside ContentView state so divider ticks stop
re-evaluating the whole window body; only the tiny applier wrappers
observe it. Wiring of the read/write sites follows in the next commit.

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

* Move sidebar width out of ContentView state (SidebarLayoutModel wiring)

Divider ticks no longer re-evaluate ContentView's body: canonical width
lives in an unobserved SidebarLayoutModel, and only the small
SidebarWidthReader / width-modifier wrappers observe it (sidebar panel,
terminal leading padding, resizer overlay, titlebar inset, chrome
border). Writes keep their call sites via a computed alias; the width
sanitizer moves from onChange to onReceive(removeDuplicates) since
ContentView no longer tracks the value in body. Behavior-identical
storage move; both flag paths keep the same values and layout.

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

* Settle-pass width handler + deterministic coalescer with unit tests

The width sanitizer's move to onReceive regressed drags: Combine
delivered it synchronously inside every width write (7.6ms/event
measured), running persistence and a portal resync per drag tick. The
handler now hops to the runloop, skips entirely mid-drag (the tracking
loop and portal anchor callbacks own live geometry), and the full
settle (sanitize/persist/portal resync/cursor band) runs once on the
registry's drag-end notification.

SidebarSelectionCoalescer becomes generic over Clock with all timing
from the injected clock, making it deterministic under test; adds
SidebarSelectionCoalescerTests (manual clock: leading edge, last-wins
trailing, quiet-window reset, cancel) wired into the pbxproj.

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

* Route the sidebar flag into the portal from its single evaluation site

lint-feature-flags requires one evaluating file per flag; the portal's
anchor-failsafe gate from round 1 read it directly. ContentView's
sidebar dispatcher (the legit site) now pushes a plain bool into
WindowTerminalPortal.usesCoalescedAnchorFailsafe on branch mount.

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

* No implicit animations in sidebar cells; group headers join the fast selection path

Dogfood video showed rails and text visibly interpolating during and
after divider resizes, and group-header clicks feeling like the old
selection path.

- noteHeightOfRows now runs in a zero-duration animation group (legacy
  never animates row geometry), and both cell types disable implicit
  layer actions in applyModel and manual layout — color and frame
  changes snap exactly like the SwiftUI sidebar.
- Group-header clicks route through the same coalescer as workspace
  rows (headers focus their anchor workspace), with an optimistic
  anchor-active press treatment and the same visible-row deselection
  sweep; chevron and plus presses are excluded (they don't select).

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

* Behavior tests for the AppKit row cell (hover enforcement, optimistic paint)

Fixture factory for SidebarWorkspaceRowModel plus a DEBUG applyModel
probe on the cell. Covers: hover enforcement short-circuits when
already correct and re-applies the full model otherwise; optimistic
selection paints a flipped model while the stored model stays
authoritative; optimistic deselection no-ops on unselected rows.

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

* Fix test compile: Foundation import + explicit continuation type

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

* Fix cmuxTests compile: hoist mutating gate calls out of #expect

The whole cmuxTests target failed to compile (blocking every unit test
run) because #expect captures its expression into a closure with an
immutable parameter, rejecting mutating calls on the captured var.
Pre-existing on main since the CI-advisory window; surfaced by the
first unit-test run against this branch.

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

* Allowlist pre-existing determinism findings so unit tests run again

WorkspaceForkConversationContextMenuTests landed 7 sleep/duration
findings during the CI-advisory window; the determinism gate fails the
whole pipeline before any unit test executes, on every branch.
Regenerated via check-test-determinism.py --write-allowlist (the gate's
migration path); fixing that test's waits properly stays with its
owners.

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

* Fix cmuxTests compile: fork-conversation test isolation and inference errors

Five compile errors landed with this file during the CI-advisory
window; since then the whole cmuxTests target has failed to build on
CI's own toolchain, so no unit test in the repo could run. Mechanical
fixes: explicit continuation element type; two main-actor local
functions converted to @sendable closures (they're called from
@sendable indexLoader closures); a main-actor snapshot builder hoisted
out of a withLock closure; a discarded Set.insert result inside
withLock to settle generic inference.

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

* Self-healing row freeze: rebuild once at drag end (stale-rename fix)

Dogfood report: sidebar value updates (renames especially) sometimes
never rendered. Root cause: row building freezes while the interactive
resize registry is active (sidebar AND split-divider drags), so an
apply during a drag serves frozen rows and consumes the fresh content
without rendering it — stale until the next unrelated sidebar change.
The scroll area now invalidates the frozen box and forces one fresh
rebuild on the registry's drag-end notification, so mid-drag mutations
always render. The rename data path itself was verified sound
(customTitle is @published and in the sidebar observation composition).

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

* Zero out the five over-budget Swift warnings from the fork/attachment files

The warning-budget gate has per-file zero budgets for these buckets;
the warnings landed during the CI-advisory window. Mechanical: two
redundant awaits on same-actor calls, one var never mutated, two unused
guard bindings replaced with nil tests (identical short-circuit
semantics), and an explicit 'as Any' for the QLPreviewPanel! coercion.

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

* Fix the remaining QLPreviewPanel coercion warning (second call site)

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

* Deliver sidebar observation on DispatchQueue.main (modal/menu stall fix)

Dogfood reports: renames (the Cmd+Shift+R flow especially) and workspace
color changes reached the sidebar UI with long delays. RunLoop.main as a
Combine scheduler delivers only in the default runloop mode, so every
hop in the sidebar observation pipeline (container observations, merged
extension stream, per-cell pump) stalled during modal panels, context
menus, and drag tracking - exactly where renames and color picks happen.
The snapshot-refresh coalescer beneath already used .common modes; all
publisher hops now schedule on DispatchQueue.main, which is
runloop-mode-agnostic. customTitle, customColor, description, pin, and
todo state all ride these publishers, so one fix covers both reports.

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

* Route indexnow jobs through vars.LINUX_RUNNER

The runner guard forbids bare GitHub-hosted runners; indexnow.yml landed
on main with ubuntu-latest during the CI-advisory week and fails
workflow-guard-tests on every gate run.

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

* Round-2 dogfood fixes: live width reflow, double-click rename, main test heal

- Rows re-wrap continuously during divider/window resize: per width tick
  the visible pure-AppKit rows re-measure at the live width (manual frame
  math, bounded by viewport size); heightOfRow falls back to a
  content-matched entry at another width mid-drag, and the settle pass
  forces a full re-measure even when the drag ends where it started.
- Double-click rename: the single-click action fires for both clicks, so
  click 2's queued coalesced selection landed after the rename field took
  the field editor, re-activated the workspace, and end-editing committed
  the untouched title. didDoubleClickTableRow now drops the queued
  selection before beginning the edit, and logs whether the field took
  first responder.
- DEBUG probes across the rename/color paint chain (write, snapshot
  refresh, table apply, title paint, geometry-resize gate transitions) to
  localize the still-reported reactivity delay with evidence.
- Heal main: DockPortalReconcileTests calls
  preparePortalHostReplacementIfOwned without the instanceSerial that
  #8310 added (merged during the
  CI-advisory week); all four unit-test shards fail to compile on main.

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

* Mount sidebar observations on the shared scroll-area parent

Root cause of the delayed rename/color reactivity, proven with the
debug probes: every workspace publisher observation
(sidebarWorkspaceObservations, process-title, agent-runtime) plus the
initial refreshWorkspaceSnapshots hung off legacyWorkspaceScrollArea's
view chain. With the AppKit sidebar flag on that subtree never mounts,
so no workspace publisher was observed at all: workspaceSnapshotsById
stayed empty, renames/colors/pins/descriptions produced no sidebar
invalidation, and rows only repainted when an unrelated change rebuilt
the body (probe: title write 19:25:01.279, paint 19:25:08.481, zero
snapshot flushes in between). The observation block now lives on the
shared parent Group so both implementations use one refresh path.

Also: a fast row drag consumed the press without any selection commit,
so the optimistic press highlight lingered on the grabbed row and every
other visible row stayed peeled; drag-session begin now drops the
queued selection and restores visible cells from stored models.

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

* Address review: peel header optimistic highlight, width settle on main queue

CodeRabbit: previewSelection's peel loop only reset workspace cells, so
a pending group-header preview replaced by a new press kept its
anchor-active paint until its model next changed. Headers now clear via
clearOptimisticAnchorActive (re-applies the stored model).

Greptile: the width-settle onReceive still hopped through RunLoop.main;
same default-mode stall class this PR fixes elsewhere, now
DispatchQueue.main.

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

---------

Co-authored-by: cmux reload-cloud <cmux-reload-cloud@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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