@@ -4,123 +4,55 @@ A native Android client (Kotlin + Jetpack Compose) for the [`opencode serve`](ht
5Application id: `soy.iko.opencode`
5Application id: `soy.iko.opencode`
7−## Status
7+## Set up a server and connect to it
9−Milestones M0–M12 complete:
9+This app is a client for the [`opencode serve`](https://opencode.ai/docs/server/) HTTP API, so you
10−
10+need an opencode server running somewhere reachable from your device.
11−- **M0–M4:** connect to a server → list/create/delete sessions → chat with **live SSE streaming**
11+
12− of the assistant's reply. Tool calls, reasoning, and token/cost are rendered.
12+### 1. Start the server
13−- **M5:** per-session **model picker** (`GET /config/providers`); **permission prompts** — the
13+
14− agent's `permission.updated` asks surface as an allow-once / always / reject dialog, answered via
14+On the host that has your project and opencode installed:
15− `POST /session/:id/permissions/:permissionID`, so tool runs don't stall; **file browser + viewer**
15+
16− (`/find/file` fuzzy search, `/file` directory listing, `/file/content` read-only view); **reconnect
16+```bash
17− banner** (live SSE connection state) and a real **Settings** screen (theme mode, persisted).
17+opencode serve --hostname 0.0.0.0 --port 4096
18−- **M6:** **markdown rendering** of assistant text (headings, code blocks, lists, blockquotes, inline
18+```
19− bold/italic/code/links/strikethrough); **rich diff/patch rendering** — tool outputs and file-viewer
19+
20− content that contain unified diffs are syntax-highlighted (added lines green, removed lines red,
20+Useful flags:
21− hunk headers); **agents & commands UI** — `GET /agent` powers a per-message agent picker (sent via
21+
22− the `agent` field on the prompt body); `GET /command` powers a slash-command runner that sends the
22+| Flag | Default | Description |
23− command template with its configured agent; **multi-server quick-switch** — the session list title
23+| --- | --- | --- |
24− is a dropdown that lists all saved servers and switches instantly without navigating back.
24+| `--port` | `4096` | Port to listen on |
25−- **M7:** **UX polish pass.** Draft retention (failed sends restore your text with a Retry action);
25+| `--hostname` | `127.0.0.1` | Hostname/interface to bind (`0.0.0.0` exposes it on your LAN) |
26− destructive-action confirmations (delete session / remove server); the app bar now shows the real
26+| `--mdns` | `false` | Enable mDNS discovery |
27− session title; smart auto-scroll that won't fight a reader who scrolled up, with a jump-to-latest
27+| `--cors` | `[]` | Extra browser origins to allow (repeatable) |
28− FAB; long-press to copy assistant markdown, plus copy buttons on tool output and diffs; expandable
28+
29− tool output (Show more / Show less) instead of a hard truncation; **session list** with relative
29+The server publishes an OpenAPI 3.1 spec at `http://<hostname>:<port>/doc`.
30− timestamps, last-message previews, and a search filter; tappable **file-browser breadcrumbs**;
30+
31− a "connected" marker on the active server; permission dialog with clear action hierarchy;
31+### 2. (Optional) Add authentication
32− dynamic "Thinking…" → "Thoughts" reasoning label; back-press guard while an agent is running;
32+
33− polished empty states; richer **Settings** (Dynamic Color / Material You toggle, version info,
33+opencode has no auth by default. To protect the server with HTTP Basic auth:
34− manage-servers link); haptic feedback; and a file viewer with line numbers, copy, and share.
34+
35− The session list also supports **renaming** a session (`PATCH /session/:id`).
35+```bash
36−- **M8:** **Enter-to-send** (hardware-keyboard Enter sends, Shift+Enter for newline);
36+OPENCODE_SERVER_PASSWORD=your-password opencode serve --hostname 0.0.0.0 --port 4096
37− **per-session draft persistence** — typed text survives back-navigation and process
37+```
38− death; **message timestamps** (relative time on every bubble); **VCS status in the file
38+
39− browser** — `GET /file/status` powers added/modified/deleted badges with `+N/−N` line
39+The username defaults to `opencode` (override with `OPENCODE_SERVER_USERNAME`). Enter these in the
40− counts on tracked files; **connectivity-aware SSE reconnect** — a `ConnectivityManager`
40+app's add-server dialog.
41− callback triggers an immediate reconnect (skipping backoff) when the device regains
41+
42− network; cleaner **token/cost formatting** (`1,234 in · 5,678 out`, precision-aware cost);
42+### 3. Connect from the app
43− and **bounded preview loading** (concurrency-capped, cancelled on refresh) so the session
43+
44− list never floods the server with parallel requests.
44+**Over Wi-Fi / LAN** — add a server with base URL `http://<host-LAN-ip>:4096`.
46−- **M9:** **Live session list** — `session.updated` / `session.deleted` SSE events now
46+**Over USB only** — forward the port to the device and point the app at localhost:
47− drive incremental list updates (new sessions, renames, deletions appear without a
47+
48− manual refresh); **pull-to-refresh** on the session list; **keep-screen-on** while an
48+```bash
49− agent is running so long tasks aren't interrupted by screen-off; **chat loading
49+adb reverse tcp:4096 tcp:4096
50− state** — a spinner replaces the empty-conversation flash before the first message
50+```
51− load; **reconnect button** when the connection is lost mid-chat; **live session
51+
52− title** in the chat header (updates when the agent renames the session);
52+Then use base URL `http://127.0.0.1:4096` in the app.
53− **connection-aware run indicator** — the spinner resets if the SSE stream drops
53+
54− mid-run instead of spinning forever; **retry-on-transient-failure** for all read
54+> The app permits cleartext HTTP because opencode serves plain `http://`. If you terminate TLS with
55− endpoints (sessions, messages, files, providers); and **per-code-block copy
55+> a reverse proxy, use an `https://` base URL instead.
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−- **M11:** **Reliability, a11y & tooling pass.**
73− - **Completion notifications** — when a background session finishes a run it posts a
74− system notification; tapping it opens the conversation. POST_NOTIFICATIONS is
75− requested on Android 13+ and respected if denied.
76− - **Foreground SSE service** — while an agent is running a low-priority foreground
77− service holds process priority so Doze can't choke the long-lived `/event` stream
78− mid-run when the app is backgrounded.
79− - **Crash reporting + Diagnostics** — an uncaught-exception handler writes crash
80− reports (stack trace + device/app metadata) to app-private storage, surfaced in a
81− new Settings → Diagnostics screen where they can be viewed, shared, or cleared. No
82− hosted backend required.
83− - **Adaptive large-screen layout** — on wide screens (tablets / unfolded foldables,
84− ≥ 840dp) the session list and the open conversation show side by side; compact
85− widths keep the single-pane back stack.
86− - **Deep linking** — `opencode://session/{id}` opens a conversation from any app, and
87− notification taps / share-in reuse the same open-session path.
88− - **Accessibility** — every clickable row is announced as a button to TalkBack
89− (`role` semantics); all icon buttons carry content descriptions.
90− - **Crash bug fix** — `StateFlow.value` was read inside composition (server switch
91− wouldn't recompose the image context); now collected as state.
92− - **App icon** — an adaptive launcher icon was added (lint flagged its absence).
93− - **Tooling/CI:** Android **Lint** and **detekt** now run in CI (detekt with a
94− baseline so only new findings block); a **Renovate** config keeps dependencies
95− current (grouped, weekly, AndroidX majors held for review); the deprecated
96− `kotlinOptions` DSL was replaced; and the release-artifact upload matches the
97− signed-output path. Instrumented tests (androidTest) cover the crash logger and
100−- **M12:** **Reliability, a11y & test-coverage pass.**
101− - **Diagnostics disk I/O off the main thread** — crash report reads in the
102− Diagnostics screen were moved from synchronous `remember` blocks (composition
103− / main thread) into coroutines with `Dispatchers.IO`, eliminating potential
104− jank on large reports.
105− - **Orphaned coroutine scope fix** — `DraftStore` no longer creates an
106− unscoped `CoroutineScope` that leaks; it now uses the app-scoped
107− `CoroutineScope` from `AppContainer`.
108− - **Predictive back** — `android:enableOnBackInvokedCallback="true"` added to
109− the manifest so the Android 14+ predictive back gesture integrates properly
110− with the app's `BackHandler` usage.
111− - **Crash report filename uniqueness** — crash report timestamps now include
112− milliseconds so two crashes within the same second don't overwrite each other.
113− - **Permission response retry** — `POST /session/:id/permissions/:permissionID`
114− now uses the same retry-with-backoff as other REST calls, so a transient
115− network failure no longer leaves a tool run stuck without automatic retry.
116− - **Accessibility: space-prefix text patterns** — six `" " + text` patterns
117− that screen readers would announce with extra spaces were replaced with
118− `Modifier.padding(start = ...)` on the Text composables.
119− - **Accessibility: unread badge** — the unread activity dot on session cards
120− now carries a `contentDescription` so TalkBack announces it.
121− - **Test coverage** — added `OpencodeApiClientTest` (3 tests covering the
122− permission-response retry path) and `DraftStoreTest` (instrumented, covering
123− set/get/remove/blank-clears round-trip).
125## Architecture
57## Architecture
@@ -156,19 +88,4 @@ nix develop # JDK 17, Gradle 8.14.4, Android SDK 35, aapt2 override
156adb install -r app/build/outputs/apk/debug/app-debug.apk
88adb install -r app/build/outputs/apk/debug/app-debug.apk
157adb shell am start -n soy.iko.opencode/.MainActivity
89adb shell am start -n soy.iko.opencode/.MainActivity
158adb logcat --pid=$(adb shell pidof soy.iko.opencode)
90adb logcat --pid=$(adb shell pidof soy.iko.opencode)
161−## Connect to a server
166−opencode serve --hostname 0.0.0.0 --port 4096
169−In the app, add a server with base URL `http://<host-lan-ip>:4096`. If the host is reachable only
170−over USB, use `adb reverse tcp:4096 tcp:4096` and point the app at `http://127.0.0.1:4096`.
172−**Auth** is optional: opencode has no auth by default. If the server runs with
173−`OPENCODE_SERVER_PASSWORD=...`, enter username `opencode` (the default) and that password — the app
174−sends HTTP Basic auth. The app permits cleartext HTTP (opencode serves plain `http://`).