* feat(appearance): let app chrome follow the terminal theme Add an "App Chrome Appearance" setting (Default / Match Terminal) next to Left Sidebar Appearance. In match-terminal mode the terminal theme's background/foreground are written as document-root CSS variables (--bg-titlebar, --background/--card/--popover/..., both sidebar token families), so the tab strip, status bar, side panels, full-page views and popovers all follow the terminal instead of the app theme. Left-sidebar modes still win because they scope their own variables below the root. The terminal color pair and token derivation move into terminal-surface-colors.ts and are shared with the sidebar resolver. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(status-bar): configurable usage format template Add Settings → Appearance → Status Bar → "Usage format": a template that replaces the built-in per-provider usage text when non-empty. Placeholders cover {provider}, {plan}, the 5h/7d/fable/30d windows as percentages (following Used/Remaining), {x.reset} countdowns, {x.resetAt} clock times and Gemini {buckets}; "[...]" groups drop out when a placeholder inside is empty so one template works across providers with different windows. Whitespace is rendered as authored (whitespace-pre). Persisted as PersistedUIState.statusBarUsageFormat with a byProvider override map reserved for later; the RPC UI-update schema gains the optional field. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(appearance): document touched functions; surface usage format in search Add one-line JSDoc to every function this branch adds or edits so the review docstring gate passes. Include the usage-format entry in the Status Bar section's search match so searching "template" or "placeholder" no longer hides the parent row, and mention popovers in the App Chrome Appearance descriptions since the setting styles them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(appearance): let file editors follow the terminal theme in match-terminal mode With App Chrome Appearance = Match Terminal, the tab strip, status bar and side panels took the terminal palette but every file pane kept the stock app theme: Monaco (code, plain text, diffs, notebooks, markdown source) was hard-wired to `vs`/`vs-dark`, and the markdown preview / rich editor painted the fixed `--editor-surface` with GitHub-colored hljs tokens. - `terminal-editor-palette.ts` derives an editor palette from the active terminal theme (background/foreground/cursor/selection + syntax roles mapped onto the ANSI slots) and builds a Monaco theme (`orca-terminal`) from it; light/dark is rated by the terminal background, so a light terminal theme under a dark OS setting still gets light styling. - match-terminal chrome variables now include `--editor-surface` and `--syntax-*`; the hljs stylesheets read those ahead of their defaults. - `useEditorSurfaceAppearance` / `useMonacoEditorTheme` replace the inline `isDark ? 'vs-dark' : 'vs'` computations; the diff-section chain passes the Monaco theme name instead of a dark flag. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VbWbChctXeMvaR2zdbFY99 (cherry picked from commit dc85fadee0a2664adbf55a49dd3c97c9b10fc111) * fix(status-bar): honor the usage template in compact usage mode ProviderSegment only rendered a user template on the verbose branch, so switching usage mode to compact silently fell back to the built-in tightest-window label. A non-empty template now replaces the built-in text in both modes; the mini bar stays verbose-only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VbWbChctXeMvaR2zdbFY99 * docs(appearance): document remaining functions touched by the diff Adds one-line JSDoc to the editor surfaces, diff viewers, palette helpers and UI-slice entry points this branch modifies so the diff's docstring coverage clears the pre-merge threshold. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VbWbChctXeMvaR2zdbFY99 * fix(editor): keep Monaco theme registration and preview clock out of render The upgraded oxlint React rules flag reassigning a module variable during render and calling Date.now() inside useMemo. Move the terminal Monaco theme's signature cache into ensureTerminalMonacoTheme() and pin the usage-format preview clock with a lazy useState initializer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VbWbChctXeMvaR2zdbFY99 * localization: complete Brazilian Portuguese catalogs * fix(i18n): localize remaining hard-coded UI copy and complete Simplified Chinese coverage Route the English literals and shared English labels that ignored the UI language through the existing catalog: host labels, plugin cards and consent, CLI and SSH errors, the checks pill, browser profile labels, shortcut groups, app and context menus, and the macOS computer-use permission window. Shared labels are translated where they render, so src/shared stays English for mobile, CLI and older paired hosts. - en.json gains 314 keys bootstrapped from inline fallbacks; four duplicate "Unassigned" keys collapse into translateUnassignedLabel() - zh gains 337 entries and refreshes existing ones; the other nine locales get the newly extracted keys - joinSentences ends a CJK sentence that has no final stop with 。 before the next one - the desktop caller passes the computer-use helper its locale; orcad and the CLI stay Electron-free - three files the copy pushed past max-lines each shed a focused module * test: expect labeled roles in the editable context menu The edit menu now passes explicit Cut, Copy and Select All labels so the roles render in the UI language; the upstream template assertion still expected bare roles. * fix(i18n): key translated memos on the UI language without bare void reads Upstream's React Compiler ratchet rejects `void language;`, which the compiler drops, and treats a react-hooks lint suppression as a bailout. The two host-label hooks now pass the language through readUiLanguageMemoInput so it stays a real memo input. ContextualTourOverlay drops its language key: getContextualTour is a static lookup, and step copy is localized at render. * fix(i18n): translate editor theme settings * Fix reactive localization and catalog extraction * Fix combined localization validation * Localize nested import failures and refine catalogs * Handle older nested import responses * Fix localized merge prompts and quota windows * Address localization and runtime review feedback * Keep host search and configuration examples usable * Preserve configured names in add-project host selector * Preserve host label origins across views * Use localized fallback for missing local host * Preserve custom names and refresh localized host labels * Refresh task host labels without repeating searches * Preserve Portuguese mobile tab translations * Address localization review feedback * fix(status-bar): import window visibility interval from its new path (#40951) --------- Co-authored-by: lisi <lisi.see@bytedance.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: marlico77 <marlonssoficial@gmail.com> Co-authored-by: x-Ai <5061489+x-Ai@users.noreply.github.com> Co-authored-by: Kelvin Amoaba <97001695+AmoabaKelvin@users.noreply.github.com>
Orca Mobile
React Native companion app for Orca. Monitor worktrees, view terminal output, and send commands from your phone.
Local development uses two processes:
- Orca desktop/Electron from the repo root. This hosts the mobile WebSocket RPC server on port
6768. - Expo Metro from
mobile/. This serves the React Native app on port8081.
Unless a command says otherwise, run mobile app commands from the mobile/ directory.
The Node commands pnpm rpc:diff and pnpm rpc:record use a development dependency linked to ../src/packages/process-host. Mobile's install supplies the link, and tsx runs its source with the orca-source condition, so these command wrappers do not require a desktop install or package build. Keep that source directory in sparse checkouts. The recording suites still need root dependencies when they exercise desktop modules. The React Native app does not import this host package.
Prerequisites
- Node.js 24+
- pnpm
- Xcode and/or Android Studio tooling for simulator or device builds
- Expo Go on your phone, or a development client build when native modules are needed
- Phone and desktop on the same LAN when testing a physical phone
Start Desktop Orca
From the repository root:
pnpm install
pnpm dev
Confirm the mobile RPC server is listening:
lsof -nP -iTCP:6768 -sTCP:LISTEN
Restart pnpm dev after changing Electron main-process code. Metro hot reload only applies to the mobile JavaScript bundle.
Start The Mobile App
cd mobile
pnpm install
pnpm start
Scan the Expo QR code with your phone's camera on iOS, or Expo Go on Android.
For a native dev-client build:
pnpm native ensure ios # or: pnpm native ensure android [--device <name|udid|serial>]
pnpm start --dev-client
ensure computes the Expo fingerprint of the native inputs (native dependencies, config plugins,
modules/, patches/, and the Expo config). On a HIT it installs the cached build on the booted
simulator or the running emulator in a few seconds. On a MISS it regenerates ios/ or android/,
runs expo run:<platform>, and caches the result. --rebuild forces a fresh build.
Builds are cached per fingerprint in the user cache directory (~/Library/Caches/orca-mobile/native-builds
on macOS, $XDG_CACHE_HOME/orca-mobile/native-builds on Linux, %LOCALAPPDATA%\orca-mobile\native-builds
on Windows, or ORCA_MOBILE_NATIVE_BUILD_CACHE_DIR), so every worktree reuses them. The newest three
per platform are kept. app.config.js also registers the cache as Expo's buildCacheProvider, so a
plain pnpm ios / pnpm android installs a cached build instead of compiling. Only ensure writes to
the cache, because expo run reuses an existing ios/ or android/ directory even when it was
generated from older inputs. Expo caches iOS Simulator builds only, not physical devices.
pnpm native fingerprint prints the current hashes. On pull requests, the Mobile native fingerprint
job reports in its summary whether the PR changes them, which means dev clients need a native rebuild.
Pair With Desktop Orca
- Open Orca desktop.
- Go to Settings > Mobile.
- Scan the pairing QR code from the mobile app.
- 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.
Development Paths
Android Phone
- Install Expo Go from Google Play
- Run
pnpm start, scan QR with Expo Go - For native modules:
pnpm exec expo run:android - Run with
pnpm start --dev-client
iOS Simulator
- Install Xcode from the App Store
- Run
pnpm start --iosto open in iOS Simulator
Physical Phone Debugging
The phone can be inspected through the connected device tooling:
orca snapshot --json
orca click --element @e3 --json
orca fill --element @e1 --value "ls" --json
orca screenshot --json
Use snapshot first to find the current element refs, then click/fill those refs. After mobile file edits, Metro usually hot reloads automatically, but navigating out of and back into the session screen can be useful because it re-runs terminal.subscribe.
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:
cd mobile
ORCA_MOBILE_WS_URL=ws://127.0.0.1:6768 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/worktree/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, then run:
cd mobile
ORCA_MOBILE_WS_URL=ws://127.0.0.1:6768 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 these checks before committing mobile terminal changes:
cd mobile
pnpm exec tsc --noEmit
pnpm run check:tests-typecheck
pnpm lint
cd ..
pnpm typecheck:node
tsc --noEmit reads tsconfig.json, which excludes test files so Metro never bundles them.
tsconfig.test.json puts them back, and pnpm run typecheck:tests shows their errors in full.
check:tests-typecheck is the gate over it: a ratchet against tests-typecheck-baseline.txt, the
127 test files that do not typecheck yet. It fails when a file that checks today stops checking,
and when a baseline entry starts checking (prune it with
node scripts/check-tests-typecheck-ratchet.mjs --prune). The list may only shrink.
The same gate censuses the program first: every *.test.ts(x) on disk must be in it, or named in
the script's TESTS_OUTSIDE_PROGRAM with a reason. Without that, a test excluded from
tsconfig.test.json — or a Foo.test.tsx shadowed by a Foo.test.ts beside it, which a wildcard
include drops for the higher-priority extension — would leave the ratchet silently.
Localization
UI copy goes through src/i18n, with the same call shape and languages as desktop:
- In components and hooks:
const t = useMobileTranslation(), thent('area.thing.label', 'English text'). The function changes with the language, so list it in memo and callback dependencies. - In helpers and imperative code (alerts, notifications):
translate(key, fallback, options)fromsrc/i18n/mobile-i18n, called when the text is shown, never at module scope. - Counts:
count === 1 ? t('x.files_one', '{{count}} file', { count }) : t('x.files_other', '{{count}} files', { count }). - Dates and numbers: pass
getUiIntlLocale()toIntlformatters instead ofundefined. - Keep identity separate from display text: never compare or key on a translated string.
Run pnpm sync:mobile-localization from the repo root to add new keys to src/i18n/locales/en.json;
other catalogs may lag and fall back to English. pnpm verify:mobile-localization and
pnpm verify:mobile-localization-coverage run in CI. iOS permission prompts live in locales/<lang>.json.
Android Release Signing
plugins/android-release-signing.js adds a release signing config to the generated
android/app/build.gradle. It reads ORCA_ANDROID_KEYSTORE_PATH, ORCA_ANDROID_KEYSTORE_PASSWORD,
ORCA_ANDROID_KEY_ALIAS and ORCA_ANDROID_KEY_PASSWORD at build time; nothing is written into the
project. Without them, local release builds keep Expo's debug key.
.github/workflows/mobile-android-release.yml turns release signing on once the
ORCA_ANDROID_RELEASE_CERT_SHA256 repository variable (or ORCA_ANDROID_REQUIRE_RELEASE_SIGNING=true)
is set. Until then it signs with the debug key as before and adds a warning to the run. Once it is
on, the workflow fails before building if any of these is missing:
| Name | Kind | Value |
|---|---|---|
ORCA_ANDROID_KEYSTORE_BASE64 |
secret | the release keystore, base64-encoded |
ORCA_ANDROID_KEYSTORE_PASSWORD |
secret | keystore password |
ORCA_ANDROID_KEY_ALIAS |
secret | key alias |
ORCA_ANDROID_KEY_PASSWORD |
secret | key password (same as the keystore password for PKCS12) |
ORCA_ANDROID_RELEASE_CERT_SHA256 |
variable | the key's certificate SHA-256 fingerprint |
After a release-signed build, the workflow checks that the APK's signing certificate matches
ORCA_ANDROID_RELEASE_CERT_SHA256 and is not the debug key, and publishes the fingerprint in the
release notes. Android only installs an update signed by the same key as the installed app, so
the keystore must be backed up: losing it means no existing install can be updated in place.
Protocol Version Compatibility
Mobile and desktop talk over a versioned protocol. Because mobile updates lag desktop by 24-48h via the App Store, both sides exchange version numbers on status.get so a genuinely incompatible combo 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/connection/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 the update that clears it. When mobile is too old it opens the newest release if the installed app's update check knows one, otherwise the App Store (iOS) or GitHub Releases (Android). When desktop is too old it opens GitHub Releases.
To exercise the block screen locally: set MIN_COMPATIBLE_DESKTOP_VERSION = 999 in mobile/src/transport/connection/protocol-version.ts, rebuild, pair to any desktop. Revert before merging.
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_CHAT_AGENT=omp— withMOCK_NATIVE_CHAT=1, present an OMP tab and four decoded transcript messages, including a tool call and result, instead of the default Claude scenario. It deliberately omitstranscriptPathto exercise legacy-hook readability discovery; current OMP hooks may report a path.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
- The QR encodes the connection endpoint, device token, and TLS fingerprint
Project Structure
mobile/
├── app/ # Expo Router screens (file-based routing)
│ ├── _layout.tsx # Root layout with navigation stack
│ ├── index.tsx # Home screen — paired hosts list
│ └── pair-scan.tsx # QR code scanning screen
├── src/
│ ├── terminal/ # Terminal WebView and xterm bridge
│ └── transport/ # WebSocket RPC client
├── scripts/
│ ├── test-subscribe.ts # Desktop streaming repro without a phone
│ └── mock-server.ts # Standalone mock WebSocket server
└── assets/ # App icons and splash screen