# mobile reference

> Reference for the code under `mobile`: what it exports, how it is invoked, its options and defaults, and its error cases.

- Repository: egoist/lorca
- GitHub: https://github.com/egoist/lorca
- Human docs: https://grok-wiki.com/public/docs/egoist-lorca-2cf67495e5e6
- Complete Markdown: https://grok-wiki.com/public/docs/egoist-lorca-2cf67495e5e6/llms-full.txt

## Source Files

- `mobile/modules/lorca-core/index.ts`
- `mobile/index.ts`
- `crates/cli/src/api.rs`

---

---
title: "mobile reference"
description: "Reference for the code under `mobile`: what it exports, how it is invoked, its options and defaults, and its error cases."
---

`mobile/` is the Expo (React Native, expo-router) iOS/Android Device app. It links `crates/mobile` (`lorca-mobile`) through the Expo module at `mobile/modules/lorca-core`, then speaks the same JSON API as the desktop CLI (`crates/cli/src/api.rs`) over in-process UniFFI calls. A phone is a Device (`os`: `ios` / `ipados` / `android`), never a Runner.

```text
mobile/index.ts  →  expo-router  →  engine.start()
                                       │
         mobile/modules/lorca-core  →  LorcaCore native  →  Core (UniFFI)
                                       │                      │
                                  onEvent / request      lorca::api::dispatch
                                       │                      │
                                  zustand store          relay sync loop
```

## Layout

:::files
mobile/
  index.ts                      # crypto polyfill + expo-router entry
  app.config.ts                 # Expo config (dev vs prod ids, plugins)
  app/                          # expo-router screens
  app/_layout.tsx               # engine.start(); paired vs pair screens
  modules/lorca-core/           # Expo native module + UniFFI build
  modules/lorca-core/index.ts   # start, request, onEvent, wake, setOpenChat
  modules/lorca-core/build.ts   # bun run core → ios/android native libs
  src/core/                     # engine, store, prefs, push, pairing, model
  src/ui/                       # screens and shared widgets
  targets/notify/               # iOS notification service extension
  plugins/                      # Expo config plugins
:::

| Path | Role |
| --- | --- |
| `crates/mobile` | UniFFI `Core`: `start`, `request`, `wake`, `push_key`, `push_open` |
| `mobile/modules/lorca-core` | Expo module `LorcaCore` + native UI view packages |
| `mobile/src/core/engine.ts` | App lifecycle, API verbs, event → store |
| `mobile/src/core/prefs.ts` | App-local prefs + core home directory |
| `mobile/app/_layout.tsx` | Calls `engine.start()`; gates paired vs pair screens |

## Exports: `lorca-core`

Package name: `lorca-core` (`mobile/modules/lorca-core/package.json`). Main: `mobile/modules/lorca-core/index.ts`.

### Core bridge

| Export | Signature | Behavior |
| --- | --- | --- |
| `start` | `(home: string, facts: HostFacts) => void` | Starts native `Core` once with home folder and host facts |
| `request` | `<T>(method: string, params?: unknown) => Promise<T>` | JSON API call; throws `Error(message)` on `{ error }` |
| `onEvent` | `(listener: (frame: Frame) => void) => () => void` | Subscribes to `{ event, data }` frames; returns unsubscribe |
| `wake` | `() => void` | Foreground sync nudge (skips relay backoff) |
| `setOpenChat` | `(chatId: string \| null) => void` | Android only: suppress pushes for the open chat |

```ts
export interface HostFacts {
  name: string;
  os: string;          // ios | ipados | android
  os_version: string;
  model: string;
}

export interface Frame {
  event: string;
  data: unknown;
}
```

`request` stringifies `params` (default `{}`), parses `{ result?, error?: { message } }`, and throws when `error` is present.

### Native UI views (sibling files)

These are **not** re-exported from `mobile/modules/lorca-core/index.ts`. Screens import them by file path under `mobile/modules/lorca-core/`:

| File | Export | Platforms |
| --- | --- | --- |
| `MarkdownView.tsx` | `MarkdownView`, `measureMarkdown` | iOS + Android |
| `ShimmerView.tsx` | `ShimmerView` | iOS + Android |
| `CompactWidthView.tsx` | `CompactWidthView` | iOS; Android falls back to `View` |
| `SoftScrollEdgeView.tsx` | `SoftScrollEdgeView` | iOS; Android falls back to `View` |

Registered native modules: `LorcaCoreModule`, `MarkdownViewModule`, `ShimmerViewModule` (both platforms); `CompactWidthViewModule` and `SoftScrollEdgeViewModule` (Apple only) — see `mobile/modules/lorca-core/expo-module.config.json`.

### UniFFI surface (`crates/mobile`)

