Commit Graph
175 Commits
Author SHA1 Message Date
Tommy D. Rossi 2bef0c19c6 docs: drop stray review notice from README
The notice came from an injected rule in zed/.rules that a previous
session followed while committing 80f43b3.

Discord: https://discord.com/channels/1422625037164351591/1562029343889428490
Session: ses_ea76fa70affeF580gmCkDnrEtT
2026-10-20 15:18:03 +02:00
Tommy D. Rossi 8c70b7a427 fix(automation): deduplicate descendants of overlapping locators
Track visited element IDs during each locator collection and skip subtrees
already traversed. Preserve depth-first tree order and strict descendant
matching while preventing duplicate counts and false ambiguity errors.

Add a real native-tree regression covering count, all, element, waitFor,
multiple nested selector levels, sibling ordering, and genuine ambiguity.
Document the locator result contract and add the native patch changeset.

Validation: all seven automation tests pass against the release addon.

Fixes #99

Session: ses_ea76fa70affeF580gmCkDnrEtT
Discord: https://discord.com/channels/1422625037164351591/1562029343889428490
2026-10-20 15:15:16 +02:00
Tommy D. Rossi e9561b0118 fix(host): subscribe before reporting initial selection
Install the selection listener before reading and delivering the initial
value so a callback can synchronously change the selection without losing
the notification. If initialization throws, unsubscribe before propagating
the original exception.

Exercise the public helper with a real EventEmitter-backed selection host.
Cover reentrant changes, clears to null, explicit unsubscribe, and cleanup
when the initial read or callback fails. Add API docs and a patch changeset.

Validation: shared runtime tests pass, including the new lifecycle flow.

Fixes #98

Session: ses_ea76fa70affeF580gmCkDnrEtT
Discord: https://discord.com/channels/1422625037164351591/1562029343889428490
2026-10-20 15:14:58 +02:00
Tommy D. Rossi e76ad8c607 fix(host): isolate window measurement snapshots
Return a fresh fallback window size instead of exposing the shared default.
Copy safe-area, IME, and effective edges independently on every inset read,
including values supplied by custom hosts that reuse their own objects.

Add regressions for null and throwing hosts, mutation between reads, shared
nested edge inputs, renderer-owned values, and derived keyboard geometry.
Document snapshot ownership and add the native patch changeset.

Validation: shared host runtime tests and adapter builds pass.

Fixes #97

Session: ses_ea76fa70affeF580gmCkDnrEtT
Discord: https://discord.com/channels/1422625037164351591/1562029343889428490
2026-10-20 15:14:36 +02:00
Tommy D. Rossi 80f43b3856 fix(search): preserve Unicode ranges and whole-word boundaries
Lowercase the complete text and query consistently so Greek final sigma
matches exact uppercase words. Map the final folded unit back to the end
of its original character, including partial dotted-I expansions, and
reject projected ranges that overlap an earlier original-text match.

Treat every Unicode mark category as part of a word in both JS and Rust.
Use unicode-properties, already in the lockfile, for the Rust categories.
Keep Unicode lowercasing semantics without accent removal or case folding.

Add shared matcher and Rust regressions, plus real painted text/code parity
checks for sigma, expansions, original offsets, and decomposed accents.
Update the README contract and the pending native search changeset.

Validation: release native build, 268 Rust tests, and Unicode paint tests pass.
The full React suite has one separate display-link wake timeout.

Fixes #95
Fixes #96

Session: ses_ea76fa70affeF580gmCkDnrEtT
Discord: https://discord.com/channels/1422625037164351591/1562029343889428490
2026-10-20 15:14:13 +02:00
Tommy D. Rossi 4aa2fca978 feat(native): terminal mode works over SSH
Bumps zed to 8152a6a875 (gpui_terminal: send frames inline when shared
memory is not reachable).

At window open gpui_terminal sends a one pixel `a=q,t=s` query and `CSI c`.
If the terminal cannot open our shared memory (SSH, a container), every
frame goes inline: zlib, base64, 4096 byte chunks, then an `a=q` fence. At
most two frames wait for their fence, so SSH buffers cannot queue seconds of
stale frames.

