Files
paseo/packages/app/gpui
Mohamed Boudra 291c8ad1a8 fix(gpui): stabilize macOS packaging, connections and appearance
Release terminal WebSocket clients on the JS thread so daemon retries do not exhaust descriptors or join their own IO thread. Preserve Cargo symbols and seal ad-hoc macOS bundles with a library-loading policy that works with hardened runtime.

Use rounded border decoration without changing Fabric child layout, and route desktop hover through pointer events and Pressable render props. Include native regressions and reproducible package verification.
2026-11-08 04:40:38 +01:00
..

Experimental GPUI client

Run Paseo's existing React Native app in a native GPUI window on Linux or macOS. This is a community starting point: the real workspace UI runs, but terminal/diff renderers and several native services still need porting. The normal desktop build remains Electron. The native app connects to a separately running daemon.

Run it

Start from the repository's development setup. A fresh checkout needs the root dependencies and workspace build outputs:

npm ci
npm run build:server
npm run gpui:install
EXPO_PUBLIC_LOCAL_DAEMON=localhost:6768 npm run dev:gpui

Use an already-running compatible daemon and replace localhost:6768 with its endpoint. These GPUI commands never start or restart a daemon. The address is inlined at bundle time. The app's normal connection/authentication rules apply.

You also need Rust stable through rustup, CMake, Ninja, Clang, Linux X11/Wayland development libraries, and Vulkan. The tested Linux x86_64 host already had most build tools; it additionally needed libxkbcommon-x11-dev. A clean-machine prerequisite check is still needed. The first native build downloads dependencies and builds Rust/C++; allow several minutes and several GB of disk.

dev:gpui bundles the app, autolinks native modules, builds, and opens the window. Restart after editing. It uses an offline production JS bundle; Fast Refresh is not wired into this launcher. build:gpui builds without opening a window. gpui:bundle -- --dev false only writes the JS bundle, source map, and assets to .artifacts/gpui/. Bundle validation checks that the real routes are present and that no react-native-webview modules were included.

Build a macOS test DMG

On an Apple Silicon Mac, install the dependencies above plus Xcode, CMake, and Ninja. From the repository root, after npm run gpui:install:

gpui_repo_root="$PWD"
cd packages/app/gpui
MACOSX_DEPLOYMENT_TARGET=15.0 NODE_OPTIONS=--max-old-space-size=6144 CARGO_BUILD_JOBS=4 \
  CARGO_TARGET_DIR="$gpui_repo_root/.artifacts/gpui/macos/target" \
  PASEO_RENDERER=gpui EXPO_PUBLIC_LOCAL_DAEMON=localhost:6767 \
  node node_modules/react-native/cli.js package-desktop \
    --target app,dmg --out "$gpui_repo_root/.artifacts/gpui/macos/dist"

Use the React Native CLI entry point shown here; the standalone GPUI CLI rejects package-desktop. Keep one absolute CARGO_TARGET_DIR for both compilation and packaging because their default target directories differ. Metro needs more than Node's default 2 GiB heap for this app. Keep MACOSX_DEPLOYMENT_TARGET aligned with desktop/desktop.json when building from source; it controls both Rust and CMake's macOS API availability.

The DMG requires macOS 15 or later, matching the bundled Hermes/JSI libraries. It contains Paseo GPUI.app with ad-hoc signing and the library-validation exception required for loading ad-hoc dylibs under hardened runtime. It is not notarized or signed with Developer ID. Copy it out of the mounted DMG before testing. Its daemon address is fixed at bundle time, and the installer does not include a daemon. Native terminals and diff rendering remain unsupported.

Check the installed bundle before sharing it:

node packages/app/gpui/verify-macos-package.mjs "/Applications/Paseo GPUI.app"

This checks the sealed signature, ad-hoc library-loading entitlement, and the minimum macOS version of each bundled dylib. A successful launch on a Mac with SIP disabled does not test Team ID library validation; verify the signing policy and use a Mac with SIP enabled for distribution launch acceptance.

Namespace GUI testing needs an active remote desktop session; SSH alone can leave GPUI waiting for a display. Use Namespace's devbox desktop commands before launching the app. Keep Cargo's generated debug-symbol symlink intact when rebuilding; the packager copies its contents alongside the installer.

Approach and renderer choice

Existing Paseo RootApp + Expo Router + state/client/plugin code
                         |
                  React in Hermes
                         |
               React Native Fabric
                         |
              Rust GPUI native renderer
                         |
                  Linux GPU window

Existing Paseo client <-- WebSocket --> separately running Node daemon

The separate npm project is deliberate: the selected runtime uses RN 0.87.1, while Paseo's mobile app uses RN 0.86.3. Metro redirects native dependencies to that installation and resolves the rest from the app. This avoids changing the mobile dependency baseline just to try another renderer. It also creates a version-maintenance cost that needs testing before this becomes a supported target.

