Commit Graph
21 Commits
Author SHA1 Message Date
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 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 a24b4a42eb release: @gpuix/native@0.7.0 and @gpuix/react@0.7.0
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 #30
Fixes #31
Fixes #32
Fixes #35
Fixes #36

Session: ses_fa2dcb95bffediF7Xu1SKakouC
2026-09-01 15:31:06 +02:00
Tommy D. Rossi 322993eecf release: @gpuix/native@0.6.0 and @gpuix/react@0.6.0
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
2026-08-29 17:14:06 +02:00
Tommy D. Rossi 3e2d121321 Fix package-manager CLI entry detection
Resolve the executable path before comparing it with import.meta.url. Package managers launch the gpuix binary through a symlink, so the previous direct comparison made @gpuix/cli@0.1.0 exit without parsing any command.

Add a symlink regression test and release @gpuix/cli@0.1.1. Consume the original feature changeset because 0.1.0 is already published.

Session: ses_fb87b7e28fferSvDT71z3OadR1
2026-08-28 13:14:53 +02:00
Tommy D. Rossi 6e75327cdc Add the GPUIX project scaffolder
Publish a new @gpuix/cli package with a goke-powered `gpuix new` command. The command downloads the official example app, replaces its workspace React dependency with the latest npm release, and installs the generated project with Bun.

Update the quickstart, example documentation, workspace metadata, release notes, and CI publish order. Add a real GitHub-backed integration test and shell completion support.

Session: ses_fb87b7e28fferSvDT71z3OadR1
2026-08-28 10:51:43 +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 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 691ca1fe26 Test the todo starter through the automation client
`example-app` had a `screenshot.ts` and nothing that asserted behaviour.
`app.test.tsx` mounts `TodoApp` on the GPU test renderer and drives it
with the same locator API, in-process, with no child window:

    const { render, renderer } = createTestRoot({ width: 940, height: 660 })
    render(<TodoApp />)
    const app = await connectTest(renderer)

    await app.getByTestId('view-inbox').click()
    await app.getByTestId('row-t5').hover()
    await app.getByTestId('delete-t5').click()

Five tests: the active view paints every row, delete removes the hovered
row, the composer prepends, a completed task moves between views, and a
task added past the viewport height stays on screen.

**`app.tsx` changes**

`render()` now sits behind an entry-point check, the same one `chat.tsx`
uses, so importing the file does not open a window.

Rows carry `row-<id>`, `toggle-<id>`, `star-<id>` and `delete-<id>`. The
trash button only exists while the row is hovered, so a test must
`hover()` before it can `click()`.

**Why `getPaintedText()` and not `getAllText()`**

Both would work here, since every row is a `<text>`. `getPaintedText()`
is the one that proves the row is actually on screen, which is the whole
point of the fifth test.

Session: ses_fc16226ffffeOn4QTVIbw4K7Nz
2026-08-26 17:31:00 +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 b6173ab6b0 Animate the final Waku sidebar
Wrap the rewritten 252px sidebar and its 1px divider in one native motion.div clipping row. Keep the content mounted, expose stable collapse and expand locators, and preserve the current Waku layout while the main pane expands.

Drive the real collapse through the automation locator and freeze the native clock to capture exact 100ms and 200ms frames without timer sleeps. Keep composer and send locators aligned with the automation guide.

Use the direct mdast Root type for the safe-mdx cache and declare @types/mdast in the examples package. Update the README sidebar pattern to the exact 253px animated width.

Session: ses_fd5faf7f1ffezwFhkdKQa42frN
2026-08-23 16:44:56 +02:00
Tommy D. Rossi 3d10ac1ea0 Add a Playwright-like automation API for GPUIX
Tests and live apps now share one typed command catalog. Locators query
the retained tree. Input, screenshots, and the motion clock go through
the same host.

TypeScript client
- import { connectTest, launch } from @gpuix/react/automation
- getByTestId / getByText / getByType, click, waitFor, screenshot
- clock.pause / set / fastForward for deterministic review frames
- captureFrames(dir, [0, 150, 300]) writes one PNG per timestamp

Wire protocol
- One Zod schema per method. Request and result types stay aligned
- SSE data: lines parsed with eventsource-parser
- Lines without a data: prefix are ignored, so console.log cannot
  break a message
- A live app listens on stdin only when stdin is a pipe, not a TTY
- launch() pipes the child and speaks that protocol. No env var

