issue/rhai-component-framework #98

Merged
seb merged 24 commits from issue/rhai-component-framework into main 2026-08-03 21:13:41 -07:00
Collaborator
No description provided.
seb added 14 commits 2026-07-27 10:03:25 -07:00
Replace the LiveKit/WebRTC media stack with Media-over-QUIC (moq) over an
iroh transport, with our server acting as the relay.

Server (server/):
- Embed moq-relay (Cluster + Connection + Auth) on an iroh endpoint as a
  new services::relay module; single-node today, cluster-ready for the future.
- Mint per-channel moq-tokens (HS256) the relay verifies; track live presence
  from origin announcements.
- GetChannelToken now returns {relay_addr, moq_token, broadcast_path};
  GetVoiceChannels reports participants from relay presence.
- Drop livekit-api and the LIVEKIT_* env; add MOQ_* config.

Client (src-tauri/):
- Rewrite channels::manager to dial the relay over iroh H3/WebTransport,
  publish the mic as an Opus broadcast via moq-audio, and discover/decode
  peers via origin announcements into the existing cpal+sonora pipeline.
- Derive active-speakers from RMS; emit room stats from the QUIC connection.
- Same Tauri command/event names, so the frontend is unchanged. Drop livekit.

Infra:
- proto/channels.proto: new token response shape.
- Dockerfile: Rust 1.96 (moq needs 1.95+) plus cmake/clang for aws-lc-sys.
- Vendor upstream source as pinned git submodules under vendor/ (moq, iroh)
  for in-tree reference; exclude vendor/ from deno fmt/lint.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Five fixes plus tests that would have caught two of them.

Server (server/src/services/relay.rs)
- moq-native always constructs the Quinn backend when the `quinn` feature
  is on, and Quinn refuses to bind without a TLS cert. Seed
  `config.server.tls.generate` with a throwaway hostname so the embedded
  Quinn endpoint binds cleanly. Real traffic still flows over iroh.
- Add a spawn smoke test and a token mint+verify round-trip test, both
  guarded by a `Once`-protected `install_default` on the aws-lc-rs
  crypto provider.

Client transport (src-tauri/src/channels/manager.rs, lib.rs)
- Install the `ring` rustls CryptoProvider at process start; without it
  tonic and iroh race to install conflicting providers and panic on the
  first handshake.
- Move the iroh `Endpoint` onto `ChannelsManager` behind a `OnceCell`
  and reuse it across joins; `leave_channel` now only closes the QUIC
  connection. Endpoint binding (DERP discovery, key derivation) is
  expensive enough to be worth caching for the app lifetime.
- Add an `AtomicBool` shutdown flag plumbed into the blocking mic
  thread, set by `leave_channel`. Previously an aborted async mic task
  could strand the blocking thread parked inside `current_signal.next()`
  until the input device next yielded a sample.
- Pick the currently selected iroh `PathId` for RTT instead of always
  reading `PathId::ZERO`, so RTT stays correct across path migration
  (direct vs relay).

Frontend (app/components/sidebar/channel-group.tsx)
- The relay's presence map updates asynchronously after the broadcast
  announcement lands, so a single invalidate-on-join races ahead of it
  and renders zero participants. Poll `["channels"]` every 1.5s while
  `currentChannelId` is set so the sublist fills in once presence
  catches up.

Tooling (deno.jsonc)
- Exclude `.claude/worktrees` from fmt/lint; sibling-branch worktrees
  have their own (unrelated) sources and routinely diverge in style.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Voice previously depended on iroh's public infrastructure, which is
canary-tier while 1.0 is in release candidates: clients resolved the
server's endpoint through n0 DNS and fell back to n0 canary relays for
NAT traversal. In practice (home-hosted server) this produced zombie
sessions: broadcasts announced at the relay and died ~1s later, wiping
presence for everyone, while clients sat in dead calls indefinitely.

Server:
- Embed iroh-relay in the server binary (IROH_RELAY_BIND), a plain-HTTP
  websocket service meant to sit behind a TLS reverse proxy. With
  IROH_RELAY_URL set, the endpoint runs fully self-contained: custom
  relay map, no n0 discovery, no n0 relays.
