Files
opencode-mobile/docs/marketing/devto.md
engineer 108260cec7 release: bump versionCode 5->6 for v0.4.4 + add changelog 6.txt
v0.4.3 already shipped as versionCode 5; a duplicate code would be rejected by Play and ignored by F-Droid. Bump to 6 unblocks the v0.4.4 release. QA gate passed (units + on-device E2E + visual render check screenshots in docs/qa/render-check/).
2026-06-08 08:36:53 -07:00

72 lines
5.0 KiB
Markdown

---
title: "Drive your self-hosted AI coding agent from your phone"
published: true
tags: [android, selfhosted, ai, devtools]
cover_image: https://dzianisv.github.io/opencode-mobile/play-graphics/feature-graphic.png
canonical_url: https://dzianisv.github.io/opencode-mobile/
---
> Publish on dev.to a day or two AFTER the HN/Reddit launch — it's evergreen SEO, not a same-day play. Set `canonical_url` to the landing page so the landing page gets SEO credit. Swap `cover_image` for a hosted hero/feature graphic before publishing. Replace `{{PLAY_URL}}` with the Play link once approved.
I run [opencode](https://github.com/sst/opencode) — an open-source AI coding agent — on my home server. It connects to Claude or GPT 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 agent proposes a file change, you see an inline line-level diff — green additions, red removals — before the write happens.
**Tool-call approval.** opencode pauses before executing file writes, shell commands, or other destructive actions and waits for explicit approval. The app surfaces this as a bottom sheet: approve or reject, and the agent proceeds or stops. This is the feature I personally find most useful — start a session, walk away, and only get pulled back when the agent needs a decision.
**Session management.** Create, resume, switch between sessions.
**Biometric unlock.** Face/fingerprint gates both app-open and individual message sends.
## How it connects
- **Local network** — phone and server 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 the 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 provider you configure: Anthropic Claude, OpenAI GPT, Google Gemini, or any local model behind an OpenAI-compatible API (ollama, LM Studio, llama.cpp). 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 on the hop, which breaks reconnects against tunneled servers. The fix is a custom fetch wrapper that sets the header on every hop and manages reconnect/backoff.
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 inbound SSE stream and the outbound user response. The solution is a React Query mutation that fires on approval plus 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 ([all options on one page](https://dzianisv.github.io/opencode-mobile/download/)): 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 review and going live shortly: {{PLAY_URL}}
Landing page and setup guide: [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 with local models — the streaming UI has a minimum render interval to avoid thrashing, and I don't know where that threshold feels wrong on slow hardware.