pr-code-change-scope keeps the branch's streamed stdin helper (main adopted the same approach inline); the lockfile is regenerated over the merged manifest. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
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
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.
- 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_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 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(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