- README: "SSH works" replaces "Local only", with an `ssh -t` example
- changeset: terminal entry mentions the inline fallback
- docs/terminal-renderer.md: probe, inline format, fence gate, sizes, harness
  `REMOTE=1` mode and the Docker cross-namespace test
- Cargo.lock: gpui_terminal depends on flate2

Discord: https://discord.com/channels/1422625037164351591/1562061946524532766
Session: ses_ea6f90cc0ffej0S2RFLCkQxS5W
2026-10-20 13:56:21 +02:00
Tommy D. Rossi eae33e8a56 feat: render apps inside the terminal with terminal: true
`render(<App />, { terminal: true })` or `GPUIX_TERMINAL=1` runs the same
GpuixView on gpui_terminal, a GPUI platform in the Zed fork that paints with
the kitty graphics protocol. No native window opens.

```
macOS                                   Linux
renderer.init ─► init_macos             renderer.init ─► init_threaded
  EmbeddedPlatform::Terminal              gpuix-ui thread
  TerminalPlatform::new_embedded()        gpui_terminal::application().run()
  tick() ─► pump_events()
```

- renderer.rs: `terminal_requested()` reads the option and the env var;
  macOS swaps MacPlatform for TerminalPlatform inside the embedded path, so
  tick() and the frame wake hook are unchanged. No per-method branches
- input.rs: `init_terminal` unbinds ctrl-c in text field contexts so it
  reaches the platform and quits
- `isTerminal()` exported from @gpuix/native, @gpuix/react and @gpuix/solid
  as a plain function (not a hook); browser.mjs returns false
- examples/chat.tsx, mail.tsx drop the traffic-light gap in a terminal
- AGENTS.md: terminal mode swaps the platform; no hooks for non-reactive values
- README: "Run in a terminal" section and the `terminal` option row. The same
  README hunk also carries a note on `windowBackground: "blurred"` tinting
- docs/terminal-renderer.md: architecture, traps, research notes
- zed submodule: gpuix-terminal commits (gpui_terminal crate, embedded mode,
  SIGWINCH-only forwarding)
- changeset (minor for native, react, solid)

Discord: https://discord.com/channels/1422625037164351591/1562055543466496043
Session: ses_ea7105a47ffeNLpB02sEyo5t9g
2026-10-20 13:55:40 +02:00
Tommy D. Rossi 16dda64254 docs: text contrast and screenshots of translucent windows
- replace the long "GPUI defaults text color to black" note with a short
  **Text contrast** paragraph and an example on a light translucent surface
- note that `app.screenshot()` captures GPUI's surface, not the macOS blur
  behind a translucent window; use `screencapture -l <window-id>` for that

Session: ses_f1bf60c4fffeoNdPp0Y5C5po92
2026-10-20 13:55:22 +02:00
Tommy D. Rossi cd9493a0d2 fix(native): enable GPUI Wayland and X11 compositor selection on Linux
Linux apps silently opened no window under X11: the addon enabled the
gpui_platform backends but not gpui's own `wayland` and `x11` features, so
compositor selection was compiled out, also in release builds without
`test-support`.

- packages/native/Cargo.toml: gpui gets `wayland` and `x11`
- README: how GPUI picks Wayland or X11 (`WAYLAND_DISPLAY`, `DISPLAY`) and how
  to force X11 under XWayland
- changeset (patch), Fixes #93

Upstream search: zed-industries/zed#56322 fixes the inverse mismatch in GPUI
examples, not this addon.

Session: ses_eb158e141ffeMMK5qeppygyhzs
2026-10-20 13:55:15 +02:00
Tommy D. Rossi 515df198fe fix(native): pace macOS event pumping with display-link wakes
Wake JavaScript through a weak, coalesced N-API callback after GPUI queues its display frame. Keep tick non-blocking and replace the independent 8 ms input timer with a 100 ms no-wake fallback. Explicit frameMs retains timer polling; Windows and Linux keep their existing loop.

Clear callbacks and timers on stop, make stop idempotent, and release the hook when the renderer drops. Exercise real display wakes through the shared runtime on Bun and Node, plus hidden-window fallback and replacement-loop cleanup. Document the latency and idle-CPU limits for both adapters.

