Skip to content

app: a demo gives back a folder opened from a zip's bytes - #596

Merged
foxnne merged 2 commits into
mainfrom
app/demo-keeps-bytes
Oct 19, 2026
Merged

foxnne merged 2 commits into
mainfrom
app/demo-keeps-bytes

Conversation

@foxnne

@foxnne foxnne commented Oct 18, 2026 •

Copy link
Copy Markdown
Collaborator

Part of fizzyedit/plans#30 (step 0: check that a guide played over the site gives the site back).

What changes

A demo sets the person's session aside and reopens it by path when it ends (Demo.begin / end). A document whose path can't open it again was lost:

  • a file the web page fetched or the person uploaded
  • a zip opened as the project folder, whose tab is its mount

The second case is fizzyed.it's ?open=/site.zip. The keyframe closes the zip's tab, which unmounts it, and after the tour the site was gone.

Now begin keeps the bytes of each document whose path can't reopen it, using Plugin.documentBytes, the same encode a save does. "Can't reopen" means the path is neither on a mount nor on disk, and on the web, not on a mount. end opens those documents from their bytes first, so a zip mounts again under the same prefix before the project folder and the files inside it are opened.

The document that was active is active again, whichever way it came back. A never-saved document (untitled-1, clean, since a demo won't start over unsaved work) is left out, as before. Reopened from its bytes, it would come back as a file named untitled-1.

Tried and dropped: keeping the person's documents open through the demo, with the demo closing only its own. The workbench's panes would still hold the person's tabs during the demo, and taking those out is workspaces work (plans/WORKSPACES_PLAN.md).

SDK impact

  • None

Verified

  • macOS: the bundled-demos test (tests/demos.zig) now opens a zip from its bytes before playing every demo, then checks the zip is mounted and is the project folder again, with the same number of documents open and site.zip still the active one. It failed before this change: the project folder was not the zip's. The test build now bundles the archive plugin for it. zig build test-integration 81/81 steps, zig build, zig build test, zig build check-web, zig build check-tests-reached.
  • Windows:
  • Linux:
  • Web: same code path (reopenable is false for anything off a mount); not run in a browser.

scripts/check-fmt.sh flags backend/src/platform/win32_titlebar.zig, which has been unformatted on main since #463 and isn't in this diff; #597 formats it.

Follow-ups

  • Steps 1–4 of fizzyedit/plans#30: the demo registry, the guides plugin, hosted guides, ?view=.

🤖 Generated with Claude Code

A demo sets the person's session aside and reopens it when it ends, by path. A document whose
path cannot open it again was lost: a file the web page fetched or the person uploaded, and a
zip opened as the project folder, whose tab is its mount. That is fizzyed.it's `?open=/site.zip`:
after the tour, the site was gone. Now `begin` keeps the bytes of each document its path cannot
reopen (`Plugin.documentBytes`), and `end` opens those first, so a zip mounts again before the
folder and the files inside it are opened.

The bundled-demos test opens a zip from its bytes first and checks it is mounted and the
project folder again once every demo has played; the test build bundles the archive plugin for
it.

Part of fizzyedit/plans#30 (step 0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@foxnne
foxnne force-pushed the app/demo-keeps-bytes branch from a978337 to 75ed63e Compare October 18, 2026 23:39
@foxnne
foxnne marked this pull request as ready for review October 18, 2026 23:39
@foxnne
foxnne enabled auto-merge (squash) October 19, 2026 01:07
@foxnne
foxnne merged commit 4438cea into main Oct 19, 2026
11 checks passed
@foxnne
foxnne deleted the app/demo-keeps-bytes branch October 19, 2026 01:32
foxnne added a commit that referenced this pull request Oct 19, 2026
…em (#608)

Part of fizzyedit/plans#30 (step 1: a demo registry).

## What changes

A plugin can ship demos, and anything can list and play the demos the
app has. This is the seam the `guides` plugin (step 2) is built on, and
that pixi's own guides come in through later.

- **`host.registerDemo(.{ .name, .title, .tape })`**, called in
`register`. The tape is ZON or binary bytes built with the SDK's `tape`
module. The host copies the name, title and tape, so nothing points into
the plugin's image, and drops them when the plugin unregisters. A name
already registered is refused (`error.DemoExists`).
`Host.demos_generation` moves whenever the list changes.
- **The `demos` service (`sdk.services.demos`)**, provided by fizzy in
`src/editor/Demos.zig`:
- `list` returns every demo fizzy can play, its own first, each with its
length, chapters and origin (`bundled`, `plugin`, or `hosted` for step
3).
- `play(name, at_ms)` plays one, from a chapter's start if asked. It
returns `unknown`, `busy` (a live tape is driving) or `unsaved` (the
same refusals a demo already had, now with a reason).
- `current` names the demo playing. A tape loaded some other way since
isn't mistaken for it.
  - `generation` is cheap to ask every frame.
- **The list is built lazily**, and again only when the host's demos
change. A plugin's tape is loaded the way a demo is played. One that
wouldn't play is left out with a warning instead of being listed and
then refused: a live tape, a step no loaded plugin applies, or a name
fizzy already has.
- **`fizzy.demo.play` ("Demo: Play by Name…")** plays any listed demo by
`name`, from `at`. The bundled demos keep their own palette rows. A
plugin's demo gets none, because a `Command` carries only its owner's
state, so one row per runtime demo would need a context it doesn't have.
The guides view (step 2) is where those are browsed.
- `Demo.startable` says why a demo can't start, and `Demo.loadDemoTape`
is the demo check `playTape` already used.

Changed from the plan: the plan had the `automation` service growing
`demos()` and `playDemo()`. `automation` is about live tapes on whatever
is open. A demo is fizzy's player setting the session aside, so it got a
service of its own, and `automation`'s version is untouched.

## SDK impact

- [ ] None
- [ ] Core-only or additive: reaches plugins at the next SDK release
- [x] 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

For plugin authors: `sdk.Demo` and `Host.registerDemo` are new;
`sdk.services.demos` is a new service at version 1. Nothing existing
changed shape.

## Verified

- [x] macOS:
- `zig build test`: a new `Host` test checks that `registerDemo` keeps
its own copy (the caller's buffers are overwritten after it returns),
refuses a name twice, and drops a plugin's demos at `unregisterPlugin`,
with the generation moving each time.
- `zig build test-integration` (81/81 steps, after merging `main` with
#596 in): the demo test lists the bundled demos through the service,
then registers three demos. A plugin demo is listed with two chapters;
one reusing the name `tour` and a live tape are left out, with exactly
those two warnings. The plugin demo plays by name to its end, named by
`current`. Started at its second chapter, it's at that chapter. A tape
loaded another way isn't `current`, and nothing is once unloaded.
- `zig build`, `zig build check-web`, `zig build test-sdk-version`, `zig
build check-tests-reached`, `scripts/check-fmt.sh`.
- [ ] Windows:
- [ ] Linux:
- [ ] Web: compiles (`check-web`); not run in a browser.

## Follow-ups

- Step 2, the `guides` plugin, lists the service and plays what's
clicked.
- Step 3: `hosted` returns `unknown` until the web's `demos/index.json`
is read into the list.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
foxnne added a commit that referenced this pull request Oct 19, 2026
Part of fizzyedit/plans#30. The new tour drops a tab on the editor's
edge, and found this.

## What changes

A demo sets the person's session aside (folder, documents, settings) and
gives it back at the end, but it never set aside the document panes
those documents sat in. A demo that split the editor, for example by
dropping a tab on its edge, did three things wrong:

- it left its pane behind after it ended;
- it wrote its arrangement over the person's `panes.zon` while it
played, because the workbench saves whenever the tree changes;
- a seek back to a keyframe replayed onto the panes a later moment had
made, so play and replay disagreed.

Now:

- **`Workbench.holdPanes`** (at `Demo.begin`) keeps the person's
arrangement in memory and the pane they were in. While it's held,
nothing is saved.
- **`Workbench.onePane`** (at every keyframe) cuts to one pane. The
others leave with their leaves (`rebuildWorkspaces`). A keyframe
declares the window outright, and now it does the same for panes.
- **`Workbench.restorePanes`** (at `Demo.end`, before the person's
documents reopen) puts their arrangement back, and saving with it.

The hold is a copy of its own, made through `snapshot` and then
`fromSnapshot`. A snapshot borrows the pane names from the live tree
"until it next changes", and the first cut replaces that tree. The first
version held the snapshot itself, and the names it restored were freed
memory: `Failed to rebuild workspaces`, which now says which error
(`NoSuchPane`).

## SDK impact

- [x] None

## Verified

- [x] macOS, `zig build test-integration` (81/81 steps): the demo test
plays a tape that drags a tab onto the editor's right edge. It checks
two panes at the end, one pane after a seek to the keyframe, the
person's own pane count once unloaded, and nothing left held. #596's zip
check still passes after all the demos, and so does the headless tour
test.
- [x] `zig build`, `zig build test`, `zig build check-web`, `zig build
check-tests-reached`, `scripts/check-fmt.sh`.
- [ ] Windows:
- [ ] Linux:
- [ ] Web: the same code path, minus `panes.zon`, which the web never
writes.

## Follow-ups

- Which pane each of the person's documents was in still isn't kept:
they reopen into the pane they were in last, as before this change.
Fixing that belongs with the workspaces plan's "documents through the
app" step (`plans/WORKSPACES_PLAN.md`).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
foxnne added a commit that referenced this pull request Oct 19, 2026
…ry (#629)

Part of fizzyedit/plans#30. **Merge after #621 and #626**: this branch
is built on both and on `main` (which has #623 and #624), and until they
land its diff shows their commits too. Its own change is
`src/editor/demos/tour.zig`, `catalog.zig` and `docs/AUTOMATION.md`.

## What changes

The tour was the explorer, typing, the palette and a markdown preview,
with long holds (8.5 s on the first card). It now covers what a person
needs to get around, and what fizzy's windowing does, in 58 seconds:

| Chapter | At | What it shows |
|---|---|---|
| Welcome | 0:00 | a short title card, no hold |
| Tabs | 0:03 | a tab dragged into order, another dropped on the
editor's edge: a pane beside |
| Places | 0:10 | a place's corner button, its picker, and Split |
| Floats | 0:18 | the Files view dropped on its own place's middle: a
float, carried by its header over the code |
| Materials | 0:25 | Liquid Glass, frost, blur, clear and opaque under
that float (`fizzy.windowMaterial`, #621), then closing the float sends
the view home |
| Plugins | 0:38 | the store: everything seen so far is a plugin |
| History | 0:42 | typing, the grip at the page's foot filling toward
the history's cap (#624), and the bar it opens |
| Your turn | 0:54 | where to find it again: Guides on the rail |

Each chapter that rearranges the window opens on a keyframe. A snapshot
holds documents and settings but not the layout, so a seek or a jump
from the guides view lands on a cut rather than on a window still
arranged the later way. The catalog names the anchors the tour aims at
(`place`, `corner`, `cornerButton`, `historyGrip`, `historyLine`).
`docs/AUTOMATION.md` lists the ones its table lacked: `history.grip:`,
`history.export:`, `history.collapse:`, `float-header:`, `float-close:`
and `picker.empty`.

The history chapter shows the bar and how to scrub it, but doesn't
scrub. A snapshot puts a document's text back but not its history, so a
seek replaying a scrub lands on other text.

What making it found, each its own PR:

- #621: the window's material had no setting.
- #623: a demo left its panes behind, and wrote them over the person's
`panes.zon`.
- #626: the player checked the previous chapter's snapshot on a rewind.
- #624: the grip's fill now measures the history cap, per review.

## SDK impact

- [x] None of its own. #621 and #623 have none either; #626 changes no
shape.

## Verified

- [x] Web, in the browser pane: played start to finish on `?demo=tour`.
Every chapter landed: the drags, picker, float, material switches, store
and history bar.
- [x] macOS, `zig build test-integration` (81/81 steps):
- `tests/demos.zig` plays the tour to its end with no timeouts and seeks
to random moments and every chapter, landing on what playing reached
with no mismatches;
- the headless tour test passes, as do #623's panes check and #596's zip
check after it.
- [x] `zig build`, `zig build check-web`, `zig build
check-tests-reached`, `scripts/check-fmt.sh`.
- [ ] Windows:
- [ ] Linux:

## Follow-ups

- A pause before the tour clicks the history grip broke seeks there (the
line never drew on replay). Something in the grip's rest timing doesn't
follow demo time. The tour clicks it without the pause.
- On the web, the float's view covers most of its glass, so the material
change shows mainly in its header and edges. A sparser view floated
would show it better.
- Hosted guides (step 3) and `?view=guides` (step 4) are next.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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