Commit Graph
350 Commits
Author SHA1 Message Date
Tommy D. Rossi 8d3ec09438 Fix Windows bun -e tests that imported files via URL.pathname.
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
@gpuix/native@0.8.0 @gpuix/react@0.8.0
2026-09-14 19:56:38 +02:00
Tommy D. Rossi 89b67995b2 release: @gpuix/native@0.8.0 and @gpuix/react@0.8.0
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 #37
Fixes #38
Fixes #39
Fixes #41
Fixes #46
Fixes #47
Fixes #59
Fixes #63

Session: ses_f5effe0ccffeXHPxxwP7cFDI4h
2026-09-14 19:46:44 +02:00
Tommy D. Rossi ef9b670b3f Add asChild support to Select and Combobox items
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
2026-09-14 12:22:27 +02:00
Tommy D. Rossi ca1a01dd17 Queue focus requests while the browser window opens
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
2026-09-14 12:22:04 +02:00
Tommy D. Rossi c98b414f38 Polish chat chrome so pickers, search, and focus work.
Select rows stole the click. GPUI has no bubbling, so a filled MenuRow
sat on top of SelectItem and the pick never ran. The fill now lives on
the item. Inner icons and labels set pointerEvents none.

Search stayed at 420px and closed on an outside press. The dim layer
used to take the hit, so onMouseDownOutside never fired. The listener
is on the card. Opening search autoFocuses the filter field.

The composer stays mounted across threads, so autoFocus only ran once.
New Task, a thread switch, and history now call focusElement.

The demo reply no longer bolds the last word, so markdown cannot put a
period on the next line.

Session: ses_f61007e8fffe0tRyMx0uRPPeCZ
2026-09-14 11:38:39 +02:00
Tommy D. Rossi 136d4f512b Make the chat example switch threads and reply to sends.
Each sidebar row now owns its own transcript. Clicking a thread paints
that conversation. New Task prepends a blank Today thread and clears
the composer. Sends append the user turn, then a demo assistant reply
that says this is the GPUIX chat demo. Retry rebuilds that last reply.

History arrows walk a stack of thread ids. Search filters titles.
Settings shows a short note and can cycle the frame overlay. The
inspector lists the current thread, model, reasoning, access, and
mode. The list filter keeps only threads for the selected project.

Tests cover thread switch, new task, demo reply, inspector, and
history. The live launch wait looks for GPUIX chat demo so the quoted
user text cannot match twice.

Session: ses_f61007e8fffe0tRyMx0uRPPeCZ
2026-09-14 11:04:15 +02:00
Tommy D. Rossi af9c8bbeb9 Stop the browser chat example from dying before WebGPU opens.
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
2026-09-14 11:04:09 +02:00
Tommy D. Rossi 9b0232374f Rewrite the homepage and README intro around GPUI search terms.
The Holocron title, docs.json description, and README lead now say GPUI
and React for native apps, not only native React on the GPU. The hero
tagline is GPUI and React / for native apps. Keywords on the homepage
frontmatter are GPUI, GPUI React, GPUI TypeScript, and native GPUI app.

The shader overlay uses a 0.7 alpha multiplier instead of a dark/light
opacity class, so the flare stays visible behind the new copy.

Session: ses_f64f287b1ffeD0rCw6d52oXfHk
2026-09-14 11:03:58 +02:00
Tommy D. Rossi 3920a5e74a Ignore the generated examples/idle-hello.cjs bundle.
That file is a 500kb CJS dump from a hermes/idle experiment. It is not source.
Leave it untracked so it cannot land in a commit by accident.

Session: ses_f61007e8fffe0tRyMx0uRPPeCZ
2026-09-14 11:03:46 +02:00
Tommy D. Rossi 0fac5c941e Merge remote-tracking branch 'origin/main' 2026-09-13 16:18:13 +02:00
Tommy D. Rossi 5388016f8c Add a homepage hero with Arizona Flare and the Holocron shader.
The docs index used to open on the README heading. Put an Above
block on the homepage like Holocron: one H1, GitHub and browser
buttons, and VideoBackgroundShader behind it.

