LearnNewsExamplesServices
Frontmatter
id19174
titleThe film's splitter resize is a real pointer drag with its live preview
stateClosed
labels
enhancementaidashboard
assigneesneo-fable
createdAtSep 24, 2026, 5:14 PM
updatedAtSep 24, 2026, 8:02 PM
githubUrlhttps://github.com/neomjs/neo/issues/19174
authorneo-fable
commentsCount0
parentIssue19126
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[x] 19178 The drag sensors keep their pressed element after a click that never became a drag
blocking[]
closedAtSep 24, 2026, 8:02 PM

The film's splitter resize is a real pointer drag with its live preview

Closed v13.2.0/archive-v13-2-0-chunk-23 enhancementaidashboard
neo-fable
neo-fable commented on Sep 24, 2026, 5:14 PM

Context

Sub of #19126 (the visual-first opening). The epic's Fix lists seven beats; two are still pending in AC-1: beat 4, the native return (#19170, @neo-gpt-emmy), and beat 6, "a splitter drag with the live preview; the layout settles", which has no cue and no leaf. The operator's steer behind the epic (2026-09-23 13:5xZ, relayed by Emmy) names splitters among the attractions the film must perform, not narrate.

The Problem

The film cannot show a splitter being dragged. Its only resize is the cold open's op step — {operation: 'resizeSplit', splitNodeId: 'split-main', sizes: [0.52, 0.48]} (apps/workstation/tour/fiveBeatFilm.mjs:118-119) — a document operation applied through the reducer: the boundary jumps, no pointer moves, and the live conserved-pair preview that #17849 shipped (both panes tracking the pointer on the main thread, the commit equal to the last previewed frame) is never on screen. The cue vocabulary (apps/workstation/tour/cueVocabulary.mjs:13) has overflow, scroll, canvas-update, cross-zone-showcase, theme, rail, the gate, the three window gestures and the perspective/history cues — nothing that drags a boundary.

The Architectural Reality

  • The drive primitive already exists and is the product's own path. Neo.ai.client.InteractionService#driveDrag({source, destination, waypoints, durationMs, steps}) (src/ai/client/InteractionService.mjs:285) resolves node anchors and deltas into window geometry, imports the DragDrop and EventSimulator addons on demand, and hands one atomic physical drag to Neo.main.addon.EventSimulator#driveDrag (src/main/addon/EventSimulator.mjs:266): mousedown on the source node, the live Mouse sensor's own delay (100 ms) and minDistance (5 px, src/main/draggable/sensor/Mouse.mjs:20,24) crossed by an arming move, steps moves over durationMs on document.body, mouseup at the path's end, with drag:start/move/end correlated by native Event identity. The receipt is typed — {success, phase, sensor: {delayMs, minDistance}, observed: {started, moveCount, ended}, dispatch, released, error} — and never throws for gesture outcomes; a failed drive attempts a best-effort mouseup. Limits: steps in [1, 120], durationMs ≥ 16 ms per step. The Neural Link's drive_drag tool and test/playwright/e2e/core/SplitterLiveResizeNL.spec.mjs:164 consume the same method.
  • The dock splitter previews live and commits once. Neo.dashboard.dock.interaction.DockSplitter (src/dashboard/dock/interaction/DockSplitter.mjs) defaults liveResize: true, carries splitNodeId_ and boundaryIndex_, captures the pair's layout boxes through parent.getLayoutRect(ids) on drag start (:239-278), and on release commits exactly one resizeSplit from the terminal's CSS-bounded pixel report (resolveSizeVector, :577: data.resizeSize wins over pointer arithmetic; the pair total is conserved), then fires dockSplitterResize or dockSplitterResizeRejected (:473). A refused or cancelled terminal restores the main-thread presentation (settleMainThreadResize, :493). The projection stamps every splitter with dockNodeType: 'splitter', dockNodeId/splitNodeId and boundaryIndex (src/dashboard/dock/projection/LayoutAdapter.mjs:320-345), so host.down({dockNodeType: 'splitter', splitNodeId, boundaryIndex}) finds the boundary a cue names.
  • The executor family and its lifecycle rules. apps/workstation/tour/GestureDriver.mjs owns in-window scripted input: runGesture (:42, dispatched input settles before cleanup releases it; run.pending is the lease), the film cursor dot (createFilmCursorDot / retireFilmCursorDot), waitFor/trap. executeRailStep (:578) with settleRailFold (:830) is the sibling shape: real input, receipts per phase, a settle that reads the live document after a failed or destroyed run and borrows nothing the destruction retires. The controller dispatches cues in TourController#executeCue (apps/workstation/view/TourController.mjs:290-321; rail at :298), and a receipt with applied: false must carry cancelled/reentered plus proof.documentsUnchanged (:527-537), so a resize that commits nothing must say so through those fields.
  • The witnesses. test/playwright/unit/apps/workstation/tour/fiveBeatFilm.spec.mjs pins the scene order, the cue list against WORKSTATION_CUE_TYPES, the op vocabulary (['resizeSplit', 'splitNode', 'addTab']), the 93 s sum and the pacing bounds; test/playwright/unit/apps/workstation/GestureDriver.spec.mjs:215-435 is the rail fixture and its five arms; test/playwright/e2e/workstation/WorkstationFilmTourNL.spec.mjs replays the journey and asserts one receipt per cue in screenplay order. evaluateExpectations accepts {path, equals, epsilon} (src/ai/client/tourScript.mjs:143), which a proportion committed from a pixel report needs: the signature's {path: 'nodes.split-main.sizes', equals: [0.52, 0.48]} (fiveBeatFilm.mjs:353) pins the cold open's exact op result today.

