git.iko.soy/opencode-android
on 80ddf73 help sign in
opencode-android/README.md
118 lines 7.5 kB history raw
1# opencode-android
3A native Android client (Kotlin + Jetpack Compose) for the [`opencode serve`](https://opencode.ai/docs/server/) HTTP API.
5Application id: `soy.iko.opencode`
7## Status
9Milestones M0–M10 complete:
11- **M0–M4:** connect to a server → list/create/delete sessions → chat with **live SSE streaming**
12 of the assistant's reply. Tool calls, reasoning, and token/cost are rendered.
13- **M5:** per-session **model picker** (`GET /config/providers`); **permission prompts** — the
14 agent's `permission.updated` asks surface as an allow-once / always / reject dialog, answered via
15 `POST /session/:id/permissions/:permissionID`, so tool runs don't stall; **file browser + viewer**
16 (`/find/file` fuzzy search, `/file` directory listing, `/file/content` read-only view); **reconnect
17 banner** (live SSE connection state) and a real **Settings** screen (theme mode, persisted).
18- **M6:** **markdown rendering** of assistant text (headings, code blocks, lists, blockquotes, inline
19 bold/italic/code/links/strikethrough); **rich diff/patch rendering** — tool outputs and file-viewer
20 content that contain unified diffs are syntax-highlighted (added lines green, removed lines red,
21 hunk headers); **agents & commands UI** — `GET /agent` powers a per-message agent picker (sent via
22 the `agent` field on the prompt body); `GET /command` powers a slash-command runner that sends the
23 command template with its configured agent; **multi-server quick-switch** — the session list title
24 is a dropdown that lists all saved servers and switches instantly without navigating back.
25- **M7:** **UX polish pass.** Draft retention (failed sends restore your text with a Retry action);
26 destructive-action confirmations (delete session / remove server); the app bar now shows the real
27 session title; smart auto-scroll that won't fight a reader who scrolled up, with a jump-to-latest
28 FAB; long-press to copy assistant markdown, plus copy buttons on tool output and diffs; expandable
29 tool output (Show more / Show less) instead of a hard truncation; **session list** with relative
30 timestamps, last-message previews, and a search filter; tappable **file-browser breadcrumbs**;
31 a "connected" marker on the active server; permission dialog with clear action hierarchy;
32 dynamic "Thinking…" → "Thoughts" reasoning label; back-press guard while an agent is running;
33 polished empty states; richer **Settings** (Dynamic Color / Material You toggle, version info,
34 manage-servers link); haptic feedback; and a file viewer with line numbers, copy, and share.
35 The session list also supports **renaming** a session (`PATCH /session/:id`).
36- **M8:** **Enter-to-send** (hardware-keyboard Enter sends, Shift+Enter for newline);
37 **per-session draft persistence** — typed text survives back-navigation and process
38 death; **message timestamps** (relative time on every bubble); **VCS status in the file
39 browser** — `GET /file/status` powers added/modified/deleted badges with `+N/−N` line
40 counts on tracked files; **connectivity-aware SSE reconnect** — a `ConnectivityManager`
41 callback triggers an immediate reconnect (skipping backoff) when the device regains
42 network; cleaner **token/cost formatting** (`1,234 in · 5,678 out`, precision-aware cost);
43 and **bounded preview loading** (concurrency-capped, cancelled on refresh) so the session
44 list never floods the server with parallel requests.
46- **M9:** **Live session list** — `session.updated` / `session.deleted` SSE events now
47 drive incremental list updates (new sessions, renames, deletions appear without a
48 manual refresh); **pull-to-refresh** on the session list; **keep-screen-on** while an
49 agent is running so long tasks aren't interrupted by screen-off; **chat loading
50 state** — a spinner replaces the empty-conversation flash before the first message
51 load; **reconnect button** when the connection is lost mid-chat; **live session
52 title** in the chat header (updates when the agent renames the session);
53 **connection-aware run indicator** — the spinner resets if the SSE stream drops
54 mid-run instead of spinning forever; **retry-on-transient-failure** for all read
55 endpoints (sessions, messages, files, providers); and **per-code-block copy
56 buttons** in rendered markdown.
58- **M10:** **Reliability & UX hardening pass.** Fixed a latent crash where a diff in a
59 tool output nested a second `LazyColumn` inside the message list (now a bounded
60 column); **inline image rendering** for `file` parts — attachments are decoded
61 (data URIs) or fetched with Basic auth via the already-present Coil dependency;
62 **connection banner on the session list** so a dropped SSE stream is visible before
63 the list goes stale; **unread activity badges** — background sessions that receive a
64 reply get a dot until opened; **conflated message stream** so per-token snapshots no
65 longer back-pressure the event reducer on long replies; **typed error reporting** —
66 network/timeout/HTTP-status failures are classified by concrete exception type (with
67 a unit-tested classifier) instead of string-matching class names; **swipe-to-delete**
68 on session cards; **conversation export/share** (Markdown via the system share
69 sheet); and an **auto-incrementing `versionCode`** derived from the git commit count
70 so each release is strictly newer.
72## Architecture
74- **Ktor 3 (OkHttp engine)** for REST + the long-lived `GET /event` SSE stream.
75- **kotlinx.serialization** with sealed polymorphism for opencode's discriminated unions
76 (`Part`/`type`, `MessageInfo`/`role`, `ToolState`/`status`, `BusEvent`/`type`). Unknown
77 discriminator values fall back to `Unknown*` variants (`OpencodeJson`) so a new server release
78 can't break decoding.
79- **Manual DI** (`AppContainer` on the `Application`) — no Hilt/KSP.
80- **Unidirectional state**: `EventStreamClient` exposes the SSE stream as a hot `SharedFlow`;
81 `SessionRepository` reduces events into per-session message state; ViewModels expose `StateFlow`;
82 Compose collects with `collectAsStateWithLifecycle`.
83- The repository subscribes to `/event` **before** the prompt POST so no early streamed part is missed.
85Source map: `data/network` (Ktor, SSE, JSON), `data/model` (wire models), `data/repo`
86(reducer + profile persistence), `di` (container/connection), `ui/<feature>` (Compose screens).
88## Build
90```bash
91nix develop # JDK 17, Gradle 8.14.4, Android SDK 35, aapt2 override
92./gradlew assembleDebug # -> app/build/outputs/apk/debug/app-debug.apk
93./gradlew assembleRelease # R8-minified + resource-shrunk release APK
94./gradlew testDebugUnitTest # JVM tests for the JSON/serialization layer
95```
97## Install & run
99```bash
100adb install -r app/build/outputs/apk/debug/app-debug.apk
101adb shell am start -n soy.iko.opencode/.MainActivity
102adb logcat --pid=$(adb shell pidof soy.iko.opencode)
103```
105## Connect to a server
107On the dev host:
109```bash
110opencode serve --hostname 0.0.0.0 --port 4096
111```
113In the app, add a server with base URL `http://<host-lan-ip>:4096`. If the host is reachable only
114over USB, use `adb reverse tcp:4096 tcp:4096` and point the app at `http://127.0.0.1:4096`.
116**Auth** is optional: opencode has no auth by default. If the server runs with
117`OPENCODE_SERVER_PASSWORD=...`, enter username `opencode` (the default) and that password — the app
118sends HTTP Basic auth. The app permits cleartext HTTP (opencode serves plain `http://`).