Tagline is native React on the GPU. That is the product: React
and TypeScript paint through GPUI to Metal, DirectX, or Vulkan.
No Electron. No web view.

The heading uses Arizona Flare from mintlify.com/data. The shader
uses Holocron's hero-bg.mp4, cropped into the ring so the dots
fill the page-width banner. Color is a light orange at low
opacity. The canvas stays inside --grid-max-width; w-screen was
what spilled past the page.

Session: ses_f64f287b1ffeD0rCw6d52oXfHk
2026-09-13 16:17:11 +02:00
Tommy D. Rossi e47bd1ca30 Merge pull request #64 from KiteShell/feat/layer-shell-window-kind
feat: Open a window as a wlr-layer-shell surface via WindowOptions.layerShell.
2026-09-12 11:15:23 +02:00
dim-ghub bbaa2023e0 Open a window as a wlr-layer-shell surface via WindowOptions.layerShell.
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.
2026-09-10 15:00:01 -05:00
Tommy D. Rossi 18e695ed0e Size input and textarea rows from style.fontSize and style.lineHeight.
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
2026-09-10 16:48:47 +02:00
Tommy D. Rossi f24d27071d Point @gpuix/react/floating at a public barrel, and drop useFocusTrap from the docs.
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
2026-09-10 16:48:37 +02:00
Tommy D. Rossi a8141d6521 Point zed at in-place GPUI image updates.
Fast-forward the submodule from 5f672a20b1 to 81c99f816b on origin/gpuix.
That is remorses/zed#4: atlas uploads keep a same-sized tile on Metal,
WGPU, and DirectX, plus nearest-neighbor polychrome sampling and
Window::present_cached_frame.

GPUIX needed no code change. rust-toolchain stays 1.97.1. cargo check,
cargo test --lib, bun run build, and img.test.tsx all passed.

Session: ses_f7ec2f4c6ffekfPE4MgCvjex0R
2026-09-08 15:42:35 +02:00
Tommy D. Rossi 833d77f7e3 Add onFileDrop for Finder and OS file drops.
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
2026-09-08 13:48:22 +02:00
Tommy D. RossiandMateo M. 7cb458eac1 Speed up PNG encoding in debug native builds.
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
2026-09-08 12:03:08 +02:00
Tommy D. RossiandMateo M. 2e7799714d Keep an element's style when React hides it.
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
2026-09-08 12:03:00 +02:00
Tommy D. Rossi e02a607149 Remove useFocusTrap from @gpuix/react/floating.
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
2026-09-08 11:37:02 +02:00
Tommy D. Rossi eac7181c33 Return getElementBounds as { x, y, width, height }.
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
2026-09-07 18:03:17 +02:00
Tommy D. Rossi fa536086ec Parse GitHub Releases in checkUpdate. Drop latest.json.
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
2026-09-07 18:00:55 +02:00
Tommy D. Rossi 246a160fe9 Add checkUpdate() on @gpuix/native using GPUIX HTTP.
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
2026-09-07 17:52:09 +02:00
Tommy D. Rossi 846de94d8d Let Select follow Base UI, and export library APIs for gpuix-ui.
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
2026-09-07 17:49:32 +02:00
Tommy D. Rossi 1da015e53b Document packing a Hermes GPUIX app, shrinking the .node, and fetch.
The hermes-node guide now covers three extra ship steps:

1. Shrink the sidecar. napi build --platform --release already uses LTO.
   It does not pass --strip. strip -x drops the macOS arm64 addon from
   22 MB to 17 MB. Bare strip fails because NAPI / AppKit imports must
   stay. Dropping test-support and more LTO do not move the needle. The
   remaining 17 MB is GPUI. CI still ships the 22 MB published binary.