The Fix

  1. resize joins the vocabulary (cueVocabulary.mjs, after rail) and TourController#executeCue routes it to executeResizeStep({sizes: cue.sizes, splitNodeId: cue.splitNodeId}, cue.options).
  2. GestureDriver#executeResizeStep({splitNodeId, sizes}, {attempts, boundaryIndex=0, durationMs, moveDelay, moveSteps, showCursor, tolerance}) inside runGesture: validate the split node and the vector (WorkspaceDocument.normalizeSplitSizes); find the splitter and read the parent's and children's layout boxes through the splitter's own seam; convert the requested proportion of the named boundary's pane into a pixel travel along the split axis (sizes[b] × children total − current px); drive it with interactionService.driveDrag({source: {targetId: splitter.id, windowId}, destination: {deltaX|deltaY}, durationMs, steps: moveSteps}) as run.pending, the cursor dot walking the same segment on the same clock; read the pane's layout box once mid-drive to prove the preview moved while dockModel.nodes[splitNodeId].sizes stayed; after the drive wait for the committed vector and refreshPromise. Receipt: {applied, errors, proof: {axis, committedOnce, documentUnchangedDuringPreview, drive: {phase, sensor, observed}, previewTracked, sizesAfter, sizesBefore, travelPx}}; applied needs a successful drive and a committed vector within tolerance (default 0.01) of the request; a boundary stopped by a CSS bound fails by name with the vector it did commit.
  3. settleResizeDrive: on any failed exit — including a destroyed driver's catch — await the pending drive (the main thread owns the mousedown→mouseup bracket; no late Escape or mouseup is dispatched), then read the live document with a plain timer poll and report {driveSettled, committed, errors}. No residue by construction: the drive releases its own pointer and a refused terminal restores the main-thread styles.
  4. The screenplay: a film-resize scene after film-rails — one pause (≤ 1 s) carrying {type: 'resize', splitNodeId: 'split-main', sizes: [0.42, 0.58], options: filmPace} (the busy twelve-tab group gains the room the grid can spare; the boundary follows the pointer with both panes re-flowing live, then commits once) and a topology-assert with epsilon; filmCueTypes, the header's show-order and the 93 s sentence move with it; the signature's readout pins the new proportion with epsilon: 0.01.
  5. Pins: the vocabulary arm, the order pin, the budget pin and the op pin in fiveBeatFilm.spec.mjs; resize arms beside the rail arms in GestureDriver.spec.mjs; the film witness asserts the resize receipt.

Contract Ledger

Surface Authority Behavior Refusal / cleanup Docs Evidence
resize cue (cueVocabulary.mjs, TourController#executeCue) #19126; the cue list the unit spec pins against the controller {type: 'resize', splitNodeId, sizes, options} drags the named boundary to the requested proportion Unknown split node, wrong vector length or a missing splitter return applied: false with a named error and no input dispatched JSDoc on the executor + the screenplay header vocabulary arm; unit arms; film replay
GestureDriver#executeResizeStep runGesture's cleanup lease; InteractionService#driveDrag's typed receipt Real Mouse-sensor arming, live preview proven mid-drive, one committed resizeSplit A failed drive commits nothing and carries the simulator's phase/code; destruction settles the physical drive before cleanup; a bounded stop fails by name with the committed vector JSDoc unit arms (happy, failed drive, destroyed mid-drive, bounded)
film-resize scene + the two-tier witness the screenplay's narrative authority; the document tier runs no cues (fiveBeatFilm.spec's spec-mode run), so a proportion a cue commits lives in the cue's receipt, not in a topology-assert budget ≤ 4 s; one pause (≤ 1 s) carrying the cue; split-main → [0.42, 0.58] ± 0.01 asserted from the cue receipt's sizesAfter in the film witness; the screenplay's readouts pin the split's order — header show-order order/budget/op pins; the film witness's receipt asserts

Decision Record impact

none — aligned with ADR 0029's worker-truth boundary: the drive previews on the main thread and the document commits through the existing operation; no new authority or remote.

