Bun on Windows cannot load `/D:/...` paths. fileURLToPath produces a real Windows path so the uncaught-exception and unhandled-rejection subprocess tests can import the renderer.
Session: ses_f5effe0ccffeXHPxxwP7cFDI4h
Consume the pending changesets into a minor release. Users get GPUI accessibility through role and aria-* props, Base UI Select wrapping with asChild, Finder file drops, GitHub Releases auto-update, Wayland layer-shell windows, http(s) img sources, textDecoration, textarea newlines, font-sized input rows, a runtime error overlay that keeps the macOS window alive, and several desktop and browser reliability fixes.
Bump the native Cargo crate, native npm package, React package, generated napi loader version checks, and Bun workspace lock to 0.8.0. Keep the published React dependency on @gpuix/native as workspace:^. CI is the only publish path for native platform binaries.
Fixes#37Fixes#38Fixes#39Fixes#41Fixes#46Fixes#47Fixes#59Fixes#63
Session: ses_f5effe0ccffeXHPxxwP7cFDI4h
GPUI uses a flat painted hit list and does not bubble clicks. A custom row
with its own background can therefore cover SelectItem or ComboboxItem and
prevent selection, hover state, and popup close behavior.
Add asChild to both item primitives through the existing renderSlot path.
The custom row becomes the host item itself, so it owns one native hit target,
one event id, the item ref, composed handlers, state styles, and disabled
selection behavior.
Migrate every chat picker to asChild. MenuRow now forwards its ref and host
props, while keeping its own row padding. Add end-to-end tests for filled
Select and Combobox rows and a chat regression assertion for menu row height.
Document when to use asChild, including the ref and host-prop forwarding
contract. Add a React patch changeset with the new public API.
Session: ses_f61007e8fffe0tRyMx0uRPPeCZ
Browser WebGPU creates its public window handle before the internal GPUI window can accept updates. Calling focusElement from a React mount effect therefore threw GPUIX web window is not ready even though WEB_WINDOW was already present.
Store the latest browser focus target outside the window update path. Consume it during GpuixView render, after the window is active, and apply it after focus handles are synchronized. Explicit focus keeps precedence over autoFocus.
Use the same deferred focus state on desktop when a requested host element does not have a focus handle yet. Clear an older pending request whenever a newer request focuses an existing handle, so stale state cannot steal focus on a later render.
Document startup queuing and the latest-request-wins rule. Add a native patch changeset for the browser API behavior.
Session: ses_f61007e8fffe0tRyMx0uRPPeCZ
https://gpuix.dev/chat-example/ stayed on the loader for two reasons.
The error overlay read process.platform at module load. A browser has
no process, so that threw before Wasm started. OVERLAY_MONO is now a
CSS font stack. readMacCpuThrottle() treats a missing process as no
THROTTLE. render() only calls process.exit when process exists.
createRoot() also called setWindowKeyEvents before GPUI finished
opening the web window, which threw GPUIX web window is not ready.
The wasm renderer now queues those listeners the same way it already
queues the debug overlay, then applies them when start_web_app builds
GpuixView.
The production chat bundle copies examples/web.css, and the website
serves it as /chat-example/web.css so the loader styles load.
Session: ses_f64d18bc8ffeysXOZPWSnfTQWv
The GPUI fork in zed/ already implements layer-shell at the Rust level
(WindowKind::LayerShell, layer_shell::LayerShellOptions), but nothing
reached JS: WindowOptions had no way to ask for it, so @gpuix/react
could only open normal floating windows. A panel, dock, notification
shade or wallpaper needs an edge-anchored surface with an exclusive
zone, which is exactly what layer-shell is for.
Add to the napi WindowOptions:
- layerShell?: LayerShellOptions - namespace, layer (background |
bottom | top | overlay), anchor (top / bottom / left / right, two
opposite edges stretch that axis), exclusiveZone, exclusiveEdge,
margin ([top, right, bottom, left]), keyboardInteractivity (none |
on-demand | exclusive). All optional; the default anchor is a top
bar (top | left | right) and the default layer is top.
- appId?: string - Wayland app_id / X11 WM_CLASS, useful on its own
and needed for a compositor to target a layer surface by rule.
to_gpui_window_options maps layerShell onto WindowKind::LayerShell and
drops the titlebar. The WindowKind variant only exists on a
wayland-enabled Linux gpui build, so the mapping is
cfg(target_os = "linux"); on macOS and Windows layerShell is ignored
and appId still applies. init_threaded opens a layer surface at the
origin instead of Bounds::centered, since the compositor places it
from its anchor.
index.d.ts is hand-edited rather than regenerated: the @napi-rs/cli
in node_modules emits a much smaller index.d.ts than the committed
one (it drops every doc comment and all of TestGpuixRenderer's
methods), so a full `napi build` regen here would be a large
destructive diff unrelated to this change. The added block matches
the shape napi produces for these structs.
Tests in renderer.rs cover the mapping (anchor union, exclusive zone
and edge, margin, keyboard interactivity, titlebar dropped) and that a
window with no layerShell stays Normal with a titlebar.
Verified on Hyprland (3 monitors): a @gpuix/react app calling
render(<Bar/>, { layerShell: { anchor: ['top','left','right'],
exclusiveZone: 34 } }) opens a 34px top bar, hyprctl reports the
kite-panel layer surface and reserved: [0, 34, 0, 0], and a React
setInterval clock re-renders live on the surface.
layout_text used window.line_height() after Taffy measure. Parent
with_text_style is gone by then, so the row was always 16×φ no matter
what React passed. A 28px font still sat in a 26px box. Issue 63.
Use the TextStyle captured during request_layout instead:
style.line_height_in_pixels(rem_size). Without an explicit lineHeight
that follows GPUI default leading, so a larger fontSize grows the box.
Pass lineHeight to set the row in pixels. minRows and maxRows still
multiply that height. An explicit height still wins.
Tests assert:
- textarea height equals lineHeight
- minRows=3 with lineHeight=30 is 90
- fontSize=28 with no lineHeight is 45 (28×φ)
- a single-line input honors lineHeight too
Session: ses_f79437cb5ffennyb8lTdU6KH7Q
The package used to export ./floating from dist/components/floating.js, which is the internal module for Select, Combobox, and Tooltip. That surface listed useControllableState and useFocusTrap as public helpers.
useFocusTrap is already gone from the implementation. It listened for Tab on a panel ref and forwarded it to focusNextWithin / focusPreviousWithin. Those renderer methods are the real scoped-tab API. A thin hook on top of them also fought a window-level Tab handler, because both moved focus and neither could stop the native key.
src/floating.ts is now the published entry. It re-exports FloatingLayer, renderSlot, and the style helpers libraries actually need. package.json points ./floating at dist/floating.js.
Docs and the pending Select changeset now match that:
- trap Tab by owning onKeyDown, then calling focusNextWithin / focusPreviousWithin
- do not also call focusNext from an element onKeyDown when a window Tab handler is already installed
- the floating table lists FloatingLayer and renderSlot only
Session: ses_f7fa3a82effee2tmI4o1tW6YzS
GPUI already turns an OS FileDropEvent into an ExternalPaths drag and
delivers on_drop to the hovered hitbox. Wire that onto host elements the
same way onClick works. Nested targets keep working because GPUI stops
propagation after a matching drop.
```tsx
<div
onFileDrop={(event) => openFiles(event.paths ?? [])}
style={{ width: 400, height: 300 }}
/>
```
event.paths is string[] of absolute Unicode filesystem paths. event.x and
event.y are the drop point in window pixels. An empty drop, or a drop that
contains a non-Unicode path, does not fire. to_string_lossy would invent a
path that does not exist on disk, so conversion uses Path::to_str instead.
Works on div, text, img, svg, input, textarea, code, markdown, diff, and
anchored. virtual-list is not a host div and does not take this event; wrap
it. Desktop only. Tests simulate Entered then Submit through GPUI.
Session: ses_f7f7c408bffe5d59D85FJmrsKT
capture_screenshot encodes a PNG on every visual test. At opt-level 0
the encoder is slow enough that vitest kills the worker. Raise
opt-level to 3 for png, image, fdeflate, flate2, and miniz_oxide in
profile.dev. Release builds stay as they are.
Co-authored-by: Mateo M. <hello@mateo.sh>
Session: ses_f7fa131f8ffe300STsqQLIywNJ
hideInstance used to send `{ visibility: hidden }` and drop every other
declaration. Suspense retries and hidden Activity trees then lost their
layout box. The hide now keeps the existing style and only adds
visibility: hidden. Hover and active still drop, so a hover style cannot
paint an element React asked to hide. unhideInstance restores the style
React passed in.
The host-config test drives hideInstance by hand. The pinned
react-reconciler is older than the React that exports Activity, so a
normal render cannot reach that path here.
Co-authored-by: Mateo M. <hello@mateo.sh>
Session: ses_f7fa131f8ffe300STsqQLIywNJ
The hook listened for Tab on a panel ref and forwarded it to
focusNextWithin / focusPreviousWithin. Those renderer methods are the
real scoped-tab API. The hook was a thin wrapper that also fought a
window-level Tab handler, because both moved focus and neither could
stop the native key.
Tests now call focusNextWithin and focusPreviousWithin on the panel id
directly. That matches how an app should trap Tab: own the key, then
ask the renderer to walk GPUI's painted tab map inside that subtree.
Also switch the last getElementBounds array index in this file to
named fields (.x / .y). The previous bounds commit missed this click.
Session: ses_f83692052ffeqSnTB42kNHE7ZK
The previous array form could not grow. Extra fields such as scroll
offsets would have to sit at magic indexes. Native napi, wasm, the test
renderer, automation, and every call site now use named fields.
Session: ses_f83f90209ffeVmkdjhaBehDTP7
The feed file was a workaround. GitHub already has tag_name, assets, and
a sibling .sig. checkUpdate now hits api.github.com:
GET https://api.github.com/repos/OWNER/REPO/releases/latest
Rewrite github.com/OWNER/REPO, /releases/latest, and
/releases/latest/download/... to that API URL. Match the host bundle
(*.app.tar.gz, *_{arch}.AppImage, *_x64-setup.exe / *_arm64-setup.exe)
and GET {name}.sig. Missing sibling signature is an error. Infer format
from the asset name.
Keep packager JSON as a silent fallback when the body already has
version plus url or platforms. Do not document that path.
CI uploads only what packager writes: the signed bundle and its .sig.
README and the changeset now show GitHub Actions plus
checkUpdate against https://github.com/OWNER/REPO/releases/latest.
Session: ses_f837ecfbeffeynBTQx5bKyvN9d
Port cargo-packager-updater 0.2.3 into packages/native/src/updater.rs
instead of depending on the crate. The original crate pulled crates.io
reqwest blocking, a second TLS stack next to Zed zed-reqwest. HTTP now
goes through reqwest_client, the same client <img> already uses.
JS:
import { checkUpdate } from '@gpuix/native'
const update = await checkUpdate('0.1.0', {
endpoints: ['https://github.com/OWNER/REPO/releases/latest/download/latest.json'],
pubkey,
})
if (update) await update.downloadAndInstall()
The feed is 204 or a latest.json with platforms keys like macos-aarch64.
downloadAndInstall() replaces packaged files and does not relaunch.
Desktop only. The browser wasm build does not export this.
Original: https://github.com/crabnebula-dev/cargo-packager/blob/v0.2.3/crates/updater/src/lib.rs
Session: ses_f83f922a9ffe9QbIBVQESIXTQM
Select no longer walks children by `child.type === SelectItem`. Root `items`
is optional and only a label lookup for SelectValue while the popup is
closed. Keyboard nav and clicks read mounted SelectItem children through
registerItem. Disabled lives on Item. A styled wrapper around Item works.
Without items, SelectValue shows the raw value.
Register items in useLayoutEffect, not a callback ref, so a middle item
can re-render without jumping to the end of the list. selectValue rejects
unmounted and root-disabled items. A late-mounted selected item becomes
the highlight.
Native:
- getFocusedElementId, focusNextWithin, focusPreviousWithin walk GPUI's
painted TabStopMap via focus_next_among / focus_prev_among
- visibility: hidden maps to GPUI invisible()
- getElementBounds is typed on NativeRenderer
JS:
- export Props, GpuixTheme, and host prop types from @gpuix/react
- export FloatingLayer, useFocusTrap, and friends from @gpuix/react/floating
AGENTS.md: built-in headless controls follow Base UI's Root-data vs
children split.
Fixes#59
Session: ses_f83f90209ffeVmkdjhaBehDTP7
The overlay used to be a dark red card titled Runtime error, with the
raw stack dumped as a list of 13px lines and a muted Reload button.
It now follows the webpack-dev-server overlay:
- title is Uncaught runtime errors:
- message sits in a red-tinted scroll pane above the frames
- frames keep webpack-style indent and a platform mono font
(Menlo / Consolas / DejaVu Sans Mono)
- overlay is near-black (#000000e6) with a bright red Reload button
- empty and duplicate Error: message lines are stripped so the
scroll pane is frames only
console.error no longer dumps a second inspect object. It prints the
formatted stack once.
The existing overlay test now looks for the new title. A second test
throws a fake MailMarkdown stack, asserts the scroll pane, and writes
runtime-error-overlay.png so the layout can be checked from a screenshot.
Session: ses_f941afdedffezh2lODSRNO04A8
Oracle found three contract holes in the first a11y slice.
- `<virtual-list>` never called apply_accessibility. GPUI List had no
id, so the scroller could not enter the AX tree. The zed submodule
now forwards List id and role, and GPUIX sets both
- `<anchored>` skipped custom_surface and never applied role or
aria-label
- empty or invalid `<img src>` returned a fallback before Image role
and alt
Tests cover those three paths, plus interpolated `<text>` joining into
one Label.
Session: ses_f89a78b8cffe4lrrAfJ8Uz4sH1
GPUI already talks to the macOS AX tree, Windows UIA, and Linux AT-SPI
through AccessKit. GPUIX never set .role(), so VoiceOver saw an empty
window. A node is in the tree only with both an id (already set) and a
role.
Prop names match React DOM. Role values are ARIA tokens.
```tsx
<div
role="button"
aria-label="Delete note"
aria-id="notes.delete"
onClick={remove}
>
Delete
</div>
```
- `none` / `presentation` produce no node
- `<text>` defaults to Label, `<input>` to TextInput, `<textarea>` to
MultilineTextInput, `<img alt>` to Image
- onClick registers AccessKit Click, so VoiceOver Press fires the same
JS handler
- Tests dump GPUI debug_a11y_tree_json after paint
The zed submodule picks up two GPUI hooks: tests can activate the a11y
tree without a screen reader, and Img / Svg now report .role() instead
of dropping it.
Browser / wasm has no AccessKit adapter. These props are no-ops there.
Fixes#47
Session: ses_f89a78b8cffe4lrrAfJ8Uz4sH1
A throw used to keep the window alive but empty. render() now paints the
error message and stack, and Reload remounts the last tree.
Unhandled React render errors, uncaughtException, and unhandledRejection
all take this path. The overlay is scheduled on a microtask keyed to the
failing root, so a later remount does not paint a stale overlay. Process
handlers uninstall on resetRender.
Save under bun --hot still remounts too.
Session: ses_f941afdedffezh2lODSRNO04A8
Desktop GPUI defaults to NullHttpClient, so a remote URL never loaded.
Install Zed reqwest_client on native apps so img(SharedUri) can fetch in
the background. Wasm already has fetch.
A definite width and height now also set aspect_ratio. GPUI otherwise
fills that from the bitmap after decode and jumps the box. Tests use a
FakeHttpClient that returns the same SVG as the disk fixture, so the
dispatcher never parks on a real network.
Set both width and height. Failed loads still show the existing
fallback. A loading URL paints an empty box of the declared size.
Session: ses_f9810208bffeUeHRs8rtxe7KaD
A search pill with extra height used to paint text at the top of the box,
and rounded corners did not clip the glyphs. HTML inputs center and clip.
Single-line inputs now use items_center when given extra height, and
overflow_hidden when borderRadius is set. Document the no-default-padding
gotcha next to the other CSS-looking style traps.
The padding coverage test checks that paddingLeft actually insets painted
text, which is how a pill wrapper is supposed to work.
Session: ses_f9922154effe0jqPuJbXGeDNke
A 13px composer used to paint a bar as tall as the line box, so it stuck
out above and below the glyphs. Default leading is phi, and cap height is
about 0.75em.
The caret now uses that ratio, clamps to line height, and sits in the
middle of the line. IME bounds use the same geometry.
Session: ses_f97dbaec6ffehVfUKGx9R6Qia8
Plain Enter used to always fire onSubmit on both input and textarea, so a
normal multiline editor could not insert a newline, and Enter did nothing
when no onSubmit listener was set.
Textarea now uses a separate key context. Enter and Shift+Enter insert a
newline. When onSubmit is present, Enter submits and Shift+Enter still
inserts a newline. Input still submits on Enter.
Adding or removing onSubmit after mount swaps the key context, so a
composer that only submits after the user types still gets the right
Enter binding.
Fixes#46
Session: ses_f998cca76ffexsRUJHhI2AdZeA
GPUI already paints those decorations. This wires `"underline"`,
`"line-through"`, and `"none"` through StyleDesc, apply_styles, and the
React StyleDesc type so a React tree can request them.
Visual coverage compares decorated text against the same glyphs with no
decoration so a missing native apply cannot hide behind a type-only
change.
Session: ses_f9922154effe0jqPuJbXGeDNke
On macOS, JS owns the event loop. startFrameLoop calls renderer.tick()
about every 8ms, and that tick pumps AppKit. A throw that escaped the
timer stopped the next setTimeout, so the window froze. bun then often
exited, which looked like a crash.
startFrameLoop now catches errors from tick() and still schedules the
next pump. Native event callbacks catch throws from React handlers so
they do not become napi_fatal_exception. render() installs
uncaughtException and unhandledRejection listeners so bun stays alive,
and it starts the frame loop before the first React flush so a throw
during mount still leaves AppKit pumping. The loop is not restarted on
remount.
The error is logged. There is no auto-reload. Save under bun --hot to
remount.
Session: ses_f97da32b5ffejnj7pvF2xzbkXL
GpuixRenderer.tick() already drains only ready AppKit events. The README
now says that, and events.test.tsx checks that 30 idle ticks return in
under 200ms so Bun timers and sockets can run between frames.
Fixes#39
Session: ses_fa1101bf5ffexVlY2PD1MZjRSg
Dispatch click, hover, wheel, and drag input through AnyWindowHandle on desktop and browser hosts. Mouse listeners can now update GpuixView during normal GPUI event delivery without triggering a nested entity lease and aborting the process.
Cover the production renderer with a child-process automation test that clicks the live chat sidebar, waits for the state change, then drives keyboard input through the composer.
Build and verify both the release native addon and the WebAssembly renderer so the ownership fix stays consistent across every live host.
Issue: #38
Session: ses_fa1aaf89dffezIsneAEezbytzc
Persist renderer-owned containers, element ID allocators, and window event IDs across JavaScript module evaluation. The native renderer callback can now route into the active React root after bun --hot replaces the module graph.
Bind render-level onEvent callbacks to the owning root. Stale element and window events are rejected before they can reach either React handlers or the replacement root observer.
Extend the real bun --hot child-process regression to click both before and after reload and confirm that remounted roots keep unique IDs.
Issue: #37
Session: ses_fa1aaf89dffezIsneAEezbytzc
Consume the seven pending changesets into a minor release. Users get native two-stop gradients, in-memory image data URLs, application-owned Tab handling, renderer-level keyboard callbacks, consistent test-renderer exports, correct Windows DPI scaling, clean last-window exit on Windows and Linux, and a frosted-glass window example.
Bump the native Cargo crate, native npm package, React package, generated napi loader version checks, and Bun workspace lock to 0.7.0. Keep the published React dependency on @gpuix/native as ^0.7.0 through workspace:^ resolution.
The release validation covers 221 Rust tests, 370 React renderer tests, 47 example tests, the starter app typecheck, CLI tests and build, package lint, release native and React builds, and both native and React tarball contents.
Fixes#30Fixes#31Fixes#32Fixes#35Fixes#36
Session: ses_fa2dcb95bffediF7Xu1SKakouC
Remove GPUIX's process-wide Tab and Shift+Tab bindings so focused editors and terminals receive the native key events directly.
Add renderer-level onKeyDown and onKeyUp callbacks, plus focusNext and focusPrevious wrappers for applications that want to install their own traversal policy. Keep browser focus on GPUI's hidden keyboard element after Tab without turning that platform safeguard into focus traversal.
Give each mounted root a key-event generation so queued callbacks cannot cross a remount. Disable native root listeners on unmount and preserve the active root when a second root is rejected.
Update the focus documentation, release note, native declarations, and end-to-end tests for element delivery, renderer delivery, explicit traversal, remount safety, and root ownership.
Session: ses_fa3b8830cffeGdTgarIef6r2Kn
Decode base64 and percent-encoded image data URLs when the src prop changes, then retain the decoded bytes through gpui::Image while preserving filesystem path loading and image element identity.
Reuse the data URL decoder for SVG sources, document the API, and cover the native render path with a screenshot comparison against the same filesystem image. Add a release changeset that references issue #35.
Keep a TODO at the JSON boundary for replacing base64 transport with binary mutations so large image payloads do not decode during paint.
Session: ses_fa3e66794ffeXNYejd15WllDnE
Two Windows host bugs in the napi `.node` path, which does not get
Zed's executable manifest or AppKit last-window-close wiring.
DPI (#31): GPUI declares Per-Monitor V2 only in the host exe
manifest (zed#8936). node.exe and bun.exe have no such manifest, so
Windows bitmap-stretched the window at >100% scaling. Call
SetProcessDpiAwarenessContext(PER_MONITOR_AWARE_V2) on the gpuix-ui
thread only, before WindowsPlatform creates an HWND. Never on the
Node/bun thread: the fallback is SetThreadDpiAwarenessContext, which
would change the host. If process awareness is already locked, the
UI thread still requests V2.
Last window (#32): GPUI already quits the UI thread on last window
close (QuitMode::LastWindowClosed is the Windows default). Node/bun
stayed alive because tick() always returned true and requiresTick()
was macOS-only, so JS never process.exit(0). Make tick() observe an
atomic that clears when Platform::run returns, and start the same JS
frame loop on Windows/Linux. Stop run_ui_commands after the last
window is gone so later window.update does not log `window not found`.
Fixes#31Fixes#32
Session: ses_fa9254252ffe4soIYSYl5CI1dp
index.d.ts already declared the class on every platform, so Linux
type-checked `new TestGpuixRenderer()` and then threw
`TypeError: TestGpuixRenderer is not a constructor`.
Keep the class defined in every napi binary. On macOS and Windows with
test-support, construction still opens the GPU test window. Everywhere
else, including Linux `--no-default-features`, the constructor throws
and explains that wgpu cannot read a rendered image back yet.
Export `hasTestGpuixRenderer()` so `@gpuix/react` can skip GPU tests.
`hasNativeTestRenderer` must not become true just because the class
exists.
Fixes#30
Session: ses_fa9254252ffe4soIYSYl5CI1dp
Expose two-stop linear gradients through the React style.background API with typed stops, CSS angle direction, and sRGB or Oklab interpolation.
Translate the structured value through the retained style pipeline into GPUI backgrounds. Reuse the same resolution for painting, mouse occlusion, and anchored overlay fill detection so transparent gradients keep the existing interaction rules.
Add native translation and transparency tests, an end-to-end GPU screenshot test for gradient direction, public documentation, and a minor changeset for @gpuix/native and @gpuix/react.
Session: ses_fae3bda32ffe3CrFWAX0OEwcI6
Run cargo fmt so rustfmt --check is clean. The 0.6.0 commits left
long match arms, asserts, and struct literals over the line limit
in renderer, retained_tree, search, selection, style, input, and
the serde bench.
No behaviour change.
Session: ses_fb1f5cd3fffeAe9VxtTeynTdjA
Consume the pending changesets into a minor release. Users get
stable GPUI identities on every host surface, background window
launch, live desktop fill/press, pixel-stable virtual-list
anchors, a single applyBatch transport, one prebuilt target per
OS, and CRLF-safe <code> rows.
CI publishes with bun, not npm, so workspace:^ never ships as a
literal string. React still rewrites @gpuix/native to the current
native version before publish, because bun pack otherwise keeps
the previous lockfile caret (^0.5.1 does not include 0.6.0).
The infinite-chat Windows assertion no longer requires the
viewport to be empty of message ids. On Windows the short window
still showed three ids after wheeling into the void; the test now
checks that the list anchor is on the trailing edge instead.
Fixes#25
Session: ses_fb1f5cd3fffeAe9VxtTeynTdjA
A drag held in a virtual-list edge band rebuilt the tree every 24 ms after
the list could no longer move. The timer now stops unless the list or the
selection actually changed.
Double-click in input and textarea selects the word under the pointer.
Triple-click still selects the whole value. Neither click arms a drag that
would collapse the selection.
Also:
- shrink the edge band on short lists so the first and last rows stay
selectable
- let a single-line drag past the right edge reach the end of the value
- keep frame mouse listeners on a weak GpuixView handle
- make advanceTime drive the GPUI test dispatcher, not the motion clock,
and deliver any events those timers emit
The 700 ms undo split now has a test that advances the clock.
Session: ses_fb7aaa3c7ffeM7g9AoFvJM7umM
Selection now tracks Comet's later generic fixes instead of the original
single-element port.
Soft-wrapped selection and highlight washes include the first glyph on the
next visual row. GPUI reports a wrap boundary with upstream affinity, so
the old range_rects started the continuation wash after that glyph.
A drag that starts in a virtual list keeps its spans after the anchor row
unmounts. Move and mouse-up listeners live on the frame, not on each text
run, so virtualization cannot drop the listener that owns the drag.
Dragging near a list edge scrolls the list and resolves the pointer against
each newly painted frame.
Input and textarea follow generic Comet composer behavior only:
- double-click or triple-click selects the whole value and does not arm
a drag that would collapse it
- dragging a textarea selection past the visible box autoscrolls
- adjacent typing or deletion undoes as one step for 700 ms, with a
200-step history cap
- one IME composition still records one undo snapshot
Document selection, React text groups, pending iOS presses, and
per-renderer state stay GPUIX-owned. Comet's global singleton, mentions,
and composer chrome are not ported.
Tests cover wrap geometry, forward and backward virtualized drags,
list edge scrolling with advanceTime, and coalesced undo.
Session: ses_fb7aaa3c7ffeM7g9AoFvJM7umM
Reduce the renderer mutation contract to one applyBatch call per React
commit across napi, wasm-bindgen, and the GPU-backed test renderer.
- replace the dynamic batching Proxy with an explicit commit-phase facade
- keep application commands on the original renderer instance
- remove direct create, append, style, text, prop, and commit entry points
- remove the unused removeChild and setCustomPropValue wire operations
- require raw JSON style and custom-prop values and boolean event flags
- regenerate native declarations and update architecture documentation
- update serialization benchmarks for the smaller protocol
- add a minor changeset for @gpuix/native and @gpuix/react
Validated with 204 Rust tests, 359 React tests, 47 example tests,
6 starter-app tests, release native and Wasm builds, lintcn, and both
serialization benchmark stages.
Session: ses_fb7d2b638ffen9PHc0FVU3DpAv
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
## 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
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
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
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
`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
Dispatch synthetic key events through the focused GPUI window input pipeline on macOS and threaded desktop platforms. Expose the generated napi methods needed by launch automation.
Add live composer integration coverage and document fill and press behavior. Replace active Waku branding with GPUIX content and accurate React-to-GPUI architecture details.
Session: ses_fc015b322ffedMaROvgdRpVsuI
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