Skip to content

automation: a tape plays on its own stage, over unsaved work too - #665

Merged
foxnne merged 1 commit into
mainfrom
automation/tape-own-stage
Oct 19, 2026
Merged

foxnne merged 1 commit into
mainfrom
automation/tape-own-stage

Conversation

@foxnne

@foxnne foxnne commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

Follows #623 / #639 (a demo sets the person's panes aside).

What changes

Opening a tape (a demo) while an unsaved document is open no longer shows the "Save or close your changes first" toast. The demo just starts, on a stage of its own, and when it ends the person's work is exactly as they left it.

Why the prompt existed. Demo.begin closed every open document and Demo.end reopened each one from its path, or from bytes kept at begin. An unsaved document would have come back as the file on disk, with its edits and undo history gone. So startable refused to start a demo while one was dirty.

What happens now. Nothing of the person's is closed:

  • Workbench.holdPanes sets their panes aside whole: the workspaces, a copy of each pane's tabs (its assignment), and the split tree (app: a demo's hold on the panes survives a failed copy and a launch #639's copy). The demo gets a pane of its own under a fresh grouping. The seating pass in rebuildWorkspaces never puts a held document into the demo's panes. restorePanes drops the demo's panes and gives back the person's, each holding what it held.
  • Demo.begin records the person's open document ids and hides their surfaces (Host.setSurfaceHidden), so no place shows them while the demo has the window. The documents stay open, with their contents, dirty state, undo history, watchers and tracks untouched.
  • A document is the demo's unless it is one of those ids, or one landing since in a pane of the person's (Workbench.holdsDocument). That covers an open already on its way when the demo started, and a document brought back by a plugin reload. Those are hidden too, never closed, and seated in their pane at the end. A close, detach or rename of the person's document during the demo updates their held panes, so no stale handle or tab id comes back.
  • Keyframes close only the demo's documents (closeDemoDocuments). Snapshots capture and restore only the demo's documents, and the replay fingerprint covers only those. A seek never touches the person's documents.
  • Demo.end closes the demo's documents, reloads the layout, restores the panes and unhides the person's documents. It also puts back their project folder, the explorer's open folders, the active document and keyboard focus. The reopen-from-path/bytes path (reopenable, documentBytes) is gone: nothing needs reopening.
  • Settings were already held in memory: the debounced settings.zon write is skipped while a demo is loaded, and personsSettings writes the person's material if anything else writes the file. The new test asserts the file is not written across a demo that changes the material, a seek back, and its end.

One root fix this surfaced. selectInRegion for a pane that had not drawn yet was dropped, so the pane's first draw showed its first tab. The demo's pane is new when the demo starts. Straight play therefore came up on the keyframe's first document, while a seek back (into the now-drawn pane) came up on the last opened one, and the bundled-demo seek test caught the mismatch. A selection for a region not drawn yet is now kept under its name's key (Region.namedSelectionKey), so the first draw honours it.

sdk.services.demos.Api.Play.unsaved is never answered now. It stays in the enum, with its doc comment saying so, so that plugins switching on it keep building and the boundary does not move. It can go at the next SDK release that moves the boundary for another reason.

SDK impact

  • None (a doc comment on Play.unsaved only; the fingerprint does not move)
  • Core-only or additive: reaches plugins at the next SDK release
  • Fingerprint moved: recorded in sdk/src/version.zig, sdk_version untouched, PR labelled sdk
  • SDK release: bumps sdk_version, lists the sdk PRs since the last sdk-v* tag

Verified

  • macOS: tests/demos.zig has a new block. It opens a document, makes it dirty with text.write, and plays a demo that changes the window material through to its end. It then seeks back to 0 and forward again, and ends the demo. The test checks that afterwards the document has the same id, is still dirty, has the same text, has the same history line (entries and position), is in the same pane, and is in front. It also checks that the pane count and the open-document count are unchanged, that no pane is left held, and that settings.zon has the same mtime and size. During the demo the stage shows only the demo's pane and document. The existing bundled-demo test still passes: every bundled demo plays through and every seek lands on what playing reached, over a zip opened from bytes and a person's two-pane split.
  • Windows:
  • Linux: CI's test-integration
  • Web: check-web builds. Not run in a browser.