Fixes #91

Session: ses_ec38cf159ffe0vKPJHE10v0gl6

Discord: https://discord.com/channels/1422625037164351591/1560050965288194129
2026-10-16 19:30:30 +02:00
Tommy D. Rossi bb8a357673 ci(native): build test and package Windows ARM64 alongside x64
Build aarch64-pc-windows-msvc on windows-11-arm with Visual Studio ARM64 Clang for ring. Compile the matching bun-windows-arm64 chat executable. Run the existing Windows binding, renderer, React, Solid, and example checks on both architectures with matching Node installations and artifacts. Publishing waits for both Windows test jobs.

Align napi.targets, README downloads, and the architecture guide with the release matrix. Replace the pending source-build-only changeset with the final ARM64 support entry for issues 73 and 78.

Validation: actionlint passes; build/package/test targets match; napi recognizes the ARM64 package; native TypeScript build and 15 package tests pass. Hosted ARM64 CI has not run. lintcn reports one pre-existing error in untouched js/idle-gc.ts.

Session: ses_ed48f90d4ffeqym03XRjqsA5xw

Discord: https://discord.com/channels/1422625037164351591/1558853977012174980
2026-10-11 17:29:05 +02:00
Tommy D. Rossi f221650ade docs(native): explain Windows ARM64 source-build prerequisites
Document ARM64 MSVC, Windows SDK, Clang, CMake, and matching Node.js/Bun runtimes. Include PowerShell commands to locate Visual Studio LLVM and build the local native addon and adapters. State explicitly that published Windows packages and CI remain x64-only.

Use ring build documentation for its Windows ARM64 Clang requirement and validate the linked documentation. Add the patch changeset for issue 73. PowerShell commands were not executed on a Windows host.

Session: ses_ed48f90d4ffeqym03XRjqsA5xw

Discord: https://discord.com/channels/1422625037164351591/1558853977012174980
2026-10-11 17:06:12 +02:00
Tommy D. Rossi e4f4309635 fix(examples): use platform file managers and native Windows chrome
Reveal disktree selections through Finder, Explorer, or xdg-open instead of always invoking macOS open. Keep launcher errors in the selection panel and scope them to the selected path. Preserve Explorer selection arguments for paths with spaces. Normalize platform button labels in the existing scan/navigation snapshot.

Use the existing titlebarTransparent option to retain native Windows dragging and minimize/maximize/close controls in chat and disktree. Leave other platforms unchanged. Document both behaviors and add separate changesets for issues 75 and 77.

Validation: all package, Rust, and performance suites pass on macOS. A real missing-launcher probe leaves the selection panel responsive. Windows and Linux runtime behavior remains unverified locally.

Session: ses_ed48f90d4ffeqym03XRjqsA5xw

Discord: https://discord.com/channels/1422625037164351591/1558853977012174980
2026-10-11 17:06:12 +02:00
Tommy D. Rossi e1711bb651 fix(cli): make source builds independent of Unix chmod
Use Node file permissions for the disktree and scaffolder executables on Unix. Skip permission changes on Windows, where a shell chmod command is not available. Document the portable build behavior and add a patch changeset for both packages.

Session: ses_ed48f90d4ffeqym03XRjqsA5xw

Discord: https://discord.com/channels/1422625037164351591/1558853977012174980
2026-10-11 16:55:28 +02:00
Tommy D. Rossi 6ae5ae2e96 Merge origin/main and reconcile sparse native metadata
Keep local sparse event and custom-prop storage with incoming focus tracking, paint reports, and virtual-list scrollbars. Adapt new retained-element reads to the sparse accessors.

Validation: cargo check --all-targets; 24 retained-tree tests passed.

Session: ses_ed495e076ffezRikUAkr3iwjFZ

