Ccmux reload-cloudtest: isolate goto split config from user state
| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
Fix browser Space key canonicalization (#8079) * test: cover canonical browser Space events * fix: canonicalize browser keyboard events * fix: use DOM legacy modifier key codes * docs: align browser keyboard command syntax --------- Co-authored-by: cmux reload-cloud <cmux-reload-cloud@users.noreply.github.com> | 2 个月前 | |
Stop XCTest crashes from reaching Sentry (#8786) * test: cover Sentry startup under XCTest * fix: disable macOS Sentry under XCTest * Make Sentry startup policy constructable * test: cover sandbox-denied CLI socket telemetry * fix: drop sandbox-denied CLI socket telemetry * fix: allow explicit Sentry opt-in under XCTest * Address Sentry startup review policy * Close Sentry test launch review gaps * Require provenance for Sentry suppression * Document Sentry suppression contract --------- Co-authored-by: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> | 2 个月前 | |
Stop XCTest crashes from reaching Sentry (#8786) * test: cover Sentry startup under XCTest * fix: disable macOS Sentry under XCTest * Make Sentry startup policy constructable * test: cover sandbox-denied CLI socket telemetry * fix: drop sandbox-denied CLI socket telemetry * fix: allow explicit Sentry opt-in under XCTest * Address Sentry startup review policy * Close Sentry test launch review gaps * Require provenance for Sentry suppression * Document Sentry suppression contract --------- Co-authored-by: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> | 2 个月前 | |
Stop XCTest crashes from reaching Sentry (#8786) * test: cover Sentry startup under XCTest * fix: disable macOS Sentry under XCTest * Make Sentry startup policy constructable * test: cover sandbox-denied CLI socket telemetry * fix: drop sandbox-denied CLI socket telemetry * fix: allow explicit Sentry opt-in under XCTest * Address Sentry startup review policy * Close Sentry test launch review gaps * Require provenance for Sentry suppression * Document Sentry suppression contract --------- Co-authored-by: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> | 2 个月前 | |
Fix browser automation recovery after load failures (#8548) * test: cover browser recovery navigation commit * test: require browser navigation commit barrier * fix: await browser navigation commits * fix: bound browser navigation transaction state * fix: preserve browser navigation semantics * fix: hand off browser navigation transactions * fix: cover browser navigation handoff outcomes * fix: validate deferred browser navigation targets * fix: complete browser download navigations * fix: preserve browser navigation results * fix: correlate browser navigation terminal paths * fix: preserve browser policy navigation identity * fix: distinguish browser navigation policy signals * fix: sanitize browser navigation failures * fix: correlate deferred browser navigation handoffs * fix: bound browser navigation outcome ownership * fix: separate same-document navigation signals * fix: correlate browser policy outcomes exactly * fix: normalize browser navigation targets --------- Co-authored-by: cmux reload-cloud <cmux-reload-cloud@users.noreply.github.com> | 2 个月前 | |
Stop XCTest crashes from reaching Sentry (#8786) * test: cover Sentry startup under XCTest * fix: disable macOS Sentry under XCTest * Make Sentry startup policy constructable * test: cover sandbox-denied CLI socket telemetry * fix: drop sandbox-denied CLI socket telemetry * fix: allow explicit Sentry opt-in under XCTest * Address Sentry startup review policy * Close Sentry test launch review gaps * Require provenance for Sentry suppression * Document Sentry suppression contract --------- Co-authored-by: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> | 2 个月前 | |
Stop XCTest crashes from reaching Sentry (#8786) * test: cover Sentry startup under XCTest * fix: disable macOS Sentry under XCTest * Make Sentry startup policy constructable * test: cover sandbox-denied CLI socket telemetry * fix: drop sandbox-denied CLI socket telemetry * fix: allow explicit Sentry opt-in under XCTest * Address Sentry startup review policy * Close Sentry test launch review gaps * Require provenance for Sentry suppression * Document Sentry suppression contract --------- Co-authored-by: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> | 2 个月前 | |
Stop XCTest crashes from reaching Sentry (#8786) * test: cover Sentry startup under XCTest * fix: disable macOS Sentry under XCTest * Make Sentry startup policy constructable * test: cover sandbox-denied CLI socket telemetry * fix: drop sandbox-denied CLI socket telemetry * fix: allow explicit Sentry opt-in under XCTest * Address Sentry startup review policy * Close Sentry test launch review gaps * Require provenance for Sentry suppression * Document Sentry suppression contract --------- Co-authored-by: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> | 2 个月前 | |
Fix browser automation recovery after load failures (#8548) * test: cover browser recovery navigation commit * test: require browser navigation commit barrier * fix: await browser navigation commits * fix: bound browser navigation transaction state * fix: preserve browser navigation semantics * fix: hand off browser navigation transactions * fix: cover browser navigation handoff outcomes * fix: validate deferred browser navigation targets * fix: complete browser download navigations * fix: preserve browser navigation results * fix: correlate browser navigation terminal paths * fix: preserve browser policy navigation identity * fix: distinguish browser navigation policy signals * fix: sanitize browser navigation failures * fix: correlate deferred browser navigation handoffs * fix: bound browser navigation outcome ownership * fix: separate same-document navigation signals * fix: correlate browser policy outcomes exactly * fix: normalize browser navigation targets --------- Co-authored-by: cmux reload-cloud <cmux-reload-cloud@users.noreply.github.com> | 2 个月前 | |
Fix browser automation recovery after load failures (#8548) * test: cover browser recovery navigation commit * test: require browser navigation commit barrier * fix: await browser navigation commits * fix: bound browser navigation transaction state * fix: preserve browser navigation semantics * fix: hand off browser navigation transactions * fix: cover browser navigation handoff outcomes * fix: validate deferred browser navigation targets * fix: complete browser download navigations * fix: preserve browser navigation results * fix: correlate browser navigation terminal paths * fix: preserve browser policy navigation identity * fix: distinguish browser navigation policy signals * fix: sanitize browser navigation failures * fix: correlate deferred browser navigation handoffs * fix: bound browser navigation outcome ownership * fix: separate same-document navigation signals * fix: correlate browser policy outcomes exactly * fix: normalize browser navigation targets --------- Co-authored-by: cmux reload-cloud <cmux-reload-cloud@users.noreply.github.com> | 2 个月前 | |
Stop XCTest crashes from reaching Sentry (#8786) * test: cover Sentry startup under XCTest * fix: disable macOS Sentry under XCTest * Make Sentry startup policy constructable * test: cover sandbox-denied CLI socket telemetry * fix: drop sandbox-denied CLI socket telemetry * fix: allow explicit Sentry opt-in under XCTest * Address Sentry startup review policy * Close Sentry test launch review gaps * Require provenance for Sentry suppression * Document Sentry suppression contract --------- Co-authored-by: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> | 2 个月前 | |
Stop XCTest crashes from reaching Sentry (#8786) * test: cover Sentry startup under XCTest * fix: disable macOS Sentry under XCTest * Make Sentry startup policy constructable * test: cover sandbox-denied CLI socket telemetry * fix: drop sandbox-denied CLI socket telemetry * fix: allow explicit Sentry opt-in under XCTest * Address Sentry startup review policy * Close Sentry test launch review gaps * Require provenance for Sentry suppression * Document Sentry suppression contract --------- Co-authored-by: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> | 2 个月前 | |
Stop XCTest crashes from reaching Sentry (#8786) * test: cover Sentry startup under XCTest * fix: disable macOS Sentry under XCTest * Make Sentry startup policy constructable * test: cover sandbox-denied CLI socket telemetry * fix: drop sandbox-denied CLI socket telemetry * fix: allow explicit Sentry opt-in under XCTest * Address Sentry startup review policy * Close Sentry test launch review gaps * Require provenance for Sentry suppression * Document Sentry suppression contract --------- Co-authored-by: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> | 2 个月前 | |
Stop XCTest crashes from reaching Sentry (#8786) * test: cover Sentry startup under XCTest * fix: disable macOS Sentry under XCTest * Make Sentry startup policy constructable * test: cover sandbox-denied CLI socket telemetry * fix: drop sandbox-denied CLI socket telemetry * fix: allow explicit Sentry opt-in under XCTest * Address Sentry startup review policy * Close Sentry test launch review gaps * Require provenance for Sentry suppression * Document Sentry suppression contract --------- Co-authored-by: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> | 2 个月前 | |
Stop XCTest crashes from reaching Sentry (#8786) * test: cover Sentry startup under XCTest * fix: disable macOS Sentry under XCTest * Make Sentry startup policy constructable * test: cover sandbox-denied CLI socket telemetry * fix: drop sandbox-denied CLI socket telemetry * fix: allow explicit Sentry opt-in under XCTest * Address Sentry startup review policy * Close Sentry test launch review gaps * Require provenance for Sentry suppression * Document Sentry suppression contract --------- Co-authored-by: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> | 2 个月前 | |
CI: bound activation scrollback fixture (#6241) * CI: bound activation scrollback fixture * CI: validate activation benchmark inputs * CI: sanitize restored tool caches * CI: make tool cache cleanup non-destructive * CI: split Zig install from GhosttyKit build * CI: split iOS Zig install from GhosttyKit setup * CI: keep local Zig install relocatable * CI: harden activation benchmark launch * CI: fix Zig fallback default root * CI: handle no-sudo activation benchmark * CI: constrain local Zig install root * CI: harden macOS UI display setup * CI: preserve display helper across retries * CI: isolate activation and UI display harnesses * CI: wait for display churn helper cleanup * CI: isolate Zig install scratch directory * CI: serialize virtual display usage * CI: preserve virtual display lock ownership * CI: guard virtual display lock checks * CI: reclaim dead virtual display locks * CI: retry virtual display setup * CI: run UI regressions on GUI runner * CI: reject broken Zig installs * CI: preserve virtual display locks for foreign owners * CI: validate sudo Zig install layout * CI: run lag display checks on Depot * CI: harden browser find socket readiness * CI: share UI test socket fallback * CI: reclaim ownerless virtual display locks * CI: accept app-side socket readiness in browser UI test * CI: add JSON fallback for browser UI socket RPCs | 3 个月前 | |
Deflake display resolution liveness UI test (#7062) * Deflake display resolution liveness UI test * Refresh Swift file length budget * Harden display churn UI liveness check * Gate display churn on XCTest baseline marker * Tolerate transient final render diagnostics loss * Refresh display UI test length budget * Anchor display liveness recency to churn completion * Require final display render diagnostics * Use present timestamp for display churn liveness * Bound display liveness recency window | 3 个月前 | |
Stop XCTest crashes from reaching Sentry (#8786) * test: cover Sentry startup under XCTest * fix: disable macOS Sentry under XCTest * Make Sentry startup policy constructable * test: cover sandbox-denied CLI socket telemetry * fix: drop sandbox-denied CLI socket telemetry * fix: allow explicit Sentry opt-in under XCTest * Address Sentry startup review policy * Close Sentry test launch review gaps * Require provenance for Sentry suppression * Document Sentry suppression contract --------- Co-authored-by: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> | 2 个月前 | |
Stop XCTest crashes from reaching Sentry (#8786) * test: cover Sentry startup under XCTest * fix: disable macOS Sentry under XCTest * Make Sentry startup policy constructable * test: cover sandbox-denied CLI socket telemetry * fix: drop sandbox-denied CLI socket telemetry * fix: allow explicit Sentry opt-in under XCTest * Address Sentry startup review policy * Close Sentry test launch review gaps * Require provenance for Sentry suppression * Document Sentry suppression contract --------- Co-authored-by: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> | 2 个月前 | |
test: isolate goto split config from user state | 2 个月前 | |
Stop XCTest crashes from reaching Sentry (#8786) * test: cover Sentry startup under XCTest * fix: disable macOS Sentry under XCTest * Make Sentry startup policy constructable * test: cover sandbox-denied CLI socket telemetry * fix: drop sandbox-denied CLI socket telemetry * fix: allow explicit Sentry opt-in under XCTest * Address Sentry startup review policy * Close Sentry test launch review gaps * Require provenance for Sentry suppression * Document Sentry suppression contract --------- Co-authored-by: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> | 2 个月前 | |
Stop XCTest crashes from reaching Sentry (#8786) * test: cover Sentry startup under XCTest * fix: disable macOS Sentry under XCTest * Make Sentry startup policy constructable * test: cover sandbox-denied CLI socket telemetry * fix: drop sandbox-denied CLI socket telemetry * fix: allow explicit Sentry opt-in under XCTest * Address Sentry startup review policy * Close Sentry test launch review gaps * Require provenance for Sentry suppression * Document Sentry suppression contract --------- Co-authored-by: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> | 2 个月前 | |
Stop XCTest crashes from reaching Sentry (#8786) * test: cover Sentry startup under XCTest * fix: disable macOS Sentry under XCTest * Make Sentry startup policy constructable * test: cover sandbox-denied CLI socket telemetry * fix: drop sandbox-denied CLI socket telemetry * fix: allow explicit Sentry opt-in under XCTest * Address Sentry startup review policy * Close Sentry test launch review gaps * Require provenance for Sentry suppression * Document Sentry suppression contract --------- Co-authored-by: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> | 2 个月前 | |
Stop XCTest crashes from reaching Sentry (#8786) * test: cover Sentry startup under XCTest * fix: disable macOS Sentry under XCTest * Make Sentry startup policy constructable * test: cover sandbox-denied CLI socket telemetry * fix: drop sandbox-denied CLI socket telemetry * fix: allow explicit Sentry opt-in under XCTest * Address Sentry startup review policy * Close Sentry test launch review gaps * Require provenance for Sentry suppression * Document Sentry suppression contract --------- Co-authored-by: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> | 2 个月前 | |
Stop XCTest crashes from reaching Sentry (#8786) * test: cover Sentry startup under XCTest * fix: disable macOS Sentry under XCTest * Make Sentry startup policy constructable * test: cover sandbox-denied CLI socket telemetry * fix: drop sandbox-denied CLI socket telemetry * fix: allow explicit Sentry opt-in under XCTest * Address Sentry startup review policy * Close Sentry test launch review gaps * Require provenance for Sentry suppression * Document Sentry suppression contract --------- Co-authored-by: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> | 2 个月前 | |
Stop XCTest crashes from reaching Sentry (#8786) * test: cover Sentry startup under XCTest * fix: disable macOS Sentry under XCTest * Make Sentry startup policy constructable * test: cover sandbox-denied CLI socket telemetry * fix: drop sandbox-denied CLI socket telemetry * fix: allow explicit Sentry opt-in under XCTest * Address Sentry startup review policy * Close Sentry test launch review gaps * Require provenance for Sentry suppression * Document Sentry suppression contract --------- Co-authored-by: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> | 2 个月前 | |
Fix remote tmux seed/live output ordering (#8436) * Add failing remote tmux seed transport regression * Make remote tmux pane seeding transport-independent * Simplify remote tmux seed capture state * Add failing single-pane seed grow regression * Repaint remote tmux panes after verified grid growth * Fix remote tmux seed test actor isolation * Add failing remote tmux blank-row parser regression * Preserve blank rows in remote tmux captures * Add failing remote tmux reconnect cutover regressions * Reset remote tmux output cursor before pane seeds * Add failing quoted pane cursor reset regression * Quote remote tmux pane cursor reset arguments * Add failing remote tmux repaint coalescing regression * Coalesce remote tmux pane repaint seeds * Add failing remote tmux grid-growth history regression * Gate remote tmux repaint until grid growth applies * Add failing exited pane seed regression * Avoid reconnecting when seeded pane exits * Add failing reconnect history boundary regression * Avoid reconnect redraw kick after pane reseed * Add failing safe pane reseed regressions * Make remote tmux pane reseeds backlog safe * Add failing bounded reconnect seed regressions * Bound remote tmux seed recovery * Add failing remote tmux seed replacement regressions * Fix remote tmux seed cutover ordering --------- Co-authored-by: cmux reload-cloud <cmux-reload-cloud@users.noreply.github.com> | 2 个月前 | |
Fix tmux mirror pane sizing and divider drag synchronization (#7996) * remote-tmux: revive remote.tmux.window as a DEBUG verb so the sizing UI suite can attach The sizing UI suite attaches its lab host through remote.tmux.window, but the handler was removed when mirrors moved into the current window while the verb stayed in the socket policy's worker list — every call hit the loud policy/handler-drift backstop, and the suite has been unrunnable since. The user-facing path is cmux ssh-tmux with its foreground auth handoff, so the raw verb comes back as DEBUG-only test tooling beside test_exec and test_set_frame: a thin alias of remote.tmux.mirror with the retired verb's activate-by-default contract. Release builds now answer method_not_found instead of an internal error. * remote-tmux: render ownership — drag sessions, exact-fit render frames, oversized-reading drops Ports the render-ownership line onto the sizing-transaction rework so the two compose instead of fighting. One geometry writer per split: divider drags are deterministic sessions (bonsplit's mouse-tracking lifecycle), sizing passes hold while a session is live — the hold sits ahead of the pass's intent consumption, so a held recovery pass keeps its intent for drag end — and the drag-end sync is cell-aware: a sub-cell drag rounds to the span tmux already holds, gets no reply, and re-imposes locally through the constraint-recovery intent instead of waiting for one. Bonsplit's retryImposedFirstExtent now refuses while a drag session owns the divider, so recovery passes cannot yank the divider mid-gesture (vendor bump: divider-drag-sessions rebased onto the imposed-first-extent line). The split tree renders at its exact grid-plus-chrome size, top leading, with the region's sub-cell remainder outside the tree as trailing margin; the plan and the drag-fraction reads both use that frame as the denominator. Per the newest upstream change, the drag walk descends with applied extents, not the imposed values. A changed render frame lands a commit after the pass that computed it, so the pass restates the plan once, two turns out. Container readings keep the pending-stash architecture and add two pieces: a probe view planted in the mirror's own subtree is the primary window bound (it survives portal churn that can leave every pane view detached mid-sync), and a reading that exceeds the bound with a size already on record is dropped rather than clamped — clamping banked the bound itself, overstating the region by the window-to-mirror chrome (measured ~30-40pt wide plans at rest in the live fuzz). First-ever readings still clamp so the initial claim exists. The title-row fold fix from the old line is NOT ported: the metrics' edge-placement accounting already matches the measured tmux 3.7 facts (interior title rows are the coordinate gaps between panes; only edge-touching panes carry an extra title charge), and zeroing the separator credit on top of it would double-count. The shared renderedCells points-to-cells helper moves into the package, and a DEBUG parity probe compares each off-assignment sample against what the plan expected through it. The portal skips already-hidden surfaces in bulk geometry syncs, logs only syncs that changed something, and carries a live-resize-gated tripwire naming programmatic window growth. Tests: ownership and exact-fit suites adapted to the planner API, drag-session deferral coverage, oversized-reading drop coverage, the clip-aware many-tabs render test, and the live-fuzz ruler lengthened so panes wider than 609 columns measure honestly. * remote-tmux: regression test — a stale imposed render frame must not inflate the mirror view's reported size * remote-tmux: keep the imposed render frame out of the mirror view's reported size The mirror sized its split tree inline: the imposed grid-plus-chrome frame sat inside a flexible frame, and a flexible frame with no minWidth reports its child's width whenever the child exceeds the proposal. The plan is derived from the banked container while the proposal comes from the live window, so any window shrink left the tree momentarily wider than its region — and that width, instead of overflowing in place, became the view's reported size. Every space-filling ancestor up to the main window's root content inherited it (the content view was observed marching wider than the display-pinned window a step per layout pass), and the mirror's geometry callback then read its own imposed width back as its container, which the oversized-reading guard could only defer, never cure. Render the tree in a top-leading overlay of a region-sized base instead. The base answers every proposal with the proposal, the tree still renders at its exact imposed size (overflowing in place during the transient), and the geometry callback now reads the true region. Also pin the hosting-view side in MainWindowSelfSizingTests: content that over-reports its size must not move the content view's own frame off the window. * remote-tmux: regression test — a stale wide bank must heal through the full pane chain The live growth spiral froze into a state nothing could cure: the mirror banked its container while wide, the window shrank, and the mirror kept rendering the stale-wide imposed tree in a fraction of the space forever. Host the REAL wrapper chain at the desk — main-window hosting view, workspace bonsplit pane, and the mirror view with its live geometry feedback — bank wide, mount into a narrower window, and require the bank to heal to it. Red on purpose: the first geometry callback can run before the probe is in a window, resolveContainerReading defers the reading, and nothing retries — the deferral neither schedules a pass nor gets re-validated once a window exists (the pass-time re-clamp only looks for a window through panelsByPaneId, which portal churn and hidden tabs empty). * remote-tmux: hold a window-less container reading and re-validate it at pass time A geometry reading and the window that validates it are sampled at different times: the first reading often arrives a beat before the probe is in a visible window, and if the region never changes size again no later callback comes. The pending-container stash already holds window-less readings and schedules the pass that adopts them; what could still rot was the recovery pass going blind — it found windows only through live panels, which portal churn and hidden tabs empty. The pass resolves its bound through the probe view planted in the mirror's own subtree, and an injected hosting-bound source that answers nil now falls through to the live probe and pane scan instead of pinning the channel, so the full-chain regression test can seed a stale bank and then hand the mirror to a real window. Two stronger liveness edges — scheduling a pass from the probe's own move-to-window, and sampling the probe's live frame against its window's bound in the same instant — were built and then dropped: the full-chain heal test passes without them on this architecture (verified by running it against the tree with only the stash and the probe-resolved bound). The design doc states the surviving rule: a deferral is only safe with a retry edge, and stashing IS the retry edge here. It also records the render-ownership amendment that re-imposing an identical extent still re-arms one apply. * remote-tmux: frame oracle for the sizing UI suite — root content must hold the window's width The growth spiral kept every tmux-side claim sane while the window's content view marched a step wider per layout pass — the oversized-reading guard dropped the inflated readings, so the grid oracle stayed green throughout. Only the frames can convict that class. remote.tmux.root_frames (DEBUG) reports, for every visible mirror, the hosting window's frame and content-view sizes plus the widths of the mirror's real ancestor chain, probe to root — the tripwire the debug log prints (mirror.container.ancestors), as data. The new scenario shrinks the window under a live imposed plan — the state where the imposed render frame exceeds the region until the next pass — and requires both oracles after each step: sizing settles to exact renders, and the whole ancestor chain holds the window's width. * remote-tmux: sizing suite — hidden-mirror lifecycle scenario and a window ceiling on claims Two situations the fuzz marathon hit that the suite never pinned. A mirror attached while another workspace holds the front takes its first container reading with no visible window to vouch for it, and the live wedge froze exactly there: a deferred reading was dropped, no later callback came, and the mirror rendered a stale-wide tree forever with every claim looking sane. The new scenario drives that lifecycle deterministically — attach with activate=false behind the launch workspace, one test_set_frame resize while hidden, workspace.select to reveal — and holds the reveal to a full settle: exact grids, root content at the window's width, claims under the ceiling. The ceiling is the other gap. A claim is a pure function of window geometry, so claimed cols/rows may never exceed what the hosting window's content area divides to at the calibrated cell size, plus two cells of slack for rounding and the chrome model's separator credit. The wedge pushed 318-column claims against a 248-column layout, and older evidence reached 2614 columns, all while every grid check passed: tmux clamps the layout and the mirror renders the clamped truth, so only the claim itself shows the runaway. The oracle reads claims from sizing_settled, the bound from root_frames, and the cell size from pane_grids calibration; both the new scenario and the shrink scenario assert it after every settle. * remote-tmux: chrome folds credit the coordinate gaps tmux actually assigned The residual folds assumed one separator cell per child boundary and charged edge panes a native title row. Both assumptions break on titled trees: tmux's title rows live in the tree's COORDINATES — the gaps between siblings and the window-edge row — and cost the native render nothing, while the per-pane charge granted the edge pane points nothing native renders, so the surface floored them into a phantom grid row (the rows3-at-1000 sweep failure: pane %6 planned 237pt for an 11-row assignment, rendered 12). Every fold now reads the actual gap cells off the assignment — parent span minus child spans — so a node's extent equals its children's sum by construction, titled or not, and the binary measured fold threads the same gap count so it agrees with the n-ary fold node for node. The binarized tree keeps the ORIGINAL node at the top of each joined group (the synthesized join spans only the children's bounds and lost the window-edge row); structure-only placeholders without spans fall back to the one-cell-per-boundary reading. The measured tree also gains span-derived minimums (minimumSpan, clampToFeasibleFirstSpan) for the drag-send feasibility rule. The parity probe stops subtracting title rows from planned outers — they no longer carry any. Package tests pin the new accounting and the fold agreement. * remote-tmux: clamp drag sends to the grid-feasible range; harden probe registration and drag-end deferral A drag past the sibling's minimum converts to a span tmux cannot assign; like a sub-cell nudge, the resize-pane changes no layout and never gets the reply drag-end waits for — the divider parks off-grid while later passes early-return on unchanged inputs. Drag sends now clamp to the split's feasible first-span range (read off the measured tree's assigned spans), and a request that clamps back to the assigned span routes to the immediate local re-impose instead. The probe view only clears its mirror slot if it is still the registrant: a tab re-show recreates the probe, and AppKit delivers the dying probe's move-to-nil-window after the replacement registered — claiming there shadowed the live probe's window handle with a windowless view. And drag-end consumes the deferred-pass flag before the remote-apply guard, rescheduling the held pass instead of stranding it when a drag ends mid-apply. * remote-tmux: fold-equivalence fuzz, feasibility clamp tests, probe shadowing test The chrome-fold equivalence fuzz pins that the binary measured fold and the n-ary residual fold agree on every node, titled or not — the disagreement class whose size lands on whichever pane the rail allocation starves. The feasibility tests pin the drag-send clamp (starved sibling, nested same-axis minimums, cross-axis overlay, packed titled stacks) plus the parked-divider desk repro with its feasible-drag control, the mid-apply drag-end deferral consumption, and the dying-probe shadowing sequence. * remote-tmux: the claim charges rail slack so every claimed cell is placeable The tight-container fuzz has been red at its boundary the whole time: shave a container to a slack-free claim's edge and the whole-point rail cannot give every pane its cells — with fractional chrome, one side of some split lands a device pixel under a cell boundary and the surface floors the cell away. The minimum-preserving rail allocation cannot save it: when both subtrees' chrome-only minimums are fractional and the container holds exactly their sum, no whole-point boundary leaves both sides at or above their minimums — someone is short by under a point, and under a point is a dropped cell at a boundary. So the claim pays for placement up front again: clientGrid charges the per-pane rail-slack point along with real chrome, claiming at most one column and one row fewer at boundary sizes, and the plan can then place every claimed cell honestly. Claim pins across the tests move by that one point; the planner's minimum-aware allocation stays as the degradation path for containers that shrink mid-flight below even the slack-inclusive claim. * remote-tmux: settle verb judges against the plan the renderer actually imposed The settlement payload re-ran the planner at the raw container size, but the render path plans at the exact-fit render frame — so the judge compared live views against a plan the renderer no longer produces and could report unsettled forever whenever the region's sub-cell remainder exceeded the 1.5pt tolerance. Read the sizing pass's own stashed outer sizes instead, and only re-plan (at the render frame, container as the last resort) when no pass has stashed a plan yet. * remote-tmux: tighten shared sizing helpers and DEBUG gating No behavior changes outside DEBUG logging and test plumbing; the existing suites pin every refactored path. - One assignedGapCells helper computes the assigned-gap cells (parent span minus child spans) for the n-ary residual fold, the measured tree's binary fold, and the minimum-span walk. Each site keeps its own degenerate-span fallback, passed as a parameter. - One exactFitSize helper computes the grid-plus-residual point size for the render frame and the five exact-fit test sites. - Drop sizingPassDeferredForDrag. performSizingPassNow already no-ops on unchanged inputs, so drag end can schedule a pass unconditionally instead of tracking a deferral flag; the drag tests now assert the hold and the reschedule through the scheduler state itself. - Drop the dead paneTitleRowHeight parameter from renderedCells; every caller passed zero. - Share one host-probe ancestor-chain walk between the growth-spiral tripwire log and remote.tmux.root_frames, dump the chain once per window instead of once per dropped reading, and merge the two adjacent DEBUG blocks in noteContainerSize into one symmetric width-or-height check. - Log remote.divider.plan only when the planned outers change. - Drop the remote.tmux.window alias verb (dispatch case and execution policy entry included); the UI suite attaches through remote.tmux.mirror with an explicit activate flag. - UI suite: merge the two attach helpers into attachSession(activate:), share one pane_grids fetch/unwrap, and make the root-frames oracle fail closed when width fields are missing instead of defaulting both sides of the comparison to -1. - Share the stale-wide-bank mirror fixture between the two imposition render wedge tests. - MirrorHostProbeView returns nil from hitTest: it backs the whole mirror region, including the sub-cell margin outside the split tree, and must not swallow clicks there. * remote-tmux: pin re-convergence when applied geometry misses the plan without an input change The sizing transaction proves convergence from its inputs alone: once a completed pass's inputs match the current inputs, every later trigger early-returns. An apply that terminates off-target with no input change (bonsplit parking a divider at a minimum, a retry budget expiring against mid-commit bounds) therefore never gets corrected — the live fuzz held a 1199pt plan against a 984pt view for 50+ seconds while every trigger reported settled. This test hosts a two-pane mirror through the real render chain, lets it converge to its own plan, moves the divider programmatically (geometry changes, no sizing input does), delivers a redundant trigger, and asserts plan == view re-converges. It fails today: the pass early-returns and the views stay off-plan indefinitely. * remote-tmux: verify outcome parity after applying and re-arm a bounded pass on a miss The transaction now checks its own output. Two runloop turns after a completed visible pass — and after any trigger that early-returns on the input fixed point — it compares the outer sizes the last imposition granted against the hosted views' actual frames (the settle payload's comparison, same 1.5pt tolerance). When they disagree, it requests one pass that ignores the settled check, so the plan gets re-imposed from the same inputs. The re-arm is capped at three per input fixed point (the counter resets when inputs change), so an extent bonsplit genuinely cannot hold stops after a bounded correction instead of looping. lastPlannedOuterSizes moves out of DEBUG: it is now the plan side of a release code path, not just the debug parity probe. The fallback branch (no metrics) clears it, since the fraction fallback is not a plan views can be judged against. The design doc records the rule: an apply may never terminate off-target without a re-arm edge, and healthy settle latency is sub-second (the harnesses fail at 8 seconds rather than waiting out a stall). * remote-tmux: fail harnesses at an 8s settle budget and capture evidence at fail time A healthy settle cycle measures ~0.4s end to end, but the live fuzz waited up to 50s (10x2s poll plus 15x2s reconfirm) before calling a window unsettled — long enough for the app to argue its way out of real stalls, which is how a 1199pt-plan-vs-984pt-view wedge rode through a marathon unnoticed. check_iter now polls every second and fails hard at 8s, printing per-iteration settle latency; only the mismatch-while-settled branch keeps a short two-poll reconfirm, because a probe can race the last frame of a transition. The attach barrier is unchanged — reconnect convergence is a separate measurement. note_fail snapshots the debug-log tail (600 lines) the moment a failure is noted, one file per failure in the evidence dir. The marathon's end-of-seed capture stays, but it provably never covers mid-seed failures: both stall captures from the last marathon contained zero stall lines because later iterations had churned the log past the window. The sizing UI suite drops its 10/15/25s budgets to 5s for steady-state assertions and 10s for attach and reveal, and prints each measured settle against its budget. A new scenario pins the liveness rule end to end: a DEBUG verb (remote.tmux.test_perturb_divider) knocks the visible mirror's root divider off the imposed plan without sending anything to tmux — no layout echo, no input change — and the suite asserts the mirror re-converges inside the steady-state budget. * remote-tmux: pin bonsplit at the bounded imposed-divider re-arm Bonsplit's side of the same liveness rule: a container resize that parks an imposed divider now re-arms one bounded apply inside bonsplit (in both the sync path and the pure-AppKit resize path, which never reaches sync). The two edges are complementary — bonsplit re-arms on its own size change, and the mirror's outcome-parity check covers every other way an apply can terminate off-target. * remote-tmux: pin the re-judgment of a reading dropped against a torn window bound During an AppKit window resize, SwiftUI can deliver the correct post-resize slot reading while the window's transient frame still holds the old bound. The oversized guard judges the reading against that torn bound and discards it permanently — geometry callbacks only re-fire when the region changes again, so the mirror freezes at the pre-resize size. The pass-top clamp cannot recover the width either: the window bound overstates the mirror slot by the sidebar. Red on this commit, replaying the fail-time log values: seed 1549x819 against bound 1789x875, deliver 1334x593 against the torn 1250x583 bound, settle the bound at 1574x617 and drain the pass. The container stays at the re-clamped 1549x617 and the claim pushes 193 cols instead of 166: Expectation failed: (mirror.containerSizePt -> (1549.0, 617.0)) == (CGSize(width: 1334, height: 593) -> (1334.0, 593.0)) Expectation failed: (pushed(connection)?.cols -> 193) == 166 The companion test pins the asymmetry the drop guard exists for and is green before and after the fix: a 6133x639 content ideal parked against a 1728x663 bound stays dropped when the settled bound is the same — re-judgment must never decay into a clamp. * remote-tmux: re-judge a dropped container reading once against the settled bound The oversized guard in noteContainerSize drops a reading that exceeds the hosting window's content bound, on the grounds that such a reading is an ancestor's content ideal and says nothing about the true slot. The verdict itself can be wrong: during an AppKit window resize, SwiftUI can deliver the correct post-resize slot reading while window.contentLayoutRect still holds the transient old frame. The reading is truth, the bound is noise — and the bare return discarded it permanently, because geometry callbacks only re-fire when the region changes again. The pass-top clamp cannot recover the width either: its invariant is container <= window content area, and the window bound overstates the mirror slot by the 240pt sidebar. The mirror froze at the pre-resize size with every claim looking sane. Park the dropped reading instead and re-judge it exactly once, at the top of the next sizing pass: if it fits that pass's bound (within the same half-point tolerance the guard uses), it was truth all along and banks verbatim; otherwise it stays discarded. It deliberately does not reuse pendingContainerSizePt — pending consumption clamps to the bound, and clamping a genuinely oversized reading would bank the window bound itself, the poison the drop path exists to prevent. Accept-if-now-fits keeps that anti-poison property and adds only the missing revalidation edge. A reading banked directly clears the parked one, so stale parked truth can never overwrite fresher truth. The design doc's deferred-input section now carries the rule: truth delivered during a torn window state must not be discarded on the noise's verdict — a dropped reading is re-judged once against the next settled bound. * remote-tmux: red tests — single-pane claims must be window-bounded, the hosting view must refuse oversized frames A mirrored single-pane window's size claim comes from its surface's rendered grid, and rendered content is downstream of SwiftUI layout: when a hosting ancestor adopts the content's ideal size, the surface renders at the inflated size, the wider grid claims a wider tmux window, and tmux's reflow grows the content ideal again. Captured live: claims growing ~1.5 columns per 100ms to 781 columns, with the main hosting view at 6373pt inside a 1728pt window. Two failing tests pin the two legs. The claim test drives the real display-pane hook with a 781x200 rendered grid while the surface sits in a 504x400 window and expects the claim to stay under what the window's content area divides to (fails today: 781 > 72 columns, 200 > 28 rows). The hosting-view test hands MainWindowHostingView the frame the layout engine applied live and expects it to stay at the window (fails today: 6373 > 501, 3000 > 401). * remote-tmux: bound single-pane display claims by the hosting window A mirrored single-pane window claimed its size from the surface's rendered grid with nothing bounding it. Rendered content is downstream of SwiftUI layout, so when a hosting ancestor adopts the content's ideal size the surface renders inflated, the wider grid claims a wider tmux window, tmux's reflow grows the content ideal again, and the loop amplifies without bound — captured live as claims growing about 1.5 columns per 100ms until tmux held a 781-column window inside a screen that fits 246, with the main hosting view at 6373pt in a 1728pt window. The multi-pane window mirror is immune by design (its claims derive from the measured container, never from rendered grids); this sibling path violated that invariant. Two changes close the loop. Every single-pane claim hook now routes through one bounded push: the claim is capped at what the hosting window's content area divides to at the sample's cell size. This path has no independently measured slot — every view between the window and the surface is laid out by the same SwiftUI pass the feedback inflates — so the hosting NSWindow, whose frame layout cannot grow, is the strongest honest bound available; in the healthy steady state the rendered grid already equals the slot and the cap never binds. And MainWindowHostingView now refuses frames beyond its window at setFrameSize. Its sizing options and windowDidLayout shadow only govern the hosting view's own sizing paths; the layout engine handed it the inflated frame directly (required constraints from hosted AppKit subtrees resolve by growing containers), and every space-filling descendant — including the terminal surfaces whose rendered grids feed the claims above — inherited the width. Clamping the frame setter kills the amplifier for any other unbounded content too. * remote-tmux: red tests — planned pane extents must respect the rendered floor The divider plan converts tmux-assigned cells straight to points, so a 2-cell pane at 8pt cells plans out to 21pt. bonsplit's pane chrome cannot render below ~32pt (the embedded config asks for a 1pt minimum, but the tab-bar controls' required constraints hold the floor), so the imposition clamps forever and the outcome never matches the target — observed live as plan=21x192 rendered at 32x192 and plan=13x381 at 32x380, a permanently unappliable plan. Two failing tests pin the fix through the production metrics path. A 2-cell-plus-120-cell split must plan the small pane at the floor with the shortfall taken from its sibling and the split still summing to its parent (fails today: 21.0 < 32). And a split whose span cannot afford two floors must degrade deterministically to the proportional division instead of emitting per-ideal extents the renderer resolves unpredictably (fails today: 21 instead of 25 over a 50pt span). * remote-tmux: clamp planned pane extents to the rendered floor The rail allocation now carries a feasibility floor. Production metrics (nativeLayoutMetrics) set minimumPaneExtent to the measured 32pt bonsplit renders as its smallest pane — the embedded config asks for 1pt, but the pane chrome's required constraints refuse less, so any smaller planned extent parks the divider at the floor and the imposition re-arms forever with the outcome never matching the target. bonsplit exposes no constant for this floor, so RemoteTmuxNativeLayoutMetrics.bonsplitMinimumPaneExtent is the one shared definition. railAllocation clamps the exact boundary, before rounding, to the two subtrees' minimum imposable extents (a pane per axis plus the divider between same-axis siblings), so the carry stays the sub-point rounding error and the points the floor takes reach the sibling through its child size — the split still sums to its parent exactly. When a span cannot afford both minimums, it divides in proportion to them, the same even degradation the ideal scale-down uses, instead of emitting per-ideal extents the renderer would resolve unpredictably. Synthetic metrics default to no floor, so the pure-math and fuzz suites keep exercising the unclamped partition arithmetic. * portal: sync one hosted view per anchor callback during window live resize During a live window resize every visible pane's anchor fires its geometry callback in the same layout pass, and each callback ran a full-portal sync: every hosted view re-synced, every visible one reconciled and force-redrawn (layout flush + displayIfNeeded + a ghostty refresh), per callback, per frame. With a session of ~40 mirrored panes that made window drags visibly chug — a main-thread sample during a real drag spent about two thirds of its samples in per-display-cycle layout, a large share of it under this fan-out. A window live resize now takes the coalesced path: each anchor callback syncs only its own hosted view (its geometry is current inside the layout pass, so the pane stays glued), and the per-tick scheduled pass covers panes whose window-relative position changed without their own frame changing. Mid-resize the synchronous redraws are skipped too — a ghostty size change schedules its own repaint, and the forced refresh was also hitting surfaces whose Metal layer was not realized yet. Split-divider drags keep the immediate full-fan-out path unchanged. End-of-resize stays unconditional: windowDidEndLiveResize still runs the full sync with the reconcile-and-refresh sweep, and each terminal view's viewDidEndLiveResize still applies the final surface size with live-resize coalescing bypassed, so the final geometry callback the remote-tmux sizing transaction depends on cannot be missed. * portal: drop the window origin from the geometry sync signature The external-geometry signature is the only terminator of the portal sync echo chain — a pass whose signature matches the last completed pass does no layout and emits no notifications, so echoes die there. It fingerprinted the window's frame WITH its origin, and a titlebar drag changes the origin every tick, so under echo pressure every echoed sync escalated to a full layout pass (four subtree layout flushes plus a sync of every hosted view) whose own notifications scheduled the next. A flight recording of a real drag logged ~2800 lines/sec for ten seconds; a healthy window move costs about 40 lines total. The signature now carries the window's size and its backing scale — the one legitimate origin-correlated input, since moving to a different-DPI screen re-snaps pixel geometry. Every other field was already window-relative. Guards for the regression class: DEBUG counters for sizing passes, output parity re-arms, and full hierarchy syncs, published through remote.tmux.sizing_settled; remote.tmux.test_set_frame accepts optional x/y so it can move a window without resizing it; and a sizing UI scenario that settles a mirror, applies 50 origin-only moves, and requires all three counter deltas to stay zero. An origin-only setFrame lands on the same frame-change path a drag does but skips AppKit's drag tracking loop, so the scenario is a necessary rather than sufficient guard. * remote-tmux: hidden mirror tabs hide their split trees at the AppKit level The workspace bonsplit keeps every tab's content alive (contentViewLifecycle .keepAllAlive) and hides deselected tabs with SwiftUI opacity 0 — which never sets isHidden on the AppKit split trees the embedded mirrors render. A live lldb census found 21 split-view instances stacked in one window whose visible layout has two dividers. The unhidden foreign trees painted their dividers over the visible panes (the phantom interior divider), registered resize-cursor rects under the pointer, and their alpha-0 drop zones sat above the selected tab and rejected pane drops (the embedded config forbids cross-pane tab moves), snapping drags back. Bonsplit already has the AppKit-level switch: isInteractive hides the split tree itself. The mirror view now drives it at the same onAppear/onChange visibility edges that drive isVisibleForSizing, so a deselected tab's tree is hidden from AppKit outright and the reveal edge flips it back before the sizing pass runs. The census test hosts two split mirrors as keep-alive tabs and asserts AppKit sees exactly the selected tab's tree and exactly its dividers, both ways across a tab switch. * remote-tmux: seed divider baselines at drag begin so the first drag after an imposition reaches tmux Imposing a changed extent parks the split's divider baseline at nil, expecting a post-layout geometry callback to record the minimum-clamped outcome. That callback can never arrive: the deferred apply runs under the programmatic-sync guard (didResize returns before onGeometryChange fires), and once the user grabs the divider the drag guard eats every mid-drag callback. Drag end then found no baseline, seeded it from the post-drag fraction, sent nothing — observed live as sent=0 on five of six drags — and re-imposed the pre-drag extent, snapping the divider back in the user's hand. Drag begin now seeds every missing baseline from the model fraction: by that point the deferred apply has landed, so the fraction IS the outcome the nil was waiting for. As a belt, drag end no longer gates the send on baseline presence — the fraction there is the user's committed move, and the cells-versus-assigned feasibility check is the real no-op detector — so even an unseeded baseline cannot swallow a drag. The test recreates the parked-nil state through the production sizing pass, drives the real drag-session hooks, and asserts one resize-pane goes out and the pre-drag extent is not re-imposed. * debug-log: serialize all debug log appends through one sequenced writer Debug builds wrote the shared debug log from two independent paths: DebugEventLog (cmuxDebugLog/logDebugEvent) and bonsplit's dlog each opened a fresh FileHandle per line on their own serial queue and did seekToEnd()+write. The two seeks race, so concurrent lines clobbered each other and landed out of timestamp order — a live capture showed 19:06:31 lines after 19:07 lines and a divider.session line missing entirely, which sent an investigation down a false trail. Log storms also dirtied about 2GB/hr because every line reopened the file. All appends now funnel through DebugEventLog's single serial queue and one kept-open O_APPEND handle; bonsplit installs an external sink at app startup (CmuxMain.main) that forwards dlog lines into the same writer. Every persisted line carries a monotonic #<seq> prefix so any future reordering is visible in captures. Floods are bounded: past 2000 persisted lines per second the disk write is skipped and the next window opens with an explicit 'log.dropped N lines' marker, so lines are never silently lost. Public signatures are unchanged. The new DebugEventLogSerializedAppendTests pump both public paths from concurrent threads and read the file back: every line intact, sequence strictly increasing, and every missing line covered by a dropped marker. On the previous writer they fail (29 of 800 lines lost with no markers, no sequence numbers); they pass on this one. Pins bonsplit at 92093ae (external debug-log sink). * tests: portal lifecycle and mirror resize suites encode the current contracts Four stale expectations surfaced when these suites joined the sizing gate; two also fail on the branch's merge base, so they predate this branch. - testDeferredSyncHidesVisibleHostedViewAfterAnchorDisappears and testHiddenPortalDefersRevealUntilFrameHasUsableSize asserted synchronous effects from synchronizeHostedViewForAnchor. Outside an interactive drag that call now coalesces into a queued portal pass, so the tests settle the queue before asserting the same hide/defer-reveal/reveal outcomes. - testPruneDeadEntriesDetachesAnchorlessHostedView (fails on the merge base too) still encoded the pre-drag-churn prune: a visible entry whose anchor vanished is now kept for tab-drag recovery and hidden by the queued pass; only a non-visible one is pruned and detached. It also counted raw hostView subviews, which includes the divider overlay, so it now counts terminal subviews. - testScheduledExternalGeometrySyncWaitsForQueuedLayoutShift (fails on the merge base too) now drives the non-immediate schedule — the deferred hop is the wait under test — and waits for the queued shift to land instead of spinning the runloop for a fixed 50ms the shared app host can starve. Red at this commit: a folded immediate request flushes the pass early and silently drops the deferred hop; the next commit fixes that. - testWindowScopedExternalGeometrySyncDoesNotRefreshOtherWindows settles queued portal passes before mutating geometry, so a leftover pass cannot masquerade as a cross-window refresh. - The two resize-routing tests compared the whole control stream against a single resize-pane line, but the stream legitimately begins with the attach-time list-windows topology fetch (also on the merge base). They now judge exactly the resize sends. * portal: honor the deferred-hop contract when requests fold into one pass A non-immediate geometry sync request is promised one extra main-queue hop, so a layout mutation queued on the same turn lands before the pass reads geometry. Requests coalesce into whichever pass is already scheduled, and an immediate request in the same burst (window and host frame observers fire constantly during setup and churn) flushed that shared pass on its first hop. The deferred request lost its hop silently, nothing remained scheduled, and the portal parked at geometry read before the queued mutation — a stale hosted frame with no recovery path. Track the un-honored deferred request across the fold and queue one follow-up pass after an early flush; the follow-up dies in the geometry fingerprint check as a no-op once geometry stops changing, so the chain stops one pass after geometry does. Also stop dropping a portal's queued pass when it fires while another portal's pass is on the stack (a re-entrant main-queue drain during that pass's layout): the scheduling flag is already down at that point, so returning without rescheduling lost the request forever. Re-queue it instead. Red test: testScheduledExternalGeometrySyncWaitsForQueuedLayoutShift in the previous commit. * remote-tmux: red tests — portal ownership, divider round-trip holds, resize routing, parked readings Eight tests pinning live-fuzz and review findings, each red against the behavior it names. Portal (the seed-1 hierarchy-sync storm): the portal host must carry no layout-engine constraints — constraints read the engine's solution, and when a hosted subtree's required width demand makes that solution unreachable for the hosting view (it refuses oversized frames), they stomp the host and every hosted terminal view to the unreachable geometry on every layout pass (+175pt uniformly, full_hierarchy_sync in the thousands per settle window). A behavioral companion asserts one sync restores host and hosted frames after an external stomp and holds across later turns. A third asserts a deferred sync request under a held interactive flag stays bounded on static geometry instead of chaining one full pass per runloop turn. Divider round trip: a drag whose send is in flight must not bounce — not from redundant triggers (the parity re-arm reading dragged views against the pre-drag plan), and not from an UNRELATED layout change replanning from a tree that is still pre-drag for the dragged split. A send tmux never answers (a span its cascade minimums clamp to a no-op) must heal at a bounded deadline that re-arms parity rather than leaving the divider parked off-grid with the guard disabled. Resize routing: dragging a divider whose first subtree hides an inner same-axis split must address the pane whose nearest same-axis split IS the dragged one (%33 in the nested shape the control-path routing tests already pin), not the subtree's first pane in order — tmux resizes the target pane's nearest split, so %11 resized the inner split instead. Sizing: a parked oversized container reading must survive a pass that runs during portal darkness (no bound anywhere to judge it against) and bank at the first bounded pass after the reveal — consuming it in the dark lost the one re-judgment and the frozen-claim class returned. * portal: the host frame follows the reference's actual bounds, not the engine's solution Live fuzz seed 1 regressed from 1 failure to 11 at the head of this branch, with a new signature: panes rendering wider than plan by a uniform +175pt while claims stayed exact, and settle windows blown with full_hierarchy_sync counters in the thousands. The chain, read off the fail-time debug logs: a hosted AppKit subtree carried a required width demand beyond the window, so the layout engine's solution for the hosting view exceeded the frame the hosting view actually holds — its frame setter refuses oversize, so the two can never reconcile. The portal host was edge-constrained to the hosting view, which reads the ENGINE's solution: every layout pass stomped the host to the oversized solution (portal.hostFrame.update logged the same reset tens of thousands of times), every hosted terminal view stretched by the same +175pt through autoresizing, and the portal pass that undid it forced the next layout pass. The echo-cut signature never matched two passes in a row, so every pass escalated to a full hierarchy sync; the sizing transaction's parity re-arm spent its budget against the external writer and gave up with the views off plan. The portal already writes the host frame from the reference's ACTUAL bounds on every pass, install, and geometry notification — the constraints were a second writer wired to a different (and here unreachable) value. Drop them; the host frame is portal-owned. A frame-change observer on the reference replaces the one thing the constraints did that the existing observer web did not: catching a reference resize that moves no anchor. * remote-tmux: divider sends carry a keyed, bounded reply hold and route like the control path Drag-end with a sent resize-pane defers to tmux's layout reply, and the first cut of that deferral was a bare flag: set at drag end, cleared by any imposition, guarding the parity re-arm. Review confirmed two holes. A no-op send (the client clamp can produce a span tmux's cascade minimums won't change) gets no %layout-change, so in an idle window the flag leaked forever — divider parked off-grid AND the guard disabled. And any unrelated %layout-change mid-round-trip replanned from a tree still pre-drag for the dragged split, re-imposed the stale extents (the bounce, back under churn), and consumed the flag. The hold is now keyed to the send: split id, axis, and the span asked of tmux. Impositions resolve it against the layout they plan from — only a layout that ASSIGNS the sent span ends the hold (the reply landed); an unrelated replan leaves it standing and skips the held split's subtree, so the user's divider survives churn; a vanished split clears it (the structure changed under the drag). A bounded deadline covers the never-answered send: it releases the hold and re-arms the pass — re-arm, not just clear, so parity heals the parked divider back onto the plan. The send itself now routes like the control path: tmux resizes the TARGET pane's nearest split along the axis, so the pane addressed for the dragged split's first subtree must not sit behind an inner same-axis split. first.paneIDsInOrder.first did exactly that in nested same-axis shapes and tmux resized the inner split (or no-oped — feeding the leak above). The tree's routing rule is now public and both senders share it. * remote-tmux: keep an oversized reading parked through bound-less passes The oversized-container guard parks a rejected reading for exactly one re-judgment against the next settled bound — a mid-resize callback can carry the true post-resize slot while the window's transient frame undersells it, and no later callback re-delivers that truth. The pass consumed the parked reading BEFORE resolving a bound, so a pass running during portal darkness (every hosted view briefly detached or hidden mid-churn: no probe window, no visible pane, no injected bound) cleared it while judging nothing. The reveal path re-registers the host probe but never re-delivers the reading, so the one chance to bank it was gone and the frozen-claim class returned. Consume the parked reading only when a bound exists to judge it; a bound-less pass leaves it parked for the first bounded pass after the reveal. * remote-tmux: divider hold releases on protocol events, never a timer A sent divider resize held the parity re-arm behind a 2-second wall-clock grace: a send tmux swallowed (a span its cascade minimums clamp to a no-op) produced no %layout-change, and only the deadline cleared the hold and recovered. Time encodes a latency assumption — on a slow link the grace fired while the reply was still in flight and bounced the divider; on a fast one it parked the divider off-plan for two full seconds. The release is now anchored on the control stream's own ordering. Every command is answered by an ordered %begin/%end block, and a notification a command causes is emitted after that command's %end but before any block for a command sent later. So the resize rides a tracked send; its ack issues one cheap barrier (an empty display-message block). A barrier ack with no intervening layout event for the window proves the resize changed nothing — release the hold and re-arm the pass so parity heals the divider back onto the plan. A barrier ack that finds the window's layout still quarantined behind its rects fetch defers the verdict to that fetch's resolution (publication or drop — the drop paths now notify so the resolution edge always reaches the mirror). %error recovers immediately, a stream reset fails the tracked completion, and a newer send supersedes an older send's acks by generation. The reply-assigns- the-sent-span release and the structure-change release are unchanged. The old round-trip tests had to lengthen or shorten the grace around their own pumping — the test needing real seconds was the defect. The reworked and new tests drive the stub stream's blocks directly: no-op proof, %error, superseded generation, and late-ack inertness all resolve synchronously on the injected protocol events, with no clock anywhere. * tests: pin tee-lease and manual-IO context release ordering to the native free The ghostty PTY tee callback fires on the io-reader thread for every output chunk until ghostty_surface_free joins that thread, and the MANUAL-mode io_write_cb fires on the io thread the same way. The retained callback userdata must outlive the native free. These tests fail today: every teardown path that defers the free to the runtime teardown coordinator (deinit, and the override-free paths of teardownSurface and agent-hibernation suspend) releases the tee lease and manual IO context immediately, leaving a window where the io-reader thread dereferences freed userdata. * terminal: release tee and manual-IO callback userdata only after the native surface free ghostty's io-reader thread calls the PTY tee callback for every output chunk, and the io thread calls the MANUAL-mode io_write_cb, right up until ghostty_surface_free joins those threads. The teardown paths that defer the free to the runtime teardown coordinator (deinit, and the override-free paths of teardownSurface and agent-hibernation suspend) released the tee lease and manual IO context immediately, so until the coordinator's worker ran the free — seconds later under load — the reader thread could fire the tee callback into freed userdata. That use-after-free killed unit-test app hosts mid-suite and can take down the app on surface close. Transport the tee lease and manual IO context through the teardown request, exactly like the surface callback context, and release all three on the main actor only after freeSurface returns. The free is the happens-before edge that joins the IO threads, so a callback can never observe released userdata. * remote-tmux: bound the divider plan to the region; portal self-writes stop re-arming the sync Live fuzz seed 1, iterations 20 and 21, replayed to two cooperating defects. A reconnect racing a resize left the claimed size and tmux's layout permanently disagreeing, and the assigned tree's exact point size (198 columns, about 1584pt) exceeded the banked region (about 1345pt). Geometry demanded past the container is satisfied by AppKit growing the non-movable window; the next pass read the growth back and the window ratcheted a point per pass to the display cap. Then, stationary, the portal fought over the disagreement at period 2: its own frame writes post frame/bounds notifications synchronously, the geometry observers re-armed the sync on them, and the single-signature guard can never latch an A,B,A,B alternation — full_hierarchy_sync hit 2520 in one settle window. Two rules close it. The plan side gets the invariant plan(w) <= w: the parent a divider plan divides is the exact-fit render frame bounded by the banked region on both axes (regionBoundedPlanParent), so under a claimed-vs-layout disagreement the render degrades to the region and never demands past it. The portal side gets a self-write token: frame writes the portal itself makes (host frame restore, hosted seed and target frames) hold the token, and geometry observers ignore notifications that arrive while it is held — only genuinely external geometry re-arms the sync, so an external stomp costs exactly one sync request and the restoring write buys zero. * docs: reconcile-pass design for remote-tmux sizing Replaces the edge-triggered sizing machinery's execution model with a reconcile pass: events set a dirty generation, one pass per window snapshots its whole world in a single instant, desired state is a pure feasibility-clamped function of the snapshot, the diff classifies every mismatch as awaiting, user-owned, drift, or infeasible-reported, and the commit writes synchronously outside layout callbacks. Fourteen named mechanisms are deleted with their replacing property stated; drag sessions and the command-ack barrier stay. Thirteen edge cases carry termination arguments; migration is three steps under one rule — no step deletes a mechanism whose replacement ships later. Reviewed adversarially by two independent passes; all findings folded, including the unreachable-desired circuit breaker, synchronous commit applies, dirty generations, the three-state claim ledger, and the plan-never-exceeds-region invariant the live fuzz proved this week. * tests: the bounce fixture's reply assigns the span the drag actually sent The fixture's region is narrower than the tmux window, so the dragged fraction converts to fewer cells than the raw tree suggests — a reply hard-coded at the tree-scale span (92) never matches the sent span and the release path it exercises can never fire. Read the sent span off the armed hold and build the reply from it; the release-and-settle assertions then judge the real protocol edge instead of a fixture artifact. * portal tests: bound each test's runtime and add a last-slot leak check TerminalWindowPortalLifecycleTests passes test-by-test but dies when the class runs whole: depending on interleaving the shared app host either livelocks in a SwiftUI reentrant-layout loop during realizeWindowLayout, hangs in removePortal teardown, or crashes on the io-reader thread inside the PTY output tee. Each test leaves state behind for the tests after it — dropped TerminalSurfaces whose native frees and io threads are still in flight, portal windows that never close, and directly-created portals that never see willCloseNotification. This adds the detector first: a leak-check test that runs in the class's last alphabetical slot and asserts the process returned to its pre-suite baseline (registered portals, portal-hosting windows, live runtime surfaces, and — via a new DEBUG counter on the teardown coordinator — native frees still in flight). It is red on this commit: running just testWindowScopedExternalGeometrySyncDoesNotRefreshOtherWindows followed by the leak check crashes the host with the tee use-after-free before the assertions can even report. Each test also gets executionTimeAllowance = 60 so a future livelock fails the one wedged test in minutes instead of hanging the host until the CI job timeout. * portal tests: tear down every window, portal, and surface a test creates The lifecycle suite's tests each stood up real NSWindows (ordered front), WindowTerminalPortals, and live TerminalSurfaces and dropped them at the end of the method. Across a whole-class run that left, for the following tests: native surface frees and io threads still in flight; blocking window-appearance NSAnimations committing CA transactions from background queue threads (two of them in every wedge sample); portals whose NotificationCenter block observers — including an object:nil split-view observer — stayed registered forever; and refcounted interactive-resize state a failed test could leave latched. Track all three through suite helpers and tear them down in tearDown: surfaces are released synchronously (releaseSurfaceForTesting frees the runtime before its io threads can race the next test), directly-created portals get tearDown() since they never see willCloseNotification, and windows are created with animationBehavior none and closed for real. tearDown also asserts the registry portal count returned to its baseline, so a future leak fails the leaking test, not a victim three tests later. Two product-side seams back this up: WindowTerminalPortal removes its notification observers in deinit (a portal that dies without ever seeing willCloseNotification leaked them permanently, in production too), and a DEBUG-only resetInteractiveGeometryStateForTesting clears the refcounted drag state the production API offers no owner handle for. * portal tests: the self-echo test uses the tracked window and portal teardown The suite hygiene rules apply to it like every other test in the class: a tracked window (animation off, closed in tearDown) and a tracked portal instead of an ad-hoc pair the last-slot leak check would count against the baseline. * portal: never force a synchronous surface redraw from inside a layout pass A geometry sync that runs while AppKit is still inside the window's layout pass (syncLayout == false: SwiftUI update callbacks and anchor geometry callbacks) called refreshSurfaceNow when a hosted frame changed or a hidden surface was revealed. displayIfNeeded there reaches ghostty's Metal drawFrame with the window's transaction still open, and waitUntilCompleted waits on a present that only that transaction can commit — the main thread wedges permanently. Seed-1 fuzz reproduced it twice at the same iteration: a programmatic window setFrame is not a live resize, so the live-resize guard did not cover it. Defer those redraws to the next main-queue turn; syncs that already run outside layout keep the synchronous flush. * remote-tmux: publish generation-stale rects replies that cover the current tree Layout publication discarded any rects reply whose generation had been superseded and sent a fresh fetch. Under continuous churn every reply is one generation behind by the time it lands (%layout-change inter-arrival is shorter than a round trip), so publication never advanced: windowsByID froze at the pre-churn tree while the app's claims and tmux kept agreeing, and the settle oracle timed out. Seed-1 fuzz held that starvation for 32 seconds against an 8-second budget once control-stream round trips inflated under load. The coalescing design already intended one publish-then-follow-up cycle (the dirty flag), but staging bumps the generation on every event, so the generation guard always won and the dirty publish-first branch was unreachable in exactly the storm it was built for. Now a stale reply that still covers every pane of the current tree publishes as interim verified state — its rects are the freshest list-panes snapshot observers can have — and owes exactly one follow-up fetch for the newest generation. Publication advances once per round trip and converges on the first quiet one. A stale reply that no longer covers the current tree (structure changed mid-flight) keeps today's behavior: nothing publishes, the owed fetch returns the new structure, and the garbled-reply retry budget is not burned. Also repairs three tests in this suite that still asserted no topology notify on the rects-drop paths; the drop's resolution notify is intentional (a divider hold deferring to 'this window's pending layout resolved' needs the edge) and this suite was missing from the pre-push gate. * portal, terminal, browser: close the remaining synchronous-display holes inside layout passes A deadlock audit of the mirror stack found three more paths in the class d063f19166 fixed — a synchronous surface display reachable while AppKit is still inside a layout pass, where ghostty's Metal draw can wait on a present that only the still-open window transaction can commit: - The interactive-drag branch of the anchor sync ended with an ungated failsafe reconcile that called refreshSurfaceNow one line after the primary sync had correctly deferred. Reachable on every divider or sidebar drag tick. The reconcile now threads syncLayout and defers through the same queue. - setVisibleInUI(true) nudged the surface synchronously, and three of its callers run inside SwiftUI update/layout (updateNSView, viewDidMoveToWindow, the geometry-callback rebind). The nudge now waits one main-queue turn. - The browser panel's hosted-WebKit refresh ended with a whole-window displayIfNeeded from updateNSView, which also flushed sibling Metal terminal panes mid-pass. The flush is now scoped to the panel's own subtree, which has no Metal wait. The first two land with red-first tests that drive the sync from inside a real layout pass; the audit's remaining lower-severity findings are tracked separately. * docs: reconcile design — audit findings, per-layer testing, diagrams, plainer prose Adds the concurrency audit's findings: the two rules the deadlocks proved (no synchronous surface display inside a layout pass; the main thread is the contended resource), the main-actor ingest pipeline as the confirmed round-trip-inflation mechanism (step zero of the migration), and the fragile tier with file references and fix directions. Adds a testing section that separates what gets stress-tested in simulation (the pure desired function, the reconcile loop against simulated ports, turn-based and seeded) from what must run against the real dependencies (AppKit, bonsplit, ghostty, tmux), with the rule that every simulator behavior must be pinned by a real-dependency test. Adds ASCII diagrams for the pass loop, the diff classification, and the main-thread contention picture, and rewrites the longest sentences into plain ones. * docs: reconcile design — testing weight goes to real-dependency assertions The bug ledger says where tests pay off: not one of the day's bugs was the loop mis-stepping on its own state. Every one was our model of a component diverging from what AppKit, bonsplit, ghostty, or tmux actually did. A simulated-world stress harness inherits the model's blind spots, so layer 2 shrinks to a skeleton check and the weight moves to driving the real components with the commit phase's instructions and asserting they handled them as modeled. * docs: pin tmux 3.7's half of the sizing loop from source; correct the no-op barrier rationale The reconcile design gains a tmux section verified against the 3.7 source (file:line cited) so the facts don't have to be re-learned by observation: claims are ceilings in every window-size mode; a control client can't become 'latest'; an unfittable claim clamps the WINDOW UP to the tree minimum, so claimed==layout never converges and the published layout is the feasibility verdict; window-resize redistribution is equal-absolute, so split ratios erode and imposition is permanent work; no-op resizes still emit %layout-change and every claim echoes one per window; %layout-change orders after its command's %end; pty resizes are deferred 250ms per pane; layout rects are borderless cell sizes that ignore border-status rows; a per-window claim makes the client's tty size participate for every other window. One of those facts corrects the divider-hold rationale in the hardened sizing doc, amended with the root cause: 'a no-op resize emits no %layout-change' was written from lab observation and was unfalsifiable by our oracles, because both hypotheses release the hold through the same observable path. tmux notifies unconditionally (layout.c:726-728); the only silent resize is the missing-container case (layout.c:686-687). The shipped mechanism is correct under both readings — the barrier is the ordering fence plus the one silent case — so this is a rationale fix, not a behavior fix. * portal: clear the hosted view's autoresizing mask on adoption Hosted terminal views reach the portal from SwiftUI hosting with autoresizingMask = [.width, .height]. In an Auto Layout window that mask is translated into edge pins - a minX constant plus a trailing margin to the host, no width constraint at all - and the pin distances freeze at whatever geometry the last constraint pass saw. Every host resize then re-derives the pane's size from stale margins against the new host bounds, stomping the frame the portal just wrote. The portal restores plan truth, the next flush re-applies the pin arithmetic, and the two writers alternate once per display refresh: panes sat exactly one host-delta wide of plan (the live 5pt case) while hierarchy syncs ran into the thousands per settle window. Live forensics pinned it: at the moment of a stomp the engine holds no width constant for the pane (engineWidthConstant=nil), just edge pins frozen at the previous generation (portal.stomp.diag, added here as rate-limited DEBUG forensics). A unit fixture reproduces the arithmetic deterministically once the view carries the production mask: growing the window 120pt stretched a 240pt pane to exactly 360. Adoption now clears the mask (bind), re-asserts it on every sync in case reparenting plumbing restores it, and puts the original back on detach. An empty mask translates to rigid position+size constants that always equal the last portal write, so the engine's opinion of a pane IS the portal's last write - there is no second geometry source left to fight, and no frame setter is overridden anywhere (a setter that swallows or rewrites engine applies desyncs engine bookkeeping and NSWindow raises its update-constraints budget exception). The mask test pins adoption/detach and the host-resize decoupling; the divergence suite keeps the convergence guards: a restore survives flushes of any scope, portal writes do not re-arm the sync, and a rapid-resize burst converges without an exception. * docs: pin the autoresizing-mask geometry-writer finding The frame ping-pong took three failed fixes before live forensics named the writer, so the mechanism, the two dead ends (refusing engine writes crashes the constraint budget; redirect-through-super is the same thing in disguise), and the reason every unit fixture missed it (test views are born mask-empty) now live in the reconcile design doc's audit section. * remote-tmux: re-arm a delivered size claim when the layout disagrees tmux is the only authority on whether a size claim actually landed. The sent-pins ledger dedups resends, so a pin the server never honored wedged silently: the reply was lost across a transport gap, or a co-client raced it, or the window-size mode changed - either way the ledger said delivered, dedup suppressed every retry, and the window sat columns wide of the claim while mirrors rendered short of the assignment. The live fuzz caught panes rendering 83 columns against an assignment of 86, persisting through settle and reconfirm, with the text wrapping off tmux truth as the visible symptom. Every %layout-change names the window's actual size, so it is the parity edge: when it disagrees with a claim the ledger says was delivered, drop that ledger entry and resend the claim. The re-arm is budgeted at three per disagreement episode - an infeasible claim (tmux clamps a window up to its tree minimum) disagrees forever and must not become a per-layout-event ping - and agreement or a new claim value opens the next episode. Claims still derive only from measured containers; this resends a decision already made, it never makes one. Also relabels the rendered-short diagnostic's plan= field to planOuter= with the tab-bar accounting spelled out: the plan charges the per-pane tab bar in the pane's outer box while view= is the content below the bar, so a healthy pane reads exactly tab-bar-height shorter there and the constant kept getting misread as a layout bug. * remote-tmux, portal: audit follow-ups before undrafting Reviewed every fix commit on the branch for necessity. Three follow-ups came out of it. The claim-parity re-arm budget now resets on reconnect: episodes are per connection, and a budget spent against the old transport must not suppress re-arms when the reseed's own resends get lost the same way. The engine-constraints test no longer calls autoresizing-translated constraints "the SAFE kind" - the mask finding disproved that generalization for hosted views, and the comment now says what is actually tolerated on the host and why. The divider-drag denominator keeps its unclamped renderFrameSize on purpose, and the comment now explains why the plan's region bound must not be applied there: drags convert against what is actually on screen. * remote-tmux: mirror grids render exactly their tmux-assigned cells A mirror pane's grid derived from its view diverges from tmux whenever the plan and the assignment disagree, and every direction of divergence corrupts the mirrored text. Short: the divider plan can legitimately hand a pane fewer points than its cells need - a starved sibling is the live case, where tmux assigns one column, bonsplit's pane chrome refuses to render below ~31pt, and the rail pays the difference out of the sibling's share (plan parent 707pt, outers 31 + 675, where 86 assigned columns need 691: the pane rendered 83 columns and wrapped off tmux truth). Long: the starved pane itself derived a ~3-column grid from its 31pt floored view, so "END 001x022" never wrapped where tmux wrapped it and the unwrapped read gained seven lines; a taller grid keeps rows tmux never repaints, which read back as stale content. Since the points genuinely are not there - or are there in excess - the grid follows tmux exactly and the view clips or letterboxes the difference: the same answer tmux gives a client whose size disagrees with the window. Mirror surfaces carry their tmux-assigned grid; updateSize pins the applied pixels to precisely the assigned cells at the current cell size plus the surface's own chrome (inert until the surface has real cell metrics), and the sizing pass sets or clears each pane's pin from the layout tree's leaves. The pin lives strictly on the surface-pixel side; claims keep deriving from the measured container alone, so the feedback loop that sank the old view-pinning approach cannot form. The pin arithmetic is a pure function pinned with the live numbers from both directions: 1351px of view and 86 assigned columns at 16px cells pins up to the assigned grid, the one-column pane's 56px view pins down to one column, and missing cell metrics or a degenerate assignment leave the size untouched. Repro: scripts/remote-tmux-fuzz-host.sh <alias> to stand up the local fixture, then scripts/remote-tmux-live-fuzz.sh <alias> 3 2 (seed 3 fails at iteration 1 without this change: pane %0's unwrapped read-screen gains seven lines over tmux capture-pane) or seed 2 x 25 (one row short on a 118x41 pane at iteration 25). * skills: document how to run the remote-tmux layout fuzz The harness scripts each explain themselves, but nothing discoverable tied a commit's 'seed 3, iteration 1' to the commands that replay it. The testing skill now covers the fixture setup, the marathon and single-seed replay invocations, the two settle oracles, what the evidence directory contains, and the quiet-machine rule. * remote-tmux: settle requires derivation parity; hidden readings park instead of dropping Two hardenings against the staleness class the fuzz surfaced today. The settle oracle checked delivery parity only - claim equals tmux's layout - which cannot see a claim tmux honored but that no longer matches what the CURRENT container derives. That is exactly how a stale claim settled green this afternoon while the region could not render the columns it promised. A settled visible window must now be able to re-derive its own claim; hidden mirrors are exempt because they hold their attach-time claim by design. noteContainerSize dropped readings that arrived while hidden, with no park and no replay. Geometry callbacks fire only on change, so a dropped reading was gone for good, and a reveal whose cached re-push happened to be degenerate would leave the claim derived from a pre-hide width. Hidden readings now park in the pending-container channel the sizing pass already judges against a real bound before anything banks; a fresh reveal reading replaces the parked one outright, so hidden geometry still never banks unjudged. * remote-tmux: redraw kick when a pin grows; settle oracle judges the live grid The definitive marathon left two failures, one per mechanism. A pin that grows a mirror's grid after tmux already streamed those rows leaves the late-granted cells blank: the content that belonged there was clipped while the grid was short, and tmux repaints only on change. setAssignedGrid now reports growth and the sizing side answers with the existing coalesced redraw kick, so tmux refills the cells it clipped (seed 2 iteration 25: a 118x41 pane reading back 40 lines). The settle oracle judged rendered grids from the cached applied-resize ledger, and the cache can lag or miss a pin's resize: it failed a pane whose actual surface held exactly its 1x22 assignment while the cache still said 10x18 from an earlier life (seed 3 iterations 1 and 14 - the content comparison passed, only the stale cache complained). The oracle now reads the surface's live grid and keeps the cache only as the no-report-yet fallback. Why the applied-resize sample misses pin resizes is still open; the parity re-arm reads the same ledger, so that staleness gets its own root-cause pass. * remote-tmux: don't pin the mirror grid mid-drag — it painted past the pane Shortening a mirrored pane by dragging a divider briefly painted the pane's content over the neighbor and the window chrome, settling correct at rest. The assigned-grid pin holds the surface at the pane's tmux assignment, and during a divider drag that assignment is the PRE-drag (larger) one: performSizingPassNow holds applyAssignedGrids while a divider drag is active, and tmux hasn't replied with the smaller layout yet, so the pin keeps the surface oversized for the whole drag. updateSize applies that oversized ghostty grid and asks the renderer to repaint it, and that present lands before the deferred divider-drag reconcile clamps the drawable and clip geometry back — so the oversized frame paints across the just-moved divider. Clipping is never disabled; the leak is an oversized surface presented against not-yet-reconciled geometry. updateSize now takes suppressAssignedGridPin, set while an interactive resize is active (window live resize or the registry's interactive geometry flag), and uses the view-derived size instead of the pin. The pin re-establishes at rest: drag end and tmux's layout reply each size the pane again, and the next updateSize runs with the flag clear and re-pins to the assignment. Mid-drag the mirror briefly renders a view-sized grid a few cells off the assignment; manualIONoReflow keeps DECAWM off so no wrap divergence persists once tmux reconciles. Verification is runtime (drag a divider shorter, no overflow) plus the existing pin-arithmetic and portal/sizing suites; the suppression is a one-line gate with no unit seam for a live ghostty surface. * remote-tmux: red test — mirror never drives key focus for a freshly split pane A window mirror renders each pane in its own Bonsplit tree, so a new split pane is marked active/selected but nothing makes its surface the window's first responder: it shows the blue highlight yet takes no keys until clicked. Adds a mirror-layer contract test that spies the (inert) key-focus establishment seam and asserts the mirror drives key focus onto the new pane's own panel at creation. Red today — creation only updates selection. * remote-tmux: give a freshly split mirror pane key focus at creation A window mirror renders each tmux pane as a TerminalPanel in its own Bonsplit tree; those pane panels are never workspace Bonsplit tabs, so the workspace focus path can't resolve them and the surface's active/visibility false->true edges don't apply first responder either. A new split pane was therefore highlighted but untypeable until clicked. Track panes the mirror just created and, the first time such a pane becomes active, drive key focus onto its own surface the way a click does (moveFocus -> makeFirstResponder). Every attempt re-checks the mirror is still on screen and this pane is still its active pane, so a pane switch within the retry window cancels the pending focus instead of stealing it, and a background or headless mirror never moves the first responder. Consumed once per created pane, so a later active-pane echo never re-drives it. * remote-tmux: land the final divider position when a drag ends during a remote apply A divider drag that ended while isApplyingRemoteLayout was set skipped its only syncChangedDividerPositions() and just scheduled a pass, so the user's final position never reached tmux and the next sizing pass restored the pre-drag layout. Defer the send one runloop turn instead: once the apply's synchronous scope clears the flag, flushDeferredDividerDragEnd runs the same conversion+send the undeferred path runs. Test: dragEndDuringRemoteApplyStillSendsTheFinalPosition. * remote-tmux: re-arm on rendered-grid lag and gate parked readings on a settled bound Two sizing residuals in one file: The exact-grid pin does not always follow an assignment that grew between our claim and settle. The input-only settle proof cannot see it — renderedLayout is an input, but a pin applied against stale cell metrics can leave a pane one row short of the grown assignment, so it renders short and wraps while inputs read unchanged. The output-parity re-arm now treats a rendered grid behind its assignment as a miss (gridParityMismatch) and re-imposes; applyAssignedGrids re-applies the pin when the value already matches but the last sample lags. Tests: settleRearmsWhenRenderedGridLagsTheAssignment, gridParityIgnoresPanesThatRenderTheirAssignment. A parked oversized reading was consumed on the next pass regardless of whether the window bound had settled. During a live resize the window still reports its transient (old, smaller) frame, so a valid post-resize reading was judged against noise and discarded for good. Consume only when the hosting window is not in a live resize; live-resize end delivers the settled callback whose pass consumes it. Test: parkedReadingSurvivesALiveResizingWindowBound. * debug-log: count a line persisted only after the disk write succeeds persist() incremented persistedInWindow before the data conversion, handle open, and write, so a failed write dropped the line from disk without counting it — the next window's 'log.dropped N' marker under-reported and the never-silently-drop guarantee broke. Increment persistedInWindow only after a successful write; count a failed conversion, open, or write as dropped. The entry still stays in the in-memory ring. * portal: move sizing diagnostics to the debug boundary; instance-scope the live-resize test override Two maintainability moves out of production TerminalWindowPortal.swift. The process-wide RemoteTmuxSizingDiagnostics counters move to Sources/Debug/RemoteTmux/TerminalWindowPortal+DebugDiagnostics.swift, the existing debug-support boundary. The process-wide isWindowLiveResizeActiveForTesting static becomes an instance property on the portal, so a test drives only its own portal instead of latching interactive state across the shared app-host; the lifecycle and teardown tests inject on the instance. Behavior identical. * terminal: project the assigned-grid pin to the new content scale (#3) On a scale change (e.g. dragging a mirror pane between a 1x and a 2x display) the reported cell metrics are still at the OLD backing scale — set_content_scale runs later in updateSize. Pinning the old cell px pinned ~half the columns on a 1x->2x move, and forcing wpx to the old backing width made sizeChanged false, defeating deferScaleUntilResized so the grid collapsed when the bigger cell landed over the un-resized screen. Project the reported cell and pad to the scale this resize is about to apply; the ratio is 1 when the scale is unchanged, so it is a no-op then. * portal: restore hosted-view autoresizing masks in deinit (#15) Adoption clears each hosted view's autoresizing mask and detach restores it. A portal that dies without tearDown()/detachHostedView never restored them, leaving a surviving hosted view pinned at [] so the next portal saved [] as its original. Restore inline in deinit (which cannot hop to the @MainActor detach path). * remote-tmux: fail the settle oracle on a grid shortfall or missing sample (#11) The settlement payload computed settled without any grid-parity gate: a short or missing grid only appended to mismatches, so once the budget-capped output-parity re-arm stopped, settled flipped true with a shortfall still listed. Track gridParityReady (false on a no-sample or shortfall branch, judged live-first) and AND it into the sizingReady conjunction, so settled is honest independent of the re-arm budget. * remote-tmux: re-run the sizing pass when a window live-resize ends (#13) A window live-resize whose final geometry arrived before mouse-up left a parked oversized reading with no edge to consume it: onGeometryChange fires only on value change, and the parked-reading consumer holds while inLiveResize is true. Fire setNeedsSizingPass from the probe view's viewDidEndLiveResize — by the time the coalesced pass runs inLiveResize is false, so the consume proceeds. * remote-tmux: correct the mirror grid pin and its parity oracle (#1, #4, #8, #9, #10, #12, #14) Several linked defects in the mirror sizing transaction: - #1: applyAssignedGrids re-pinned a stale grid during a WINDOW live-resize (or interactive geometry drag), painting past the shrinking pane. The divider-drag early return does not cover a window resize, so gate the stale re-pin on the same suppression the view path uses. - #8: under zoom the visible tree is the single zoomed leaf, so hidden but live base panes were unpinned and rendered on a stale grid. Pin each pane from the visible tree or the base tree; clear only panes in neither. - #9: the pin-grow repaint went through the attach-only redraw kick (armed only at .enter), so late-granted cells stayed blank mid-session. Extract the shrink/restore SIGWINCH body into forceRedrawKick(windowIds:) and call it directly on a pin grow. - #10: the stale-repin else-if and gridParityMismatch tested only the under direction, so an over-render (rendered > assigned) was an invisible no-op. Compare with != on both axes; reapplyAssignedGrid clamps either way. - #12: gridParityMismatch read only the ledger, which goes stale because a same-size re-apply returns early before reporting. Read the surface's live grid first, falling back to the ledger. - #4: the parked-container consumer clamped an oversized parked reading to the bound and banked it, overwriting a correct size. Reject it (as the sibling oversized consumer does) and keep the last good container. - #14: rearmIfOutputMissedPlan gated on the plain isVisibleForSizing, which goes stale-true when a hidden tab's view is dismantled; gate on isEffectivelyVisibleForSizing so an offscreen mirror cannot spin re-arms. Tests: parkedHiddenReadingOverTheBoundIsRejectedNotClamped (#4), gridParityFlagsAnOverRenderedPane (#10, #12), and the reworked grid-lag test now pins that an offscreen mirror does not re-arm (#14). * terminal: move the teardown-coordinator debug counter behind the DEBUG boundary * tests: record native free in the deinit ordering test; close the divergence test window Route the deinit teardown through runtimeSurfaceFreeOverrideForTesting like the teardownSurface/suspend paths already do, so the deinit lifetime test can record the native free and assert it lands before the tee-lease release. Also give the self-echo divergence test the same window teardown its neighbors use so it stops leaking its portal. * skills: clarify the live-fuzz host setup and failure recovery Name the dedicated fuzz alias (cmux-fuzzhost, stood up by remote-tmux-fuzz-host.sh) and warn off cmux-srvA/srvB, whose interactive tmux the harness won't clobber and whose dir isn't where ssh-tmux connects. Add a run-once-and-wait note (killing the wrapper orphans the driver) and a failure-message playbook: "no workspace mirroring session 'fuzz'", "refusing to kill an unowned lab", "another fuzz driver is running", and the regenerated-key ssh errors. * remote-tmux: make the window-size claim independent of title-row folding clientGrid subtracted residual(of:), which reads the live parent-minus- children gap. Under pane-border-status the pane-border title row sits in that gap, and it moves in and out of the measured child spans as the window reflows, so the claim changed by a row whenever tmux republished the tree. The claim read its own effect and the refresh-client -C size oscillated (…x39, …x38, …x39) and never settled. Give the claim its own chrome residual computed from the stable model: one native divider per structural boundary (children.count - 1 per split) rather than the assigned gap, plus one title row at the configured window edge. Interior title rows share a separator row that is already charged, so the single edge title is the whole reservation with no double count. The claim now depends only on the container, cell size, structure, and border-status setting, so the same window yields the same claim titled or not and tmux converges. residual(of:) keeps its live-span behavior for the planner and render frame. * remote-tmux: take the first non-empty pane-border-status per rects reply Only panes touching the configured edge carry pane-border-status in their border-status field; interior panes report empty. Taking the last pane's value let a trailing interior pane clear a real top/bottom, flipping the window-level placement reply-to-reply. Combined with the claim reading it, that flipped the title-row reservation and the claim oscillated by a row. Keep the first non-empty value so the placement is stable across replies. * remote-tmux: stop the redraw-kick loop and settle on column parity A live-fuzz bounce: on a vertically split window settle never converged — claimed 108x43, layout 108x42, grids matching, sizing_pass climbing into the tens of thousands. Root cause is tmux's own rounding. We send a CLIENT size; tmux lays out the WINDOW. Columns agree exactly, but when a stacked split has an odd leftover row tmux hands it to one pane or the other from its prior state, so the window height it reports wobbles by a row around one stable claim, and a stacked pane's grid wobbles with it. Two places treated that wobble as something to correct: - The pin-grow redraw kick shrinks then restores the client size to force a repaint of cells granted after tmux streamed them. At an unchanged claim that can't reveal new cells, but the size change makes tmux re-round the split, which re-fires the kick: an unbounded loop (23k kicks in one iteration). The mirror now kicks a pane only when its grid reaches a size not yet refilled at the current claim (a per-pane high-water, reset when the claim changes or the pane leaves). A genuine grow refills once; the ±1 re-round, which never exceeds the high, is starved. - The settle oracle and the claim re-arm required client==window on both axes. Rows can't satisfy that, so assert it only on columns, where it holds, and let grid parity and derivation cover the rest. Also gives each window its own redraw-kick task, so a second window's kick can no longer cancel the first window's restore and strand it shrunk. * remote-tmux: content oracle + churn scenarios for the sizing UI suite The sizing UI suite asserts grid DIMENSIONS (pane_grids: assigned==rendered) but never the actual on-screen TEXT, and it only judges multi-pane mirror windows — so a pane at the right-looking size holding stale content, or a single-pane window (no mirror, no pane_grids entry) rendering a stale frame, passes every existing check. That is the class the live fuzz's text oracle catches and this suite could not. Adds a per-pane content oracle — the fuzz's own probe: a full-width ruler in each pane, then compare the mirror's read-text against tmux capture-pane -J, tolerating the 2s ruler redraw by accepting a match before OR after the read. Five scenarios on a fast-settling two-window lab (one split, one single-pane) exercise the churn edges the class lives on: content parity for a split and a single-pane window, a single-pane window resized from the tmux side, a zoom toggle, a collapse to one pane, and a hidden window churned then revealed. A size-stability wait covers windows assertSettles cannot judge (single-pane, zoomed) since its coherence check assumes a normal multi-pane layout. Extends the test_exec allowlist (DEBUG-only, confined to the UI-test tmux dir) with the commands the oracle drives: capture-pane, select-pane, send-keys, kill-pane, resize-window, and resize-pane -Z/-y. Also documents that this suite runs LOCALLY (it is hermetic — not CI-only), with the sandboxed-agent recipe (ssh hairpin + CMUX_SKIP_ZIG_BUILD=1). * remote-tmux: content oracles must read the named pane's own surface Both content oracles read "the app's focused surface" and compared it against every pane of every tmux window. That cannot verify a named pane. cmux does not follow tmux's active pane or current window — select-pane leaves tmux's current window alone (verified against tmux 3.7), handleActivePaneChanged only moves the strip dot and the directory, and %session-window-changed is only recorded — so the read returns whichever pane the app already showed. It matches the target's capture whenever the two panes share dimensions, because the probe prints the same text at the same size, and mismatches when they do not. That is what the live fuzz reported as a mirror defect on seed 2 iteration 25, every run: it compared window @4's pane %16 (99x35) against window @3's surface. The surface it read reported 118x41 and 160x49 at different moments and @3 claimed both of those sizes; @4's own claim was exactly 99x35, matching tmux. The mirror was right about every window. With the oracle reading %16's own surface, seed 2 runs 25/25 clean. Fixing this needed a seam the app did not expose: which surface renders a given tmux pane. remote.tmux.pane_surfaces reports that map for every mirrored window, single-pane windows included — they have no mirror, so they appear in no other introspection verb, which is also why nothing ever checked them. Each entry carries on_screen: a hidden tab holds its last render by design, so judging it would report a designed lag as a defect. The UI suite's oracle had the same flaw and passed by luck; it now reads by surface id too, and asserts every pane of the window under test is actually on screen so a scenario that forgets to select its tab fails loudly instead of quietly checking nothing. * remote-tmux: follow pane-border-status changes via a control-mode subscription Turning pane-border-status on or off resizes and moves every pane touching the configured edge, but tmux emits no %layout-change for it: the window's layout string does not encode the title row, so tmux considers the layout unchanged. Measured on tmux 3.7 with a control client watching — a 12-row pane at top 0 becomes an 11-row pane at top 1, and the client sees nothing. Pane heights come from the rects fetch a %layout-change drives, so the published tree kept the pre-toggle heights until some unrelated layout event happened to refresh it. Every edge-touching pane then rendered a row off from what tmux actually held, and no internal oracle could see it: they compare the claim against the app's OWN tree, never against tmux's live panes. The live fuzz's text oracle caught it (seed 5, iterations 10 and 11: the app assigned %0/%1 119x11 while tmux said 119x12, border=off, the window size agreeing exactly). tmux does publish the change — just not as a layout event. A control-mode subscription (refresh-client -B, tmux 3.2+) pushes pane-border-status on every change, for hidden windows as well as the current one (both verified on 3.7). The connection already subscribes per-pane for cwd, reflow and header labels, so this follows that idiom: subscribe pane-border-status per window, and on a CHANGE re-read the topology, which restages each window and republishes real geometry through the same path a genuine layout event takes. No polling, no timers. Only a change refetches — tmux pushes the value once on subscribe, and that initial push rides alongside an attach whose rects fetch is already current. Subscriptions belong to the client, so the reconnect reseed drops the watch flags and lets the restage reissue them. Marathon: 5 seeds x 25 iterations, zero failures, zero hangs, zero crashes, with 11 border changes observed and healed. * remote-tmux: repaint a grown pane by reading tmux, not by resizing the client An adversarial review found the rationed redraw kick still dropped genuine repaints, and the reasoning behind the ration was wrong to begin with. When a pane's grid grows after tmux already streamed those rows, the late cells hold nothing: the surface clipped that content while it was short, and tmux repaints only on change. The old repair moved the CLIENT size (shrink a row, restore) to force a SIGWINCH — but that resize makes tmux re-round an odd split, which grows a pane again and re-fires the kick: an unbounded loop, 23k kicks in one fuzz iteration. Rationing it by a per-pane high-water bounded the loop but suppressed real grows, and took the two axes' maxima independently, so 120x30 -> 100x50 -> 110x40 recorded a 120x50 that never existed and the final grow never repainted. The kick was the wrong tool. tmux's own grid HAS the rows — only the mirror lost them — so the repair is to READ tmux's screen: capture-pane plus the pane-state query, both reads. Nothing perturbs tmux, so no split re-rounds, no loop, and every genuine grow repaints exactly once with no budget and no high-water. The capture omits -S: the seed's scrollback is already in the surface and re-emitting it would stack a second copy, while the visible screen is exactly what a clipped grow lost. forceRedrawKick is gone; the attach kick keeps the size-moving form, which is correct there (a fresh client's TUIs must repaint at the size just applied, and a no-op apply sends them no SIGWINCH). Also from the review, each a real hole: - The claim re-arm compared columns only, so a claim tmux never applied was undetectable: 108x35 against a 108x43 claim reported green forever, because every grid check passes when the panes faithfully render the short assignment. Rows cannot be compared exactly either (chrome plus an odd-split remainder), so windowMatchesClaim bounds them: 42-for-43 is chrome, 35-for-43 is a lost pin. - The attach kick still required exact row equality, dropping every window tmux landed a row short — precisely the windows whose TUIs get no SIGWINCH and need the kick. It uses the same predicate now. - pane-border-status subscriptions were forgotten in reseedAfterReconnect, which runs INSIDE the list-windows handler — after the restage that re-issues them — so every surviving window skipped its resubscribe and the option went unwatched for the rest of the connection. Cleared at beginReconnecting() instead. - tmux's first subscription push is not automatically a baseline: it arrives up to a second late, so the option can change between the rects fetch and the push. It is compared against the published window's rects-derived placement. capture-pane joins refresh-client in the send log so 'did the repaint fire?' is answerable from evidence: 50 repaints observed in a 12-iteration run, distinct from the 15 attach seeds. Marathon on this binary: 10 seeds x 25 iterations, 250 iterations, zero failures, zero hangs, zero crashes, 19 border changes healed, zero redraw kicks. * remote-tmux: close three ways the content oracles could pass without testing All three let a green run mean nothing, which is the failure mode that let a broken text oracle stand for hours. The probe could not tell panes apart. Its lines carried only the size, so two panes of equal dimensions printed byte-identical screens — and the fuzz's even splits produce exactly that (200 columns becomes three 66-wide panes). A comparison that read the wrong pane's surface then passed. Every line now carries the pane's own id from TMUX_PANE, verified against tmux 3.7: '%0 040x020 0123…' next to '%1 039x020 0123…', so a wrong surface can never alias a right one. The fuzz let the app pick its own coverage. It asked the app which panes were on screen and only rejected zero — so a pane the app quietly omitted dropped out of the comparison and the run still read green. Every window with an on-screen pane is the visible window, so tmux's own pane census for it must be on screen; a missing pane is now a failure (the app not rendering a pane of the visible window), not less coverage. The UI scenarios discarded the failures of the commands they depend on. A send-keys that never ran left the pane at an idle shell prompt, which the mirror renders faithfully — so every content assertion passed against no probe at all. A resize, zoom or kill that never ran left the scenario asserting on un-churned state. The ruler is now a precondition (poll tmux's capture until its marker appears) and each churn command must report success. paneSurfaceEntries attributed panes by scanning published trees, ignoring the session's authoritative windowIdByPane. A join-pane/swap-pane in flight leaves the source window holding the pane until its reconcile runs, so the scan could report the pane twice, or pick the stale window's frozen surface — whichever came first in dictionary order. It now attributes through the ownership map and keys the result by pane, so neither is representable. Verified: fuzz seed 5 clean, all five UI content scenarios pass with the stricter assertions, ruler pane-identity confirmed against real tmux. --------- Co-authored-by: ejc3 <ejc3@users.noreply.github.com> Co-authored-by: austinpower1258 <austinwang115@gmail.com> | 2 个月前 | |
Fix tmux mirror pane sizing and divider drag synchronization (#7996) * remote-tmux: revive remote.tmux.window as a DEBUG verb so the sizing UI suite can attach The sizing UI suite attaches its lab host through remote.tmux.window, but the handler was removed when mirrors moved into the current window while the verb stayed in the socket policy's worker list — every call hit the loud policy/handler-drift backstop, and the suite has been unrunnable since. The user-facing path is cmux ssh-tmux with its foreground auth handoff, so the raw verb comes back as DEBUG-only test tooling beside test_exec and test_set_frame: a thin alias of remote.tmux.mirror with the retired verb's activate-by-default contract. Release builds now answer method_not_found instead of an internal error. * remote-tmux: render ownership — drag sessions, exact-fit render frames, oversized-reading drops Ports the render-ownership line onto the sizing-transaction rework so the two compose instead of fighting. One geometry writer per split: divider drags are deterministic sessions (bonsplit's mouse-tracking lifecycle), sizing passes hold while a session is live — the hold sits ahead of the pass's intent consumption, so a held recovery pass keeps its intent for drag end — and the drag-end sync is cell-aware: a sub-cell drag rounds to the span tmux already holds, gets no reply, and re-imposes locally through the constraint-recovery intent instead of waiting for one. Bonsplit's retryImposedFirstExtent now refuses while a drag session owns the divider, so recovery passes cannot yank the divider mid-gesture (vendor bump: divider-drag-sessions rebased onto the imposed-first-extent line). The split tree renders at its exact grid-plus-chrome size, top leading, with the region's sub-cell remainder outside the tree as trailing margin; the plan and the drag-fraction reads both use that frame as the denominator. Per the newest upstream change, the drag walk descends with applied extents, not the imposed values. A changed render frame lands a commit after the pass that computed it, so the pass restates the plan once, two turns out. Container readings keep the pending-stash architecture and add two pieces: a probe view planted in the mirror's own subtree is the primary window bound (it survives portal churn that can leave every pane view detached mid-sync), and a reading that exceeds the bound with a size already on record is dropped rather than clamped — clamping banked the bound itself, overstating the region by the window-to-mirror chrome (measured ~30-40pt wide plans at rest in the live fuzz). First-ever readings still clamp so the initial claim exists. The title-row fold fix from the old line is NOT ported: the metrics' edge-placement accounting already matches the measured tmux 3.7 facts (interior title rows are the coordinate gaps between panes; only edge-touching panes carry an extra title charge), and zeroing the separator credit on top of it would double-count. The shared renderedCells points-to-cells helper moves into the package, and a DEBUG parity probe compares each off-assignment sample against what the plan expected through it. The portal skips already-hidden surfaces in bulk geometry syncs, logs only syncs that changed something, and carries a live-resize-gated tripwire naming programmatic window growth. Tests: ownership and exact-fit suites adapted to the planner API, drag-session deferral coverage, oversized-reading drop coverage, the clip-aware many-tabs render test, and the live-fuzz ruler lengthened so panes wider than 609 columns measure honestly. * remote-tmux: regression test — a stale imposed render frame must not inflate the mirror view's reported size * remote-tmux: keep the imposed render frame out of the mirror view's reported size The mirror sized its split tree inline: the imposed grid-plus-chrome frame sat inside a flexible frame, and a flexible frame with no minWidth reports its child's width whenever the child exceeds the proposal. The plan is derived from the banked container while the proposal comes from the live window, so any window shrink left the tree momentarily wider than its region — and that width, instead of overflowing in place, became the view's reported size. Every space-filling ancestor up to the main window's root content inherited it (the content view was observed marching wider than the display-pinned window a step per layout pass), and the mirror's geometry callback then read its own imposed width back as its container, which the oversized-reading guard could only defer, never cure. Render the tree in a top-leading overlay of a region-sized base instead. The base answers every proposal with the proposal, the tree still renders at its exact imposed size (overflowing in place during the transient), and the geometry callback now reads the true region. Also pin the hosting-view side in MainWindowSelfSizingTests: content that over-reports its size must not move the content view's own frame off the window. * remote-tmux: regression test — a stale wide bank must heal through the full pane chain The live growth spiral froze into a state nothing could cure: the mirror banked its container while wide, the window shrank, and the mirror kept rendering the stale-wide imposed tree in a fraction of the space forever. Host the REAL wrapper chain at the desk — main-window hosting view, workspace bonsplit pane, and the mirror view with its live geometry feedback — bank wide, mount into a narrower window, and require the bank to heal to it. Red on purpose: the first geometry callback can run before the probe is in a window, resolveContainerReading defers the reading, and nothing retries — the deferral neither schedules a pass nor gets re-validated once a window exists (the pass-time re-clamp only looks for a window through panelsByPaneId, which portal churn and hidden tabs empty). * remote-tmux: hold a window-less container reading and re-validate it at pass time A geometry reading and the window that validates it are sampled at different times: the first reading often arrives a beat before the probe is in a visible window, and if the region never changes size again no later callback comes. The pending-container stash already holds window-less readings and schedules the pass that adopts them; what could still rot was the recovery pass going blind — it found windows only through live panels, which portal churn and hidden tabs empty. The pass resolves its bound through the probe view planted in the mirror's own subtree, and an injected hosting-bound source that answers nil now falls through to the live probe and pane scan instead of pinning the channel, so the full-chain regression test can seed a stale bank and then hand the mirror to a real window. Two stronger liveness edges — scheduling a pass from the probe's own move-to-window, and sampling the probe's live frame against its window's bound in the same instant — were built and then dropped: the full-chain heal test passes without them on this architecture (verified by running it against the tree with only the stash and the probe-resolved bound). The design doc states the surviving rule: a deferral is only safe with a retry edge, and stashing IS the retry edge here. It also records the render-ownership amendment that re-imposing an identical extent still re-arms one apply. * remote-tmux: frame oracle for the sizing UI suite — root content must hold the window's width The growth spiral kept every tmux-side claim sane while the window's content view marched a step wider per layout pass — the oversized-reading guard dropped the inflated readings, so the grid oracle stayed green throughout. Only the frames can convict that class. remote.tmux.root_frames (DEBUG) reports, for every visible mirror, the hosting window's frame and content-view sizes plus the widths of the mirror's real ancestor chain, probe to root — the tripwire the debug log prints (mirror.container.ancestors), as data. The new scenario shrinks the window under a live imposed plan — the state where the imposed render frame exceeds the region until the next pass — and requires both oracles after each step: sizing settles to exact renders, and the whole ancestor chain holds the window's width. * remote-tmux: sizing suite — hidden-mirror lifecycle scenario and a window ceiling on claims Two situations the fuzz marathon hit that the suite never pinned. A mirror attached while another workspace holds the front takes its first container reading with no visible window to vouch for it, and the live wedge froze exactly there: a deferred reading was dropped, no later callback came, and the mirror rendered a stale-wide tree forever with every claim looking sane. The new scenario drives that lifecycle deterministically — attach with activate=false behind the launch workspace, one test_set_frame resize while hidden, workspace.select to reveal — and holds the reveal to a full settle: exact grids, root content at the window's width, claims under the ceiling. The ceiling is the other gap. A claim is a pure function of window geometry, so claimed cols/rows may never exceed what the hosting window's content area divides to at the calibrated cell size, plus two cells of slack for rounding and the chrome model's separator credit. The wedge pushed 318-column claims against a 248-column layout, and older evidence reached 2614 columns, all while every grid check passed: tmux clamps the layout and the mirror renders the clamped truth, so only the claim itself shows the runaway. The oracle reads claims from sizing_settled, the bound from root_frames, and the cell size from pane_grids calibration; both the new scenario and the shrink scenario assert it after every settle. * remote-tmux: chrome folds credit the coordinate gaps tmux actually assigned The residual folds assumed one separator cell per child boundary and charged edge panes a native title row. Both assumptions break on titled trees: tmux's title rows live in the tree's COORDINATES — the gaps between siblings and the window-edge row — and cost the native render nothing, while the per-pane charge granted the edge pane points nothing native renders, so the surface floored them into a phantom grid row (the rows3-at-1000 sweep failure: pane %6 planned 237pt for an 11-row assignment, rendered 12). Every fold now reads the actual gap cells off the assignment — parent span minus child spans — so a node's extent equals its children's sum by construction, titled or not, and the binary measured fold threads the same gap count so it agrees with the n-ary fold node for node. The binarized tree keeps the ORIGINAL node at the top of each joined group (the synthesized join spans only the children's bounds and lost the window-edge row); structure-only placeholders without spans fall back to the one-cell-per-boundary reading. The measured tree also gains span-derived minimums (minimumSpan, clampToFeasibleFirstSpan) for the drag-send feasibility rule. The parity probe stops subtracting title rows from planned outers — they no longer carry any. Package tests pin the new accounting and the fold agreement. * remote-tmux: clamp drag sends to the grid-feasible range; harden probe registration and drag-end deferral A drag past the sibling's minimum converts to a span tmux cannot assign; like a sub-cell nudge, the resize-pane changes no layout and never gets the reply drag-end waits for — the divider parks off-grid while later passes early-return on unchanged inputs. Drag sends now clamp to the split's feasible first-span range (read off the measured tree's assigned spans), and a request that clamps back to the assigned span routes to the immediate local re-impose instead. The probe view only clears its mirror slot if it is still the registrant: a tab re-show recreates the probe, and AppKit delivers the dying probe's move-to-nil-window after the replacement registered — claiming there shadowed the live probe's window handle with a windowless view. And drag-end consumes the deferred-pass flag before the remote-apply guard, rescheduling the held pass instead of stranding it when a drag ends mid-apply. * remote-tmux: fold-equivalence fuzz, feasibility clamp tests, probe shadowing test The chrome-fold equivalence fuzz pins that the binary measured fold and the n-ary residual fold agree on every node, titled or not — the disagreement class whose size lands on whichever pane the rail allocation starves. The feasibility tests pin the drag-send clamp (starved sibling, nested same-axis minimums, cross-axis overlay, packed titled stacks) plus the parked-divider desk repro with its feasible-drag control, the mid-apply drag-end deferral consumption, and the dying-probe shadowing sequence. * remote-tmux: the claim charges rail slack so every claimed cell is placeable The tight-container fuzz has been red at its boundary the whole time: shave a container to a slack-free claim's edge and the whole-point rail cannot give every pane its cells — with fractional chrome, one side of some split lands a device pixel under a cell boundary and the surface floors the cell away. The minimum-preserving rail allocation cannot save it: when both subtrees' chrome-only minimums are fractional and the container holds exactly their sum, no whole-point boundary leaves both sides at or above their minimums — someone is short by under a point, and under a point is a dropped cell at a boundary. So the claim pays for placement up front again: clientGrid charges the per-pane rail-slack point along with real chrome, claiming at most one column and one row fewer at boundary sizes, and the plan can then place every claimed cell honestly. Claim pins across the tests move by that one point; the planner's minimum-aware allocation stays as the degradation path for containers that shrink mid-flight below even the slack-inclusive claim. * remote-tmux: settle verb judges against the plan the renderer actually imposed The settlement payload re-ran the planner at the raw container size, but the render path plans at the exact-fit render frame — so the judge compared live views against a plan the renderer no longer produces and could report unsettled forever whenever the region's sub-cell remainder exceeded the 1.5pt tolerance. Read the sizing pass's own stashed outer sizes instead, and only re-plan (at the render frame, container as the last resort) when no pass has stashed a plan yet. * remote-tmux: tighten shared sizing helpers and DEBUG gating No behavior changes outside DEBUG logging and test plumbing; the existing suites pin every refactored path. - One assignedGapCells helper computes the assigned-gap cells (parent span minus child spans) for the n-ary residual fold, the measured tree's binary fold, and the minimum-span walk. Each site keeps its own degenerate-span fallback, passed as a parameter. - One exactFitSize helper computes the grid-plus-residual point size for the render frame and the five exact-fit test sites. - Drop sizingPassDeferredForDrag. performSizingPassNow already no-ops on unchanged inputs, so drag end can schedule a pass unconditionally instead of tracking a deferral flag; the drag tests now assert the hold and the reschedule through the scheduler state itself. - Drop the dead paneTitleRowHeight parameter from renderedCells; every caller passed zero. - Share one host-probe ancestor-chain walk between the growth-spiral tripwire log and remote.tmux.root_frames, dump the chain once per window instead of once per dropped reading, and merge the two adjacent DEBUG blocks in noteContainerSize into one symmetric width-or-height check. - Log remote.divider.plan only when the planned outers change. - Drop the remote.tmux.window alias verb (dispatch case and execution policy entry included); the UI suite attaches through remote.tmux.mirror with an explicit activate flag. - UI suite: merge the two attach helpers into attachSession(activate:), share one pane_grids fetch/unwrap, and make the root-frames oracle fail closed when width fields are missing instead of defaulting both sides of the comparison to -1. - Share the stale-wide-bank mirror fixture between the two imposition render wedge tests. - MirrorHostProbeView returns nil from hitTest: it backs the whole mirror region, including the sub-cell margin outside the split tree, and must not swallow clicks there. * remote-tmux: pin re-convergence when applied geometry misses the plan without an input change The sizing transaction proves convergence from its inputs alone: once a completed pass's inputs match the current inputs, every later trigger early-returns. An apply that terminates off-target with no input change (bonsplit parking a divider at a minimum, a retry budget expiring against mid-commit bounds) therefore never gets corrected — the live fuzz held a 1199pt plan against a 984pt view for 50+ seconds while every trigger reported settled. This test hosts a two-pane mirror through the real render chain, lets it converge to its own plan, moves the divider programmatically (geometry changes, no sizing input does), delivers a redundant trigger, and asserts plan == view re-converges. It fails today: the pass early-returns and the views stay off-plan indefinitely. * remote-tmux: verify outcome parity after applying and re-arm a bounded pass on a miss The transaction now checks its own output. Two runloop turns after a completed visible pass — and after any trigger that early-returns on the input fixed point — it compares the outer sizes the last imposition granted against the hosted views' actual frames (the settle payload's comparison, same 1.5pt tolerance). When they disagree, it requests one pass that ignores the settled check, so the plan gets re-imposed from the same inputs. The re-arm is capped at three per input fixed point (the counter resets when inputs change), so an extent bonsplit genuinely cannot hold stops after a bounded correction instead of looping. lastPlannedOuterSizes moves out of DEBUG: it is now the plan side of a release code path, not just the debug parity probe. The fallback branch (no metrics) clears it, since the fraction fallback is not a plan views can be judged against. The design doc records the rule: an apply may never terminate off-target without a re-arm edge, and healthy settle latency is sub-second (the harnesses fail at 8 seconds rather than waiting out a stall). * remote-tmux: fail harnesses at an 8s settle budget and capture evidence at fail time A healthy settle cycle measures ~0.4s end to end, but the live fuzz waited up to 50s (10x2s poll plus 15x2s reconfirm) before calling a window unsettled — long enough for the app to argue its way out of real stalls, which is how a 1199pt-plan-vs-984pt-view wedge rode through a marathon unnoticed. check_iter now polls every second and fails hard at 8s, printing per-iteration settle latency; only the mismatch-while-settled branch keeps a short two-poll reconfirm, because a probe can race the last frame of a transition. The attach barrier is unchanged — reconnect convergence is a separate measurement. note_fail snapshots the debug-log tail (600 lines) the moment a failure is noted, one file per failure in the evidence dir. The marathon's end-of-seed capture stays, but it provably never covers mid-seed failures: both stall captures from the last marathon contained zero stall lines because later iterations had churned the log past the window. The sizing UI suite drops its 10/15/25s budgets to 5s for steady-state assertions and 10s for attach and reveal, and prints each measured settle against its budget. A new scenario pins the liveness rule end to end: a DEBUG verb (remote.tmux.test_perturb_divider) knocks the visible mirror's root divider off the imposed plan without sending anything to tmux — no layout echo, no input change — and the suite asserts the mirror re-converges inside the steady-state budget. * remote-tmux: pin bonsplit at the bounded imposed-divider re-arm Bonsplit's side of the same liveness rule: a container resize that parks an imposed divider now re-arms one bounded apply inside bonsplit (in both the sync path and the pure-AppKit resize path, which never reaches sync). The two edges are complementary — bonsplit re-arms on its own size change, and the mirror's outcome-parity check covers every other way an apply can terminate off-target. * remote-tmux: pin the re-judgment of a reading dropped against a torn window bound During an AppKit window resize, SwiftUI can deliver the correct post-resize slot reading while the window's transient frame still holds the old bound. The oversized guard judges the reading against that torn bound and discards it permanently — geometry callbacks only re-fire when the region changes again, so the mirror freezes at the pre-resize size. The pass-top clamp cannot recover the width either: the window bound overstates the mirror slot by the sidebar. Red on this commit, replaying the fail-time log values: seed 1549x819 against bound 1789x875, deliver 1334x593 against the torn 1250x583 bound, settle the bound at 1574x617 and drain the pass. The container stays at the re-clamped 1549x617 and the claim pushes 193 cols instead of 166: Expectation failed: (mirror.containerSizePt -> (1549.0, 617.0)) == (CGSize(width: 1334, height: 593) -> (1334.0, 593.0)) Expectation failed: (pushed(connection)?.cols -> 193) == 166 The companion test pins the asymmetry the drop guard exists for and is green before and after the fix: a 6133x639 content ideal parked against a 1728x663 bound stays dropped when the settled bound is the same — re-judgment must never decay into a clamp. * remote-tmux: re-judge a dropped container reading once against the settled bound The oversized guard in noteContainerSize drops a reading that exceeds the hosting window's content bound, on the grounds that such a reading is an ancestor's content ideal and says nothing about the true slot. The verdict itself can be wrong: during an AppKit window resize, SwiftUI can deliver the correct post-resize slot reading while window.contentLayoutRect still holds the transient old frame. The reading is truth, the bound is noise — and the bare return discarded it permanently, because geometry callbacks only re-fire when the region changes again. The pass-top clamp cannot recover the width either: its invariant is container <= window content area, and the window bound overstates the mirror slot by the 240pt sidebar. The mirror froze at the pre-resize size with every claim looking sane. Park the dropped reading instead and re-judge it exactly once, at the top of the next sizing pass: if it fits that pass's bound (within the same half-point tolerance the guard uses), it was truth all along and banks verbatim; otherwise it stays discarded. It deliberately does not reuse pendingContainerSizePt — pending consumption clamps to the bound, and clamping a genuinely oversized reading would bank the window bound itself, the poison the drop path exists to prevent. Accept-if-now-fits keeps that anti-poison property and adds only the missing revalidation edge. A reading banked directly clears the parked one, so stale parked truth can never overwrite fresher truth. The design doc's deferred-input section now carries the rule: truth delivered during a torn window state must not be discarded on the noise's verdict — a dropped reading is re-judged once against the next settled bound. * remote-tmux: red tests — single-pane claims must be window-bounded, the hosting view must refuse oversized frames A mirrored single-pane window's size claim comes from its surface's rendered grid, and rendered content is downstream of SwiftUI layout: when a hosting ancestor adopts the content's ideal size, the surface renders at the inflated size, the wider grid claims a wider tmux window, and tmux's reflow grows the content ideal again. Captured live: claims growing ~1.5 columns per 100ms to 781 columns, with the main hosting view at 6373pt inside a 1728pt window. Two failing tests pin the two legs. The claim test drives the real display-pane hook with a 781x200 rendered grid while the surface sits in a 504x400 window and expects the claim to stay under what the window's content area divides to (fails today: 781 > 72 columns, 200 > 28 rows). The hosting-view test hands MainWindowHostingView the frame the layout engine applied live and expects it to stay at the window (fails today: 6373 > 501, 3000 > 401). * remote-tmux: bound single-pane display claims by the hosting window A mirrored single-pane window claimed its size from the surface's rendered grid with nothing bounding it. Rendered content is downstream of SwiftUI layout, so when a hosting ancestor adopts the content's ideal size the surface renders inflated, the wider grid claims a wider tmux window, tmux's reflow grows the content ideal again, and the loop amplifies without bound — captured live as claims growing about 1.5 columns per 100ms until tmux held a 781-column window inside a screen that fits 246, with the main hosting view at 6373pt in a 1728pt window. The multi-pane window mirror is immune by design (its claims derive from the measured container, never from rendered grids); this sibling path violated that invariant. Two changes close the loop. Every single-pane claim hook now routes through one bounded push: the claim is capped at what the hosting window's content area divides to at the sample's cell size. This path has no independently measured slot — every view between the window and the surface is laid out by the same SwiftUI pass the feedback inflates — so the hosting NSWindow, whose frame layout cannot grow, is the strongest honest bound available; in the healthy steady state the rendered grid already equals the slot and the cap never binds. And MainWindowHostingView now refuses frames beyond its window at setFrameSize. Its sizing options and windowDidLayout shadow only govern the hosting view's own sizing paths; the layout engine handed it the inflated frame directly (required constraints from hosted AppKit subtrees resolve by growing containers), and every space-filling descendant — including the terminal surfaces whose rendered grids feed the claims above — inherited the width. Clamping the frame setter kills the amplifier for any other unbounded content too. * remote-tmux: red tests — planned pane extents must respect the rendered floor The divider plan converts tmux-assigned cells straight to points, so a 2-cell pane at 8pt cells plans out to 21pt. bonsplit's pane chrome cannot render below ~32pt (the embedded config asks for a 1pt minimum, but the tab-bar controls' required constraints hold the floor), so the imposition clamps forever and the outcome never matches the target — observed live as plan=21x192 rendered at 32x192 and plan=13x381 at 32x380, a permanently unappliable plan. Two failing tests pin the fix through the production metrics path. A 2-cell-plus-120-cell split must plan the small pane at the floor with the shortfall taken from its sibling and the split still summing to its parent (fails today: 21.0 < 32). And a split whose span cannot afford two floors must degrade deterministically to the proportional division instead of emitting per-ideal extents the renderer resolves unpredictably (fails today: 21 instead of 25 over a 50pt span). * remote-tmux: clamp planned pane extents to the rendered floor The rail allocation now carries a feasibility floor. Production metrics (nativeLayoutMetrics) set minimumPaneExtent to the measured 32pt bonsplit renders as its smallest pane — the embedded config asks for 1pt, but the pane chrome's required constraints refuse less, so any smaller planned extent parks the divider at the floor and the imposition re-arms forever with the outcome never matching the target. bonsplit exposes no constant for this floor, so RemoteTmuxNativeLayoutMetrics.bonsplitMinimumPaneExtent is the one shared definition. railAllocation clamps the exact boundary, before rounding, to the two subtrees' minimum imposable extents (a pane per axis plus the divider between same-axis siblings), so the carry stays the sub-point rounding error and the points the floor takes reach the sibling through its child size — the split still sums to its parent exactly. When a span cannot afford both minimums, it divides in proportion to them, the same even degradation the ideal scale-down uses, instead of emitting per-ideal extents the renderer would resolve unpredictably. Synthetic metrics default to no floor, so the pure-math and fuzz suites keep exercising the unclamped partition arithmetic. * portal: sync one hosted view per anchor callback during window live resize During a live window resize every visible pane's anchor fires its geometry callback in the same layout pass, and each callback ran a full-portal sync: every hosted view re-synced, every visible one reconciled and force-redrawn (layout flush + displayIfNeeded + a ghostty refresh), per callback, per frame. With a session of ~40 mirrored panes that made window drags visibly chug — a main-thread sample during a real drag spent about two thirds of its samples in per-display-cycle layout, a large share of it under this fan-out. A window live resize now takes the coalesced path: each anchor callback syncs only its own hosted view (its geometry is current inside the layout pass, so the pane stays glued), and the per-tick scheduled pass covers panes whose window-relative position changed without their own frame changing. Mid-resize the synchronous redraws are skipped too — a ghostty size change schedules its own repaint, and the forced refresh was also hitting surfaces whose Metal layer was not realized yet. Split-divider drags keep the immediate full-fan-out path unchanged. End-of-resize stays unconditional: windowDidEndLiveResize still runs the full sync with the reconcile-and-refresh sweep, and each terminal view's viewDidEndLiveResize still applies the final surface size with live-resize coalescing bypassed, so the final geometry callback the remote-tmux sizing transaction depends on cannot be missed. * portal: drop the window origin from the geometry sync signature The external-geometry signature is the only terminator of the portal sync echo chain — a pass whose signature matches the last completed pass does no layout and emits no notifications, so echoes die there. It fingerprinted the window's frame WITH its origin, and a titlebar drag changes the origin every tick, so under echo pressure every echoed sync escalated to a full layout pass (four subtree layout flushes plus a sync of every hosted view) whose own notifications scheduled the next. A flight recording of a real drag logged ~2800 lines/sec for ten seconds; a healthy window move costs about 40 lines total. The signature now carries the window's size and its backing scale — the one legitimate origin-correlated input, since moving to a different-DPI screen re-snaps pixel geometry. Every other field was already window-relative. Guards for the regression class: DEBUG counters for sizing passes, output parity re-arms, and full hierarchy syncs, published through remote.tmux.sizing_settled; remote.tmux.test_set_frame accepts optional x/y so it can move a window without resizing it; and a sizing UI scenario that settles a mirror, applies 50 origin-only moves, and requires all three counter deltas to stay zero. An origin-only setFrame lands on the same frame-change path a drag does but skips AppKit's drag tracking loop, so the scenario is a necessary rather than sufficient guard. * remote-tmux: hidden mirror tabs hide their split trees at the AppKit level The workspace bonsplit keeps every tab's content alive (contentViewLifecycle .keepAllAlive) and hides deselected tabs with SwiftUI opacity 0 — which never sets isHidden on the AppKit split trees the embedded mirrors render. A live lldb census found 21 split-view instances stacked in one window whose visible layout has two dividers. The unhidden foreign trees painted their dividers over the visible panes (the phantom interior divider), registered resize-cursor rects under the pointer, and their alpha-0 drop zones sat above the selected tab and rejected pane drops (the embedded config forbids cross-pane tab moves), snapping drags back. Bonsplit already has the AppKit-level switch: isInteractive hides the split tree itself. The mirror view now drives it at the same onAppear/onChange visibility edges that drive isVisibleForSizing, so a deselected tab's tree is hidden from AppKit outright and the reveal edge flips it back before the sizing pass runs. The census test hosts two split mirrors as keep-alive tabs and asserts AppKit sees exactly the selected tab's tree and exactly its dividers, both ways across a tab switch. * remote-tmux: seed divider baselines at drag begin so the first drag after an imposition reaches tmux Imposing a changed extent parks the split's divider baseline at nil, expecting a post-layout geometry callback to record the minimum-clamped outcome. That callback can never arrive: the deferred apply runs under the programmatic-sync guard (didResize returns before onGeometryChange fires), and once the user grabs the divider the drag guard eats every mid-drag callback. Drag end then found no baseline, seeded it from the post-drag fraction, sent nothing — observed live as sent=0 on five of six drags — and re-imposed the pre-drag extent, snapping the divider back in the user's hand. Drag begin now seeds every missing baseline from the model fraction: by that point the deferred apply has landed, so the fraction IS the outcome the nil was waiting for. As a belt, drag end no longer gates the send on baseline presence — the fraction there is the user's committed move, and the cells-versus-assigned feasibility check is the real no-op detector — so even an unseeded baseline cannot swallow a drag. The test recreates the parked-nil state through the production sizing pass, drives the real drag-session hooks, and asserts one resize-pane goes out and the pre-drag extent is not re-imposed. * debug-log: serialize all debug log appends through one sequenced writer Debug builds wrote the shared debug log from two independent paths: DebugEventLog (cmuxDebugLog/logDebugEvent) and bonsplit's dlog each opened a fresh FileHandle per line on their own serial queue and did seekToEnd()+write. The two seeks race, so concurrent lines clobbered each other and landed out of timestamp order — a live capture showed 19:06:31 lines after 19:07 lines and a divider.session line missing entirely, which sent an investigation down a false trail. Log storms also dirtied about 2GB/hr because every line reopened the file. All appends now funnel through DebugEventLog's single serial queue and one kept-open O_APPEND handle; bonsplit installs an external sink at app startup (CmuxMain.main) that forwards dlog lines into the same writer. Every persisted line carries a monotonic #<seq> prefix so any future reordering is visible in captures. Floods are bounded: past 2000 persisted lines per second the disk write is skipped and the next window opens with an explicit 'log.dropped N lines' marker, so lines are never silently lost. Public signatures are unchanged. The new DebugEventLogSerializedAppendTests pump both public paths from concurrent threads and read the file back: every line intact, sequence strictly increasing, and every missing line covered by a dropped marker. On the previous writer they fail (29 of 800 lines lost with no markers, no sequence numbers); they pass on this one. Pins bonsplit at 92093ae (external debug-log sink). * tests: portal lifecycle and mirror resize suites encode the current contracts Four stale expectations surfaced when these suites joined the sizing gate; two also fail on the branch's merge base, so they predate this branch. - testDeferredSyncHidesVisibleHostedViewAfterAnchorDisappears and testHiddenPortalDefersRevealUntilFrameHasUsableSize asserted synchronous effects from synchronizeHostedViewForAnchor. Outside an interactive drag that call now coalesces into a queued portal pass, so the tests settle the queue before asserting the same hide/defer-reveal/reveal outcomes. - testPruneDeadEntriesDetachesAnchorlessHostedView (fails on the merge base too) still encoded the pre-drag-churn prune: a visible entry whose anchor vanished is now kept for tab-drag recovery and hidden by the queued pass; only a non-visible one is pruned and detached. It also counted raw hostView subviews, which includes the divider overlay, so it now counts terminal subviews. - testScheduledExternalGeometrySyncWaitsForQueuedLayoutShift (fails on the merge base too) now drives the non-immediate schedule — the deferred hop is the wait under test — and waits for the queued shift to land instead of spinning the runloop for a fixed 50ms the shared app host can starve. Red at this commit: a folded immediate request flushes the pass early and silently drops the deferred hop; the next commit fixes that. - testWindowScopedExternalGeometrySyncDoesNotRefreshOtherWindows settles queued portal passes before mutating geometry, so a leftover pass cannot masquerade as a cross-window refresh. - The two resize-routing tests compared the whole control stream against a single resize-pane line, but the stream legitimately begins with the attach-time list-windows topology fetch (also on the merge base). They now judge exactly the resize sends. * portal: honor the deferred-hop contract when requests fold into one pass A non-immediate geometry sync request is promised one extra main-queue hop, so a layout mutation queued on the same turn lands before the pass reads geometry. Requests coalesce into whichever pass is already scheduled, and an immediate request in the same burst (window and host frame observers fire constantly during setup and churn) flushed that shared pass on its first hop. The deferred request lost its hop silently, nothing remained scheduled, and the portal parked at geometry read before the queued mutation — a stale hosted frame with no recovery path. Track the un-honored deferred request across the fold and queue one follow-up pass after an early flush; the follow-up dies in the geometry fingerprint check as a no-op once geometry stops changing, so the chain stops one pass after geometry does. Also stop dropping a portal's queued pass when it fires while another portal's pass is on the stack (a re-entrant main-queue drain during that pass's layout): the scheduling flag is already down at that point, so returning without rescheduling lost the request forever. Re-queue it instead. Red test: testScheduledExternalGeometrySyncWaitsForQueuedLayoutShift in the previous commit. * remote-tmux: red tests — portal ownership, divider round-trip holds, resize routing, parked readings Eight tests pinning live-fuzz and review findings, each red against the behavior it names. Portal (the seed-1 hierarchy-sync storm): the portal host must carry no layout-engine constraints — constraints read the engine's solution, and when a hosted subtree's required width demand makes that solution unreachable for the hosting view (it refuses oversized frames), they stomp the host and every hosted terminal view to the unreachable geometry on every layout pass (+175pt uniformly, full_hierarchy_sync in the thousands per settle window). A behavioral companion asserts one sync restores host and hosted frames after an external stomp and holds across later turns. A third asserts a deferred sync request under a held interactive flag stays bounded on static geometry instead of chaining one full pass per runloop turn. Divider round trip: a drag whose send is in flight must not bounce — not from redundant triggers (the parity re-arm reading dragged views against the pre-drag plan), and not from an UNRELATED layout change replanning from a tree that is still pre-drag for the dragged split. A send tmux never answers (a span its cascade minimums clamp to a no-op) must heal at a bounded deadline that re-arms parity rather than leaving the divider parked off-grid with the guard disabled. Resize routing: dragging a divider whose first subtree hides an inner same-axis split must address the pane whose nearest same-axis split IS the dragged one (%33 in the nested shape the control-path routing tests already pin), not the subtree's first pane in order — tmux resizes the target pane's nearest split, so %11 resized the inner split instead. Sizing: a parked oversized container reading must survive a pass that runs during portal darkness (no bound anywhere to judge it against) and bank at the first bounded pass after the reveal — consuming it in the dark lost the one re-judgment and the frozen-claim class returned. * portal: the host frame follows the reference's actual bounds, not the engine's solution Live fuzz seed 1 regressed from 1 failure to 11 at the head of this branch, with a new signature: panes rendering wider than plan by a uniform +175pt while claims stayed exact, and settle windows blown with full_hierarchy_sync counters in the thousands. The chain, read off the fail-time debug logs: a hosted AppKit subtree carried a required width demand beyond the window, so the layout engine's solution for the hosting view exceeded the frame the hosting view actually holds — its frame setter refuses oversize, so the two can never reconcile. The portal host was edge-constrained to the hosting view, which reads the ENGINE's solution: every layout pass stomped the host to the oversized solution (portal.hostFrame.update logged the same reset tens of thousands of times), every hosted terminal view stretched by the same +175pt through autoresizing, and the portal pass that undid it forced the next layout pass. The echo-cut signature never matched two passes in a row, so every pass escalated to a full hierarchy sync; the sizing transaction's parity re-arm spent its budget against the external writer and gave up with the views off plan. The portal already writes the host frame from the reference's ACTUAL bounds on every pass, install, and geometry notification — the constraints were a second writer wired to a different (and here unreachable) value. Drop them; the host frame is portal-owned. A frame-change observer on the reference replaces the one thing the constraints did that the existing observer web did not: catching a reference resize that moves no anchor. * remote-tmux: divider sends carry a keyed, bounded reply hold and route like the control path Drag-end with a sent resize-pane defers to tmux's layout reply, and the first cut of that deferral was a bare flag: set at drag end, cleared by any imposition, guarding the parity re-arm. Review confirmed two holes. A no-op send (the client clamp can produce a span tmux's cascade minimums won't change) gets no %layout-change, so in an idle window the flag leaked forever — divider parked off-grid AND the guard disabled. And any unrelated %layout-change mid-round-trip replanned from a tree still pre-drag for the dragged split, re-imposed the stale extents (the bounce, back under churn), and consumed the flag. The hold is now keyed to the send: split id, axis, and the span asked of tmux. Impositions resolve it against the layout they plan from — only a layout that ASSIGNS the sent span ends the hold (the reply landed); an unrelated replan leaves it standing and skips the held split's subtree, so the user's divider survives churn; a vanished split clears it (the structure changed under the drag). A bounded deadline covers the never-answered send: it releases the hold and re-arms the pass — re-arm, not just clear, so parity heals the parked divider back onto the plan. The send itself now routes like the control path: tmux resizes the TARGET pane's nearest split along the axis, so the pane addressed for the dragged split's first subtree must not sit behind an inner same-axis split. first.paneIDsInOrder.first did exactly that in nested same-axis shapes and tmux resized the inner split (or no-oped — feeding the leak above). The tree's routing rule is now public and both senders share it. * remote-tmux: keep an oversized reading parked through bound-less passes The oversized-container guard parks a rejected reading for exactly one re-judgment against the next settled bound — a mid-resize callback can carry the true post-resize slot while the window's transient frame undersells it, and no later callback re-delivers that truth. The pass consumed the parked reading BEFORE resolving a bound, so a pass running during portal darkness (every hosted view briefly detached or hidden mid-churn: no probe window, no visible pane, no injected bound) cleared it while judging nothing. The reveal path re-registers the host probe but never re-delivers the reading, so the one chance to bank it was gone and the frozen-claim class returned. Consume the parked reading only when a bound exists to judge it; a bound-less pass leaves it parked for the first bounded pass after the reveal. * remote-tmux: divider hold releases on protocol events, never a timer A sent divider resize held the parity re-arm behind a 2-second wall-clock grace: a send tmux swallowed (a span its cascade minimums clamp to a no-op) produced no %layout-change, and only the deadline cleared the hold and recovered. Time encodes a latency assumption — on a slow link the grace fired while the reply was still in flight and bounced the divider; on a fast one it parked the divider off-plan for two full seconds. The release is now anchored on the control stream's own ordering. Every command is answered by an ordered %begin/%end block, and a notification a command causes is emitted after that command's %end but before any block for a command sent later. So the resize rides a tracked send; its ack issues one cheap barrier (an empty display-message block). A barrier ack with no intervening layout event for the window proves the resize changed nothing — release the hold and re-arm the pass so parity heals the divider back onto the plan. A barrier ack that finds the window's layout still quarantined behind its rects fetch defers the verdict to that fetch's resolution (publication or drop — the drop paths now notify so the resolution edge always reaches the mirror). %error recovers immediately, a stream reset fails the tracked completion, and a newer send supersedes an older send's acks by generation. The reply-assigns- the-sent-span release and the structure-change release are unchanged. The old round-trip tests had to lengthen or shorten the grace around their own pumping — the test needing real seconds was the defect. The reworked and new tests drive the stub stream's blocks directly: no-op proof, %error, superseded generation, and late-ack inertness all resolve synchronously on the injected protocol events, with no clock anywhere. * tests: pin tee-lease and manual-IO context release ordering to the native free The ghostty PTY tee callback fires on the io-reader thread for every output chunk until ghostty_surface_free joins that thread, and the MANUAL-mode io_write_cb fires on the io thread the same way. The retained callback userdata must outlive the native free. These tests fail today: every teardown path that defers the free to the runtime teardown coordinator (deinit, and the override-free paths of teardownSurface and agent-hibernation suspend) releases the tee lease and manual IO context immediately, leaving a window where the io-reader thread dereferences freed userdata. * terminal: release tee and manual-IO callback userdata only after the native surface free ghostty's io-reader thread calls the PTY tee callback for every output chunk, and the io thread calls the MANUAL-mode io_write_cb, right up until ghostty_surface_free joins those threads. The teardown paths that defer the free to the runtime teardown coordinator (deinit, and the override-free paths of teardownSurface and agent-hibernation suspend) released the tee lease and manual IO context immediately, so until the coordinator's worker ran the free — seconds later under load — the reader thread could fire the tee callback into freed userdata. That use-after-free killed unit-test app hosts mid-suite and can take down the app on surface close. Transport the tee lease and manual IO context through the teardown request, exactly like the surface callback context, and release all three on the main actor only after freeSurface returns. The free is the happens-before edge that joins the IO threads, so a callback can never observe released userdata. * remote-tmux: bound the divider plan to the region; portal self-writes stop re-arming the sync Live fuzz seed 1, iterations 20 and 21, replayed to two cooperating defects. A reconnect racing a resize left the claimed size and tmux's layout permanently disagreeing, and the assigned tree's exact point size (198 columns, about 1584pt) exceeded the banked region (about 1345pt). Geometry demanded past the container is satisfied by AppKit growing the non-movable window; the next pass read the growth back and the window ratcheted a point per pass to the display cap. Then, stationary, the portal fought over the disagreement at period 2: its own frame writes post frame/bounds notifications synchronously, the geometry observers re-armed the sync on them, and the single-signature guard can never latch an A,B,A,B alternation — full_hierarchy_sync hit 2520 in one settle window. Two rules close it. The plan side gets the invariant plan(w) <= w: the parent a divider plan divides is the exact-fit render frame bounded by the banked region on both axes (regionBoundedPlanParent), so under a claimed-vs-layout disagreement the render degrades to the region and never demands past it. The portal side gets a self-write token: frame writes the portal itself makes (host frame restore, hosted seed and target frames) hold the token, and geometry observers ignore notifications that arrive while it is held — only genuinely external geometry re-arms the sync, so an external stomp costs exactly one sync request and the restoring write buys zero. * docs: reconcile-pass design for remote-tmux sizing Replaces the edge-triggered sizing machinery's execution model with a reconcile pass: events set a dirty generation, one pass per window snapshots its whole world in a single instant, desired state is a pure feasibility-clamped function of the snapshot, the diff classifies every mismatch as awaiting, user-owned, drift, or infeasible-reported, and the commit writes synchronously outside layout callbacks. Fourteen named mechanisms are deleted with their replacing property stated; drag sessions and the command-ack barrier stay. Thirteen edge cases carry termination arguments; migration is three steps under one rule — no step deletes a mechanism whose replacement ships later. Reviewed adversarially by two independent passes; all findings folded, including the unreachable-desired circuit breaker, synchronous commit applies, dirty generations, the three-state claim ledger, and the plan-never-exceeds-region invariant the live fuzz proved this week. * tests: the bounce fixture's reply assigns the span the drag actually sent The fixture's region is narrower than the tmux window, so the dragged fraction converts to fewer cells than the raw tree suggests — a reply hard-coded at the tree-scale span (92) never matches the sent span and the release path it exercises can never fire. Read the sent span off the armed hold and build the reply from it; the release-and-settle assertions then judge the real protocol edge instead of a fixture artifact. * portal tests: bound each test's runtime and add a last-slot leak check TerminalWindowPortalLifecycleTests passes test-by-test but dies when the class runs whole: depending on interleaving the shared app host either livelocks in a SwiftUI reentrant-layout loop during realizeWindowLayout, hangs in removePortal teardown, or crashes on the io-reader thread inside the PTY output tee. Each test leaves state behind for the tests after it — dropped TerminalSurfaces whose native frees and io threads are still in flight, portal windows that never close, and directly-created portals that never see willCloseNotification. This adds the detector first: a leak-check test that runs in the class's last alphabetical slot and asserts the process returned to its pre-suite baseline (registered portals, portal-hosting windows, live runtime surfaces, and — via a new DEBUG counter on the teardown coordinator — native frees still in flight). It is red on this commit: running just testWindowScopedExternalGeometrySyncDoesNotRefreshOtherWindows followed by the leak check crashes the host with the tee use-after-free before the assertions can even report. Each test also gets executionTimeAllowance = 60 so a future livelock fails the one wedged test in minutes instead of hanging the host until the CI job timeout. * portal tests: tear down every window, portal, and surface a test creates The lifecycle suite's tests each stood up real NSWindows (ordered front), WindowTerminalPortals, and live TerminalSurfaces and dropped them at the end of the method. Across a whole-class run that left, for the following tests: native surface frees and io threads still in flight; blocking window-appearance NSAnimations committing CA transactions from background queue threads (two of them in every wedge sample); portals whose NotificationCenter block observers — including an object:nil split-view observer — stayed registered forever; and refcounted interactive-resize state a failed test could leave latched. Track all three through suite helpers and tear them down in tearDown: surfaces are released synchronously (releaseSurfaceForTesting frees the runtime before its io threads can race the next test), directly-created portals get tearDown() since they never see willCloseNotification, and windows are created with animationBehavior none and closed for real. tearDown also asserts the registry portal count returned to its baseline, so a future leak fails the leaking test, not a victim three tests later. Two product-side seams back this up: WindowTerminalPortal removes its notification observers in deinit (a portal that dies without ever seeing willCloseNotification leaked them permanently, in production too), and a DEBUG-only resetInteractiveGeometryStateForTesting clears the refcounted drag state the production API offers no owner handle for. * portal tests: the self-echo test uses the tracked window and portal teardown The suite hygiene rules apply to it like every other test in the class: a tracked window (animation off, closed in tearDown) and a tracked portal instead of an ad-hoc pair the last-slot leak check would count against the baseline. * portal: never force a synchronous surface redraw from inside a layout pass A geometry sync that runs while AppKit is still inside the window's layout pass (syncLayout == false: SwiftUI update callbacks and anchor geometry callbacks) called refreshSurfaceNow when a hosted frame changed or a hidden surface was revealed. displayIfNeeded there reaches ghostty's Metal drawFrame with the window's transaction still open, and waitUntilCompleted waits on a present that only that transaction can commit — the main thread wedges permanently. Seed-1 fuzz reproduced it twice at the same iteration: a programmatic window setFrame is not a live resize, so the live-resize guard did not cover it. Defer those redraws to the next main-queue turn; syncs that already run outside layout keep the synchronous flush. * remote-tmux: publish generation-stale rects replies that cover the current tree Layout publication discarded any rects reply whose generation had been superseded and sent a fresh fetch. Under continuous churn every reply is one generation behind by the time it lands (%layout-change inter-arrival is shorter than a round trip), so publication never advanced: windowsByID froze at the pre-churn tree while the app's claims and tmux kept agreeing, and the settle oracle timed out. Seed-1 fuzz held that starvation for 32 seconds against an 8-second budget once control-stream round trips inflated under load. The coalescing design already intended one publish-then-follow-up cycle (the dirty flag), but staging bumps the generation on every event, so the generation guard always won and the dirty publish-first branch was unreachable in exactly the storm it was built for. Now a stale reply that still covers every pane of the current tree publishes as interim verified state — its rects are the freshest list-panes snapshot observers can have — and owes exactly one follow-up fetch for the newest generation. Publication advances once per round trip and converges on the first quiet one. A stale reply that no longer covers the current tree (structure changed mid-flight) keeps today's behavior: nothing publishes, the owed fetch returns the new structure, and the garbled-reply retry budget is not burned. Also repairs three tests in this suite that still asserted no topology notify on the rects-drop paths; the drop's resolution notify is intentional (a divider hold deferring to 'this window's pending layout resolved' needs the edge) and this suite was missing from the pre-push gate. * portal, terminal, browser: close the remaining synchronous-display holes inside layout passes A deadlock audit of the mirror stack found three more paths in the class d063f19166 fixed — a synchronous surface display reachable while AppKit is still inside a layout pass, where ghostty's Metal draw can wait on a present that only the still-open window transaction can commit: - The interactive-drag branch of the anchor sync ended with an ungated failsafe reconcile that called refreshSurfaceNow one line after the primary sync had correctly deferred. Reachable on every divider or sidebar drag tick. The reconcile now threads syncLayout and defers through the same queue. - setVisibleInUI(true) nudged the surface synchronously, and three of its callers run inside SwiftUI update/layout (updateNSView, viewDidMoveToWindow, the geometry-callback rebind). The nudge now waits one main-queue turn. - The browser panel's hosted-WebKit refresh ended with a whole-window displayIfNeeded from updateNSView, which also flushed sibling Metal terminal panes mid-pass. The flush is now scoped to the panel's own subtree, which has no Metal wait. The first two land with red-first tests that drive the sync from inside a real layout pass; the audit's remaining lower-severity findings are tracked separately. * docs: reconcile design — audit findings, per-layer testing, diagrams, plainer prose Adds the concurrency audit's findings: the two rules the deadlocks proved (no synchronous surface display inside a layout pass; the main thread is the contended resource), the main-actor ingest pipeline as the confirmed round-trip-inflation mechanism (step zero of the migration), and the fragile tier with file references and fix directions. Adds a testing section that separates what gets stress-tested in simulation (the pure desired function, the reconcile loop against simulated ports, turn-based and seeded) from what must run against the real dependencies (AppKit, bonsplit, ghostty, tmux), with the rule that every simulator behavior must be pinned by a real-dependency test. Adds ASCII diagrams for the pass loop, the diff classification, and the main-thread contention picture, and rewrites the longest sentences into plain ones. * docs: reconcile design — testing weight goes to real-dependency assertions The bug ledger says where tests pay off: not one of the day's bugs was the loop mis-stepping on its own state. Every one was our model of a component diverging from what AppKit, bonsplit, ghostty, or tmux actually did. A simulated-world stress harness inherits the model's blind spots, so layer 2 shrinks to a skeleton check and the weight moves to driving the real components with the commit phase's instructions and asserting they handled them as modeled. * docs: pin tmux 3.7's half of the sizing loop from source; correct the no-op barrier rationale The reconcile design gains a tmux section verified against the 3.7 source (file:line cited) so the facts don't have to be re-learned by observation: claims are ceilings in every window-size mode; a control client can't become 'latest'; an unfittable claim clamps the WINDOW UP to the tree minimum, so claimed==layout never converges and the published layout is the feasibility verdict; window-resize redistribution is equal-absolute, so split ratios erode and imposition is permanent work; no-op resizes still emit %layout-change and every claim echoes one per window; %layout-change orders after its command's %end; pty resizes are deferred 250ms per pane; layout rects are borderless cell sizes that ignore border-status rows; a per-window claim makes the client's tty size participate for every other window. One of those facts corrects the divider-hold rationale in the hardened sizing doc, amended with the root cause: 'a no-op resize emits no %layout-change' was written from lab observation and was unfalsifiable by our oracles, because both hypotheses release the hold through the same observable path. tmux notifies unconditionally (layout.c:726-728); the only silent resize is the missing-container case (layout.c:686-687). The shipped mechanism is correct under both readings — the barrier is the ordering fence plus the one silent case — so this is a rationale fix, not a behavior fix. * portal: clear the hosted view's autoresizing mask on adoption Hosted terminal views reach the portal from SwiftUI hosting with autoresizingMask = [.width, .height]. In an Auto Layout window that mask is translated into edge pins - a minX constant plus a trailing margin to the host, no width constraint at all - and the pin distances freeze at whatever geometry the last constraint pass saw. Every host resize then re-derives the pane's size from stale margins against the new host bounds, stomping the frame the portal just wrote. The portal restores plan truth, the next flush re-applies the pin arithmetic, and the two writers alternate once per display refresh: panes sat exactly one host-delta wide of plan (the live 5pt case) while hierarchy syncs ran into the thousands per settle window. Live forensics pinned it: at the moment of a stomp the engine holds no width constant for the pane (engineWidthConstant=nil), just edge pins frozen at the previous generation (portal.stomp.diag, added here as rate-limited DEBUG forensics). A unit fixture reproduces the arithmetic deterministically once the view carries the production mask: growing the window 120pt stretched a 240pt pane to exactly 360. Adoption now clears the mask (bind), re-asserts it on every sync in case reparenting plumbing restores it, and puts the original back on detach. An empty mask translates to rigid position+size constants that always equal the last portal write, so the engine's opinion of a pane IS the portal's last write - there is no second geometry source left to fight, and no frame setter is overridden anywhere (a setter that swallows or rewrites engine applies desyncs engine bookkeeping and NSWindow raises its update-constraints budget exception). The mask test pins adoption/detach and the host-resize decoupling; the divergence suite keeps the convergence guards: a restore survives flushes of any scope, portal writes do not re-arm the sync, and a rapid-resize burst converges without an exception. * docs: pin the autoresizing-mask geometry-writer finding The frame ping-pong took three failed fixes before live forensics named the writer, so the mechanism, the two dead ends (refusing engine writes crashes the constraint budget; redirect-through-super is the same thing in disguise), and the reason every unit fixture missed it (test views are born mask-empty) now live in the reconcile design doc's audit section. * remote-tmux: re-arm a delivered size claim when the layout disagrees tmux is the only authority on whether a size claim actually landed. The sent-pins ledger dedups resends, so a pin the server never honored wedged silently: the reply was lost across a transport gap, or a co-client raced it, or the window-size mode changed - either way the ledger said delivered, dedup suppressed every retry, and the window sat columns wide of the claim while mirrors rendered short of the assignment. The live fuzz caught panes rendering 83 columns against an assignment of 86, persisting through settle and reconfirm, with the text wrapping off tmux truth as the visible symptom. Every %layout-change names the window's actual size, so it is the parity edge: when it disagrees with a claim the ledger says was delivered, drop that ledger entry and resend the claim. The re-arm is budgeted at three per disagreement episode - an infeasible claim (tmux clamps a window up to its tree minimum) disagrees forever and must not become a per-layout-event ping - and agreement or a new claim value opens the next episode. Claims still derive only from measured containers; this resends a decision already made, it never makes one. Also relabels the rendered-short diagnostic's plan= field to planOuter= with the tab-bar accounting spelled out: the plan charges the per-pane tab bar in the pane's outer box while view= is the content below the bar, so a healthy pane reads exactly tab-bar-height shorter there and the constant kept getting misread as a layout bug. * remote-tmux, portal: audit follow-ups before undrafting Reviewed every fix commit on the branch for necessity. Three follow-ups came out of it. The claim-parity re-arm budget now resets on reconnect: episodes are per connection, and a budget spent against the old transport must not suppress re-arms when the reseed's own resends get lost the same way. The engine-constraints test no longer calls autoresizing-translated constraints "the SAFE kind" - the mask finding disproved that generalization for hosted views, and the comment now says what is actually tolerated on the host and why. The divider-drag denominator keeps its unclamped renderFrameSize on purpose, and the comment now explains why the plan's region bound must not be applied there: drags convert against what is actually on screen. * remote-tmux: mirror grids render exactly their tmux-assigned cells A mirror pane's grid derived from its view diverges from tmux whenever the plan and the assignment disagree, and every direction of divergence corrupts the mirrored text. Short: the divider plan can legitimately hand a pane fewer points than its cells need - a starved sibling is the live case, where tmux assigns one column, bonsplit's pane chrome refuses to render below ~31pt, and the rail pays the difference out of the sibling's share (plan parent 707pt, outers 31 + 675, where 86 assigned columns need 691: the pane rendered 83 columns and wrapped off tmux truth). Long: the starved pane itself derived a ~3-column grid from its 31pt floored view, so "END 001x022" never wrapped where tmux wrapped it and the unwrapped read gained seven lines; a taller grid keeps rows tmux never repaints, which read back as stale content. Since the points genuinely are not there - or are there in excess - the grid follows tmux exactly and the view clips or letterboxes the difference: the same answer tmux gives a client whose size disagrees with the window. Mirror surfaces carry their tmux-assigned grid; updateSize pins the applied pixels to precisely the assigned cells at the current cell size plus the surface's own chrome (inert until the surface has real cell metrics), and the sizing pass sets or clears each pane's pin from the layout tree's leaves. The pin lives strictly on the surface-pixel side; claims keep deriving from the measured container alone, so the feedback loop that sank the old view-pinning approach cannot form. The pin arithmetic is a pure function pinned with the live numbers from both directions: 1351px of view and 86 assigned columns at 16px cells pins up to the assigned grid, the one-column pane's 56px view pins down to one column, and missing cell metrics or a degenerate assignment leave the size untouched. Repro: scripts/remote-tmux-fuzz-host.sh <alias> to stand up the local fixture, then scripts/remote-tmux-live-fuzz.sh <alias> 3 2 (seed 3 fails at iteration 1 without this change: pane %0's unwrapped read-screen gains seven lines over tmux capture-pane) or seed 2 x 25 (one row short on a 118x41 pane at iteration 25). * skills: document how to run the remote-tmux layout fuzz The harness scripts each explain themselves, but nothing discoverable tied a commit's 'seed 3, iteration 1' to the commands that replay it. The testing skill now covers the fixture setup, the marathon and single-seed replay invocations, the two settle oracles, what the evidence directory contains, and the quiet-machine rule. * remote-tmux: settle requires derivation parity; hidden readings park instead of dropping Two hardenings against the staleness class the fuzz surfaced today. The settle oracle checked delivery parity only - claim equals tmux's layout - which cannot see a claim tmux honored but that no longer matches what the CURRENT container derives. That is exactly how a stale claim settled green this afternoon while the region could not render the columns it promised. A settled visible window must now be able to re-derive its own claim; hidden mirrors are exempt because they hold their attach-time claim by design. noteContainerSize dropped readings that arrived while hidden, with no park and no replay. Geometry callbacks fire only on change, so a dropped reading was gone for good, and a reveal whose cached re-push happened to be degenerate would leave the claim derived from a pre-hide width. Hidden readings now park in the pending-container channel the sizing pass already judges against a real bound before anything banks; a fresh reveal reading replaces the parked one outright, so hidden geometry still never banks unjudged. * remote-tmux: redraw kick when a pin grows; settle oracle judges the live grid The definitive marathon left two failures, one per mechanism. A pin that grows a mirror's grid after tmux already streamed those rows leaves the late-granted cells blank: the content that belonged there was clipped while the grid was short, and tmux repaints only on change. setAssignedGrid now reports growth and the sizing side answers with the existing coalesced redraw kick, so tmux refills the cells it clipped (seed 2 iteration 25: a 118x41 pane reading back 40 lines). The settle oracle judged rendered grids from the cached applied-resize ledger, and the cache can lag or miss a pin's resize: it failed a pane whose actual surface held exactly its 1x22 assignment while the cache still said 10x18 from an earlier life (seed 3 iterations 1 and 14 - the content comparison passed, only the stale cache complained). The oracle now reads the surface's live grid and keeps the cache only as the no-report-yet fallback. Why the applied-resize sample misses pin resizes is still open; the parity re-arm reads the same ledger, so that staleness gets its own root-cause pass. * remote-tmux: don't pin the mirror grid mid-drag — it painted past the pane Shortening a mirrored pane by dragging a divider briefly painted the pane's content over the neighbor and the window chrome, settling correct at rest. The assigned-grid pin holds the surface at the pane's tmux assignment, and during a divider drag that assignment is the PRE-drag (larger) one: performSizingPassNow holds applyAssignedGrids while a divider drag is active, and tmux hasn't replied with the smaller layout yet, so the pin keeps the surface oversized for the whole drag. updateSize applies that oversized ghostty grid and asks the renderer to repaint it, and that present lands before the deferred divider-drag reconcile clamps the drawable and clip geometry back — so the oversized frame paints across the just-moved divider. Clipping is never disabled; the leak is an oversized surface presented against not-yet-reconciled geometry. updateSize now takes suppressAssignedGridPin, set while an interactive resize is active (window live resize or the registry's interactive geometry flag), and uses the view-derived size instead of the pin. The pin re-establishes at rest: drag end and tmux's layout reply each size the pane again, and the next updateSize runs with the flag clear and re-pins to the assignment. Mid-drag the mirror briefly renders a view-sized grid a few cells off the assignment; manualIONoReflow keeps DECAWM off so no wrap divergence persists once tmux reconciles. Verification is runtime (drag a divider shorter, no overflow) plus the existing pin-arithmetic and portal/sizing suites; the suppression is a one-line gate with no unit seam for a live ghostty surface. * remote-tmux: red test — mirror never drives key focus for a freshly split pane A window mirror renders each pane in its own Bonsplit tree, so a new split pane is marked active/selected but nothing makes its surface the window's first responder: it shows the blue highlight yet takes no keys until clicked. Adds a mirror-layer contract test that spies the (inert) key-focus establishment seam and asserts the mirror drives key focus onto the new pane's own panel at creation. Red today — creation only updates selection. * remote-tmux: give a freshly split mirror pane key focus at creation A window mirror renders each tmux pane as a TerminalPanel in its own Bonsplit tree; those pane panels are never workspace Bonsplit tabs, so the workspace focus path can't resolve them and the surface's active/visibility false->true edges don't apply first responder either. A new split pane was therefore highlighted but untypeable until clicked. Track panes the mirror just created and, the first time such a pane becomes active, drive key focus onto its own surface the way a click does (moveFocus -> makeFirstResponder). Every attempt re-checks the mirror is still on screen and this pane is still its active pane, so a pane switch within the retry window cancels the pending focus instead of stealing it, and a background or headless mirror never moves the first responder. Consumed once per created pane, so a later active-pane echo never re-drives it. * remote-tmux: land the final divider position when a drag ends during a remote apply A divider drag that ended while isApplyingRemoteLayout was set skipped its only syncChangedDividerPositions() and just scheduled a pass, so the user's final position never reached tmux and the next sizing pass restored the pre-drag layout. Defer the send one runloop turn instead: once the apply's synchronous scope clears the flag, flushDeferredDividerDragEnd runs the same conversion+send the undeferred path runs. Test: dragEndDuringRemoteApplyStillSendsTheFinalPosition. * remote-tmux: re-arm on rendered-grid lag and gate parked readings on a settled bound Two sizing residuals in one file: The exact-grid pin does not always follow an assignment that grew between our claim and settle. The input-only settle proof cannot see it — renderedLayout is an input, but a pin applied against stale cell metrics can leave a pane one row short of the grown assignment, so it renders short and wraps while inputs read unchanged. The output-parity re-arm now treats a rendered grid behind its assignment as a miss (gridParityMismatch) and re-imposes; applyAssignedGrids re-applies the pin when the value already matches but the last sample lags. Tests: settleRearmsWhenRenderedGridLagsTheAssignment, gridParityIgnoresPanesThatRenderTheirAssignment. A parked oversized reading was consumed on the next pass regardless of whether the window bound had settled. During a live resize the window still reports its transient (old, smaller) frame, so a valid post-resize reading was judged against noise and discarded for good. Consume only when the hosting window is not in a live resize; live-resize end delivers the settled callback whose pass consumes it. Test: parkedReadingSurvivesALiveResizingWindowBound. * debug-log: count a line persisted only after the disk write succeeds persist() incremented persistedInWindow before the data conversion, handle open, and write, so a failed write dropped the line from disk without counting it — the next window's 'log.dropped N' marker under-reported and the never-silently-drop guarantee broke. Increment persistedInWindow only after a successful write; count a failed conversion, open, or write as dropped. The entry still stays in the in-memory ring. * portal: move sizing diagnostics to the debug boundary; instance-scope the live-resize test override Two maintainability moves out of production TerminalWindowPortal.swift. The process-wide RemoteTmuxSizingDiagnostics counters move to Sources/Debug/RemoteTmux/TerminalWindowPortal+DebugDiagnostics.swift, the existing debug-support boundary. The process-wide isWindowLiveResizeActiveForTesting static becomes an instance property on the portal, so a test drives only its own portal instead of latching interactive state across the shared app-host; the lifecycle and teardown tests inject on the instance. Behavior identical. * terminal: project the assigned-grid pin to the new content scale (#3) On a scale change (e.g. dragging a mirror pane between a 1x and a 2x display) the reported cell metrics are still at the OLD backing scale — set_content_scale runs later in updateSize. Pinning the old cell px pinned ~half the columns on a 1x->2x move, and forcing wpx to the old backing width made sizeChanged false, defeating deferScaleUntilResized so the grid collapsed when the bigger cell landed over the un-resized screen. Project the reported cell and pad to the scale this resize is about to apply; the ratio is 1 when the scale is unchanged, so it is a no-op then. * portal: restore hosted-view autoresizing masks in deinit (#15) Adoption clears each hosted view's autoresizing mask and detach restores it. A portal that dies without tearDown()/detachHostedView never restored them, leaving a surviving hosted view pinned at [] so the next portal saved [] as its original. Restore inline in deinit (which cannot hop to the @MainActor detach path). * remote-tmux: fail the settle oracle on a grid shortfall or missing sample (#11) The settlement payload computed settled without any grid-parity gate: a short or missing grid only appended to mismatches, so once the budget-capped output-parity re-arm stopped, settled flipped true with a shortfall still listed. Track gridParityReady (false on a no-sample or shortfall branch, judged live-first) and AND it into the sizingReady conjunction, so settled is honest independent of the re-arm budget. * remote-tmux: re-run the sizing pass when a window live-resize ends (#13) A window live-resize whose final geometry arrived before mouse-up left a parked oversized reading with no edge to consume it: onGeometryChange fires only on value change, and the parked-reading consumer holds while inLiveResize is true. Fire setNeedsSizingPass from the probe view's viewDidEndLiveResize — by the time the coalesced pass runs inLiveResize is false, so the consume proceeds. * remote-tmux: correct the mirror grid pin and its parity oracle (#1, #4, #8, #9, #10, #12, #14) Several linked defects in the mirror sizing transaction: - #1: applyAssignedGrids re-pinned a stale grid during a WINDOW live-resize (or interactive geometry drag), painting past the shrinking pane. The divider-drag early return does not cover a window resize, so gate the stale re-pin on the same suppression the view path uses. - #8: under zoom the visible tree is the single zoomed leaf, so hidden but live base panes were unpinned and rendered on a stale grid. Pin each pane from the visible tree or the base tree; clear only panes in neither. - #9: the pin-grow repaint went through the attach-only redraw kick (armed only at .enter), so late-granted cells stayed blank mid-session. Extract the shrink/restore SIGWINCH body into forceRedrawKick(windowIds:) and call it directly on a pin grow. - #10: the stale-repin else-if and gridParityMismatch tested only the under direction, so an over-render (rendered > assigned) was an invisible no-op. Compare with != on both axes; reapplyAssignedGrid clamps either way. - #12: gridParityMismatch read only the ledger, which goes stale because a same-size re-apply returns early before reporting. Read the surface's live grid first, falling back to the ledger. - #4: the parked-container consumer clamped an oversized parked reading to the bound and banked it, overwriting a correct size. Reject it (as the sibling oversized consumer does) and keep the last good container. - #14: rearmIfOutputMissedPlan gated on the plain isVisibleForSizing, which goes stale-true when a hidden tab's view is dismantled; gate on isEffectivelyVisibleForSizing so an offscreen mirror cannot spin re-arms. Tests: parkedHiddenReadingOverTheBoundIsRejectedNotClamped (#4), gridParityFlagsAnOverRenderedPane (#10, #12), and the reworked grid-lag test now pins that an offscreen mirror does not re-arm (#14). * terminal: move the teardown-coordinator debug counter behind the DEBUG boundary * tests: record native free in the deinit ordering test; close the divergence test window Route the deinit teardown through runtimeSurfaceFreeOverrideForTesting like the teardownSurface/suspend paths already do, so the deinit lifetime test can record the native free and assert it lands before the tee-lease release. Also give the self-echo divergence test the same window teardown its neighbors use so it stops leaking its portal. * skills: clarify the live-fuzz host setup and failure recovery Name the dedicated fuzz alias (cmux-fuzzhost, stood up by remote-tmux-fuzz-host.sh) and warn off cmux-srvA/srvB, whose interactive tmux the harness won't clobber and whose dir isn't where ssh-tmux connects. Add a run-once-and-wait note (killing the wrapper orphans the driver) and a failure-message playbook: "no workspace mirroring session 'fuzz'", "refusing to kill an unowned lab", "another fuzz driver is running", and the regenerated-key ssh errors. * remote-tmux: make the window-size claim independent of title-row folding clientGrid subtracted residual(of:), which reads the live parent-minus- children gap. Under pane-border-status the pane-border title row sits in that gap, and it moves in and out of the measured child spans as the window reflows, so the claim changed by a row whenever tmux republished the tree. The claim read its own effect and the refresh-client -C size oscillated (…x39, …x38, …x39) and never settled. Give the claim its own chrome residual computed from the stable model: one native divider per structural boundary (children.count - 1 per split) rather than the assigned gap, plus one title row at the configured window edge. Interior title rows share a separator row that is already charged, so the single edge title is the whole reservation with no double count. The claim now depends only on the container, cell size, structure, and border-status setting, so the same window yields the same claim titled or not and tmux converges. residual(of:) keeps its live-span behavior for the planner and render frame. * remote-tmux: take the first non-empty pane-border-status per rects reply Only panes touching the configured edge carry pane-border-status in their border-status field; interior panes report empty. Taking the last pane's value let a trailing interior pane clear a real top/bottom, flipping the window-level placement reply-to-reply. Combined with the claim reading it, that flipped the title-row reservation and the claim oscillated by a row. Keep the first non-empty value so the placement is stable across replies. * remote-tmux: stop the redraw-kick loop and settle on column parity A live-fuzz bounce: on a vertically split window settle never converged — claimed 108x43, layout 108x42, grids matching, sizing_pass climbing into the tens of thousands. Root cause is tmux's own rounding. We send a CLIENT size; tmux lays out the WINDOW. Columns agree exactly, but when a stacked split has an odd leftover row tmux hands it to one pane or the other from its prior state, so the window height it reports wobbles by a row around one stable claim, and a stacked pane's grid wobbles with it. Two places treated that wobble as something to correct: - The pin-grow redraw kick shrinks then restores the client size to force a repaint of cells granted after tmux streamed them. At an unchanged claim that can't reveal new cells, but the size change makes tmux re-round the split, which re-fires the kick: an unbounded loop (23k kicks in one iteration). The mirror now kicks a pane only when its grid reaches a size not yet refilled at the current claim (a per-pane high-water, reset when the claim changes or the pane leaves). A genuine grow refills once; the ±1 re-round, which never exceeds the high, is starved. - The settle oracle and the claim re-arm required client==window on both axes. Rows can't satisfy that, so assert it only on columns, where it holds, and let grid parity and derivation cover the rest. Also gives each window its own redraw-kick task, so a second window's kick can no longer cancel the first window's restore and strand it shrunk. * remote-tmux: content oracle + churn scenarios for the sizing UI suite The sizing UI suite asserts grid DIMENSIONS (pane_grids: assigned==rendered) but never the actual on-screen TEXT, and it only judges multi-pane mirror windows — so a pane at the right-looking size holding stale content, or a single-pane window (no mirror, no pane_grids entry) rendering a stale frame, passes every existing check. That is the class the live fuzz's text oracle catches and this suite could not. Adds a per-pane content oracle — the fuzz's own probe: a full-width ruler in each pane, then compare the mirror's read-text against tmux capture-pane -J, tolerating the 2s ruler redraw by accepting a match before OR after the read. Five scenarios on a fast-settling two-window lab (one split, one single-pane) exercise the churn edges the class lives on: content parity for a split and a single-pane window, a single-pane window resized from the tmux side, a zoom toggle, a collapse to one pane, and a hidden window churned then revealed. A size-stability wait covers windows assertSettles cannot judge (single-pane, zoomed) since its coherence check assumes a normal multi-pane layout. Extends the test_exec allowlist (DEBUG-only, confined to the UI-test tmux dir) with the commands the oracle drives: capture-pane, select-pane, send-keys, kill-pane, resize-window, and resize-pane -Z/-y. Also documents that this suite runs LOCALLY (it is hermetic — not CI-only), with the sandboxed-agent recipe (ssh hairpin + CMUX_SKIP_ZIG_BUILD=1). * remote-tmux: content oracles must read the named pane's own surface Both content oracles read "the app's focused surface" and compared it against every pane of every tmux window. That cannot verify a named pane. cmux does not follow tmux's active pane or current window — select-pane leaves tmux's current window alone (verified against tmux 3.7), handleActivePaneChanged only moves the strip dot and the directory, and %session-window-changed is only recorded — so the read returns whichever pane the app already showed. It matches the target's capture whenever the two panes share dimensions, because the probe prints the same text at the same size, and mismatches when they do not. That is what the live fuzz reported as a mirror defect on seed 2 iteration 25, every run: it compared window @4's pane %16 (99x35) against window @3's surface. The surface it read reported 118x41 and 160x49 at different moments and @3 claimed both of those sizes; @4's own claim was exactly 99x35, matching tmux. The mirror was right about every window. With the oracle reading %16's own surface, seed 2 runs 25/25 clean. Fixing this needed a seam the app did not expose: which surface renders a given tmux pane. remote.tmux.pane_surfaces reports that map for every mirrored window, single-pane windows included — they have no mirror, so they appear in no other introspection verb, which is also why nothing ever checked them. Each entry carries on_screen: a hidden tab holds its last render by design, so judging it would report a designed lag as a defect. The UI suite's oracle had the same flaw and passed by luck; it now reads by surface id too, and asserts every pane of the window under test is actually on screen so a scenario that forgets to select its tab fails loudly instead of quietly checking nothing. * remote-tmux: follow pane-border-status changes via a control-mode subscription Turning pane-border-status on or off resizes and moves every pane touching the configured edge, but tmux emits no %layout-change for it: the window's layout string does not encode the title row, so tmux considers the layout unchanged. Measured on tmux 3.7 with a control client watching — a 12-row pane at top 0 becomes an 11-row pane at top 1, and the client sees nothing. Pane heights come from the rects fetch a %layout-change drives, so the published tree kept the pre-toggle heights until some unrelated layout event happened to refresh it. Every edge-touching pane then rendered a row off from what tmux actually held, and no internal oracle could see it: they compare the claim against the app's OWN tree, never against tmux's live panes. The live fuzz's text oracle caught it (seed 5, iterations 10 and 11: the app assigned %0/%1 119x11 while tmux said 119x12, border=off, the window size agreeing exactly). tmux does publish the change — just not as a layout event. A control-mode subscription (refresh-client -B, tmux 3.2+) pushes pane-border-status on every change, for hidden windows as well as the current one (both verified on 3.7). The connection already subscribes per-pane for cwd, reflow and header labels, so this follows that idiom: subscribe pane-border-status per window, and on a CHANGE re-read the topology, which restages each window and republishes real geometry through the same path a genuine layout event takes. No polling, no timers. Only a change refetches — tmux pushes the value once on subscribe, and that initial push rides alongside an attach whose rects fetch is already current. Subscriptions belong to the client, so the reconnect reseed drops the watch flags and lets the restage reissue them. Marathon: 5 seeds x 25 iterations, zero failures, zero hangs, zero crashes, with 11 border changes observed and healed. * remote-tmux: repaint a grown pane by reading tmux, not by resizing the client An adversarial review found the rationed redraw kick still dropped genuine repaints, and the reasoning behind the ration was wrong to begin with. When a pane's grid grows after tmux already streamed those rows, the late cells hold nothing: the surface clipped that content while it was short, and tmux repaints only on change. The old repair moved the CLIENT size (shrink a row, restore) to force a SIGWINCH — but that resize makes tmux re-round an odd split, which grows a pane again and re-fires the kick: an unbounded loop, 23k kicks in one fuzz iteration. Rationing it by a per-pane high-water bounded the loop but suppressed real grows, and took the two axes' maxima independently, so 120x30 -> 100x50 -> 110x40 recorded a 120x50 that never existed and the final grow never repainted. The kick was the wrong tool. tmux's own grid HAS the rows — only the mirror lost them — so the repair is to READ tmux's screen: capture-pane plus the pane-state query, both reads. Nothing perturbs tmux, so no split re-rounds, no loop, and every genuine grow repaints exactly once with no budget and no high-water. The capture omits -S: the seed's scrollback is already in the surface and re-emitting it would stack a second copy, while the visible screen is exactly what a clipped grow lost. forceRedrawKick is gone; the attach kick keeps the size-moving form, which is correct there (a fresh client's TUIs must repaint at the size just applied, and a no-op apply sends them no SIGWINCH). Also from the review, each a real hole: - The claim re-arm compared columns only, so a claim tmux never applied was undetectable: 108x35 against a 108x43 claim reported green forever, because every grid check passes when the panes faithfully render the short assignment. Rows cannot be compared exactly either (chrome plus an odd-split remainder), so windowMatchesClaim bounds them: 42-for-43 is chrome, 35-for-43 is a lost pin. - The attach kick still required exact row equality, dropping every window tmux landed a row short — precisely the windows whose TUIs get no SIGWINCH and need the kick. It uses the same predicate now. - pane-border-status subscriptions were forgotten in reseedAfterReconnect, which runs INSIDE the list-windows handler — after the restage that re-issues them — so every surviving window skipped its resubscribe and the option went unwatched for the rest of the connection. Cleared at beginReconnecting() instead. - tmux's first subscription push is not automatically a baseline: it arrives up to a second late, so the option can change between the rects fetch and the push. It is compared against the published window's rects-derived placement. capture-pane joins refresh-client in the send log so 'did the repaint fire?' is answerable from evidence: 50 repaints observed in a 12-iteration run, distinct from the 15 attach seeds. Marathon on this binary: 10 seeds x 25 iterations, 250 iterations, zero failures, zero hangs, zero crashes, 19 border changes healed, zero redraw kicks. * remote-tmux: close three ways the content oracles could pass without testing All three let a green run mean nothing, which is the failure mode that let a broken text oracle stand for hours. The probe could not tell panes apart. Its lines carried only the size, so two panes of equal dimensions printed byte-identical screens — and the fuzz's even splits produce exactly that (200 columns becomes three 66-wide panes). A comparison that read the wrong pane's surface then passed. Every line now carries the pane's own id from TMUX_PANE, verified against tmux 3.7: '%0 040x020 0123…' next to '%1 039x020 0123…', so a wrong surface can never alias a right one. The fuzz let the app pick its own coverage. It asked the app which panes were on screen and only rejected zero — so a pane the app quietly omitted dropped out of the comparison and the run still read green. Every window with an on-screen pane is the visible window, so tmux's own pane census for it must be on screen; a missing pane is now a failure (the app not rendering a pane of the visible window), not less coverage. The UI scenarios discarded the failures of the commands they depend on. A send-keys that never ran left the pane at an idle shell prompt, which the mirror renders faithfully — so every content assertion passed against no probe at all. A resize, zoom or kill that never ran left the scenario asserting on un-churned state. The ruler is now a precondition (poll tmux's capture until its marker appears) and each churn command must report success. paneSurfaceEntries attributed panes by scanning published trees, ignoring the session's authoritative windowIdByPane. A join-pane/swap-pane in flight leaves the source window holding the pane until its reconcile runs, so the scan could report the pane twice, or pick the stale window's frozen surface — whichever came first in dictionary order. It now attributes through the ownership map and keys the result by pane, so neither is representable. Verified: fuzz seed 5 clean, all five UI content scenarios pass with the stricter assertions, ruler pane-identity confirmed against real tmux. --------- Co-authored-by: ejc3 <ejc3@users.noreply.github.com> Co-authored-by: austinpower1258 <austinwang115@gmail.com> | 2 个月前 | |
Stop XCTest crashes from reaching Sentry (#8786) * test: cover Sentry startup under XCTest * fix: disable macOS Sentry under XCTest * Make Sentry startup policy constructable * test: cover sandbox-denied CLI socket telemetry * fix: drop sandbox-denied CLI socket telemetry * fix: allow explicit Sentry opt-in under XCTest * Address Sentry startup review policy * Close Sentry test launch review gaps * Require provenance for Sentry suppression * Document Sentry suppression contract --------- Co-authored-by: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> | 2 个月前 | |
Fix tmux mirror pane sizing and divider drag synchronization (#7996) * remote-tmux: revive remote.tmux.window as a DEBUG verb so the sizing UI suite can attach The sizing UI suite attaches its lab host through remote.tmux.window, but the handler was removed when mirrors moved into the current window while the verb stayed in the socket policy's worker list — every call hit the loud policy/handler-drift backstop, and the suite has been unrunnable since. The user-facing path is cmux ssh-tmux with its foreground auth handoff, so the raw verb comes back as DEBUG-only test tooling beside test_exec and test_set_frame: a thin alias of remote.tmux.mirror with the retired verb's activate-by-default contract. Release builds now answer method_not_found instead of an internal error. * remote-tmux: render ownership — drag sessions, exact-fit render frames, oversized-reading drops Ports the render-ownership line onto the sizing-transaction rework so the two compose instead of fighting. One geometry writer per split: divider drags are deterministic sessions (bonsplit's mouse-tracking lifecycle), sizing passes hold while a session is live — the hold sits ahead of the pass's intent consumption, so a held recovery pass keeps its intent for drag end — and the drag-end sync is cell-aware: a sub-cell drag rounds to the span tmux already holds, gets no reply, and re-imposes locally through the constraint-recovery intent instead of waiting for one. Bonsplit's retryImposedFirstExtent now refuses while a drag session owns the divider, so recovery passes cannot yank the divider mid-gesture (vendor bump: divider-drag-sessions rebased onto the imposed-first-extent line). The split tree renders at its exact grid-plus-chrome size, top leading, with the region's sub-cell remainder outside the tree as trailing margin; the plan and the drag-fraction reads both use that frame as the denominator. Per the newest upstream change, the drag walk descends with applied extents, not the imposed values. A changed render frame lands a commit after the pass that computed it, so the pass restates the plan once, two turns out. Container readings keep the pending-stash architecture and add two pieces: a probe view planted in the mirror's own subtree is the primary window bound (it survives portal churn that can leave every pane view detached mid-sync), and a reading that exceeds the bound with a size already on record is dropped rather than clamped — clamping banked the bound itself, overstating the region by the window-to-mirror chrome (measured ~30-40pt wide plans at rest in the live fuzz). First-ever readings still clamp so the initial claim exists. The title-row fold fix from the old line is NOT ported: the metrics' edge-placement accounting already matches the measured tmux 3.7 facts (interior title rows are the coordinate gaps between panes; only edge-touching panes carry an extra title charge), and zeroing the separator credit on top of it would double-count. The shared renderedCells points-to-cells helper moves into the package, and a DEBUG parity probe compares each off-assignment sample against what the plan expected through it. The portal skips already-hidden surfaces in bulk geometry syncs, logs only syncs that changed something, and carries a live-resize-gated tripwire naming programmatic window growth. Tests: ownership and exact-fit suites adapted to the planner API, drag-session deferral coverage, oversized-reading drop coverage, the clip-aware many-tabs render test, and the live-fuzz ruler lengthened so panes wider than 609 columns measure honestly. * remote-tmux: regression test — a stale imposed render frame must not inflate the mirror view's reported size * remote-tmux: keep the imposed render frame out of the mirror view's reported size The mirror sized its split tree inline: the imposed grid-plus-chrome frame sat inside a flexible frame, and a flexible frame with no minWidth reports its child's width whenever the child exceeds the proposal. The plan is derived from the banked container while the proposal comes from the live window, so any window shrink left the tree momentarily wider than its region — and that width, instead of overflowing in place, became the view's reported size. Every space-filling ancestor up to the main window's root content inherited it (the content view was observed marching wider than the display-pinned window a step per layout pass), and the mirror's geometry callback then read its own imposed width back as its container, which the oversized-reading guard could only defer, never cure. Render the tree in a top-leading overlay of a region-sized base instead. The base answers every proposal with the proposal, the tree still renders at its exact imposed size (overflowing in place during the transient), and the geometry callback now reads the true region. Also pin the hosting-view side in MainWindowSelfSizingTests: content that over-reports its size must not move the content view's own frame off the window. * remote-tmux: regression test — a stale wide bank must heal through the full pane chain The live growth spiral froze into a state nothing could cure: the mirror banked its container while wide, the window shrank, and the mirror kept rendering the stale-wide imposed tree in a fraction of the space forever. Host the REAL wrapper chain at the desk — main-window hosting view, workspace bonsplit pane, and the mirror view with its live geometry feedback — bank wide, mount into a narrower window, and require the bank to heal to it. Red on purpose: the first geometry callback can run before the probe is in a window, resolveContainerReading defers the reading, and nothing retries — the deferral neither schedules a pass nor gets re-validated once a window exists (the pass-time re-clamp only looks for a window through panelsByPaneId, which portal churn and hidden tabs empty). * remote-tmux: hold a window-less container reading and re-validate it at pass time A geometry reading and the window that validates it are sampled at different times: the first reading often arrives a beat before the probe is in a visible window, and if the region never changes size again no later callback comes. The pending-container stash already holds window-less readings and schedules the pass that adopts them; what could still rot was the recovery pass going blind — it found windows only through live panels, which portal churn and hidden tabs empty. The pass resolves its bound through the probe view planted in the mirror's own subtree, and an injected hosting-bound source that answers nil now falls through to the live probe and pane scan instead of pinning the channel, so the full-chain regression test can seed a stale bank and then hand the mirror to a real window. Two stronger liveness edges — scheduling a pass from the probe's own move-to-window, and sampling the probe's live frame against its window's bound in the same instant — were built and then dropped: the full-chain heal test passes without them on this architecture (verified by running it against the tree with only the stash and the probe-resolved bound). The design doc states the surviving rule: a deferral is only safe with a retry edge, and stashing IS the retry edge here. It also records the render-ownership amendment that re-imposing an identical extent still re-arms one apply. * remote-tmux: frame oracle for the sizing UI suite — root content must hold the window's width The growth spiral kept every tmux-side claim sane while the window's content view marched a step wider per layout pass — the oversized-reading guard dropped the inflated readings, so the grid oracle stayed green throughout. Only the frames can convict that class. remote.tmux.root_frames (DEBUG) reports, for every visible mirror, the hosting window's frame and content-view sizes plus the widths of the mirror's real ancestor chain, probe to root — the tripwire the debug log prints (mirror.container.ancestors), as data. The new scenario shrinks the window under a live imposed plan — the state where the imposed render frame exceeds the region until the next pass — and requires both oracles after each step: sizing settles to exact renders, and the whole ancestor chain holds the window's width. * remote-tmux: sizing suite — hidden-mirror lifecycle scenario and a window ceiling on claims Two situations the fuzz marathon hit that the suite never pinned. A mirror attached while another workspace holds the front takes its first container reading with no visible window to vouch for it, and the live wedge froze exactly there: a deferred reading was dropped, no later callback came, and the mirror rendered a stale-wide tree forever with every claim looking sane. The new scenario drives that lifecycle deterministically — attach with activate=false behind the launch workspace, one test_set_frame resize while hidden, workspace.select to reveal — and holds the reveal to a full settle: exact grids, root content at the window's width, claims under the ceiling. The ceiling is the other gap. A claim is a pure function of window geometry, so claimed cols/rows may never exceed what the hosting window's content area divides to at the calibrated cell size, plus two cells of slack for rounding and the chrome model's separator credit. The wedge pushed 318-column claims against a 248-column layout, and older evidence reached 2614 columns, all while every grid check passed: tmux clamps the layout and the mirror renders the clamped truth, so only the claim itself shows the runaway. The oracle reads claims from sizing_settled, the bound from root_frames, and the cell size from pane_grids calibration; both the new scenario and the shrink scenario assert it after every settle. * remote-tmux: chrome folds credit the coordinate gaps tmux actually assigned The residual folds assumed one separator cell per child boundary and charged edge panes a native title row. Both assumptions break on titled trees: tmux's title rows live in the tree's COORDINATES — the gaps between siblings and the window-edge row — and cost the native render nothing, while the per-pane charge granted the edge pane points nothing native renders, so the surface floored them into a phantom grid row (the rows3-at-1000 sweep failure: pane %6 planned 237pt for an 11-row assignment, rendered 12). Every fold now reads the actual gap cells off the assignment — parent span minus child spans — so a node's extent equals its children's sum by construction, titled or not, and the binary measured fold threads the same gap count so it agrees with the n-ary fold node for node. The binarized tree keeps the ORIGINAL node at the top of each joined group (the synthesized join spans only the children's bounds and lost the window-edge row); structure-only placeholders without spans fall back to the one-cell-per-boundary reading. The measured tree also gains span-derived minimums (minimumSpan, clampToFeasibleFirstSpan) for the drag-send feasibility rule. The parity probe stops subtracting title rows from planned outers — they no longer carry any. Package tests pin the new accounting and the fold agreement. * remote-tmux: clamp drag sends to the grid-feasible range; harden probe registration and drag-end deferral A drag past the sibling's minimum converts to a span tmux cannot assign; like a sub-cell nudge, the resize-pane changes no layout and never gets the reply drag-end waits for — the divider parks off-grid while later passes early-return on unchanged inputs. Drag sends now clamp to the split's feasible first-span range (read off the measured tree's assigned spans), and a request that clamps back to the assigned span routes to the immediate local re-impose instead. The probe view only clears its mirror slot if it is still the registrant: a tab re-show recreates the probe, and AppKit delivers the dying probe's move-to-nil-window after the replacement registered — claiming there shadowed the live probe's window handle with a windowless view. And drag-end consumes the deferred-pass flag before the remote-apply guard, rescheduling the held pass instead of stranding it when a drag ends mid-apply. * remote-tmux: fold-equivalence fuzz, feasibility clamp tests, probe shadowing test The chrome-fold equivalence fuzz pins that the binary measured fold and the n-ary residual fold agree on every node, titled or not — the disagreement class whose size lands on whichever pane the rail allocation starves. The feasibility tests pin the drag-send clamp (starved sibling, nested same-axis minimums, cross-axis overlay, packed titled stacks) plus the parked-divider desk repro with its feasible-drag control, the mid-apply drag-end deferral consumption, and the dying-probe shadowing sequence. * remote-tmux: the claim charges rail slack so every claimed cell is placeable The tight-container fuzz has been red at its boundary the whole time: shave a container to a slack-free claim's edge and the whole-point rail cannot give every pane its cells — with fractional chrome, one side of some split lands a device pixel under a cell boundary and the surface floors the cell away. The minimum-preserving rail allocation cannot save it: when both subtrees' chrome-only minimums are fractional and the container holds exactly their sum, no whole-point boundary leaves both sides at or above their minimums — someone is short by under a point, and under a point is a dropped cell at a boundary. So the claim pays for placement up front again: clientGrid charges the per-pane rail-slack point along with real chrome, claiming at most one column and one row fewer at boundary sizes, and the plan can then place every claimed cell honestly. Claim pins across the tests move by that one point; the planner's minimum-aware allocation stays as the degradation path for containers that shrink mid-flight below even the slack-inclusive claim. * remote-tmux: settle verb judges against the plan the renderer actually imposed The settlement payload re-ran the planner at the raw container size, but the render path plans at the exact-fit render frame — so the judge compared live views against a plan the renderer no longer produces and could report unsettled forever whenever the region's sub-cell remainder exceeded the 1.5pt tolerance. Read the sizing pass's own stashed outer sizes instead, and only re-plan (at the render frame, container as the last resort) when no pass has stashed a plan yet. * remote-tmux: tighten shared sizing helpers and DEBUG gating No behavior changes outside DEBUG logging and test plumbing; the existing suites pin every refactored path. - One assignedGapCells helper computes the assigned-gap cells (parent span minus child spans) for the n-ary residual fold, the measured tree's binary fold, and the minimum-span walk. Each site keeps its own degenerate-span fallback, passed as a parameter. - One exactFitSize helper computes the grid-plus-residual point size for the render frame and the five exact-fit test sites. - Drop sizingPassDeferredForDrag. performSizingPassNow already no-ops on unchanged inputs, so drag end can schedule a pass unconditionally instead of tracking a deferral flag; the drag tests now assert the hold and the reschedule through the scheduler state itself. - Drop the dead paneTitleRowHeight parameter from renderedCells; every caller passed zero. - Share one host-probe ancestor-chain walk between the growth-spiral tripwire log and remote.tmux.root_frames, dump the chain once per window instead of once per dropped reading, and merge the two adjacent DEBUG blocks in noteContainerSize into one symmetric width-or-height check. - Log remote.divider.plan only when the planned outers change. - Drop the remote.tmux.window alias verb (dispatch case and execution policy entry included); the UI suite attaches through remote.tmux.mirror with an explicit activate flag. - UI suite: merge the two attach helpers into attachSession(activate:), share one pane_grids fetch/unwrap, and make the root-frames oracle fail closed when width fields are missing instead of defaulting both sides of the comparison to -1. - Share the stale-wide-bank mirror fixture between the two imposition render wedge tests. - MirrorHostProbeView returns nil from hitTest: it backs the whole mirror region, including the sub-cell margin outside the split tree, and must not swallow clicks there. * remote-tmux: pin re-convergence when applied geometry misses the plan without an input change The sizing transaction proves convergence from its inputs alone: once a completed pass's inputs match the current inputs, every later trigger early-returns. An apply that terminates off-target with no input change (bonsplit parking a divider at a minimum, a retry budget expiring against mid-commit bounds) therefore never gets corrected — the live fuzz held a 1199pt plan against a 984pt view for 50+ seconds while every trigger reported settled. This test hosts a two-pane mirror through the real render chain, lets it converge to its own plan, moves the divider programmatically (geometry changes, no sizing input does), delivers a redundant trigger, and asserts plan == view re-converges. It fails today: the pass early-returns and the views stay off-plan indefinitely. * remote-tmux: verify outcome parity after applying and re-arm a bounded pass on a miss The transaction now checks its own output. Two runloop turns after a completed visible pass — and after any trigger that early-returns on the input fixed point — it compares the outer sizes the last imposition granted against the hosted views' actual frames (the settle payload's comparison, same 1.5pt tolerance). When they disagree, it requests one pass that ignores the settled check, so the plan gets re-imposed from the same inputs. The re-arm is capped at three per input fixed point (the counter resets when inputs change), so an extent bonsplit genuinely cannot hold stops after a bounded correction instead of looping. lastPlannedOuterSizes moves out of DEBUG: it is now the plan side of a release code path, not just the debug parity probe. The fallback branch (no metrics) clears it, since the fraction fallback is not a plan views can be judged against. The design doc records the rule: an apply may never terminate off-target without a re-arm edge, and healthy settle latency is sub-second (the harnesses fail at 8 seconds rather than waiting out a stall). * remote-tmux: fail harnesses at an 8s settle budget and capture evidence at fail time A healthy settle cycle measures ~0.4s end to end, but the live fuzz waited up to 50s (10x2s poll plus 15x2s reconfirm) before calling a window unsettled — long enough for the app to argue its way out of real stalls, which is how a 1199pt-plan-vs-984pt-view wedge rode through a marathon unnoticed. check_iter now polls every second and fails hard at 8s, printing per-iteration settle latency; only the mismatch-while-settled branch keeps a short two-poll reconfirm, because a probe can race the last frame of a transition. The attach barrier is unchanged — reconnect convergence is a separate measurement. note_fail snapshots the debug-log tail (600 lines) the moment a failure is noted, one file per failure in the evidence dir. The marathon's end-of-seed capture stays, but it provably never covers mid-seed failures: both stall captures from the last marathon contained zero stall lines because later iterations had churned the log past the window. The sizing UI suite drops its 10/15/25s budgets to 5s for steady-state assertions and 10s for attach and reveal, and prints each measured settle against its budget. A new scenario pins the liveness rule end to end: a DEBUG verb (remote.tmux.test_perturb_divider) knocks the visible mirror's root divider off the imposed plan without sending anything to tmux — no layout echo, no input change — and the suite asserts the mirror re-converges inside the steady-state budget. * remote-tmux: pin bonsplit at the bounded imposed-divider re-arm Bonsplit's side of the same liveness rule: a container resize that parks an imposed divider now re-arms one bounded apply inside bonsplit (in both the sync path and the pure-AppKit resize path, which never reaches sync). The two edges are complementary — bonsplit re-arms on its own size change, and the mirror's outcome-parity check covers every other way an apply can terminate off-target. * remote-tmux: pin the re-judgment of a reading dropped against a torn window bound During an AppKit window resize, SwiftUI can deliver the correct post-resize slot reading while the window's transient frame still holds the old bound. The oversized guard judges the reading against that torn bound and discards it permanently — geometry callbacks only re-fire when the region changes again, so the mirror freezes at the pre-resize size. The pass-top clamp cannot recover the width either: the window bound overstates the mirror slot by the sidebar. Red on this commit, replaying the fail-time log values: seed 1549x819 against bound 1789x875, deliver 1334x593 against the torn 1250x583 bound, settle the bound at 1574x617 and drain the pass. The container stays at the re-clamped 1549x617 and the claim pushes 193 cols instead of 166: Expectation failed: (mirror.containerSizePt -> (1549.0, 617.0)) == (CGSize(width: 1334, height: 593) -> (1334.0, 593.0)) Expectation failed: (pushed(connection)?.cols -> 193) == 166 The companion test pins the asymmetry the drop guard exists for and is green before and after the fix: a 6133x639 content ideal parked against a 1728x663 bound stays dropped when the settled bound is the same — re-judgment must never decay into a clamp. * remote-tmux: re-judge a dropped container reading once against the settled bound The oversized guard in noteContainerSize drops a reading that exceeds the hosting window's content bound, on the grounds that such a reading is an ancestor's content ideal and says nothing about the true slot. The verdict itself can be wrong: during an AppKit window resize, SwiftUI can deliver the correct post-resize slot reading while window.contentLayoutRect still holds the transient old frame. The reading is truth, the bound is noise — and the bare return discarded it permanently, because geometry callbacks only re-fire when the region changes again. The pass-top clamp cannot recover the width either: its invariant is container <= window content area, and the window bound overstates the mirror slot by the 240pt sidebar. The mirror froze at the pre-resize size with every claim looking sane. Park the dropped reading instead and re-judge it exactly once, at the top of the next sizing pass: if it fits that pass's bound (within the same half-point tolerance the guard uses), it was truth all along and banks verbatim; otherwise it stays discarded. It deliberately does not reuse pendingContainerSizePt — pending consumption clamps to the bound, and clamping a genuinely oversized reading would bank the window bound itself, the poison the drop path exists to prevent. Accept-if-now-fits keeps that anti-poison property and adds only the missing revalidation edge. A reading banked directly clears the parked one, so stale parked truth can never overwrite fresher truth. The design doc's deferred-input section now carries the rule: truth delivered during a torn window state must not be discarded on the noise's verdict — a dropped reading is re-judged once against the next settled bound. * remote-tmux: red tests — single-pane claims must be window-bounded, the hosting view must refuse oversized frames A mirrored single-pane window's size claim comes from its surface's rendered grid, and rendered content is downstream of SwiftUI layout: when a hosting ancestor adopts the content's ideal size, the surface renders at the inflated size, the wider grid claims a wider tmux window, and tmux's reflow grows the content ideal again. Captured live: claims growing ~1.5 columns per 100ms to 781 columns, with the main hosting view at 6373pt inside a 1728pt window. Two failing tests pin the two legs. The claim test drives the real display-pane hook with a 781x200 rendered grid while the surface sits in a 504x400 window and expects the claim to stay under what the window's content area divides to (fails today: 781 > 72 columns, 200 > 28 rows). The hosting-view test hands MainWindowHostingView the frame the layout engine applied live and expects it to stay at the window (fails today: 6373 > 501, 3000 > 401). * remote-tmux: bound single-pane display claims by the hosting window A mirrored single-pane window claimed its size from the surface's rendered grid with nothing bounding it. Rendered content is downstream of SwiftUI layout, so when a hosting ancestor adopts the content's ideal size the surface renders inflated, the wider grid claims a wider tmux window, tmux's reflow grows the content ideal again, and the loop amplifies without bound — captured live as claims growing about 1.5 columns per 100ms until tmux held a 781-column window inside a screen that fits 246, with the main hosting view at 6373pt in a 1728pt window. The multi-pane window mirror is immune by design (its claims derive from the measured container, never from rendered grids); this sibling path violated that invariant. Two changes close the loop. Every single-pane claim hook now routes through one bounded push: the claim is capped at what the hosting window's content area divides to at the sample's cell size. This path has no independently measured slot — every view between the window and the surface is laid out by the same SwiftUI pass the feedback inflates — so the hosting NSWindow, whose frame layout cannot grow, is the strongest honest bound available; in the healthy steady state the rendered grid already equals the slot and the cap never binds. And MainWindowHostingView now refuses frames beyond its window at setFrameSize. Its sizing options and windowDidLayout shadow only govern the hosting view's own sizing paths; the layout engine handed it the inflated frame directly (required constraints from hosted AppKit subtrees resolve by growing containers), and every space-filling descendant — including the terminal surfaces whose rendered grids feed the claims above — inherited the width. Clamping the frame setter kills the amplifier for any other unbounded content too. * remote-tmux: red tests — planned pane extents must respect the rendered floor The divider plan converts tmux-assigned cells straight to points, so a 2-cell pane at 8pt cells plans out to 21pt. bonsplit's pane chrome cannot render below ~32pt (the embedded config asks for a 1pt minimum, but the tab-bar controls' required constraints hold the floor), so the imposition clamps forever and the outcome never matches the target — observed live as plan=21x192 rendered at 32x192 and plan=13x381 at 32x380, a permanently unappliable plan. Two failing tests pin the fix through the production metrics path. A 2-cell-plus-120-cell split must plan the small pane at the floor with the shortfall taken from its sibling and the split still summing to its parent (fails today: 21.0 < 32). And a split whose span cannot afford two floors must degrade deterministically to the proportional division instead of emitting per-ideal extents the renderer resolves unpredictably (fails today: 21 instead of 25 over a 50pt span). * remote-tmux: clamp planned pane extents to the rendered floor The rail allocation now carries a feasibility floor. Production metrics (nativeLayoutMetrics) set minimumPaneExtent to the measured 32pt bonsplit renders as its smallest pane — the embedded config asks for 1pt, but the pane chrome's required constraints refuse less, so any smaller planned extent parks the divider at the floor and the imposition re-arms forever with the outcome never matching the target. bonsplit exposes no constant for this floor, so RemoteTmuxNativeLayoutMetrics.bonsplitMinimumPaneExtent is the one shared definition. railAllocation clamps the exact boundary, before rounding, to the two subtrees' minimum imposable extents (a pane per axis plus the divider between same-axis siblings), so the carry stays the sub-point rounding error and the points the floor takes reach the sibling through its child size — the split still sums to its parent exactly. When a span cannot afford both minimums, it divides in proportion to them, the same even degradation the ideal scale-down uses, instead of emitting per-ideal extents the renderer would resolve unpredictably. Synthetic metrics default to no floor, so the pure-math and fuzz suites keep exercising the unclamped partition arithmetic. * portal: sync one hosted view per anchor callback during window live resize During a live window resize every visible pane's anchor fires its geometry callback in the same layout pass, and each callback ran a full-portal sync: every hosted view re-synced, every visible one reconciled and force-redrawn (layout flush + displayIfNeeded + a ghostty refresh), per callback, per frame. With a session of ~40 mirrored panes that made window drags visibly chug — a main-thread sample during a real drag spent about two thirds of its samples in per-display-cycle layout, a large share of it under this fan-out. A window live resize now takes the coalesced path: each anchor callback syncs only its own hosted view (its geometry is current inside the layout pass, so the pane stays glued), and the per-tick scheduled pass covers panes whose window-relative position changed without their own frame changing. Mid-resize the synchronous redraws are skipped too — a ghostty size change schedules its own repaint, and the forced refresh was also hitting surfaces whose Metal layer was not realized yet. Split-divider drags keep the immediate full-fan-out path unchanged. End-of-resize stays unconditional: windowDidEndLiveResize still runs the full sync with the reconcile-and-refresh sweep, and each terminal view's viewDidEndLiveResize still applies the final surface size with live-resize coalescing bypassed, so the final geometry callback the remote-tmux sizing transaction depends on cannot be missed. * portal: drop the window origin from the geometry sync signature The external-geometry signature is the only terminator of the portal sync echo chain — a pass whose signature matches the last completed pass does no layout and emits no notifications, so echoes die there. It fingerprinted the window's frame WITH its origin, and a titlebar drag changes the origin every tick, so under echo pressure every echoed sync escalated to a full layout pass (four subtree layout flushes plus a sync of every hosted view) whose own notifications scheduled the next. A flight recording of a real drag logged ~2800 lines/sec for ten seconds; a healthy window move costs about 40 lines total. The signature now carries the window's size and its backing scale — the one legitimate origin-correlated input, since moving to a different-DPI screen re-snaps pixel geometry. Every other field was already window-relative. Guards for the regression class: DEBUG counters for sizing passes, output parity re-arms, and full hierarchy syncs, published through remote.tmux.sizing_settled; remote.tmux.test_set_frame accepts optional x/y so it can move a window without resizing it; and a sizing UI scenario that settles a mirror, applies 50 origin-only moves, and requires all three counter deltas to stay zero. An origin-only setFrame lands on the same frame-change path a drag does but skips AppKit's drag tracking loop, so the scenario is a necessary rather than sufficient guard. * remote-tmux: hidden mirror tabs hide their split trees at the AppKit level The workspace bonsplit keeps every tab's content alive (contentViewLifecycle .keepAllAlive) and hides deselected tabs with SwiftUI opacity 0 — which never sets isHidden on the AppKit split trees the embedded mirrors render. A live lldb census found 21 split-view instances stacked in one window whose visible layout has two dividers. The unhidden foreign trees painted their dividers over the visible panes (the phantom interior divider), registered resize-cursor rects under the pointer, and their alpha-0 drop zones sat above the selected tab and rejected pane drops (the embedded config forbids cross-pane tab moves), snapping drags back. Bonsplit already has the AppKit-level switch: isInteractive hides the split tree itself. The mirror view now drives it at the same onAppear/onChange visibility edges that drive isVisibleForSizing, so a deselected tab's tree is hidden from AppKit outright and the reveal edge flips it back before the sizing pass runs. The census test hosts two split mirrors as keep-alive tabs and asserts AppKit sees exactly the selected tab's tree and exactly its dividers, both ways across a tab switch. * remote-tmux: seed divider baselines at drag begin so the first drag after an imposition reaches tmux Imposing a changed extent parks the split's divider baseline at nil, expecting a post-layout geometry callback to record the minimum-clamped outcome. That callback can never arrive: the deferred apply runs under the programmatic-sync guard (didResize returns before onGeometryChange fires), and once the user grabs the divider the drag guard eats every mid-drag callback. Drag end then found no baseline, seeded it from the post-drag fraction, sent nothing — observed live as sent=0 on five of six drags — and re-imposed the pre-drag extent, snapping the divider back in the user's hand. Drag begin now seeds every missing baseline from the model fraction: by that point the deferred apply has landed, so the fraction IS the outcome the nil was waiting for. As a belt, drag end no longer gates the send on baseline presence — the fraction there is the user's committed move, and the cells-versus-assigned feasibility check is the real no-op detector — so even an unseeded baseline cannot swallow a drag. The test recreates the parked-nil state through the production sizing pass, drives the real drag-session hooks, and asserts one resize-pane goes out and the pre-drag extent is not re-imposed. * debug-log: serialize all debug log appends through one sequenced writer Debug builds wrote the shared debug log from two independent paths: DebugEventLog (cmuxDebugLog/logDebugEvent) and bonsplit's dlog each opened a fresh FileHandle per line on their own serial queue and did seekToEnd()+write. The two seeks race, so concurrent lines clobbered each other and landed out of timestamp order — a live capture showed 19:06:31 lines after 19:07 lines and a divider.session line missing entirely, which sent an investigation down a false trail. Log storms also dirtied about 2GB/hr because every line reopened the file. All appends now funnel through DebugEventLog's single serial queue and one kept-open O_APPEND handle; bonsplit installs an external sink at app startup (CmuxMain.main) that forwards dlog lines into the same writer. Every persisted line carries a monotonic #<seq> prefix so any future reordering is visible in captures. Floods are bounded: past 2000 persisted lines per second the disk write is skipped and the next window opens with an explicit 'log.dropped N lines' marker, so lines are never silently lost. Public signatures are unchanged. The new DebugEventLogSerializedAppendTests pump both public paths from concurrent threads and read the file back: every line intact, sequence strictly increasing, and every missing line covered by a dropped marker. On the previous writer they fail (29 of 800 lines lost with no markers, no sequence numbers); they pass on this one. Pins bonsplit at 92093ae (external debug-log sink). * tests: portal lifecycle and mirror resize suites encode the current contracts Four stale expectations surfaced when these suites joined the sizing gate; two also fail on the branch's merge base, so they predate this branch. - testDeferredSyncHidesVisibleHostedViewAfterAnchorDisappears and testHiddenPortalDefersRevealUntilFrameHasUsableSize asserted synchronous effects from synchronizeHostedViewForAnchor. Outside an interactive drag that call now coalesces into a queued portal pass, so the tests settle the queue before asserting the same hide/defer-reveal/reveal outcomes. - testPruneDeadEntriesDetachesAnchorlessHostedView (fails on the merge base too) still encoded the pre-drag-churn prune: a visible entry whose anchor vanished is now kept for tab-drag recovery and hidden by the queued pass; only a non-visible one is pruned and detached. It also counted raw hostView subviews, which includes the divider overlay, so it now counts terminal subviews. - testScheduledExternalGeometrySyncWaitsForQueuedLayoutShift (fails on the merge base too) now drives the non-immediate schedule — the deferred hop is the wait under test — and waits for the queued shift to land instead of spinning the runloop for a fixed 50ms the shared app host can starve. Red at this commit: a folded immediate request flushes the pass early and silently drops the deferred hop; the next commit fixes that. - testWindowScopedExternalGeometrySyncDoesNotRefreshOtherWindows settles queued portal passes before mutating geometry, so a leftover pass cannot masquerade as a cross-window refresh. - The two resize-routing tests compared the whole control stream against a single resize-pane line, but the stream legitimately begins with the attach-time list-windows topology fetch (also on the merge base). They now judge exactly the resize sends. * portal: honor the deferred-hop contract when requests fold into one pass A non-immediate geometry sync request is promised one extra main-queue hop, so a layout mutation queued on the same turn lands before the pass reads geometry. Requests coalesce into whichever pass is already scheduled, and an immediate request in the same burst (window and host frame observers fire constantly during setup and churn) flushed that shared pass on its first hop. The deferred request lost its hop silently, nothing remained scheduled, and the portal parked at geometry read before the queued mutation — a stale hosted frame with no recovery path. Track the un-honored deferred request across the fold and queue one follow-up pass after an early flush; the follow-up dies in the geometry fingerprint check as a no-op once geometry stops changing, so the chain stops one pass after geometry does. Also stop dropping a portal's queued pass when it fires while another portal's pass is on the stack (a re-entrant main-queue drain during that pass's layout): the scheduling flag is already down at that point, so returning without rescheduling lost the request forever. Re-queue it instead. Red test: testScheduledExternalGeometrySyncWaitsForQueuedLayoutShift in the previous commit. * remote-tmux: red tests — portal ownership, divider round-trip holds, resize routing, parked readings Eight tests pinning live-fuzz and review findings, each red against the behavior it names. Portal (the seed-1 hierarchy-sync storm): the portal host must carry no layout-engine constraints — constraints read the engine's solution, and when a hosted subtree's required width demand makes that solution unreachable for the hosting view (it refuses oversized frames), they stomp the host and every hosted terminal view to the unreachable geometry on every layout pass (+175pt uniformly, full_hierarchy_sync in the thousands per settle window). A behavioral companion asserts one sync restores host and hosted frames after an external stomp and holds across later turns. A third asserts a deferred sync request under a held interactive flag stays bounded on static geometry instead of chaining one full pass per runloop turn. Divider round trip: a drag whose send is in flight must not bounce — not from redundant triggers (the parity re-arm reading dragged views against the pre-drag plan), and not from an UNRELATED layout change replanning from a tree that is still pre-drag for the dragged split. A send tmux never answers (a span its cascade minimums clamp to a no-op) must heal at a bounded deadline that re-arms parity rather than leaving the divider parked off-grid with the guard disabled. Resize routing: dragging a divider whose first subtree hides an inner same-axis split must address the pane whose nearest same-axis split IS the dragged one (%33 in the nested shape the control-path routing tests already pin), not the subtree's first pane in order — tmux resizes the target pane's nearest split, so %11 resized the inner split instead. Sizing: a parked oversized container reading must survive a pass that runs during portal darkness (no bound anywhere to judge it against) and bank at the first bounded pass after the reveal — consuming it in the dark lost the one re-judgment and the frozen-claim class returned. * portal: the host frame follows the reference's actual bounds, not the engine's solution Live fuzz seed 1 regressed from 1 failure to 11 at the head of this branch, with a new signature: panes rendering wider than plan by a uniform +175pt while claims stayed exact, and settle windows blown with full_hierarchy_sync counters in the thousands. The chain, read off the fail-time debug logs: a hosted AppKit subtree carried a required width demand beyond the window, so the layout engine's solution for the hosting view exceeded the frame the hosting view actually holds — its frame setter refuses oversize, so the two can never reconcile. The portal host was edge-constrained to the hosting view, which reads the ENGINE's solution: every layout pass stomped the host to the oversized solution (portal.hostFrame.update logged the same reset tens of thousands of times), every hosted terminal view stretched by the same +175pt through autoresizing, and the portal pass that undid it forced the next layout pass. The echo-cut signature never matched two passes in a row, so every pass escalated to a full hierarchy sync; the sizing transaction's parity re-arm spent its budget against the external writer and gave up with the views off plan. The portal already writes the host frame from the reference's ACTUAL bounds on every pass, install, and geometry notification — the constraints were a second writer wired to a different (and here unreachable) value. Drop them; the host frame is portal-owned. A frame-change observer on the reference replaces the one thing the constraints did that the existing observer web did not: catching a reference resize that moves no anchor. * remote-tmux: divider sends carry a keyed, bounded reply hold and route like the control path Drag-end with a sent resize-pane defers to tmux's layout reply, and the first cut of that deferral was a bare flag: set at drag end, cleared by any imposition, guarding the parity re-arm. Review confirmed two holes. A no-op send (the client clamp can produce a span tmux's cascade minimums won't change) gets no %layout-change, so in an idle window the flag leaked forever — divider parked off-grid AND the guard disabled. And any unrelated %layout-change mid-round-trip replanned from a tree still pre-drag for the dragged split, re-imposed the stale extents (the bounce, back under churn), and consumed the flag. The hold is now keyed to the send: split id, axis, and the span asked of tmux. Impositions resolve it against the layout they plan from — only a layout that ASSIGNS the sent span ends the hold (the reply landed); an unrelated replan leaves it standing and skips the held split's subtree, so the user's divider survives churn; a vanished split clears it (the structure changed under the drag). A bounded deadline covers the never-answered send: it releases the hold and re-arms the pass — re-arm, not just clear, so parity heals the parked divider back onto the plan. The send itself now routes like the control path: tmux resizes the TARGET pane's nearest split along the axis, so the pane addressed for the dragged split's first subtree must not sit behind an inner same-axis split. first.paneIDsInOrder.first did exactly that in nested same-axis shapes and tmux resized the inner split (or no-oped — feeding the leak above). The tree's routing rule is now public and both senders share it. * remote-tmux: keep an oversized reading parked through bound-less passes The oversized-container guard parks a rejected reading for exactly one re-judgment against the next settled bound — a mid-resize callback can carry the true post-resize slot while the window's transient frame undersells it, and no later callback re-delivers that truth. The pass consumed the parked reading BEFORE resolving a bound, so a pass running during portal darkness (every hosted view briefly detached or hidden mid-churn: no probe window, no visible pane, no injected bound) cleared it while judging nothing. The reveal path re-registers the host probe but never re-delivers the reading, so the one chance to bank it was gone and the frozen-claim class returned. Consume the parked reading only when a bound exists to judge it; a bound-less pass leaves it parked for the first bounded pass after the reveal. * remote-tmux: divider hold releases on protocol events, never a timer A sent divider resize held the parity re-arm behind a 2-second wall-clock grace: a send tmux swallowed (a span its cascade minimums clamp to a no-op) produced no %layout-change, and only the deadline cleared the hold and recovered. Time encodes a latency assumption — on a slow link the grace fired while the reply was still in flight and bounced the divider; on a fast one it parked the divider off-plan for two full seconds. The release is now anchored on the control stream's own ordering. Every command is answered by an ordered %begin/%end block, and a notification a command causes is emitted after that command's %end but before any block for a command sent later. So the resize rides a tracked send; its ack issues one cheap barrier (an empty display-message block). A barrier ack with no intervening layout event for the window proves the resize changed nothing — release the hold and re-arm the pass so parity heals the divider back onto the plan. A barrier ack that finds the window's layout still quarantined behind its rects fetch defers the verdict to that fetch's resolution (publication or drop — the drop paths now notify so the resolution edge always reaches the mirror). %error recovers immediately, a stream reset fails the tracked completion, and a newer send supersedes an older send's acks by generation. The reply-assigns- the-sent-span release and the structure-change release are unchanged. The old round-trip tests had to lengthen or shorten the grace around their own pumping — the test needing real seconds was the defect. The reworked and new tests drive the stub stream's blocks directly: no-op proof, %error, superseded generation, and late-ack inertness all resolve synchronously on the injected protocol events, with no clock anywhere. * tests: pin tee-lease and manual-IO context release ordering to the native free The ghostty PTY tee callback fires on the io-reader thread for every output chunk until ghostty_surface_free joins that thread, and the MANUAL-mode io_write_cb fires on the io thread the same way. The retained callback userdata must outlive the native free. These tests fail today: every teardown path that defers the free to the runtime teardown coordinator (deinit, and the override-free paths of teardownSurface and agent-hibernation suspend) releases the tee lease and manual IO context immediately, leaving a window where the io-reader thread dereferences freed userdata. * terminal: release tee and manual-IO callback userdata only after the native surface free ghostty's io-reader thread calls the PTY tee callback for every output chunk, and the io thread calls the MANUAL-mode io_write_cb, right up until ghostty_surface_free joins those threads. The teardown paths that defer the free to the runtime teardown coordinator (deinit, and the override-free paths of teardownSurface and agent-hibernation suspend) released the tee lease and manual IO context immediately, so until the coordinator's worker ran the free — seconds later under load — the reader thread could fire the tee callback into freed userdata. That use-after-free killed unit-test app hosts mid-suite and can take down the app on surface close. Transport the tee lease and manual IO context through the teardown request, exactly like the surface callback context, and release all three on the main actor only after freeSurface returns. The free is the happens-before edge that joins the IO threads, so a callback can never observe released userdata. * remote-tmux: bound the divider plan to the region; portal self-writes stop re-arming the sync Live fuzz seed 1, iterations 20 and 21, replayed to two cooperating defects. A reconnect racing a resize left the claimed size and tmux's layout permanently disagreeing, and the assigned tree's exact point size (198 columns, about 1584pt) exceeded the banked region (about 1345pt). Geometry demanded past the container is satisfied by AppKit growing the non-movable window; the next pass read the growth back and the window ratcheted a point per pass to the display cap. Then, stationary, the portal fought over the disagreement at period 2: its own frame writes post frame/bounds notifications synchronously, the geometry observers re-armed the sync on them, and the single-signature guard can never latch an A,B,A,B alternation — full_hierarchy_sync hit 2520 in one settle window. Two rules close it. The plan side gets the invariant plan(w) <= w: the parent a divider plan divides is the exact-fit render frame bounded by the banked region on both axes (regionBoundedPlanParent), so under a claimed-vs-layout disagreement the render degrades to the region and never demands past it. The portal side gets a self-write token: frame writes the portal itself makes (host frame restore, hosted seed and target frames) hold the token, and geometry observers ignore notifications that arrive while it is held — only genuinely external geometry re-arms the sync, so an external stomp costs exactly one sync request and the restoring write buys zero. * docs: reconcile-pass design for remote-tmux sizing Replaces the edge-triggered sizing machinery's execution model with a reconcile pass: events set a dirty generation, one pass per window snapshots its whole world in a single instant, desired state is a pure feasibility-clamped function of the snapshot, the diff classifies every mismatch as awaiting, user-owned, drift, or infeasible-reported, and the commit writes synchronously outside layout callbacks. Fourteen named mechanisms are deleted with their replacing property stated; drag sessions and the command-ack barrier stay. Thirteen edge cases carry termination arguments; migration is three steps under one rule — no step deletes a mechanism whose replacement ships later. Reviewed adversarially by two independent passes; all findings folded, including the unreachable-desired circuit breaker, synchronous commit applies, dirty generations, the three-state claim ledger, and the plan-never-exceeds-region invariant the live fuzz proved this week. * tests: the bounce fixture's reply assigns the span the drag actually sent The fixture's region is narrower than the tmux window, so the dragged fraction converts to fewer cells than the raw tree suggests — a reply hard-coded at the tree-scale span (92) never matches the sent span and the release path it exercises can never fire. Read the sent span off the armed hold and build the reply from it; the release-and-settle assertions then judge the real protocol edge instead of a fixture artifact. * portal tests: bound each test's runtime and add a last-slot leak check TerminalWindowPortalLifecycleTests passes test-by-test but dies when the class runs whole: depending on interleaving the shared app host either livelocks in a SwiftUI reentrant-layout loop during realizeWindowLayout, hangs in removePortal teardown, or crashes on the io-reader thread inside the PTY output tee. Each test leaves state behind for the tests after it — dropped TerminalSurfaces whose native frees and io threads are still in flight, portal windows that never close, and directly-created portals that never see willCloseNotification. This adds the detector first: a leak-check test that runs in the class's last alphabetical slot and asserts the process returned to its pre-suite baseline (registered portals, portal-hosting windows, live runtime surfaces, and — via a new DEBUG counter on the teardown coordinator — native frees still in flight). It is red on this commit: running just testWindowScopedExternalGeometrySyncDoesNotRefreshOtherWindows followed by the leak check crashes the host with the tee use-after-free before the assertions can even report. Each test also gets executionTimeAllowance = 60 so a future livelock fails the one wedged test in minutes instead of hanging the host until the CI job timeout. * portal tests: tear down every window, portal, and surface a test creates The lifecycle suite's tests each stood up real NSWindows (ordered front), WindowTerminalPortals, and live TerminalSurfaces and dropped them at the end of the method. Across a whole-class run that left, for the following tests: native surface frees and io threads still in flight; blocking window-appearance NSAnimations committing CA transactions from background queue threads (two of them in every wedge sample); portals whose NotificationCenter block observers — including an object:nil split-view observer — stayed registered forever; and refcounted interactive-resize state a failed test could leave latched. Track all three through suite helpers and tear them down in tearDown: surfaces are released synchronously (releaseSurfaceForTesting frees the runtime before its io threads can race the next test), directly-created portals get tearDown() since they never see willCloseNotification, and windows are created with animationBehavior none and closed for real. tearDown also asserts the registry portal count returned to its baseline, so a future leak fails the leaking test, not a victim three tests later. Two product-side seams back this up: WindowTerminalPortal removes its notification observers in deinit (a portal that dies without ever seeing willCloseNotification leaked them permanently, in production too), and a DEBUG-only resetInteractiveGeometryStateForTesting clears the refcounted drag state the production API offers no owner handle for. * portal tests: the self-echo test uses the tracked window and portal teardown The suite hygiene rules apply to it like every other test in the class: a tracked window (animation off, closed in tearDown) and a tracked portal instead of an ad-hoc pair the last-slot leak check would count against the baseline. * portal: never force a synchronous surface redraw from inside a layout pass A geometry sync that runs while AppKit is still inside the window's layout pass (syncLayout == false: SwiftUI update callbacks and anchor geometry callbacks) called refreshSurfaceNow when a hosted frame changed or a hidden surface was revealed. displayIfNeeded there reaches ghostty's Metal drawFrame with the window's transaction still open, and waitUntilCompleted waits on a present that only that transaction can commit — the main thread wedges permanently. Seed-1 fuzz reproduced it twice at the same iteration: a programmatic window setFrame is not a live resize, so the live-resize guard did not cover it. Defer those redraws to the next main-queue turn; syncs that already run outside layout keep the synchronous flush. * remote-tmux: publish generation-stale rects replies that cover the current tree Layout publication discarded any rects reply whose generation had been superseded and sent a fresh fetch. Under continuous churn every reply is one generation behind by the time it lands (%layout-change inter-arrival is shorter than a round trip), so publication never advanced: windowsByID froze at the pre-churn tree while the app's claims and tmux kept agreeing, and the settle oracle timed out. Seed-1 fuzz held that starvation for 32 seconds against an 8-second budget once control-stream round trips inflated under load. The coalescing design already intended one publish-then-follow-up cycle (the dirty flag), but staging bumps the generation on every event, so the generation guard always won and the dirty publish-first branch was unreachable in exactly the storm it was built for. Now a stale reply that still covers every pane of the current tree publishes as interim verified state — its rects are the freshest list-panes snapshot observers can have — and owes exactly one follow-up fetch for the newest generation. Publication advances once per round trip and converges on the first quiet one. A stale reply that no longer covers the current tree (structure changed mid-flight) keeps today's behavior: nothing publishes, the owed fetch returns the new structure, and the garbled-reply retry budget is not burned. Also repairs three tests in this suite that still asserted no topology notify on the rects-drop paths; the drop's resolution notify is intentional (a divider hold deferring to 'this window's pending layout resolved' needs the edge) and this suite was missing from the pre-push gate. * portal, terminal, browser: close the remaining synchronous-display holes inside layout passes A deadlock audit of the mirror stack found three more paths in the class d063f19166 fixed — a synchronous surface display reachable while AppKit is still inside a layout pass, where ghostty's Metal draw can wait on a present that only the still-open window transaction can commit: - The interactive-drag branch of the anchor sync ended with an ungated failsafe reconcile that called refreshSurfaceNow one line after the primary sync had correctly deferred. Reachable on every divider or sidebar drag tick. The reconcile now threads syncLayout and defers through the same queue. - setVisibleInUI(true) nudged the surface synchronously, and three of its callers run inside SwiftUI update/layout (updateNSView, viewDidMoveToWindow, the geometry-callback rebind). The nudge now waits one main-queue turn. - The browser panel's hosted-WebKit refresh ended with a whole-window displayIfNeeded from updateNSView, which also flushed sibling Metal terminal panes mid-pass. The flush is now scoped to the panel's own subtree, which has no Metal wait. The first two land with red-first tests that drive the sync from inside a real layout pass; the audit's remaining lower-severity findings are tracked separately. * docs: reconcile design — audit findings, per-layer testing, diagrams, plainer prose Adds the concurrency audit's findings: the two rules the deadlocks proved (no synchronous surface display inside a layout pass; the main thread is the contended resource), the main-actor ingest pipeline as the confirmed round-trip-inflation mechanism (step zero of the migration), and the fragile tier with file references and fix directions. Adds a testing section that separates what gets stress-tested in simulation (the pure desired function, the reconcile loop against simulated ports, turn-based and seeded) from what must run against the real dependencies (AppKit, bonsplit, ghostty, tmux), with the rule that every simulator behavior must be pinned by a real-dependency test. Adds ASCII diagrams for the pass loop, the diff classification, and the main-thread contention picture, and rewrites the longest sentences into plain ones. * docs: reconcile design — testing weight goes to real-dependency assertions The bug ledger says where tests pay off: not one of the day's bugs was the loop mis-stepping on its own state. Every one was our model of a component diverging from what AppKit, bonsplit, ghostty, or tmux actually did. A simulated-world stress harness inherits the model's blind spots, so layer 2 shrinks to a skeleton check and the weight moves to driving the real components with the commit phase's instructions and asserting they handled them as modeled. * docs: pin tmux 3.7's half of the sizing loop from source; correct the no-op barrier rationale The reconcile design gains a tmux section verified against the 3.7 source (file:line cited) so the facts don't have to be re-learned by observation: claims are ceilings in every window-size mode; a control client can't become 'latest'; an unfittable claim clamps the WINDOW UP to the tree minimum, so claimed==layout never converges and the published layout is the feasibility verdict; window-resize redistribution is equal-absolute, so split ratios erode and imposition is permanent work; no-op resizes still emit %layout-change and every claim echoes one per window; %layout-change orders after its command's %end; pty resizes are deferred 250ms per pane; layout rects are borderless cell sizes that ignore border-status rows; a per-window claim makes the client's tty size participate for every other window. One of those facts corrects the divider-hold rationale in the hardened sizing doc, amended with the root cause: 'a no-op resize emits no %layout-change' was written from lab observation and was unfalsifiable by our oracles, because both hypotheses release the hold through the same observable path. tmux notifies unconditionally (layout.c:726-728); the only silent resize is the missing-container case (layout.c:686-687). The shipped mechanism is correct under both readings — the barrier is the ordering fence plus the one silent case — so this is a rationale fix, not a behavior fix. * portal: clear the hosted view's autoresizing mask on adoption Hosted terminal views reach the portal from SwiftUI hosting with autoresizingMask = [.width, .height]. In an Auto Layout window that mask is translated into edge pins - a minX constant plus a trailing margin to the host, no width constraint at all - and the pin distances freeze at whatever geometry the last constraint pass saw. Every host resize then re-derives the pane's size from stale margins against the new host bounds, stomping the frame the portal just wrote. The portal restores plan truth, the next flush re-applies the pin arithmetic, and the two writers alternate once per display refresh: panes sat exactly one host-delta wide of plan (the live 5pt case) while hierarchy syncs ran into the thousands per settle window. Live forensics pinned it: at the moment of a stomp the engine holds no width constant for the pane (engineWidthConstant=nil), just edge pins frozen at the previous generation (portal.stomp.diag, added here as rate-limited DEBUG forensics). A unit fixture reproduces the arithmetic deterministically once the view carries the production mask: growing the window 120pt stretched a 240pt pane to exactly 360. Adoption now clears the mask (bind), re-asserts it on every sync in case reparenting plumbing restores it, and puts the original back on detach. An empty mask translates to rigid position+size constants that always equal the last portal write, so the engine's opinion of a pane IS the portal's last write - there is no second geometry source left to fight, and no frame setter is overridden anywhere (a setter that swallows or rewrites engine applies desyncs engine bookkeeping and NSWindow raises its update-constraints budget exception). The mask test pins adoption/detach and the host-resize decoupling; the divergence suite keeps the convergence guards: a restore survives flushes of any scope, portal writes do not re-arm the sync, and a rapid-resize burst converges without an exception. * docs: pin the autoresizing-mask geometry-writer finding The frame ping-pong took three failed fixes before live forensics named the writer, so the mechanism, the two dead ends (refusing engine writes crashes the constraint budget; redirect-through-super is the same thing in disguise), and the reason every unit fixture missed it (test views are born mask-empty) now live in the reconcile design doc's audit section. * remote-tmux: re-arm a delivered size claim when the layout disagrees tmux is the only authority on whether a size claim actually landed. The sent-pins ledger dedups resends, so a pin the server never honored wedged silently: the reply was lost across a transport gap, or a co-client raced it, or the window-size mode changed - either way the ledger said delivered, dedup suppressed every retry, and the window sat columns wide of the claim while mirrors rendered short of the assignment. The live fuzz caught panes rendering 83 columns against an assignment of 86, persisting through settle and reconfirm, with the text wrapping off tmux truth as the visible symptom. Every %layout-change names the window's actual size, so it is the parity edge: when it disagrees with a claim the ledger says was delivered, drop that ledger entry and resend the claim. The re-arm is budgeted at three per disagreement episode - an infeasible claim (tmux clamps a window up to its tree minimum) disagrees forever and must not become a per-layout-event ping - and agreement or a new claim value opens the next episode. Claims still derive only from measured containers; this resends a decision already made, it never makes one. Also relabels the rendered-short diagnostic's plan= field to planOuter= with the tab-bar accounting spelled out: the plan charges the per-pane tab bar in the pane's outer box while view= is the content below the bar, so a healthy pane reads exactly tab-bar-height shorter there and the constant kept getting misread as a layout bug. * remote-tmux, portal: audit follow-ups before undrafting Reviewed every fix commit on the branch for necessity. Three follow-ups came out of it. The claim-parity re-arm budget now resets on reconnect: episodes are per connection, and a budget spent against the old transport must not suppress re-arms when the reseed's own resends get lost the same way. The engine-constraints test no longer calls autoresizing-translated constraints "the SAFE kind" - the mask finding disproved that generalization for hosted views, and the comment now says what is actually tolerated on the host and why. The divider-drag denominator keeps its unclamped renderFrameSize on purpose, and the comment now explains why the plan's region bound must not be applied there: drags convert against what is actually on screen. * remote-tmux: mirror grids render exactly their tmux-assigned cells A mirror pane's grid derived from its view diverges from tmux whenever the plan and the assignment disagree, and every direction of divergence corrupts the mirrored text. Short: the divider plan can legitimately hand a pane fewer points than its cells need - a starved sibling is the live case, where tmux assigns one column, bonsplit's pane chrome refuses to render below ~31pt, and the rail pays the difference out of the sibling's share (plan parent 707pt, outers 31 + 675, where 86 assigned columns need 691: the pane rendered 83 columns and wrapped off tmux truth). Long: the starved pane itself derived a ~3-column grid from its 31pt floored view, so "END 001x022" never wrapped where tmux wrapped it and the unwrapped read gained seven lines; a taller grid keeps rows tmux never repaints, which read back as stale content. Since the points genuinely are not there - or are there in excess - the grid follows tmux exactly and the view clips or letterboxes the difference: the same answer tmux gives a client whose size disagrees with the window. Mirror surfaces carry their tmux-assigned grid; updateSize pins the applied pixels to precisely the assigned cells at the current cell size plus the surface's own chrome (inert until the surface has real cell metrics), and the sizing pass sets or clears each pane's pin from the layout tree's leaves. The pin lives strictly on the surface-pixel side; claims keep deriving from the measured container alone, so the feedback loop that sank the old view-pinning approach cannot form. The pin arithmetic is a pure function pinned with the live numbers from both directions: 1351px of view and 86 assigned columns at 16px cells pins up to the assigned grid, the one-column pane's 56px view pins down to one column, and missing cell metrics or a degenerate assignment leave the size untouched. Repro: scripts/remote-tmux-fuzz-host.sh <alias> to stand up the local fixture, then scripts/remote-tmux-live-fuzz.sh <alias> 3 2 (seed 3 fails at iteration 1 without this change: pane %0's unwrapped read-screen gains seven lines over tmux capture-pane) or seed 2 x 25 (one row short on a 118x41 pane at iteration 25). * skills: document how to run the remote-tmux layout fuzz The harness scripts each explain themselves, but nothing discoverable tied a commit's 'seed 3, iteration 1' to the commands that replay it. The testing skill now covers the fixture setup, the marathon and single-seed replay invocations, the two settle oracles, what the evidence directory contains, and the quiet-machine rule. * remote-tmux: settle requires derivation parity; hidden readings park instead of dropping Two hardenings against the staleness class the fuzz surfaced today. The settle oracle checked delivery parity only - claim equals tmux's layout - which cannot see a claim tmux honored but that no longer matches what the CURRENT container derives. That is exactly how a stale claim settled green this afternoon while the region could not render the columns it promised. A settled visible window must now be able to re-derive its own claim; hidden mirrors are exempt because they hold their attach-time claim by design. noteContainerSize dropped readings that arrived while hidden, with no park and no replay. Geometry callbacks fire only on change, so a dropped reading was gone for good, and a reveal whose cached re-push happened to be degenerate would leave the claim derived from a pre-hide width. Hidden readings now park in the pending-container channel the sizing pass already judges against a real bound before anything banks; a fresh reveal reading replaces the parked one outright, so hidden geometry still never banks unjudged. * remote-tmux: redraw kick when a pin grows; settle oracle judges the live grid The definitive marathon left two failures, one per mechanism. A pin that grows a mirror's grid after tmux already streamed those rows leaves the late-granted cells blank: the content that belonged there was clipped while the grid was short, and tmux repaints only on change. setAssignedGrid now reports growth and the sizing side answers with the existing coalesced redraw kick, so tmux refills the cells it clipped (seed 2 iteration 25: a 118x41 pane reading back 40 lines). The settle oracle judged rendered grids from the cached applied-resize ledger, and the cache can lag or miss a pin's resize: it failed a pane whose actual surface held exactly its 1x22 assignment while the cache still said 10x18 from an earlier life (seed 3 iterations 1 and 14 - the content comparison passed, only the stale cache complained). The oracle now reads the surface's live grid and keeps the cache only as the no-report-yet fallback. Why the applied-resize sample misses pin resizes is still open; the parity re-arm reads the same ledger, so that staleness gets its own root-cause pass. * remote-tmux: don't pin the mirror grid mid-drag — it painted past the pane Shortening a mirrored pane by dragging a divider briefly painted the pane's content over the neighbor and the window chrome, settling correct at rest. The assigned-grid pin holds the surface at the pane's tmux assignment, and during a divider drag that assignment is the PRE-drag (larger) one: performSizingPassNow holds applyAssignedGrids while a divider drag is active, and tmux hasn't replied with the smaller layout yet, so the pin keeps the surface oversized for the whole drag. updateSize applies that oversized ghostty grid and asks the renderer to repaint it, and that present lands before the deferred divider-drag reconcile clamps the drawable and clip geometry back — so the oversized frame paints across the just-moved divider. Clipping is never disabled; the leak is an oversized surface presented against not-yet-reconciled geometry. updateSize now takes suppressAssignedGridPin, set while an interactive resize is active (window live resize or the registry's interactive geometry flag), and uses the view-derived size instead of the pin. The pin re-establishes at rest: drag end and tmux's layout reply each size the pane again, and the next updateSize runs with the flag clear and re-pins to the assignment. Mid-drag the mirror briefly renders a view-sized grid a few cells off the assignment; manualIONoReflow keeps DECAWM off so no wrap divergence persists once tmux reconciles. Verification is runtime (drag a divider shorter, no overflow) plus the existing pin-arithmetic and portal/sizing suites; the suppression is a one-line gate with no unit seam for a live ghostty surface. * remote-tmux: red test — mirror never drives key focus for a freshly split pane A window mirror renders each pane in its own Bonsplit tree, so a new split pane is marked active/selected but nothing makes its surface the window's first responder: it shows the blue highlight yet takes no keys until clicked. Adds a mirror-layer contract test that spies the (inert) key-focus establishment seam and asserts the mirror drives key focus onto the new pane's own panel at creation. Red today — creation only updates selection. * remote-tmux: give a freshly split mirror pane key focus at creation A window mirror renders each tmux pane as a TerminalPanel in its own Bonsplit tree; those pane panels are never workspace Bonsplit tabs, so the workspace focus path can't resolve them and the surface's active/visibility false->true edges don't apply first responder either. A new split pane was therefore highlighted but untypeable until clicked. Track panes the mirror just created and, the first time such a pane becomes active, drive key focus onto its own surface the way a click does (moveFocus -> makeFirstResponder). Every attempt re-checks the mirror is still on screen and this pane is still its active pane, so a pane switch within the retry window cancels the pending focus instead of stealing it, and a background or headless mirror never moves the first responder. Consumed once per created pane, so a later active-pane echo never re-drives it. * remote-tmux: land the final divider position when a drag ends during a remote apply A divider drag that ended while isApplyingRemoteLayout was set skipped its only syncChangedDividerPositions() and just scheduled a pass, so the user's final position never reached tmux and the next sizing pass restored the pre-drag layout. Defer the send one runloop turn instead: once the apply's synchronous scope clears the flag, flushDeferredDividerDragEnd runs the same conversion+send the undeferred path runs. Test: dragEndDuringRemoteApplyStillSendsTheFinalPosition. * remote-tmux: re-arm on rendered-grid lag and gate parked readings on a settled bound Two sizing residuals in one file: The exact-grid pin does not always follow an assignment that grew between our claim and settle. The input-only settle proof cannot see it — renderedLayout is an input, but a pin applied against stale cell metrics can leave a pane one row short of the grown assignment, so it renders short and wraps while inputs read unchanged. The output-parity re-arm now treats a rendered grid behind its assignment as a miss (gridParityMismatch) and re-imposes; applyAssignedGrids re-applies the pin when the value already matches but the last sample lags. Tests: settleRearmsWhenRenderedGridLagsTheAssignment, gridParityIgnoresPanesThatRenderTheirAssignment. A parked oversized reading was consumed on the next pass regardless of whether the window bound had settled. During a live resize the window still reports its transient (old, smaller) frame, so a valid post-resize reading was judged against noise and discarded for good. Consume only when the hosting window is not in a live resize; live-resize end delivers the settled callback whose pass consumes it. Test: parkedReadingSurvivesALiveResizingWindowBound. * debug-log: count a line persisted only after the disk write succeeds persist() incremented persistedInWindow before the data conversion, handle open, and write, so a failed write dropped the line from disk without counting it — the next window's 'log.dropped N' marker under-reported and the never-silently-drop guarantee broke. Increment persistedInWindow only after a successful write; count a failed conversion, open, or write as dropped. The entry still stays in the in-memory ring. * portal: move sizing diagnostics to the debug boundary; instance-scope the live-resize test override Two maintainability moves out of production TerminalWindowPortal.swift. The process-wide RemoteTmuxSizingDiagnostics counters move to Sources/Debug/RemoteTmux/TerminalWindowPortal+DebugDiagnostics.swift, the existing debug-support boundary. The process-wide isWindowLiveResizeActiveForTesting static becomes an instance property on the portal, so a test drives only its own portal instead of latching interactive state across the shared app-host; the lifecycle and teardown tests inject on the instance. Behavior identical. * terminal: project the assigned-grid pin to the new content scale (#3) On a scale change (e.g. dragging a mirror pane between a 1x and a 2x display) the reported cell metrics are still at the OLD backing scale — set_content_scale runs later in updateSize. Pinning the old cell px pinned ~half the columns on a 1x->2x move, and forcing wpx to the old backing width made sizeChanged false, defeating deferScaleUntilResized so the grid collapsed when the bigger cell landed over the un-resized screen. Project the reported cell and pad to the scale this resize is about to apply; the ratio is 1 when the scale is unchanged, so it is a no-op then. * portal: restore hosted-view autoresizing masks in deinit (#15) Adoption clears each hosted view's autoresizing mask and detach restores it. A portal that dies without tearDown()/detachHostedView never restored them, leaving a surviving hosted view pinned at [] so the next portal saved [] as its original. Restore inline in deinit (which cannot hop to the @MainActor detach path). * remote-tmux: fail the settle oracle on a grid shortfall or missing sample (#11) The settlement payload computed settled without any grid-parity gate: a short or missing grid only appended to mismatches, so once the budget-capped output-parity re-arm stopped, settled flipped true with a shortfall still listed. Track gridParityReady (false on a no-sample or shortfall branch, judged live-first) and AND it into the sizingReady conjunction, so settled is honest independent of the re-arm budget. * remote-tmux: re-run the sizing pass when a window live-resize ends (#13) A window live-resize whose final geometry arrived before mouse-up left a parked oversized reading with no edge to consume it: onGeometryChange fires only on value change, and the parked-reading consumer holds while inLiveResize is true. Fire setNeedsSizingPass from the probe view's viewDidEndLiveResize — by the time the coalesced pass runs inLiveResize is false, so the consume proceeds. * remote-tmux: correct the mirror grid pin and its parity oracle (#1, #4, #8, #9, #10, #12, #14) Several linked defects in the mirror sizing transaction: - #1: applyAssignedGrids re-pinned a stale grid during a WINDOW live-resize (or interactive geometry drag), painting past the shrinking pane. The divider-drag early return does not cover a window resize, so gate the stale re-pin on the same suppression the view path uses. - #8: under zoom the visible tree is the single zoomed leaf, so hidden but live base panes were unpinned and rendered on a stale grid. Pin each pane from the visible tree or the base tree; clear only panes in neither. - #9: the pin-grow repaint went through the attach-only redraw kick (armed only at .enter), so late-granted cells stayed blank mid-session. Extract the shrink/restore SIGWINCH body into forceRedrawKick(windowIds:) and call it directly on a pin grow. - #10: the stale-repin else-if and gridParityMismatch tested only the under direction, so an over-render (rendered > assigned) was an invisible no-op. Compare with != on both axes; reapplyAssignedGrid clamps either way. - #12: gridParityMismatch read only the ledger, which goes stale because a same-size re-apply returns early before reporting. Read the surface's live grid first, falling back to the ledger. - #4: the parked-container consumer clamped an oversized parked reading to the bound and banked it, overwriting a correct size. Reject it (as the sibling oversized consumer does) and keep the last good container. - #14: rearmIfOutputMissedPlan gated on the plain isVisibleForSizing, which goes stale-true when a hidden tab's view is dismantled; gate on isEffectivelyVisibleForSizing so an offscreen mirror cannot spin re-arms. Tests: parkedHiddenReadingOverTheBoundIsRejectedNotClamped (#4), gridParityFlagsAnOverRenderedPane (#10, #12), and the reworked grid-lag test now pins that an offscreen mirror does not re-arm (#14). * terminal: move the teardown-coordinator debug counter behind the DEBUG boundary * tests: record native free in the deinit ordering test; close the divergence test window Route the deinit teardown through runtimeSurfaceFreeOverrideForTesting like the teardownSurface/suspend paths already do, so the deinit lifetime test can record the native free and assert it lands before the tee-lease release. Also give the self-echo divergence test the same window teardown its neighbors use so it stops leaking its portal. * skills: clarify the live-fuzz host setup and failure recovery Name the dedicated fuzz alias (cmux-fuzzhost, stood up by remote-tmux-fuzz-host.sh) and warn off cmux-srvA/srvB, whose interactive tmux the harness won't clobber and whose dir isn't where ssh-tmux connects. Add a run-once-and-wait note (killing the wrapper orphans the driver) and a failure-message playbook: "no workspace mirroring session 'fuzz'", "refusing to kill an unowned lab", "another fuzz driver is running", and the regenerated-key ssh errors. * remote-tmux: make the window-size claim independent of title-row folding clientGrid subtracted residual(of:), which reads the live parent-minus- children gap. Under pane-border-status the pane-border title row sits in that gap, and it moves in and out of the measured child spans as the window reflows, so the claim changed by a row whenever tmux republished the tree. The claim read its own effect and the refresh-client -C size oscillated (…x39, …x38, …x39) and never settled. Give the claim its own chrome residual computed from the stable model: one native divider per structural boundary (children.count - 1 per split) rather than the assigned gap, plus one title row at the configured window edge. Interior title rows share a separator row that is already charged, so the single edge title is the whole reservation with no double count. The claim now depends only on the container, cell size, structure, and border-status setting, so the same window yields the same claim titled or not and tmux converges. residual(of:) keeps its live-span behavior for the planner and render frame. * remote-tmux: take the first non-empty pane-border-status per rects reply Only panes touching the configured edge carry pane-border-status in their border-status field; interior panes report empty. Taking the last pane's value let a trailing interior pane clear a real top/bottom, flipping the window-level placement reply-to-reply. Combined with the claim reading it, that flipped the title-row reservation and the claim oscillated by a row. Keep the first non-empty value so the placement is stable across replies. * remote-tmux: stop the redraw-kick loop and settle on column parity A live-fuzz bounce: on a vertically split window settle never converged — claimed 108x43, layout 108x42, grids matching, sizing_pass climbing into the tens of thousands. Root cause is tmux's own rounding. We send a CLIENT size; tmux lays out the WINDOW. Columns agree exactly, but when a stacked split has an odd leftover row tmux hands it to one pane or the other from its prior state, so the window height it reports wobbles by a row around one stable claim, and a stacked pane's grid wobbles with it. Two places treated that wobble as something to correct: - The pin-grow redraw kick shrinks then restores the client size to force a repaint of cells granted after tmux streamed them. At an unchanged claim that can't reveal new cells, but the size change makes tmux re-round the split, which re-fires the kick: an unbounded loop (23k kicks in one iteration). The mirror now kicks a pane only when its grid reaches a size not yet refilled at the current claim (a per-pane high-water, reset when the claim changes or the pane leaves). A genuine grow refills once; the ±1 re-round, which never exceeds the high, is starved. - The settle oracle and the claim re-arm required client==window on both axes. Rows can't satisfy that, so assert it only on columns, where it holds, and let grid parity and derivation cover the rest. Also gives each window its own redraw-kick task, so a second window's kick can no longer cancel the first window's restore and strand it shrunk. * remote-tmux: content oracle + churn scenarios for the sizing UI suite The sizing UI suite asserts grid DIMENSIONS (pane_grids: assigned==rendered) but never the actual on-screen TEXT, and it only judges multi-pane mirror windows — so a pane at the right-looking size holding stale content, or a single-pane window (no mirror, no pane_grids entry) rendering a stale frame, passes every existing check. That is the class the live fuzz's text oracle catches and this suite could not. Adds a per-pane content oracle — the fuzz's own probe: a full-width ruler in each pane, then compare the mirror's read-text against tmux capture-pane -J, tolerating the 2s ruler redraw by accepting a match before OR after the read. Five scenarios on a fast-settling two-window lab (one split, one single-pane) exercise the churn edges the class lives on: content parity for a split and a single-pane window, a single-pane window resized from the tmux side, a zoom toggle, a collapse to one pane, and a hidden window churned then revealed. A size-stability wait covers windows assertSettles cannot judge (single-pane, zoomed) since its coherence check assumes a normal multi-pane layout. Extends the test_exec allowlist (DEBUG-only, confined to the UI-test tmux dir) with the commands the oracle drives: capture-pane, select-pane, send-keys, kill-pane, resize-window, and resize-pane -Z/-y. Also documents that this suite runs LOCALLY (it is hermetic — not CI-only), with the sandboxed-agent recipe (ssh hairpin + CMUX_SKIP_ZIG_BUILD=1). * remote-tmux: content oracles must read the named pane's own surface Both content oracles read "the app's focused surface" and compared it against every pane of every tmux window. That cannot verify a named pane. cmux does not follow tmux's active pane or current window — select-pane leaves tmux's current window alone (verified against tmux 3.7), handleActivePaneChanged only moves the strip dot and the directory, and %session-window-changed is only recorded — so the read returns whichever pane the app already showed. It matches the target's capture whenever the two panes share dimensions, because the probe prints the same text at the same size, and mismatches when they do not. That is what the live fuzz reported as a mirror defect on seed 2 iteration 25, every run: it compared window @4's pane %16 (99x35) against window @3's surface. The surface it read reported 118x41 and 160x49 at different moments and @3 claimed both of those sizes; @4's own claim was exactly 99x35, matching tmux. The mirror was right about every window. With the oracle reading %16's own surface, seed 2 runs 25/25 clean. Fixing this needed a seam the app did not expose: which surface renders a given tmux pane. remote.tmux.pane_surfaces reports that map for every mirrored window, single-pane windows included — they have no mirror, so they appear in no other introspection verb, which is also why nothing ever checked them. Each entry carries on_screen: a hidden tab holds its last render by design, so judging it would report a designed lag as a defect. The UI suite's oracle had the same flaw and passed by luck; it now reads by surface id too, and asserts every pane of the window under test is actually on screen so a scenario that forgets to select its tab fails loudly instead of quietly checking nothing. * remote-tmux: follow pane-border-status changes via a control-mode subscription Turning pane-border-status on or off resizes and moves every pane touching the configured edge, but tmux emits no %layout-change for it: the window's layout string does not encode the title row, so tmux considers the layout unchanged. Measured on tmux 3.7 with a control client watching — a 12-row pane at top 0 becomes an 11-row pane at top 1, and the client sees nothing. Pane heights come from the rects fetch a %layout-change drives, so the published tree kept the pre-toggle heights until some unrelated layout event happened to refresh it. Every edge-touching pane then rendered a row off from what tmux actually held, and no internal oracle could see it: they compare the claim against the app's OWN tree, never against tmux's live panes. The live fuzz's text oracle caught it (seed 5, iterations 10 and 11: the app assigned %0/%1 119x11 while tmux said 119x12, border=off, the window size agreeing exactly). tmux does publish the change — just not as a layout event. A control-mode subscription (refresh-client -B, tmux 3.2+) pushes pane-border-status on every change, for hidden windows as well as the current one (both verified on 3.7). The connection already subscribes per-pane for cwd, reflow and header labels, so this follows that idiom: subscribe pane-border-status per window, and on a CHANGE re-read the topology, which restages each window and republishes real geometry through the same path a genuine layout event takes. No polling, no timers. Only a change refetches — tmux pushes the value once on subscribe, and that initial push rides alongside an attach whose rects fetch is already current. Subscriptions belong to the client, so the reconnect reseed drops the watch flags and lets the restage reissue them. Marathon: 5 seeds x 25 iterations, zero failures, zero hangs, zero crashes, with 11 border changes observed and healed. * remote-tmux: repaint a grown pane by reading tmux, not by resizing the client An adversarial review found the rationed redraw kick still dropped genuine repaints, and the reasoning behind the ration was wrong to begin with. When a pane's grid grows after tmux already streamed those rows, the late cells hold nothing: the surface clipped that content while it was short, and tmux repaints only on change. The old repair moved the CLIENT size (shrink a row, restore) to force a SIGWINCH — but that resize makes tmux re-round an odd split, which grows a pane again and re-fires the kick: an unbounded loop, 23k kicks in one fuzz iteration. Rationing it by a per-pane high-water bounded the loop but suppressed real grows, and took the two axes' maxima independently, so 120x30 -> 100x50 -> 110x40 recorded a 120x50 that never existed and the final grow never repainted. The kick was the wrong tool. tmux's own grid HAS the rows — only the mirror lost them — so the repair is to READ tmux's screen: capture-pane plus the pane-state query, both reads. Nothing perturbs tmux, so no split re-rounds, no loop, and every genuine grow repaints exactly once with no budget and no high-water. The capture omits -S: the seed's scrollback is already in the surface and re-emitting it would stack a second copy, while the visible screen is exactly what a clipped grow lost. forceRedrawKick is gone; the attach kick keeps the size-moving form, which is correct there (a fresh client's TUIs must repaint at the size just applied, and a no-op apply sends them no SIGWINCH). Also from the review, each a real hole: - The claim re-arm compared columns only, so a claim tmux never applied was undetectable: 108x35 against a 108x43 claim reported green forever, because every grid check passes when the panes faithfully render the short assignment. Rows cannot be compared exactly either (chrome plus an odd-split remainder), so windowMatchesClaim bounds them: 42-for-43 is chrome, 35-for-43 is a lost pin. - The attach kick still required exact row equality, dropping every window tmux landed a row short — precisely the windows whose TUIs get no SIGWINCH and need the kick. It uses the same predicate now. - pane-border-status subscriptions were forgotten in reseedAfterReconnect, which runs INSIDE the list-windows handler — after the restage that re-issues them — so every surviving window skipped its resubscribe and the option went unwatched for the rest of the connection. Cleared at beginReconnecting() instead. - tmux's first subscription push is not automatically a baseline: it arrives up to a second late, so the option can change between the rects fetch and the push. It is compared against the published window's rects-derived placement. capture-pane joins refresh-client in the send log so 'did the repaint fire?' is answerable from evidence: 50 repaints observed in a 12-iteration run, distinct from the 15 attach seeds. Marathon on this binary: 10 seeds x 25 iterations, 250 iterations, zero failures, zero hangs, zero crashes, 19 border changes healed, zero redraw kicks. * remote-tmux: close three ways the content oracles could pass without testing All three let a green run mean nothing, which is the failure mode that let a broken text oracle stand for hours. The probe could not tell panes apart. Its lines carried only the size, so two panes of equal dimensions printed byte-identical screens — and the fuzz's even splits produce exactly that (200 columns becomes three 66-wide panes). A comparison that read the wrong pane's surface then passed. Every line now carries the pane's own id from TMUX_PANE, verified against tmux 3.7: '%0 040x020 0123…' next to '%1 039x020 0123…', so a wrong surface can never alias a right one. The fuzz let the app pick its own coverage. It asked the app which panes were on screen and only rejected zero — so a pane the app quietly omitted dropped out of the comparison and the run still read green. Every window with an on-screen pane is the visible window, so tmux's own pane census for it must be on screen; a missing pane is now a failure (the app not rendering a pane of the visible window), not less coverage. The UI scenarios discarded the failures of the commands they depend on. A send-keys that never ran left the pane at an idle shell prompt, which the mirror renders faithfully — so every content assertion passed against no probe at all. A resize, zoom or kill that never ran left the scenario asserting on un-churned state. The ruler is now a precondition (poll tmux's capture until its marker appears) and each churn command must report success. paneSurfaceEntries attributed panes by scanning published trees, ignoring the session's authoritative windowIdByPane. A join-pane/swap-pane in flight leaves the source window holding the pane until its reconcile runs, so the scan could report the pane twice, or pick the stale window's frozen surface — whichever came first in dictionary order. It now attributes through the ownership map and keys the result by pane, so neither is representable. Verified: fuzz seed 5 clean, all five UI content scenarios pass with the stricter assertions, ruler pane-identity confirmed against real tmux. --------- Co-authored-by: ejc3 <ejc3@users.noreply.github.com> Co-authored-by: austinpower1258 <austinwang115@gmail.com> | 2 个月前 | |
Stop XCTest crashes from reaching Sentry (#8786) * test: cover Sentry startup under XCTest * fix: disable macOS Sentry under XCTest * Make Sentry startup policy constructable * test: cover sandbox-denied CLI socket telemetry * fix: drop sandbox-denied CLI socket telemetry * fix: allow explicit Sentry opt-in under XCTest * Address Sentry startup review policy * Close Sentry test launch review gaps * Require provenance for Sentry suppression * Document Sentry suppression contract --------- Co-authored-by: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> | 2 个月前 | |
macOS: adopt the shared CmuxAuthRuntime AuthCoordinator, delete AuthManager Replaces the ~1.5k-LOC ObservableObject AuthManager singleton with the injected auth graph the iOS app already uses: - MacAuthComposition builds the graph once at app launch: the keychain/file fallback token store (unchanged storage, so existing sessions survive), a StackClientApp over it wrapped in StackAuthClient, and the shared AuthCoordinator bound to the historical cmux.auth.* defaults keys (cached user, selected team, plus a one-time has-tokens seed so the first launch primes as restoring instead of signed-out). - HostBrowserSignInFlow owns the hosted-browser sign-in: ASWebAuthenticationSession behind an injectable session seam, callback parsing/seeding, attempt + sign-out generation guards so a late callback can never resurrect a signed-out session, and a clock-injected deadline for the socket auth.begin_sign_in command. - Call sites are injected instead of reaching for AuthManager.shared: AppDelegate (URL callbacks), VMClient, PhonePushClient, MobileHostService, TerminalController socket commands (auth.status/begin_sign_in/sign_out), and HostAccountFlow, which is now a thin @Observable projection of the coordinator instead of a Combine bridge. - The token stores move out of AuthManager.swift into one file per type; dead code (runCLIAuthFlow, seedTokensFromCLI, signInWithCredential paths, didCompleteBrowserSignIn, the CMUX_UI_TEST_AUTH_STUB client, AuthSettingsStore) is deleted with the singleton. - Adds the AuthError auth.error.* en+ja strings the shared coordinator surfaces. Closes https://github.com/manaflow-ai/cmux/issues/5375 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> | 3 个月前 | |
fix: honor disabled workspace cwd inheritance | 2 个月前 | |
Reimplement Settings as CmuxSettings/CmuxSettingsUI SPM packages (Claude) (#4975) * Add CmuxSettings + CmuxSettingsUI packages and integrate into the app target Foundation for the settings system migration. Both packages compile into cmux DEV.app and pass their own unit-test suites, but the running Settings window is still the legacy `SettingsView`; the new `SettingsWindowScene` is exported and wired but not yet substituted. The cutover is deliberately a separate, later change. CmuxSettings (Foundation only; 22/22 Swift Testing green) - `DefaultsKey<V>` / `JSONKey<V>` — phantom-typed setting handles; each store accepts only its flavor, so wrong-store mismatches are compile errors, not runtime traps. - `UserDefaultsSettingsStore` / `JSONConfigStore` — both `actor`s, no locks, no KVO, no `@Published`. Observation surfaces as `AsyncStream<V>`. UserDefaults observation via `NotificationCenter.default.notifications(named:)` async sequence; JSON observation via `JSONConfigFileWatcher` (DispatchSource hidden behind an `AsyncStream<Void>`). - `SettingCatalog` + `SettingCatalogSection` — struct-based registry composed of sub-section structs by dotted-id prefix. `catalog.all` walks `Mirror` recursively so there is no parallel hand-maintained list; adding a key in any `*CatalogSection` shows up everywhere. - ~107 catalog entries declared across nine sections (App, Terminal, Notifications, Sidebar, SidebarAppearance, WorkspaceColors, Automation, Browser, BetaFeatures, Shortcuts). - Value types for every storable enum, plus `StoredShortcut` + `ShortcutStroke` + 77-case `ShortcutAction` enum with display names + group categorization. - Legacy-key migration is type-validated: `AnySettingKey` captures the underlying `Value` in a closure so type-incompatible legacy values are skipped rather than silently coerced. CmuxSettingsUI (depends on CmuxSettings; 5/5 Swift Testing green) - Default-isolated to `MainActor` via `.defaultIsolation(MainActor.self)` (swift-tools-version 6.2). - `@Observable @MainActor` view-models (`DefaultsValueModel<V>`, `JSONValueModel<V>`) bridge the actor stores to SwiftUI synchronous Bindings. No `deinit` cleanup needed; `[weak self]` inside the observation Task ends naturally on dealloc. - Row primitives: `SettingsToggleRow`, `SettingsPickerRow<V>`, `SettingsTextFieldRow`, `SettingsStepperRow`, `ShortcutRecorderView` (NSViewRepresentable over a focusable NSView capturing keyDown with modifier flags). - `SettingsWindowScene` exposes a `Scene` the app composes; renders `NavigationSplitView` with a searchable sidebar and detail-switched section views. - 12 of 13 section views are real and bound to catalog keys; the remaining one (Account) is intentional out-of-scope until the auth storage moves out of `CMUXAuthCore`. - `KeyboardShortcutsSection` lists all 77 actions in groups and reads / writes `[ActionID: StoredShortcut]` through the JSON store with the recorder primitive. App-target integration - `cmux.xcodeproj/project.pbxproj`: both packages added as `XCLocalSwiftPackageReference`s and linked into the `cmux` target's frameworks build phase. - `cmux.xcworkspace`: both packages added to the `Packages` workspace group so they appear in the navigator. - `Sources/AppSettingsRuntime.swift`: app-owned singleton holder that constructs one `SettingCatalog`, one `UserDefaultsSettingsStore` (running migrations against `UserDefaults.standard`), and one `JSONConfigStore` (against `~/.config/cmux/cmux.json`). The package itself has no shared statics; this file is the DI seam. CLAUDE.md - New "Package design discipline" section codifying the rules that emerged this session: no `static let standard` singletons in packages, no namespace-enums, no parallel hand-maintained registries, prefer compile-time invariants over runtime traps, file-scope `private func` vs `private static func`, nested types still count for one-type-per-file. - New "Testability" section: every public type in `Packages/` must be testable without launching the app, without booting AppKit, and without depending on `UserDefaults.standard` or the user's filesystem. No global state in package code; DI via constructor. What's still pending (so the diff for the next round is interpretable) - Scene swap in `cmuxApp.swift` to present `SettingsWindowScene` in place of the legacy `SettingsView`. Held back until the explanatory sections (`Account`, `GlobalHotkey`) have real catalog-backed controls so the cutover does not visibly regress the running app. - ~50–60 more catalog entries to reach JSON-path parity with `Sources/CmuxSettingsJSONPathSupport.swift`. - ~200 `@AppStorage(...)` call sites across `Sources/` that still target legacy keys directly; the new catalog mirrors those keys exactly so the conversion is mechanical but bulky. - Default-bindings table for `ShortcutAction` (mining from the legacy `KeyboardShortcutSettings.swift` defaults dictionary). - Conflict detection and two-stroke chord recording in `ShortcutRecorderView`. - Runtime bridge from `[String: StoredShortcut]` to the legacy `KeyboardShortcutSettings.shared` lookup so the app's keyboard event path consults the new persistence. - `BetaFeaturesCatalogSection` is wired but its single `dock` flag is the only entry — additional beta flags add as they land. - `AccountCatalogSection` does not exist yet; `AccountSection` renders an explanation pointing at `Sources/Auth/AuthSettingsStore.swift`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Fix empty Settings window + harden JSON store error path SettingsWindowRootView gated its body on a `shouldRenderSettingsContent` flag driven by NSWindow notifications. `WindowAccessor` defaulted to dedupe-by-window, so the second open of the same SwiftUI Window reused the same NSWindow and the visibility handler was suppressed — leaving the body stuck on the Color.clear placeholder. Removed the gate; the content view now renders unconditionally and WindowAccessor is set to `dedupeByWindow: false` so the presenter is wired every reopen. Also wired the SettingsRuntime + SettingsErrorAlertModifier into both the Settings and Config window scenes (sibling Window scenes don't inherit environment values from the main WindowGroup), and hardened the JSON config error path: - JSONConfigStore.readFromDisk() now throws on corrupt / malformed JSON instead of silently returning [:]. mutateRoot() reads through the throwing path so writes refuse to overwrite a corrupt file (previously a stale-cache write would have destroyed user data). File-not-found is still treated as the empty state. - KeyboardShortcutsSection now records write failures into the injected SettingsErrorLog instead of `try?`-swallowing them. - DefaultsValueModel's asymmetry with JSONValueModel is documented (UserDefaults.set is non-throwing in Foundation; no error to route). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Dogfood: convert first @AppStorage to @Setting Converts the Right-Sidebar Dock beta toggle in `SettingsView` from `@AppStorage(RightSidebarBetaFeatureSettings.dockEnabledKey)` to `@Setting(\.betaFeatures.rightSidebarDock)`. The UserDefaults key is unchanged (`rightSidebar.beta.dock.enabled`), so existing user values round-trip. Reads now flow through `UserDefaultsSettingsStore` -> `DefaultsValueModel`'s `AsyncStream` projection, and writes go through the actor instead of synchronous UserDefaults. Same Binding<Bool> shape via the wrapper's `projectedValue`, so the existing `$rightSidebarDockEnabled` call site at cmuxApp.swift:7149 keeps working unchanged. This is the first concrete site exercising `SettingsRuntime` injection into the Settings window environment, and the first time `DefaultsValueModel` runs at app runtime. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Convert 18 @AppStorage call sites in SettingsView to @Setting All flagged via the catalog's existing DefaultsKey entries; UserDefaults keys are unchanged so existing user values round-trip. Storage now goes through UserDefaultsSettingsStore actor + DefaultsValueModel. App section: warnBeforeQuit, warnBeforeClosingTab, warnBeforeClosingTabXButton, hideTabCloseButton, renameSelectsExistingName, commandPaletteSearchesAllSurfaces, workspaceInheritWorkingDirectory, keepWorkspaceOpenWhenClosingLastSurface, focusPaneOnFirstClick, menuBarOnly, reorderOnNotification, iMessageMode. Notifications: dockBadge, unreadPaneRing, paneFlash, showInMenuBar. Terminal: showScrollBar, copyOnSelect, autoResumeAgentSessions, agentHibernationEnabled. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Expand package: integrations catalog + AppSection / SidebarSection / BrowserSection / AccountSection parity Catalog additions: - IntegrationsCatalogSection (claude/cursor/gemini hooks, claude path, ripgrep path, subagent notification suppression) - SidebarCatalogSection: activeTabIndicatorStyle, selectionColorHex, notificationBadgeColorHex - BrowserCatalogSection: disabled, importHintVariant, importHintDismissed Section views built out to legacy parity (or close): - AppSection: Appearance / Workspace Behavior / Command Palette / Quit-and-Close / Editor / File Handling / Workspace Presentation / Notifications / Telemetry subsections wired to ~25 catalog entries - SidebarSection: full Material / BlendMode / State pickers, light + dark tint hex fields, blur opacity slider, Custom Colors subsection - BrowserSection: disabled toggle, custom search engine fields, multiline hostname pattern editors for hosts/external/insecure - AccountSection: real Form with integrations toggles + custom paths + notification suppression. Sign-in / team selection still in legacy New row primitive: - SettingsDefaultsTextFieldRow — sibling of SettingsTextFieldRow for UserDefaults-backed strings with optional subtitle cmuxApp.swift @AppStorage -> @Setting conversions: - claudeCodeHooksEnabled, customClaudePath, customRipgrepPath, suppressSubagentNotifications, cursorHooksEnabled, geminiHooksEnabled, sendAnonymousTelemetry, preferredEditorCommand, openSupportedFilesInCmux, openMarkdownInCmuxViewer, browserSearchSuggestionsEnabled, browserDisabled, browserHiddenWebViewDiscardEnabled, browserHiddenWebViewDiscardDelay, isBrowserImportHintDismissed, showBrowserImportHintOnBlankTabs, reactGrabVersion, openTerminalLinksInCmuxBrowser Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Bring all section views in CmuxSettingsUI to parity; swap to package UI Catalog: - AppCatalogSection: add systemWideHotkeyEnabled (key "systemWideHotkey.enabled") so the global-hotkey toggle has a home - AnySettingKey: add resetInJSON closure so ResetSection can wipe JSON-backed entries in addition to UserDefaults overrides Row primitives: - SettingsDoubleStepperRow for bounded Double-typed catalog entries (hibernation idle seconds, etc.) Section views built out: - TerminalSection: idle-threshold stepper + multi-line Resume Commands editor backed by the JSON-config resumeCommands list - NotificationsRows: add Notification Sound subsection with sound identifier, custom file path, and custom command rows - BrowserImportSection: import-hint variant + dismissed toggle in addition to blank-tab toggle; explanatory copy for the actual flow - GlobalHotkeySection: real enable toggle + pointer at Keyboard Shortcuts for the chord - AutomationSection: portBase + portRange steppers added - ResetSection: now resets both UserDefaults and JSON-backed catalog entries - SettingsSearchIndex: route integrations.* under Account section, rightSidebar.beta.*/betaFeatures.* under Beta Features Window swap: - cmuxApp.swift Settings Window now hosts SettingsWindowRoot(runtime:) from the package. SettingsWindowPresenter wiring kept via outer WindowAccessor(dedupeByWindow: false). The legacy SettingsView / SettingsRootView / SettingsWindowRootView types remain in source for now (unreachable from the running UI) and will be deleted in a follow-up cleanup commit once the package UI has been dogfooded. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Curated settings search index + SettingsNavigationRequest deeplink CuratedSettingEntries: hand-curated table mirroring the legacy SettingsSearchIndex.settingEntries + SettingsSearchAliasIndex tables. ~85 entries with user-facing titles plus the synonym strings the legacy index supported (so typing "copy on select", "imessage", "kagi", "react grab", etc. surfaces the right row). SettingsSearchIndex now combines three sources: 1. Section entries (one per SettingsSectionID, default sidebar view) 2. Curated setting entries (high-quality titles + synonyms) 3. Fallback dotted-id entries for catalog keys not yet curated isCovered dedupes catalog keys that the curated table already covers, so each setting surfaces exactly once. SettingsWindowRoot subscribes to NotificationCenter "cmux.settings.navigate" (the same name the legacy SettingsNavigationRequest uses). Existing host-app deeplinks route the user to the correct section automatically once the window swaps to the package UI. Anchor scroll-to + highlight glow are intentionally not reproduced - each section is its own pane in the split-view layout, so there is no cross-section scroll. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Replace CuratedSettingEntries namespace-enum with injectable DI Per the package-design discipline rule against namespace-enums ("no `enum Foo { static func bar() }` posing as a namespace"), the hand-curated search table is no longer hidden behind an enum. - CuratedSettingEntry: the public struct (one type per file). - CuratedSettingEntry+Default.swift: extension on [CuratedSettingEntry] exposing the cmux-shipped default table as `.cmuxDefault`. - SettingsSearchIndex.init now takes `curatedEntries: [CuratedSettingEntry] = .cmuxDefault` so tests pass an empty or focused list and hosts can append their own entries without forking the package. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Major parity pass: AccountFlow, AppIcon grid, palette editor, shortcut defaults+conflicts CmuxSettings: - PIIDisplayMode value type - AccountCatalogSection: piiDisplayMode, selectedTeamID, welcomeShown - ShortcutAction+Defaults: every action's factory ShortcutStroke ported from the legacy KeyboardShortcutSettings.Action table CmuxSettingsUI: - AccountFlow protocol + AccountIdentity + AccountTeamSummary types. Host wraps its own auth surface in an AccountFlow and injects via SettingsRuntime; the package's AccountSection drives sign-in, sign-out, refresh, team selection, identity card, and the PII display mode without depending on CMUXAuthCore. - AccountIdentityCard with avatar, redaction (PII display mode), Sign In / Sign Out / Refresh affordances. - AccountTeamPicker bound to the flow's selected team. - AccountSection rebuilt: Identity, Team, Privacy, Claude Code, Cursor, Gemini, Tools, Agent Notifications subsections. - AppIconGridPicker: visual 3-tile grid mirroring legacy App Icon UI. - WorkspaceColorsSection rebuilt: indicator + selection/badge color hex + custom palette array editor + palette overrides editor, observed and persisted through the JSON store with error-log surfacing on write failures. - KeyboardShortcutsSection: shows the factory default in the recorder placeholder ("⌘N (default)"); flags real conflict between two actions resolving to the same first chord; Reset (per row) and Reset All (section footer). cmuxApp.swift: - HostAccountFlow wraps AuthManager + AuthSettingsStore behind the package's AccountFlow protocol. - Settings runtime now injects accountFlow: HostAccountFlow() at app startup. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Add SettingsHostActions: clear history, external editor, test notification, restart, browser import, feedback SettingsHostActions protocol surfaces seven host-side actions the package's UI invokes: - clearBrowserHistory() - openConfigInExternalEditor() - sendFeedback() - sendTestNotification() - openSystemNotificationSettings() - restartApp() - openBrowserImportFlow() Wired through SettingsRuntime alongside AccountFlow. Sections check for nil before showing the corresponding button so previews / tests don't need a host. Section UI updates: - BrowserSection: "Clear Browser History" with confirmation dialog - SettingsJSONSection: "Open in External Editor" alongside the inline editor - AppSection: Onboarding "Welcome Shown" toggle (resurfaces the welcome flow on next launch), Feedback "Send Feedback…" button, language picker restart prompt that fires when the language changes - NotificationsRows: "Send Test" + "Open System Notification Settings…" buttons - BrowserImportSection: "Import Browser Data…" button - WorkspaceColorsSection: per-row Reset buttons for selection + notification badge color hex fields cmuxApp wiring: HostSettingsActions routes to BrowserHistoryStore, NSWorkspace, TerminalNotificationStore, BrowserDataImportCoordinator, and the GitHub issues URL. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Wire HostAccountFlow + HostSettingsActions into the cmux app target Adds the four pbxproj entries each for HostAccountFlow.swift and HostSettingsActions.swift so xcodebuild picks them up. The normalize script ran clean and check-pbxproj.sh passes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Two-stroke chord recording + notification authorization request ShortcutRecorderView: opt-in chord mode captures two consecutive strokes and yields a chorded StoredShortcut via a new onChord callback. Escape during chord-pending aborts cleanly. Single-stroke recording is unchanged. KeyboardShortcutsSection: each row exposes a per-row "chord" toggle button next to the recorder. While the toggle is on, the next two key strokes are captured as a chord (tmux-style); after the chord commits, the toggle automatically resets to single-stroke mode. SettingsHostActions: adds requestNotificationAuthorization() so the package's NotificationsRows can offer a "Request Permission" button alongside the existing "Open System Notification Settings" and "Send Test" affordances. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Rebuild Settings UI: legacy single-scroll SettingsCard layout Replaces the NavigationSplitView + Form/Section chrome with cmux's legacy in-app layout — one tall ScrollView of stacked sections, each section as a SettingsSectionHeader + SettingsCard pair. Sidebar is a List that scrolls the proxy to the section anchor on click. Mirrors what the user sees today and resolves the architectural mismatch flagged in the dogfood pass. Package additions (Chrome/ folder): - SettingsSectionHeader (small secondary-colored title above a card) - SettingsCard<Content> (13pt rounded grouped container with stroke) - SettingsCardRow<Trailing> (title + optional subtitle + control; configurationReview slot for dotted-cmux.json-path metadata) - SettingsCardDivider (1pt half-opacity separator) - SettingsConfigurationReview (.json("..."), .action, .settingsOnly, .debugOnly enum mirroring the legacy review metadata) All 13 sections rewritten as VStack(SettingsSectionHeader, SettingsCard) groups. Helper functions per section (toggleRow, textRow, pickerRow, stepperRow, sliderRow) keep call sites tight. Behavior is unchanged; chrome now matches legacy exactly. SettingsWindowRoot scrolls to the selected section's anchor (`.id`) in the LazyVStack, animating with easeInOut. Search via the sidebar's `.searchable` placement; deeplinks via the `cmux.settings.navigate` notification still route to the right section as before. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Account section trimmed to identity card only (legacy parity) Moves the integration toggles (Claude Code / Cursor / Gemini / ripgrep / subagent suppression) out of Account and into Automation to match legacy ordering. AccountSection is now just SettingsSectionHeader + SettingsCard wrapping AccountIdentityCard. Adds SettingsCardNote primitive (caption-style note rendered inside a card) ported from legacy chrome. Reorders sectionStack in SettingsWindowRoot to match legacy: account, app, terminal, sidebar, beta, automation, browser, browserImport, globalHotkey, keyboardShortcuts, workspaceColors, settingsJSON, reset. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Row-by-row port: Automation, Terminal, Sidebar, Beta, Global Hotkey, Keyboard Shortcuts, Reset Each section now mirrors the legacy SettingsView body row-for-row with the exact localized strings + accessibility identifiers + card structure ported from cmuxApp.swift: - AutomationSection: 8 cards (Socket Control with conditional password subrow + warnings, Claude Code, Claude Path, Ripgrep Path, Suppress Subagent, Cursor, Gemini, Port Base/Range) with exact SettingsCardNote copy - TerminalSection: scroll bar, textBoxMaxLines, copy on selection, auto resume, hibernation, idle seconds, max live terminals — all with the legacy dynamic subtitles - SidebarSection: match terminal + 18 sidebar detail toggles with the legacy conditional disabled() chain (hideAllDetails / PR visibility / PR clickability gating) - BetaFeaturesSection: dock toggle + warning note (ported BetaFeaturesWarningNote chrome) - GlobalHotkeySection: enable toggle + recorder + caption note - KeyboardShortcutsSection: chord docs link + Reset Defaults + per-action recorder rows with chord-mode toggle - ResetSection: centered Reset All Settings button + confirmation dialog - AccountSection trimmed to identity card only (legacy lives in one card with AuthSettingsRow) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Row-by-row port: App, Browser, Workspace Colors, cmux.json Final parity pass with the legacy SettingsView body. Every row in every section now uses the exact String(localized:defaultValue:) literals, accessibility identifiers, controlWidth values, divider placement, and (where applicable) conditional disabled() / hidden branches from the legacy implementation. AppSection: single SettingsCard containing 30 rows in legacy order — Language (with restart subtitle), Appearance, App Icon, New Workspace Placement, Inherit Working Directory, Minimal Mode, Keep Workspace Open When Closing Last Surface, Focus Pane on First Click, File Drops, Open Files With, Open Supported Files in cmux, Open Markdown in cmux Viewer, iMessage Mode, Reorder on Notification, Dock Badge, Menu Bar Only, Show in Menu Bar (disabled when menuBarOnly), Unread Pane Ring, Pane Flash, Desktop Notifications (host actions), Notification Sound, Notification Command, Send anonymous telemetry, Warn Before Quit (segmented), Warn Before Closing Tab, Warn Before Tab Close Button (disabled when hide), Hide Tab Close Button, Rename Selects Existing Name, Command Palette Searches All Surfaces. BrowserSection: single SettingsCard with Enable cmux Browser, Default Search Engine (custom subrows conditional), Show Search Suggestions, Browser Theme, Memory Saver + Delay, Open Terminal Links + Intercept open, conditional Hosts / External Patterns text editors, HTTP Allowlist editor with hint, embedded Import Browser Data block, React Grab Version, Browsing History (host action). BrowserImportSection collapsed to an empty deeplink anchor since the legacy renders import inline inside the Browser card. WorkspaceColorsSection: indicator-style picker, selection + notification badge color rows with ColorPicker + Reset, palette note, per-entry editor with ColorPicker + Remove, Reset Palette action. SettingsJSONSection: User config file row with display path + Open button, Documentation row with Open Docs link. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Review-loop fixes (iters 1-8): catalog encodings, missing rows, localization Catalog encoding alignment with legacy on-disk format (BLOCKERs from iter 7): - sidebar.branchVerticalLayout is Bool-backed (true=vertical) matching legacy sidebarBranchVerticalLayout key. Removed dead SidebarBranchLayout enum. - FileDropDefaultBehavior cases changed to .text + .preview (drop .path / .editor) matching legacy on-disk rawValue. Default flipped from .path to .text. AppSection: added Terminal Config row (Open Config button) via new SettingsHostActions.openTerminalConfigWindow(); rebuilt Notification Sound row to match legacy — Picker over NSSound system names + Preview button + conditional custom-file path/Clear when "custom". BrowserSection: added Save button + draftState to HTTP Allowlist editor (Save disabled when no unsaved changes), Browser Theme subtitle interpolates mode (system/light/dark), added SettingsHostActions.previewNotificationSound() seam. AutomationSection: Socket Mode Picker iterates SocketControlMode .allCases via socketModeLabel(_:) helper (matching legacy displayName routing). AccountSection / AccountIdentityCard: section header now localized, all visible identity strings routed through String(localized:); Sign In/Sign Out buttons honor isWorkingOnAuth as disabled state; dropped Refresh button (not present in legacy). KeyboardShortcutsSection: added Open cmux.json button next to Chord docs (matches legacy chord row affordances). WorkspaceColorsSection: indicator Picker iterates WorkspaceIndicatorStyle.allCases; selection/badge color rows show "Default" sentinel when hex is empty (matching legacy nil sentinel). BrowserImport navigation: removed BrowserImportSection's empty pane. SettingsWindowRoot sidebar filters out .browserImport, and the inline import block inside BrowserSection now carries the `section:browserImport` anchor id so deeplinks scroll there. Deleted dead-code section files: SidebarAppearanceSection, PlaceholderSection, NotificationsRows. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Wire up Settings search + iter 10 fixes The sidebar's .searchable field was previously bound to a state but not consumed. Now it filters the sidebar List in real time: - "Sections" group: section rows whose title or section keywords contain every query token (case- and diacritic-insensitive). - "Settings" group: curated entries whose title or synonym string match; each renders as a button that selects the parent section. - "No results" placeholder when both groups are empty. Updated stale catalog-key synonym (sidebar.branchLayout → sidebar.branchVerticalLayout) in CuratedSettingEntry+Default so searches for "branch layout" hit the right entry, and so the SettingsSearchIndex's isCovered() dedupe correctly recognizes the catalog key (was producing a duplicate raw dotted-id entry). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Match legacy SettingsRootView layout pixel-for-pixel The legacy SettingsView body wraps everything in: - NavigationSplitView with sidebar.navigationSplitViewColumnWidth(210) - ScrollView -> VStack(spacing: 14) -> all headers + cards flat - .padding(.horizontal, 20) .padding(.bottom, 20) .padding(.top, 20) - Sidebar entries via SettingsSidebarEntryRow (16pt icon + 1-line title + optional 1-line caption subtitle) The package previously rendered each section wrapped in its own inner VStack(spacing: 14) and used `LazyVStack(spacing: 18)` + padding (24, 22) at the root, which double-spaced sibling sections and left visible chrome offsets vs the legacy. All 12 section views now emit content via a flat `Group { … }` so the root VStack lays out every SettingsSectionHeader + SettingsCard pair at 14pt spacing, matching the legacy exactly. Sidebar restored to show every SettingsSectionID (Browser Import included, matching legacy `SettingsNavigationTarget.allCases`). Selecting Browser Import scrolls to the inline import block inside BrowserSection via the section:browserImport anchor id. Added SettingsSidebarEntryRow chrome (icon + 16pt slot + 1-line title + caption secondary subtitle) ported byte-for-byte from Sources/cmuxApp.swift:9119-9142. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Drive Settings package to legacy parity (Claude) Ports the legacy in-app SettingsView controls into the CmuxSettingsUI package row-by-row so the new SPM-backed window matches the stable cmux build pixel-for-pixel. Window shell - defaultSize(980,680), .contentMinSize, localized title, SidebarCommands - .toggleStyle(.switch) on the scroll container so every Toggle renders as a macOS switch - DispatchQueue.main.async scroll-to-section (no animation) to match legacy Theme + App Icon pickers - ThemeWindowThumbnail + ThemePickerRow + AppIconPickerRow ported from legacy; AppIconGridPicker removed Account - AccountIdentityCard rewritten as the legacy AuthSettingsRow shape: no avatar, title = primary email, subtitle = display name, plain Sign Out button, padding 14h/10v, PII redaction preserved App - File Drops uses legacy displayName / settingsSubtitle - Notification Sound custom path uses NSOpenPanel Choose... with truncated last-path-component preview - Telemetry shows "Change takes effect on next launch." when toggled from launch value - AccountTeamPicker strings localized Terminal - Removed fabricated Resume Commands TextEditor card - Threaded hostActions through TerminalSection Automation - SocketControlMode.uiCases + .displayName + .description ported from Sources/SocketControlSettings.swift - Password row shows save / clear / error status text under the field Browser - Memory Saver Delay label uses Xm Ys format - Browsing History subtitle is dynamic count; Clear disabled at 0 - Refresh button + footnote restored in import block - Added SettingsHostActions.browserHistoryEntryCount() (default nil) Global Hotkey - Recorder now persists chords to shortcuts.bindings["showHideAllWindows"] - Streams updates back via JSONConfigStore.values(for:) - Reset button matches legacy Keyboard Shortcuts - Filtered out .showHideAllWindows (Global Hotkey owns it) - Dropped per-row rawValue caption + extra chord toggle button - X / restore SF-Symbol icon button replaces text Reset / Clear Reset - Fires immediately, no confirmation dialog (matches legacy) Workspace Colors - Palette source switched from JSON to catalog.workspaceColors.palette - Built-in palette always renders with default hexes - Base: <hex> subtitle for built-ins; Remove gated to custom entries Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Settings parity rounds 9-10: sidebar collapse, key display, recorder polish (Claude) Final parity-loop passes over the package: - SettingsWindowScene: bind NavigationSplitView columnVisibility so the sidebar chevron / SidebarCommands toggle works; resolve sidebar selection through SettingsSearchIndex entries instead of re-parsing the id string, matching legacy SettingsRootView.selectSidebarEntry - GlobalHotkeySection + KeyboardShortcutsSection: render named keys (Tab, Space, return, media keys) via a keyDisplayString helper that mirrors legacy ShortcutStroke.keyDisplayString - AppSection + BrowserSection: column-width and delay-bound alignment Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Settings: explicit @State models, non-optional deps, correct defaults (Claude) Architecture - Value-models (DefaultsValueModel/JSONValueModel) now live in each view as explicit @State, built once in init; no inline construction, no cache. - set/reset are sync façades (callable from Bindings) that fire the store write in a Task and never mutate `current`; the observation stream is the single source of truth. JSONConfigStore now self-notifies subscribers on write so JSON-backed controls update deterministically (atomic-rename-safe). - errorLog and hostActions are non-optional throughout (runtime + every section); added NoopSettingsHostActions for previews/tests; removed all `if let hostActions` / `== nil` branches. Correctness / parity - keepWorkspaceOpen: catalog default true (legacy close-on-last-surface semantics) and the "Keep Workspace Open" toggle binds to the inverse. - Encoding fixes to round-trip with legacy: titlebarControlsStyle Int, workspaceButtonFade -> key workspaceButtonsFadeMode default "disabled", workspaceTitlebarVisibility -> Bool key workspaceTitlebarVisible default true. - GlobalHotkey recorder shows "None" when unbound (matches legacy). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Settings value models: single-writer current, recorder + colorpicker fixes (Claude) - Value models simplified to a single writer of `current` (the observation stream); `set`/`reset` write through to the store, no optimistic in-memory write, no dual-writer race. `UserDefaultsSettingsStore.values` uses `.bufferingNewest(1)` so a control sprayed with values (ColorPicker drag) coalesces to the latest instead of replaying every intermediate. - ColorPicker hex conversion is a `Color` extension (`Color(cmuxHex:)` / `.cmuxHexString`), replacing the prior namespace-enum. - Shortcut recorder: clicking again after a cancel re-enters recording (was a no-op because the button stayed first responder). - Localized the settings error alert strings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Settings: behavioral UI test suite, error-alert localization, fix Browser enable key (Claude) - Add Settings behavioral XCUITests on a shared SettingsUITestCase harness, one file per section. Tier-1 tests verify the observable effect (reactive binding/subtitle updates, Reset clears keys, password subrow reveal, import wizard opens); tier-2 (terminal/Metal/NSColorPanel/system surfaces) and tier-3 (cross-app/telemetry) are documented in each file, not faked. - Localize the settings error alert (settings.error.alert.title/.dismiss) with en + ja in Localizable.xcstrings. - Fix: catalog browser.disabled wrote `browserDisabled`, but the runtime gate BrowserAvailabilitySettings.isDisabled() reads `browserDisabledOverride` — the Enable Browser toggle was a no-op. Aligned the catalog to the legacy key. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * CmuxSettingsUI: drop to Swift tools 6.0 so CI (Swift 6.1) can resolve it (Claude) The package required tools 6.2 (.defaultIsolation(MainActor.self) in the manifest + an isolated deinit), but the required CI checks run on Swift 6.1, so package resolution failed on every gate. Removed the manifest default-isolation and added explicit @MainActor where the implicit isolation was relied on: - Setting: @preconcurrency DynamicProperty conformance - SettingsRuntime.init: @MainActor (for the NoopSettingsHostActions default) - RecorderHostButton.eventMonitor: nonisolated(unsafe) for the deinit - isolated deinit -> deinit Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Fix Swift 6.1 build: drop nonisolated(unsafe) UserDefaults sync read (Claude) CI builds on Swift 6.1, which rejects reading the non-Sendable `underlyingDefaults` from a nonisolated context (`currentValue(for:)`). Removed that sync read; DefaultsValueModel now seeds from the key default and the observation stream's first element supplies the stored value. underlyingDefaults reverts to an actor-isolated `let`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Fix Swift 6.1 warning: drop main-actor default arg in HostAccountFlow.init The default argument `authManager: AuthManager = .shared` evaluates in a nonisolated context, so referencing the @MainActor `AuthManager.shared` triggered 'main actor-isolated static property cannot be referenced from a nonisolated context' on Swift 6.1, breaking the warning budget (actual=1 budget=0). Pass .shared explicitly at the single main-actor call site. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: raise tests-build-and-lag timeout 20->35 for cold-cache builds The DerivedData cache key hashes project.pbxproj and Package.resolved with no restore-keys fallback, so any pbxproj/Package.resolved change (e.g. adding the CmuxSettings/CmuxSettingsUI packages) mints a fresh key and forces a full cold build. The cmux Swift codegen alone runs ~18-20 min, so the 20-min cap killed the build mid-compile; because the build was cancelled the post-cache step never saved the cache, so every retry stayed cold and timed out identically. main hit the same cancellation on cold builds (run 26634466118). 35 min lets a cold build finish and populate the cache. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> | 4 个月前 | |
Stop XCTest crashes from reaching Sentry (#8786) * test: cover Sentry startup under XCTest * fix: disable macOS Sentry under XCTest * Make Sentry startup policy constructable * test: cover sandbox-denied CLI socket telemetry * fix: drop sandbox-denied CLI socket telemetry * fix: allow explicit Sentry opt-in under XCTest * Address Sentry startup review policy * Close Sentry test launch review gaps * Require provenance for Sentry suppression * Document Sentry suppression contract --------- Co-authored-by: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> | 2 个月前 | |
Reimplement Settings as CmuxSettings/CmuxSettingsUI SPM packages (Claude) (#4975) * Add CmuxSettings + CmuxSettingsUI packages and integrate into the app target Foundation for the settings system migration. Both packages compile into cmux DEV.app and pass their own unit-test suites, but the running Settings window is still the legacy `SettingsView`; the new `SettingsWindowScene` is exported and wired but not yet substituted. The cutover is deliberately a separate, later change. CmuxSettings (Foundation only; 22/22 Swift Testing green) - `DefaultsKey<V>` / `JSONKey<V>` — phantom-typed setting handles; each store accepts only its flavor, so wrong-store mismatches are compile errors, not runtime traps. - `UserDefaultsSettingsStore` / `JSONConfigStore` — both `actor`s, no locks, no KVO, no `@Published`. Observation surfaces as `AsyncStream<V>`. UserDefaults observation via `NotificationCenter.default.notifications(named:)` async sequence; JSON observation via `JSONConfigFileWatcher` (DispatchSource hidden behind an `AsyncStream<Void>`). - `SettingCatalog` + `SettingCatalogSection` — struct-based registry composed of sub-section structs by dotted-id prefix. `catalog.all` walks `Mirror` recursively so there is no parallel hand-maintained list; adding a key in any `*CatalogSection` shows up everywhere. - ~107 catalog entries declared across nine sections (App, Terminal, Notifications, Sidebar, SidebarAppearance, WorkspaceColors, Automation, Browser, BetaFeatures, Shortcuts). - Value types for every storable enum, plus `StoredShortcut` + `ShortcutStroke` + 77-case `ShortcutAction` enum with display names + group categorization. - Legacy-key migration is type-validated: `AnySettingKey` captures the underlying `Value` in a closure so type-incompatible legacy values are skipped rather than silently coerced. CmuxSettingsUI (depends on CmuxSettings; 5/5 Swift Testing green) - Default-isolated to `MainActor` via `.defaultIsolation(MainActor.self)` (swift-tools-version 6.2). - `@Observable @MainActor` view-models (`DefaultsValueModel<V>`, `JSONValueModel<V>`) bridge the actor stores to SwiftUI synchronous Bindings. No `deinit` cleanup needed; `[weak self]` inside the observation Task ends naturally on dealloc. - Row primitives: `SettingsToggleRow`, `SettingsPickerRow<V>`, `SettingsTextFieldRow`, `SettingsStepperRow`, `ShortcutRecorderView` (NSViewRepresentable over a focusable NSView capturing keyDown with modifier flags). - `SettingsWindowScene` exposes a `Scene` the app composes; renders `NavigationSplitView` with a searchable sidebar and detail-switched section views. - 12 of 13 section views are real and bound to catalog keys; the remaining one (Account) is intentional out-of-scope until the auth storage moves out of `CMUXAuthCore`. - `KeyboardShortcutsSection` lists all 77 actions in groups and reads / writes `[ActionID: StoredShortcut]` through the JSON store with the recorder primitive. App-target integration - `cmux.xcodeproj/project.pbxproj`: both packages added as `XCLocalSwiftPackageReference`s and linked into the `cmux` target's frameworks build phase. - `cmux.xcworkspace`: both packages added to the `Packages` workspace group so they appear in the navigator. - `Sources/AppSettingsRuntime.swift`: app-owned singleton holder that constructs one `SettingCatalog`, one `UserDefaultsSettingsStore` (running migrations against `UserDefaults.standard`), and one `JSONConfigStore` (against `~/.config/cmux/cmux.json`). The package itself has no shared statics; this file is the DI seam. CLAUDE.md - New "Package design discipline" section codifying the rules that emerged this session: no `static let standard` singletons in packages, no namespace-enums, no parallel hand-maintained registries, prefer compile-time invariants over runtime traps, file-scope `private func` vs `private static func`, nested types still count for one-type-per-file. - New "Testability" section: every public type in `Packages/` must be testable without launching the app, without booting AppKit, and without depending on `UserDefaults.standard` or the user's filesystem. No global state in package code; DI via constructor. What's still pending (so the diff for the next round is interpretable) - Scene swap in `cmuxApp.swift` to present `SettingsWindowScene` in place of the legacy `SettingsView`. Held back until the explanatory sections (`Account`, `GlobalHotkey`) have real catalog-backed controls so the cutover does not visibly regress the running app. - ~50–60 more catalog entries to reach JSON-path parity with `Sources/CmuxSettingsJSONPathSupport.swift`. - ~200 `@AppStorage(...)` call sites across `Sources/` that still target legacy keys directly; the new catalog mirrors those keys exactly so the conversion is mechanical but bulky. - Default-bindings table for `ShortcutAction` (mining from the legacy `KeyboardShortcutSettings.swift` defaults dictionary). - Conflict detection and two-stroke chord recording in `ShortcutRecorderView`. - Runtime bridge from `[String: StoredShortcut]` to the legacy `KeyboardShortcutSettings.shared` lookup so the app's keyboard event path consults the new persistence. - `BetaFeaturesCatalogSection` is wired but its single `dock` flag is the only entry — additional beta flags add as they land. - `AccountCatalogSection` does not exist yet; `AccountSection` renders an explanation pointing at `Sources/Auth/AuthSettingsStore.swift`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Fix empty Settings window + harden JSON store error path SettingsWindowRootView gated its body on a `shouldRenderSettingsContent` flag driven by NSWindow notifications. `WindowAccessor` defaulted to dedupe-by-window, so the second open of the same SwiftUI Window reused the same NSWindow and the visibility handler was suppressed — leaving the body stuck on the Color.clear placeholder. Removed the gate; the content view now renders unconditionally and WindowAccessor is set to `dedupeByWindow: false` so the presenter is wired every reopen. Also wired the SettingsRuntime + SettingsErrorAlertModifier into both the Settings and Config window scenes (sibling Window scenes don't inherit environment values from the main WindowGroup), and hardened the JSON config error path: - JSONConfigStore.readFromDisk() now throws on corrupt / malformed JSON instead of silently returning [:]. mutateRoot() reads through the throwing path so writes refuse to overwrite a corrupt file (previously a stale-cache write would have destroyed user data). File-not-found is still treated as the empty state. - KeyboardShortcutsSection now records write failures into the injected SettingsErrorLog instead of `try?`-swallowing them. - DefaultsValueModel's asymmetry with JSONValueModel is documented (UserDefaults.set is non-throwing in Foundation; no error to route). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Dogfood: convert first @AppStorage to @Setting Converts the Right-Sidebar Dock beta toggle in `SettingsView` from `@AppStorage(RightSidebarBetaFeatureSettings.dockEnabledKey)` to `@Setting(\.betaFeatures.rightSidebarDock)`. The UserDefaults key is unchanged (`rightSidebar.beta.dock.enabled`), so existing user values round-trip. Reads now flow through `UserDefaultsSettingsStore` -> `DefaultsValueModel`'s `AsyncStream` projection, and writes go through the actor instead of synchronous UserDefaults. Same Binding<Bool> shape via the wrapper's `projectedValue`, so the existing `$rightSidebarDockEnabled` call site at cmuxApp.swift:7149 keeps working unchanged. This is the first concrete site exercising `SettingsRuntime` injection into the Settings window environment, and the first time `DefaultsValueModel` runs at app runtime. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Convert 18 @AppStorage call sites in SettingsView to @Setting All flagged via the catalog's existing DefaultsKey entries; UserDefaults keys are unchanged so existing user values round-trip. Storage now goes through UserDefaultsSettingsStore actor + DefaultsValueModel. App section: warnBeforeQuit, warnBeforeClosingTab, warnBeforeClosingTabXButton, hideTabCloseButton, renameSelectsExistingName, commandPaletteSearchesAllSurfaces, workspaceInheritWorkingDirectory, keepWorkspaceOpenWhenClosingLastSurface, focusPaneOnFirstClick, menuBarOnly, reorderOnNotification, iMessageMode. Notifications: dockBadge, unreadPaneRing, paneFlash, showInMenuBar. Terminal: showScrollBar, copyOnSelect, autoResumeAgentSessions, agentHibernationEnabled. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Expand package: integrations catalog + AppSection / SidebarSection / BrowserSection / AccountSection parity Catalog additions: - IntegrationsCatalogSection (claude/cursor/gemini hooks, claude path, ripgrep path, subagent notification suppression) - SidebarCatalogSection: activeTabIndicatorStyle, selectionColorHex, notificationBadgeColorHex - BrowserCatalogSection: disabled, importHintVariant, importHintDismissed Section views built out to legacy parity (or close): - AppSection: Appearance / Workspace Behavior / Command Palette / Quit-and-Close / Editor / File Handling / Workspace Presentation / Notifications / Telemetry subsections wired to ~25 catalog entries - SidebarSection: full Material / BlendMode / State pickers, light + dark tint hex fields, blur opacity slider, Custom Colors subsection - BrowserSection: disabled toggle, custom search engine fields, multiline hostname pattern editors for hosts/external/insecure - AccountSection: real Form with integrations toggles + custom paths + notification suppression. Sign-in / team selection still in legacy New row primitive: - SettingsDefaultsTextFieldRow — sibling of SettingsTextFieldRow for UserDefaults-backed strings with optional subtitle cmuxApp.swift @AppStorage -> @Setting conversions: - claudeCodeHooksEnabled, customClaudePath, customRipgrepPath, suppressSubagentNotifications, cursorHooksEnabled, geminiHooksEnabled, sendAnonymousTelemetry, preferredEditorCommand, openSupportedFilesInCmux, openMarkdownInCmuxViewer, browserSearchSuggestionsEnabled, browserDisabled, browserHiddenWebViewDiscardEnabled, browserHiddenWebViewDiscardDelay, isBrowserImportHintDismissed, showBrowserImportHintOnBlankTabs, reactGrabVersion, openTerminalLinksInCmuxBrowser Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Bring all section views in CmuxSettingsUI to parity; swap to package UI Catalog: - AppCatalogSection: add systemWideHotkeyEnabled (key "systemWideHotkey.enabled") so the global-hotkey toggle has a home - AnySettingKey: add resetInJSON closure so ResetSection can wipe JSON-backed entries in addition to UserDefaults overrides Row primitives: - SettingsDoubleStepperRow for bounded Double-typed catalog entries (hibernation idle seconds, etc.) Section views built out: - TerminalSection: idle-threshold stepper + multi-line Resume Commands editor backed by the JSON-config resumeCommands list - NotificationsRows: add Notification Sound subsection with sound identifier, custom file path, and custom command rows - BrowserImportSection: import-hint variant + dismissed toggle in addition to blank-tab toggle; explanatory copy for the actual flow - GlobalHotkeySection: real enable toggle + pointer at Keyboard Shortcuts for the chord - AutomationSection: portBase + portRange steppers added - ResetSection: now resets both UserDefaults and JSON-backed catalog entries - SettingsSearchIndex: route integrations.* under Account section, rightSidebar.beta.*/betaFeatures.* under Beta Features Window swap: - cmuxApp.swift Settings Window now hosts SettingsWindowRoot(runtime:) from the package. SettingsWindowPresenter wiring kept via outer WindowAccessor(dedupeByWindow: false). The legacy SettingsView / SettingsRootView / SettingsWindowRootView types remain in source for now (unreachable from the running UI) and will be deleted in a follow-up cleanup commit once the package UI has been dogfooded. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Curated settings search index + SettingsNavigationRequest deeplink CuratedSettingEntries: hand-curated table mirroring the legacy SettingsSearchIndex.settingEntries + SettingsSearchAliasIndex tables. ~85 entries with user-facing titles plus the synonym strings the legacy index supported (so typing "copy on select", "imessage", "kagi", "react grab", etc. surfaces the right row). SettingsSearchIndex now combines three sources: 1. Section entries (one per SettingsSectionID, default sidebar view) 2. Curated setting entries (high-quality titles + synonyms) 3. Fallback dotted-id entries for catalog keys not yet curated isCovered dedupes catalog keys that the curated table already covers, so each setting surfaces exactly once. SettingsWindowRoot subscribes to NotificationCenter "cmux.settings.navigate" (the same name the legacy SettingsNavigationRequest uses). Existing host-app deeplinks route the user to the correct section automatically once the window swaps to the package UI. Anchor scroll-to + highlight glow are intentionally not reproduced - each section is its own pane in the split-view layout, so there is no cross-section scroll. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Replace CuratedSettingEntries namespace-enum with injectable DI Per the package-design discipline rule against namespace-enums ("no `enum Foo { static func bar() }` posing as a namespace"), the hand-curated search table is no longer hidden behind an enum. - CuratedSettingEntry: the public struct (one type per file). - CuratedSettingEntry+Default.swift: extension on [CuratedSettingEntry] exposing the cmux-shipped default table as `.cmuxDefault`. - SettingsSearchIndex.init now takes `curatedEntries: [CuratedSettingEntry] = .cmuxDefault` so tests pass an empty or focused list and hosts can append their own entries without forking the package. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Major parity pass: AccountFlow, AppIcon grid, palette editor, shortcut defaults+conflicts CmuxSettings: - PIIDisplayMode value type - AccountCatalogSection: piiDisplayMode, selectedTeamID, welcomeShown - ShortcutAction+Defaults: every action's factory ShortcutStroke ported from the legacy KeyboardShortcutSettings.Action table CmuxSettingsUI: - AccountFlow protocol + AccountIdentity + AccountTeamSummary types. Host wraps its own auth surface in an AccountFlow and injects via SettingsRuntime; the package's AccountSection drives sign-in, sign-out, refresh, team selection, identity card, and the PII display mode without depending on CMUXAuthCore. - AccountIdentityCard with avatar, redaction (PII display mode), Sign In / Sign Out / Refresh affordances. - AccountTeamPicker bound to the flow's selected team. - AccountSection rebuilt: Identity, Team, Privacy, Claude Code, Cursor, Gemini, Tools, Agent Notifications subsections. - AppIconGridPicker: visual 3-tile grid mirroring legacy App Icon UI. - WorkspaceColorsSection rebuilt: indicator + selection/badge color hex + custom palette array editor + palette overrides editor, observed and persisted through the JSON store with error-log surfacing on write failures. - KeyboardShortcutsSection: shows the factory default in the recorder placeholder ("⌘N (default)"); flags real conflict between two actions resolving to the same first chord; Reset (per row) and Reset All (section footer). cmuxApp.swift: - HostAccountFlow wraps AuthManager + AuthSettingsStore behind the package's AccountFlow protocol. - Settings runtime now injects accountFlow: HostAccountFlow() at app startup. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Add SettingsHostActions: clear history, external editor, test notification, restart, browser import, feedback SettingsHostActions protocol surfaces seven host-side actions the package's UI invokes: - clearBrowserHistory() - openConfigInExternalEditor() - sendFeedback() - sendTestNotification() - openSystemNotificationSettings() - restartApp() - openBrowserImportFlow() Wired through SettingsRuntime alongside AccountFlow. Sections check for nil before showing the corresponding button so previews / tests don't need a host. Section UI updates: - BrowserSection: "Clear Browser History" with confirmation dialog - SettingsJSONSection: "Open in External Editor" alongside the inline editor - AppSection: Onboarding "Welcome Shown" toggle (resurfaces the welcome flow on next launch), Feedback "Send Feedback…" button, language picker restart prompt that fires when the language changes - NotificationsRows: "Send Test" + "Open System Notification Settings…" buttons - BrowserImportSection: "Import Browser Data…" button - WorkspaceColorsSection: per-row Reset buttons for selection + notification badge color hex fields cmuxApp wiring: HostSettingsActions routes to BrowserHistoryStore, NSWorkspace, TerminalNotificationStore, BrowserDataImportCoordinator, and the GitHub issues URL. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Wire HostAccountFlow + HostSettingsActions into the cmux app target Adds the four pbxproj entries each for HostAccountFlow.swift and HostSettingsActions.swift so xcodebuild picks them up. The normalize script ran clean and check-pbxproj.sh passes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Two-stroke chord recording + notification authorization request ShortcutRecorderView: opt-in chord mode captures two consecutive strokes and yields a chorded StoredShortcut via a new onChord callback. Escape during chord-pending aborts cleanly. Single-stroke recording is unchanged. KeyboardShortcutsSection: each row exposes a per-row "chord" toggle button next to the recorder. While the toggle is on, the next two key strokes are captured as a chord (tmux-style); after the chord commits, the toggle automatically resets to single-stroke mode. SettingsHostActions: adds requestNotificationAuthorization() so the package's NotificationsRows can offer a "Request Permission" button alongside the existing "Open System Notification Settings" and "Send Test" affordances. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Rebuild Settings UI: legacy single-scroll SettingsCard layout Replaces the NavigationSplitView + Form/Section chrome with cmux's legacy in-app layout — one tall ScrollView of stacked sections, each section as a SettingsSectionHeader + SettingsCard pair. Sidebar is a List that scrolls the proxy to the section anchor on click. Mirrors what the user sees today and resolves the architectural mismatch flagged in the dogfood pass. Package additions (Chrome/ folder): - SettingsSectionHeader (small secondary-colored title above a card) - SettingsCard<Content> (13pt rounded grouped container with stroke) - SettingsCardRow<Trailing> (title + optional subtitle + control; configurationReview slot for dotted-cmux.json-path metadata) - SettingsCardDivider (1pt half-opacity separator) - SettingsConfigurationReview (.json("..."), .action, .settingsOnly, .debugOnly enum mirroring the legacy review metadata) All 13 sections rewritten as VStack(SettingsSectionHeader, SettingsCard) groups. Helper functions per section (toggleRow, textRow, pickerRow, stepperRow, sliderRow) keep call sites tight. Behavior is unchanged; chrome now matches legacy exactly. SettingsWindowRoot scrolls to the selected section's anchor (`.id`) in the LazyVStack, animating with easeInOut. Search via the sidebar's `.searchable` placement; deeplinks via the `cmux.settings.navigate` notification still route to the right section as before. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Account section trimmed to identity card only (legacy parity) Moves the integration toggles (Claude Code / Cursor / Gemini / ripgrep / subagent suppression) out of Account and into Automation to match legacy ordering. AccountSection is now just SettingsSectionHeader + SettingsCard wrapping AccountIdentityCard. Adds SettingsCardNote primitive (caption-style note rendered inside a card) ported from legacy chrome. Reorders sectionStack in SettingsWindowRoot to match legacy: account, app, terminal, sidebar, beta, automation, browser, browserImport, globalHotkey, keyboardShortcuts, workspaceColors, settingsJSON, reset. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Row-by-row port: Automation, Terminal, Sidebar, Beta, Global Hotkey, Keyboard Shortcuts, Reset Each section now mirrors the legacy SettingsView body row-for-row with the exact localized strings + accessibility identifiers + card structure ported from cmuxApp.swift: - AutomationSection: 8 cards (Socket Control with conditional password subrow + warnings, Claude Code, Claude Path, Ripgrep Path, Suppress Subagent, Cursor, Gemini, Port Base/Range) with exact SettingsCardNote copy - TerminalSection: scroll bar, textBoxMaxLines, copy on selection, auto resume, hibernation, idle seconds, max live terminals — all with the legacy dynamic subtitles - SidebarSection: match terminal + 18 sidebar detail toggles with the legacy conditional disabled() chain (hideAllDetails / PR visibility / PR clickability gating) - BetaFeaturesSection: dock toggle + warning note (ported BetaFeaturesWarningNote chrome) - GlobalHotkeySection: enable toggle + recorder + caption note - KeyboardShortcutsSection: chord docs link + Reset Defaults + per-action recorder rows with chord-mode toggle - ResetSection: centered Reset All Settings button + confirmation dialog - AccountSection trimmed to identity card only (legacy lives in one card with AuthSettingsRow) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Row-by-row port: App, Browser, Workspace Colors, cmux.json Final parity pass with the legacy SettingsView body. Every row in every section now uses the exact String(localized:defaultValue:) literals, accessibility identifiers, controlWidth values, divider placement, and (where applicable) conditional disabled() / hidden branches from the legacy implementation. AppSection: single SettingsCard containing 30 rows in legacy order — Language (with restart subtitle), Appearance, App Icon, New Workspace Placement, Inherit Working Directory, Minimal Mode, Keep Workspace Open When Closing Last Surface, Focus Pane on First Click, File Drops, Open Files With, Open Supported Files in cmux, Open Markdown in cmux Viewer, iMessage Mode, Reorder on Notification, Dock Badge, Menu Bar Only, Show in Menu Bar (disabled when menuBarOnly), Unread Pane Ring, Pane Flash, Desktop Notifications (host actions), Notification Sound, Notification Command, Send anonymous telemetry, Warn Before Quit (segmented), Warn Before Closing Tab, Warn Before Tab Close Button (disabled when hide), Hide Tab Close Button, Rename Selects Existing Name, Command Palette Searches All Surfaces. BrowserSection: single SettingsCard with Enable cmux Browser, Default Search Engine (custom subrows conditional), Show Search Suggestions, Browser Theme, Memory Saver + Delay, Open Terminal Links + Intercept open, conditional Hosts / External Patterns text editors, HTTP Allowlist editor with hint, embedded Import Browser Data block, React Grab Version, Browsing History (host action). BrowserImportSection collapsed to an empty deeplink anchor since the legacy renders import inline inside the Browser card. WorkspaceColorsSection: indicator-style picker, selection + notification badge color rows with ColorPicker + Reset, palette note, per-entry editor with ColorPicker + Remove, Reset Palette action. SettingsJSONSection: User config file row with display path + Open button, Documentation row with Open Docs link. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Review-loop fixes (iters 1-8): catalog encodings, missing rows, localization Catalog encoding alignment with legacy on-disk format (BLOCKERs from iter 7): - sidebar.branchVerticalLayout is Bool-backed (true=vertical) matching legacy sidebarBranchVerticalLayout key. Removed dead SidebarBranchLayout enum. - FileDropDefaultBehavior cases changed to .text + .preview (drop .path / .editor) matching legacy on-disk rawValue. Default flipped from .path to .text. AppSection: added Terminal Config row (Open Config button) via new SettingsHostActions.openTerminalConfigWindow(); rebuilt Notification Sound row to match legacy — Picker over NSSound system names + Preview button + conditional custom-file path/Clear when "custom". BrowserSection: added Save button + draftState to HTTP Allowlist editor (Save disabled when no unsaved changes), Browser Theme subtitle interpolates mode (system/light/dark), added SettingsHostActions.previewNotificationSound() seam. AutomationSection: Socket Mode Picker iterates SocketControlMode .allCases via socketModeLabel(_:) helper (matching legacy displayName routing). AccountSection / AccountIdentityCard: section header now localized, all visible identity strings routed through String(localized:); Sign In/Sign Out buttons honor isWorkingOnAuth as disabled state; dropped Refresh button (not present in legacy). KeyboardShortcutsSection: added Open cmux.json button next to Chord docs (matches legacy chord row affordances). WorkspaceColorsSection: indicator Picker iterates WorkspaceIndicatorStyle.allCases; selection/badge color rows show "Default" sentinel when hex is empty (matching legacy nil sentinel). BrowserImport navigation: removed BrowserImportSection's empty pane. SettingsWindowRoot sidebar filters out .browserImport, and the inline import block inside BrowserSection now carries the `section:browserImport` anchor id so deeplinks scroll there. Deleted dead-code section files: SidebarAppearanceSection, PlaceholderSection, NotificationsRows. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Wire up Settings search + iter 10 fixes The sidebar's .searchable field was previously bound to a state but not consumed. Now it filters the sidebar List in real time: - "Sections" group: section rows whose title or section keywords contain every query token (case- and diacritic-insensitive). - "Settings" group: curated entries whose title or synonym string match; each renders as a button that selects the parent section. - "No results" placeholder when both groups are empty. Updated stale catalog-key synonym (sidebar.branchLayout → sidebar.branchVerticalLayout) in CuratedSettingEntry+Default so searches for "branch layout" hit the right entry, and so the SettingsSearchIndex's isCovered() dedupe correctly recognizes the catalog key (was producing a duplicate raw dotted-id entry). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Match legacy SettingsRootView layout pixel-for-pixel The legacy SettingsView body wraps everything in: - NavigationSplitView with sidebar.navigationSplitViewColumnWidth(210) - ScrollView -> VStack(spacing: 14) -> all headers + cards flat - .padding(.horizontal, 20) .padding(.bottom, 20) .padding(.top, 20) - Sidebar entries via SettingsSidebarEntryRow (16pt icon + 1-line title + optional 1-line caption subtitle) The package previously rendered each section wrapped in its own inner VStack(spacing: 14) and used `LazyVStack(spacing: 18)` + padding (24, 22) at the root, which double-spaced sibling sections and left visible chrome offsets vs the legacy. All 12 section views now emit content via a flat `Group { … }` so the root VStack lays out every SettingsSectionHeader + SettingsCard pair at 14pt spacing, matching the legacy exactly. Sidebar restored to show every SettingsSectionID (Browser Import included, matching legacy `SettingsNavigationTarget.allCases`). Selecting Browser Import scrolls to the inline import block inside BrowserSection via the section:browserImport anchor id. Added SettingsSidebarEntryRow chrome (icon + 16pt slot + 1-line title + caption secondary subtitle) ported byte-for-byte from Sources/cmuxApp.swift:9119-9142. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Drive Settings package to legacy parity (Claude) Ports the legacy in-app SettingsView controls into the CmuxSettingsUI package row-by-row so the new SPM-backed window matches the stable cmux build pixel-for-pixel. Window shell - defaultSize(980,680), .contentMinSize, localized title, SidebarCommands - .toggleStyle(.switch) on the scroll container so every Toggle renders as a macOS switch - DispatchQueue.main.async scroll-to-section (no animation) to match legacy Theme + App Icon pickers - ThemeWindowThumbnail + ThemePickerRow + AppIconPickerRow ported from legacy; AppIconGridPicker removed Account - AccountIdentityCard rewritten as the legacy AuthSettingsRow shape: no avatar, title = primary email, subtitle = display name, plain Sign Out button, padding 14h/10v, PII redaction preserved App - File Drops uses legacy displayName / settingsSubtitle - Notification Sound custom path uses NSOpenPanel Choose... with truncated last-path-component preview - Telemetry shows "Change takes effect on next launch." when toggled from launch value - AccountTeamPicker strings localized Terminal - Removed fabricated Resume Commands TextEditor card - Threaded hostActions through TerminalSection Automation - SocketControlMode.uiCases + .displayName + .description ported from Sources/SocketControlSettings.swift - Password row shows save / clear / error status text under the field Browser - Memory Saver Delay label uses Xm Ys format - Browsing History subtitle is dynamic count; Clear disabled at 0 - Refresh button + footnote restored in import block - Added SettingsHostActions.browserHistoryEntryCount() (default nil) Global Hotkey - Recorder now persists chords to shortcuts.bindings["showHideAllWindows"] - Streams updates back via JSONConfigStore.values(for:) - Reset button matches legacy Keyboard Shortcuts - Filtered out .showHideAllWindows (Global Hotkey owns it) - Dropped per-row rawValue caption + extra chord toggle button - X / restore SF-Symbol icon button replaces text Reset / Clear Reset - Fires immediately, no confirmation dialog (matches legacy) Workspace Colors - Palette source switched from JSON to catalog.workspaceColors.palette - Built-in palette always renders with default hexes - Base: <hex> subtitle for built-ins; Remove gated to custom entries Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Settings parity rounds 9-10: sidebar collapse, key display, recorder polish (Claude) Final parity-loop passes over the package: - SettingsWindowScene: bind NavigationSplitView columnVisibility so the sidebar chevron / SidebarCommands toggle works; resolve sidebar selection through SettingsSearchIndex entries instead of re-parsing the id string, matching legacy SettingsRootView.selectSidebarEntry - GlobalHotkeySection + KeyboardShortcutsSection: render named keys (Tab, Space, return, media keys) via a keyDisplayString helper that mirrors legacy ShortcutStroke.keyDisplayString - AppSection + BrowserSection: column-width and delay-bound alignment Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Settings: explicit @State models, non-optional deps, correct defaults (Claude) Architecture - Value-models (DefaultsValueModel/JSONValueModel) now live in each view as explicit @State, built once in init; no inline construction, no cache. - set/reset are sync façades (callable from Bindings) that fire the store write in a Task and never mutate `current`; the observation stream is the single source of truth. JSONConfigStore now self-notifies subscribers on write so JSON-backed controls update deterministically (atomic-rename-safe). - errorLog and hostActions are non-optional throughout (runtime + every section); added NoopSettingsHostActions for previews/tests; removed all `if let hostActions` / `== nil` branches. Correctness / parity - keepWorkspaceOpen: catalog default true (legacy close-on-last-surface semantics) and the "Keep Workspace Open" toggle binds to the inverse. - Encoding fixes to round-trip with legacy: titlebarControlsStyle Int, workspaceButtonFade -> key workspaceButtonsFadeMode default "disabled", workspaceTitlebarVisibility -> Bool key workspaceTitlebarVisible default true. - GlobalHotkey recorder shows "None" when unbound (matches legacy). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Settings value models: single-writer current, recorder + colorpicker fixes (Claude) - Value models simplified to a single writer of `current` (the observation stream); `set`/`reset` write through to the store, no optimistic in-memory write, no dual-writer race. `UserDefaultsSettingsStore.values` uses `.bufferingNewest(1)` so a control sprayed with values (ColorPicker drag) coalesces to the latest instead of replaying every intermediate. - ColorPicker hex conversion is a `Color` extension (`Color(cmuxHex:)` / `.cmuxHexString`), replacing the prior namespace-enum. - Shortcut recorder: clicking again after a cancel re-enters recording (was a no-op because the button stayed first responder). - Localized the settings error alert strings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Settings: behavioral UI test suite, error-alert localization, fix Browser enable key (Claude) - Add Settings behavioral XCUITests on a shared SettingsUITestCase harness, one file per section. Tier-1 tests verify the observable effect (reactive binding/subtitle updates, Reset clears keys, password subrow reveal, import wizard opens); tier-2 (terminal/Metal/NSColorPanel/system surfaces) and tier-3 (cross-app/telemetry) are documented in each file, not faked. - Localize the settings error alert (settings.error.alert.title/.dismiss) with en + ja in Localizable.xcstrings. - Fix: catalog browser.disabled wrote `browserDisabled`, but the runtime gate BrowserAvailabilitySettings.isDisabled() reads `browserDisabledOverride` — the Enable Browser toggle was a no-op. Aligned the catalog to the legacy key. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * CmuxSettingsUI: drop to Swift tools 6.0 so CI (Swift 6.1) can resolve it (Claude) The package required tools 6.2 (.defaultIsolation(MainActor.self) in the manifest + an isolated deinit), but the required CI checks run on Swift 6.1, so package resolution failed on every gate. Removed the manifest default-isolation and added explicit @MainActor where the implicit isolation was relied on: - Setting: @preconcurrency DynamicProperty conformance - SettingsRuntime.init: @MainActor (for the NoopSettingsHostActions default) - RecorderHostButton.eventMonitor: nonisolated(unsafe) for the deinit - isolated deinit -> deinit Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Fix Swift 6.1 build: drop nonisolated(unsafe) UserDefaults sync read (Claude) CI builds on Swift 6.1, which rejects reading the non-Sendable `underlyingDefaults` from a nonisolated context (`currentValue(for:)`). Removed that sync read; DefaultsValueModel now seeds from the key default and the observation stream's first element supplies the stored value. underlyingDefaults reverts to an actor-isolated `let`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Fix Swift 6.1 warning: drop main-actor default arg in HostAccountFlow.init The default argument `authManager: AuthManager = .shared` evaluates in a nonisolated context, so referencing the @MainActor `AuthManager.shared` triggered 'main actor-isolated static property cannot be referenced from a nonisolated context' on Swift 6.1, breaking the warning budget (actual=1 budget=0). Pass .shared explicitly at the single main-actor call site. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: raise tests-build-and-lag timeout 20->35 for cold-cache builds The DerivedData cache key hashes project.pbxproj and Package.resolved with no restore-keys fallback, so any pbxproj/Package.resolved change (e.g. adding the CmuxSettings/CmuxSettingsUI packages) mints a fresh key and forces a full cold build. The cmux Swift codegen alone runs ~18-20 min, so the 20-min cap killed the build mid-compile; because the build was cancelled the post-cache step never saved the cache, so every retry stayed cold and timed out identically. main hit the same cancellation on cold builds (run 26634466118). 35 min lets a cold build finish and populate the cache. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> | 4 个月前 | |
Gate Feed behind Beta Features (mirror Dock), default off Feed was made unconditionally available by #3854 (5829da2d9, "Enable Feed by default"), which deleted its beta gating. This reinstates the gating — mirroring Dock exactly — so Feed is hidden from the right-sidebar mode switcher until the user opts in under Settings → Beta Features. Default is off, same as Dock. Only the gating-removal hunks of #3854 are reinstated; that PR's Feed focus-handling bugfixes (and the "All Activity" rename) are preserved. The Settings layer was refactored into Swift packages since #3854, so Feed is mirrored in both parallel registrations Dock now lives in: - RightSidebarBetaFeatureSettings: add feedEnabledKey / defaultFeedEnabled / isFeedEnabled mirroring the Dock members (rightSidebar.beta.feed.enabled, default false). - RightSidebarMode+Availability: gate .feed on feedEnabled, threaded through availableModes / isAvailable alongside dockEnabled (reverts to the pre-#3854 shape). - RightSidebarPanelView: read feedEnabled via @AppStorage and feed it into availability + refresh-on-change. - App-target search index (SettingsNavigation, SettingsSearchAliases) and CmuxSettingsUI package index (CuratedSettingEntry+Default, SettingsSectionID): register the feed beta setting, anchor, and aliases mirroring dock. - CmuxSettings BetaFeaturesCatalogSection: add rightSidebarFeed catalog key (cmux.json-configurable, default false). - CmuxSettingsUI BetaFeaturesSection: re-add the Feed toggle row (SettingsBetaFeedToggle) with on/off subtitle. - CommandPaletteSettingsToggle: add the betaFeatures.feed toggle command. - Localizable.xcstrings: restore settings.betaFeatures.feed / .subtitleOn / .subtitleOff (en + ja). The plural beta warning already covers >1 feature (Dock + Extensions), so it is unchanged. Tests: - FileExplorerStateModePersistenceTests / RightSidebarCommandPaletteTests: restore the gated assertions (disabled Feed clamps to Files; default mode set excludes Feed). - RightSidebarChromeHeightUITests: re-add the feed-enable launch arg so the feed secondary bar is reachable. - SettingsSidebarBetaBehaviorUITests: add a Feed toggle derived-subtitle test mirroring the Dock one. - SettingsRowAnchorResolutionTests: register the feed search anchor. Feed hooks (per-tool-use feed.push installers) are intentionally NOT gated on this flag — see PR description. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> | 3 个月前 | |
fix: harden emergency agent hibernation | 2 个月前 | |
Stop XCTest crashes from reaching Sentry (#8786) * test: cover Sentry startup under XCTest * fix: disable macOS Sentry under XCTest * Make Sentry startup policy constructable * test: cover sandbox-denied CLI socket telemetry * fix: drop sandbox-denied CLI socket telemetry * fix: allow explicit Sentry opt-in under XCTest * Address Sentry startup review policy * Close Sentry test launch review gaps * Require provenance for Sentry suppression * Document Sentry suppression contract --------- Co-authored-by: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> | 2 个月前 | |
Organize SPM packages into Packages/{Shared,iOS,macOS}/ so the folder tree mirrors the workspace groups (#6278) Every Swift package now lives physically under exactly one group directory (Packages/Shared, Packages/iOS, Packages/macOS), so the repo's directory tree and the root workspace's group columns are the same shape. Opening cmux.xcworkspace shows each package under the Shared / iOS / macOS group matching the folder it lives in. Folder is the source of truth. Group = which app(s) consume the package: both apps -> Shared, iOS app only -> iOS, macOS app only -> macOS. check-workspace-package-groups.py mirrors the folders directly; --write regenerates the workspace, --check (in CI, beside check-pbxproj) fails on drift. All boundary-crossing relative paths were rewritten to keep the build intact: inter-package deps same group `../Name` / cross group `../../<Group>/Name`; escaping paths gain one level (vendor `../../../vendor/...`, GhosttyKit `../../../GhosttyKit.xcframework`); macOS project relativePaths, ios/cmuxPackage and Examples deps + project relativePaths, the file-length budget, the iOS conventions lint scopes, the namespace-type baseline, the test-ios change globs, the ci.yml per-package `swift test` loop (now resolves the group dir), and doc/skill references all updated to the nested paths. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> | 3 个月前 | |
Stop XCTest crashes from reaching Sentry (#8786) * test: cover Sentry startup under XCTest * fix: disable macOS Sentry under XCTest * Make Sentry startup policy constructable * test: cover sandbox-denied CLI socket telemetry * fix: drop sandbox-denied CLI socket telemetry * fix: allow explicit Sentry opt-in under XCTest * Address Sentry startup review policy * Close Sentry test launch review gaps * Require provenance for Sentry suppression * Document Sentry suppression contract --------- Co-authored-by: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> | 2 个月前 | |
Stop XCTest crashes from reaching Sentry (#8786) * test: cover Sentry startup under XCTest * fix: disable macOS Sentry under XCTest * Make Sentry startup policy constructable * test: cover sandbox-denied CLI socket telemetry * fix: drop sandbox-denied CLI socket telemetry * fix: allow explicit Sentry opt-in under XCTest * Address Sentry startup review policy * Close Sentry test launch review gaps * Require provenance for Sentry suppression * Document Sentry suppression contract --------- Co-authored-by: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> | 2 个月前 | |
Stop XCTest crashes from reaching Sentry (#8786) * test: cover Sentry startup under XCTest * fix: disable macOS Sentry under XCTest * Make Sentry startup policy constructable * test: cover sandbox-denied CLI socket telemetry * fix: drop sandbox-denied CLI socket telemetry * fix: allow explicit Sentry opt-in under XCTest * Address Sentry startup review policy * Close Sentry test launch review gaps * Require provenance for Sentry suppression * Document Sentry suppression contract --------- Co-authored-by: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> | 2 个月前 | |
Stop XCTest crashes from reaching Sentry (#8786) * test: cover Sentry startup under XCTest * fix: disable macOS Sentry under XCTest * Make Sentry startup policy constructable * test: cover sandbox-denied CLI socket telemetry * fix: drop sandbox-denied CLI socket telemetry * fix: allow explicit Sentry opt-in under XCTest * Address Sentry startup review policy * Close Sentry test launch review gaps * Require provenance for Sentry suppression * Document Sentry suppression contract --------- Co-authored-by: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> | 2 个月前 | |
Stop XCTest crashes from reaching Sentry (#8786) * test: cover Sentry startup under XCTest * fix: disable macOS Sentry under XCTest * Make Sentry startup policy constructable * test: cover sandbox-denied CLI socket telemetry * fix: drop sandbox-denied CLI socket telemetry * fix: allow explicit Sentry opt-in under XCTest * Address Sentry startup review policy * Close Sentry test launch review gaps * Require provenance for Sentry suppression * Document Sentry suppression contract --------- Co-authored-by: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> | 2 个月前 | |
Stop XCTest crashes from reaching Sentry (#8786) * test: cover Sentry startup under XCTest * fix: disable macOS Sentry under XCTest * Make Sentry startup policy constructable * test: cover sandbox-denied CLI socket telemetry * fix: drop sandbox-denied CLI socket telemetry * fix: allow explicit Sentry opt-in under XCTest * Address Sentry startup review policy * Close Sentry test launch review gaps * Require provenance for Sentry suppression * Document Sentry suppression contract --------- Co-authored-by: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> | 2 个月前 | |
Stop XCTest crashes from reaching Sentry (#8786) * test: cover Sentry startup under XCTest * fix: disable macOS Sentry under XCTest * Make Sentry startup policy constructable * test: cover sandbox-denied CLI socket telemetry * fix: drop sandbox-denied CLI socket telemetry * fix: allow explicit Sentry opt-in under XCTest * Address Sentry startup review policy * Close Sentry test launch review gaps * Require provenance for Sentry suppression * Document Sentry suppression contract --------- Co-authored-by: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> | 2 个月前 | |
test: tighten workspace cwd spawn coverage | 2 个月前 | |
Stop XCTest crashes from reaching Sentry (#8786) * test: cover Sentry startup under XCTest * fix: disable macOS Sentry under XCTest * Make Sentry startup policy constructable * test: cover sandbox-denied CLI socket telemetry * fix: drop sandbox-denied CLI socket telemetry * fix: allow explicit Sentry opt-in under XCTest * Address Sentry startup review policy * Close Sentry test launch review gaps * Require provenance for Sentry suppression * Document Sentry suppression contract --------- Co-authored-by: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> | 2 个月前 |