Files
orca/mobile
..

Orca Mobile

React Native/Expo companion app for Orca Desktop. Pairing, encrypted connectivity, device capabilities, recovery, settings, and diagnostics remain native. Ordinary release builds preserve the existing native workspace UI. A dedicated hybrid release renders that same UI through React Native Web from a verified package shipped with each paired Desktop.

This is one shared UI, not a replacement web implementation. Read the hybrid architecture reference before changing package delivery, hosted adapters, the native bridge, or recovery.

Local development normally uses:

  • Orca Desktop/Electron from the repository root. It hosts the authenticated mobile RPC server on port 6768 and serves out/mobile-web-rnw.
  • Expo Metro from mobile/. It serves the native application on port 8081.

Unless a command says otherwise, run mobile commands from mobile/.

Prerequisites

  • Node.js 24+
  • pnpm
  • Xcode and/or Android Studio tooling for simulator or device builds
  • A development-client build. The hybrid shell uses custom native modules and cannot be validated in Expo Go.
  • Phone and desktop on the same LAN when testing a physical phone

Start Desktop Orca

From the repository root:

pnpm install
pnpm dev

Confirm Settings → Mobile shows the pairing QR and companion server state.

Restart pnpm dev after changing Electron main-process code. Metro hot reload only applies to the mobile JavaScript bundle.

Build and verify the hosted package after changing shared workspace UI, hosted adapters, or package contracts:

pnpm build:mobile-web-rnw

Start The Mobile App

cd mobile
pnpm install
pnpm start

For a native development-client build:

pnpm exec expo run:android
pnpm exec expo run:ios
pnpm start --dev-client

Pair With Desktop Orca

  1. Open Orca Desktop and go to Settings → Mobile.
  2. Open the mobile pairing scanner.
  3. Scan the QR code displayed by Desktop.
  4. Confirm the mobile host endpoint is ws://<desktop-ip>:6768.

For the Android emulator, use ws://10.0.2.2:6768. For a physical phone, use the desktop LAN IP, for example ws://192.168.0.179:6768.

If the phone has a stale host entry, remove it from the app and pair again.

The hybrid package travels through this authenticated encrypted connection. It is not loaded from an HTTP development server, and pairing credentials must never be placed in a hosted document URL or support report.

Development Paths

Android

  1. Run pnpm exec expo run:android.
  2. Run pnpm start --dev-client.
  3. Pair with ws://10.0.2.2:6768 in the Android emulator or the Desktop LAN address on a physical phone.

iOS Simulator

The repository launcher starts Metro, creates an isolated paired Desktop runtime, registers the simulator with Orca, and opens the development client:

pnpm start:emulator -- --device "iPhone 17 Pro" --wait-for-ready

The launcher owns its temporary user-data directory; do not repeat ORCA_DEV_USER_DATA_PATH or ORCA_USER_DATA_PATH prefixes manually. Control the registered simulator through Orca:

orca emulator list --json
orca emulator attach "iPhone 17 Pro" --json
orca emulator ax --json
orca emulator tap 0.5 0.7 --json

After mobile edits, Metro usually reloads the native route. Rebuild out/mobile-web-rnw before checking hosted package identity or Desktop delivery. Native module changes require a fresh development-client build.

Shared UI Development

Workspace presentation lives in the existing routes under app/ and components under src/. host-web-app/ is a small React Native Web entry graph that imports those screens and supplies hosted operation adapters.

  • Change the existing screen once; do not copy it into DOM-only JSX.
  • Keep native/hosted differences behind concrete platform modules or named operation adapters.
  • Treat the shared app/h/ screens as the behavior oracle. They remain source modules for the hosted package, but the native shell no longer exposes them as workspace destinations.
  • Test the hosted route and the relevant shared presentation modules when a screen changes.

Host, session, task, account, onboarding, notification, and cold-resume entry follow the selected mobile architecture. Ordinary release builds default to the native routes; a hybrid TestFlight/APK opts in at build time:

EXPO_PUBLIC_ORCA_MOBILE_ARCHITECTURE=hybrid pnpm exec expo run:ios
EXPO_PUBLIC_ORCA_MOBILE_ARCHITECTURE=hybrid pnpm exec expo run:android

EXPO_PUBLIC_ORCA_MOBILE_ARCHITECTURE=native always selects the native routes. Leaving it unset is not the same everywhere: a release build defaults to native, but a development build defaults to hybrid so the hosted journeys and the native baselines share one Metro bundle. Set it to native explicitly to exercise the native app from a dev build:

EXPO_PUBLIC_ORCA_MOBILE_ARCHITECTURE=native pnpm exec expo run:ios