Acceptance Criteria

  • AC-1 resize is a handled cue; executeResizeStep drags the named split boundary through the Mouse sensor's own arming via InteractionService#driveDrag, proves the live preview mid-gesture (the pane's layout box moved while the committed sizes stayed), and the release commits exactly one resizeSplit within tolerance of the requested proportion; the receipt carries the sizes before and after, the drive's sensor thresholds and observed counts, and the travel.
  • AC-2 Unit lifecycle controls: a failed drive commits nothing and names the simulator's phase and code; destruction during the drive settles the physical drag before cleanup and reports the document as it is; a boundary stopped by a CSS bound fails by name with the committed vector; no cursor dot survives any exit.
  • AC-3 The film carries a film-resize scene after the rails (budget ≤ 4 s, one pause ≤ 1 s carrying the cue), the film witness asserts the committed proportion from the cue's receipt (sizesAfter within 0.01 of [0.42, 0.58]; the screenplay's readouts pin the split's order because the document tier runs no cues — restated 2026-09-24 from "the signature's readout with an epsilon" after the spec-mode run showed a cue's effect cannot be asserted there), and the film replay (WorkstationFilmTourNL.spec.mjs -g 'under autoGates') asserts the resize receipt at the PR head on the seat's own bridge — the two-tier witness (restated 2026-09-24, in step with AC-5): headless for the cold open's drive, and headed on the film-stage display (NEO_FILM_DISPLAY_BOUNDS) for the after-rails beat, because the film's native-return beat (#19181) refuses its frame move in headless Chromium and stops the headless replay before that beat; the headed run asserts the same receipt fields through the same spec arm.
  • AC-4 The vocabulary arm pins the new cue; the order, budget and op pins move with the scene in the same change, inside the 90–150 s envelope.
  • AC-5 (evidence-gated) The cold open's programmatic resizeSplit op becomes the first resize drag (60/40 → 52/48, the epic's "a splitter drag is the first motion") when the film replay proves both resize receipts (applied, previewTracked, committedOnce, the committed proportion) green 3/3 at the PR head — headless for the cold open's drive, headed for beat 6 while the film's beat 4 (native-return, #19181) refuses its frame move in headless Chromium and stops the headless replay before beat 6 (restated 2026-09-24: the full-replay 3/3 gate as first written is unreachable for any head since #19181; the witness break is noted for the film parent); otherwise the op stays and the PR body says why. Met at PR #19182 head e7f95d9043 (2026-09-24 17:36–17:39Z): the cold open's drive 3/3 headless and 3/3 headed, beat 6's drive 3/3 headed under the fail-closed proof — the headed replays on the film-stage display through the shared NEO_FILM_DISPLAY_BOUNDS contract, both receipts per run recorded in the PR body's AC-3 row; the op is replaced.

Out of Scope

  • The native-return cue (#19170, @neo-gpt-emmy's seam); the dense tour (denseWorkstation.mjs); the take, native capture, voice (#15252).
  • Splitter engine changes: #17849 shipped the preview; an engine defect the drive surfaces gets its own ticket with a browser proof.

Avoided Traps

  • Hand-dispatching mousedown/mousemove like the tear-out executor. The simulator already owns the sensor arming (delay + distance + correlated drag:start) and returns a typed receipt; duplicating it in the driver would be a second, uncorrelated arming path. The drive is the product's own path — the same the Neural Link's drive_drag tool uses — so the film's claim is the product's path.
  • Applying resizeSplit through the reducer. The cold open does that already; it is a document operation, not a pointer beat, and shows no preview.
  • Reading main-thread preview styles. The App worker cannot see them; the layout box through the splitter's own getLayoutRect seam is the transform-immune truth the splitter itself captures.
  • Pinning an exact fraction after a pixel-quantised drive. The commit is px / total; the readout needs epsilon.

Related

#19126 · #15252 · #19170 (beat 4) · #19127 / PR #19128 (the rail cue, the sibling shape) · #17849 / #17820 (the live conserved-pair preview) · #19152 / PR #19153 (the re-paced opening)

Sweeps: live latest-20 open at 2026-09-24T15:12:30Z — none equivalent (#19170 is beat 4; #19173 is canvas input); A2A (all read-states, last 60 min) — no claim on a resize cue, my [lane-intent] 15:05Z stands alone; Memory Core (query_raw_memories, the beat's nouns) — no prior decision, the 09-23 filing names the beat as planned with no leaf; own open assignments (#19126, #19101, #15252) — parents only.

Origin Session ID: a31cea94-2b9d-43cb-8a4e-3daf8706ce45 Retrieval Hint: query_raw_memories("film resize cue executeResizeStep driveDrag splitter live preview split-main")

🪢 Mnemosyne (Claude Fable 5.1 · Claude Code) · session a31cea94-2b9d-43cb-8a4e-3daf8706ce45

tobiu referenced in commit 202699d - "feat(workstation): the film's splitter resize is a real pointer drag with its live preview (#19174) (#19182) on Sep 24, 2026, 8:02 PM
tobiu closed this issue on Sep 24, 2026, 8:02 PM