Repository navigation
sdk: release 0.2.30 - #580
Merged
Merged
Conversation
foxnne
enabled auto-merge (squash)
October 18, 2026 19:48
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>
This was referenced Oct 18, 2026
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>
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.
What changes
sdk_versiongoes from 0.2.29 to 0.2.30. Merging this tagssdk-v0.2.30and publishes its tarball.Why now: this is the first SDK on Zig 0.17 (#575,
plans/ZIG_0_17_PLAN.mdstep 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 frommain, so the store plugins can't trail by days. Their 0.17 PRs are ready and only need this tarball to repin.SDK impact
sdk/src/version.zig,sdk_versionuntouched, PR labelledsdksdk_version, lists thesdkPRs since the lastsdk-v*tagsdkPRs sincesdk-v0.2.29:minimum_zig_version = "0.17.0"and Zig 0.17 to build; there's no overlap window with 0.16. Also new:fizzy.plugin.installBeside, for a helper program a plugin ships.dvui.pointerKind()andcursorSetPriority; tabs show the hand, and a tapped item is left unlit.The fingerprint is
0x5f9c818a6972efd4.Verified
zig build test-sdk-versionpasses, andscripts/pack-sdk.shpacksfizzy-sdk-v0.2.30.tar.gz.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.Follow-ups
sdk: repin to fizzy SDK 0.2.30PRs. The store plugins' own 0.17 PRs carry the port and only need the pin moved:I'll repin those branches to
sdk-v0.2.30and close the bot's duplicates.sdk-v0.2.30and fizzy's switch commit. Thenscripts/check-examples.shpoints back atmain.zig-versionis still 0.16.0; every plugin passes its own.🤖 Generated with Claude Code