Commit Graph
11 Commits
Author SHA1 Message Date
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 de1c74aa7d feat: serve the infinite-chat example in the browser at /infinite
`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
2026-08-27 17:23:33 +02:00
Tommy D. Rossi 291b92d7da website: deploy the WebGPU chat example
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
2026-08-26 22:12:32 +02:00
Tommy D. Rossi 7d6e1f97ed Serve the browser example through Bun's dev server with React Fast Refresh
An edit to `examples/chat.tsx` now hot-updates the running page instead of
reloading it. Components swap in place and `useState` survives, so the GPUI
canvas, the composer text and the scroll position all stay where they were.
A reload re-downloads a ~19 MB Wasm module; this avoids that entirely.

`scripts/web.ts` drops the `Bun.build` step and the hand-written static file
map. It now registers `examples/web.html` as a Bun HTML route with
`development: { hmr: true }`, so Bun bundles the page, watches the graph and
applies the React Fast Refresh transform.

Fast Refresh never talks to a renderer directly. It goes through the React
DevTools global hook:

    Bun HMR runtime
      -> refreshRuntime.injectIntoGlobalHook(window)
    @gpuix/react reconciler.ts
      -> reconciler.injectIntoDevTools()
      -> hands the hook scheduleRefresh + setRefreshHandler
    react-refresh
      -> performReactRefresh() -> scheduleRefresh(root)

That injection already existed. What was missing was a bundler that runs the
transform, and a test proving the wiring holds.

`examples/web-chat.tsx` must NOT call `import.meta.hot.accept("./chat", ...)`.
Bun runs an importer's dependency-accept callback even when the imported module
already self-accepted for Fast Refresh, so the callback remounted the tree on
top of a successful refresh and wiped every hook. That looks exactly like Fast
Refresh being broken, and it is not.

The Wasm half is a singleton: `WebGpuixRenderer::init` fails with "GPUIX web is
already running" once its thread-local app exists, and gpui_web appends its own
canvas to <body>. Living in node_modules is not what protects it, because Bun
bundles it into the same client registry. What protects it is that Bun re-runs
only the changed module and then walks upward through its importers, so an
unchanged dependency stays evaluated and cached.

packages/native/.cargo/config.toml links with `--shared-memory`, so the
generated `WebAssembly.Memory` is `shared: true` and the document must be
cross-origin isolated. `Bun.serve` has no way to add headers to an HTML route
(oven-sh/bun#16873), so the bundled document is registered on a private path and
re-sent from `/` with COOP + COEP. Only the top-level document needs them:
`require-corp` constrains cross-origin subresources, and every asset the dev
server emits is same-origin.

`packages/react/src/__tests__/fast-refresh.test.tsx` mounts through the GPU test
renderer, clicks to set state, swaps the component family, and asserts the new
output keeps the old state. It fails when `injectIntoDevTools()` is removed.

This matters because the failure mode is silent. Without the injection there is
no error and no page reload: Bun still accepts the update and still calls
`performReactRefresh()`, which iterates zero mounted roots and schedules
nothing. The bundle changes and the painted UI stays stale.

Do not assert on the return value of `injectIntoDevTools()`. It ends in
`hook.checkDCE ? true : false` and react-refresh installs no `checkDCE`, so a
working injection still reports `false`.

- `--build-only` / `--rebuild` / auto-build-if-missing in scripts/web.ts, and
  the missing check tests both wasm-bindgen outputs, not just the `.wasm`
- `examples/tsconfig.json` gets `noEmit: true`; nothing there is ever compiled
  by tsc and a stray `tsc -p .` littered a `.js` next to every `.tsx`
- `@types/bun` as a root devDependency plus a `typeRoots` entry so
  `scripts/tsconfig.json` types `Bun.serve` and the `*.html` bundler import
- README documents that automation locator bounds are canvas pixels, not CSS
  pixels. Do not use `window.devicePixelRatio`; a tool can override the viewport
  scale after GPUI has sized its canvas. Use `canvas.width / canvas.clientWidth`

Validated in Chrome with playwriter: text edits and a module-level colour
constant edit both repaint the canvas with state preserved, and a revert
restores a byte-identical screenshot.

Session: ses_fc233909fffeHxAGjrf2jkWSet
2026-08-26 17:59:44 +02:00
Tommy D. Rossi 7d45d3681a Make GPUIX installable: quickstart, todo example app, and a key that typechecks
Installing the published packages and writing an app was not possible by
following the README. Three things blocked it.

**1. `key` failed on every GPUIX element**

    error TS2322: Type '{ key: string; ... }' is not assignable to type 'Props'.
      Property 'key' does not exist on type 'Props'.

Any list built with `.map()` broke on the first `tsc` run. `key` was expected
to arrive through `JSX.IntrinsicAttributes`, but TypeScript 5 ignores that
member for intrinsic elements. React's DOM types work only because
`DetailedHTMLProps` already carries `key`.

`key` now sits on `Props`, next to `ref`, and on `VirtualListShared`, the one
element prop type that does not extend `Props`. Verified with a probe over all
12 intrinsic elements plus `motion.div`, `VirtualList`, Select, Combobox and
Tooltip.

`jsx-dev-runtime.d.ts` also re-exported `jsx` and `jsxs` from
`react/jsx-dev-runtime`, which exports only `jsxDEV`. The declarations now
match `jsx-dev-runtime.js`.

Nothing caught this because Bun never typechecks and the build tsconfig
excludes `src/__tests__`. CI now runs `bun run typecheck` in `example-app`,
whose `app.tsx` maps a keyed list inside a `<virtual-list>`.

**2. A hello world installed 254 MB**

`@gpuix/native` packed all six platform binaries into the main tarball through
a `*.node` glob, on top of the six per-platform packages that
`optionalDependencies` already resolves.

    node_modules                       254M
    |- @gpuix/native                   185M  <- all six, never loaded
    |- @gpuix/native-darwin-arm64       23M  <- the one that loads
    |- @gpuix/react                    544K

The glob is gone. `index.js` still resolves the platform package at runtime.

**3. The README never said how to install**

It opened with the architecture and the mutation protocol. There was no
`bun add` anywhere, and no mention of `jsxImportSource`, which is required:
without it TypeScript uses DOM types and `<virtual-list>`, `<markdown>`,
`<code>` and `style.hover` all fail.

A **Quickstart** now sits between the hero and Examples: install, tsconfig,
entry file, `bun --hot`, `bun build --compile`. Nothing was removed. The
Building section says up front that it is for working on GPUIX itself.

**example-app/**

A todo app to copy, in one file, using the Waku palette from `chat.tsx`.

| Script | What it does |
|---|---|
| `bun run dev` | `bun --hot app.tsx` |
| `bun run build` | standalone binary in `dist/todo` |
| `bun run web:build` | browser renderer, needs nightly Rust |
| `bun run web:dev` | browser bundle served with COOP/COEP |
| `bun run screenshot` | drives the app through the automation client |
| `bun run typecheck` | `tsc --noEmit` |

It shows `<virtual-list>`, a native `<input>`, `motion.div`, tinted `<svg>`,
native `hover` and `active`, and `testId` hooks. `scripts/screenshots.ts`
regenerates `docs/images/todo-app.png` from it.

Verified on all four paths: `bun --hot`, the 77 MB binary, the browser build at
`localhost:4188`, and a clean `typecheck`.

Two things worth remembering, both now in comments:

- the browser build embeds only IBM Plex Sans and Lilex, so `Helvetica` falls
  back to a monospace and the family has to differ per target
- `maxWidth` on a `<virtual-list>` row does nothing; the list lays each row out
  at list width, so the column cap goes one level in

`bun.lock` carries a few entries from unrelated in-flight work in `examples/`.
They are inert and prune on the next install.

Session: ses_fc1c928b0ffeeHIfdRpxtE0QaV
2026-08-26 15:55:10 +02:00
Tommy D. Rossi bcfcaa92f8 Add GPUI browser rendering
Compile the retained GPUIX renderer for wasm32-unknown-unknown and expose the React mutation interface through wasm-bindgen while keeping napi-rs as the desktop host.

Publish the browser loader and Wasm asset, add the ChatGPT browser example and isolated development server, and prepare the release workflow with the required Rust and wasm-bindgen toolchains.

Use GPUI Web bundled IBM Plex Sans and Lilex fonts, add portable raw SVG source rendering, and embed the chat icons so the browser canvas matches the native layout.

Validation: 86 Rust tests, 12 example tests, 3 native image/SVG tests, React builds and package lints, Bun package dry-run, and Playwriter WebGPU visual checks.

Session: ses_fd2303a27ffeNl7JAmV7905R7z
2026-08-24 19:29:14 +02:00
Tommy D. Rossi 574b76058b Let the chat example send a turn and scroll it into view.
Composer submit used to only clear the draft. Enter and the send
button now append a user turn and scroll the virtual-list to that
row. The test types hello, submits, and asserts the last row.

Markdown turns now go through SafeMdxContent. Parse results are
cached by source so a 5k remount does not re-parse the same
blocks. The live window starts at 5,000 turns instead of 10,000.

Also add a wheel-flush profiler for bun --cpu-prof, retitle the
compiled binary as Waku-style, refresh chat-app.png, and drop the
stale chat-diff curated screenshot.

Session: ses_fd1333811ffefCKd7t9qN7SLgI
2026-08-23 15:30:58 +02:00
Tommy D. Rossi 9932d50d9b Add native GPUI runtime and text editor backend
Run GPUI through its native platform architecture on macOS, Windows, Linux, and FreeBSD. Keep the embedded AppKit pump on macOS and route non-macOS commands through a dedicated GPUI UI thread.

Replace the painted input label with a platform-backed editor model. Add multiline layout, IME ranges, grapheme-safe navigation, caret and selection painting, clipboard actions, undo and redo, horizontal and vertical scrolling, native focus control, and textarea registration.

Update the native test renderer, generated declarations, platform dependencies, submodule target, and development-loop documentation to match the new runtime.

Session: ses_fd70de910ffe1g2yG1yAkCjeLu
2026-08-22 13:48:56 +02:00
Tommy D. Rossi fdd6cffb06 Use GPUI native macOS platform
Replace the custom NodePlatform, window, dispatcher, display, clipboard, and winit event translation with the real gpui_macos MacPlatform and MacWindow implementations.

Add remorses/zed as the zed submodule on its gpuix branch. The fork adds a main-thread embedded AppKit mode with non-blocking event pumps, including the after-wait run-loop phase required to start GPUI frame sources and present Metal drawables.

Point Cargo at the submodule GPUI crates, remove the custom-platform dependency stack, invalidate the retained GPUI window through ApplicationHandle after React commits, and keep the paced JavaScript frame loop responsible only for pumping AppKit.

Remove obsolete platform plans, update build and development guidance, and add the native package changeset.

Validated with 82 Rust tests, 145 React tests, 11 example tests, live counter rendering and click updates, native transcript scrolling, and live text selection.

Session: ses_fdb06bf3affe9nMT6DamndI4zV
2026-08-21 22:09:22 +02:00
Tommy D. Rossi c7e8be643b Add a ChatGPT-style example, and screenshots in the README
examples/chat.tsx is a full ChatGPT-like app in ~900 lines: sidebar with
grouped history, scrolling transcript, composer, and <markdown>, <code>
and <diff> inside assistant turns. Data is hardcoded.

    cd examples && bun run chat

Two details it exists to demonstrate:

  Chrome sets userSelect: 'none'. The sidebar, top bar and composer opt
  out, so a drag across the transcript selects message text and never a
  conversation title. A test asserts both halves.

  <diff> needs a bounded viewport, because it virtualizes internally, and
  it carries its own borderRadius. GPUI clips a scroll container to its
  bounds RECTANGLE, never to a rounded path, so a rounded wrapper with
  overflow: hidden still shows square corners underneath.

examples/native-text.tsx is the smaller demo: the three components behind
a tab switcher, with a status line for events.

Screenshots
-----------
Test output lands in gitignored screenshots/ folders and churns on every
run, so only a curated set is committed, under docs/images. They are
regenerated with:

    bun scripts/screenshots.ts

which runs the visual tests and copies eight frames through a 256-colour
palette. That is lossless in practice for flat dark UI (no gradients to
band) and cuts the committed size from 976 KB to 352 KB.

The README now opens with the chat app, gains an Examples table, and shows
each native component next to the snippet that produces it.

AGENTS.md records the two rules that were expensive to learn: all text
must go through crate::text (never div().child(String), or it is invisible
to both selection and getPaintedText), and layout numbers belong on
Theme::metrics rather than in Rust constants.
2026-08-19 22:58:37 +02:00
Tommy D. Rossi 1d2b470936 Add bun run dev, a watch loop for the Rust side
There is no hot reload for the native half and there cannot be. require()
of a .node file calls process.dlopen, Node has no matching unload, and the
live state (the winit event loop, the wgpu device, the open window, the
selection registry) lives in thread-locals of the loaded library. A second
load would get empty thread-locals and a dead window, and napi registers
the module once per process.

The rebuild is fast enough that it does not matter. Measured after
touching one file:

    cargo check --lib             1.5s
    cargo build --lib             4.9s
    bun run build:debug (napi)   ~2s
    one vitest screenshot file   ~2s

scripts/dev.ts watches packages/native/src, rebuilds, and refreshes a
target. A Rust edit reaches fresh PNGs in about four seconds.

    bun run dev                            rebuild, re-render screenshots
    bun scripts/dev.ts --shots diff        only tests matching "diff"
    bun scripts/dev.ts --app native-text   rebuild, restart an example app

Screenshot mode is the default because Preview.app reloads a PNG on write
and, unlike a live window, the PNG can be read by an agent.

Builds are serialized: a burst of saves otherwise starts overlapping cargo
runs that then block on cargo's own lock and report confusing timings.

scripts/ gets its own package.json and tsconfig.json so the watcher is
type-checked (`bun x tsc -p scripts`) rather than only being run.
2026-08-19 22:58:22 +02:00