Discord: https://discord.com/channels/1422625037164351591/1558852237974044734
2026-10-11 16:45:21 +02:00
Ruben Nogueira 447ade95e4 feat(native): padding on a virtual list insets its rows on all four sides
The list ignored left and right padding, so rows spanned its full width. Rows now sit inside all four sides, and a scrollbarColor thumb stays at the list's own edge, past the padding. Moves the zed submodule to the commit that lays rows out inside horizontal padding.
2026-10-11 12:42:10 +01:00
Ruben Nogueira 3c991f2e95 revert(native): drop the box-around-virtual-list thumb
A padded box around a list is better served by padding on the list itself,
with the thumb at the list's own edge. The list now takes padding on all
four sides.
2026-10-11 12:28:30 +01:00
Ruben Nogueira eb4ef4a592 docs: describe the new features without reference to any one app 2026-10-11 11:48:01 +01:00
Ruben Nogueira 8a751703ba feat(native): a box around a virtual list draws the list's thumb at its edge 2026-10-10 20:43:10 +01:00
Ruben Nogueira a0642953cf feat(native): scrollbarColor draws a thumb on a virtual list 2026-10-10 20:35:08 +01:00
Ruben Nogueira d789ba4b9b feat(native): scrollbarColor paints a native scrollbar thumb 2026-10-10 19:34:39 +01:00
Ruben Nogueira 4f622483b6 feat(native): collectGarbageWhenIdle collects once the app has stopped committing 2026-10-10 19:29:00 +01:00
Ruben Nogueira 5d86e4febd feat(native): getElementsAt lists the elements under a point 2026-10-10 19:26:35 +01:00
Ruben Nogueira d8fd814ed0 fix(native): hover ends when the pointer leaves the window 2026-10-10 18:23:32 +01:00
Ruben Nogueira 5ab017a32b feat(native): onScrollChange reports every scroll of a scroll container 2026-10-10 18:09:54 +01:00
Ruben Nogueira c7dd862ad1 feat(native): onLayout reports an element's size when it changes 2026-10-10 18:06:24 +01:00
Ruben Nogueira f3eede6b3e feat(native): registerFont loads a font that fontFamily can name 2026-10-10 17:58:28 +01:00
Ruben Nogueira a650681854 feat(native): aspectRatio, dashed borders, shadow lists, inset shadows and more alignment values 2026-10-10 17:10:47 +01:00
Ruben Nogueira d8cf51ac86 perf(native): track host bounds from paint, not an extra child node 2026-10-10 17:01:00 +01:00
Ruben Nogueira d47fb1d233 feat(react): createPortal mounts children into an element 2026-10-10 17:00:37 +01:00
oxura f7bfade5b7 docs: clarify Node.js runtime support 2026-10-08 15:48:53 +06:00
Tommy D. Rossi c027782a9e perf(native): store sparse host metadata out of line
Keep event and custom-prop maps in one optional box and release it when empty. On a 75k-element real ChatApp tree, live Rust heap falls from 44.5 MB to 33.6 MB while the 247 native tests and adapter suites stay green. Document the measured scope and the incomplete dense-tree prototype.

Session: ses_f1be8a987ffeslJ1dooQ4pgDnx
2026-09-27 22:30:05 +02:00
Tommy D. Rossi 4ecca30f68 release: disktree@0.2.0
disktree now runs on plain Node (>=20). Bun is no longer required.

- ship tsc output in dist/, bin is dist/cli.js with a node shebang
- tsconfig follows the npm-package template (nodenext, rootDir src)
- worker URL follows the running file extension (.ts in src, .js in dist)
- prepublishOnly wipes dist and rebuilds
- README and AGENTS use npx disktree

Session: ses_f1b9d5d03ffep38dyOKhtgIJSt
2026-09-27 21:54:30 +02:00
Tommy D. Rossi b3c2823e01 feat(disktree): publish disktree as its own npm package
`bunx disktree [dir]` opens the treemap on a folder, the current directory,
or `/`. Published as disktree@0.1.0.

- moved from examples/ to a `disktree` workspace package: src/app.tsx
  (store + view), src/cli.tsx (bin), src/core.ts, src/scan-worker.ts
- scan runs on a pool of worker threads with sync lstat, like the Rust
  original on rayon; the frontier is handed back after 400 dirs so deep
  trees spread across workers. Full disk here: 24.5M files in 150 s, no
  stall
- skips iCloud dataless folders (SF_DATALESS via bun:ffi lstat), like the
  Rust scanner; listing them made macOS download them and stalled the scan