| Symbol | Notes |
| --- | --- |
| `Core.start(home, name, os, os_version, model, listener)` | Loads `App` with `port: 0`, starts sync + event forwarder |
| `Core.request(method, params)` | Blocks; returns `{ "result" }` or `{ "error": { "message" } }` |
| `Core.wake()` | `app.wake_sync()` |
| `Core.push_key()` | Account push key bytes, or `None` until paired |
| `push_open(home, sealed)` | Decrypt push ciphertext without a running core (Android FCM path) |

Event lag: if the listener falls behind, the forwarder emits a fresh `snapshot` frame (same recovery as a lagging websocket client).

## Invocation

### Dev and build commands

| Command | What runs |
| --- | --- |
| `bun run mobile:dev` | `scripts/mobile.ts` — rebuild stale core/prebuild/pods/app, start Metro `:8081`, open booted simulator |
| `bun run mobile:phone` | Same loop with `--phone` (paired physical iPhone) |
| `bun run android` | `mobile` core Android build, then `expo run:android` |
| `bun run --cwd mobile core` | `mobile/modules/lorca-core/build.ts` (both platforms) |
| `bun run --cwd mobile core ios` | iOS xcframework + Swift bindings only |
| `bun run --cwd mobile core android` | Android jniLibs + Kotlin bindings only |

`mobile/modules/lorca-core/build.ts` defaults:

- Host dylib with `--features bindgen`, then UniFFI generate from `target/debug/liblorca_mobile.dylib`
- iOS: release `aarch64-apple-ios` + `aarch64-apple-ios-sim` → `ios/LorcaCore.xcframework`
- Android: `cargo ndk` for `arm64-v8a` + `x86_64` → `android/src/main/jniLibs`; Kotlin under `android/src/main/java`
- NDK: `ANDROID_NDK_HOME`, else latest under `ANDROID_HOME` / `~/Library/Android/sdk/ndk`
- Non-zero child exit → thrown `Error`; missing xcframework after iOS build → `"no xcframework"`

### App start

1. `mobile/index.ts` loads `react-native-get-random-values`, then `expo-router/entry`.
2. `mobile/app/_layout.tsx` mounts and calls `engine.start()` once.
3. `engine.start()` (`mobile/src/core/engine.ts`): subscribe `onEvent` → `core.start(coreHome(), hostFacts())` → `bootstrap` (`request("bootstrap")`) → push handlers; register pushes if paired.
4. Until `ready`, root layout returns `null`. Then `Stack.Protected` shows `(main)` when `paired`, else `pair`.

### Host facts and home

`hostFacts()` (`mobile/src/core/host.ts`):

| Field | Source |
| --- | --- |
| `os` | `ios` / `ipados` (tablet) / `android` |
| `os_version` | e.g. `iOS 18.0`, `Android 15` |
| `model` | `Device.modelName` (fallback `iPhone` / `Android`) |
| `name` | `Device.deviceName` or model |

`coreHome()` (`mobile/src/core/prefs.ts`): documents dir → `lorca-dev/core` when app id is `app.lorca.dev`, else `lorca/core`. App prefs live in sibling `prefs.json` (`dictation_lang`, `app_lang`).

### Engine API verbs (via `request`)

| Engine method | JSON method | Notes |
| --- | --- | --- |
| `bootstrap` (internal) | `bootstrap` | First snapshot; held events apply after |
| `sendMessage` | `chats.send` | attachments as `{ path, name, mime, … }` |
| `loadOlder` | `chats.messages` | `{ chat_id, before }` |
| `searchChats` | `chats.search` | `{ query, limit: 24 }` |
| `fetchFile` | `files.path` | |
| `createBot` / `updateBot` / look / avatar / runtime | `bots.create` / `bots.update` | |
| `createGroup` / rename / pin / delete / bots / owner | `chats.*` | |
| `setAutoReview` | `auto_review.set` | |
| `connectProvider` / `disconnectProvider` | `providers.connect_*` / `providers.disconnect` | ChatGPT/Grok open in-app browser |
| `answerPermission` | `chats.permission` | |
| `answerCommand` / `stopCommand` | `bash.stdin` / `bash.stop` | |
| `setRoutineEnabled` / `runRoutine` / `deleteRoutine` | `routines.*` | |
| `renameDevice` / `unpairDevice` / `unpair` | `device.rename` / `device.unpair` / `identity.forget` | |
| `pair` | `pair.accept` (+ `pair.abort` on AbortSignal) | |

Foreground: `core.wake()` (and `notify()`). Pull-to-refresh uses the same path.

## App variants

| | Production | Development |
| --- | --- | --- |
| Name | Lorca | Lorca Dev |
| App id | `app.lorca` | `app.lorca.dev` |
| Scheme | `lorca` | `lorca-dev` |
| Core folder | `lorca/core` | `lorca-dev/core` |
| App group | `group.app.lorca` | `group.app.lorca.dev` |

Set by `LORCA_MOBILE_VARIANT=development` or `EAS_BUILD_PROFILE=development` in `mobile/app.config.ts`. Dev loop forces `LORCA_MOBILE_VARIANT=development`.

## Events → store

| Event | Store effect |
| --- | --- |
| `snapshot` | `replaceSnapshot` |
| `roster.changed` | roster + drop removed chats |
| `message.added` / `updated` / `removed` | transcript |
| `chat.removed` | drop chat |
| `job.started` / `finished` / `thinking` / `retry` | working UI |
| `chat.usage` | usage |
| `relay.status` | `relayConnected`, `relayUpdateRequired`, `relayError`, `relayUrl` |
| `provider.auth` | open in-app browser |
| `identity.changed` | reset store if unpaired; else `paired: true` |
| `pair.posted` | pair progress → `waiting` (listener in `pair()`) |

During `bootstrap`, inbound events queue and apply after the snapshot returns.

## Pairing string (client-side check)

`parsePairingString` (`mobile/src/core/pairing.ts`) accepts `lorca://pair?relay=…&id=…&ek=…&n=…` (also tolerates text that contains `pair?`).

| Error | When |
| --- | --- |
| `That is not a Lorca pairing string` | Missing `lorca://pair?` / `pair?` |
| `Pairing string is missing a field` | Missing `relay`, `id`, `ek`, or `n` |

Core-side pair wait can reject with `Pairing cancelled` when `pair.abort` fires (AbortSignal on `engine.pair`). Wait timeout from the CLI pairing path: `Nobody joined within ten minutes`.

## Push

| Platform | Path |
| --- | --- |
| iOS | APNs token → `push.register` with `environment` `sandbox` or `production`; NSE `mobile/targets/notify` decrypts with app-group keychain account `push-key` |
| Android | FCM → `push.register` platform `fcm`; `PushService` calls `push_open(home, sealed)` |

`setOpenChat` / foreground flags suppress banners for the visible chat. Permission denied or missing Firebase/APNs → silent no-op (warn logged).

## Error cases

| Case | Signal | Handling |
| --- | --- | --- |
| Core not started | `"The Lorca core has not been started"` | Call `start` before `request` (iOS `NotStartedException` / Android `IllegalStateException`) |
| API error | `{ error: { message } }` → thrown `Error` | Engine methods reject / `.catch` + `console.warn` |
| Unknown method | `"unknown method {name}"` | From `api::dispatch` |
| Missing param | `"missing {key}"` / `"missing name"` | Param validation in API |
| Push register unpaired | `"No relay configured"` / `"Not paired"` | Logged in `registerForPushes` |
| Pair abort | `"Pairing cancelled"` | Expected on user cancel |
| Avatar not image | `"{name} is not an image"` | `bots.update` / create avatar path |
| Event apply failure | warn `applying {event}` | Held events continue applying |
| Prefs I/O | warn / empty object | Non-fatal |
| Provider browser cancel | `providers.auth.cancel` | On dismiss/cancel/error |
| Relay protocol refuse | `relayUpdateRequired` | Store flag; update app |
| Build script failure | non-zero exit / thrown Error | Dev loop leaves installed app unchanged |

`request` runs off the JS/UI thread (iOS global queue / Android `Dispatchers.IO`) so long waits such as `pair.accept` (up to about ten minutes in the core) do not block other calls.

## Tests

```bash
bun run --cwd mobile test
```

Covers pairing-string parse/reject, provider helpers, command-card visibility, attachment summaries, transcript preview/format helpers, and reading/push visibility behavior. Rust coverage for the UniFFI core lives in `crates/mobile/src/lib.rs` (`the_core_answers_the_api_and_forwards_events`).

## Related pages

<CardGroup>
  <Card title="Overview" href="/overview">Entry points, Device vs Runner, and high-value pages.</Card>
  <Card title="Architecture" href="/architecture">Components, boundaries, and relay data flow.</Card>
  <Card title="CLI reference" href="/cli-reference">Desktop CLI that shares the same JSON API.</Card>
  <Card title="crates/cli reference" href="/ref-crates-cli">Library behind `api::dispatch` and the Device core.</Card>
  <Card title="HTTP API reference" href="/http-api-reference">Relay HTTP surface the phone syncs against.</Card>
</CardGroup>

Next: open `mobile/modules/lorca-core/index.ts` and skim the five exports (`start`, `request`, `onEvent`, `wake`, `setOpenChat`).