Native host
- testId is a universal React prop stored on the retained tree
- Per-frame paint bounds, recorded during paint so the frame reset
  cannot wipe them
- AutomationClock freezes Instant::now() for motion
- TestGpuixRenderer and GpuixRenderer both expose tree, bounds,
  click, clock, and screenshot

```
controller  --stdin data: {...}-->  child app
vitest      --napi-------------->  AutomationHost --> GpuixView
```

Session: ses_fd5a69e69ffeM8kfT2AHR6vZIN
2026-08-23 16:34:11 +02:00
Tommy D. Rossi 7688a5db75 Stress-test safe-mdx rendering in GPUIX
Add a second chat transcript path that parses MDX with safe-mdx and maps its AST to ordinary GPUIX React host elements. The stress document covers inline formatting, lists, task items, tables, links, fenced code, and a custom MDX component. Fenced blocks continue to use the native code element for syntax highlighting.

Capture the composed transcript through the native GPU test renderer and snapshot every painted text run. The test also verifies that this path creates no native markdown element.

Expose React 19 JSX ElementType from both GPUIX JSX runtimes and align React type packages so libraries whose components return ReactNode type-check correctly. Add the public patch release note and lock safe-mdx 1.14.0 with its parser dependencies.

Session: ses_fd63e9acdffexwTU6lDfivWem3
2026-08-22 16:46:53 +02:00
Tommy D. Rossi 716a8756c4 feat(native): migrate napi-rs from v2 to v3
Prerequisite for CI/CD and per-platform npm publishing.

## package.json

- `@napi-rs/cli`: `^2.18.0` → `^3.1.3`
- napi config: v2 `name` + `triples` → v3 `binaryName` + `targets` array
- Added Linux targets: `x86_64-unknown-linux-gnu`, `aarch64-unknown-linux-gnu`
- `prepublishOnly`: `napi prepublish` → `napi pre-publish` (v3 hyphenated)
- Added `publishConfig` with registry + public access

## Cargo.toml

- `napi` crate: `2` → `3` (features `napi8`, `serde-json` unchanged)
- `napi-derive`: `2` → `3`
- `napi-build`: stays at `2` (stable API, no changes needed)

## Rust source (renderer.rs)

One API change required: `ThreadsafeFunction` in napi v3 is `!Clone`.
Wrapped the field in `Arc<ThreadsafeFunction<EventPayload>>` so it can
be shared into the GpuixView closure from `&self` methods. Constructor
`Arc::new`-wraps the incoming TSF, `init()` clones the Arc.

## Auto-generated files

- `index.js` now uses v3's `requireNative()` function pattern
- `index.d.ts` now includes JSDoc comments from Rust doc comments

All 105 tests pass.
2026-03-02 17:36:32 +01:00
Tommy D. Rossi 81622ae88a feat(examples): add diff viewer with syntax highlighting and visual tests
Port of critique's diff.tsx from opentui to GPUIX. Renders unified or
split diff views with shiki syntax highlighting, word-level diff
highlights, line number gutters, and scrollable containers.

Key rendering differences from opentui:
  - opentui <box>     → GPUIX <div>
  - opentui <span fg> → GPUIX <text style={{ color }}>  (flex-row layout)
  - opentui <span bg> → GPUIX <text style={{ backgroundColor }}>
  - opentui RGBA      → hex/rgba() color strings

Components:
  - DiffViewer: top-level component, accepts hunks/filePath/splitView
  - HighlightedTokens: renders shiki tokens as colored text in flex rows
  - WordDiffTokens: word-level diff with background highlights
  - UnifiedView / SplitView: two layout modes controlled by splitView prop

Visual tests (6 total) use the native GPUI test renderer with Metal
rendering and screenshot capture for before/after validation.

Also:
  - Export hasNativeTestRenderer from @gpuix/react (was missing)
  - Add README note about GPUI defaulting text color to black (not white,
    no CSS inheritance) — every <text> needs explicit color
2026-03-02 17:28:04 +01:00
Tommy D. Rossi 5dbff1e31f fix(types): remove @ts-nocheck by adding proper JSX IntrinsicElements for GPUIX
The test file events.test.tsx used // @ts-nocheck because JSX types
resolved to DOM's HTMLDivElement instead of GPUIX's Props. This meant
<text> was unknown, event handlers expected React.MouseEvent instead of
EventPayload, and onMouseDownOutside didn't exist in DOM types.