- Advertise the relay URL and the endpoint's direct addresses in
  GetChannelToken, so clients dial without any third-party discovery.
  MOQ_IROH_BIND_V4 pins the voice UDP port for router forwarding;
  MOQ_IROH_PUBLIC_ADDRS advertises the WAN address.
- Track presence as a per-DID broadcast count instead of a set, so an
  announce/unannounce reorder during reconnect can't drop a live
  participant.

Client:
- Cache the shared iroh endpoint per relay URL and rebuild it (with a
  graceful close) when the server's relay changes; dial with the full
  EndpointAddr (relay URL + direct addresses).
- Watch each call's connection for unexpected closure, clear native
  state, and emit channel_disconnected; the frontend shows
  "reconnecting" and rejoins with exponential backoff.
- Retry peer catalog subscribes (3 attempts) and bound the initial
  catalog read with a timeout so a stalled peer can't block the
  announce loop.

Tests: catalog_round_trips_through_relay_between_two_peers drives the
real client publish/subscribe path through the embedded relay on
loopback with zero external infrastructure. Token-key paths in tests
must stay relative: moq-relay's Auth URL-parses the key string first,
and absolute Windows paths (c:\...) parse as URLs.

Toolchain: the pinned moq crates declare rust-version 1.95; built and
tested with rustc 1.96.0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Enabling the embedded iroh relay previously required setting both
IROH_RELAY_BIND and IROH_RELAY_URL, and the fallback derivation only
produced localhost. The server already knows the hostname clients use
(PUBLIC_HTTP_URL), so reuse it: with just IROH_RELAY_BIND set, the
advertised URL becomes http://<public host>:<relay port>. An explicit
IROH_RELAY_URL is now only needed when the relay sits behind a TLS
reverse proxy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
async move blocks only capture variables they mention, so binding the
broadcast and catalog producers outside the spawned task dropped them as
soon as join_channel's setup block ended. The drop unannounced the
broadcast at the relay milliseconds after it was announced, which is why
nobody ever appeared in the participant list and peers' catalog reads
failed with "moq: cancelled". Bind them inside the async block so the
task actually owns them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add a deployment guide covering the server configuration
Some checks failed
CI / test (pull_request) Successful in 9m17s
CI / test-ui (pull_request) Successful in 43s
Deno / lint (pull_request) Successful in 11s
Rust / lint (pull_request) Failing after 2m27s
Rust / build (pull_request) Successful in 8m22s
f7b1c1387f
Document every env var the server reads (auth, database, HTTP, and the
voice/relay stack), the ports to expose, reverse-proxy considerations,
and a fresh-deployment checklist, with a pointer from the README.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Begin replacing Tauri+React with a Rust-native GPUI app. This commit
carves all non-UI backend logic out of src-tauri/ into standalone
workspace crates, decoupled from Tauri:

- microclimate-store: tiny JSON KV store replacing tauri-plugin-store,
  persisting under directories::ProjectDirs.
- microclimate-proto: gRPC clients/messages (ts-rs/tauri-build dropped).
- microclimate-auth / -users: lifted verbatim minus Tauri imports.
- microclimate-audio: AppHandle::emit -> tokio::sync::watch<MuteSnapshot>
  bus (MuteEngine + ptt_listener publish through it); pure command
  helpers moved to controls.rs.
- microclimate-channels: active_speakers/room_stats/connection_quality/
  channel_exited now flow through a ChannelsEvents bus (watch+broadcast);
  tauri::async_runtime::spawn -> tokio::spawn.
- microclimate-app: new GPUI binary with bridge.rs (tokio runtime +
  managers + PTT supervisor) and a placeholder window.

src-tauri/ and the React app/ remain in place and are excluded from the
workspace; they'll be removed once the GPUI screens reach parity.

Verified: cargo check --workspace clean; cargo test 143 passed/0 failed.
The GPUI binary compiles up to gpui_macos's Metal shader build step,
which requires full Xcode (CLT-only machines cannot build it).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaces the React/Tauri client with a GPUI + gpui-component port across
five planned platforms (mac/linux/windows now; web/mobile stubbed).

