Orca Mobile
React Native/Expo companion app for Orca Desktop. Pairing, encrypted connectivity, device capabilities, recovery, settings, and diagnostics remain native. The production hybrid route renders the existing mobile workspace 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
6768and servesout/mobile-web-rnw. - Expo Metro from
mobile/. It serves the native application on port8081.
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
- Open Orca Desktop and go to Settings → Mobile.
- Open the mobile pairing scanner.
- Scan the QR code displayed by Desktop.
- 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
- Run
pnpm exec expo run:android. - Run
pnpm start --dev-client. - Pair with
ws://10.0.2.2:6768in 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.
- Preserve the native route as the behavior oracle and fallback until the release gates approve cutover.
- Test both native and hosted routes when a shared screen changes.
Open the hosted route through Settings → Hybrid workspace UI. A reviewed
build may set EXPO_PUBLIC_ORCA_MOBILE_WEB_DEFAULT=1 to exercise the default
navigation seam, but the flag must remain off for ordinary production builds
until the documented gates pass.
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 integration journeys:
pnpm test:e2e:hosted-mobile-webview
pnpm test:e2e:hosted-mobile-webview:ssh
pnpm test:e2e:hosted-mobile-webview:ssh:packaged
The iOS journeys require macOS, Xcode, and an available simulator. SSH journeys
also require Docker. Android route, security, release, cache, and crash-loop
commands are listed in mobile/package.json.
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_VERSIONmobile/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 has a separate policy in
src/shared/mobile-web/bridge-release-policy.ts. Additive hosted operations use
capability negotiation 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(defaultorca-mock-send-modein the system temporary directory) —accept(default) accepts the send,errorfails it withmobile_input_floor_unavailable, anything else reports the send as rejected.MOCK_TERMINAL_LIST_MODE_FILE(defaultorca-mock-terminal-list-modein the system temporary directory) —omitreturns an empty terminal list,otherreturns a list that omits the chat handle, anything else lists it.MOCK_TERMINAL_STREAM_MODE_FILE(defaultorca-mock-terminal-stream-modein the system temporary directory) —deadanswers a subscribe withsubscribedthenend(a gone PTY), which is what exercises the rearm bound and terminal prune; anything else streams normally.
Connecting to Real Orca
- Start Orca desktop with WebSocket transport enabled
- In Orca, go to Settings > Mobile and scan the QR code with this app
- 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