Repository navigation
Conversation
Fix `hasNativeTestRenderer` always reporting false for anyone who
installs the package, found by copying `example-app/` out of the
repository and installing `@gpuix/react` from npm:
Test Files 1 skipped (1)
Tests 6 skipped (6)
`testing.js` ships as ESM and loaded the addon with a bare
`require("@gpuix/native")`. Node has no `require` in ESM. Inside this
repository vitest inlines the workspace package and provides one, so
every suite here passed; installed from npm the package is externalized
and run by Node, the call threw `require is not defined`, and the catch
reported native as missing. Every guarded suite then skipped silently,
which is worse than failing.
It loads through `createRequire(import.meta.url)` now. Verified by
dropping the built file over the installed 0.5.0 package in that copy:
the 6 tests run, and `bun run screenshot` drives the real window.
Session: ses_fc0b7cd62ffe3aBw0pyYBOBDjf
There was a problem hiding this comment.
🟡 Changes recommended
The zed submodule URL is switched to SSH in .gitmodules, which is likely to break CI submodule checkout (and new clones) without additional SSH-key setup.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR expands GPUIX’s styling and motion capabilities across the React reconciler and native renderer by introducing CSS-value parsing (including variables and modern color syntaxes), a className resolver seam, and motion support for animating height to/from auto, along with new tests and a demo app to exercise the features end-to-end.
Changes:
- Add a
classNameresolver option on roots and route class-derived styles through the reconciler’s style send path. - Improve native style handling/performance (boxed styles, cached resolutions) and add motion
height: automeasurement via isolated layout. - Add comprehensive tests + a new demo app showcasing CSS values (colors/lengths/vars/gradients/corners), inheritance, class names, and motion.
File summaries
| File | Description |
|---|---|
| packages/react/tsconfig.typecheck.json | Adds a dedicated typecheck config to include type-only assertion files. |
| packages/react/src/testing.ts | Extends test renderer helpers (clipboard read, style-resolution counters, pixel sampling, root options). |
| packages/react/src/reconciler/renderer.ts | Extends render() options to include root options (e.g., class resolver). |
| packages/react/src/reconciler/reconciler.ts | Adds RootOptions to createRoot and stores renderer on the returned Root. |
| packages/react/src/reconciler/host-config.ts | Centralizes style computation and adds className + inline-style merging; fixes hide/unhide style behavior. |
| packages/react/src/reconciler/class-names.ts | Implements className token/string caching and inline override semantics. |
| packages/react/src/index.ts | Exports ClassNameResolver and RootOptions types. |
| packages/react/src/tests/style-types.check.ts | Adds type-level assertions for StyleDesc (custom props, field types, state rules). |
| packages/react/src/tests/style-resolution-cache.test.tsx | Adds native tests asserting style-resolution caching behavior across frames/updates/motion. |
| packages/react/src/tests/sizing.test.tsx | Adds screenshot/geometry tests for width/height/min/max accepting CSS lengths, vars, calc, etc. |
| packages/react/src/tests/selection.test.tsx | Adds cmd-c copy test and updates lineHeight cases to length strings. |
| packages/react/src/tests/render.test.tsx | Tests passing a class resolver through render(). |
| packages/react/src/tests/jsx-types.check.tsx | Adds JSX type assertions (including key on intrinsic tags). |
| packages/react/src/tests/input.test.tsx | Tests clipboard behavior for input selection vs document selection. |
| packages/react/src/tests/inheritance.test.tsx | Adds screenshot tests pinning inherited text properties behavior. |
| packages/react/src/tests/css-lengths.test.tsx | Adds screenshot tests for CSS lineHeight semantics and calc folding. |
| packages/react/src/tests/color-functions.test.tsx | Updates color parsing expectations (lightningcss-based, drops nonstandard syntaxes). |
| packages/react/src/tests/class-names.test.tsx | Adds screenshot tests verifying className output matches equivalent inline styles. |
| packages/react/package.json | Adds a typecheck script for CI. |
| packages/react/jsx-runtime.d.ts | Fixes nodenext import path and adds key support on intrinsic tags via Tag<P>. |
| packages/react/jsx-dev-runtime.d.ts | Simplifies dev-runtime typings to re-export JSX namespace from jsx-runtime. |
| packages/native/src/theme.rs | Adds dark to Theme, derives PartialEq for Theme/SyntaxPalette, narrows diff-layout hashing semantics/tests. |
| packages/native/src/text/paint.rs | Moves copy handling to keystroke observation and adds optional cursor styling for selectable text. |
| packages/native/src/text/mod.rs | Re-exports the new copy keystroke watcher. |
| packages/native/src/test_renderer.rs | Adds style-resolution counters, clipboard read, pixel sampling, and refactors screenshot capture; updates style parsing call. |
| packages/native/src/retained_tree.rs | Boxes style storage, adds resolved-style/cascade caches, and ensures empty styles don’t create false changes. |
| packages/native/src/renderer/virtual_list.rs | Introduces retained virtual list state/config syncing logic. |
| packages/native/src/renderer/batch.rs | Adds atomic batch parsing/validation and shared apply logic. |
| packages/native/src/renderer/auto_height.rs | Implements measured-layout wrapper for animating height to/from content (“auto”). |
| packages/native/src/lib.rs | Splits event payload types into events and makes style module public. |
| packages/native/src/events.rs | Defines Rust→JS event payload module (renamed/relocated). |
| packages/native/src/custom_elements/mod.rs | Adds cascade to custom render context and centralizes style application via ctx.styled(). |
| packages/native/src/custom_elements/markdown.rs | Uses ctx.styled() to apply styles with correct variable scope. |
| packages/native/src/custom_elements/input.rs | Uses ctx.styled() for consistent style/cascade application. |
| packages/native/src/custom_elements/img.rs | Uses ctx.styled() for consistent style/cascade application. |
| packages/native/src/custom_elements/diff.rs | Ensures borderRadius reads through cascade scope; uses ctx.styled(). |
| packages/native/src/custom_elements/code.rs | Uses ctx.styled() for consistent style/cascade application. |
| packages/native/src/custom_elements/anchored.rs | Uses ctx.styled() for consistent style/cascade application. |
| packages/native/src/color.rs | Switches native color parsing to the new CSS engine (gpuix-css/lightningcss) and adds context-aware parsing. |
| packages/native/index.d.ts | Updates generated TS declarations for new test renderer APIs. |
| packages/native/css/src/color.rs | Adds GPU-independent CSS color parsing crate using lightningcss with contextual resolution. |
| packages/native/css/Cargo.toml | Declares the new gpuix-css crate and its lightningcss dependency. |
| packages/native/Cargo.toml | Adds gpuix-css and image deps; removes csscolorparser; adds style parse benchmark target. |
| packages/native/benches/style_parse.rs | Adds benchmark measuring StyleDesc JSON parse performance. |
| examples/tsconfig.json | Broadens example TS include patterns. |
| examples/package.json | Adds demo run/compile scripts and updates compile invocation args. |
| examples/demo/variables.tsx | Demo panel for CSS custom properties and var(). |
| examples/demo/ui.tsx | Shared demo UI components implemented via className. |
| examples/demo/perf.tsx | Demo performance panel using frame overlay stats + list virtualization example. |
| examples/demo/lengths.tsx | Demo panel for CSS lengths, calc/min/max/clamp, and dropped units. |
| examples/demo/inheritance.tsx | Demo panel for inherited text props, currentColor, and selection inheritance. |
| examples/demo/gradients.tsx | Demo panel for linear gradients. |
| examples/demo/corners.tsx | Demo panel for corner shapes + motion interpolation. |
| examples/demo/colors.tsx | Demo panel for modern CSS color syntaxes including mix/relative/light-dark/currentColor. |
| examples/demo/classes.ts | Example class token resolver (tailwind-like) + counter. |
| examples/demo/class-names.tsx | Demo panel illustrating className caching/override semantics. |
| examples/demo/app.tsx | Demo app shell with sidebar navigation + palette swapping via custom properties. |
| examples/demo.tsx | Entry point wiring demo app with render() + resolveClassName. |
| examples/demo.test.tsx | Integration tests driving the demo app/panels via native test renderer and asserting key behaviors. |
| examples/compile.ts | Generalizes compile script (args parsing, optional icons, macOS codesign) and supports multiple apps. |
| examples/chat.test.tsx | Updates expectations for retained style reporting (e.g., width now reports '100%'). |
| CONTEXT.md | Adds shared vocabulary/terminology documentation for the codebase. |
| AGENTS.md | Expands contributor guidance (commit message rules, etc.). |
| .gitmodules | Updates zed submodule URL/branch. |
| .github/workflows/ci.yml | Adds React typecheck step to CI. |
| .changeset/split-the-renderer-by-subject.md | Changeset documenting renderer module split. |
| .changeset/sizes-read-css-lengths.md | Changeset documenting sizing props accepting CSS lengths. |
| .changeset/resolve-styles-once.md | Changeset documenting resolved-style caching + visibility/hide fixes. |
| .changeset/render-takes-a-class-resolver.md | Changeset documenting passing resolver via render() and exposing renderer on Root. |
| .changeset/read-styles-without-buffering.md | Changeset documenting faster StyleDesc parsing and boxed storage. |
| .changeset/read-colours-with-lightningcss.md | Changeset documenting move to lightningcss for color parsing and syntax changes. |
| .changeset/motion-keeps-the-style-cache.md | Changeset documenting keeping style cache stable during motion. |
| .changeset/linear-gradient-backgrounds.md | Changeset documenting linear-gradient background support. |
| .changeset/key-on-a-built-in-tag.md | Changeset documenting intrinsic key typing + nodenext import fix. |
| .changeset/custom-properties-and-var.md | Changeset documenting custom properties, var(), currentColor, and numeric fields as string/number. |
| .changeset/cursor-keywords.md | Changeset documenting cursor keyword support + I-beam over selectable text. |
| .changeset/css-lengths-and-calc.md | Changeset documenting CSS lineHeight semantics + calc support in lengths. |
| .changeset/corner-shape.md | Changeset documenting corner-shape support and motion interpolation semantics. |
| .changeset/copy-selection-with-cmd-c.md | Changeset documenting cmd-c copy behavior change. |
| .changeset/close-a-height-from-where-it-is.md | Changeset documenting smooth collapse behavior for height:auto animations. |
| .changeset/class-name-prop.md | Changeset documenting className prop and resolver seam. |
| .changeset/bend-toward-content-that-grows.md | Changeset documenting motion bending when content changes mid-animation. |
| .changeset/animate-height-to-auto.md | Changeset documenting animating height to/from auto via measurement. |
Review details
- Files reviewed: 98/103 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Three changes to the release pipeline, all found while cutting 0.5.0. **One target per OS.** The matrix built six targets; it now builds the architecture each OS is mostly used on and nothing else: | OS | Target | Renderer | |---------|---------------------------|---------------| | macOS | aarch64-apple-darwin | Metal | | Linux | x86_64-unknown-linux-gnu | Vulkan / wgpu | | Windows | x86_64-pc-windows-msvc | Direct3D | Every extra target is a full gpui build, and gpui is most of the wall clock of this workflow, so a release was paying twice over for platforms almost nobody installs. Intel macOS, arm64 Linux and arm64 Windows lose their prebuilt binary. `napi.targets` is trimmed to match; the two lists must stay identical or the published loader looks for a platform package CI never built. Both test jobs already pull targets that stay. **The chat example gets an extension.** A GitHub release asset is served as raw bytes, so a download lost the executable bit and, on macOS and Linux, arrived with no extension at all. The build job now packs those two into `example-chat-<target>.tar.gz`, which keeps the mode and names itself. Windows keeps its `.exe`. Verified end to end with the real 0.5.0 macOS binary: pack, drop the mode to 644 like a browser download, `tar -xzf`, and the extracted file is `-rwxr-xr-x` and opens its window. The download also drops from 83 MB to 33 MB. **Create the GitHub release before CI reaches the publish job.** The upload step exits 0 when no release exists for the tag, so a release created afterwards gets no binaries, and the only way to add them later is to move ~100 MB through a laptop. There is no API that attaches an artifact to a release: the assets endpoint wants the raw bytes in the request body, and `gh release upload` cannot read stdin (cli/cli#5820). AGENTS.md now spells out the order, and what to do about the tags when CI fails and you push fixes. Session: ses_fc073cf53ffem2NBtBXPFaCgyF
My previous commit used `git add -A` and swept in `website/docs.json` and `website/src/changelog/intro.mdx`, which another session wrote while I was working. They are not part of the CI change. This restores both to their state before that commit and leaves the work on disk, untouched: docs.json is a modified working tree file again and intro.mdx is untracked again, so whoever wrote them can commit them with their own message. Session: ses_fc073cf53ffem2NBtBXPFaCgyF
Replace the stale pending-validation note with the result that CI now runs the full React and example suites through the DirectX-backed Windows test renderer. This matches the successful Windows validation added for issue remorses#26 and released in 0.5.1. Session: ses_fc05758b6ffevGYCjaV7pxsfuO
Replace the single docs sidebar with a tabbed navigation that separates the main documentation from generated release notes. Keep GitHub and npm as link-only tabs so all top-level destinations use one consistent navigation model and icon set. Add introductory content for the changelog tab. It explains that native and React packages ship together from one commit and points readers to the prebuilt chat binaries attached to each release. Holocron uses this page as the stable introduction before the release entries fetched from GitHub. This commit restores the website-only work that was deliberately left in the working tree by a906cf9 after it was accidentally included in an unrelated CI commit. Session: ses_fc04fcb1dffeooxK1KAopXSTAY
Build the browser chat entry as part of the documentation website's production build. The web build now emits a minified `chat.js` bundle and its Wasm asset into the website's ignored public directory, after rebuilding the native Wasm and React package so deployed output cannot use stale generated code. Keep the production destination fixed at `website/public/chat-example` rather than accepting a caller-controlled directory. The build clears that generated directory before bundling without allowing a malformed command to remove an arbitrary repository path. Serve the shared example HTML at `/chat-example/`, redirect the non-trailing slash form, and set COOP and COEP on the document. Those headers make the page cross-origin isolated, which the shared WebAssembly memory requires. Static JavaScript and Wasm stay same-origin under the same route prefix. Add an accessible loading screen while the browser downloads and initializes the renderer, then remove it after GPUIX mounts. The same shell remains usable for local browser development, while the production route swaps its TypeScript entry for the generated JavaScript asset. Ignore the generated website bundle and add a minor changeset for the new live example at https://gpuix.dev/chat-example/. Session: ses_fc04fcb1dffeooxK1KAopXSTAY
Normalize CRLF when the code prop enters the native element so syntax highlighting, painting, selection, and clipboard output all use the same LF-only source. Add GPU-backed regression coverage for painted rows and cross-line selection. Preserve the final empty row produced by a trailing newline. Validation: - native release build - 201 Rust library tests - 342 React renderer tests - 36 example application tests Session: ses_fc057a79fffeOPMjeaNqNgpvHd
`render()` takes `focus` and `show`. `focus: false` opens the window behind
whatever app you were typing in, like `open -g`. `show: false` opens nothing
at all, so the process boots with a live React tree and an empty screen.
render(<App />, { title: 'Notes', focus: false })
**Two knobs, not one**
`gpui::WindowOptions` already had `focus` and `show`, and `to_gpui_window_options`
simply never set them. But mapping the fields alone does nothing, because
`init()` called `cx.activate(true)` unconditionally on both init paths.
On macOS the window flag only decides whether the window becomes *key inside*
the app; activation is what pulls the whole process forward. Both had to move
together, so `activate` is computed before the closure (the options are moved)
and the call is gated on it.
**Revealing it later**
`activateWindow()` brings the window forward and focuses it. It is the only way
to reveal a `show: false` window. macOS goes through `update_window`, the
threaded platforms through a new `UiCommand::ActivateWindow`. It runs
`cx.activate(true)` alongside `Window::activate_window()`, because the latter
only makes the window key within the app.
**Platform support**
Straight from GPUI's `WindowParams`:
| Platform | `focus: false` | `show: false` |
|----------|------------------------------------|---------------|
| macOS | orders in front without becoming key | honored |
| Windows | `SW_SHOWNOACTIVATE` | honored |
| Linux | ignored | ignored |
The macOS Dock icon still appears. `did_finish_launching` hardcodes
`NSApplicationActivationPolicyRegular`, so a menu-bar-agent app would need a
`remorses/zed` fork commit. Searched upstream first per AGENTS.md: no issue and
no PR asks for a configurable policy, and the only code hit is that hardcoded
call. Left for a follow-up.
**Verified**
Five new `window_options_tests` cover the mapping, including that an unset
option still defaults to true and that the existing fields keep mapping.
Focus stealing is not unit-testable, so I drove real windows and read the
frontmost process id and the AppKit window count through `osascript`:
default frontmost=app windows=1
focus: false frontmost=other windows=1
show: false frontmost=other windows=0
after activateWindow() frontmost=app windows=1
`cargo test --lib` 137 pass, `packages/react` 302 pass, `examples` 35 pass.
`x86_64-unknown-linux-musl` cross-builds, which type-checks the threaded
branch; Windows cannot cross-compile here because `onig_sys` needs Windows C
headers, and CI covers it.
Session: ses_fc17399a8ffeTt72KZetsU15ty
`focus: false` shipped in the previous commit with no guidance on when to
reach for it. The answer is: whenever an agent starts the app. An agent that
launches the window to check its own work takes the keyboard away from
whoever is typing, mid-sentence, once per iteration, and it gains nothing by
doing so.
**README**
The `Background launch` section now leads with that rule, and a new
`Let an agent drive the app` section documents the environment-variable
pattern so a human run is unchanged and an agent run stays out of the way:
render(<App />, { focus: process.env.GPUIX_BACKGROUND !== '1' })
bun app.tsx # you: window comes to the front
GPUIX_BACKGROUND=1 bun app.tsx # agent: opens behind your editor
**AGENTS.md**
The same rule as an instruction, in `Drive the live window`, so an agent
follows it without being asked.
**examples/chat.tsx**
The AGENTS.md snippet launches `chat.tsx` with `GPUIX_BACKGROUND=1`, but
`chat.tsx` read no such variable, so the documented example would have
silently kept stealing focus. Its `render()` now honours it. This is the app
AGENTS.md tells agents to launch, so it is the one that most needs the flag.
**A claim I had to retract**
The first draft said typing works on a background window. It does not.
`fill()` and `press()` throw `keystrokes are not live yet`, because the live
renderer implements no `simulateKeystrokes`. That is a pre-existing `launch()`
gap and is unrelated to focus; it fails on a focused window too. All three
documents now say so and point at `createTestRoot()` for anything that types.
**Verified rather than assumed**
Every claim was run against a real launched window, then the probe deleted:
click() on an unfocused window works, count=2 after two clicks
screenshot() on an unfocused window works
both on a show:false window works, never on screen
focus not stolen Chrome stayed frontmost
fill() / press() on a live process throws
The exact AGENTS.md snippet was then run end to end: it launched `chat.tsx`,
clicked `sidebar-collapse`, wrote a screenshot of the fully painted app, and
left Chrome frontmost throughout.
`examples` 36 tests pass.
Session: ses_fc17399a8ffeTt72KZetsU15ty
Export the public host instance type for typed element refs. Add a bounded virtual-list chat example with cursor pagination in both directions, stable prepends, variable-height Safe MDX rows, route links, loading states, and retained-page limits. Include focused coverage for initial rendering, route navigation, paging to both ends, stable retained rows, and request termination at history boundaries. Session: ses_fbcc7a504ffe9Ff4lP0KDJLp3B
An older page landing while the reader waited in the loading row moved the screen. Three stacked causes, three layers of fix: 1. gpui anchors a list on the item at the viewport top. While the reader waits in the void that item IS the void, so the splice replaced the screen under it. The old restore (`scrollToItem(listId, index)`) snapped the anchor message to the viewport top with offset 0, itself a jump of up to EDGE_HEIGHT pixels. No non-negative ListOffset can express "keep this row where it is": the rows above it are freshly inserted and unmeasured. The gpui fork (submodule commit 8b94defe56) now accepts a negative `offset_in_item` in `ListState::scroll_to`; `layout_items` resolves it by walking upward with real measured heights, which is what makes the restore exact rather than estimate-based. 2. On the live renderer `scrollToItem` applied `scroll_to` immediately, but the just-committed prepend only reaches the ListState at the next `GpuixView::render`, so `splice_focusable` shifted the freshly restored anchor a second time. The test renderer hid this because its wrapper flushes first. Virtual-list scrolls are now queued in `PENDING_VIRTUAL_LIST_SCROLLS` and applied right after that frame's splice, then the queue is cleared (one-shot). 3. Scrolling DOWN jumped for a different reason: a reader resting at the bottom sits on gpui's at-end sentinel (`logical_scroll_top` is None, reported as itemIndex == item count), where any offset-in-void math is meaningless. `getListScrollTop` returns `[itemIndex, offsetInItemPx, viewportHeightPx]`; the app converts the sentinel with `EDGE_HEIGHT - viewportHeight` so the void's old top edge stays put and the new page fills the blank space below the text. New API on all four surfaces (napi, UiCommand, wasm, test renderer): - `scrollToItem(listId, index, offsetInItem?)` — px, may be negative - `getListScrollTop(listId)` — logical anchor plus viewport height `examples/infinite-chat.tsx` drops the snap-to-top workaround and restores both directions. The loading row shrank from 560px to 160px. Tests, each verified to fail against the previous behaviour: - examples: "keeps the message under the reader at the same pixel when an older page lands", "keeps the trailing messages at the same pixel when a newer page lands", "lands a newer page inside the void the reader is staring at" - packages/react: scrollToItem offset roundtrip, negative-offset normalization ([50,-100] over 40px rows resolves to [47,20]), top clamp Session: ses_fbc66676dffeMDwZ3jJhimdkw6
`bun scripts/web.ts` now registers one bundled document per example:
`/` keeps the chat and `/infinite` serves the infinite history. The
shared cross-origin isolation headers wrap each document the same way.
The inline page styles move from web.html into a shared web.css that
both documents import, and web-infinite-chat.{html,tsx} are the browser
entry pair for the example. bun.lock picks up string-dedent for the
example's seeded message generator.
Session: ses_fbc66676dffeMDwZ3jJhimdkw6
GPUI keeps hover, active, pointer capture, implicit scroll, the accessibility
node and its own per-element state behind a `GlobalElementId`. An element with
no `.id(..)` has none of that, and the failure is always silent: the prop
type-checks, the value crosses the bridge, and nothing happens.
Three holes, all invisible from JavaScript:
| You write | What happened |
|---|---|
| `<text onClick={..}>` | listener registered, never fired |
| `<code style={{ hover: {..} }}>` | style serialized, then dropped |
| `<img src="cat.gif" />` | frozen on frame 0 |
`<text>` had its own builder. It made a bare `gpui::div()` with no id and wired
none of the shared `Props` surface, so `onClick`, `onMouseEnter`, `onKeyDown`,
`autoFocus`, `tabIndex` and pointer capture were all accepted and ignored.
`build_div` already did everything `<text>` needs, so it is renamed
`build_host_container` and both types go through it. `build_text` is deleted.
`StyleDesc` carries `hover` and `active` for every element type, but only
`build_div` read them. New `apply_interactive_styles` applies the base styles
plus both refinements, and every stateful GPUI root uses it now. The one
exception is `<virtual-list>`, whose `style` type drops them: gpui's `List` has
no interactive identity to hold a pressed or hovered state.
`wire_standard_events` moves out of `code.rs` into `custom_elements/mod.rs` and
grows into `custom_surface`, which does styles, bounds and events in one call
for `<code>`, `<markdown>`, `<diff>` and the image fallbacks.
`<img>` and `<svg>` get host-derived ids, so gpui's `ImgState` (the animated
frame index and the delayed loading placeholder) survives a frame.
## Bounds without a wrapper
`<img>`, `<svg>` and `<anchored>` accepted `testId`, appeared in the automation
tree, and then threw `Element has no painted bounds` on `click()`.
`bounds_tracker` needs a positioned parent and a canvas child, which a leaf
cannot have, and wrapping the leaf in a div would move the layout box: the
wrapper becomes the flex item and the image loses intrinsic sizing and corner
clipping. They use gpui's new `on_painted` instead, through
`automation::track_own_bounds`. `<anchored>` uses it too, because only gpui
knows where a deferred overlay landed after snapping, so the recorded box is
the overlay and not the trigger.
## Element ids are integers
`<div>` and `<text>` use `ElementId::Integer(host_id)` rather than a formatted
`"__gpuix_<id>"`. Host ids are already unique per renderer, and the
`SharedString` allocation ran on every node on every frame. Measured on the
3,259-clip timeline pan with culling off: the formatted name cost 14% over the
old text builder, the integer costs 4%. Custom elements keep
`ElementId::Name`, a different enum variant, so the namespaces cannot collide.
## gpui submodule
Bumped to `d8f267829d` on `remorses/zed:gpuix`, which carries the two fixes this
depends on:
- `should_insert_hitbox` now includes `active_style` and `group_active_style`.
An `active` style with no click listener, hover style or cursor never got a
hitbox, so the press was never recorded and the refinement never applied. It
looked like it worked as soon as you added an unrelated `onClick`.
- New `InteractiveElement::on_painted`, an own-bounds listener on
`Interactivity`. It reports from paint, not prepaint, because `List::prepaint`
speculatively prepaints a row range and can roll the window back and prepaint
a different one.
Session: ses_fc10c354effe0X3SbzUKVPca44
## Every removed string leaked
```tsx
<div>{visible ? 'hello' : null}</div>
```
Toggle `visible` off and the text left the screen, but the native element stayed
in the retained tree, detached and unreachable, for the life of the process. A
long chat or list grew without limit.
React calls `detachDeletedInstance` only for host **components**; a host text
node never gets one. `removeChild` only unlinked, so nothing ever destroyed the
node. React calls `removeChild` from the deletion path alone, never to move a
node, so it now destroys the child and its subtree and unregisters their
handlers. The later `detachDeletedInstance` for a host component is then a
no-op, which the recursive destroy already tolerates.
`getRetainedElementCount()` on the test renderer is what proves it.
`getTreeJson` walks from the root, so it cannot see a node that was detached and
never freed.
## A second root silently took over the first
```ts
const renderer = createRenderer()
const a = createRoot(renderer)
const b = createRoot(renderer) // took the window, the root id and the events
```
A renderer owns one window, one native `root_id` and one event map, and the
second root replaced all three. Worse, unmounting **either** root deleted the
shared entry in `containersByRenderer`, so every handler on the other root went
dead.
`attachRoot` now throws when a different container already owns the renderer,
and `detachRoot` takes the container so only the owner can remove the entry.
Sequential remounts still work, which is what `render()` relies on.
Session: ses_fc10c354effe0X3SbzUKVPca44
`AGENTS.md` replaces the "custom elements are invisible to automation" section, which is now wrong, with the two rules a new element has to follow: - give it a host-derived GPUI id, or it silently loses hover, active, pointer capture, implicit scroll, its accessibility node, and any state gpui keeps. `<img>` had no id, which is why an animated GIF never left frame zero - call `apply_interactive_styles`, never `apply_styles`, on a stateful root Plus how bounds are recorded, and the trap behind it: containers add a `bounds_tracker` child, leaves and `<anchored>` use gpui's `on_painted`, and neither may move to prepaint because `gpui::list()` prepaints a speculative row range and then rolls it back. `README.md` drops the claim that `<img>`, `<svg>`, `<anchored>`, `<diff>` and `<markdown>` have no painted bounds, says `hover` and `active` work everywhere except `<virtual-list>`, and states the one-root-per-renderer rule. Session: ses_fc10c354effe0X3SbzUKVPca44
The section body was updated when the gpui listener moved from prepaint to paint, but the heading kept the old name, which is the line a reader skims. Session: ses_fc10c354effe0X3SbzUKVPca44
All three `extends Props`, so `onClick`, `onMouseEnter` and `onMouseLeave`
type-check on them. All three returned `supported_events() -> &[]`, so the
registry filtered `ctx.events` down to nothing and no handler was ever
installed. The prop compiled, the JS listener registered, and nothing happened.
This is the same defect as `<text>`, in three more places.
```tsx
<img src={avatar} onClick={openProfile} />
<anchored side="bottom" onMouseLeave={close}>{items}</anchored>
```
`wire_standard_events` is now generic over `StatefulInteractiveElement` rather
than taking a `Stateful<Div>`. `<img>` and `<svg>` are gpui leaves and cannot
hold the `bounds_tracker` child that `custom_surface` adds, but `.id(..)` makes
both `Stateful`, so they can take the same listeners.
Also in the same area:
- the empty `<diff>` branch returned before `custom_surface`, so a diff with no
changes was the one state with no automation bounds and no click or hover
events. It goes through the same path as the non-empty branch now
- the `<anchored>` comment still said its box is read during prepaint. It has
been `on_painted` since the gpui submodule bump, and `AGENTS.md` now forbids
prepaint for exactly the `List::prepaint` rollback reason
- `<img>` painted its "no src" fallback with a raw string, which `AGENTS.md`
forbids: a raw child is invisible to `getPaintedText()`, so that state could
only be tested by screenshot. It goes through `chrome_text`
Found by a review against the original audit plan, which named the wider-type
problem for styles and stopped there.
Session: ses_fc10c354effe0X3SbzUKVPca44
- `README.md` claimed **every element type** records painted bounds. `<virtual-list>` records none: gpui's list is not an interactive element, so there is nothing to record a box against. It is safe today only because `VirtualListProps` does not extend `Props` and so takes no `testId`. The claim is now scoped to elements that accept `testId`, and the exception is named - `AGENTS.md` said the two id namespaces cannot collide, which is true only for as long as nobody writes `.id(<index>)`. `impl From<usize> for ElementId` makes that the idiomatic gpui row id, and it lands in the same `Integer` namespace as a host id. Every per-row id in this crate is a formatted name for that reason, and the rule is now written down - the changeset is `minor`, not `patch`. `VirtualListShared.style` became `Omit<StyleDesc, "hover" | "active">`, so an app passing either stops compiling - the changeset now records that a filled `<text>` takes mouse hits. Routing `<text>` through the same builder as `<div>` means `should_occlude` applies to it, so a label with an opaque background stops clicks reaching what is behind it, like an HTML element with a background. The old text builder inserted no hitbox at all. `element-identity.test.tsx` pins the new behaviour - the `<img>` element-state test explained the offscreen window wrong. The `focus: false` comes from gpui's `VisualTestContext::open_offscreen_window`, not from anything in `test_renderer.rs` Session: ses_fc10c354effe0X3SbzUKVPca44
The two commits it described are gone. `Make every RetainedTree mutation total` and `Report a rejected mutation instead of dropping it silently` were removed from this branch after review: every failure they guarded needs a hand-written call to the napi surface, React cannot produce any of them, and the cost did not match. - ~480 lines, an `is_ancestor` walk on every valid `appendChild`, and a public `createElement` return type change reaching napi, wasm, batching and the React types - a batch skipped an impossible op and logged it while a direct call threw, so the same mutation behaved differently depending on transport, and a skipped op left React believing a commit it does not have If the hardening comes back it should be one atomic prevalidation pass beside `resolve_styles`, with one policy for both entry points. Session: ses_fc10c354effe0X3SbzUKVPca44
Publish a new @gpuix/cli package with a goke-powered `gpuix new` command. The command downloads the official example app, replaces its workspace React dependency with the latest npm release, and installs the generated project with Bun. Update the quickstart, example documentation, workspace metadata, release notes, and CI publish order. Add a real GitHub-backed integration test and shell completion support. Session: ses_fb87b7e28fferSvDT71z3OadR1
Move @gpuix/cli into a focused publish job that runs its integration test and publishes without waiting for native platform test jobs. This keeps an unrelated Windows GPU example failure from blocking the platform-independent scaffolder. Session: ses_fb87b7e28fferSvDT71z3OadR1
Resolve the executable path before comparing it with import.meta.url. Package managers launch the gpuix binary through a symlink, so the previous direct comparison made @gpuix/cli@0.1.0 exit without parsing any command. Add a symlink regression test and release @gpuix/cli@0.1.1. Consume the original feature changeset because 0.1.0 is already published. Session: ses_fb87b7e28fferSvDT71z3OadR1
# Conflicts: # .github/workflows/ci.yml # examples/compile.ts # packages/native/src/custom_elements/anchored.rs # packages/native/src/custom_elements/code.rs # packages/native/src/custom_elements/diff.rs # packages/native/src/custom_elements/img.rs # packages/native/src/custom_elements/input.rs # packages/native/src/custom_elements/markdown.rs # packages/native/src/renderer.rs # packages/react/src/types/host.ts # zed
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
|
Moved to remorses#50. |
What this adds
The commits of feat/css-values-and-classname:
widthandheightaccept css lengths.corner-shapeand cursor keywords.heighttoauto, measures the content at the resolved width, and collapses from the height auto reached.rendertakes root options and exposes the renderer.keyworks on a built-in tag, and compiled example apps get signed.What I ran and saw
cargo testin packages/native at this branch: 291 passed, 0 failed.lineHeight: 20read as a multiple. Both fixes are in this branch, with tests.lineHeightnumber is pixels, as upstream reads it. Only a string gets the css multiple reading. A unit test covers each reading.What I did not verify