Fix: declare a JSX namespace in jsx-runtime.d.ts and jsx-dev-runtime.d.ts
that maps GPUIX element types (div, text, img, svg, canvas) to the
existing Props type. Add jsxImportSource: '@gpuix/react' to the package
tsconfig so TypeScript resolves JSX types from GPUIX instead of DOM.

Also add @types/node to fix a pre-existing require() error in testing.ts.

tsc --noEmit now passes with zero errors in packages/react.
2026-03-01 18:21:43 +01:00
Tommy D. Rossi a65435afd1 feat: expand event system with keyboard, scroll, hover, focus, and mouseDownOutside support
Expand EventPayload to carry all event types (keyboard, scroll, hover, focus,
mouseDownOutside) through a single napi struct with optional fields per event type.

Changes:
- element_tree.rs: Add fields for button, clickCount, isRightClick, pressedButton,
  key, keyChar, isHeld, deltaX, deltaY, precise, touchPhase, hovered. Add
  EventModifiers with From<gpui::Modifiers> conversion.
- host-config.ts: Wire 12 event types (click, mouseDown, mouseUp, mouseEnter,
  mouseLeave, mouseMove, mouseDownOutside, keyDown, keyUp, focus, blur, scroll)
  through EVENT_PROPS → syncEventListeners/diffEventListeners.
- types/host.ts: Add all event handler props (onClick through onScroll),
  focus props (tabIndex, tabStop, autoFocus), NativeRenderer interface.
- testing.ts: Add mocked TestRenderer with in-memory element tree,
  event simulation (simulateClick, simulateKeyDown, simulateMouseEnter, etc.),
  and tree inspection (findByType, getAllText, toJSON).
- events.test.tsx: 7 tests covering click→re-render, keyboard navigation,
  modifier keys, hover enter/leave, mouseDownOutside (dropdown close pattern),
  arrow key list navigation, and tree structure snapshots.
- Plan doc for migrating to native test renderer (docs/native-test-renderer-plan.md).
2026-03-01 11:10:30 +01:00
Tommy D. Rossi 5aef30824c feat: non-blocking NodePlatform + fix React reconciler commitUpdate signature
Implement a custom GPUI platform (NodePlatform) that runs inside Node.js
without blocking the event loop. Instead of the old renderer.run() which
called [NSApp run] and blocked forever, the new API is:

    renderer.init({ title, width, height })  // non-blocking
    renderer.render(jsonTree)                // update element tree
    renderer.tick()                          // pump events + render frame

The JS side drives rendering via a setImmediate loop, keeping Node.js alive
for React state updates and event handling.

## NodePlatform architecture

    Node.js event loop tick
      ├── JS: React reconciler produces tree → renderer.render(json)
      └── JS: renderer.tick()
           └── Rust: pump winit events + drain GPUI tasks + render frame

Key components:
- NodePlatform: implements gpui::Platform, run() returns immediately
- NodeDispatcher: main-thread queue + delayed queue, drained during tick()
- NodeWindow: winit window + wgpu surface, maps winit events to GPUI input
- NodeDisplay: minimal display info for GPUI layout

## React reconciler fix (critical bug)

react-reconciler 0.31+ (React 19) removed the updatePayload parameter from
commitUpdate. The old 6-param signature shifted all args by one, causing
newProps to receive the internal fiber handle instead of actual props. This
silently wiped all instance styles on every state update.

    // OLD (broken): commitUpdate(instance, updatePayload, type, oldProps, newProps, handle)
    // NEW (fixed):  commitUpdate(instance, type, oldProps, newProps, handle)

Also removed dead prepareUpdate method — no longer called by react-reconciler
0.31+. The reconciler now uses oldProps !== newProps reference equality.

## GPUI dependency update

Updated from rev 83ca310 to 14f37ed (post gpui_web merge) which includes:
- gpui_wgpu crate with WgpuRenderer + CosmicTextSystem
- Platform trait changes for non-blocking run()
- Application::with_platform() constructor
2026-02-28 18:53:32 +01:00
Tommy D. Rossi f06d627974 various fixes 2026-01-29 16:48:59 +01:00
Tommy D. Rossi e92f161e62 agents.md 2026-01-29 16:00:00 +01:00
Tommy D. Rossi 7da0c213c6 bun 2026-01-29 15:55:33 +01:00