71 lines
4.6 KiB
Markdown
71 lines
4.6 KiB
Markdown
---
|
|
title: "Drive your self-hosted AI coding agent from your phone"
|
|
published: true
|
|
tags: [android, selfhosted, ai, devtools]
|
|
cover_image:
|
|
canonical_url: https://dzianisv.github.io/opencode-mobile/
|
|
---
|
|
|
|
I run [opencode](https://github.com/sst/opencode) — an open-source AI coding agent — on my home server. It connects to Claude or GPT-4 via my own API keys and edits code through tool calls: reading files, writing diffs, running shell commands.
|
|
|
|
The workflow works well at my desk. Away from my desk, I had nothing useful. SSH into a terminal app loses the structured session UI. Generic AI chat apps know nothing about file diffs or tool call approval. I wanted to actually *steer* a running session from my phone, not just read raw text output.
|
|
|
|
So I built OpenCode Mobile.
|
|
|
|
## What it is
|
|
|
|
OpenCode Mobile is a React Native / Expo Android app. It's a thin client for the opencode HTTP + SSE API — no backend of mine between your phone and your server.
|
|
|
|
You run opencode in server mode:
|
|
|
|
```bash
|
|
npm install -g opencode-ai
|
|
OPENCODE_SERVER_PASSWORD=yourpassword opencode serve --hostname 0.0.0.0 --port 4096
|
|
```
|
|
|
|
Then in the app you paste the server URL — a local network IP, a Tailscale address, a Cloudflare Tunnel URL, whatever you already use to reach that machine. The app connects, lists your sessions, and streams output in real time.
|
|
|
|
## What the UI gives you
|
|
|
|
**Streaming chat.** Responses come in token by token via SSE. The same stream you'd see in the terminal, rendered in a mobile chat UI.
|
|
|
|
**Diff viewer.** When the AI coding agent proposes a file change, you see an inline line-level diff — green additions, red removals — before the write happens. No more wondering what changed.
|
|
|
|
**Tool call approval.** opencode pauses before executing file writes, shell commands, or other destructive actions and waits for explicit approval. The mobile app surfaces this as a bottom sheet: you approve or reject, and the agent proceeds or stops. This is the feature I personally find most useful — you can start a session, walk away, and only get pulled back when the agent needs a decision.
|
|
|
|
**Session management.** Create new sessions, resume old ones, switch between them.
|
|
|
|
**Biometric unlock.** Face/fingerprint gates both app open and individual message sends.
|
|
|
|
## How it connects
|
|
|
|
The app supports any method you use to expose a local server:
|
|
|
|
- **Local network** — works when phone and server are on the same Wi-Fi
|
|
- **Tailscale** — Tailscale IP works out of the box, no port forwarding
|
|
- **Cloudflare Tunnel** — `cloudflared tunnel` gives you a public HTTPS URL
|
|
- **ngrok** — same idea
|
|
|
|
Each saved connection is stored in Android Keystore via `expo-secure-store`. The URL and password never leave the device except to reach your server directly.
|
|
|
|
## Model support
|
|
|
|
opencode connects to whatever AI provider you configure: Anthropic Claude, OpenAI GPT-4, Google Gemini, or any local model behind an OpenAI-compatible API (ollama, LM Studio, etc.). The mobile app doesn't care — it talks to opencode, and opencode talks to your model. Your API keys stay on your server.
|
|
|
|
## Technical notes for the curious
|
|
|
|
The trickiest part was SSE streaming. React Native lacks a native `EventSource`. The standard web polyfill mostly works, but Android's OkHttp follows redirects and drops the `Accept: text/event-stream` header, which breaks reconnects. The fix is a custom fetch wrapper that sets the header on every hop.
|
|
|
|
The diff viewer is a custom line-level parser feeding a `FlatList`, not a WebView. This keeps dark-mode theming consistent and avoids the overhead of a full browser engine for rendering `+/-` lines.
|
|
|
|
Tool call approval required coordinating two async streams: the SSE stream coming in and the user response going out. The solution is a React Query mutation that fires on approval and an optimistic UI update that unblocks the stream client-side while the server confirms.
|
|
|
|
## Open source
|
|
|
|
MIT licensed. Source at [github.com/dzianisv/opencode-mobile](https://github.com/dzianisv/opencode-mobile). Issues and PRs welcome.
|
|
|
|
Install: add the self-hosted F-Droid repo `https://dzianisv.github.io/opencode-mobile/fdroid/repo` in your F-Droid client, or grab the [direct APK](https://github.com/dzianisv/opencode-mobile/releases/latest). Google Play is in internal testing and coming soon.
|
|
Landing page and docs: [https://dzianisv.github.io/opencode-mobile/](https://dzianisv.github.io/opencode-mobile/)
|
|
|
|
If you run opencode and try it, I'm especially interested in feedback on latency when using local models — the streaming UI has a minimum render interval to avoid thrashing, but I don't know where that threshold feels wrong on slow hardware.
|