2. Wrap gpuix-hermes and the .node in a .app with cargo-packager. List
   the addon as a second binary so it lands in Contents/MacOS, where
   dlopen looks. Measured .app size is 34 MB. A Bun chat .app with no
   sidecar was 82 MB on the same machine.

3. hermes-node has no global fetch. node-fetch@2 over Node http works
   for plain HTTP. HTTPS throws because TLS is not implemented. Do not
   install node-fetch v3. It is ESM and Hermes cannot parse it.

Also link tmikov/hermes-node#11 for the macOS -dead_strip napi_* drop.
Update the docs-hermes-runtime changeset to mention packager, strip,
and the 34 MB .app.

Session: ses_f8427a2d6ffeLODe2UB6Cw8300
2026-09-07 15:25:49 +02:00
Tommy D. Rossi e892721f74 Add cargo-packager configs so a compiled GPUIX app gets a Dock icon.
A raw Mach-O has no icon. cargo-packager wraps the binary in a .app.
Pass an .icns, not a 1024 PNG. cargo-packager rejected a 1024 PNG with
No matching IconType.

examples/packager.json packs the Bun chat compile as one main binary.
hermes/packager.json lists gpuix-hermes as main and the .node as a
second binary, not a resource. Resources land in Contents/Resources.
Hermes dlopen looks next to the exe, which is Contents/MacOS.

README documents the flow: install cargo-packager, build AppIcon.icns
with sips + iconutil, then cargo packager --release --config packager.json.
formats is host OS only (app/dmg, nsis, appimage). On this machine the
Bun chat .app is 82 MB and the Hermes counter .app is 34 MB.

Ignore packager output: examples/bundle/, examples/AppIcon.icns,
hermes/bundle/, hermes/AppIcon.icns, and tmp-icons/. Do not track
generated hermes/chat.cjs.

Session: ses_f84b88b16ffePB3X7Fqzot47OY
2026-09-07 15:25:27 +02:00
Tommy D. Rossi 2e20b1b44e Restyle the runtime error overlay to match webpack.
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
2026-09-07 15:24:59 +02:00
Tommy D. Rossi 6b4be86952 Update the iOS plan: in-bundle dlopen is legal.
Callstack already loads Node-API addons on iOS Hermes via
hermes_napi_load_module of a signed .framework in the IPA.
A sidecar .node next to a CLI is still wrong. hermes-node is
desktop-only. v1 still prefers static libgpuix.a.

Session: ses_f841351faffe0EraOLQ9Tgpkps
2026-09-07 15:10:03 +02:00
Tommy D. Rossi 2d6e599c2b Document shipping a GPUIX app on hermes-node.
Add a Holocron guide at website/src/guides/hermes.mdx and link it from the
README. The page covers rebuilding hermes-node on macOS (the published
tarball dead-strips napi_*), bundling React to CJS with bun build, and the
measured ship set: 12 MB executable plus a 22 MB native sidecar.

A hermes/ prototype sits next to the docs so the same path can be rerun.

Session: ses_f84b88b16ffePB3X7Fqzot47OY
2026-09-07 14:18:43 +02:00
Tommy D. Rossi 9e0db5187c Apply accessibility props on virtual-list, anchored, and empty img
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
2026-09-06 15:34:54 +02:00
Tommy D. Rossi 8138d4c054 Expose GPUI accessibility through React role and aria-* props
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
2026-09-06 15:23:53 +02:00
Tommy D. Rossi 65fc1b130d Bump zod in the lockfile from 4.4.3 to 4.5.4.
No package.json change. The resolved entry moved during an earlier
publish pass.

Session: ses_fa2dcb95bffediF7Xu1SKakouC
2026-09-04 17:40:20 +02:00
Tommy D. Rossi 58397f4e58 Tell agents to leave bun --hot example windows running.
When someone asks to open an example so they can look, relaunching or
killing the session after a screenshot drops the window. A save remounts
React on the same host. bun --hot still drops useState; that is the
desktop remount path, not Fast Refresh.