The development-only EXPO_PUBLIC_ORCA_E2E_MOBILE_NATIVE_BASELINE=1 override also selects native routes, for parity captures.

Package Build

From the repository root:

pnpm build:mobile-web-rnw

This exports mobile/host-web-app/, packages content-addressed assets into out/mobile-web-rnw, writes the canonical manifest, and verifies hashes, CSP, source boundaries, and bundle budgets. build:desktop and build:release include this step, and Electron Builder copies the verified tree to the Desktop resources directory.

Terminal Streaming Repro Without A Phone

Use this when terminal output does not render on device and you need to split server streaming bugs from WebView/UI bugs. Set ORCA_MOBILE_WS_URL to the Desktop endpoint, such as ws://127.0.0.1:6768, in the command environment, then run:

cd mobile
pnpm exec tsx scripts/test-subscribe.ts <deviceToken> <serverPublicKeyB64>

You can pass a worktree selector as the third argument:

pnpm exec tsx scripts/test-subscribe.ts <deviceToken> <serverPublicKeyB64> "id:<worktreeId>"
pnpm exec tsx scripts/test-subscribe.ts <deviceToken> <serverPublicKeyB64> "path:<absolute-workspace-path>"
pnpm exec tsx scripts/test-subscribe.ts <deviceToken> <serverPublicKeyB64> "name:my-worktree"

The expected result includes:

streamSawMarker: true
readSawMarker: true

If this repro fails, debug the desktop runtime/PTY path before the mobile WebView. If it passes but the phone is blank, debug the session screen or TerminalWebView readiness/queueing path.

Terminal Color Repro Without A Phone

Use this when terminal colors disappear after switching tabs. Open a Claude Code terminal and at least one other terminal in the target worktree, set ORCA_MOBILE_WS_URL in the command environment, then run:

cd mobile
pnpm exec tsx scripts/repro-terminal-colors.ts <deviceToken> <serverPublicKeyB64> "id:<worktreeId>"

The script captures terminal.subscribe snapshots in an A → B → A sequence and writes raw snapshots to mobile/terminal-color-repro/. If the two A snapshots have different sgrColor counts, the desktop snapshot changed during the switch. If they match, the ANSI color data is still present and the bug is in mobile replay/rendering.

Validation

Run the checks relevant to the change:

pnpm --dir mobile typecheck
pnpm --dir mobile lint
pnpm typecheck:node
pnpm typecheck:mobile-web
pnpm lint:mobile-web
pnpm build:mobile-web-rnw

Hosted WebView journeys

Repository-root entrypoints build Desktop and the hosted bundle first, then hand off to the mobile runner:

Command Needs
pnpm test:e2e:hosted-mobile-webview macOS, Xcode, a booted iOS simulator
pnpm test:e2e:hosted-mobile-webview:ssh the above, plus Docker for the SSH relay target
pnpm test:e2e:hosted-mobile-webview:ssh:packaged the above, against packaged mobile-web resources

Mobile-side entrypoints (pnpm --dir mobile <script>) drive an already-built Desktop through the orca CLI, so a paired Desktop must be reachable:

Command Needs
test:e2e:hosted-webview macOS, Xcode, a booted iOS simulator
test:e2e:hosted-webview:security same; runs only the origin/capability assertions
test:e2e:hosted-webview:android-routes an Android emulator or device on adb, with accessibility reads
test:e2e:hosted-webview:android-back an Android emulator or device on adb
test:e2e:hosted-webview:android-security an Android device on adb plus a local network probe port
test:e2e:hosted-webview:android-release a user-build (release-signed, non-debuggable) install; --serial or ANDROID_SERIAL
test:e2e:hosted-webview:android-crash-loop an Android device on adb; forwards the WebView DevTools socket
test:e2e:hosted-webview:android-corrupt-cache an Android device on adb, POSIX signals, and --desktop-pid
test:e2e:hosted-webview:ios-crash-loop macOS, Xcode, an iOS simulator reachable through xcrun simctl

Native store suites

Command Needs
pnpm --dir mobile test:native:ios-web-store macOS with Xcode command line tools; compiles the shipped Swift store sources with xcrun swiftc and needs no simulator
cd mobile/android && ./gradlew :orca-expo-mobile-web-shell:testDebugUnitTest JDK 17 and the Android SDK, after npx expo prebuild --platform android --no-install

Both native suites run in CI from .github/workflows/mobile-native-shell-tests.yml. The hosted journeys do not: none of them can run on a GitHub-hosted runner that has an emulator and Docker at once.

Protocol Compatibility

Native routes and Desktop exchange broad protocol versions through status.get. Because store updates can lag Desktop, a genuinely incompatible combination can hard-block instead of silently misbehaving.