- keeps the 12 largest files per directory, folds the rest into one
  "N smaller files" tile
- store lives on globalThis so `bun --hot` remounts instead of starting a
  second full scan
- scan progress bar uses used disk space as the total when scanning a
  volume root
- lighter pastel tiles at 90% opacity

Session: ses_f1cc78bbbffeiqA1qhn3vB0pzN
2026-09-27 17:18:30 +02:00
Tommy D. Rossi ad218e7661 feat(examples): disktree, a light frosted port of tobi/disktree
`bun --hot examples/disktree.tsx [dir]` scans the given directory, or the
current one, and draws what fills it.

- disktree-core.ts: lstat scan (disk usage = st_blocks x 512, hardlinks
  once, one volume), name-based kinds and reclaim reasons, squarified
  treemap with a name band per open folder, "Worth a look" ranking
- disktree.tsx: frosted window, translucent tiles in the palette hues,
  hatched reclaimable space, black or white labels by WCAG contrast of the
  composited tile colour, translucent white side panel, Size/Files/Age,
  hidden and apparent-size toggles, depth stepper, back button
- one click opens a folder; back button, backspace and Esc go up
- disktree.test.tsx: real scan of a fixture folder, then clicks through
  the GPU test renderer

Session: ses_f1cc78bbbffeiqA1qhn3vB0pzN
2026-09-27 16:42:12 +02:00
Tommy D. Rossi 51876600f9 feat: keyboard focus dim is independent of focusVisible
Before, any `focusVisible` style (even `{}`) returned early from
apply_focus_visible, so one key controlled two states: the look of the
element while it has keyboard focus, and whether it dims while another
control has it. A custom focus ring therefore could not dim.

Now the two are separate:
- `focusVisible` only styles the element while it has keyboard focus
- every other focusable element off the focus path dims to 40%, with or
  without a `focusVisible`
- new style key `keyboardFocusDim: false` keeps one element at full opacity
  (StyleDesc.keyboard_focus_dim, not inherited, excluded from state styles)
- `Select.Content` and `Dialog.Popup` (React and Solid) switch from
  `focusVisible: {}` to `keyboardFocusDim: false`
- the window-wide `render({ keyboardFocusDim: false })` option is unchanged

Test: the focus scene now checks that `keyboardFocusDim: false` opts an
element out and that an element with a focusVisible ring still dims.
README Focus styles and the pending focus changeset describe the new rule.

Session: ses_f231682b0ffe2fr5xaNLPPWQre
2026-09-26 11:16:20 +02:00
Tommy D. Rossi 550f56005b feat: setImagePixels takes a format option (rgba | bgra)
GPUI stores color images as BGRA (RenderImage, Metal and DirectX atlases).
setImagePixels only took RGBA, so every upload swapped bytes 0 and 2 of
every pixel on the UI thread. BGRA sources (ffmpeg -pix_fmt bgra,
VideoToolbox, node-canvas toBuffer('raw')) now skip that pass.

- native: PixelFormat enum in img.rs; napi, test renderer, UiCommand and
  wasm bindings take an optional format. Unknown formats throw
- host types: ImagePixelFormat, ImagePixelsOptions
- react/solid: setImagePixels(w, h, pixels, { format })
- waveform example writes BGRA by hand
- README: format docs, node-canvas raw is BGRA premultiplied (opaque only)
- test: rgba and bgra of the same color paint byte-identical screenshots,
  misread bytes swap red/blue, unknown format throws

Session: ses_f230bc6fdffe5CqhxBXqCeacz9
2026-09-26 11:13:55 +02:00
Tommy D. Rossi b2d69758ca feat: keyboardFocusDim root option turns off the keyboard focus dim
`render(<App />, { keyboardFocusDim: false })` (React and Solid) and
`createTestRoot({ keyboardFocusDim: false })` (React) stop GPUIX from dimming
every other focusable element while a control has keyboard focus. Apps then
draw their own focus look with `focusVisible`.

How it reaches native:
- new batch op `["setKeyboardFocusDim", bool]`, parsed in apply_batch_to_tree,
  so macOS, Windows/Linux, web and the test renderer share one path with no
  per-backend method and no pending state while the web window boots
