Repository navigation
sdk: a document's bytes for a checkpoint, without a save's bookkeeping - #606
Merged
Merged
Conversation
The crash checkpoint (`Checkpoint`) captures every dirty document every few seconds of typing, and for its unsaved bytes asks the owner for the bytes a save would write (`documentBytes`). The text plugin's `savedBytes` closed the undo group there, as if it were the save, so a run of typing was cut into a new undo step each time a checkpoint landed in it. It is also what made the tour test's step check flaky: on a slow runner a checkpoint fell between the two keystrokes it types, and the second came as `.added`, not `.joined`. The save's undo stop now comes when its write lands (`written`), which only a real save reports, and only while the written state is still the top step. The test takes a checkpoint between its keystrokes itself, so it holds whatever a frame takes, and checks that a save still stops the run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The host copied a document for things that are not saves (the crash checkpoint, a restart or plugin reload, a tape's file, a fizzy.document.changed listener) through the save's own snapshot, so each copy did what a save's start does. A new optional Plugin hook, documentBytes, gives the bytes a save would write with none of that; Plugin.documentBytes uses it and falls back to the snapshot pair. Saves keep the snapshot: DocumentIo, and the web download through the new Plugin.saveBytes. The text and archive plugins implement it. In text, a checkpoint no longer runs format on save, and a checkpoint during a save's write no longer moves the edit that write cleans up to. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
foxnne
marked this pull request as ready for review
October 19, 2026 03:23
foxnne
enabled auto-merge (squash)
October 19, 2026 03:30
foxnne
added a commit
that referenced
this pull request
Oct 19, 2026
## What changes `sdk_version` goes from 0.2.31 to 0.2.32. Merging this tags `sdk-v0.2.32` and publishes its tarball. `sdk-repin` then asks each store plugin to repin. **Why now:** the SDK's shape has moved three times since 0.2.31, so store plugins built for 0.2.31 don't load in a fizzy built from `main`. Pixi's history trim (reading fizzy's history limit) needs #620's call. ## SDK impact - [ ] None - [ ] 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` - [x] SDK release: bumps `sdk_version`, lists the `sdk` PRs since the last `sdk-v*` tag **`sdk` PRs since `sdk-v0.2.31`:** - **#608: plugins ship demos.** - `host.registerDemo(.{ .name, .title, .tape })` in `register`; the host copies them and drops them on unregister. - The `demos` service (`sdk.services.demos`, version 1) lists every demo fizzy can play and plays one by name. - `sdk.Demo` is new. Nothing existing changed shape. - **#606: a document's bytes without a save.** - New optional `Plugin.VTable.documentBytes(state, doc, path, allocator)`: the bytes a save would write, with none of a save's bookkeeping (no format on save, no undo stop). Fizzy uses them for crash checkpoints, a restart's carried work, a tape's file and `fizzy.document.changed` listeners. - An owner without it falls back to `documentSnapshot` + `encodeSnapshot`, as before. - **#620: the history limit is fizzy's.** `host.historyLimit()` (a new `EditorAPI` member) is how many steps a document's undo history keeps: fizzy's "Undo steps per document" setting, 0 for no limit. Each plugin that owns a history trims its own. **Also reaching plugins at this release** (no boundary change): - **#616:** `Host.Activity` gains an optional `agent`, the author index of an agent's edit. - **#589:** the dvui pin moves to the zig-0.17 branch's floating-window background hook (`docs/DEPENDENCIES.md`, patch 14). - **#614:** glass panels in `core/dialogs.zig` get a drop's rim. (`app/` changes since 0.2.31 are not compiled into plugins.) ## Verified - [x] macOS: `scripts/check-fmt.sh` and `zig build test-sdk-version` pass. The fingerprint is main's recorded value (`0x3d7fca8d3d211e73`); only `sdk_version` changed. - [x] Store plugins built against this `sdk/` (`--fork`, Zig 0.17, macOS, sandbox profile): pixi, zig, atlas, drive and ghostty, each from its `main`, all build. - [ ] Linux: CI ## Follow-ups - Repin and release the store plugins (pixi, zig, ghostty, drive, atlas; then agent and chat, by hand) when `sdk-repin` opens their PRs. - Make 0.2.32 live on fizzyed.it/app once the repinned plugins are in the store: `gh workflow run web.yml --ref main -f go_live=0.2.32`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #601 and based on its branch, so the diff is this change alone; GitHub retargets it to
mainwhen #601 merges. It takes up the follow-ups that PR lists.What changes
For plugin authors: a new optional
Plugin.VTable.documentBytes(state, doc, path, allocator) → []u8. It returns the bytes a save topathwould write, for a copy that is not a save, with none of a save's bookkeeping or side effects: no format on save, no edit remembered fordocumentWritten, no undo stop. fizzy takes these bytes:KeptDocuments.capturefromCheckpoint, every few seconds of typing)Tracks.fileAt)fizzy.document.changedlistener that asks for the unsaved textAn owner without the hook keeps working as before:
Plugin.documentBytesfalls back todocumentSnapshot+encodeSnapshot, bookkeeping included.Saves keep the snapshot:
DocumentIo.save, and the web download (Editor.downloadDocument), which now calls the newPlugin.saveBytes(both halves of a save at once, followed bydocumentWritten).Why:
documentSnapshotis "the storage-agnostic half of saving". It runs at a save's start, so it formats on save and records which edit the bytes stand for. Every non-save copy did the same. In the text plugin that meant:format_on_saveon (off by default), each checkpoint ran the formatter on a dirty document while the person was typing in it.serialized_op_id. If an edit came in between, the write landed anddocumentWrittenmarked the document clean at an edit newer than the file on disk.Implemented in:
serialized_generation, so it gets the hook too (doc.pack, with no generation recorded).Document.savedBytes's comment now says only a save takes it.docs/PLUGINS.mdnames the hook, and its storage-agnostic section now names the snapshot pair, which is what a save uses.Tests (
tests/integration.zig, the tour test's "A document's steps" block, after #601's save check):KeptDocuments.capturekeeps the text as typed and leaves the document unformatted. A real save (saveBytes) still formats, which shows the formatter was live.documentWritten). The document is still dirty.Each check fails without the text plugin's hook: the first with
formattedwhereas typedwas expected, the second with the document clean.SDK impact
sdk/src/version.zig,sdk_versionuntouched, PR labelledsdksdk_version, lists thesdkPRs since the lastsdk-v*tagA new optional vtable field, null by default. An existing plugin builds unchanged and gets the fallback, which behaves as before.
pixi (
fizzyedit/pixi) implementsdocumentSnapshotand has both problems in its own form:File.saveSnapshotsetseditor.saving_bookmark = history.bookmark, so a checkpoint during a save moves the bookmark thatwrittencleans to.finishReveal(), so a checkpoint ends a reveal in progress.pixi would need a
documentBytesthat builds the sameSnapshotcontent without those two lines, encodes it in place (File.encode), and destroys it. The.pixizip encode then runs on the UI thread at each checkpoint, as it already does through the fallback today.Verified
zig build test-integration(80/80 steps; the two new checks each fail with the text hook removed),zig build,zig build test(113/113),check-web(42/42),test-sdk-version(35/35).scripts/check-fmt.shflags onlybackend/src/platform/win32_titlebar.zig, which this branch's base (text: a crash checkpoint no longer splits a run of typing #601's) predatesmain's fix for (ci: main's checks pass again (a formatted file, the examples' branch) #597); untouched here.check-webbuilds; the download path was not runFollow-ups
documentBytesis written and builds against this branch's SDK, with pixi'szig build testpassing. It's a local change for the pixi repo, pushed at the repin that brings this SDK, since SDK 0.2.30 has no such field. It splitssaveSnapshotinto the save's bookkeeping around atakeSnapshotthat changes nothing. A drawing still being revealed is completed in the copy, ascaptureStatedoes; a flat image is the one case that still finishes the reveal, because its composite is read back from the GPU.DocumentIo's worker.🤖 Generated with Claude Code