3An Android client for the [opencode](https://github.com/anomalyco/opencode) server, built with
4[Crux](https://github.com/redbadger/crux) (shared Rust core + Kotlin shell) and Jetpack Compose.
9┌─────────────────────────────────────────────────┐
10│ Shell (Kotlin / Jetpack Compose) │
11│ ┌─────────────┐ ┌──────────────────────────┐ │
12│ │ Compose UI │ │ HttpClient / SseClient │ │
13│ │ (3 screens) │ │ (Ktor + OkHttp) │ │
14│ └──────┬───────┘ └────────────┬─────────────┘ │
16│ Event (bincode) Effect (bincode) │
18│ ┌──────▼───────────────────────┴────────────┐ │
19│ │ Core (ViewModel) — FFI bridge │ │
20│ │ CoreFfi → NativeCoreFfi / PureCoreFfi │ │
21│ └───────────────────────────────────────────┘ │
22└──────────┬──────────────────────────────────────┘
24┌──────────▼──────────────────────────────────────┐
26│ ┌────────┐ ┌─────────┐ ┌──────────┐ │
27│ │ App │ │ Model │ │ Commands │ │
28│ │ update │ │ state │ │ effects │ │
29│ └────────┘ └─────────┘ └──────────┘ │
30│ No side-effects — pure logic │
31└─────────────────────────────────────────────────┘
34### Rust Core (`shared/`)
35- **`app.rs`** — Crux `App` implementation: `Event`, `Model`, `ViewModel`, `Effect`, `update()`, `view()`
36 - **Basic-auth detection**: probes `/global/health` without credentials; on 401 surfaces
37 `auth_required` in the ViewModel and waits for the user to supply credentials. All subsequent
38 requests carry `Authorization: Basic <base64(user:pass)>` (RFC 7617).
39 - **Crash log tracking**: accepts `CrashLog(String)` events from the shell and exposes
40 `crash_log_count` / `latest_crash_log` in the ViewModel.
41- **`ffi.rs`** — BoltFFI export wrapping `crux_core::bridge::Bridge`
42- **`bin/codegen.rs`** — Type generator (produces Kotlin types via `facet-generate`)
44### Kotlin Shell (`app/`)
45- **`CrashLogger.kt`** — Installs as the global `UncaughtExceptionHandler`. Crashes are
46 persisted to `filesDir/crashes/` (max 10 reports, newest first) and logged to logcat.
47 Reports survive process death and can be enumerated/exported/cleared.
48- **`core/CoreFfi.kt`** — FFI interface + JNI bridge (loads `libopencode.so`)
49- **`core/PureCoreFfi.kt`** — Pure-Kotlin fallback (mirrors Rust logic for dev without NDK),
50 includes basic-auth detection matching the Rust core
51- **`core/Core.kt`** — Android ViewModel bridge: sends events, processes effects, exposes
52 `StateFlow<ViewModel>`, forwards crash logs
53- **`core/HttpClient.kt`** — Executes `Effect.Http` via Ktor/OkHttp, returns `HttpResult` to core
54- **`core/SseClient.kt`** — Subscribes to opencode SSE event stream; errors reported to CrashLogger
55- **`ui/screens/`** — Connect (with credential fields), Sessions, Chat screens (Jetpack Compose + Material 3)
57### Generated Types (`generated/`)
58- Auto-generated Kotlin types matching the Rust core (Event, Effect, ViewModel, Request, etc.)
59- Bincode serialization helpers (`com.novi.bincode`, `com.novi.serde`)
64- Rust 1.90+ (with Android NDK targets for native builds)
65- Android SDK (compile SDK 36, build-tools 36.0.0)
68### Generate Kotlin types
71cargo run --bin codegen --features codegen -- --language kotlin --output-dir ../generated
74### Build native library (requires NDK)
76# Install Android targets
77rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android
79# Build and pack with BoltFFI
85./gradlew assembleDebug
88The app works without the native library — `PureCoreFfi` provides a pure-Kotlin
89reimplementation of the core logic. When `libopencode.so` is present, `NativeCoreFfi`
95cd shared && cargo test
98./gradlew testDebugUnitTest
101## Basic Auth Detection
103The opencode server uses HTTP Basic Auth when `OPENCODE_SERVER_PASSWORD` is set
104(username defaults to `opencode`). The client detects this automatically:
1061. On Connect, the core probes `GET /global/health` without credentials
1072. If the server responds `401`, the core sets `auth_required = true` in the ViewModel
1083. The UI shows username/password fields
1094. The user enters credentials and taps Connect again
1105. The core retries the probe with `Authorization: Basic <base64(user:pass)>`
1116. On `200`, the core marks `authed = true` and all subsequent requests (sessions,
112 messages, send, SSE) carry the Authorization header
1137. On `401` with credentials, the core shows "Invalid credentials"
117`CrashLogger` installs as the global `Thread.UncaughtExceptionHandler` in
118`OpencodeApp.onCreate()`. When a crash occurs:
120- The stack trace is written to logcat (`opencode-crash` tag)
121- A crash report file is persisted to `filesDir/crashes/crash_<timestamp>.txt`
122- Reports are pruned to the 10 most recent
123- Reports survive process death and can be accessed via `CrashLogger.getReports()`
125The crash log count is displayed on the Connect screen.
129The client connects to an opencode server and supports:
130- **Health check** (`GET /global/health`) — with basic-auth detection
131- **Session list** (`GET /session`)
132- **Create session** (`POST /session`)
133- **Load messages** (`GET /session/{id}/message`)
134- **Send message** (`POST /session/{id}/prompt_async`)
135- **SSE events** (`GET /event`) — live message streaming
139The default server URL is `http://localhost:4096`. Change it in the Connect screen.
140`android:usesCleartextTraffic="true"` is enabled for local development.
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list