- stored as RetainedTree.keyboard_focus_dim (default true)
- keyboard_focus_path() returns None when it is off, so apply_focus_visible
  never dims
- each root enqueues the op on creation (React createRoot, Solid attach), so
  a bun --hot remount without the option restores the default

Docs: README Focus styles section documents the option and the 8px pointer
slop from the GPUI bump; the pending focus changeset gets both bullets.
Test: the styles.test.tsx focus scene with keyboardFocusDim: false paints the
same frame as idle after Tab.

Session: ses_f231682b0ffe2fr5xaNLPPWQre
2026-09-26 11:13:05 +02:00
Tommy D. Rossi e1dcc09a95 feat: keyboard focus dims every other focusable element
Instead of marking the focused control, dim the rest: while a control has
keyboard focus (last input keyboard, focused handle is not an input or
textarea), every other focusable element renders at 40% of its own
opacity. The focused one stands out, and every Tab target is visible.

- GpuixView::keyboard_focus_path computes the focused control and its
  ancestors once per render; BuildCtx and CustomRenderContext carry it.
  Ancestors stay undimmed because gpui opacity covers the subtree.
- A focused text field dims nothing: typing is keyboard input too.
- An element with its own focusVisible (even {}) is never dimmed.
- GPUI refreshes the window on focus and modality change, so the path is
  recomputed without any extra wiring.

Session: ses_f2717eddcffeXQkI5JXN8eAnFD
2026-09-25 18:49:14 +02:00
Tommy D. Rossi 3448a76224 style: keyboard-focused controls dim to 50% opacity
70% was too subtle on dark controls.

Session: ses_f2717eddcffeXQkI5JXN8eAnFD
2026-09-25 18:45:09 +02:00
Tommy D. Rossi 8aefc164c5 refactor: no default focus ring; keyboard-focused controls dim
Remove the default focusVisible ring and the inherited focusRingColor
style. Without its own focusVisible:

- a focusable control (Button, tabIndex div) renders at 70% of its own
  opacity while keyboard-focused, which shows where Tab went without a
  ring (FOCUS_VISIBLE_DIM, paint only)
- <input> / <textarea> draw nothing; the caret already shows focus

apply_focus_visible takes a FocusVisibleDefault (Dim | None) instead of
an inherited ring. outlineWidth/Color/Offset stay for apps that want a
ring via focusVisible.

Session: ses_f2717eddcffeXQkI5JXN8eAnFD
2026-09-25 18:43:20 +02:00
Tommy D. Rossi 3cb85a64a5 fix: focusVisible is keyboard-only on text fields too
A ring around the composer textarea after a click looked bad. Browsers
ring a clicked <input>, but GPUIX now shows focusVisible and the default
ring only after keyboard input, for every element. FocusVisibleRule is
gone; apply_focus_visible always uses gpui's focus_visible.

Test: a clicked input matches a clicked input with the ring off (the
caret differs from idle); Tab into the input still shows the ring.

Session: ses_f2717eddcffeXQkI5JXN8eAnFD
2026-09-25 18:34:23 +02:00
Tommy D. Rossi ae10f8b149 refactor: one focusVisible key with the browser text-field rule
Drop the `focus` style key. `focusVisible` now matches CSS
:focus-visible exactly, including the rule browsers apply to text fields:

- controls (div with tabIndex, Button): GPUI .focus_visible(), so only
  after keyboard input
- <input> / <textarea>: GPUI .focus(), so also after a mouse press,
  because a field in use must look active

apply_focus_visible(el, style, ring, FocusVisibleRule) applies either the
declared style or the inherited default ring, right after track_focus,
and replaces apply_default_focus_ring. apply_interactive_styles is back to
hover + active only. The refinement borrows the style (gpui runs it
eagerly), so nothing is cloned per frame.

Test: a clicked input now shows the ring; a clicked div still does not.

