Skip to content

sdk: release 0.2.30 - #580

Merged
foxnne merged 1 commit into
mainfrom
sdk/release-0.2.30
Oct 18, 2026
Merged

foxnne merged 1 commit into
mainfrom
sdk/release-0.2.30

Conversation

@foxnne

@foxnne foxnne commented Oct 18, 2026

Copy link
Copy Markdown
Collaborator

What changes

sdk_version goes from 0.2.29 to 0.2.30. Merging this tags sdk-v0.2.30 and publishes its tarball.

Why now: this is the first SDK on Zig 0.17 (#575, plans/ZIG_0_17_PLAN.md step 2), and the plan has the release follow the switch at once. A 0.17 fizzy refuses every plugin built against 0.2.29, and fizzyed.it/testapp is built from main, so the store plugins can't trail by days. Their 0.17 PRs are ready and only need this tarball to repin.

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
  • SDK release: bumps sdk_version, lists the sdk PRs since the last sdk-v* tag

sdk PRs since sdk-v0.2.29:

The fingerprint is 0x5f9c818a6972efd4.

Verified

  • macOS (arm64, Zig 0.17.0):
    • On this head, zig build test-sdk-version passes, and scripts/pack-sdk.sh packs fizzy-sdk-v0.2.30.tar.gz.
    • All seven store plugins build against this sdk/ by path with no further changes: pixi, zig, atlas, drive, ghostty, agent and chat (their 0.17 PRs below). All seven loaded together in a 0.17 fizzy earlier on the branch.
  • Windows, Linux: CI.

Follow-ups

🤖 Generated with Claude Code

The first SDK on Zig 0.17 (#575), with #536, #538, #540, #544 and #563.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@foxnne
foxnne enabled auto-merge (squash) October 18, 2026 19:48
@foxnne
foxnne merged commit 214243e into main Oct 18, 2026
11 checks passed
@foxnne
foxnne deleted the sdk/release-0.2.30 branch October 18, 2026 20:00
foxnne added a commit that referenced this pull request Oct 18, 2026
Brings in the SDK 0.2.30 release (#580). The one conflict was the
recorded SDK shape fingerprint, re-recorded for the merged boundary.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
foxnne added a commit that referenced this pull request Oct 18, 2026
Brings in the SDK 0.2.30 release (#580) and the store's PluginManager
seam. The one textual conflict was the recorded SDK shape fingerprint,
re-recorded for the merged boundary. Adapted to main: the store asks the
app through a new optional PluginManager.VTable.refuseCaseClash rather
than reaching App, and App.recordCaseClash reads no build version on the
web, whose loader has none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
foxnne added a commit that referenced this pull request Oct 18, 2026
On Zig 0.17: main (with #575 and the SDK 0.2.30 release, #580) is merged
in, and every gate passes on the 0.17 toolchain.

Part of #530

## What changes

A plugin gets a folder of its own for what it keeps between runs that is
not a setting (recents, an index, a cache):

```zig
pub fn pluginDataFolder(self: *Host, plugin: *const Plugin) ?[]const u8
```

- `<config>/plugin-data/<id>`, the id in lowercase. It follows the
config folder, so a `FIZZY_PROFILE` sandbox gets its own.
- **Plugin ids are unique ignoring ASCII case, enforced before the disk
is touched.**
- `Host.pluginById` stays exact, as do
`isPluginDisabled`/`Undecided`/`AutoUpdate`, because everything keyed by
a loaded id needs exact matching. `registerPlugin` refuses `Graphl`
beside `graphl` (`error.DuplicatePluginId`), logging both names, through
the new `Host.pluginByIdIgnoringCase`.
- `App.pluginCaseClash(id)` checks for a loaded plugin, or a folder
already in `plugins/`, whose id is `id` in another case. It runs before
anything keyed by `id` touches the disk:
- the store's install/update path (`PluginStore.startDownloadUrl`,
before `plugins/<id>` is created or written: on macOS and Windows that
folder *is* the other plugin's), through a new optional
`PluginManager.VTable.refuseCaseClash`, since on main the store talks to
the app only through `PluginManager`;
    - `loadUserPluginById`;
    - the startup scan.
A clash becomes a store failure card through `App.recordCaseClash`: "id
'Graphl' differs only in case from the plugin 'graphl'".
- **Uninstall from a clash card never deletes the loaded plugin's
folder.** Before deleting `plugins/<id>`, `uninstallPlugin` compares it
with the loaded plugin's folder by what each is on disk
(`App.sameFolder`, same inode), not by name. On a case-insensitive
filesystem they are one folder, so only the card goes. On a
case-sensitive one, only that exact-case folder is deleted.
- **The startup scan doesn't let byte order choose.** Two plugin folders
whose names differ only in case (possible on a case-sensitive
filesystem) are one id twice: neither loads, and one card names both and
asks for one to be removed. It's the simplest deterministic rule. Which
build was meant can't be told from the folders alone, and the declared
id would be the same either way once case is folded. A folder whose name
case-clashes with a loaded plugin gets the case-clash card, as before.
- **Thread safety without walking the plugin list, symmetric for any
app.** Two optional `EditorAPI.VTable` hooks, both on the main thread:
- `pluginRegistered(ctx, id)`, fired by `Host.registerPlugin`, where the
app sets up the plugin's entry;
- `pluginUnregistered(ctx, id)`, fired by `Host.unregisterPlugin` before
the plugin's `deinit`. The app marks the entry leaving: it is still
handed out for that `deinit`, and stops at the next frame's top
(`App.releaseRetiredFolders`, which every app built on `App` already
calls). An app without fizzy's `Editor` stays correct.
- `pluginDataFolder` only reads the table: under the lock, then the
folder is made outside it, then `registered` is checked again under the
lock. Entries stay after a plugin goes, so a path a worker holds stays
valid.
- If registration-time setup runs out of memory, the plugin's id is
remembered (`plugin_data_missed`, no allocation) and its entry is made
on its first ask. The out-of-memory is logged.
- One key function (`pluginDataKey`) and one wasm-aware lock pair
(`lockPluginData`/`unlockPluginData`) serve both sides.
- **Ids that aren't plain identifiers:** not all registered ids pass
`isValidPluginId`; the store page registers as `fizzy.store`. So
`registerPlugin` doesn't enforce it. Such a plugin gets null from
`pluginDataFolder`, logged once, and the docs say so.
- **No `mkdir` on every call.** The folder is made on the first ask and
checked again only on an ask a second or more after the last
(`App.plugin_data_recheck_ns`). The contract, documented: a plugin whose
write into a kept path fails with `FileNotFound` asks again, a second
later if it just asked.
- **Failure backoff:** `failed_at` is recorded after `createDirPath`
returns. Asks within 5 s of it return null (`App.plugin_data_retry_ns`),
and each run of failures is logged once. Running out of memory is
logged.
- **`App.deinit` frees what the App owns**: the data-folder table and
the disabled/undecided/auto-update id lists, moved there from
`Editor.deinit`. `Editor.deinit` calls it through a `defer` at its top.
- Not `plugins/<id>`, which an update replaces and an uninstall deletes;
the App, Host and PLUGINS.md docs agree. Always local, whether or not
the open folder is on a mount.
- **Web:** as with `paletteFolder`, the path names no directory. It is
the prefix under which `core.fs.read`/`readZ`/`write`/`remove` keep
files in the page's storage; those four calls do the same on the
desktop, and `std.Io.Dir` calls on the path fail on the web.
- **Uninstall leaves it**, as it leaves the plugin's `settings.zon`
block. The named seam for removing it too is `Editor.uninstallPlugin`
(see Follow-ups).

Graphl asked for it (#530): today it keeps its recents beside
`paletteFolder()`'s parent.

Boundary: `EditorAPI.VTable.pluginDataFolder(ctx, id)` and the optional
`pluginRegistered(ctx, id)`/`pluginUnregistered(ctx, id)`, the
`Host`/`EditorAPI` calls, and `Host.pluginByIdIgnoringCase`. Docs:
`docs/PLUGINS.md` §3.3.

## SDK impact

- [ ] None
- [ ] Core-only or additive: reaches plugins at the next SDK release
- [x] Fingerprint moved: recorded in `sdk/src/version.zig`
(`0xba4664870675e13d` after merging main, which released SDK 0.2.30;
whichever of this and #566 merges second records the combined value),
`sdk_version` untouched, PR labelled `sdk`
- [ ] SDK release: bumps `sdk_version`, lists the `sdk` PRs since the
last `sdk-v*` tag

## Verified

- [x] macOS: `scripts/check-fmt.sh`, `zig build`, `zig build test`, `zig
build test-integration` (54/54 steps, 595/595 tests), `zig build
check-web`, `zig build test-sdk-version`, `zig build
check-tests-reached`, `scripts/check-examples.sh`. A new `sdk` unit test
covers `registerPlugin` refusing the same id and the same id in another
case. The headless whole-editor test (fresh profile) checks:
- `text`'s and `markdown`'s folders are `<profile>/plugin-data/<id>`,
exist, are stable across asks, and differ;
- `../escape` gets null and leaves no table entry, and `Text` finds
`text`'s folder;
- a test-registered `CaseTest` gets `plugin-data/casetest`, and
`casetest` is then refused registration;
- once `CaseTest` is unregistered its folder is still handed out (for
its `deinit`) until the next frame's top, then not;
- `loadUserPluginById("Text")` is refused before opening anything, and
its card shows failed with the case reason;
- uninstalling `Text` works, `text` stays loaded, and a file in
`plugins/text` survives (on macOS `plugins/Text` is that folder);
  - `pluginCaseClash("graphl")` finds a `plugins/Graphl` folder.

Not testable on this case-insensitive Mac: two folders differing only in
case (the startup-scan rule), and deleting the exact-case folder on a
case-sensitive filesystem.
  - `dotted.id` gets null;
- a deleted folder is made again by an ask once its trusted second is
wound past;
- a test-registered `blocked` plugin with a file in its folder's way
gets null, and null again right after the way clears (backoff); with the
backoff wound past, the folder is made and the failure cleared.

The warnings these tests cause are muted with `std.testing.log_level`.
The test's one remaining `failed command:` comes from its own warning
already on `main` (a tape needing `no-such-plugin`).
- [ ] Windows:
- [ ] Linux:
- [ ] Web: builds (`check-web`); not run in a browser

## Follow-ups

- Removing the data folder on uninstall is not built. It would be an
option of `Editor.uninstallPlugin`, probably ask-first in its dialog,
since the folder can hold work the user would miss.
- The other items in #530 (timed recents, a home surface, the
`scrolls_itself` warning) are left for their own PRs. Hiding a surface
is the next PR (`sdk/hide-surface`).

🤖 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