This document explains how the Pi Mobile project is organized, how data flows through the system, and where to make changes safely.
For visual system diagrams, see Architecture (Mermaid diagrams). For durable decision rationale, see Architecture Decision Records.
- System Overview
- Repository Layout
- Module Responsibilities
- Key Runtime Flows
- Bridge Control Model
- State Management in Android
- Testing Strategy
- Common Change Scenarios
- Reference Files
Android App (Compose)
│ WebSocket (envelope: { channel, payload })
▼
Bridge (Node.js)
│ stdin/stdout JSON RPC
▼
pi --mode rpc
+ internal extensions (pi-mobile-tree, pi-mobile-open-stats)
The app never talks directly to a pi process. It talks to the bridge, which:
- handles auth and client identity
- manages one pi subprocess per cwd
- enforces single-client control lock per cwd/session
- forwards RPC events and bridge control messages
| Path | Purpose |
|---|---|
app/ |
Android UI, view models, host/session UX |
core-rpc/ |
Kotlin RPC command/event models and parser |
core-net/ |
WebSocket transport, envelope routing, reconnect/resync |
core-sessions/ |
Session index models, cache, repository logic |
bridge/ |
Node bridge server, protocol, process manager, extensions |
benchmark/ |
Android macrobenchmark module and baseline-profile scaffolding |
docs/ |
Human-facing project docs |
docs/ai/ |
Planning/progress artifacts |
- Compose screens and overlays
ChatViewModel: chat timeline, command palette, extension dialogs/widgets, tree/stats/model sheetsRpcSessionController: high-level session operations backed byPiRpcConnection- Host management and token storage
RpcCommandsealed models for outgoing commandsRpcIncomingMessagesealed models for incoming events/responsesRpcMessageParsermapping wiretype→ typed event classes
WebSocketTransport: reconnecting socket transport with outbound queuePiRpcConnection:- wraps socket messages in envelope protocol
- routes bridge vs rpc channels
- performs handshake (
bridge_hello, cwd set, control acquire) - exposes
rpcEvents,bridgeEvents, andresyncEvents
- Host-scoped session index state and filtering
- merge + cache behavior for remote session lists
- in-memory and file cache implementations
server.ts: WebSocket server, token validation, protocol dispatch, health endpointprocess-manager.ts: per-cwd forwarders + control locksrpc-forwarder.ts: pi subprocess lifecycle/restart/backoffsession-indexer.ts: reads/normalizes session.jsonlfilesextensions/: internal mobile bridge extensions
- App creates
PiRpcConnectionConfig(url,token,cwd,clientId) - Bridge returns
bridge_hello - If needed, app sends:
bridge_set_cwdbridge_acquire_control
- App resyncs via:
get_stateget_messages
- If resuming a specific session path, app sends
switch_session
- User sends prompt from
ChatViewModel RpcSessionController.sendPrompt()sendsprompt- Bridge forwards RPC payload to active cwd process
- pi emits streaming events (
message_update, tool events,agent_end, etc.) ChatViewModelupdates timeline and streaming state
WebSocketTransport auto-reconnects with exponential backoff.
On reconnect, PiRpcConnection:
- waits for new
bridge_hello - re-acquires cwd/control if needed
- emits
RpcResyncSnapshotafter freshget_state + get_messages
This keeps timeline and streaming flags consistent after network interruptions.
Tree flow uses both bridge control and internal extension command:
- App sends
bridge_navigate_tree { entryId } - Bridge checks internal command availability (
get_commands) - Bridge sends RPC
promptwith internal command:/pi-mobile-tree <entryId> <statusKey>
- Extension emits
setStatus(statusKey, JSON payload) - Bridge parses payload and replies with
bridge_tree_navigation_result - App updates input text and tree state
To protect against cross-device edits on the same session file:
ChatViewModelpollsbridge_get_session_freshnessevery few seconds- Bridge computes a fingerprint (
mtime, size, entry count, last ids/hash) - Client compares fingerprint against previous snapshot
- If mismatch is outside local mutation grace window:
- show coherency warning banner
- emit warning notification with lock owner hints
- User can trigger Sync now to force timeline reload and clear warning
This helps avoid writing on stale in-memory state after another client changed the session.
The bridge uses lock ownership to prevent conflicting writers.
- Lock scope: cwd (and optional sessionPath)
- Only lock owner can send RPC traffic for that cwd
- Non-owner receives
bridge_error(control_lock_requiredorcontrol_lock_denied)
This protects session integrity when multiple mobile clients are connected.
Primary state owner: ChatViewModel (StateFlow<ChatUiState>).
Important sub-states:
- connection + streaming state
- timeline (windowed history + realtime updates)
- command palette and slash command metadata
- extension dialogs/notifications/widgets/title
- bash dialog state
- stats/model/tree bottom-sheet state
- session coherency warning + sync-in-progress state
High-level design:
- transport/network concerns stay in
core-net+RpcSessionController - rendering concerns stay in Compose screens
- event-to-state logic stays in
ChatViewModel
- ViewModel-focused unit tests in
app/src/test/... - Covers command filtering, extension workflow handling, timeline behavior, queue semantics
- Vitest suites under
bridge/test/... - Covers auth, malformed payloads, control locks, reconnect, tree navigation, health endpoint
# Android quality gates
./gradlew ktlintCheck detekt test
# Bridge quality gates
cd bridge && pnpm run check- Add command model in
core-rpc/RpcCommand.kt - Add encoder mapping in
core-net/RpcCommandEncoding.kt - Add controller method in
RpcSessionController - Call from ViewModel/UI
- Add tests in app + bridge (if bridge control involved)
- Add message handling in
bridge/src/server.ts - Add payload parser/use site in Android (
PiRpcConnection.requestBridgecaller) - Add protocol docs in
docs/bridge-protocol.md - Add tests in
bridge/test/server.test.ts
Follow docs/extensions.md checklist.
app/src/main/java/com/ayagmar/pimobile/chat/ChatViewModel.ktapp/src/main/java/com/ayagmar/pimobile/sessions/RpcSessionController.ktcore-net/src/main/kotlin/com/ayagmar/pimobile/corenet/PiRpcConnection.ktcore-net/src/main/kotlin/com/ayagmar/pimobile/corenet/WebSocketTransport.ktcore-rpc/src/main/kotlin/com/ayagmar/pimobile/corerpc/RpcCommand.ktcore-rpc/src/main/kotlin/com/ayagmar/pimobile/corerpc/RpcIncomingMessage.ktbridge/src/server.tsbridge/src/process-manager.tsbridge/src/session-indexer.ts