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>
androguard 4.1.4 raises 'NoOverwriteDict object has no attribute append' in
parse_v2_v3_signature when fdroidserver extracts the signer cert — this broke the
self-hosted F-Droid publish from v0.4.2 on. 4.1.3 (which shipped v0.3.2–v0.4.1)
parses our re-signed v1+v2-only APK cleanly; verified locally with
fdroidserver.common.get_first_signer_certificate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The self-hosted F-Droid repo (https://dzianisv.github.io/opencode-mobile/fdroid/repo)
has been stuck at v0.4.1 because publish-fdroid crashed in androguard parsing the
CI APK's v2+v3 signature block pair ('NoOverwriteDict' object has no attribute
'append'). Force v1+v2-only signing: gradle flags for local builds, plus a
deterministic apksigner re-sign step in the workflow (expo prebuild regenerates
build.gradle, so the workflow step is the real guarantee). Bump to v0.4.3 /
versionCode 5 so a fresh tag re-runs the publish with the verified bug fixes
(#10 scope fixes, send-error fix) included.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
sendMessage captured currentSession at call time; if the user switched sessions
while a prompt was in flight and it failed, the catch handler surfaced the error
on and refetched the NOW-current session. Clear the sending flag for the session
we actually sent to, but only set error / refreshMessages when it's still on
screen. Found via runtime bug audit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
App ID is cc.agentlabs.opencode (post-rename), not ai.opencode.mobile; bump the
example release to v0.4.2. Verified against the live v0.4.2 APK: package
cc.agentlabs.opencode, versionName 0.4.2, signing SHA-256 0C:25:..:99 matches the
documented fingerprint. Makes the IzzyOnDroid submission filing-ready.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
createSession now stamps the scope directory onto the returned session, and the
create-session navigation passes that directory to the detail screen. Previously
a freshly created home-scoped session was opened/addressed with the default
(CWD) client because the route carried no directory param — the same scope
mismatch class as #10, on the open/send path instead of the list path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extract the list/create scope rule into sessionScopeDirectory() so loadSessions
and createSession can no longer drift apart — the root cause of #10 (sessions
empty after connect/create). Add a zero-dependency node:test regression guard
proving both paths resolve identically across all inputs, a 'test' npm script,
and exclude test files from the app typecheck.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
versionName in android/app/build.gradle was stale at 0.4.1 while app.json /
package.json declare 0.4.2, so CI-built APKs reported the wrong version string.
Align it. Also adds context.md (goal/design/plan/progress) and ignores the
generated index.android.bundle artifact.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Root cause of the empty-sessions-after-connect bug, caught by the now-live E2E
smoke: createSession() created via the plain connection client (server CWD),
while loadSessions() lists home-scoped when the connection has no explicit
directory. When CWD != home the new session was invisible to the list.
Fix: createSession now mirrors loadSessions' directory scoping (home-scoped
client when no explicit directory), so a freshly created session reliably
appears. No change for connections with an explicit directory.
Verified by the connect-and-verify-sessions CUA smoke on push.
Refs #10.
* ci(play): add track/status inputs to publish workflow
Lets the Play publish run target a public track (production/beta) and
choose draft vs completed, instead of being hard-wired to internal.
Defaults stay internal/completed so tag-push and release triggers are
unchanged. Enables promoting the app to a publicly-downloadable track —
the prerequisite for any real download growth.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(play): record user authorization for production go-live
* docs(fdroid): correct metadata to cc.agentlabs.opencode + agentlabs.cc, flag post-rename tag gate
The fdroiddata submission still referenced the old package ai.opencode.mobile
and v0.3.1. Update package id, website, and document the real blocker: F-Droid
mainline needs a release tag built AFTER the package rename (v0.4.1 APK is the
old id) plus Play production live and a reproducible build. Signing fingerprint
is unchanged across the rename.
* docs(launch): ready-to-fire distribution kit (Show HN, Reddit, PH, X, dev.to)
Copy-paste launch posts + ordered fire checklist so distribution starts the
moment the public listing is live. Store URLs left as {{PLAY_URL}}/{{FDROID_URL}}
placeholders; web hub agentlabs.cc/opencode is live now.
---------
Co-authored-by: engineer <engineer@opencode.ai>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
agentlabs.cc/opencode and /opencode/privacy are now live (200). Repoint
README, distribution listings (Play/App Store/F-Droid/IzzyOnDroid/iOS),
docs, and in-app privacy links (settings + telemetry consent) from
www.vibebrowser.app/opencode to the canonical agentlabs.cc hub.
Co-authored-by: engineer <engineer@opencode.ai>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>