Repository navigation
feat(viewer): desktop-style UI features in core, pop-out panels, and ada desktop dev mode, remote servers and SSO - #501
Merged
Merged
Conversation
…ktop ada desktop --dev runs the viewer under uvicorn --reload, the frontend under Vite (HMR) with a config that proxies /api (websockets included), /config.js and /healthz to the viewer, and opens the native shell on Vite. --frontend-url opens an already running Vite instead; --dev-port picks the Vite port. pixi run desktop (new desktop-dev environment: viewer-api's closure and solve group, so only that environment is added to the lock) builds the shell (incremental) and the frontend dependencies through task dependencies on the desktop-shell and frontend environments, then starts ada desktop --dev. Windows only for now; other systems get a clear message. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013iYuG3wvgeMxW9CZvRrSRP
…ut as core commands The built-in Desktop UI's menus lacked these; each is now a core command (CORE_COMMAND_IDS) any composed shell can name, and the Desktop UI's menus list them. - File -> Open... (core:open-file, Ctrl+O): GLB / glTF read in the browser and loaded straight into the scene, no upload, in any viewer. Other formats go through storage on a converting REST server (asked first, then uploaded and loaded through the load queue) and are refused with the reason elsewhere. Files dropped on the 3D view open the same way, added to the scene. - Edit -> Select all (Ctrl+A) / Invert selection: the shown elements of the loaded models, hidden ones and switched-off models never taken; one undo entry each. - View -> Camera -> Top / Bottom / Front / Back / Left / Right / Isometric view: the model framed from that side (Z up, or turned for Y up), animated with camera-controls. - File -> Export selection... / Export model...: a dialog of scope and format over the server's existing STEP / IFC export (the selection scope reuses the object info panel's control); greyed off a REST viewer. - Help -> About: the viewer's build, the API versions, the UI, the connection, the server's reported image tags, documentation links. The upload extensions move to a light module so the picker needs no upload code; the selection export's wiring is shared by the panel and the dialog. VIEWER_CORE_API_VERSION and PLUGIN_API_VERSION 1.14.0 (Ctrl+A and Ctrl+O are now core's reserved chords). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013iYuG3wvgeMxW9CZvRrSRP
…ions Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013iYuG3wvgeMxW9CZvRrSRP
…e check finds it The JSX entity ' is a plain apostrophe in the built bundle, which the provenance check compares literally. Bundle rebuilt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013iYuG3wvgeMxW9CZvRrSRP
… reloading switch A switch between core's own UI and composed shells (Classic <-> Desktop, and any plugin shell without `load`) no longer reloads the page. UiShellHost keeps one root of core's mounted (CoreShellRoot: provider, canvas, layout) and tells it which shell to show; ComposedLayout installs the new shell (modes, arrangements, top bar, panel overrides, look) after undoing the previous one (its palette command, and a plugin shell's own commands, shortcuts and HUDs, parked until it returns). The canvas, the models, the selection and the camera stay. ?ui= is dropped from the address bar; the multi-window dashboard stays the shell the page booted in (bootUiShellId), and new pop-outs boot into it. A shell with `load`, or one that sets the new `reloadOnSwitch`, is still switched by a reload. That reload now carries a deep link (#484's design: carryOnShellSwitch / shellSwitchUrl / deepLinkCarry, ui never carried, a closed model not brought back, a throwing carry skipped) and marks itself so the scene restore brings stored models, the camera, hidden objects and the selection back even with "restore after reload" off. Inside a shell, leaving a shell (not a mode) no longer hides its placed panels, and leaving a shell hands a mode's scene colouring back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013iYuG3wvgeMxW9CZvRrSRP
Core's root mounts one palette host (CorePaletteHost) under AppRoot, so it survives an in-place shell switch and is never mounted twice. It binds the new core shortcut core:command-palette (Ctrl+K, reserved; not while typing in a field), registers the palette's command and draws the palette over listCommands(). Core's own UI now registers core's commands as well and gets a palette button in its top bar; the shortcuts dialog lists Ctrl+K. UiShellChrome.commandPalette: absent means on; `keys` adds a chord of the shell's own beside Ctrl+K (a shell using Shift+K keeps it); `false` turns the palette off while the shell is active. A composed shell no longer mounts a palette of its own. "Reset every mode's layout" is greyed with its reason in a UI without modes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013iYuG3wvgeMxW9CZvRrSRP
…sed shell With the setting off in the classic UI, the Desktop UI's docked layout fell apart: it is built on the split grid that setting turns off. The setting is now the user's per UI shell (LayoutPrefs.customisablePanelsByShell; the one setting of before moves to core's own UI's on load), and a composed shell requires it (UiShellLayout.customisablePanels: "required", the default, or "optional"): always on there, with the Options toggle on, disabled and explaining why. Every reader (the classic preset's zones, the grid, windows, zone keys, the exclusive-group rule, the shortcuts dialog, Options) goes through one selector, layout/customisable.ts, which follows the served shell through an in-place switch, so Classic (off) -> Desktop -> Classic comes back off and the Desktop UI looks as designed in between. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013iYuG3wvgeMxW9CZvRrSRP
… a plugin shell's registrations again
An in-place switch installed the incoming shell while ComposedLayout was
rendering, which notified the mode store's subscribers (the menu bar) mid-render
("Cannot update a component while rendering a different component"). The first
install still runs before the first render; a switch now installs in a layout
effect, before the browser paints. The incoming plugin shell's baseline (what
its plugin had before the shell came up) is read while rendering, before its
chrome's chunks run, and before parked registrations come back, so those are
taken down again the next time the shell is left.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013iYuG3wvgeMxW9CZvRrSRP
… and the core palette Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013iYuG3wvgeMxW9CZvRrSRP
Ctrl+K opens it in every UI and the shortcuts dialog lists it; the button is not needed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013iYuG3wvgeMxW9CZvRrSRP
Until the user chooses in Options, a 3D mouse the page may already use (the desktop app's native reader, or a device the site was granted before) switches support on, as does one plugged in later. getDevices() only, so it never prompts. Choosing on or off in Options is kept from then on. A stored false from before counts as the old default, not a choice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013iYuG3wvgeMxW9CZvRrSRP
ada desktop --server URL|NAME (or ADA_DESKTOP_SERVER) opens a hosted deployment in the native window instead of starting the local viewer. The page runs on the server's own origin, so sign-in, the API and uploads work as in a browser tab. Named profiles live in a per-user servers.json (ada desktop servers list|add|remove); https is required except for loopback or an explicit --insecure. The shell takes --trust-origin and --auth-origin: windows show only the trusted origin (identity provider origins for navigation only, discovered from /api/config and the issuer's OIDC metadata), and native events are granted to the trusted origin alone through a runtime capability that replaces the static loopback one. Origin logic is unit tested. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013iYuG3wvgeMxW9CZvRrSRP
…lation, in every UI While an FEA result is loaded, core adds "Sets" (element sets and node sets, with counts; node sets are informational, since nodes name no faces) and "Properties" (each property field's values and how many elements carry each, read from the field's blobs the first time the rows or a pick need them) to the Scene tree after the models. Selecting element-set or value rows isolates their union: the isolated elements keep their result colours and deformation, the legend measures what is drawn, and the rest is hidden or drawn as its element edges. Each change is one undo entry, the isolation a slice of it. - Rows: utils/scene/fea/feaTreeRows (pure), feaTree (provider, `core:fea-tree` mirror topic, reset on a result change), feaIsolation (scene actions), feaPropertyMembership (lazy reads), state/feaIsolationStore. - TreeNodeData.informational / marked / title; an FEA row menu (isolate, add, remove, clear, rest as wireframe, hide / show); a right-click on an FEA row does not select (and so isolate) it. - Core commands fea-isolate-sets, fea-isolate-selection-sets, fea-clear-isolation, fea-toggle-context-wireframe (palette; Desktop UI's View -> FEA sets), through new scene actions with mirror commands, so pop-out windows act on the main window's scene. - The Selected Object panel's fea-sets section: a picked element's values and sets, each with Isolate. - groupVisibility: applyFeaIsolationOverlays (what undo / redo re-apply), and the overlay rule imported from streaming/visibility instead of the loader. - Facade exports on @/viewer-core/scene, 1.14.0 changelog, UI_SHELLS.md and the Ada Studio FE results section. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013iYuG3wvgeMxW9CZvRrSRP
…default and the palette button Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013iYuG3wvgeMxW9CZvRrSRP
Vite serves the local frontend with HMR on http://localhost:<fixed port> and proxies /api (websockets included), /healthz and /config.js to a hosted server (TLS verified, Host/Origin presented as the server's, cookies forwarded). The window trusts only the localhost origin plus the identity provider origins discovered from /api/config, and the command prints the SPA redirect URI to register. /config.js is fetched and any absolute address of the server rewritten to the dev origin. --dev --server is an alias; plain --dev stays local. New pixi tasks desktop-server NAME and desktop-backend NAME. Fixes the "only apply" grammar in the local-only flag error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013iYuG3wvgeMxW9CZvRrSRP
…op UI
Marking menu: a wheel of up to eight commands, with a short list under it,
at the pointer over the 3D view. Hold the right button still for 300 ms
and it opens; flick towards a command and release to run it, or release
in place and click, or use the arrows and Enter. Shift+E (core shortcut
core:marking-menu, now reserved) opens it from the keyboard. A quick
right click is still the Scene menu and a right drag still pans: the
gesture is a small state machine (MarkingGesture) that holds a
press-time contextmenu back until it knows which of the three a press
is. Once open, the camera's drag is cancelled and the flick's moves are
kept from it. Commands that cannot run are greyed in place with the
reason. Shells configure it with UiShellChrome.markingMenu (items, more,
submenus, gesture). It is off by default in the classic UI, where
Options -> Theme turns it on, and the choice is kept per shell in the
layout prefs.
Tool rail: UiShellChrome.rail may now be {tools} (command ids and
separators) instead of a lazy component, which still works. Each button
takes the command's icon (core's commands have icons now) and shows its
label and chord as a tooltip. It is pressed while a toggle is on, and
greyed with the reason while the command cannot run. Users can remove a
tool from its context menu. The customise dialog adds any command,
reorders, adds separators and resets to the shell's list. The user's
rail is kept per shell in LayoutPrefs.rail; a tool the shell adds later
is put in beside its neighbour.
The Desktop UI has both, with Window -> Customise tool rail… and
View -> Display -> Marking menu. Only the main window has them, since a
pop-out has no 3D view and draws no shell chrome.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013iYuG3wvgeMxW9CZvRrSRP
…users UI_SHELLS.md gains a section on each. The marking menu section covers the gesture and why it is a hold, the platform contextmenu handling, items and slots, the defaults, and where it is on and why. The tool rail section covers the data form of chrome.rail, the user's list and how it is upgraded, and the customise dialog. The Desktop UI's tables list its rail, its marking menu and the new menu items. Ada Studio's user page says how to use both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013iYuG3wvgeMxW9CZvRrSRP
… the shell tauri-build watches the capabilities folder; once the static capability file went (the capability is built at startup), the missing folder left the crate always dirty and every build relinked it (about a minute). A placeholder keeps the folder; an unchanged build is a no-op again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013iYuG3wvgeMxW9CZvRrSRP
…e tool rail Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013iYuG3wvgeMxW9CZvRrSRP
…ws single sign-on
A sign-in against an Entra ID tenant failed with AADSTS900561 ("the endpoint only
accepts POST requests"): after the authorize page the provider POSTs to
login.microsoft.com (passkey / Windows Hello step, /fido/get), a host the window
was not allowed to show. The shell cancelled that navigation and handed the URL to
the system browser, which re-issued it as a GET.
Navigation policy (origin.rs):
* when a sign-in origin is a Microsoft login host, a documented built-in set of
Entra ID sign-in hosts is allowed too (https, default port only);
* once a window has gone from the app to a sign-in page, the provider may send it
on to any https page (federation, MFA) until it is back on the trusted origin;
tracked per window, so federated providers need no --auth-origin per host;
* sign-in pop-ups open as native windows WebView2 navigates itself (opener and
POST bodies kept); no sign-in page ever gets IPC;
* every sign-in navigation is logged with host and path only.
Single sign-on: the shell creates the WebView2 environment with
AllowSingleSignOnUsingOSPrimaryAccount (webview2-com, passed to wry with
with_environment) in the profile folder tauri uses anyway; on by default,
--no-sso (shell and ada desktop) turns it off. A clear error replaces the panic
when another window with other options holds the profile.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013iYuG3wvgeMxW9CZvRrSRP
A mode's toolbar slot now comes with content from core. UiShellMode.toolbars
names core toolbars ("playback", "section", "representation"); the shell mode
toolbar draws them before the mode's own toolbar, so a shell needs no code.
The components (PlaybackToolbar, SectionToolbar, RepresentationToolbar,
CoreModeToolbars, CommandButton) are exported on the layout entry.
New core commands with icons, reachable from the palette, menus, tool rail and
marking menu: results play/pause/stop/step, section planes per axis (toggle,
flip, clear), and the result view switches (undeformed outline, element
edges, result colours). The transport actions move to
utils/scene/fea/playbackActions, shared with the Simulation panel. The Desktop
UI's modes use the toolbars and its View menu lists the commands.
Core has no scene-wide wireframe, transparency or annotation tools, so there
is no toolbar group for them. VIEWER_CORE_API_VERSION and PLUGIN_API_VERSION
1.15.0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013iYuG3wvgeMxW9CZvRrSRP
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013iYuG3wvgeMxW9CZvRrSRP
Number labels: core:labels-nodes and core:labels-elements draw the node and element ids of a loaded FEA result as HTML over the view (NumberLabels, drawn by the overlay layer when chrome.overlays.labels is set). Bounded: the selection's elements, else a sample of at most 4000, thinned on a screen grid to at most 400, redone only when the camera, selection or deformation change. Node ids are the solver's when the bake has them. Colour by: a colorBy mode toolbar (picker and reset) and commands colour the scene by an FEA result's model property (selected as a result field, so the existing painter and legend are used) or by model (range colours, with ColorByLegend). registerColorBySource lets a plugin add sources. The representation toolbar gains the label switches, and the Desktop UI uses all of it. VIEWER_CORE_API_VERSION and PLUGIN_API_VERSION 1.16.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013iYuG3wvgeMxW9CZvRrSRP
…r by Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013iYuG3wvgeMxW9CZvRrSRP
Core gains the pieces a docked shell needs for the procedural builder, and the Desktop UI uses them as a Build mode next to Model and Results. - Commands: core:builder-undo / -redo / -compile / -recompile / -compile-browser / -commit / -close over the cellbuilder store, each greyed with its reason (no model, read off the scene, a connection that cannot edit or compile on a server, a job running, nothing to undo or commit). - A "build" core mode toolbar (BuilderToolbar): model name and revision, an unsaved marker, undo / redo, compile, compile in the browser, commit, close. - UiShellMode.requires: "builder" offers a mode only while a procedural model is open; it is left out of the switcher, Modes menu and palette otherwise, and the shell falls back to another mode when the model goes away. - chrome.overlays.builder mounts the builder's canvas menus and gizmo HUD for a shell that docks the panel (shared CellBuilderOverlays, also used by classic). - Desktop Build mode: the builder on the left (tree and storage behind it), the selection and scene on the right with the catalogs; the panel leaves out its footer and undo / redo there because the toolbar carries them; a Build menu. Viewer-core and plugin API 1.17.0 (additive). Documented in UI_SHELLS.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013iYuG3wvgeMxW9CZvRrSRP
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013iYuG3wvgeMxW9CZvRrSRP
The Simulation panel, the result data panel and the Scene panel (its Model tab) are mirrored, so they can be moved to a window of their own, and the scene tree there gains the Systems tab and the row menu's selection box, unload, saved-group and related entries. - core:fea-controls publishes the result session as plain data (manifest sent when it changes, field / component / step, transport, toggles, colour scale, legend range, edge fade; the playing fields throttled to 100 ms). The panel's controls go through one seam, feaControlActions(): the main window runs the playbackActions / loader / resultSelection code, a pop-out sends one checked command per action (FEA_CONTROL_COMMANDS). - core:fea-table: the pop-out sends its filter and sort, the main window reads the step, applies the same tableRowIndices the table uses and publishes the first 2000 rows with the count matched. - core:scene-info: loaded models and whether each is shown, groups without member lists (capped at 5000, sent when they change), take-off and selection boxes; the panel acts through new SceneActions (setModelVisible, unloadModels, selectGroup, box actions, runRelated). - The Systems tab's select / load by name and the related actions run in the main window from a pop-out. Not in a pop-out: the Sources tab, the Scene panel's other tabs, the FE model and plugin tabs of the Simulation panel, the animation export. VIEWER_CORE_API_VERSION and PLUGIN_API_VERSION 1.18.0. Tests round-trip the topics and commands through a fake bus. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013iYuG3wvgeMxW9CZvRrSRP
…ult data and scene info Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013iYuG3wvgeMxW9CZvRrSRP
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013iYuG3wvgeMxW9CZvRrSRP
PR Review👋 I checked your PR and found no issues. Thanks!
|
🚀 Profiling Results (Top 20 most expensive calls)
|
The mode toolbars, labels and colour by, the Build mode and the pop-out panels each bumped the viewer-core and plugin API on the way (1.15 to 1.18), but none of those versions was released. Fold them into one 1.15.0 with a single changelog entry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013iYuG3wvgeMxW9CZvRrSRP
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013iYuG3wvgeMxW9CZvRrSRP
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013iYuG3wvgeMxW9CZvRrSRP
The conda package test runs against the installed package, which has no frontend sources, so run_dev_backend stopped at the source-checkout check before reaching what the tests assert. Point checkout_src at a temporary folder holding the dev Vite config. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013iYuG3wvgeMxW9CZvRrSRP
… page loads The macOS runner exceeded the 1 s allowance for loading the single-file bundle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013iYuG3wvgeMxW9CZvRrSRP
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-ups to #495. Generic features that alternative UI shells previously had to build themselves now live in core, so a shell's docked UI can be pure configuration. The built-in Desktop UI (
?ui=desktop) uses all of them. The desktop app (ada desktop) gains a dev mode, remote servers and single sign-on.Viewer-core and plugin API go from 1.14.0 to 1.15.0. Every change is additive, and one combined changelog entry is in
viewer-core/index.tsandplugins/registry.ts.Viewer
Shell switching and global commands
chrome.commandPalette. Classic has no palette button; the shortcuts dialog lists Ctrl+K.UiShellLayout.customisablePanels).FEA features
registerColorBySource.Interaction and modes
Marking menu. Hold the right mouse button, or press Shift+E. A quick right-click still opens the context menu, and a right-drag still pans.
Customisable tool rail (
chrome.rail), saved per shell.Mode toolbars from core. A shell lists them with
UiShellMode.toolbars, or mounts the components itself:playbacksectionrepresentationcolorBybuildEvery button is also a core command, so the palette, rail and marking menu can reach it. When a command doesn't apply, it is disabled and says why.
Build mode.
UiShellMode.requires: "builder"offers a mode only while a procedural model is open. The Desktop UI uses it for a Build mode that docks the builder.Pop-out windows
Simulation (Results tab), Result data and Scene info (Model tab) now work in a separate window:
core:fea-controls,core:fea-tableandcore:scene-info;The pop-out tree gains the Systems tab and the row-menu actions. Panels that still need the scene in their own window keep refusing "Move to window".
Desktop app (
ada desktop)pixi run desktop: a dev mode with hot reload. Vite runs inside the native window, withuvicorn --reload.--server URL|NAMEconnects to a hosted server, with saved profiles (ada desktop servers add|list|remove).--dev --backendruns the local frontend source against a hosted server. Pixi tasks:desktop-serveranddesktop-backend.--no-ssoturns it off).Known gaps
Verification
tsc --noEmitis clean.npm test: 2520 tests, with only the 3 known baseline failures (tab, fade, WebGPU pick).npm run buildpasses, and the bundle provenance check prints "OK: all".?ui=desktopcovered:Supersedes #484.
🤖 Generated with Claude Code
https://claude.ai/code/session_013iYuG3wvgeMxW9CZvRrSRP