Repository navigation
layout: the picker's buttons are named for tapes and agents - #468
Merged
Merged
Conversation
The picker's Single, Multiple, Split, Clear, Defaults and Remove, and the Split menu's Vertical and Horizontal, now publish `picker.*` anchors. The row's buttons come and go with the place (Remove sits where Split sat on a place with nothing to Clear), so a click aimed by position removed places while a split was being driven; a name cannot land on the wrong action. `core.widgets.menuRow` takes an `anchor` for a row that runs no command. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
foxnne
force-pushed
the
layout/picker-anchors
branch
from
October 16, 2026 21:15
6599555 to
d8dc931
Compare
2 of 5 tasks
foxnne
marked this pull request as ready for review
October 16, 2026 21:15
foxnne
added a commit
that referenced
this pull request
Oct 16, 2026
Stacked on #465 (it reuses that PR's `ColumnCardFrame` test frame); review after it, or retarget to `main` once it merges. This is the follow-up #465 named. ## What changes Take Main | (Main/r1 over Main/r1/b1). Emptying **Main/r1** (carrying its last view to the trash or elsewhere), or picking **Remove** on it, now closes just that half. b1's views move into Main/r1, and the column keeps its name, its width and its link in `layout.zon`. That's the same rule as emptying the half that was split anywhere else (SPLITS.md, "Removing a split"). If b1 is split again too, there's no single place to merge into, and Main/r1 stays, empty. **Before:** Main/r1 is two places under one name: the half Main's split minted, and the half its own split kept. `closeEmptied` saw "minted" and slid shut the branch box registered under that name, which is the whole column. Then `Forest.collapse("Main/r1")` matched the inner *leaf*, so b1 was promoted into Main's split. b1 took the column at an extent that was its height (195 pt in the test). The only link left in `layout.zon` still named the gone `Main/r1`, and b1 had no link, so it was lost on reload. **The model change:** - **`SplitTree.Forest.collapse`** takes the outermost node that carries the name: for a place split again, that's the branch its split made, never the leaf that kept the name. New `isSplit` and `placeLeaves` read the same rule. - **`ViewDrag.closeEmptied`** treats a minted half that was split again as an origin: it merges in its other half, or stays. The **Picker's Remove** goes through it for such a place. - **`Region.persistExtent`** counts a column as kept while *any* place in it holds a view. When the column closes, it forgets every place in it, not only the one whose name the column carries. Before, closing a column with an empty Main/r1 and a full b1 would have left b1's views assigned to a place that no longer existed. It walks the forest only once the place has shut, not every frame. - **The picker** offers no Remove on a half that was split again when its other half is split too: there's nothing to merge, and Remove would only have cleared it. - **A use-after-free while drawing.** When a place was forgotten in the middle of drawing its branch, the tree went on drawing children that `collapse` had just freed. The old code had the same hazard on a rarer path. `persistExtent` now returns whether it dropped the place, and `initTree` and the `.branch` arm draw nothing more of it that frame (a refresh is already queued). The new column-close test caught this: the freed b1 leaf re-stored its extent after the column had cleared it. A leaf that was never split again behaves exactly as before. Every changed path checks `isSplit` first, and `placeLeaves` of a plain leaf is that leaf alone. ## SDK impact - [x] None (`app/` only; `test-sdk-version` passes) ## Verified - [x] macOS: - integration tests (new) on fizzy's forest with a carded Main: - `split: emptying the half that was split again closes only that half`. It failed before the fix: Main/r1 was gone and b1 sat in Main's split. - `split: emptying the half that was split again stays when its other half is split too`. - `split: a column shut by its sash goes only once every place in it is empty`: shut with a view in b1, it stays; emptied, the whole column goes and leaves no assignment or extent behind. - unit tests in `SplitTree.zig`: collapsing a place split again drops its whole branch, and collapsing the minted half of a nested split leaves its origin, with one link. - sandbox fizzy (`--profile`, through the agent plugin, macOS 2×): - Main → Split → Vertical gives 870 \| 870 px. - Main/r1 → Split → Horizontal gives 478 / 478 px, with the column's extent unchanged at 435 pt. - Output into b1, then Main/r1 → Remove gives Main 870 \| Main/r1 870 px. Main/r1 holds Output, and `layout.zon` has one link (`Main/r1` from `Main`, right) and no b1. - Remove on b1 itself still closes just b1, as before. - `scripts/check-fmt.sh`, `zig build`, `zig build test` (91/91 steps), `zig build test-integration` (53/53 steps), `zig build check-tests-reached`, `zig build check-web`, `zig build test-sdk-version`. - [ ] Windows: - [ ] Linux: - [ ] Web: builds ## Follow-ups - **The deeper fix is a name of its own for a branch.** A branch and the half that kept its name share one name, and this PR special-cases that (`isSplit`) where it bites: `closeEmptied`, the picker's Remove and `persistExtent`. Anything else that resolves a place by name (`regionNamed`, which finds the branch box first; `placeSize`) still sees two places. Naming branches separately would need a `layout.zon` migration for saved links, so it's its own change. - Picker buttons named for drivers: #468. 🤖 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 17, 2026
## What changes `sdk_version` goes from 0.2.28 to 0.2.29. Merging this tags `sdk-v0.2.29` and publishes its tarball. **Why now:** authors (#474, #475) need a release before plugins can use them: - the agent acts as its connection around each command, and plays its pointer tapes with `playAs`; - chat acts around its writes; - pixi's calls that wait out the person's stroke keep their author (`plans/SHARED_HISTORY_PLAN.md` step 2's plugin PRs). ## 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.28`:** - **#474:** who made each step. - `sdk.Author`; `Host.act` / `endAct` / `acted` / `actAgain` / `author` / `setInputAuthor`. - The host keeps a per-document author map filled in `recordStep`. - `stepAuthor`, `topStep`, `mayMerge`, `forgetDocumentAuthors`, and `Step.top`. - The fingerprint moved. Everything is additive (a defaulted field, new functions). **Reaching plugins, no fingerprint move:** - **#475:** the automation service's `playAs(bytes, author)` (`service_version` 5 → 6). Activity titles name an acting agent ("Claude · Draw"). - **#468:** `core`: `MenuRowOptions.anchor`, a stable name for a menu row's button. ## Verified - [x] macOS: - On this head, `zig build test-sdk-version` passes, and `scripts/pack-sdk.sh` packs `fizzy-sdk-v0.2.29.tar.gz`. - fizzyedit/pixi, fizzyedit/agent and fizzyedit/atlas `main` build against this `sdk/` (`--fork`) with no changes. - [ ] Windows: - [ ] Linux: CI. - [ ] Web: ## Follow-ups - The repin automation opens `sdk: repin to fizzy SDK 0.2.29` PRs in the store plugins whose fingerprint moved. - The agent has no repin automation: its repin is a hand-made PR, carrying the agent's `act` around `agentCall` and `playAs` for its pointer tapes. - Chat's repin carries its `act` around ACP writes. 🤖 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
A tape or an agent can aim at the picker's controls by name, not by position:
picker.mode:one,picker.mode:manypicker.action:split,:clear,:defaults,:removepicker.split:vertical,picker.split:horizontalThese sit beside the cards' existing
picker:<view id>, which is now indocs/AUTOMATION.md's anchor table along with the new names.Why. The row's buttons come and go with the place. Remove sits where Split sat on a place with nothing to Clear, so a point read off one place's picker can land on a different action in the next. While driving splits through the agent plugin for #467, a click aimed at Split hit Remove twice and closed the place.
How.
modeButtonandactionButtondraw throughnamedButton:dvui.button's body with acore.anchor.mark, sincedvui.buttondoesn't hand back its widget.core.widgets.menuRowgets ananchoroption for a row that runs no command (a command row is alreadyfizzy.command:<id>).wait_for/expecton a name means the action can run.core.anchor.markcosts nothing unless someone asked for anchors this frame.SDK impact
MenuRowOptions.anchorreaches plugins at the next SDK release (test-sdk-versionpasses; the fingerprint didn't move).Verified
picker: its controls are named for a tape or an agent to aim at, while they can be pressed(anchors wanted, the picker drawn over a place: Single, Multiple and Split are named; a dimmed Clear and a shape place's absent Remove are not).--profile), driven through the agent plugin'sfizzy_runby name only:fizzy_screenlistspicker.mode:one,picker.mode:many,picker.action:split,picker.action:clearandpicker.action:defaults;picker.action:split, and the menu listedpicker.split:verticalandpicker.split:horizontal;picker.split:vertical, and Main split into Main | Main/r1;expectedpicker.action:remove, clicked it, and Main was whole again.scripts/check-fmt.sh,zig build,zig build test(93/93 steps),zig build test-integration(53/53 steps),zig build check-tests-reached,zig build check-web,zig build test-sdk-version,scripts/check-examples.sh.Follow-ups
namedButtonrepeatsdvui.button's body to reach the widget's data, becausedvui.buttondoesn't hand it back. Advui.buttonvariant in the dvui-dev fork that exposes it would keep one body (docs/DEPENDENCIES.md).🤖 Generated with Claude Code