The choice was to preserve the existing app's RN component and library contracts:

Candidate Integration model Consequence for this experiment
Streetwriters react-native-gpui React Native Fabric, Hermes, and GPUI, with ports of screens, gestures, Reanimated/worklets, safe areas, and SVG Chosen because it lets the existing RN app reach a real Linux window with targeted adapters. These ports are still experimental.
natew/react-native-gpui Custom React reconciler with an RN-like component surface; its README targets macOS arm64/Metal Useful implementation reference. Running this Linux app would also require platform work and compatibility with the RN libraries it imports.
GPUIX Custom React/Solid renderer, its own JSX primitives, and Node-API native bindings; supports Bun/Node execution Also native GPUI, with interesting packaging and specialized surfaces. Adopting it would require replacing or adapting Paseo's RN component/library contracts. It remains an alternative worth testing, not a rejected renderer.
Existing Electron target RN Web inside Chromium, with the desktop host bridge Preserves the working desktop feature set, but does not answer the experiment's question of running the app through a native renderer.

This is an integration choice, not a performance comparison. There are no controlled CPU, memory, startup, or frame-time results against these alternatives.

What belongs in the change

Area Why it is here
vendor/react-native-gpui/ Editable, pinned runtime source. This accounts for roughly 130,000 lines of the diff. GitHub marks it as vendored; inspect UPSTREAM for provenance and the local patches.
npm and Cargo lockfiles Reproduce the experiment's dependencies. Roughly 15,000 generated lines, collapsed on GitHub.
desktop/, index.js, routes.js, Metro/Babel config, root launcher Register the full app, select the native runtime, compile native services, and bundle the actual route graph. The explicit route context prevents an apparently successful build with no app routes.
adapters/ Desktop implementations of the app-used APIs, plus explicit unavailable-capability implementations for mobile features. About 700 lines; these are limited ports, not complete replacements for Expo/Unistyles.
Shared app edits One capability gate for alpha-mask shimmer. GPUI shows the logo and labels without that effect. Existing web/iOS/Android paths keep their behavior.
Native tests and this guide Make the working slice reproducible and give contributors bounded follow-up work.

Upstream examples, its tester/testing package, website, unused fonts/vector-icon ports, starter navigation dependencies, sample icon, and unused nested Cargo config are omitted. Build outputs, native binaries, downloaded SDKs, logs, screenshots, and the exploratory size-measurement copy stay out of Git.

The retained runtime still includes its build machinery, platform backends, headers, and tests. Arbitrarily deleting those to reduce line count makes an upstream refresh harder. Vendoring can later be replaced by a pinned dependency once the local patches are upstreamed and distribution is reproducible.

Editing the runtime

Edit vendor/react-native-gpui, then run npm run gpui:install before rebuilding. npm copies local packages rather than symlinking them; the install command refreshes those copies even when their versions have not changed.

There are two local renderer changes:

  • node_view.rs: a pointerEvents="box-none" view must not install its own hit area. Before the patch, Paseo's empty portal overlay intercepted sidebar clicks.
  • StatusBar.desktop.js: desktop windows do not have the mobile OS status bar.

GPUI remains a pinned Cargo git dependency. Upstream tooling downloads Hermes and React Native prebuilt libraries. The snapshot is not an offline or fully source-built distribution, and release packaging has not been verified.

Working slice and limits

Verified on Linux x86_64 and macOS arm64: all 24 app routes in the bundle, connection to the daemon, the real project/workspace sidebar, opening an existing workspace, existing message rendering, and native composer typing/deletion. The native regression test restores the draft and does not send a message. Rust tests cover SHA-256/random-byte bounds and real SQLite parameter binding/transaction rollback. The macOS build also passed draft restoration after restarting the native process.

The following are not feature-parity claims:

Area Current state
Styling Adapter evaluates existing Unistyles theme/style factories and breakpoints. No Nitro shadow-tree integration. Full theme/resize behavior needs QA.
Storage/crypto/localization Native filesystem + SQLite + SHA-256 + OS randomness. Intl ships English locale data and IANA time zones. The Rust service contains Linux-specific OS access.
Clipboard/files Text clipboard, native file dialogs, PNG/JPEG picking, and download/export adapters exist. End-to-end dialog/transfer coverage is still missing.
Voice/camera/push/power inhibition Unavailable. Requests either report the missing capability or use the app's existing unavailable-capability implementation.
Image conversion/image clipboard Unavailable. The picker accepts PNG/JPEG without conversion.
Mermaid/HTML/document previews Mermaid and HTML show source. Document previews report unavailable. No embedded browser backend.
Terminals The mobile view expects an Expo PaseoTerminal component. No GPUI implementation yet.
Diffs The mobile surface uses React Native Skia. No GPUI drawing backend yet.
Plugins The existing loader evaluates JS and injects host React/RN. No end-to-end plugin verification on GPUI yet.
Other platforms Linux x86_64 and macOS arm64 have been exercised. Windows remains unverified.