New crates:
- ui: shared GPUI views, holds AppCore (moved from app/bridge.rs) plus
  Entity<AppState> routing. Four routes wired end-to-end to the existing
  backend managers: /connect (ATProto OAuth), / (channels + participant
  tiles + mute/deafen), /settings/audio (devices, volumes, PTT toggle),
  /settings/admin/users (allowlist).
- app-web, app-mobile: registered in the workspace as documented stubs;
  blocked on cpal/livekit/rdev not building for wasm/iOS/Android targets.

Existing crates:
- app: slimmed to a thin desktop shim (~30 lines).
- channels: transport.rs added as the documented seam where the future
  LiveKit replacement plugs in.
- audio: capture.rs added as the parallel seam for the cpal-coupled
  input/output managers.

Workspace:
- Both gpui and gpui-component pulled from their default branches so cargo
  unifies them into one zed rev (gpui-component pulled its own, otherwise
  we'd have two incompatible View types in the build).
- rust-toolchain.toml pins nightly — gpui main uses the cold_path
  intrinsic.

Deleted: app/, src-tauri/, package.json, deno.jsonc, deno.lock, vite +
vitest configs, tsconfig*.json, react-router.config.ts, components.json,
public/, stories/, .storybook/. Cargo.toml exclude drops src-tauri.
README rewritten to drop the JS toolchain.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add dark theme tokens, Tabler icons, ATProto avatar fetching, and a
draggable sidebar (replaces the prior collapse toggle). Stack the audio
settings into full-width sections so device names render uncut, make the
PTT key editable in-app instead of via config.json, and wire a visible
scrollbar over the inset pane.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
cargo fmt --all under the rust-toolchain.toml nightly produces
different output than the rustfmt these files were written with;
formatting now so the pre-commit hook and CI fmt checks run clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The rebase onto the moq port brings the GPUI workspace and the React
deletion together; this fixes everything around the edges that still
pointed at the old stack. The pre-commit hook and lint workflow now
format/lint the client workspace instead of src-tauri, the deno
checks are gone along with the JS toolchain, and the release workflow
ships the server binary only until the GPUI app gets its own
packaging pipeline (left as a TODO in the workflow). vendor/README
now points at the workspace manifest for the pinned moq/iroh deps.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replays the sonora work (968dfa5) onto the workspace audio crate,
which had forked from the pre-sonora src-tauri code. processor.rs
arrives verbatim (AEC/NS/AGC stages on a background thread with a
fast passthrough when everything is disabled); input capture now
routes raw frames through the processor and returns the thread
handle in CapturedAudio so callers keep it alive for the call; the
output mixer feeds mixed frames to the AEC render tap with a
recycled buffer pool; and AudioManager loads/persists the config
under audio-apm-config. The UI call sites stash the processor in
AudioManager.active_processor on join and device swap.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All screens are now authored as .rhai scripts driving a recorded-builder
DSL (crates/ui/src/rhai): on-disk packs with hot reload and a bundled
include_str fallback, a Rhai module system, persistent input/slider/
select/scroll state, host-API effects over an async channel, and fixture
seeding for deterministic screenshots. The old native shell/routes/
components are deleted.

Also included:
- Backend telemetry parity: RFC3550 inter-arrival jitter EWMA and a real
  connection_quality publisher (local RTT/loss classifier + remote
  frame-age ticker) in crates/channels
- Web-fidelity theme: exact Tailwind stone palette, emerald actives,
  embedded Inter (OFL), extended style DSL (hover, cursor, side borders/
  padding, scroll regions, mono/tabular text, hex color literals)
- Route-enter reinit so screens refetch against the current session, and
  in-call presence polling (the server presence map lags the relay
  announcement, as the web client documented)
- Vendored gpui_windows with the DComp first-present reassert patch and
  a render-storm self-report
- Pin rustls to the ring CryptoProvider (aws-lc-rs is also in-tree via
  iroh, so rustls cannot auto-select)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add the default UI pack, screenshot fixtures, and capture harness
Some checks are pending
CI / test (pull_request) Waiting to run
CI / test-ui (pull_request) Waiting to run
Rust / lint (pull_request) Waiting to run
Rust / build (pull_request) Waiting to run
37bd8f9d6b
packs/default recreates the reference web UI in Rhai: home (emerald
active channel rows, participant sub-rail, tiles with quality dots and
paint-only speaking rings, floating stats), settings (audio two-column
layout with APM disclosures and PTT key capture, users with add-user
typeahead dialog, development), connect, shared lib modules
(menu_button, quality, user_button, stats_panel, settings_shell,
server_rail, widgets), and a widget gallery.

packs/fixtures seed deterministic state for scripts/screenshot.ps1,
which launches each scenario, captures via PrintWindow, rejects
near-black frames, and retries around the known first-present flake.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Close the React-parity gaps: reconnect, toasts, affordances, sidebar
Some checks are pending
CI / test (pull_request) Waiting to run
CI / test-ui (pull_request) Waiting to run
Rust / lint (pull_request) Waiting to run
Rust / build (pull_request) Waiting to run
79de438f7b
Feature-parity fixes against the reference/web React client:

- Auto-reconnect: the channels manager now emits channel_disconnected
  (with the channel id) on unexpected QUIC closes, and a UI-side driver
  rejoins with 1s -> 30s backoff (6 attempts), surfacing progress as a
  "Reconnecting..." row under the channel and a failure toast if all
  attempts are exhausted.
- Toast layer: RootView pumps audio alerts (sticky, keyed per kind,
  cleared on recovery/channel exit) and a new AppCore toast bus
  (profile/add-user/remove-user failures, plus a script-facing toast())
  into gpui-component's window notifications - alerts are now visible
  on every screen, not just home.
- Failure/loading legibility: skeleton() element plus loading states for
  the channel list, participant rows/tiles, user button, and user cards;
  inline error states for channels/users fetch failures and unresolvable
  profiles (which no longer clobber known-good profile data); tooltip()
  on buttons and containers (mute/deafen titles, PTT-locked mute, the
  AGC-managed volume slider, participant tiles).
- Sidebar: collapsible to an icon rail via Ctrl/Cmd-B or trigger
  buttons (persisted in the config store), with edge-triggered
  auto-collapse below the web client's 768px breakpoint.
- Behavior regressions: AGC off->on resets input volume to 100% and
  locks the slider; mute/deafen, device selects, and test buttons lock
  during audio tests; sliders commit on release with live-drag labels;
  PTT key clear button; mic-test countdown + progress bar; channel_exited
  clears stale active-speaker/quality/stats state.

New collapsed-sidebar screenshot fixture + scenario; all harness
scenarios re-captured and verified.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Merge origin/main (MOQ port #97) into the Rhai rewrite
Some checks failed
CI / test (pull_request) Has been cancelled
CI / test-ui (pull_request) Has been cancelled
Rust / lint (pull_request) Has been cancelled
Rust / build (pull_request) Has been cancelled
e18e63befb
Reconciles the branch with main's squash-merged MOQ port. The React/Tauri
trees main touched (app/, src-tauri/, deno tooling) stay deleted — their
behavior already lives in crates/ — while main's post-port fixes carry
over into the native code:

- Stable CPAL DeviceId-based device selection (crates/audio/src/types.rs):
  device lists are (id, name) pairs, selection/persistence use the id so
  the chosen device survives enumeration-order changes and reboots. The
  settings UI maps display names back to ids on select.
- Audio-processor thread panic-catching, recorded into capture_error so
  the capture watchdog can name the real cause.
- rebuild_input_signal now falls back to the system default (updating the
  persisted selection) when the selected input vanished mid-call.
- ChannelsError: From<anyhow::Error> + Media display test.
- Server: relay fixes from #97 (Cluster::new error context, cfg(test) on
  direct_addrs, moq catalog API rename) and dependency bumps.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rhai proved the component-framework architecture but is a poor long-term
authoring language: no macros, no module-level composition, and a builder
API that reads like Rust wearing a costume. This starts the move to Steel,
an embedded Scheme, keeping the architecture and the React/Tauri parity
target intact. Both runtimes coexist until every pack is ported.

A gating spike (crates/ui/examples/steel_spike.rs) settled three things the
docs did not:

* register_fn requires Send + Sync + 'static regardless of the "sync"
  feature, and there is no non-Send registration path. Host closures
  therefore capture nothing and reach per-component state through a
  thread-local registry, which is sound because every engine and component
  lives on the gpui main thread.
* Engine construction costs ~1.2s in debug (~110ms release), so one engine
  per component was untenable. All components share one engine and load as
  prefixed modules; hot reload re-requires in place in ~10ms, which is
  faster than Rhai's fresh-engine rebuild.
* Steel's compiler recurses on the Rust stack ~70 nesting levels per MB in
  debug builds, overflowing 1MB between depth 70 and 80. /STACK:16777216 on
  the Windows targets lifts that past depth 1000; real trees are 10-25 deep.

Components build a plain-data element tree via keyword-argument constructors
in a Scheme prelude, and the host decodes it once per render into the same
Kind/Style structs the Rhai materialiser used. Falsy children are dropped
and list children spliced, so "when" and "map" compose without building
vectors by hand. Handlers are ordinary closures lifted into a rooted table
with the tree carrying indices, which retires Rhai's dual-dispatch invoke()
and its curry() workarounds entirely.

Script failures now render Steel's codespan diagnostic - error code, source
line, caret - in an in-app panel instead of only reaching the log, and a
load-time error is kept in preference to the "does not define render"
symptom it causes.

The materialiser is a faithful port: the inline(never) split, depth cap,
children-first boxing, and three-pass styling all carry over, since that
structure exists to survive debug-build stack frames.

Verified: widgets_demo.scm renders pixel-identical to its Rhai counterpart,
7 new tests cover the prelude, prefixing, handler identity, and diagnostics,
and the workspace suite is green at 160.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Brings the Scheme runtime up to the Rhai one's capabilities: all 44 host
functions, the async effects queue, the nine live subscriptions, the
reconnect driver, and fixture-seeded state for the screenshot harness.

Effects keep the same shape as before and for the same reason: SteelVal is
Rc-based and therefore !Send, so AsyncMsg carries only Send primitives
across the tokio boundary and converts to Steel values on the gpui thread.
State keys move to lisp-case throughout (current-channel, mic-test-level,
slider-live-<key>) so scripts spell them the way they spell everything else.

Two things the port turned up:

* Steel keeps exact integers as IntV, and its f64 and bool extractors match
  only NumV and BoolV. A strict signature would have made
  (set-input-volume 100) raise a conversion error and abort the handler,
  while 100.0 worked - a trap for anyone writing a pack. Numeric and boolean
  host arguments now coerce through num_of/truthy, the latter following
  Scheme truthiness rather than requiring a literal boolean.
* A keyword written without its value silently swallowed the following
  keyword, so #:mono #:truncate #t parsed as #:mono with the value
  #:truncate. No prop legitimately takes a keyword as its value, so the
  prelude now rejects it outright.

Fixtures load as a module under a reserved prefix, so a fixture's seed can
never collide with a component binding, and a seeded component skips both
init and its subscriptions exactly as before.

Packs ported so far: connect, lib/widgets, lib/quality, lib/menu_button,
lib/participant_tile, and the widgets fixture. connect.scm also closes a
parity gap the Rhai version never had - the OAuth screen now shows the
authorization URL as a fallback when the browser does not open on its own.

Verified: the widgets fixture renders pixel-identical to its Rhai
counterpart, and the workspace suite is green at 161.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All seven components, eight shared libs, and four fixtures now exist as
.scm alongside their .rhai originals, and every screen renders identically:
a pixel diff of all ten screenshot scenarios shows no content differences.

The Scheme versions are markedly shorter than what they replace, because the
prelude absorbs the patterns Rhai needed by hand. Dropping falsy children
turns the `if cond { x = x.child(...) }` rebuild dance into an inline
(and cond ...), splicing list children turns array-building into (map ...),
and lambdas closing over their environment retire the Fn("name").curry(id)
contortion that existed only because Rhai handlers could not capture.

Two harness improvements came out of the comparison:

* settings_development was the only scenario running without a fixture, so
  it resolved the signed-in profile over the network and whether the user
  card had arrived by capture time was a coin flip - the screen genuinely
  differed between runs of the *same* runtime. It now uses the demo fixture
  like every other settings screen, and the two runtimes match exactly.
* The modals fixture was stale: it seeded bare strings for the device lists,
  but settings_audio was updated to expect {id, name} records, so that
  screen would have failed on the Rhai side too. The Scheme port seeds
  proper records.

A new test loads every shipped component and asserts that each entry point
it defines actually resolves under its module prefix. An init that silently
fails to resolve looks fine on screen but never fetches anything, which is
the kind of failure that otherwise only shows up as a mysteriously empty
sidebar.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RootView and the demo hooks now construct SchemeComponents against the
shared Steel engine, so nothing reaches the Rhai runtime any more. With the
last consumer gone, crates/ui/src/rhai (five files, ~4600 lines), all twenty
.rhai packs and fixtures, and the rhai dependency are removed; cargo tree no
longer mentions it and the crate builds with zero warnings.

Env vars are engine-neutral now - MC_UI_DEMO and MC_UI_FIXTURE rather than
MC_RHAI_*. The name deliberately says nothing about the interpreter, so the
next time the scripting layer changes the harness does not have to. The
screenshot scenarios collapse back to one set per screen instead of the
side-by-side pairs used during the cutover.

Two things were dropped rather than carried over:

* The Select element had a placeholder field that nothing ever read -
  gpui-component supplies its own empty-state text. Keeping it would have
  meant shipping a documented prop that silently does nothing.
* The M0 spike binary is gone. What it established - the Send + Sync bound
  on registered functions, engine construction cost driving the shared-engine
  design, and the ~70-nesting-levels-per-MB stack budget behind /STACK -
  is recorded where it is actually needed: the module docs that depend on it.

Verified: all eleven screenshot scenarios render correctly from the
Steel-only binary, including the full application shell, and the workspace
suite is green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wait for a component file to be readable before reloading it
Some checks are pending
CI / test (pull_request) Waiting to run
CI / test-ui (pull_request) Waiting to run
Rust / lint (pull_request) Waiting to run
Rust / build (pull_request) Waiting to run
5164f259cf
Editors save by writing and renaming, and on Windows the file is exclusively
locked for the moment in between. When a reload lost that race Steel surfaced
an os-error-32 IO failure as a script diagnostic, so a perfectly valid save
could leave an error panel on screen until the next one - indistinguishable,
to the reader, from a bug in their own code.

The reload now waits (bounded, 200ms) for the file to open before requiring
it. Verified by driving a live edit, a deliberate break, and a fix against a
running window: the title swaps without a restart, the break shows a
diagnostic naming the offending identifier, and the fix recovers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Center the sidebar title and move the collapse trigger to the footer
Some checks are pending
CI / test (pull_request) Waiting to run
CI / test-ui (pull_request) Waiting to run
Rust / lint (pull_request) Waiting to run
Rust / build (pull_request) Waiting to run
696d5a13b7
The trigger sat in the header next to the title, which pushed "microclimate"
off-centre and put a control in the spot the eye reads as a heading. It now
lives in the footer's trailing corner, beside the mute/deafen row, and the
title is centred. The settings sidebar gets the same treatment so the trigger
occupies the same corner on every screen, and the collapsed rail stacks it
under the audio controls so it does not appear to jump when the sidebar
collapses.

Doing this surfaced a bug in the runtime: bundled `lib/` modules were
registered unconditionally, and a module registered under the name a
component requires wins over the search path. The compiled-in copy therefore
shadowed the on-disk one, so edits to any shared lib were silently ignored -
at startup and on hot reload alike. That is how the settings sidebar kept
rendering its old layout from a file that had already changed. The bundled
libs are a fallback, as their comment always claimed, so they are now
registered only when there is no pack directory on disk. A test pins it,
since the failure mode is invisible: the edit simply never takes effect.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Connect the gRPC clients on login, submit forms on Enter, dismiss the user menu
Some checks are pending
CI / test (pull_request) Waiting to run
CI / test-ui (pull_request) Waiting to run
Rust / lint (pull_request) Waiting to run
Rust / build (pull_request) Waiting to run
e641495346
Three fixes from driving the app.

**Channels never loaded after signing in.** The channels and users gRPC
clients are built in AppCore::new, and only when a server URL was already
stored from a previous run. A session established during the current run
therefore left both managers clientless, and every fetch failed with "no
gRPC client connected" until the app was restarted - surfacing as "Failed
to load channels" on the screen the user lands on immediately after login.
start-login now connects both once await_login succeeds, before reporting
completion. This was never a Steel regression; the same hole existed in the
Rhai build, and the same fix would have applied there.

**Enter did nothing in the connect form.** Inputs only listened for Change,
so the form could not be completed from the keyboard. They now also handle
PressEnter and dispatch a new `on-submit` entry point keyed by input name.
It is an entry point rather than a per-element prop deliberately: the
subscription is created once per input and outlives any single render,
while handler indices are rebuilt every render, so capturing one would go
stale. Shift-Enter is left alone for any future multi-line input. connect
signs in; settings_users adds the user, honouring a picked typeahead
suggestion. Both guard against firing while a request is already in flight.

**The user menu could only be closed by clicking the card again.** The
dropdown now renders as an overlay at the screen root instead of beside the
profile card, with a full-window scrim that closes it. It has to live at the
root: nested in the sidebar, the scrim is painted before the main area and
so would never catch a click there. Clicking the card while open lands on
the scrim, which still reads as a toggle.

A new usermenu fixture and home_user_menu scenario cover the open menu,
which no existing fixture reached.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Stop the dismiss scrim from eating clicks meant for the user menu
Some checks failed
CI / test (pull_request) Has been cancelled
CI / test-ui (pull_request) Has been cancelled
Rust / lint (pull_request) Has been cancelled
Rust / build (pull_request) Has been cancelled
5315873dc9
Making the menu a child of its dismiss scrim broke Settings and Sign Out.
Container clicks materialise as on_mouse_down, while gpui-component Buttons
fire on mouse-up: the scrim's mouse-down ran first, closed the menu, and the
button was gone by the time the mouse-up that would have triggered it
arrived. The menu still closed, so it looked like the item did nothing.

Sibling-vs-child was not the real axis - a full-window scrim covers the
menu's area either way, and gpui dispatches to every listener whose bounds
contain the point. What matters is which one consumes the event. gpui's
bubble phase walks listeners in reverse paint order and stops on
stop_propagation, so the fix is to make the topmost element consume:

* a new `#:swallow-clicks` prop, which a popover panel uses to absorb
  mouse-down so the scrim beneath it never sees clicks aimed at the panel;
* container `#:on-click` handlers now stop propagation once they have run.

The second one is a fix in its own right. Without it, clicking the profile
card while the menu was open would dismiss via the scrim and then toggle the
card underneath, reopening it immediately - the menu would have looked
impossible to close by clicking the thing that opened it. Consuming a
handled click is also the behaviour a script author would assume.

Not verified interactively: synthetic mouse input does not reach the gpui
window on this machine (cursor moves and hover states respond, clicks are
dropped), so this rests on gpui's documented dispatch order rather than a
driven test. Rendering is unchanged - the menu screenshots are identical.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
seb changed title from WIP: issue/rhai-component-framework to issue/rhai-component-framework 2026-08-03 21:13:38 -07:00
seb merged commit 158a1c515c into main 2026-08-03 21:13:41 -07:00
seb referenced this pull request from a commit 2026-08-03 21:13:42 -07:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
puregarlic/microclimate!98
No description provided.