fix(funnel): repair activation-path bugs found in end-to-end audit (#114)

Systematic trace of the activation funnel (store listing -> docs setup
guide -> in-app onboarding -> connect flow) after #113 showed a docs
404 bug had slipped through. Fixes found in this pass:

- README.md and CONTRIBUTING.md still told developers to run
  `npm install -g opencode` (missing -ai), the exact 404 #113 fixed
  everywhere else. Same package-name fix applied here.
- src/lib/diagnostics.ts: the health probe reported ok:true for any
  resolved fetch, including 401/403/404/500 responses. A wrong
  password or broken server therefore classified as "Health endpoint
  responded - connection actually works now.", contradicting the
  error shown right next to it. Health now requires a real 2xx;
  root-reachability probe explicitly opts out (requireOk: false) to
  keep its existing reachability-only semantics (already encoded in
  diagnostics-classify.test.ts).
- docs-site/vs-termux: architecture explainer described
  /session/{id}/events and /session/{id}/chat, neither of which
  exists; the app actually uses /global/event and
  /session/{id}/prompt_async (src/lib/sdk.ts).
- docs-site/features: opencode repo link pointed at
  github.com/opencode-ai/opencode instead of github.com/sst/opencode,
  the org used everywhere else in the docs.
- Stale "v0.4.3" version strings (README x3, docs-site/download x2)
  bumped to v0.4.7, the actual latest shipped release confirmed live
  on GitHub Releases and the F-Droid repo index. Fixed the associated
  Android min-OS contradiction (7.0 vs 8.0+) to 7.0, matching the live
  F-Droid manifest's minSdkVersion 24.
- Dropped stale "GPT-4" model naming on 3 docs-site pages, consistent
  with the model-agnostic policy already applied to the Play listing
  in #83.

See PR body for the found-not-fixed list (Play Console listing drift,
GitHub Sponsors not enabled, a Cloud-vs-waitlist messaging conflict)
that need a product/human decision rather than a code fix.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Den
2026-07-17 21:47:13 -07:00
committed by GitHub
parent b802df9797
commit 3c88e16ae5
7 changed files with 25 additions and 18 deletions

View File

@@ -84,7 +84,7 @@ You need a running [opencode](https://github.com/sst/opencode) server to test th
```bash ```bash
# Install opencode # Install opencode
npm install -g opencode npm install -g opencode-ai
# Start it in server mode on all interfaces # Start it in server mode on all interfaces
OPENCODE_SERVER_PASSWORD=devpassword opencode serve --hostname 0.0.0.0 --port 4096 OPENCODE_SERVER_PASSWORD=devpassword opencode serve --hostname 0.0.0.0 --port 4096

View File

@@ -28,7 +28,7 @@ There are **three working ways** to install OpenCode Mobile today, all for Andro
``` ```
https://dzianisv.github.io/opencode-mobile/fdroid/repo https://dzianisv.github.io/opencode-mobile/fdroid/repo
``` ```
In the F-Droid app: **Settings → Repositories → + (add)** and paste the URL above. Current version: **v0.4.3**. In the F-Droid app: **Settings → Repositories → + (add)** and paste the URL above. Current version: **v0.4.7**.
3. **Direct signed APK** — download the latest release and install it manually: 3. **Direct signed APK** — download the latest release and install it manually:
**https://github.com/dzianisv/opencode-mobile/releases/latest** **https://github.com/dzianisv/opencode-mobile/releases/latest**
@@ -64,7 +64,7 @@ OpenCode Mobile is a React Native / Expo app that brings the power of the [openc
## Get OpenCode Mobile ## Get OpenCode Mobile
Package: `cc.agentlabs.opencode` · Android only · current version v0.4.3 Package: `cc.agentlabs.opencode` · Android only · current version v0.4.7
| Channel | Status | How | | Channel | Status | How |
|---|---|---| |---|---|---|
@@ -86,7 +86,7 @@ Package: `cc.agentlabs.opencode` · Android only · current version v0.4.3
```bash ```bash
# Install opencode (if you haven't already) # Install opencode (if you haven't already)
npm install -g opencode npm install -g opencode-ai
# Run opencode in server mode # Run opencode in server mode
OPENCODE_SERVER_PASSWORD=yourpassword opencode serve --hostname 0.0.0.0 --port 4096 OPENCODE_SERVER_PASSWORD=yourpassword opencode serve --hostname 0.0.0.0 --port 4096
@@ -137,7 +137,7 @@ OpenCode Mobile is a thin client. It speaks the opencode HTTP + SSE API: listing
## Project Status ## Project Status
**Current version: v0.4.3** **Current version: v0.4.7**
| Feature | Status | | Feature | Status |
|---|---| |---|---|

View File

@@ -198,7 +198,7 @@ td:first-child{font-weight:500}
</tr> </tr>
<tr> <tr>
<td>Bring your own LLM</td> <td>Bring your own LLM</td>
<td class="yes">Yes — any provider opencode supports (Claude, GPT-4, local models)</td> <td class="yes">Yes — any provider opencode supports (Claude, GPT, Gemini, local models)</td>
<td class="no">OpenAI models only</td> <td class="no">OpenAI models only</td>
<td class="no">GitHub Copilot models only</td> <td class="no">GitHub Copilot models only</td>
</tr> </tr>

View File

@@ -42,11 +42,11 @@
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "SoftwareApplication", "@type": "SoftwareApplication",
"name": "OpenCode Mobile", "name": "OpenCode Mobile",
"operatingSystem": "Android 8.0+", "operatingSystem": "Android 7.0+",
"applicationCategory": "DeveloperApplication", "applicationCategory": "DeveloperApplication",
"description": "Open-source Android client for the OpenCode AI coding agent. Connect to an opencode server you run yourself and code from your phone.", "description": "Open-source Android client for the OpenCode AI coding agent. Connect to an opencode server you run yourself and code from your phone.",
"downloadUrl": "https://github.com/dzianisv/opencode-mobile/releases/latest/download/app-release.apk", "downloadUrl": "https://github.com/dzianisv/opencode-mobile/releases/latest/download/app-release.apk",
"softwareVersion": "0.4.3", "softwareVersion": "0.4.7",
"license": "https://github.com/dzianisv/opencode-mobile/blob/main/LICENSE", "license": "https://github.com/dzianisv/opencode-mobile/blob/main/LICENSE",
"author": { "@type": "Organization", "name": "VIBE TECHNOLOGIES, LLC" }, "author": { "@type": "Organization", "name": "VIBE TECHNOLOGIES, LLC" },
"offers": { "@type": "Offer", "price": "0", "priceCurrency": "USD" } "offers": { "@type": "Offer", "price": "0", "priceCurrency": "USD" }
@@ -171,7 +171,7 @@ footer a{margin:0 8px}
OpenCode Mobile is the free, open-source Android client for the OpenCode Mobile is the free, open-source Android client for the
<a href="https://github.com/sst/opencode">OpenCode</a> AI coding agent. Install it from <a href="https://github.com/sst/opencode">OpenCode</a> AI coding agent. Install it from
the F-Droid repository for automatic updates, or grab the signed APK directly. the F-Droid repository for automatic updates, or grab the signed APK directly.
No account, no tracking, no ads. Current version <strong>0.4.3</strong>. No account, no tracking, no ads. Current version <strong>0.4.7</strong>.
</p> </p>
<div class="note"> <div class="note">
<strong>Before you install:</strong> OpenCode Mobile is a <em>client</em> — it connects to an <strong>Before you install:</strong> OpenCode Mobile is a <em>client</em> — it connects to an
@@ -252,7 +252,7 @@ footer a{margin:0 8px}
</p> </p>
<h2>What do I need to run it?</h2> <h2>What do I need to run it?</h2>
<p> <p>
An Android 8.0+ device, a machine running <code>opencode serve</code>, a way to reach that machine An Android 7.0+ device, a machine running <code>opencode serve</code>, a way to reach that machine
from your phone (Tailscale, a Cloudflare/ngrok tunnel, or the same local network), and your own AI from your phone (Tailscale, a Cloudflare/ngrok tunnel, or the same local network), and your own AI
provider key. The <a href="../guide/">setup guide</a> walks through every step in about 15 minutes. provider key. The <a href="../guide/">setup guide</a> walks through every step in about 15 minutes.
</p> </p>

View File

@@ -50,7 +50,7 @@
"offers": { "@type": "Offer", "price": "0", "priceCurrency": "USD" }, "offers": { "@type": "Offer", "price": "0", "priceCurrency": "USD" },
"featureList": [ "featureList": [
"Real-time AI coding sessions via Server-Sent Events", "Real-time AI coding sessions via Server-Sent Events",
"Multi-model support: Claude, GPT-4, Gemini, Ollama, and any opencode-compatible model", "Multi-model support: Claude, GPT, Gemini, Ollama, and any opencode-compatible model",
"Tailscale and VPN connectivity for private server access", "Tailscale and VPN connectivity for private server access",
"Streaming diff and tool-use output as the agent works", "Streaming diff and tool-use output as the agent works",
"Full session history with scroll and search", "Full session history with scroll and search",
@@ -234,7 +234,7 @@ footer a:hover{color:var(--accent)}
<h2>System requirements</h2> <h2>System requirements</h2>
<ul class="check-list"> <ul class="check-list">
<li>Android 7.0 (API 24) or later</li> <li>Android 7.0 (API 24) or later</li>
<li><a href="https://github.com/opencode-ai/opencode">opencode</a> running on a reachable server (<code>opencode serve</code>)</li> <li><a href="https://github.com/sst/opencode">opencode</a> running on a reachable server (<code>opencode serve</code>)</li>
<li>Network path from phone to server (LAN, Tailscale VPN, or any tunnel)</li> <li>Network path from phone to server (LAN, Tailscale VPN, or any tunnel)</li>
<li>~15 MB storage for the app; session history grows with use</li> <li>~15 MB storage for the app; session history grows with use</li>
</ul> </ul>

View File

@@ -436,9 +436,9 @@ OPENCODE_SERVER_PASSWORD=yourpassword opencode serve --hostname 0.0.0.0 --port 4
happens: happens:
</p> </p>
<ol> <ol>
<li>The app opens an SSE stream to your opencode server's <code>/session/{id}/events</code> endpoint.</li> <li>The app opens an SSE stream to your opencode server's <code>/global/event</code> endpoint.</li>
<li>You type a prompt — the app posts it to <code>/session/{id}/chat</code>.</li> <li>You type a prompt — the app posts it to <code>/session/{id}/prompt_async</code>.</li>
<li>The server forwards the prompt to your configured LLM (Claude, GPT-4, a local model, whatever you set up).</li> <li>The server forwards the prompt to your configured LLM (Claude, GPT, a local model, whatever you set up).</li>
<li>Tokens stream back from the LLM to the server, and from the server over SSE to the app.</li> <li>Tokens stream back from the LLM to the server, and from the server over SSE to the app.</li>
<li>When the agent proposes a file change, the app renders the diff and waits for your approve/reject tap.</li> <li>When the agent proposes a file change, the app renders the diff and waits for your approve/reject tap.</li>
<li>Your approval goes back to the server, which applies the change to the real filesystem.</li> <li>Your approval goes back to the server, which applies the change to the real filesystem.</li>

View File

@@ -36,13 +36,20 @@ export interface DiagnosticReport {
timestamp: string timestamp: string
} }
async function timedFetch(name: string, target: string, init?: RequestInit): Promise<ProbeAttempt> { // requireOk: whether a non-2xx HTTP response counts as a probe failure.
// The health probe must actually succeed (2xx) to mean "the server works" —
// otherwise a 401/403/404/500 response was being reported as ok:true, which
// made classify() short-circuit to "connection actually works now" even when
// auth failed or the server errored. The root probe only checks reachability
// (any HTTP response, even an error status, proves something is listening).
async function timedFetch(name: string, target: string, init?: RequestInit, opts?: { requireOk?: boolean }): Promise<ProbeAttempt> {
const requireOk = opts?.requireOk ?? true
const start = Date.now() const start = Date.now()
const controller = new AbortController() const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS) const timer = setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS)
try { try {
const res = await fetch(target, { ...init, signal: controller.signal }) const res = await fetch(target, { ...init, signal: controller.signal })
return { name, target, ok: true, status: res.status, durationMs: Date.now() - start } return { name, target, ok: requireOk ? res.ok : true, status: res.status, durationMs: Date.now() - start }
} catch (error: unknown) { } catch (error: unknown) {
const err = error as { name?: string; message?: string; cause?: unknown } const err = error as { name?: string; message?: string; cause?: unknown }
const aborted = err?.name === "AbortError" const aborted = err?.name === "AbortError"
@@ -80,7 +87,7 @@ export async function probeConnection(url: string, auth?: { username: string; pa
const base = `${parsed.scheme}://${parsed.host}:${parsed.port}` const base = `${parsed.scheme}://${parsed.host}:${parsed.port}`
;[health, root, internet] = await Promise.all([ ;[health, root, internet] = await Promise.all([
timedFetch("health", `${base}/global/health`, { headers }), timedFetch("health", `${base}/global/health`, { headers }),
timedFetch("server-root", `${base}/`, { headers }), timedFetch("server-root", `${base}/`, { headers }, { requireOk: false }),
timedFetch("internet", INTERNET_CHECK_URL), timedFetch("internet", INTERNET_CHECK_URL),
]) ])
} else { } else {