The lane-resize release gets its own hold — the strip stops two-stepping through the stale snapshot

LaneWidthHold: after the release the session's frozen standard and the
WRITTEN unit count keep governing the strip until a landed snapshot
carries that width back — dividing the already-grown window by the
stale unit total in between was the visible two-step. A deliberately
separate type from CommittedHold: that hold stands in for an
arrangement and any landing retires it; this one stands in for a value
and only a landing that carries it will do. Echo reads through
LaneLayoutMath.displayUnits so the width-1 key-removal case compares
right; the watch rides landedReloads so a value-equal echo still
answers; width writes now return whether bytes reached disk so a
refused or no-op write dissolves the hold instead of arming it; the
1500ms timeout family covers the rest.

Drag-perf card 231e3693.

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
This commit is contained in:
2026-08-01 19:33:33 -04:00
parent f6105d4389
commit 409f430813
6 changed files with 591 additions and 32 deletions
+24 -3
View File
@@ -172,6 +172,17 @@ struct BoardView: View {
.onChange(of: store.snapshotGeneration) { _, generation in
appModel.dragSession.handOff(root: store.rootKey, generation: generation)
}
// **The lane-resize release hold's hand-off** (`LaneWidthHold`), the same shape one rung
// narrower: that overlay stands in for an *arrangement* and any landing retires it, this one
// for a *width* and only a landing that carries it will do an unrelated reload arriving
// first must not hand the strip back to a snapshot that still says the old unit count.
//
// **`landedReloads`, not `snapshotGeneration`**: the question is "is the width current",
// asked on every walk that landed, so a reload whose tree came back value-equal which
// skips the assignment and moves no generation still gets to answer it.
.onChange(of: store.landedReloads) { _, _ in
resize.handOff(against: store.snapshot.lanes)
}
.trashPurgeAlert(store: store, confirmations: confirmations)
// The board's own anchor for the Style popover the surface a board-targeted session hangs
// off, since the board has no item to attach to (`styleEditorPresentation`'s `nil` anchor).
@@ -435,8 +446,12 @@ struct BoardView: View {
/// cross-board case alone, being ignored by a within-board lane drag.
@ViewBuilder
private func laneSlot(_ lane: Lane, standard: CGFloat) -> some View {
let resizing = resize.isResizing(lane.id)
let units = resizing ? resize.units : LaneLayoutMath.displayUnits(of: lane)
// **The session governs through the release**, not just the drag: after the release its
// frozen standard and the unit count it *wrote* keep answering here until the snapshot
// carries that width back (`LaneWidthHold`). Reading `lane.width` in that window would draw
// the pre-drag layout for a round trip.
let resizing = resize.governs(lane.id)
let units = resize.displayUnits(of: lane)
let slotWidth = LaneLayoutMath.slotWidth(units: units, standard: standard, gap: spacing)
ZStack(alignment: .topLeading) {
if resizing {
@@ -474,7 +489,7 @@ struct BoardView: View {
store: store,
session: resize,
laneID: lane.id,
committedUnits: LaneLayoutMath.displayUnits(of: lane),
committedUnits: units,
standard: standard,
gap: spacing,
window: window
@@ -561,6 +576,12 @@ struct BoardView: View {
/// value equals what the formula yields once the session ends the handoff is seamless (see
/// `LaneResizeSession`).
///
/// **"Once the session ends" is the echo, not the release** (`LaneWidthHold`). The equality that
/// makes the handoff seamless is between the frozen standard and the formula run over the *new*
/// unit total, and the write takes a watcher round trip to put that total in the snapshot;
/// dividing the already-grown window by the stale total in between is exactly the two-step the
/// hold exists to remove.
///
/// Otherwise it is the ordinary division, over three contributions:
///
/// - the **live lanes**' units. A lane in flight is still a lane on the board, so a within-board