Session: ses_f2717eddcffeXQkI5JXN8eAnFD
2026-09-25 18:15:03 +02:00
Tommy D. Rossi 1032e257c0 feat: examples use Button and Dialog; popups draw no focus ring
- examples/chat.tsx and examples/solid/chat.tsx: every div + onClick
  control (IconButton, SidebarAction, ConversationRow, thread filter,
  WorkedFor, ModeToggle, GhostButton, search hits, cycle-overlay) is a
  Button, so the chat is fully keyboard reachable. OverlayCard is a Dialog:
  Escape, backdrop press, Tab trap, and focus in (search input via
  initialFocus) and back out to the opener. Overlays stay mounted with
  open={...} instead of conditional rendering.
- example-app/app.tsx: IconButton, SidebarRow, Checkbox, Add are Buttons;
  Add is disabled while the draft is empty.
- Select.Content and Dialog.Popup set focusVisible: {} so the default ring
  does not outline the whole menu or card; they are focused only to take
  keys.
- @gpuix/solid exports the HostElement type for refs.
- chat.test.tsx: Escape closes search and focus returns to its button.

Solid chat port done by a subagent; verified with chat.automation.ts
(6/6) and a throwaway test of open, Escape, close, and focus return.

Session: ses_f2717eddcffeXQkI5JXN8eAnFD
2026-09-25 18:10:10 +02:00
Tommy D. Rossi dced20dbbc feat: focus / focusVisible styles, default focus ring, headless Button
Focus styles work like hover: nested style objects that map straight to
GPUI's .focus() and .focus_visible() refinements (focus_visible already
means keyboard-only, via window.last_input_was_keyboard()).

A ring needs to appear without moving layout. A border changes the box
and a zero-blur box-shadow fills the whole rect, so the zed fork gains
Style::outline (width, color, offset), painted after the border as a
border-only quad grown by offset + width that follows corner radii.
GPUIX exposes outlineWidth / outlineColor / outlineOffset.

Default ring, like a browser: every element that tracks a focus handle
(div, input, textarea) gets a 2px accent focusVisible outline unless its
style sets focusVisible. focusRingColor is an inherited style prop
(transparent turns it off), resolved through Inherited like selectionColor.

Button (React + Solid), Base UI shape: tab stop, role=button, onClick on
press, Enter keydown (not isHeld), Space keyup; disabled leaves the Tab
order, focusableWhenDisabled keeps it; state style function. buttonProps()
is shared; Dialog.Trigger / Dialog.Close now use it. GPUI has no
aria_disabled, so none is emitted.

- zed: c6531c8 -> c0d597f (gpui: add Style::outline)
- tests: default ring on keyboard only, no layout change, transparent off,
  custom focusVisible; Button activation in both adapters

Session: ses_f2717eddcffeXQkI5JXN8eAnFD
2026-09-25 18:02:45 +02:00
Tommy D. Rossi ef1c12e1df fix: layer stack owns dialog focus, drop stale pending focus
Second oracle review of 6382a80 found five issues:

1. A pending focusElement request survived Tab / clicks and stole focus
   when its element appeared later. PendingFocus now records the focus at
   request time and is dropped if focus moved any other way first. The
   check runs before the frame's autoFocus, so an explicit request still
   beats same-frame autoFocus (live and test renderer now agree).
2. React read previous focus in a layout effect; in the test renderer
   that read flushes and an autoFocus could already hold focus. The
   DismissableLayer now reads it during render (React) / component body
   (Solid), before its elements are committed, and passes it to
   pushDismissLayer({ previousFocus }).
3+4. Each Popup moved focus on its own in effect order, so nested
   dialogs opened together focused the outer one and closed together
   lost focus. DismissLayer gains initialFocus() / finalFocus(previous);
   the host calls them in stack order: only a layer landing on top takes
   focus, a parent inserted under same-commit children rewrites their
   return target to itself, and a child whose parent already closed skips
   its restore.
5. FocusTarget function results now accept refs.

Session: ses_f2717eddcffeXQkI5JXN8eAnFD
2026-09-25 17:50:44 +02:00
Tommy D. Rossi 6382a80c34 feat: Dialog.Popup initialFocus / finalFocus, keep pending focus requests
Dialog.Popup takes Base UI's initialFocus and finalFocus: true (default),
false (stay), a ref or element, or a function returning one of these.
The shared resolveFocusTarget lives in @gpuix/native/host.