Session: ses_f93fdf898ffeH0OlmQZz6Vz38t
2026-09-04 17:40:20 +02:00
Tommy D. Rossi 5f066f3c55 Show a stack overlay after a JavaScript runtime error.
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
2026-09-04 17:40:04 +02:00
Tommy D. Rossi 0bb3c94cda Add http(s) src support to img.
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
2026-09-04 17:39:31 +02:00
Tommy D. Rossi fa61d80ed8 Center single-line input text and clip it to borderRadius.
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
2026-09-04 17:38:32 +02:00
Tommy D. Rossi 4bea250745 Draw the input caret at about 75 percent of fontSize.
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
2026-09-04 17:38:03 +02:00
Tommy D. Rossi ff3c1a2bb7 Make Enter insert a newline in textarea unless onSubmit is set.
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
2026-09-04 17:37:25 +02:00
Tommy D. Rossi 95757bb10b Add a textDecoration style for underline and strikethrough.
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
2026-09-04 17:34:41 +02:00
Tommy D. Rossi e082247fb4 Add a plan for shipping GPUIX apps as iOS IPAs.
This documents the Hermes bytecode plus Node-API embedding path, so a
GPUIX React app can ship on the App Store without a JIT. It is planning
only; no runtime code changes in this commit.

Session: ses_f994c0082ffe4xUZ3lhqPnPgVK
2026-09-04 17:33:57 +02:00
Tommy D. Rossi cbc3de0a12 Show the mail.tax example as the README hero.
The homepage README now opens on the Superhuman-style mail client
sitting on the desktop wallpaper, not the chat window. Alt text is
`mail.tax example`. The launch snippet is `bun --hot mail.tsx`.

The committed JPEG is a window capture composited onto the Tahoe
wallpaper so the lake shows around the rounded window. Full-screen
capture cannot see other apps on this machine.

Also land the mail example itself: three panes, thread list, reading
pane, and the Framer newsletter assets.

Session: ses_f92fa1015ffeSFgm8TqqfEZnVb
2026-09-04 17:29:25 +02:00
Tommy D. Rossi 248752177e Keep the macOS window alive after a JavaScript runtime error.
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
2026-09-03 20:50:12 +02:00
Tommy D. Rossi 028f70af6d Merge remote-tracking branch 'origin/main' 2026-09-02 20:41:55 +02:00
Tommy D. Rossi 5700c9674a Document nonblocking macOS AppKit ticks and add a regression test.
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
2026-09-02 20:36:19 +02:00
Tommy D. Rossi 9b89506654 Merge pull request #43 from remorses/fix/embedded-primary-clicks
fix(native): deliver onClick from primary mouse-up
2026-09-02 16:50:14 +02:00
Tom X Nguyen bf98e0760c fix(native): deliver clicks from primary mouse-up 2026-09-02 21:33:09 +07:00
Tommy D. Rossi 2c807b2e13 docs: allow collaborator pull requests
Permit monotykamary, alongside remorses, to open pull requests without prior issue approval. Update both the top-level rule and the external contributor section so the policy stays consistent.

Session: ses_f9e107cc3ffeh2N63MF1cirKc1
2026-09-02 13:46:11 +02:00
Tommy D. Rossi 9b1def2f55 Use nonblocking embedded AppKit event pumping
Advance the GPUI fork to df3c9b7260. Embedded macOS hosts now drain only ready AppKit and Core Foundation work instead of entering NSApplication::run and waiting for a display-link wake.

This keeps Bun timers, sockets, promises, and PTY callbacks responsive between GPUIX frame-loop ticks.

Fixes #39

Session: ses_fa1101bf5ffexVlY2PD1MZjRSg
2026-09-02 00:40:11 +02:00
Tommy D. Rossi e948b20ddd Prevent live automation mouse input from leasing the root view
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
2026-09-01 22:58:09 +02:00
Tommy D. Rossi 1c4c67b409 Keep events live across Bun hot reloads
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
2026-09-01 22:57:49 +02:00