Follow-ups

  • A whole-app command a tape runs (save all, say) still reaches the person's hidden documents. No bundled tape does this.

  • Loads still in flight when a keyframe cuts are cancelled as before. An open the person started the instant before a demo is dropped.

  • Play.unsaved goes at the next boundary-moving SDK release.

  • Behind FIZZY_WORKSPACES=1 (the app arranging the panes, Workbench.places), the held panes are not views of the app's while the demo plays. Not tried under that flag.

🤖 Generated with Claude Code

A demo no longer refuses to start over unsaved changes. It used to close
every open document at its start and reopen them from disk at its end,
so unsaved edits and undo history would have been lost; the toast asking
the person to save or close first guarded that.

Now the person's session is set aside untouched. The workbench holds
their panes whole (workspaces, each pane's tabs, the split tree) and
the demo plays in a fresh pane of its own. Their documents stay open
behind it, their surfaces hidden: never closed, saved, reloaded, or
put back by a snapshot. Keyframes, snapshots, seeks and the
fingerprint touch only the demo's own documents. At the end the demo's
documents close and the person's panes, documents, folder, explorer
folders, active document and focus come back as they were.
@foxnne
foxnne force-pushed the automation/tape-own-stage branch from 9c922c5 to 31eb8a5 Compare October 19, 2026 21:09
@foxnne
foxnne marked this pull request as ready for review October 19, 2026 21:09
@foxnne
foxnne enabled auto-merge October 19, 2026 21:10
@foxnne
foxnne added this pull request to the merge queue Oct 19, 2026
Merged via the queue into main with commit 2e85581 Oct 19, 2026
11 checks passed
@foxnne
foxnne deleted the automation/tape-own-stage branch October 19, 2026 21:33
foxnne added a commit that referenced this pull request Oct 19, 2026
Two bugs the maintainer saw in the tour (the "A Tour of Fizzy" tape on the web build), and what replaying the tour without its mid-tour cuts turned up.

## 1. "When dragging the document tabs, the drop zone flickers rapidly"

**Cause.** A glide finds its target again every frame (`Sequencer.advanceSpans` → `locate`). While a button is held, the thing under the pointer moves *because* of what is carried. The tour's first drag carries README onto `src/main.zig`'s tab, and the strip makes room for it, so `main.zig`'s tab moves away. The glide then chased it, and the room closed again. Measured headless at 16 ms frames: for the last ~300 ms of that drag the pointer jumped between x≈156 and x≈402 every other frame, so the strip's insertion gap flickered. Frame pacing doesn't matter: it happens at any frame rate, the web's included.

**Fix (`sdk/tape/Sequencer.zig`).** A glide that begins with a button held (`Glide.carried`) follows its target until it has seen it in the same place on two frames, then stops following (`locked`). A person aims where they saw the target, too. With nothing held, a glide still follows its target, as a place easing open needs. The `drop:right` drag (op 13–15) now lights centre, then right, and holds.

## 2. "The tape adjusts the layout and snaps it back"

**Cause.** `tour.zig` cut to a fresh keyframe at **Places** (`.reset`: Main's split undone, explorer and panel opened), at **Floats** (`.reset`: the sidebar split undone) and at **History** (`.focused`: explorer and panel shut). That is three snaps. Those keyframes existed because a snapshot does not hold the layout: a seek back across a split had to land on a cut.

**Fix.**
- `tour.zig` has one keyframe. Each chapter carries on in the window the last one left: Places opens the explorer from the rail, the Floats chapter floats one half of the split sidebar, and History clicks README's tab with the store left open beside it. Headless layout timeline: `Pane 0` → `+Pane 1` (Tabs) → `+Sidebar` (Places) → `+Sidebar/b1` → `+Float 1` (Floats) → `−Float 1` (Materials, by its close button). Nothing reverts.
- `Demo.layoutPrint`: the app's places (name, shut or not, what each shows), how many panes the documents have, and how many floats. A snapshot keeps it, and `restore` refuses to put a snapshot back into another arrangement. A seek back across a split replays from the keyframe instead of landing on a window arranged the later way. On main, a restore across a float or split put the documents back and left the window as it was, which nothing checked.

## What replaying through the whole tour turned up (each fixed here)

With no mid-tour keyframes, every seek replays the tour's drags. Four replay bugs the cuts had been hiding:
- **A drop's front document settled a frame late** (`Workspace.setTabs`). `active` was set only when the pane drew, so a pane a drop had just opened had no active document. Live play had drawn it by the next calm moment, but a seek arrived first, and the replay's fingerprint differed. `setTabs` now sets `active` along with the selection.
- **A replayed glide aimed at a place still moving** (`Sequencer.Sink.VTable.still`, `Player.targetStill`). A replay covers a glide in one call and finds its target once. While seeking, a glide onto a tagged target is held until the target stops moving: two runs at the same point, at most `Settling.max_runs`, with the backend clock moved a frame per run so animations advance.
- **A replayed drag lost most of its carry** (`Sequencer.replaying`). A float moved by its header follows the frames it is drawn in. With a whole glide in one frame it moved 2 px rather than across the window. While replaying, a carried `.move` takes a frame per 16 ms path step. Traces (strokes) don't, and live play is unchanged.
- **A forward seek from mid-drag carried on after letting go** (`Player.letGo`, `goBack`). Letting go now marks the app diverged, so if no restore can then be made the seek replays from the keyframe.

## Tests

- `tests/demos.zig` `Steady`, run on every bundled demo's straight play:
  - no chapter opens on another `layoutPrint` than the last one left;
  - a carried glide's pointer never moves further from where it lands (it fails on main's tab drag);
  - a drop's lit bubble, once left, is not lit again within the drag.
- Seek moments are no longer taken mid-drag (button held), the same rule the player's snapshots follow.
- `Sequencer` unit tests: a carried glide goes where its target settled and a free one follows it; a carried glide replays a frame per step.

## Not done / follow-ups

- A float dragged by its header still lands somewhere else after a seek than in straight play. Even a frame per step moves it less than live does. `layoutPrint` counts floats but has no float positions, so a restore puts a float back without moving it. That is a fidelity gap (fingerprints do not cover it) that main had too.
- The glass aim offset (`dropCenter`, 0.55 R down-right of the pointer) applies only where `LiquidField.ready()`. Headless tests can't exercise it. The fix doesn't depend on it.
- Seeks replay more than before: from the keyframe across splits, plus a frame per drag step. No seek benchmark was run.

Coordination: #665 (tape-own-stage) has merged. This PR is rebased on it with no conflicts. These bugs are separate from #665.

## SDK impact

- [x] None (fingerprint unchanged; `test-sdk-version` passes)

## Verified

macOS, headless: `scripts/check-fmt.sh`, `zig build`, `zig build test`, `zig build test-integration` (all steps), `zig build check-web`, `zig build test-sdk-version`, `zig build check-tests-reached`. No fizzy launched. The flicker and the snaps were measured with a temporary 16 ms-frame probe of the tour: the pointer x per frame and the region list per chapter, quoted above.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
foxnne added a commit that referenced this pull request Oct 19, 2026
Two bugs the maintainer saw in the tour (the "A Tour of Fizzy" tape on the web build), and what replaying the tour without its mid-tour cuts turned up.

## 1. "When dragging the document tabs, the drop zone flickers rapidly"

**Cause.** A glide finds its target again every frame (`Sequencer.advanceSpans` → `locate`). While a button is held, the thing under the pointer moves *because* of what is carried. The tour's first drag carries README onto `src/main.zig`'s tab, and the strip makes room for it, so `main.zig`'s tab moves away. The glide then chased it, and the room closed again. Measured headless at 16 ms frames: for the last ~300 ms of that drag the pointer jumped between x≈156 and x≈402 every other frame, so the strip's insertion gap flickered. Frame pacing doesn't matter: it happens at any frame rate, the web's included.

**Fix (`sdk/tape/Sequencer.zig`).** A glide that begins with a button held (`Glide.carried`) follows its target until it has seen it in the same place on two frames, then stops following (`locked`). A person aims where they saw the target, too. With nothing held, a glide still follows its target, as a place easing open needs. The `drop:right` drag (op 13–15) now lights centre, then right, and holds.

## 2. "The tape adjusts the layout and snaps it back"

**Cause.** `tour.zig` cut to a fresh keyframe at **Places** (`.reset`: Main's split undone, explorer and panel opened), at **Floats** (`.reset`: the sidebar split undone) and at **History** (`.focused`: explorer and panel shut). That is three snaps. Those keyframes existed because a snapshot does not hold the layout: a seek back across a split had to land on a cut.

**Fix.**
- `tour.zig` has one keyframe. Each chapter carries on in the window the last one left: Places opens the explorer from the rail, the Floats chapter floats one half of the split sidebar, and History clicks README's tab with the store left open beside it. Headless layout timeline: `Pane 0` → `+Pane 1` (Tabs) → `+Sidebar` (Places) → `+Sidebar/b1` → `+Float 1` (Floats) → `−Float 1` (Materials, by its close button). Nothing reverts.
- `Demo.layoutPrint`: the app's places (name, shut or not, what each shows), how many panes the documents have, and how many floats. A snapshot keeps it, and `restore` refuses to put a snapshot back into another arrangement. A seek back across a split replays from the keyframe instead of landing on a window arranged the later way. On main, a restore across a float or split put the documents back and left the window as it was, which nothing checked.

## What replaying through the whole tour turned up (each fixed here)

With no mid-tour keyframes, every seek replays the tour's drags. Four replay bugs the cuts had been hiding:
- **A drop's front document settled a frame late** (`Workspace.setTabs`). `active` was set only when the pane drew, so a pane a drop had just opened had no active document. Live play had drawn it by the next calm moment, but a seek arrived first, and the replay's fingerprint differed. `setTabs` now sets `active` along with the selection.
- **A replayed glide aimed at a place still moving** (`Sequencer.Sink.VTable.still`, `Player.targetStill`). A replay covers a glide in one call and finds its target once. While seeking, a glide onto a tagged target is held until the target stops moving: two runs at the same point, at most `Settling.max_runs`, with the backend clock moved a frame per run so animations advance.
- **A replayed drag lost most of its carry** (`Sequencer.replaying`). A float moved by its header follows the frames it is drawn in. With a whole glide in one frame it moved 2 px rather than across the window. While replaying, a carried `.move` takes a frame per 16 ms path step. Traces (strokes) don't, and live play is unchanged.
- **A forward seek from mid-drag carried on after letting go** (`Player.letGo`, `goBack`). Letting go now marks the app diverged, so if no restore can then be made the seek replays from the keyframe.

## Tests

- `tests/demos.zig` `Steady`, run on every bundled demo's straight play:
  - no chapter opens on another `layoutPrint` than the last one left;
  - a carried glide's pointer never moves further from where it lands (it fails on main's tab drag);
  - a drop's lit bubble, once left, is not lit again within the drag.
- Seek moments are no longer taken mid-drag (button held), the same rule the player's snapshots follow.
- `Sequencer` unit tests: a carried glide goes where its target settled and a free one follows it; a carried glide replays a frame per step.

## Seek cost, measured

Headless, ReleaseFast, dvui's testing backend. Each row is a seek from the tour's end back to 200 ms into the chapter: the app runs it took (displayed + silent) and the CPU time to land.

| chapter | main: runs / ms | this PR: runs / ms |
|---|---|---|
| Welcome | 4 / 2.1 | 4 / 2.1 |
| Tabs | 4 / 1.9 | 6 / 2.2 |
| Places | 4 / 2.1 | 217 / 23.9 |
| Floats | 4 / 2.1 | 241 / 29.4 |
| Materials | 4 / 0.7 | 376 / 59.5 |
| Plugins | 9 / 1.5 | 387 / 64.5 |
| History | 4 / 2.0 | 4 / 0.7 |
| Your turn | 9 / 1.1 | 9 / 1.7 |

A seek back into Places through Plugins is now a replay from the start. On main it was a cut to that chapter's own keyframe. The replay plays the tour's drags a frame per step and holds each glide until its target is still. Against the player's 8 ms budget that is up to about 8 displayed frames on this machine (about 65 ms of work), where main took one. It is correct, but slower. The way back to one-frame seeks is to put the layout itself (assignments, splits, floats) into the snapshot, so a restore can put it back rather than refuse. That is a follow-up:  is the seam, and  is what it would replace.

## Review (code-review at medium, two passes on this PR)

Fixed:
-  could hold  forever. It is replaced by the root fix:  sets , null for a surface that is not a document.
-  saw no motion between runs on a microsecond clock. A held run now moves the backend clock 16 ms.
- The hold is skipped for untagged targets and for , so a drawing's strokes cost no extra runs.
-  is now cleared on rewind and restore.
- A duplicated comment is gone.
- Steady's flicker check gave a false positive when several bubbles were lit at once. It now fires only on lit, then off, then lit again.
-  marked the app diverged, which turned a later forward scrub into a seek back. It is now a per-seek .
- The clock during a step-per-frame replayed drag now advances a step per run ().

Declined, or left as follow-ups:
- Snapshots holding the layout (above).
- A carried glide can lock on a target before its easing has begun.
- Steady checks only the boundary frame of a chapter.
- A  drag that legitimately returns to a bubble would trip the flicker check (no bundled demo does that).

## Not done / follow-ups

- A float dragged by its header still lands somewhere else after a seek than in straight play. Even a frame per step moves it less than live does. `layoutPrint` counts floats but has no float positions, so a restore puts a float back without moving it. That is a fidelity gap (fingerprints do not cover it) that main had too.
- The glass aim offset (`dropCenter`, 0.55 R down-right of the pointer) applies only where `LiquidField.ready()`. Headless tests can't exercise it. The fix doesn't depend on it.

Coordination: #665 (tape-own-stage) has merged. This PR is rebased on it with no conflicts. These bugs are separate from #665.

## SDK impact

- [x] None (fingerprint unchanged; `test-sdk-version` passes)

## Verified

macOS, headless: `scripts/check-fmt.sh`, `zig build`, `zig build test`, `zig build test-integration` (all steps), `zig build check-web`, `zig build test-sdk-version`, `zig build check-tests-reached`. No fizzy launched. The flicker and the snaps were measured with a temporary 16 ms-frame probe of the tour: the pointer x per frame and the region list per chapter, quoted above.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
foxnne added a commit that referenced this pull request Oct 19, 2026
Two bugs the maintainer saw in the tour (the "A Tour of Fizzy" tape on the web build), and what replaying the tour without its mid-tour cuts turned up.

## 1. "When dragging the document tabs, the drop zone flickers rapidly"

**Cause.** A glide finds its target again every frame (`Sequencer.advanceSpans` → `locate`). While a button is held, the thing under the pointer moves *because* of what is carried. The tour's first drag carries README onto `src/main.zig`'s tab, and the strip makes room for it, so `main.zig`'s tab moves away. The glide then chased it, and the room closed again. Measured headless at 16 ms frames: for the last ~300 ms of that drag the pointer jumped between x≈156 and x≈402 every other frame, so the strip's insertion gap flickered. Frame pacing doesn't matter: it happens at any frame rate, the web's included.

**Fix (`sdk/tape/Sequencer.zig`).** A glide that begins with a button held (`Glide.carried`) follows its target until it has seen it in the same place on two frames, then stops following (`locked`). A person aims where they saw the target, too. With nothing held, a glide still follows its target, as a place easing open needs. The `drop:right` drag (op 13–15) now lights centre, then right, and holds.

## 2. "The tape adjusts the layout and snaps it back"

**Cause.** `tour.zig` cut to a fresh keyframe at **Places** (`.reset`: Main's split undone, explorer and panel opened), at **Floats** (`.reset`: the sidebar split undone) and at **History** (`.focused`: explorer and panel shut). That is three snaps. Those keyframes existed because a snapshot does not hold the layout: a seek back across a split had to land on a cut.

**Fix.**
- `tour.zig` has one keyframe. Each chapter carries on in the window the last one left: Places opens the explorer from the rail, the Floats chapter floats one half of the split sidebar, and History clicks README's tab with the store left open beside it. Headless layout timeline: `Pane 0` → `+Pane 1` (Tabs) → `+Sidebar` (Places) → `+Sidebar/b1` → `+Float 1` (Floats) → `−Float 1` (Materials, by its close button). Nothing reverts.
- `Demo.layoutPrint`: the app's places (name, shut or not, what each shows), how many panes the documents have, and how many floats. A snapshot keeps it, and `restore` refuses to put a snapshot back into another arrangement. A seek back across a split replays from the keyframe instead of landing on a window arranged the later way. On main, a restore across a float or split put the documents back and left the window as it was, which nothing checked.

## What replaying through the whole tour turned up (each fixed here)

With no mid-tour keyframes, every seek replays the tour's drags. Four replay bugs the cuts had been hiding:
- **A drop's front document settled a frame late** (`Workspace.setTabs`). `active` was set only when the pane drew, so a pane a drop had just opened had no active document. Live play had drawn it by the next calm moment, but a seek arrived first, and the replay's fingerprint differed. `setTabs` now sets `active` along with the selection.
- **A replayed glide aimed at a place still moving** (`Sequencer.Sink.VTable.still`, `Player.targetStill`). A replay covers a glide in one call and finds its target once. While seeking, a glide onto a tagged target is held until the target stops moving: two runs at the same point, at most `Settling.max_runs`, with the backend clock moved a frame per run so animations advance.
- **A replayed drag lost most of its carry** (`Sequencer.replaying`). A float moved by its header follows the frames it is drawn in. With a whole glide in one frame it moved 2 px rather than across the window. While replaying, a carried `.move` takes a frame per 16 ms path step. Traces (strokes) don't, and live play is unchanged.
- **A forward seek from mid-drag carried on after letting go** (`Player.letGo`, `goBack`). Letting go now marks the app diverged, so if no restore can then be made the seek replays from the keyframe.

## Tests

- `tests/demos.zig` `Steady`, run on every bundled demo's straight play:
  - no chapter opens on another `layoutPrint` than the last one left;
  - a carried glide's pointer never moves further from where it lands (it fails on main's tab drag);
  - a drop's lit bubble, once left, is not lit again within the drag.
- Seek moments are no longer taken mid-drag (button held), the same rule the player's snapshots follow.
- `Sequencer` unit tests: a carried glide goes where its target settled and a free one follows it; a carried glide replays a frame per step.

## Seek cost, measured

Headless, ReleaseFast, dvui's testing backend. Each row is a seek from the tour's end back to 200 ms into the chapter: the app runs it took (displayed + silent) and the CPU time to land.

| chapter | main: runs / ms | this PR: runs / ms |
|---|---|---|
| Welcome | 4 / 2.1 | 4 / 2.1 |
| Tabs | 4 / 1.9 | 6 / 2.2 |
| Places | 4 / 2.1 | 217 / 23.9 |
| Floats | 4 / 2.1 | 241 / 29.4 |
| Materials | 4 / 0.7 | 376 / 59.5 |
| Plugins | 9 / 1.5 | 387 / 64.5 |
| History | 4 / 2.0 | 4 / 0.7 |
| Your turn | 9 / 1.1 | 9 / 1.7 |

A seek back into Places through Plugins is now a replay from the start. On main it was a cut to that chapter's own keyframe. The replay plays the tour's drags a frame per step and holds each glide until its target is still. Against the player's 8 ms budget that is up to about 8 displayed frames on this machine (about 65 ms of work), where main took one. It is correct, but slower. The way back to one-frame seeks is to put the layout itself (assignments, splits, floats) into the snapshot, so a restore can put it back rather than refuse. That is a follow-up: `Demo.Snapshot` is the seam, and `layoutPrint` is what it would replace.

## Review (code-review at medium, two passes on this PR)

Fixed:
- `paneUnsettled` could hold `idle` forever. It is replaced by the root fix: `Workspace.setTabs` sets `active`, null for a surface that is not a document.
- `targetStill` saw no motion between runs on a microsecond clock. A held run now moves the backend clock 16 ms.
- The hold is skipped for untagged targets and for `.trace`, so a drawing's strokes cost no extra runs.
- `aiming` is now cleared on rewind and restore.
- A duplicated comment is gone.
- Steady's flicker check gave a false positive when several bubbles were lit at once. It now fires only on lit, then off, then lit again.
- `letGo` marked the app diverged, which turned a later forward scrub into a seek back. It is now a per-seek `let_go`.
- The clock during a step-per-frame replayed drag now advances a step per run (`Sequencer.nextAt`).

Declined, or left as follow-ups:
- Snapshots holding the layout (above).
- A carried glide can lock on a target before its easing has begun.
- Steady checks only the boundary frame of a chapter.
- A `.via` drag that legitimately returns to a bubble would trip the flicker check (no bundled demo does that).

## Not done / follow-ups

- A float dragged by its header still lands somewhere else after a seek than in straight play. Even a frame per step moves it less than live does. `layoutPrint` counts floats but has no float positions, so a restore puts a float back without moving it. That is a fidelity gap (fingerprints do not cover it) that main had too.
- The glass aim offset (`dropCenter`, 0.55 R down-right of the pointer) applies only where `LiquidField.ready()`. Headless tests can't exercise it. The fix doesn't depend on it.

Coordination: #665 (tape-own-stage) has merged. This PR is rebased on it with no conflicts. These bugs are separate from #665.

## SDK impact

- [x] None (fingerprint unchanged; `test-sdk-version` passes)

## Verified

macOS, headless: `scripts/check-fmt.sh`, `zig build`, `zig build test`, `zig build test-integration` (all steps), `zig build check-web`, `zig build test-sdk-version`, `zig build check-tests-reached`. No fizzy launched. The flicker and the snaps were measured with a temporary 16 ms-frame probe of the tour: the pointer x per frame and the region list per chapter, quoted above.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant