Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions mobile/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files

# dependencies
node_modules/

# Expo
.expo/
dist/
web-build/
expo-env.d.ts

# Native
.kotlin/
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision

# Metro
.metro-health-check*

# debug
npm-debug.*
yarn-debug.*
yarn-error.*

# macOS
.DS_Store
*.pem

# local env files
.env*.local

# typescript
*.tsbuildinfo

# generated native folders
/ios
/android

# logs (expo.log, expo-web.log, metro, etc.)
*.log

# one-shot / scratch scripts (e.g. asset generation via --no-save deps)
.gen-*.js
*.local.*

# generated QR codes / scratch images at repo root
/qr-*.png

# Agents
.claude
1 change: 1 addition & 0 deletions mobile/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
legacy-peer-deps=true
1 change: 1 addition & 0 deletions mobile/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@CLAUDE.md
113 changes: 113 additions & 0 deletions mobile/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Expo HAS CHANGED

Read the exact versioned docs at https://docs.expo.dev/versions/v54.0.0/ before writing any code.

# AO Mobile — project guide

A phone remote-control for **Agent Orchestrator (AO)**, the monorepo this folder
lives in. It mirrors the AO web dashboard for a phone: a Kanban board of agent
sessions, a live controllable terminal per session, PR review/merge, and
orchestrator launch/open — over LAN or Tailscale.

## Location & tooling

- Lives at `ao-fork/mobile/` **but is a standalone npm + Expo project**, NOT part of
AO's pnpm workspace (`pnpm-workspace.yaml` only globs `packages/*`). Run
`npm install` / `npx expo start` **from inside `mobile/`** — never `pnpm` here.
- `.npmrc` has `legacy-peer-deps=true` because `@fressh` pins exact peer versions.
- TypeScript, file-based routing via **expo-router**. Dark-only.

## Expo SDK is pinned to 54 — do not bump

Expo Go supports a **single** SDK at a time. This app is pinned to **SDK 54** to
match the test phone's Expo Go. Symptoms of a mismatch: _"incompatible with this
version of Expo Go."_ Don't change the SDK unless the user's Expo Go updated.
Read **v54** docs (<https://docs.expo.dev/versions/v54.0.0/>) — APIs differ by SDK.
Pinned: `expo 54`, `react 19.1.0`, `react-native 0.81.5`, `expo-router 6`,
`react-native-webview 13.15.0` (react + webview match `@fressh` peers exactly).

## How it connects to AO

AO's web dashboard is `ao-fork/packages/web` (Next.js). This app talks to that
server two ways, configured in the **Settings** tab (`host` + ports, persisted via
AsyncStorage in `lib/config.ts`):

1. **REST API** (`http://<host>:<apiPort>`, default `3000` — but AO often runs on
**`3001`** when something holds 3000). Client: `lib/api.ts`. Endpoints used:
- `GET /api/sessions?project=all` → `{ sessions[], orchestrators[], orchestratorId, stats }`
- `GET /api/projects` → `{ projects[] }`
- `POST /api/spawn` `{ projectId, prompt?, issueId? }` — new worker
- `POST /api/orchestrators` `{ projectId, clean? }` — launch/relaunch orchestrator
- `POST /api/sessions/:id/kill | /restore | /send` `{ message }`
- `POST /api/prs/:number/merge?owner=&repo=` — squash-merge
2. **Mux WebSocket** (`ws://<host>:<muxPort>/mux`, default `14801`). Client:
`lib/mux.ts`. One multiplexed socket carries:
- **Terminal I/O**: `{ch:'terminal', id, type:'open'|'data'|'resize'|'close', projectId?}`
out; `data`/`opened`/`exited`/`error` in. (`id` = AO session id; `projectId`
disambiguates across projects.)
- **Live session snapshots**: `{ch:'subscribe', topics:['sessions','notifications']}`
→ periodic `{ch:'sessions', type:'snapshot', sessions:[SessionPatch]}` (id,
status, activity, attentionLevel, lastActivityAt).
- Heartbeat `{ch:'system', type:'ping'}`; auto-reconnect with backoff.

**No auth** — AO's API is unauthenticated; the network (LAN/Tailscale) is the
boundary. `lib/config.ts` builds `http`/`ws` (or `https`/`wss` when the TLS flag is
on) and strips any scheme the user pastes into Host.

The **attention levels** (merge / respond / review / pending / working / done) and
the **Mission Control palette** (bg `#0a0b0d`, blue `#4d8dff` = conductor, orange
`#f59f4c` = working agent, amber/red/green states) mirror AO's `DESIGN.md`.

## Architecture

- **`lib/store.tsx` (`<AppProvider>`)** is the heart: opens **one shared mux socket**
(live session patches) + a periodic REST poll, merges them (patches are
authoritative for live fields; snapshot-only sessions are surfaced immediately),
and exposes everything via `useApp()` plus `useVisibleSessions()` / `usePRs()`.
All actions (spawn, merge, kill, restore, send, launchConductor) live here. The
context value is memoized; consumers re-render only on real changes.
- **Screens** consume the store. Board groups by `attentionOf(session)`; the
Orchestrator tab lists every project's orchestrator (open if a link exists, else
spawn).
- **Terminal** (`app/session/[id].tsx`) opens its **own** MuxClient for terminal I/O
(a known duplication vs the store's socket — a deferred refactor).

## The terminal (xterm.js in a WebView) — read before touching

`@fressh/react-native-xtermjs-webview` runs xterm.js inside `react-native-webview`.
Hard-won constraints (don't relearn them the hard way):

- **Keyboard is RN-controlled, not the WebView's.** The injected JS disables the
WebView's hidden textarea so a tap can't raise a keyboard; a hidden RN
`<TextInput>` is the real keyboard (focus/blur via the ⌨ button), and `onKeyPress`
→ mux `sendInput`. This is why single-tap doesn't open the keyboard and the
terminal resizes above the keyboard.
- **Scroll**: inject `.xterm-screen{pointer-events:none}` so drags fall through the
selection canvas to native momentum scroll. Custom touch-scroll handlers do NOT
work (xterm's selection auto-scroll is timer-based).
- **Sizing is measured, not guessed**: the WebView's FitAddon fits on container
resize and reports real cols/rows back through fressh's **`debug → logger.log`**
channel; RN forwards them to the PTY. **Never** pass `onMessage` via
`webViewOptions` — fressh spreads user options after its own `onMessage`, so it
**clobbers the bridge** and breaks the terminal. Use the `logger` prop.
- `window.terminal` / `window.fitAddon` are exposed; injected JS reaches the WebView
via `webViewOptions.injectedJavaScript`.

## Dev & test workflow

- **Claude verifies the UI on Expo Web** (`npx expo start --web`) with the
chrome-devtools MCP at a phone viewport. The **terminal does not render on web**
(native WebView), and browser **CORS blocks REST** to AO — so use a `fetch` shim
in an `initScript` to mock `/api/*` when checking populated screens.
- **The user tests the terminal + keyboard on a physical iPhone via Expo Go** — only
they can verify WebView behavior. Don't claim terminal/keyboard fixes are
verified; ask them to confirm on device.
- After changes: `npx tsc --noEmit` must be clean.

## Conventions

- Match the existing screen structure: `ScreenHeader` (title + AO mascot) → optional
`ProjectSwitcher` → list/scroll. Reuse `lib/ui.tsx` primitives and `lib/theme.ts`
helpers (`statusVisual`, `attentionMeta`, `ciVisual`) — don't re-derive colors
inline. Color is rationed (it always means something); the card is the only
bordered surface.
21 changes: 21 additions & 0 deletions mobile/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2015-present 650 Industries, Inc. (aka Expo)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
104 changes: 104 additions & 0 deletions mobile/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# AO Mobile

A phone remote-control for **Agent Orchestrator (AO)** — the project this folder
lives inside. Triage your fleet on a Kanban board, open a **live terminal** for any
session and drive it, review and merge PRs, and launch/open orchestrators — from
your phone, over your LAN or Tailscale.

This is an [Expo](https://expo.dev) (React Native) app. It lives in the AO monorepo
at `mobile/` but is a **standalone npm project** — it is _not_ part of AO's pnpm
workspace. Run all commands below from inside `mobile/`.

## Requirements — Expo Go is pinned to SDK 54

> [!IMPORTANT]
> **The Expo Go app supports only ONE Expo SDK at a time** (whatever the latest
> store build targets). This project is **pinned to Expo SDK 54** to match the
> Expo Go currently installed on the test phone. If your Expo Go shows
> _"Project is incompatible with this version of Expo Go"_, your Expo Go and this
> project's SDK don't match.
>
> - **Don't bump the Expo SDK** unless your Expo Go has updated to that SDK. When
> you do upgrade, run `npx expo install expo@^<new> && npx expo install --fix`.
> - When writing code, read the **v54** docs: <https://docs.expo.dev/versions/v54.0.0/>
> (the API changes between SDKs — don't trust older/newer snippets).

Pinned versions: `expo 54`, `react 19.1.0`, `react-native 0.81.5`,
`expo-router 6`, `react-native-webview 13.15.0`. `react`/`react-native-webview`
are pinned exactly to `@fressh`'s peer requirements (see `.npmrc`:
`legacy-peer-deps=true`).

## Run it (Expo Go)

```bash
cd mobile
npm install
npx expo start
```

Scan the QR with **Expo Go** (Android: scan in the app; iOS: scan with the Camera
app). Phone and PC must be on the same Wi-Fi. If they aren't, use
`npx expo start --tunnel`. Edits hot-reload on the device.

### Preview the UI in a browser (no phone)

```bash
npx expo start --web
```

Every screen renders in the browser **except the terminal** (it's a native
WebView — device only). Note: browser **CORS** blocks the cross-origin REST calls
to AO, so the web preview shows empty/"couldn't reach server" data — that's a
browser-only limit; on a real device (native fetch, no CORS) the data loads.

## First-run setup

Open the **Settings** tab and enter your AO server:

- **Host** — your PC's LAN IP (e.g. `192.168.x.x`) or Tailscale name / `100.x`.
- **API port** — AO's dashboard (Next.js). Default `3000`, but AO often runs on
**`3001`** when another app holds `3000`.
- **Terminal port** — AO's mux/terminal WebSocket, `14801`.
- **Use TLS** — leave **off** for plain LAN/Tailscale (AO serves http/ws). Only
turn on if AO is behind HTTPS (proxy / Tailscale funnel).

Tap **Test connection**, then **Save**.

## Server side (AO) checklist

- The terminal WebSocket (`:14801`) already binds all interfaces — reachable over
LAN/Tailscale out of the box.
- The REST API must be reachable too. If a connection test fails, start AO's web
server bound to all interfaces (`HOSTNAME=0.0.0.0`) and confirm phone + PC are on
the same network/tailnet.
- AO's API has **no auth** — your network (LAN/Tailscale) is the boundary. Don't
expose these ports to the public internet.

## What's inside

```
app/
_layout.tsx Root Stack: tabs + pushed terminal + spawn modal; wraps <AppProvider>
(tabs)/
_layout.tsx Bottom tab bar (Kanban · PRs · Orchestrator · Settings)
index.tsx Kanban board — sessions grouped by AO attention level
prs.tsx Pull requests — filter, merge, open
orchestrator.tsx Per-project orchestrator: status, worker zones, open/spawn
settings.tsx Server config + projects list
session/[id].tsx Live terminal (xterm.js in a WebView) + keys + send + Kill
spawn.tsx New-agent modal (pick project + optional task)
lib/
config.ts Server config (AsyncStorage), http/ws URL builders, TLS flag
api.ts AO REST client (sessions, spawn, merge, kill, restore, send)
mux.ts AO mux WebSocket client (terminal I/O + live session snapshots)
store.tsx <AppProvider> — one shared mux socket + REST, app-wide state
theme.ts AO "Mission Control" palette + status/attention helpers
ui.tsx Shared primitives (Dot, Chip, Pill, Card, Button, ScreenHeader…)
SessionCard.tsx A session card for the board
ProjectSwitcher.tsx Multi-project pill switcher
assets/ App icon / splash / favicon / header mascot (AO brand)
```

Terminal rendering uses
[`@fressh/react-native-xtermjs-webview`](https://www.npmjs.com/package/@fressh/react-native-xtermjs-webview)
(MIT). Design language mirrors AO's `DESIGN.md`.
54 changes: 54 additions & 0 deletions mobile/app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"expo": {
"name": "AO",
"slug": "ao-mobile",
"owner": "priyanchew",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "dark",
"backgroundColor": "#0a0b0d",
"scheme": "aomobile",
"splash": {
"image": "./assets/splash-icon.png",
"resizeMode": "contain",
"backgroundColor": "#0a0b0d"
},
"ios": {
"supportsTablet": true,
"bundleIdentifier": "aoagents.ao",
"config": {
"usesNonExemptEncryption": false
}
},
"android": {
"package": "aoagents.ao",
"adaptiveIcon": {
"backgroundColor": "#0a0b0d",
"foregroundImage": "./assets/android-icon-foreground.png"
},
"predictiveBackGestureEnabled": false
},
"web": {
"favicon": "./assets/favicon.png",
"bundler": "metro"
},
"plugins": [
"expo-router",
[
"expo-build-properties",
{
"android": {
"usesCleartextTraffic": true
}
}
]
],
"extra": {
"router": {},
"eas": {
"projectId": "5bd2863a-4238-4f2e-8017-f5df7e6899c3"
}
}
}
}
Loading