The initial stripped development executable was about 68 MiB, with about 10 MiB of Hermes/JSI libraries and a 21 MiB JS bundle, approximately 100 MiB uncompressed. That measurement excludes the daemon, is not an optimized release installer, and does not establish memory usage or a size advantage over another renderer.

Experiments to pick up

Each experiment should preserve the real app and demonstrate a user workflow.

  1. Native terminal. Implement a GPUI component for the existing PaseoTerminalView frame contract. Keep the JS headless xterm emulator, transport, and frame generation initially. Verify resize/replay, keyboard/IME/paste, scrollback/selection, and the existing Unicode corpus. Terminal rendering explains why shaping must preserve xterm's cell columns.
  2. Diff surface. Keep src/git/diff-document's model and review actions; add GPUI text measurement and painting instead of the Skia surface. Test unified/split views, wrapping, scrolling, selection, and inline reviews on large real diffs. The C++ layout experiment is a separate candidate for shared geometry; it is not a finished renderer.
  3. Plugins. Load a small plugin into an isolated daemon, open its real screen or workspace panel, call a plugin RPC, and reload it. Verify subscriptions and cleanup. Track unsupported services explicitly instead of adding fake APIs.
  4. Native services and styling. Exercise dialogs, downloads, clipboard, persistence/restart, theme switching, resizing, and focus. Replace the narrow adapters with complete implementations only where the app needs them.
  5. Portability and packaging. Build on clean Linux, then macOS/Windows. Replace Linux-only randomness/timezone access, check font/shaping and IME behavior, and verify relocatable libraries/assets. Measure a release build and a controlled workflow before claiming a performance or size improvement.
  6. Bundled daemon, independently. First run the unchanged daemon under Bun in isolation. Verify PTY/native dependencies, terminal-worker subprocesses, provider launches, and dynamic plugins. Then compare standalone/AOT packaging and size. This branch neither ports the daemon nor embeds it in the UI process.

A Rust/GPUI terminal or diff painter can share desktop implementation across the upstream renderer's supported OSes. Reusing it on mobile still requires mobile bindings and graphics integration; choosing Rust or C++ alone does not provide that.

Verification

Run the normal checks from the root. GPUI adapters are excluded from the mobile TypeScript project; its typecheck alone does not validate this target.

npm run typecheck
npm run lint
npm run format:check

For the native UI regression, use a daemon with an existing workspace and build with the driver. Run the native app on an isolated display when testing on a machine in use:

EXPO_PUBLIC_LOCAL_DAEMON=localhost:6768 npm run build:gpui -- --features test-driver,visual-capture
RN_GPUI_TEST_DRIVER=1 RN_GPUI_TEST_DRIVER_TOKEN=local-test \
RN_GPUI_DEV_MODE=0 RN_GPUI_BUNDLE="$PWD/.artifacts/gpui/paseo.bundle" \
RN_GPUI_ASSETS_DIR="$PWD/.artifacts/gpui/assets/assets" \
xvfb-run -a -s '-screen 0 2560x1600x24' \
packages/app/gpui/desktop/target/debug/react-native-gpui-paseo-gpui

Use the printed RN_GPUI_TEST_DRIVER_PORT in another shell:

PASEO_GPUI_TEST_URL=ws://127.0.0.1:PORT PASEO_GPUI_TEST_TOKEN=local-test \
npx vitest run packages/app/gpui/native.test.ts --bail=1

Without a driver URL the test is skipped. Put diagnostic logs/captures in .artifacts/gpui/; do not commit them or publish private workspace contents as QA.

For border and hover regressions, bundle the isolated scene from packages/app/gpui:

PASEO_RENDERER=gpui node node_modules/react-native/cli.js bundle --platform desktop \
  --entry-file appearance-regression.fixture.js --config desktop/metro.config.js \
  --bundle-output ../../../.artifacts/gpui/appearance.bundle --dev false --max-workers 1

Launch the driver-enabled executable with RN_GPUI_BUNDLE pointing at that bundle. Run native.test.ts from the root with the driver URL/token above and PASEO_GPUI_TEST_SCENE=appearance. It checks the rendered border pixels, Pressable hover callbacks and render props, and hover across nested controls. No daemon is needed for this scene.

On macOS, test WebSocket cleanup against a compiled native executable:

PASEO_GPUI_TEST_BINARY="/absolute/path/to/react-native-gpui-paseo-gpui" \
npx vitest run packages/app/gpui/websocket.native.test.ts --bail=1

This opens an isolated native window and uses a loopback peer for 128 rejected handshakes, 128 server closes, and 128 client closes under a 256-descriptor limit. It needs an active display and the GPUI npm dependencies, but no Paseo daemon. Without the executable path it is skipped. Its bundle and logs stay in .artifacts/gpui/.