Constants live in two files (Metro can't resolve outside mobile/):

  • src/shared/protocol-version.ts — DESKTOP_PROTOCOL_VERSION, MIN_COMPATIBLE_MOBILE_VERSION
  • mobile/src/transport/protocol-version.ts — MOBILE_PROTOCOL_VERSION, MIN_COMPATIBLE_DESKTOP_VERSION

Today all four are set so evaluateCompat always returns { kind: 'ok' } — nothing blocks. The wire format is in place to flip a switch when needed.

When to bump

Bump DESKTOP_PROTOCOL_VERSION (and the mobile mirror MOBILE_PROTOCOL_VERSION when relevant) for breaking changes:

  • Removed RPC method or required parameter that mobile uses
  • Changed meaning (units, nullability) of an existing field mobile reads
  • Changed encryption, framing, or auth handshake

Do not bump for additive changes:

  • New RPC methods
  • New optional fields on existing methods
  • New event types in terminal.subscribe

Set MIN_COMPATIBLE_MOBILE_VERSION (kill-switch) when desktop ships a change that requires a minimum mobile version to function safely. Same for MIN_COMPATIBLE_DESKTOP_VERSION from the mobile side.

When a verdict is blocked, mobile/src/components/ProtocolBlockScreen.tsx renders a screen pointing the user at either the App Store (mobile too old) or GitHub Releases (desktop too old).

To exercise the block screen locally: set MIN_COMPATIBLE_DESKTOP_VERSION = 999 in mobile/src/transport/protocol-version.ts, rebuild, pair to any desktop. Revert before merging.

The hosted capability bridge gates on MOBILE_WEB_BRIDGE_PROTOCOL_VERSION in src/shared/mobile-web/bridge-limits.ts. Additive hosted operations negotiate through init.grants without bumping bridge version 2. Breaking bridge or security semantics require a native shell release, and Desktop must retain the old bridge floor for at least two stable mobile releases containing its replacement.

Mock Server

Develop the mobile app without a running Orca desktop instance:

pnpm mock-server           # starts mock WebSocket server on port 6768

Connect from the app using endpoint ws://localhost:6768 and token mock-device-token.

Environment variables

  • MOCK_NATIVE_CHAT=1 — serve the native-chat scenario (one live agent tab, empty transcript, image upload) instead of the default terminal fixtures.
  • MOCK_SERVER_KEY_FILE — persist the server keypair across restarts so a paired device keeps its public-key pin. A missing or invalid file is re-keyed with a warning, which forces a re-pair.

Scenario control files

Read on every request, so behaviour can be flipped mid-session without a restart (a restart would re-key E2EE and force a re-pair). Write the mode into the file, or delete it for the default.

  • MOCK_SEND_MODE_FILE (default orca-mock-send-mode in the system temporary directory) — accept (default) accepts the send, error fails it with mobile_input_floor_unavailable, anything else reports the send as rejected.
  • MOCK_TERMINAL_LIST_MODE_FILE (default orca-mock-terminal-list-mode in the system temporary directory) — omit returns an empty terminal list, other returns a list that omits the chat handle, anything else lists it.
  • MOCK_TERMINAL_STREAM_MODE_FILE (default orca-mock-terminal-stream-mode in the system temporary directory) — dead answers a subscribe with subscribed then end (a gone PTY), which is what exercises the rearm bound and terminal prune; anything else streams normally.

Connecting to Real Orca

  1. Start Orca desktop with WebSocket transport enabled
  2. In Orca, go to Settings > Mobile and scan the QR code with this app
  3. Keep the QR payload private; it contains pairing material.

Troubleshooting and Recovery

In the app, open Settings → Troubleshooting to run reachability checks. Open Connection Log → Copy diagnostics for the selected host when filing a support issue.

Do not attach pairing material, endpoints, full build IDs, absolute cache paths, filenames, repository content, terminal bytes, or hosted page payloads. For package/cache incidents, follow the rollback and recovery runbook.

Project Structure

mobile/
├── app/                   # Native Expo Router graph and shared screens
├── host-web-app/          # RNW entry graph importing the shared screens
├── packages/
│   └── expo-mobile-web-shell/ # iOS/Android private origin and verified cache
├── src/
│   ├── mobile-web/        # Native shell, package download, bridge authority
│   ├── terminal/          # Shared terminal presentation and adapters
│   └── transport/         # Authenticated encrypted mobile RPC client
├── scripts/
│   ├── export-host-mobile-web.mjs
│   ├── start-emulator.mjs
│   ├── test-subscribe.ts
│   └── mock-server.ts
└── assets/                # App icons and splash screen