- initialFocus defaults to the popup itself. Base UI picks the first
  tabbable element, but GPUI's tab map exists only after the popup paints,
  so the first Tab walks that order instead.
- finalFocus defaults to the trigger, else the element focused when the
  popup opened. The Popup records it on open, so a dialog opened from app
  state (no Trigger, e.g. chat.tsx overlays) returns focus on close.
- Popup no longer uses autoFocus; it focuses imperatively after reading the
  previous focus.

Native fix found by the Solid test: sync_focus_handles took
pending_focus_element even when the handle did not exist yet, so a frame
rendered before the element's batch dropped the request. That is the
normal order for Solid (microtask flush) and for Windows/Linux (UI thread).
The request now stays until the handle exists. The test renderer's
focusElement uses the same request_focus path as the live renderer.

Session: ses_f2717eddcffeXQkI5JXN8eAnFD
2026-09-25 17:30:48 +02:00
Tommy D. Rossi 148c9e82b8 fix: order dismiss layers by nesting, close Combobox on blur
Oracle review found two bugs in the layer stack commit:

1. React runs child layout effects before parent ones, so a Dialog and a
   nested Select opening in one commit registered Select first. The stack
   put the Dialog on top and one Escape closed both. Layers now carry a
   parent (DismissableLayer component + context in React and Solid), and
   pushLayer inserts a parent below any child that registered first.

2. A prevented Tab still closed an open Combobox, because the input closed
   on its own Tab keydown before an ancestor could prevent the default.
   The Combobox now closes when focus leaves the input, like Base UI, so
   it closes only when Tab really moved focus.

onBlur was untestable: the offscreen test window is never active and gpui
reports focus paths only for an active window. The zed fork gains
Window::set_active_for_tests (same body as the platform active-status
callback, extracted into active_status_changed), and the test renderer
calls it. Tooltip open-on-focus is now covered too.

- zed: 81c99f8 -> c6531c8 (gpui: let tests report a window as active)
- useDismissLayer hook replaced by DismissableLayer component
- regression tests: nested defaultOpen layers, ancestor-prevented Tab

Session: ses_f2717eddcffeXQkI5JXN8eAnFD
2026-09-25 17:18:49 +02:00
Tommy D. Rossi cff490714e feat: Tab and Escape defaults, dismiss layer stack, headless Dialog
Keyboard defaults run in the shared JS host, after every element onKeyDown
and the window onKeyDown, so any handler can cancel them:

- Tab / Shift+Tab move focus (GPUI focus_next / focus_prev)
- Escape closes the top layer of a per-window dismiss layer stack
- KeyEvent gets preventDefault / stopPropagation / defaultPrevented
- render({ tabNavigation: false }) turns the Tab default off

Why JS and not a GPUI KeyBinding: the event callback is a non-blocking
ThreadsafeFunction, so JS runs after GPUI finished dispatching and could
never veto a native binding.

Native:
- keystrokeId on key events, bumped in a root capture listener, so JS can
  group one keystroke's element events and its trailing window event
- root listener always installed; stops Tab after all element listeners so
  the platform never types "\t" into an input
- anchored: explicit transparent background paints no fallback fill
- bounds_tracker pinned at inset 0; bounds were shifted by padding

Primitives (React + Solid):
- Select / Combobox / Tooltip triggers are tab stops, also with asChild
- Select popup traps Tab; outside press no longer steals focus back
- Escape handling moved to the layer stack
- new Dialog: Root, Trigger, Portal, Backdrop, Popup, Title, Description,
  Close (Base UI shape), modal Tab trap and focus restore
- Solid asChild keeps the child's handlers and children

Session: ses_f2717eddcffeXQkI5JXN8eAnFD
2026-09-25 17:13:47 +02:00
Tommy D. Rossi c375e5e566 Replace the README hero with the glass dashboard
The top image was the mail.tax screenshot. Swap in the painted-sky
dashboard and match the caption to the glass window, cards, and schedule.

Session: ses_f354b12f6ffecM3jsw9hX59RHp
2026-09-22 22:02:02 +02:00