Files
orca/mobile
Jinwoo-H 57ef1fecfb Merge remote-tracking branch 'origin/main' into mobile-rearch
# Conflicts:
#	mobile/src/tasks/mobile-tasks-refactor-parity.test.ts
#	mobile/src/tasks/source-workspace-create.ts
#	mobile/src/tasks/worktree-create-retry.ts
#	src/main/daemon/headless-emulator.ts
#	src/main/index.ts
2026-08-31 22:10:08 -04:00
..

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 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 always use the production hybrid route. There is no Experimental Settings launcher or environment-controlled native workspace fallback.

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_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 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 (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