New high-intent SEO pages:
- /remote-access/ — step-by-step Tailscale, Cloudflare Tunnel, ngrok setups
for reaching a self-hosted opencode server from Android anywhere. HowTo +
FAQ + BreadcrumbList structured data.
- /ios/ — honest 'no iOS app yet' page: current Android app, why, options,
roadmap, how to follow. FAQ + BreadcrumbList structured data.
Added to sitemap.xml; internal links from index, guide, opencode-on-phone,
and troubleshooting.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Run 27139243275 FAILED with "/usr/bin/sh: Syntax error: end of file unexpected
(expecting fi)" — android-emulator-runner runs the script under dash, which
mangled the multi-line if/then/else/fi I added, so the scenarios never ran (a
real bug I introduced, not environmental). Fix:
- Move scenario-set selection into the probe step (runs under bash) and export
SCENARIOS as a step output; the emulator script is now single-line.
- The earlier probe returned a FALSE NEGATIVE: it POSTed to /session/{id}/message
with no model, but that endpoint REQUIRES model {providerID,modelID} (per the
opencode SDK the app uses). Probe now sends azure/gpt-5.4 exactly like the app,
uses -s + %{http_code} (not -sf) so the body/status are visible, and only flags
capable on HTTP 200 + an assistant text part.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The CI opencode server had NO LLM provider configured — the server log only
showed "listening", never a model. opencode-ai (released npm pkg) does not read
AZURE_OPENAI_* for its own LLM; it needs an explicit provider in opencode.json +
a default `model`. So send_message/multi_turn could never pass and the gate was
stuck on --only-connect-scenario (UI journey minus the model reply).
- Wire opencode to the same Azure resource the CUA driver uses via a generated
~/.config/opencode/opencode.json (@ai-sdk/azure provider, resourceName derived
from the endpoint secret at runtime, apiKey from env, default model azure/gpt-5.4).
- Add a deterministic REST probe step: create a session + send a prompt and check
for an assistant reply BEFORE the ~30min emulator run, exporting MODEL_CAPABLE.
- Add --scenarios to android-cua-smoke.py to run an explicit named set.
- Emulator step now runs connect_and_verify_sessions + send_message +
verify_session_list when MODEL_CAPABLE=true; falls back to the UI-only journey
(connect + verify_session_list) otherwise, logging the environmental reason.
- Raise --max-steps to 40 so multiple scenarios fit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Found via parallel screen audit; each confirmed in code:
- AuthGate: auto-prompt biometrics on lock (useEffect was imported but unused)
- CodeBlock: horizontal scroll for long code lines (were wrapped/mangled)
- DiffView: horizontal scroll instead of numberOfLines=1 truncation
- chat: biometric-cancel on send shows feedback instead of silently dropping msg
- chat: send failure restores input + attachments and alerts
- chat: removed dead /compact + /clear builtin commands (advertised, no-op)
- sessions: delete + rename failures alert instead of silent; rename guarded
against double-submit
- sessions: onRefresh spinner no longer hangs forever if a refresh rejects
- add/edit connection: validate URL has http(s):// scheme before save/test
typecheck clean, 65/65 unit tests pass. Runtime UI behavior still needs on-device
verification per the pre-posting test gate (HANDOFF §0b).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Google 'opencode android' surfaces a 40+ comment Reddit thread ('best way to
setup opencode on phone') and not us. New high-intent page answers it directly:
HowTo + FAQ + Breadcrumb JSON-LD, 4-step setup, tunnel table, Termux contrast.
Linked from landing footer + in-content, added to sitemap (priority 0.8).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- title.txt: 'OpenCode Mobile' -> 'OpenCode Mobile: AI Coding' (26/30 chars,
adds the 'AI coding' search keyword to the strongest Play ranking field)
- short_description: front-load 'AI coding agent for Android' (71/80)
- full_description: dead privacy URL opencode.vibebrowser.app (000) ->
live dzianisv.github.io/opencode-mobile/privacy/ (200) — was a Play
rejection risk
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Visual proof above the fold-ish: 3 UI previews with captions + descriptive
alt text. Responsive flex strip, lazy-loaded. Conversion lever for first-time
visitors — landing had QR codes but no app visuals.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 long-tail pages targeting 'opencode vs chatgpt/copilot', 'vs termux',
'claude code on android'. Added to sitemap, footer nav, in-content links
from landing + guide + troubleshooting. All have canonical + install CTAs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Targets "opencode mobile features", "AI coding android", and
"opencode vs ChatGPT for coding" queries not covered by existing pages.
Includes SoftwareApplication + BreadcrumbList structured data.
Adds /features/ to sitemap and internal footer nav on all 5 pages.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The upstream opencode project asks 'opencode-*' named projects to clarify they are
not built by or affiliated with the opencode team. Adds that note near the top.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Authentic troubleshooting content mapping the app's six connection-failure
diagnostics (malformed-url, no-internet, tls, health-failed, timeout,
server-unreachable) to concrete fixes. Targets real problem queries
('opencode mobile can't connect', 'opencode tailscale setup', 'network request
failed'). FAQPage + BreadcrumbList JSON-LD for rich results. Linked from landing,
guide, and download footers; added to sitemap.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
build.yml only built the APK — the 65 unit tests and typecheck never ran in CI,
so regressions in the covered logic (headers/SSE/diagnostics/settings/etc.) could
land silently. Add a fast 'test' job (Node 24, native TS test-running) so every
push and PR enforces typecheck + npm test before merge.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extract TOOL_STATUS + statusFromPart from stores/events.ts into a pure
status-labels.ts (type-only Part import, erased at runtime; events.ts delegates).
8 tests pin the live 'what is the agent doing' labels: reasoning/text/known-tool
mappings, shared labels (search/edit groups), unknown-tool degrade to
'Running <tool>...', and the no-tool/unknown-type fallthroughs. typecheck clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extract parseUrl + classify from diagnostics.ts into a pure diagnostics-classify.ts
(diagnostics.ts imports + re-exports the types; behavior unchanged) so the
connection-failure decision tree — the user-facing 'why did connecting fail'
guidance — is unit-testable without react-native/expo. 15 tests: URL parsing
(port defaults, IPv4-vs-hostname, path/query stripping, malformed rejection) and
every classify branch (malformed, ok, tls, no-internet, health-failed, timeout,
server-unreachable incl. the hostname-only MagicDNS hint). typecheck clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extract two pure helpers so they're testable without zustand/expo:
- settings-merge.ts: clampPageSize + forward-compatible mergeStoredSettings (the
upgrade path where stored data predates a new notification category must yield
the default, not undefined). stores/settings.ts now delegates.
- notify-format.ts: sanitizeBody (strip C0/DEL control chars, trim, cap at 200)
used for server-supplied notification text. stores/events.ts now imports it.
Behavior unchanged; typecheck clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extract the SSE chunk-buffering from sdk.ts into a pure SSEParser (sdk.ts delegates;
behavior unchanged) and pin the framing rules that are easy to break when an event
splits across network reads: partial trailing lines held until completed, frames
reassembled across 2-3 reads, [DONE] sentinel and empty/non-data lines filtered,
each frame emitted exactly once. typecheck clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extract the auth + directory-header encoding from sdk.ts into a pure, dep-free
headers.ts (sdk.ts now delegates — behavior unchanged) so the connection-critical
logic is unit-testable without expo/fetch. Add 8 header tests (ASCII passthrough,
non-ASCII/CJK percent-encoding stays header-safe, Basic auth, empty-dir handling)
and 8 logbuffer tests (serialization incl. circular-ref fallback, 200-entry ring
cap, copy semantics, formatting). typecheck clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Landing/guide/download descriptions were 257/246/217 chars and truncated in
Google results. Tightened to ~150 while keeping target keywords.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Targets 'download opencode android', 'opencode apk', 'opencode f-droid'.
Serves the live install channels (self-hosted F-Droid repo + signed APK)
with SoftwareApplication + BreadcrumbList structured data. Linked from
landing hero, landing footer, and guide footer.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Easier to maintain in this TS repo: typed gate definitions, native fetch/JSON
(drops the fragile jq/python3/grep branches), runs via `node check.ts` with no build/deps.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Set expo.autolinking.buildFromSource: [".*"] so F-Droid mainline's
scanner doesn't see the prebuilt .aar + -sources.jar files that Expo
ships inside every expo-* npm package's local-maven-repo/ directory.
When this flag is set, expo-modules-autolinking routes each module
through a Gradle source build instead of consuming the prebuilt AAR.
Verified: `./gradlew assembleRelease` succeeds (11 min, 92 MB APK)
with this flag and no prebuilt AARs consumed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Addresses linsui review: summary/description pulled from fastlane; honest, policy-compliant copy.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Privacy URL was inconsistent across owner docs (agentlabs.cc/opencode/privacy
in 3 spots vs opencode.vibebrowser.app/privacy in the majority). Reconciled
all to the canonical opencode.vibebrowser.app/privacy (Search-Console-verified
domain; what privacy-policy.html self-references). Also fixed stale v0.4.2 AAB
reference in PLAY-APP-CONTENT-ANSWERS.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
onCreateInDirectory navigated with dir.trim() (raw user input) while the
sibling create paths use the server-authoritative session.directory. When
the opencode server normalizes the path (trailing slash/symlink/relative),
the nav param diverged from the session's real scope, causing the new
session/[id] screen to scope to the wrong client -> 'Failed to load
session'. Mirror the sibling paths' session.directory spread.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>