A native Android client for the spettro AI coding agent — a Kotlin port
of the iOS companion app (SpettroMobile). The app ships no AI itself: it is
a remote control and transcript view for an agent that runs elsewhere.
UI is Jetpack Compose with Material 3 Expressive; package root is
to.eyed.spettro.mobile. minSdk 33, targetSdk 36, Kotlin + coroutines
throughout (no RxJava, no Hilt — manual DI via AppContainer).
Spettro for Android talks to a spettro agent host and renders
everything it emits: chats, streamed tokens, tool calls, diffs, plans,
permission prompts, and ask-user questions. It pairs with the Spettro
desktop app on your PC — Linux, macOS, or Windows — over the local network
(Bonjour discovery, QR-code pairing, WebSocket + JSON-RPC 2.0) and mirrors
its chat list; the PC owns the agent. Every prompt, config change, and
permission reply is a request; every chat update is a notification. The app
owns the display transcript, the composer, and the sheets; the agent owns
the model, the tool loop, and the file edits.
| Spettro Remote (Protocol B) | |
|---|---|
| Connects to | Spettro desktop app acting as host |
| Transport | WebSocket, JSON-RPC 2.0 (symmetric peer) |
| Discovery | Bonjour _spettro-remote._tcp via NsdManager |
| Auth | QR pair-once, HMAC-SHA256 challenge/response, durable device key |
| Surface | Full chat list, projects, multi-session, config, permissions, questions |
| Spec | ../Spettro/docs/34-remote-protocol.md |
The companion-app pairing is the only supported connection: the durable, HMAC-authenticated pairing is safer than exposing a raw agent endpoint, so there is deliberately no direct/headless CLI mode.
RemoteClient exposes a StateFlow<RemoteState> state machine
(Unpaired → Searching → Connecting → Connected, plus Offline with a
typed reason), a diagnostics log, and RemoteApi typed wrappers for every
host request (chatsList, chatsOpen, prompt, cancel, config,
permissionReply, questionReply, agentCall, …). Raw ACP payloads cross
module boundaries as JsonObject — the protocol layers never parse ACP
content; core.acp does.
Failed connections retry on their own: exponential backoff from 1s to 30s, reset the moment anything succeeds, the app comes to the foreground, or Bonjour sees the host reappear. While the app is offline, the disconnected screen keeps a "Forget This PC" escape hatch so a stale pairing can always be discarded.
app/src/main/java/to/eyed/spettro/mobile/
├── MainActivity.kt entry point; starts/stops the client with lifecycle
├── coordinator/ manual DI + app-wide coordinators
│ ├── AppContainer.kt service locator (the "DI graph")
│ └── remote/MobileModel.kt root coordinator: mirrors the host's state
├── core/
│ ├── SpettroJson.kt, B64.kt shared Json instance + base64url/std helpers
│ ├── rpc/ JsonRpcPeer — symmetric JSON-RPC 2.0 over OkHttp WS
│ ├── remote/ Protocol B: RemoteClient, RemoteApi, discovery (NSD),
│ │ pairing (QR + HMAC), credential store, notifications
│ └── acp/ ACP payload parsing: session updates, config options
│ (both wire shapes), permission/question requests,
│ StoredSession decode, extension types
├── model/ ChatSession transcript state holder, TranscriptItem,
│ ToolCallItem, image attachments
└── ui/
├── theme/ Spettro colors/type — M3 Expressive with Spettro tokens
├── components/ shared composables (cards, chips, badges, spinners…)
├── screens/
│ ├── home/ PairingScreen, QR scanner, ChatListScreen,
│ │ DisconnectedScreen, project picker
│ ├── chat/ ChatScreen, composer, transcript rendering,
│ │ tool-call rows, markdown, config sheet, status strip
│ ├── settings/ SettingsScreen, ProvidersScreen
│ └── sheets/ PermissionSheet, QuestionSheet
└── AppRoot.kt routes on RemoteState; hosts global sheets
The architecture contract the modules were built against lives in
DESIGN.md — package ownership, module APIs, wire-format
gotchas, and the design tokens.
Prerequisites: Android Studio (recent stable) with a JDK 11+ toolchain, an emulator or device on Android 13+ (API 33).
./gradlew assembleDebug # build
./gradlew installDebug # install on a connected device/emulator
./gradlew testDebugUnitTest # JVM unit testsTo actually use the app you need the Spettro desktop app running on a PC on
the same network with remote hosting enabled — scan its pairing QR code from
the app's pairing screen (or paste the spettro-pair:// URL manually).
JVM unit tests (app/src/test) cover the parts that must not drift:
- Pairing — HMAC-SHA256 proof vector,
spettro-pair://QR payload parse - JSON-RPC — framing and round-trips in
JsonRpcPeer(MockWebServer) - ACP parsing — both config-option wire shapes (ACP-native and the
Swift-synthesized
StoredSessionshape),StoredSessiondecode, session updates, permission/question requests, extension types - Model —
ChatSessiontranscript mutation and replay suppression,ToolCallItemtitle parsing
End-to-end verification is manual: the Spettro desktop app hosting on a PC plus this app on a device.
- All JSON goes through
core.SpettroJson(ignoreUnknownKeys,explicitNulls = false). Key spellings are copied exactly from the wire:hostID/chatID/deviceIDare capital-ID;sessionId/toolCallIdare not. - base64url unpadded for pairing material; standard padded base64 for image attachments.
- Async state is coroutines +
StateFlow/SharedFloweverywhere. - Design tokens mirror the desktop design system: accent
#526FFFlight /#6073CCdark, canvas#F9F9F7/#0E0E0E, hairline borders instead of shadows, user bubble = solid accent, assistant = full-width prose.
../Spettro— the desktop app (and iOS companion) this is a port of; itsdocs/folder is the protocol and design-system reference../spettro-CLI— the agent itself