LearnNewsExamplesServices
Frontmatter
id19070
titleThe Workstation tour can play the film: gated cross-window steps
stateClosed
labels
enhancementaitestingdashboard
assigneesneo-fable
createdAtSep 23, 2026, 2:27 AM
updatedAtSep 23, 2026, 10:56 AM
githubUrlhttps://github.com/neomjs/neo/issues/19070
authorneo-fable
commentsCount2
parentIssue15252
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtSep 23, 2026, 10:56 AM

The Workstation tour can play the film: gated cross-window steps

Closed Backlog/active-chunk-24 enhancementaitestingdashboard
neo-fable
neo-fable commented on Sep 23, 2026, 2:27 AM

Context

Operator, 2026-09-23 ~00:30 CEST: the tour inside apps/workstation "looks boring" now that multi-window, tear-out and rails were polished, and a generated video for the README is wanted. Measured on dev@a186e92cfa:

  • The in-app tour (TourToolbar "Start dense tour" → TourController#startTour → TourRunner with workstationTourScript from apps/workstation/tour/denseWorkstation.mjs) plays four scenes — dense overview, dropzone choreography, promote, theme handoff. None births a window.
  • The film screenplay apps/workstation/tour/fiveBeatFilm.mjs (#16311 / #16376; witness authority test/playwright/e2e/workstation/WorkstationFiveBeatNL.spec.mjs) has eight scenes: cold open, showcase, tear-out, re-entry, second-window docking, stack return, perspectives + undo/redo, signature. Its scenes carry narration and beats (strings) and no runnable steps; nothing consumes it at runtime.
  • The choreography the operator misses exists as app-owned real-pointer executors in apps/workstation/tour/NativeGestureDriver.mjs (executeTearOutStep with cancel / reenter, the convert-while-dragging executor, executeStackReturnStep, the vessel birth / commit / retirement gates) — driven today only from Playwright.

The Problem

Two authorities for one journey: the viewer sees the dense script; the film and its witness use another. The film's window scenes cannot run from a button as they stand, for one mechanical reason: a scripted vessel birth (Neo.Main.windowOpen at apps/workstation/view/VesselWorkspace.mjs:1035) needs transient user activation — browsers block window.open outside a user gesture, and the app already reports that outcome fail-closed as lastVesselOpen.stage = 'windowOpen-false'. The runner's v1 vocabulary is op | pause | topology-assert | cross-window (src/ai/client/tourScript.mjs:66); it has no step that waits for the viewer.

The Architectural Reality

  • Neo.ai.client.TourRunner: modes demo (paced), record (script-authored pacing), spec (waits skipped); crossWindowExecutor.executeCrossWindowStep(step) returns a settlement receipt validated by validateCrossWindowResult; pause is viewer pacing only — and the host settlement boundary (stepSettlement) already holds the next beat until a cue resolves.
  • TourController#runTourSpec(script, options) already accepts any script; startTour hardcodes the dense one (TourController.mjs:67).
  • Perspectives live on DockService (capturePerspective / listPerspectives / restorePerspective), not in the Operations vocabulary; 'perspective' is a RESERVED_STEP_TYPES entry. Dock undo/redo run on one Group cursor through the Neural Link (#19063 is the proof).
  • Film-take mode (NEO_FILM_TAKE=1) records the same journey drive per page. One runnable script therefore serves both the viewer's tour and the README film.

The Fix

  1. A gate as a host cue — {type: 'pause', ms: 0, cue: {type: 'gate', prompt}}: the controller publishes the prompt and holds the beat through its existing settlement boundary until the viewer clicks Continue; autoGates resolves gates at once for replays and takes. The runner stays document-tier (delta from the first draft of this ticket, which put a gate step type into the runner: the app already owns the seam).
  2. apps/workstation/tour/fiveBeatFilm.mjs: every scene gains steps: the cold open and showcase reuse the dense script's op / pause + cue shapes; the morph, the committed tear-out, the second-window conversion and the stack return become gate + real-pointer cues over the existing executors; perspectives and undo/redo become cues over the workspace's own seams; the signature is a topology-assert. narration stays the caption source; beats name what each step shows.
  3. TourController: the cue vocabulary (gate, tear-out, convert-while-dragging, stack-return, perspective-capture, perspective-restore, undo, redo), startFilmTour, continueTour, read-only oracles for whitebox specs. TourToolbar: a "Start film tour" button and a Continue affordance bound to the gate (a click or Space is the activation the birth needs; the gate caption says where to look).
  4. Witnesses: unit — fiveBeatFilmScript validates, names only cues the controller handles, gates every birth, keeps its budget, and runs its document tier twice with identical logs; e2e — the film plays from the toolbar under Playwright with real Continue clicks and the executors' own receipts for scenes 3–7; the dense tour and its specs stay untouched.
  5. Docs: the Workstation README documents both tours; the dense tour remains the no-window fallback.

Contract Ledger Matrix

Backfilled 2026-09-23 after review (PR #19083 RA-4), matching the shipped implementation.

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
TourController#startTour(script, {autoGates}) apps/workstation/view/TourController.mjs Plays one screenplay visibly; autoGates given sets the controller's gate mode (true = gates settle without a viewer), omitted keeps it A running playback returns the running promise and a caption class JSDoc, Workstation README TourController.spec.mjs gate-mode test; WorkstationFilmTourNL.spec.mjs viewer and autoGates arms
TourController#startFilmTour(options) same startTour(fiveBeatFilmScript, options) as above README e2e film arms
TourController#continueTour() same Resolves the pending gate with {applied: true, continued: 'viewer', prompt}; the resolving click is the user activation the next birth runs inside; returns false when nothing waits — README, TourToolbar Continue button unit gate test; e2e viewer arm (three Continue clicks)
TourController#getPendingGate() same {prompt} while a gate waits, else null — JSDoc unit gate test; e2e hold test
TourController#getTourState() same {caption, completedCount, gate, running, totalBeats} read from the root provider's tour.* keys — the transport oracle (the bridge resolves property paths only) null fields when unset JSDoc e2e polls
TourController#getCueLog() same {cueErrors, cues, pending, vessels}: settled cues with applied / cancelled / reentered / documentsUnchanged / errors, promise pending-ness probes, compact vessel states — forensics without proof documents — JSDoc e2e diagnostics on timeout
autoGates (controller field) same Gate mode; settable directly before Start by a transport (set_instance_properties), or through startTour false JSDoc e2e autoGates arm sets it through the fixture, then presses the ordinary button
Gate cue {type: 'pause', ms: 0, cue: {type: 'gate', prompt}} apps/workstation/tour/fiveBeatFilm.mjs, cueVocabulary.mjs Holds the beat at the host settlement boundary until continueTour(); under autoGates settles at once with continued: 'auto' — screenplay header fiveBeatFilm.spec.mjs (a gate precedes every birth)
Provider key tour.gatePrompt denseWorkstation.mjs initial state, TourToolbar binds A string while a gate waits (the Continue button's text and visibility), null otherwise null README unit gate test reads the provider
Cue settlement (fail closed) TourController#onTourBeat / #settleTourStep A cue receipt with errors, or applied: false without a cancelled/reentered terminal, or an un-applied terminal without proof.documentsUnchanged === true, fails the beat: the runner aborts before the next beat; the receipt and the cue error stay recorded — class JSDoc, README TourController.spec.mjs negative controls; Workspace.spec.mjs truth-binding tests
Cue receipt fields driver executors and controller cues applied, errors[], optional cancelled, reentered, continued, proof.{born, documentsUnchanged, …}, tabs (membership after restore/undo/redo), transactionId — JSDoc per cue method e2e assertFilmReceipt
WORKSTATION_CUE_TYPES apps/workstation/tour/cueVocabulary.mjs The exact cue list executeCue handles; screenplays are validated against it unknown cue → false → failed cue file header fiveBeatFilm.spec.mjs

Acceptance Criteria

  • AC-1 A gate holds playback until the viewer continues and resolves on its own under autoGates; unit- and e2e-witnessed.
  • AC-2 fiveBeatFilmScript validates and runs end to end — its document tier in spec mode (unit), the full screenplay including scenes 3–7 as the visible tour under Playwright with the same executor receipts WorkstationFiveBeatNL.spec.mjs asserts (proof.born, reentered, applied, phase order).
  • AC-3 In a headed browser, "Start film tour" plays scenes 1–2 unattended, presents a gate before every vessel birth, and a click on Continue births the vessel (no windowOpen-false) — measured in Chromium; a second engine is a residual owned by #15243.
  • AC-4 The dense tour and denseWorkstation.spec.mjs are unchanged and green, and WorkstationNL.spec.mjs keeps baseline parity with clean dev: the same 8/9, whose one red is #19086 (the dark splitter's :active read at :1294, pre-existing, owned there). Restated 2026-09-23 after review: the control's red is deterministic on clean dev and is not this leaf's to fix.
  • AC-5 NEO_FILM_TAKE=1 records the film script's run (per-page videos), so the README film derives from the runnable script — one authority, stated in the file header. Struck 2026-09-23: the take's recording of the runnable screenplay is production work on the parent #15252, not this leaf's wiring; the screenplay header states the one-authority intent.
  • AC-6 (post-merge) one recorded take is reviewed against the treatment's continuity contract on #15252 (one continuous world; no reboot on camera).

Out of Scope

  • #16498 (the resilience beat stays conditional) and #17871 (if the film's cold open touches the 100k matrix and reds, that is #17871's current red — reported there, not fixed here).
  • Narration audio, the voice, the cut itself (D-011 sequencing on #15252).
  • The SharedWorker boundary and the second-root crash (D#18730, #18376); the film runs one root plus its vessels.
  • The driver's own preconditions measured while filming (a tab folded into an overflow menu does not arm; a twelve-tab group does not re-arm its reattach on re-entry; a stack return needs the torn-out pane's stored home to survive) — defect-notes, respected by the screenplay, not fixed here.

Avoided Traps

  • Scripted vessel births without activation: blocked silently in production browsers, and windowOpen-false is the receipt that proves the trap. The gate is the honest carriage, not a workaround.
  • A second screenplay: the film script is the screenplay; the tour and the take consume it. No parallel step list in the controller.
  • Reusing the dense tour's timings: the film's targetSeconds budget the cut, and record mode pins the authored pacing.
  • Asserting cue effects with a following topology-assert: the document-tier replay executes no cues, so cue effects are witnessed through their receipts (each carries the tabs membership it produced).

Related

#15252 (parent) · #16311 · #16376 · #17871 · #16498 · #19063 · #18461 · D#18730 · #15243

Sweeps: live latest-20 open at 2026-09-23T00:26:32Z — none; tour/film title search — #17871 and #16498 are the epic's open subs, neither makes the film runnable; A2A herd window — the film epic is mine, [lane-claim] followed the filing; Memory Core: the 2026-08-08 treatment and Emmy's 2026-08-09 disposition (SCALE folded into the cold open, L4 narrowed to the two-window climax unless a bounded probe composes a third vessel) — this leaf implements that shape; own-assignment: #15252 (parent), #19055, #19059. Structure map: N/A (one new data-only sibling, cueVocabulary.mjs, beside the two screenplays).

Origin Session ID: 610d2189-0b28-46e2-89f3-3c42eae5de21 (earlier in the same session: 28a95670-a9dc-4214-8387-189e996c16b6) Retrieval Hint: query_raw_memories("Workstation film tour gate cue cross-window runnable fiveBeatFilm NativeGestureDriver windowOpen-false stored home")

Ownership: self-assigned; the parent epic is mine.

🪢 Mnemosyne (Claude Fable 5.1 · Claude Code) · session 610d2189-0b28-46e2-89f3-3c42eae5de21

tobiu referenced in commit be9d239 - "feat(workstation): the tour plays the film — gated window beats over the driver's executors (#19070) (#19083) on Sep 23, 2026, 10:56 AM
tobiu closed this issue on Sep 23, 2026, 10:56 AM