The v0.4.15 release commit (f564869) bumped package.json and app.json
`expo.version` but left `android/app/build.gradle` at versionCode 41 /
versionName "0.4.14" and app.json `android.versionCode` at 41. Consequences,
all observed on tag v0.4.15:
- `npm run check:versions` fails, so the F-Droid publish (run 31820921979)
died at its first step and the `release` job in build.yml never ran — no
GitHub release exists for v0.4.15. That is the source the new in-app update
check polls, so the mechanism this release exists to ship had nothing to
find.
- versionCode 41 is v0.4.14's. F-Droid and every direct-APK install key
upgrades off versionCode, so even a successful publish would not have been
offered to the 0.4.10/0.4.14 cohort. Play was unaffected only because the
publish workflow overrides the code with run_number+100.
Fix is the missing half of the release bump: versionCode 42 / versionName
0.4.15, plus the changelog files named after the code (distribution/ for the
record, fastlane/ for F-Droid).
check-version-parity.mjs now also requires distribution/changelogs/<code>.txt
to exist and to describe the version being released, and the fastlane copy to
exist. A stale versionCode is otherwise internally consistent and silent;
verified it discriminates — code 41 with version 0.4.15 fails, 42 passes.
Tests: npm test 320 pass.
Co-authored-by: engineer <engineer@macbookpro.lan>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Carries the two AGE-110 mechanisms: the in-app update check (#179) and the
first release whose tag push publishes straight to Play production (#177).
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Ships 7c8bc7d (#169). Until this release, the filter exists only in main: every
installed build still uploads `connect timeout` / `connect server-unreachable`
and un-deduped retry loops, which is what makes opencode-mobile the org's #1
Sentry volume source (~4,500 events/month against a 3,500/month org quota).
User-visible change is deliberately small — quieter crash reporting, real
crashes unaffected — so the Play changelog says exactly that.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* chore(release): v0.4.13 (versionCode 40) — waitlist retry queue reaches users
Ships 2f81d34 (#165): failed waitlist signups are persisted on-device and
retried on app foreground instead of silently falling back to mailto.
Until this Play release, no user is running that fix.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix(waitlist): stamp the app version into the mailto escape hatch
AGE-100 asks for the post-release mailto count "split by app version where the
mail body allows it". It did not allow it: the body was "Sign me up!\n\nEmail: x"
and nothing else, so a mail from an unreachable pre-v0.4.8 sideload is byte-identical
to one from a current build whose retry queue leaked. Those two readings have
opposite meanings — the first is the known permanent cohort, the second is a defect.
Now the escape hatch appends "App: OpenCode Mobile v<version>" (app.json, same
source Sentry uses). Absence of the line == pre-v0.4.13 build. waitlist.ts stays
free of react-native/JSON imports; the screen injects the version.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
---------
Co-authored-by: engineer <engineer@macbookpro.lan>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Supersedes v0.4.9 (versionCode 36, on internal but lacking these) so a SECURE,
current build is available in the Play library to promote to production:
- #124 reconnect resync (stuck 'processing' after network drop)
- #125 HIGH: biometric app-lock re-locks on background (was bypassable after
first unlock); connection password edits now persist
plus everything in v0.4.9 (demo mode, first-run clarity, core + notification
fixes). Promote versionCode 37 to production.
Claude-Session: https://claude.ai/code/session_01T12AhSnQVrSxNnvwfCx2z6
Co-authored-by: engineer <engineer@macbookpro.lan>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Supersedes v0.4.8 (still on internal, not promoted) with everything merged
since: offline demo mode + first-run clarity (0.4.8), core session fixes
(#120: queued-message ghosting, selectSession race), and notification/
permission fixes (#121: permission never requested, wrong-session-on-back-nav,
misleading completion pushes, question double-reply). Production is on 0.4.5,
so promoting v0.4.9 gets users the full hardened app in one step.
Claude-Session: https://claude.ai/code/session_01T12AhSnQVrSxNnvwfCx2z6
Co-authored-by: engineer <engineer@macbookpro.lan>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Bundles the unreleased work sitting on main since v0.4.7:
- Offline demo mode (#108) — value without a server
- First-run clarity + fast-fail connect timeout (#107)
- Directory-browser stuck-state fix (#106)
Version + versionCode bumped and a user-facing changelog added so the owner
can cut the release (tag v0.4.8 / dispatch publish-play-store.yml). Does NOT
itself publish — releasing to production stays an owner action.
Claude-Session: https://claude.ai/code/session_01T12AhSnQVrSxNnvwfCx2z6
Co-authored-by: engineer <engineer@macbookpro.lan>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
- Add expo-localization, i18next, react-i18next (versions aligned with
Expo 54 / RN 0.81)
- src/lib/i18n/locale-resolve.ts: pure locale-resolution helpers
(system tag -> supported catalog, with en fallback), unit-tested via
node --test with no RN imports
- src/lib/i18n/config.ts: i18next init wired to expo-localization
device detection, en.json + zh-Hans.json catalogs
- Persist a locale preference (system | en | zh-Hans) in the settings
zustand store, applied immediately via i18next.changeLanguage
- Wire I18nextProvider in app/_layout.tsx
- Localize the Settings screen (~28 strings) as the reference pattern
for extracting user-facing strings, with a language picker row and
reviewed Simplified Chinese translations
Other screens (session/[id], connection/*, index, chat components)
are deferred follow-up — issue #68 stays open for that work.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Releases 0.4.3-0.4.7 uploaded zero source-map files to Sentry, leaving every
JS frame unsymbolicated (app:///index.android.bundle:1). Root-caused two
independent bugs:
1. No metro.config.js existed, so Metro never ran Sentry's debug-ID
injection. Without an embedded debug ID, sentry.gradle's upload task
falls back to matching source maps to events by release/dist string
alone (see has-sourcemap-debugid.js check in sentry.gradle) - and that
fallback was broken (see #2). Added metro.config.js wrapping Expo's
default config with getSentryExpoConfig from @sentry/react-native/metro,
the officially documented path for Expo + debug-ID symbolication.
The installed @sentry/react-native@6.14.0 could not actually bundle with
this enabled: its metro integration does a hard `require("metro/src/lib/
countLines")`, a deep path metro 0.83.x (bundled by Expo SDK 54) no
longer exposes via its package.json `exports` map, crashing every build.
Bumped to ~6.22.0 (package.json:18), which vendors countLines and adds
metro/private/* fallbacks for other deep metro imports. Verified via a
real `npx expo export:embed` run: bundle and source map now share a
matching `debugId`.
2. sentry.gradle's default release/dist for the upload is
`${applicationId}@${versionName}+${versionCode}` (computed from
android/app/build.gradle), which never matched what Sentry.init() reports
at runtime (`opencode-mobile@${app.json version}`, src/lib/sentry.ts:33-34).
Every source map was therefore filed under a release Sentry never
queries. Added a "Set Sentry release identifiers" step to build.yml,
publish-play-store.yml, and publish-fdroid.yml that exports
SENTRY_RELEASE/SENTRY_DIST from app.json's version before the Gradle
build step, forcing an exact match.
Also filled in organization/project on the `@sentry/react-native/expo`
plugin in app.json (previously a bare string, which only warned "Missing
config for organization, project" and relied on env-var fallback) so
android/sentry.properties is generated deterministically instead of by
accident/history.
Verified locally (no push - GitHub is down, consolidating to local main):
- npx expo export:embed (real Metro bundle) succeeds and embeds a matching
debugId in both index.android.bundle and its .map
- npm run typecheck: clean
- npm test: 81/81 passing
- Full ./gradlew Android build not verified: this machine has no
ANDROID_HOME/SDK and a JDK/Gradle-wrapper version mismatch unrelated to
this change; CI's Java 17 + Android SDK toolchain is unaffected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NJKAQ6HAikWGQK7PGZ5Y4E
Installs are up 615% but 7-day retention is ~0% and we had no analytics SDK
to see where users drop off. Adds a thin PostHog wrapper (src/lib/analytics.ts)
that tracks app_opened, connection_form_submitted, connection_attempted,
connection_succeeded/failed (with a coarse error_class, e.g. the known 401
auth bug), message_sent, and response_received.
PostHog was chosen over Aptabase for its GMS-free JS-only RN SDK (fine for
the F-Droid/no-Firebase build), EU-hosted/self-host option, and generous
free tier. Analytics shares the exact same consent flag as Sentry
(telemetry.ts now gates both) so zero network calls happen without explicit
opt-in.
Requires a new EXPO_PUBLIC_POSTHOG_KEY CI secret (wired into build.yml,
publish-fdroid.yml, publish-play-store.yml, and documented in
publish-app-store.yml alongside the existing Sentry secrets).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NJKAQ6HAikWGQK7PGZ5Y4E
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/).
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>
First tagged release after the ai.opencode.mobile -> cc.agentlabs.opencode
rename. Produces a signed cc.agentlabs.opencode APK on the GitHub release and
refreshes the self-hosted F-Droid repo to the current package — prerequisite
for the F-Droid mainline / IzzyOnDroid submissions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The earlier rename commit did not persist the package-identity edits for
app.json, build.gradle, fastlane, the publish workflow, and the Kotlin
package declarations (they were reverted in the working tree after staging).
HEAD therefore still built ai.opencode.mobile. This commits the real
cc.agentlabs.opencode identity so CI builds the rebranded package.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(sessions): use active connection client directly, remove roots filter
Root cause A: loadSessions was calling clientForDirectory(serverHome) which
scoped the session list to /home/azureuser — a different project than the
server's active CWD. Sessions in the current project (e.g. opencode-mobile)
were never returned.
Root cause B: roots:true filtered out sessions that have a parentID (sub-task /
AUTO-REVIEW sessions), hiding valid sessions from the list.
Fix: use connState.client directly (the connection's active directory) and drop
the roots filter so all sessions for that project are visible.
Also adds a verify_session_list CUA smoke scenario that navigates back to the
sessions tab after creating a session and asserts the list is non-empty —
covering the regression path that was previously untested.
* fix(sessions): fetch serverHome in addConnection so loadSessions shows correct sessions
Root cause: addConnection() built the HTTP client but never fetched serverHome
(only loadConnections and setActiveConnection did). When the user adds a new
connection (fresh install / first sign-in), serverHome = null, so loadSessions
fell through to connState.client (the server's CWD). On this dev server the CWD
is the deploy directory — 11 old May-19 sessions that are not the user's recent
work sessions.
Fix: addConnection now fetches currentProject + serverHome via the same
Promise.all as setActiveConnection, before calling set(). This ensures
loadSessions immediately uses clientForDirectory(serverHome) → the global
project → the user's actual recent parent sessions.
Also adds --opencode-url flag to the CUA smoke script, which appends a
connect_and_verify_sessions scenario that reproduces the regression:
python scripts/android-cua-smoke.py --opencode-url http://100.108.64.76:4096
* fix(sessions): recover home scope after fresh connect
Resolve stale deploy-only session list by recovering server home during first load and keeping regression coverage in default Android CUA smoke and CI.
* chore(release): bump version to 0.4.0
- build.yml: use production keystore (KEYSTORE_BASE64) on tag pushes,
fall back to debug key for PRs/branch builds — build.gradle already
reads RELEASE_STORE_FILE env var so no Gradle changes needed
- distribution/fdroid-submission/metadata.yml: filled
AllowedAPKSigningKeys with actual SHA-256 fingerprint, commit tag
updated to v0.3.1, version bumped to 0.3.1
- app.json: bump version 0.2.3 → 0.3.1, versionCode 1 → 2
- Add eas.json + EAS README for iOS App Store builds
- Add fastlane/metadata/android for Play Store / F-Droid graphics
- Add distribution docs: applestore, fdroid, market, playstore,
security, threat-model, opencode-site-deploy
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(security): fail closed on biometric init error
H-03: setting isAuthenticated: true on initialization failure was a
security bypass — any crash during biometric setup granted full access.
Fail closed instead; user sees auth prompt on next open.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(security): use Crypto.randomUUID for connection IDs
H-04: Math.random() is not cryptographically random. Connection IDs are
used as SecureStore key suffixes; switch to expo-crypto randomUUID for
a secure source.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(deps): pin expo-crypto to ~15.0.9
15.0.10 does not exist on npm; ~15.0.9 is the latest stable in the 15.x series compatible with Expo SDK 54.
* feat: add OpenCode Connect coming-soon waitlist card
Adds a discoverable 'OpenCode Connect — Coming Soon' card to the
add-connection quick-connect screen. Users can enter their email and
tap 'Join Waitlist' to send a pre-filled mailto. No backend required.
* fix(cua): detect actual screen dimensions and fix JSON parsing
- Get real screen size via `wm size` instead of hardcoding 1080x2400;
emulator is 1080x1920 so y-coordinates were systematically off
- Extract first JSON object via regex when model returns multiple objects
- Use AZURE_OPENAI_MODEL env var for deployment name (defaults gpt-5.4)
- Add AZURE_DEV_AI_* path for Azure AI Foundry endpoints
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(security): SHA-pin upload-google-play and sanitize notification bodies
M-02: Pin r0adkll/upload-google-play to commit SHA e738b9d (v1.1.5)
to prevent supply-chain hijack via tag mutation.
M-03: Sanitize all push notification bodies — strip control chars,
truncate to 200 chars. Prevents server-supplied strings (error messages,
file paths from permission patterns, session titles) from leaking
unbounded text into the OS notification drawer.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(privacy): add telemetry consent gate for Sentry crash reporting
Sentry was always-on, violating F-Droid anti-feature policy and user
trust norms. Now gated behind explicit opt-in:
- First-launch consent modal (TelemetryConsentModal) shows once on
fresh install; user can Allow or Decline.
- Consent state persisted in expo-secure-store (survives restarts).
- Settings > Privacy section: crash reporting toggle + privacy policy link.
- initSentry() called only after consent granted — not on app start.
Closes#3 (partial)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(config): add real icons and complete iOS/Android app.json config
- Add 1024×1024 app icon, 432×432 adaptive icon foreground, 200×200 splash
- iOS: push notification entitlement (aps-environment: production), speech/
microphone/camera/photo usage descriptions for future features, disable
ITSAppUsesNonExemptEncryption
- Android: adaptive icon with dark background (#0F172A), versionCode: 1
- expo-notifications plugin wired in app.json
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(dist): add iOS CI workflow, README rewrite, CONTRIBUTING, and LICENSE
- publish-app-store.yml: EAS Build + TestFlight submission; runs on tag/release/
workflow_dispatch; bumps ios.buildNumber from github.run_number
- README: full rewrite — features, install badges, connection guide, contributing
- CONTRIBUTING.md: contribution guide for OSS contributors
- LICENSE: MIT
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(dist): add store listings, strategy, privacy policy, F-Droid/IzzyOnDroid templates
- distribution/strategy.md: monetization strategy (free client + opencode Cloud)
- distribution/play-listing.md: Google Play store copy (name, description, tags)
- distribution/app-store-listing.md: App Store listing copy
- distribution/privacy-policy.{md,html}: GDPR-compliant privacy policy
- distribution/PLAY_CONSOLE_SETUP.md: Play Console setup runbook
- distribution/ios-enrollment-runbook.md: Apple Developer Program enrollment steps
- distribution/SIGNING-KEY-FINGERPRINTS.md: keystore fingerprint for reproducible builds
- distribution/fdroid-submission/: F-Droid metadata template
- distribution/izzyondroid-submission/: IzzyOnDroid submission template
- distribution/whatsnew/: Play Store release notes (en-US)
- distribution/whatsnew-ios/: TestFlight release notes
- distribution/play-graphics/: Play Store screenshot placeholders
- distribution/app-store-graphics/: App Store screenshot placeholders
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(telemetry): handle SecureStore failure + Android back button
- add .catch() on loadTelemetryConsent() so SecureStore rejection
shows the consent modal instead of blocking startup forever
- add onRequestClose={onDecline} to Modal so Android back button
records the decline rather than silently dismissing
- fix catch block in telemetry.ts to not clobber _resolved when
SecureStore read fails mid-session
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(ci): run gradlew clean to prevent stale modules.json duplicate
Sentry Gradle plugin writes modules.json to src/main/assets; cached
build intermediates contain an old copy → mergeReleaseAssets fails
with 'Duplicate resources'. Running clean before assembleRelease
clears the intermediate state.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(ci): remove android build output cache causing duplicate modules.json
Caching android/app/build/intermediates and android/app/.cxx causes
two issues:
1. Stale modules.json in intermediates → Duplicate resources error
2. .cxx CMake artifacts reference absolute paths → ninja clean fails
Keeping only Gradle distribution cache (~/.gradle) which is safe.
Expo prebuild regenerates android sources fresh each run anyway.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(diagnostics): active connect-failure triage + Sentry + in-app share
Replaces the opaque "Connection Failed" / "Network request failed" dead-end
with on-device diagnostics that classify *why* a connect attempt failed.
On failure (quick connect and edit-connection test), the app now:
- runs parallel probes: target /global/health, target root, and a public
204 endpoint (internet reachability check)
- classifies the cause: malformed-url, no-internet, server-unreachable,
health-failed, tls-error, timeout
- shows a plain-English summary + a "Share report" button that copies a
full report (target URL, per-probe results w/ error.cause, device/app
info, recent log ring-buffer) to the clipboard and opens the share sheet
- captures the same structured context to Sentry (auto-upload), gated on
EXPO_PUBLIC_SENTRY_DSN so dev/CI builds work without secrets
New: src/lib/logbuffer.ts (ring buffer + logger), src/lib/diagnostics.ts
(regex URL parse — Hermes URL is incomplete — probe + report + share),
src/lib/sentry.ts (no-op-without-DSN wrapper, scrubs basic-auth from URLs).
Wired Sentry.wrap around RootLayout and initSentry() at module load.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci(sentry): wire Sentry DSN + source-map upload env into build; bump to 0.2.2
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>