refactor: one control surface, and no Tauri runtime in a headless browser - #95
Merged
Conversation
added 9 commits
September 9, 2026 21:29
`stylo_taffy` mapped `Position::Sticky` onto `taffy::Position::Relative` and stopped there. Relative is the right layout answer: a sticky box takes its flow position and reserves its space there. Nothing supplied the adjustment on top of it, so every sticky navbar and document sidebar on the fleet scrolled away, on documents 5,000 to 16,700px tall. Worse than nothing, in fact. Taffy applies the inset of a relative box as a displacement of its flow position, and a sticky inset is a threshold rather than an offset, so `position:sticky;top:24px` was drawn 24px below where it belonged before anything had scrolled at all. Sticky boxes now reach taffy with no inset and the engine reads the authored values back from the computed style when it applies the adjustment. `resolve_sticky_positions` runs after layout and after a scroll, and writes the displacement straight into the box's `final_layout` location. Paint, hit testing and `absolute_position` all read that one field, so writing it is what makes them agree; a parallel offset would have to be threaded through each of them and would disagree the moment one was missed. The price is that the pass has to recover the flow position it started from, which `sticky_offsets` records and `resolve_layout` clears along with the locations rounding rewrites. Both halves of the CSS rule are here: the box is held between its own flow position and the far edge of its containing block, so it pins when the page scrolls past it and travels away with its section rather than floating over the next one. Percentage insets resolve against the sticky view rectangle. The scrollport is the nearest scrolling ancestor's padding box, or the viewport when nothing between the box and the root scrolls, which matches where a scroll that reaches the root element is forwarded. The scroll path is hooked as well as the resolve path: a wheel event does not necessarily produce a style and layout pass, and a header that only unstuck on the next restyle is a header that visibly lags the scroll.
Paint translates the whole tree by the negated viewport scroll, and a hoisted fixed layer sits in that tree like everything else, so a fixed box held its document position and left the screen exactly like flow content. Measured: an overlay authored `top: 0`, on a page scrolled 800px down, painted at screen y=-800. Its document position has to track the scroll for its screen position to hold still, which is the whole of the correction. Applied in the same shape as the sticky adjustment and next to it, because it is the same kind of thing: a displacement that depends on a scroll position rather than on layout, written into the box so that paint, hit testing and client rects cannot disagree about it. One value for the document rather than one per node, since every viewport-anchored box shares the same containing block and therefore the same displacement. Boxes under a transformed ancestor are untouched: that ancestor is their containing block, so they are not viewport-anchored, and the hoist walk already leaves them alone. This does not give a fixed box the containing block CSS asks for. That is still the root element, which takes its height from its content, so `bottom` and `inset` resolve against the document. Reproduced on the reported case: a fixed panel with `bottom: 0; height: 816px` on a 7,934px document lays out at y=7118, which is where a chat panel was found "opening off screen", and `inset: 0` sizes 1000x7934 rather than 1000x700. Closing that needs an initial containing block distinct from the root element, which `opposite_insets_should_size_against_the_viewport` has recorded as an ignored test since the hoist landed.
… tree There was no notion of selectedness anywhere in the engine. An <option> was a display:none node carrying a `selected` attribute nobody read, so a select could be measured and pressed but could not say what it offered or what was chosen. Its options reached the accessibility tree with a role and nothing else: no label, no selected state, and no value on the select itself. A QA harness driving worktables.dev's schema designer had nothing to assert against. SpecialElementData::Select(SelectData) holds one selectedness flag per option, seeded at layout construction from the `selected` content attributes and from HTML's "ask for a reset" step, which is why a plain <select> now reports its first option rather than an empty value. Construction is idempotent the way create_checkbox_input is, so a selection survives the next resolve; only the option count is refreshed, which is how a script-added option gets an entry. Positions rather than node ids: selectedIndex is the spec's own handle and a position cannot dangle once the option is removed. The list of options is recomputed from the subtree on every read, which flattens <optgroup> for free.
…bmittable Three ways in, none of which worked. `handle_click` had no arm for a select, so the walk fell through to the no-match tail, which calls `clear_focus()`. Pressing a select actively unfocused the page, and the keyboard handler is gated on focus, so the arrows could not drive a control the user had just pressed. `checkable_activation_target` gets the matching arm, because the two have to agree about where the walk ends. Form submission had a standing TODO exactly where a select's serialization belongs, so a select fell through to the generic tail and submitted its own literal `value` attribute, which a select does not have. Every form containing a picker posted the wrong body. HTMLSelectElement existed only as an instanceof brand. `select.value` read an attribute that is never there and reported the empty string; there was no selectedIndex, no options, and no option.selected. Those go on the shared Element prototype behind a tag guard, the way `value` already branches on text_input_data(), and they write live state then snapshot, which is the rule set_checked's comment lays down.
…en it does There was no keyboard activation for anything but a text input, so a focussed picker could not be driven at all. The arrows never even reached the keyboard handler: the KeyDown default action scrolls the page on ArrowUp, ArrowDown, Home and End and then returns, and `scroll_key_is_claimed_by`, the one way past it, tested only for a text input. A press on a select scrolled the document instead of choosing anything. ArrowUp and ArrowDown step over disabled options and do not wrap; Home and End go to the first and last selectable option. A selection that does not move dispatches nothing, so a `change` listener does not fire on a no-op. The keystroke is the commit, so `input` fires immediately and blitz-script synthesises `change` from it, the way it already does for a checkbox. A select does not wait for blur the way a text control does. The QA case this unblocks: worktables.dev's schema designer picks its column type and index backend by keyboard or not at all, which is why two composite primary key rules there were implemented but untestable.
Still a character count: the options never reach layout, so nothing shapes them and there is no real measurement to be had. What it counts changes. `text_content().trim()` measured a labelled option's element text, which is the one string the control never shows, and `trim()` only strips the ends, so a label whose own words were split across source lines counted the newline and the indentation before the next word. A picker with a `label` attribute came out sized to hidden markup; one formatted across lines came out wider than the same picker written on one line.
`blitz-script` takes `ps-blitz-debug-control` behind its `debug-control` feature, and that crate lived in pathscale/ps-observability. So ps-blitz depended on ps-observability, and the work that puts the Blitz control surface into ps-observability's `blitz-control-protocol` would have made ps-observability depend on ps-blitz for `blitz-dom`. Two repositories pointing at each other have no release order: whichever publishes first publishes against the other's previous version. The crate is the lower half of the pair, so it comes here. It depends on getrandom, serde and serde_json and nothing else, and nothing in it knows about ps-observability's harness. Its version stays on its own 0.3.x line rather than inheriting the engine's 0.4, because it versions a loopback debug transport that does not change when the engine does. The publish plan lists it before `ps-blitz-script`, which needs it, and the existing crates.io version check skips it on any release where its version has not moved.
There were two. This one built the AccessKit tree a screen reader reads;
a second copy in tauri-runtime-blitz's `agent.rs` built the semantic tree
an agent and the QA harness read. They were written months apart against
the same specification and nothing could compare them, because this table
was private and the other lived in another repository.
They had drifted. `<th>` is a column or row header here and arrived as a
plain `cell` there, so a check that wanted "the Version column" had
nothing to ask for while every header was spelled the same as the data
beneath it. That was one of eight naming defects found in the other copy.
`accessibility::implicit_role` is the table, now public, and
`build_accessibility_node` is its first caller. `accesskit` is
re-exported beside it so a consumer names the same `Role` type rather
than resolving a second copy of the crate.
Two rules move in with it, because they were only ever in the other copy
and belong to the mapping rather than to either consumer:
- A `<section>` with `aria-label`, `aria-labelledby` or `title` is a
`region`; without one it stays a plain section. HTML-AAM says both
halves, and a named section is how a page says "this part is the
connection settings".
- `scope` on `<th>` was already here and is now stated once.
The role rules read a tag name and a handful of attributes, so the tests
build elements directly: blitz-dom holds the tree but does not parse
HTML, and a fixture here cannot be a string of markup.
This was referenced Sep 11, 2026
Closed
added 4 commits
September 11, 2026 23:41
This was referenced Sep 11, 2026
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.
Prepare the 0.4.8 engine release for the shared control protocol and UI consumers. The native select work from #94 is included; #96 is closed and subsumed.
Review restored missing option labels and selectedness, corrected text-node geometry, and made client rectangles follow painted two-dimensional transforms, including ancestor transforms. Native pointer activation and Worktables zoom/drag exposed the geometry problem through actual application behavior.
Validation: the select accessibility, inline text, transformed geometry and full local engine suites pass. Native ps-qa fixture groups pass with the coordinated host, including select changes and exactly one activation of a transformed button with exact client dimensions. Worktables passes 112/112, including real drag/cancel/undo and zoom geometry; UI passes 273 checks across 75 native fixtures.
The previous CI failure used chuzz master, which did not implement the pointer input exercised by the new fixture. Matching protocol, runtime and host commits are now pinned, with unpublished dependencies patched from those checkouts. Nested checkouts are excluded from this Cargo workspace to prevent incorrect workspace inheritance. The native fixture explicitly uses border-box sizing and exact dimensions. Coordinated CI run 34632611122 passes at 97ca22c, including the refreshed WebSocket listener and contrast-settling stack.
Release order: ps-blitz 0.4.8 → ps-observability #21 (protocol 0.5.0 / ps-qa 0.7.1) → tauri-runtime-blitz #57 (0.4.0) → chuzz #45 → UI and consumers, including AgencyZero. Verify each registry publication before advancing.
Owner PR review and approval are required before merging and publishing. The packed UI candidate and final consumer sweep are complete; remaining failures are documented backend or unpublished-registry boundaries in the UI release review.