feat(privacy+dist): telemetry consent gate + app store distribution prep (#4)
* 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>
11
.github/workflows/build.yml
vendored
@@ -42,17 +42,6 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gradle-
|
||||
|
||||
- name: Cache Android build outputs
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
android/app/build/intermediates
|
||||
android/build
|
||||
android/app/.cxx
|
||||
key: ${{ runner.os }}-android-build-${{ hashFiles('package-lock.json', 'android/**/*.gradle*') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-android-build-
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install --legacy-peer-deps
|
||||
|
||||
|
||||
174
.github/workflows/publish-app-store.yml
vendored
Normal file
@@ -0,0 +1,174 @@
|
||||
# STATUS: Validated structure — awaiting Apple Developer Program enrollment approval.
|
||||
# Once enrollment is approved, complete these steps and this workflow is production-ready:
|
||||
#
|
||||
# REMAINING GAPS (must complete before first run):
|
||||
# 1. Update eas.json: replace REPLACE_WITH_APP_STORE_CONNECT_APP_ID and REPLACE_WITH_APPLE_TEAM_ID
|
||||
# (see eas.json.README.md for exact click paths in App Store Connect)
|
||||
# 2. Add GitHub secrets (Settings > Secrets and variables > Actions):
|
||||
# EAS_TOKEN Expo access token (expo.dev > Account > Access Tokens)
|
||||
# APPLE_APP_STORE_CONNECT_API_KEY_ID Key ID from App Store Connect > Users & Access > Integrations > App Store Connect API
|
||||
# APPLE_APP_STORE_CONNECT_ISSUER_ID Issuer ID from same page
|
||||
# APPLE_APP_STORE_CONNECT_API_KEY base64-encoded .p8 file (download at key creation — one time only)
|
||||
# 3. Run `eas login` locally and `eas build:configure` on first run to let EAS set up signing
|
||||
# 4. Manually upload first build to App Store Connect (required once to create the app record)
|
||||
#
|
||||
# OPTIONAL secrets (crash reporting):
|
||||
# EXPO_PUBLIC_SENTRY_DSN SENTRY_AUTH_TOKEN SENTRY_ORG SENTRY_PROJECT
|
||||
#
|
||||
# Build strategy: EAS Build (Expo Application Services)
|
||||
# - No Mac runner needed; Expo hosts macOS workers with managed certificates.
|
||||
# - Cost: free tier (30 builds/month); upgrade to $19/month for unlimited/priority queue.
|
||||
# - See distribution/ios-enrollment-runbook.md for full enrollment steps.
|
||||
# - See eas.json.README.md for placeholder fill-in instructions.
|
||||
# - Alternative (self-hosted Mac runner): see commented section at bottom of this file.
|
||||
|
||||
name: Publish to App Store (TestFlight)
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
push:
|
||||
tags: ["v*"]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
publish-ios:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
EXPO_PUBLIC_SENTRY_DSN: ${{ secrets.EXPO_PUBLIC_SENTRY_DSN }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
|
||||
# Install EAS CLI globally. Pin to a recent stable version.
|
||||
- name: Install EAS CLI
|
||||
run: npm install -g eas-cli@13
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install --legacy-peer-deps
|
||||
|
||||
# Bump ios.buildNumber to match github.run_number (monotonically increasing).
|
||||
# App Store Connect rejects duplicate build numbers for the same version string.
|
||||
- name: Bump ios.buildNumber in app.json
|
||||
run: |
|
||||
node -e "
|
||||
const f = 'app.json';
|
||||
const j = require('./' + f);
|
||||
j.expo.ios = j.expo.ios || {};
|
||||
j.expo.ios.buildNumber = String(${{ github.run_number }});
|
||||
require('fs').writeFileSync(f, JSON.stringify(j, null, 2) + '\n');
|
||||
"
|
||||
echo "buildNumber now: $(node -p "require('./app.json').expo.ios.buildNumber")"
|
||||
|
||||
# EAS Build: builds the IPA in Expo's cloud (macOS workers managed by Expo).
|
||||
# --non-interactive: no prompts, suitable for CI.
|
||||
# --platform ios: iOS only (Android is handled by publish-play-store.yml).
|
||||
# --profile production: uses the "production" profile in eas.json (created below if missing).
|
||||
- name: Build IPA via EAS
|
||||
env:
|
||||
EXPO_TOKEN: ${{ secrets.EAS_TOKEN }}
|
||||
APPLE_APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APPLE_APP_STORE_CONNECT_API_KEY_ID }}
|
||||
APPLE_APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APPLE_APP_STORE_CONNECT_ISSUER_ID }}
|
||||
APPLE_APP_STORE_CONNECT_API_KEY: ${{ secrets.APPLE_APP_STORE_CONNECT_API_KEY }}
|
||||
run: |
|
||||
eas build \
|
||||
--platform ios \
|
||||
--profile production \
|
||||
--non-interactive \
|
||||
--no-wait \
|
||||
--json \
|
||||
| tee eas-build-output.json
|
||||
BUILD_ID=$(cat eas-build-output.json | node -e "const d=require('fs').readFileSync('/dev/stdin','utf8');console.log(JSON.parse(d).id)")
|
||||
echo "EAS_BUILD_ID=$BUILD_ID" >> $GITHUB_ENV
|
||||
echo "Build ID: $BUILD_ID"
|
||||
|
||||
# Wait for the EAS build to complete (iOS builds typically take 15–25 minutes).
|
||||
- name: Wait for EAS build
|
||||
env:
|
||||
EXPO_TOKEN: ${{ secrets.EAS_TOKEN }}
|
||||
run: |
|
||||
echo "Waiting for build $EAS_BUILD_ID to complete..."
|
||||
eas build:view "$EAS_BUILD_ID" --json --wait
|
||||
echo "Build complete."
|
||||
|
||||
# Submit to TestFlight via EAS Submit. Uses the same App Store Connect API key.
|
||||
# --latest: picks the most recent finished build for this app + platform.
|
||||
- name: Submit to TestFlight via EAS Submit
|
||||
env:
|
||||
EXPO_TOKEN: ${{ secrets.EAS_TOKEN }}
|
||||
APPLE_APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APPLE_APP_STORE_CONNECT_API_KEY_ID }}
|
||||
APPLE_APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APPLE_APP_STORE_CONNECT_ISSUER_ID }}
|
||||
APPLE_APP_STORE_CONNECT_API_KEY: ${{ secrets.APPLE_APP_STORE_CONNECT_API_KEY }}
|
||||
run: |
|
||||
eas submit \
|
||||
--platform ios \
|
||||
--id "$EAS_BUILD_ID" \
|
||||
--non-interactive
|
||||
|
||||
# Upload release notes to TestFlight (what's new text for testers).
|
||||
# NOTE: EAS Submit does not yet support whatsNew natively; use fastlane pilot
|
||||
# or App Store Connect API directly if per-build release notes are needed.
|
||||
- name: Upload TestFlight release notes (informational)
|
||||
run: |
|
||||
echo "TestFlight release notes for this build:"
|
||||
cat distribution/whatsnew-ios/release-notes-en-US.txt
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ALTERNATIVE: Self-hosted Mac runner (macbook13-pro at 100.68.120.26)
|
||||
# ---------------------------------------------------------------------------
|
||||
# To use the Mac mini instead of EAS Build:
|
||||
# 1. SSH to macbook13-pro and set up GitHub self-hosted runner:
|
||||
# https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/adding-self-hosted-runners
|
||||
# 2. Change "runs-on: ubuntu-latest" above to "runs-on: self-hosted"
|
||||
# and add label "macos" for clarity.
|
||||
# 3. Replace the EAS Build + Submit steps with:
|
||||
#
|
||||
# - name: Install CocoaPods
|
||||
# run: sudo gem install cocoapods
|
||||
#
|
||||
# - name: Expo prebuild (iOS)
|
||||
# run: npx expo prebuild --platform ios --no-install
|
||||
#
|
||||
# - name: Install CocoaPods dependencies
|
||||
# working-directory: ios
|
||||
# run: pod install
|
||||
#
|
||||
# - name: Build IPA
|
||||
# run: |
|
||||
# xcodebuild -workspace ios/opencodemobile.xcworkspace \
|
||||
# -scheme opencodemobile \
|
||||
# -sdk iphoneos \
|
||||
# -configuration Release \
|
||||
# -archivePath $RUNNER_TEMP/opencodemobile.xcarchive \
|
||||
# archive \
|
||||
# CODE_SIGN_STYLE=Manual \
|
||||
# DEVELOPMENT_TEAM=${{ secrets.APPLE_TEAM_ID }} \
|
||||
# CODE_SIGN_IDENTITY="Apple Distribution" \
|
||||
# PROVISIONING_PROFILE_SPECIFIER="${{ secrets.IOS_PROVISIONING_PROFILE_NAME }}"
|
||||
#
|
||||
# - name: Export IPA
|
||||
# run: |
|
||||
# xcodebuild -exportArchive \
|
||||
# -archivePath $RUNNER_TEMP/opencodemobile.xcarchive \
|
||||
# -exportOptionsPlist ios/ExportOptions.plist \
|
||||
# -exportPath $RUNNER_TEMP/export
|
||||
#
|
||||
# - name: Upload to TestFlight (xcrun altool / notarytool)
|
||||
# run: |
|
||||
# xcrun altool --upload-app \
|
||||
# -f "$RUNNER_TEMP/export/opencodemobile.ipa" \
|
||||
# --type ios \
|
||||
# --apiKey "${{ secrets.APPLE_APP_STORE_CONNECT_API_KEY_ID }}" \
|
||||
# --apiIssuer "${{ secrets.APPLE_APP_STORE_CONNECT_ISSUER_ID }}"
|
||||
#
|
||||
# Self-hosted runner cost: $0 compute (your hardware), but requires maintaining
|
||||
# a macOS machine with Xcode, certificates, and provisioning profiles.
|
||||
# EAS Build is strongly recommended for the first release.
|
||||
10
.github/workflows/publish-play-store.yml
vendored
@@ -56,6 +56,13 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: npm install --legacy-peer-deps
|
||||
|
||||
- name: Bump android.versionCode in app.json
|
||||
# Play Store rejects duplicate versionCodes, so derive a monotonic value
|
||||
# from github.run_number. expo prebuild reads this into build.gradle.
|
||||
run: |
|
||||
node -e "const f='app.json';const j=require('./'+f);j.expo.android=j.expo.android||{};j.expo.android.versionCode=${{ github.run_number }};require('fs').writeFileSync(f,JSON.stringify(j,null,2)+'\n')"
|
||||
echo "versionCode now: $(node -p "require('./app.json').expo.android.versionCode")"
|
||||
|
||||
- name: Expo prebuild
|
||||
run: npx expo prebuild --platform android --no-install
|
||||
|
||||
@@ -78,10 +85,11 @@ jobs:
|
||||
path: android/app/build/outputs/bundle/release/app-release.aab
|
||||
|
||||
- name: Publish to Play Store
|
||||
uses: r0adkll/upload-google-play@v1
|
||||
uses: r0adkll/upload-google-play@e738b9dd8f2476ea806d921b64aacd24f34515a5 # v1.1.5
|
||||
with:
|
||||
serviceAccountJsonPlainText: ${{ secrets.PLAY_STORE_SERVICE_ACCOUNT_JSON }}
|
||||
packageName: ai.opencode.mobile
|
||||
releaseFiles: android/app/build/outputs/bundle/release/app-release.aab
|
||||
track: internal
|
||||
status: completed
|
||||
whatsNewDirectory: distribution/whatsnew
|
||||
|
||||
20
.tasks/3/STATE.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# Task 3 — STATE
|
||||
- phase: 5-impl
|
||||
- issue: #3
|
||||
- started: 2026-05-25T21:00:00Z
|
||||
- supervisor: claude-sonnet-4-6
|
||||
- branch: own/3-telemetry-consent-gate
|
||||
|
||||
## What was done before (prior session)
|
||||
- Telemetry consent gate fully implemented:
|
||||
- src/lib/telemetry.ts — ConsentState persistence via expo-secure-store
|
||||
- src/components/TelemetryConsentModal.tsx — first-launch UI
|
||||
- app/_layout.tsx — Sentry init gated on consent, consent modal integration
|
||||
- app/(tabs)/settings.tsx — Privacy section with crash reporting toggle
|
||||
- App.json updated: real icons, iOS push entitlements, Android adaptive icon
|
||||
- Distribution docs written: strategy.md, play-listing.md, app-store-listing.md, privacy-policy*, ios-enrollment-runbook.md
|
||||
- iOS CI workflow: .github/workflows/publish-app-store.yml
|
||||
- TypeScript: no errors
|
||||
|
||||
## Current phase
|
||||
Committing and pushing for PR
|
||||
167
CONTRIBUTING.md
Normal file
@@ -0,0 +1,167 @@
|
||||
# Contributing to OpenCode Mobile
|
||||
|
||||
Thank you for your interest in contributing. This document covers how to set up the dev environment, run the app, connect to a local opencode server for testing, code style, and the contribution process.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Dev environment setup](#dev-environment-setup)
|
||||
2. [Running on an emulator or device](#running-on-an-emulator-or-device)
|
||||
3. [Connecting to a local opencode server](#connecting-to-a-local-opencode-server)
|
||||
4. [Code style and linting](#code-style-and-linting)
|
||||
5. [Filing a bug](#filing-a-bug)
|
||||
6. [Proposing a feature](#proposing-a-feature)
|
||||
7. [Submitting a pull request](#submitting-a-pull-request)
|
||||
8. [Developer Certificate of Origin](#developer-certificate-of-origin)
|
||||
9. [Code of Conduct](#code-of-conduct)
|
||||
|
||||
---
|
||||
|
||||
## Dev environment setup
|
||||
|
||||
**Prerequisites**
|
||||
|
||||
- Node.js 18 or 20 (check with `node --version`)
|
||||
- npm 9+ or bun (bun is faster — install via `npm install -g bun`)
|
||||
- Expo CLI: `npm install -g expo-cli` (optional — `npx expo` works too)
|
||||
- For Android: Android Studio + an AVD or a physical device with USB debugging
|
||||
- For iOS: macOS + Xcode 15+
|
||||
|
||||
**Clone and install**
|
||||
|
||||
```bash
|
||||
git clone https://github.com/dzianisv/opencode-mobile.git
|
||||
cd opencode-mobile
|
||||
npm install # or: bun install
|
||||
```
|
||||
|
||||
**Environment variables**
|
||||
|
||||
Copy the example env file and fill in optional values (Sentry DSN is only needed for crash testing):
|
||||
|
||||
```bash
|
||||
cp .env.example .env # if the example doesn't exist yet, skip this
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Running on an emulator or device
|
||||
|
||||
**Android**
|
||||
|
||||
```bash
|
||||
# Start an Android emulator from Android Studio first, then:
|
||||
npx expo start --android
|
||||
|
||||
# Or run the dev build directly:
|
||||
npm run android
|
||||
```
|
||||
|
||||
**iOS (macOS only)**
|
||||
|
||||
```bash
|
||||
npx expo start --ios
|
||||
|
||||
# Or:
|
||||
npm run ios
|
||||
```
|
||||
|
||||
**Expo Go (fastest for quick experiments)**
|
||||
|
||||
```bash
|
||||
npx expo start
|
||||
# Scan the QR code with Expo Go on your phone
|
||||
```
|
||||
|
||||
> Note: Some native modules (biometric auth, secure store) require a development build and will not work in Expo Go. Use `npx expo run:android` / `npx expo run:ios` for a full dev build.
|
||||
|
||||
---
|
||||
|
||||
## Connecting to a local opencode server
|
||||
|
||||
You need a running [opencode](https://github.com/sst/opencode) server to test the app end-to-end.
|
||||
|
||||
```bash
|
||||
# Install opencode
|
||||
npm install -g opencode
|
||||
|
||||
# Start it in server mode on all interfaces
|
||||
OPENCODE_SERVER_PASSWORD=devpassword opencode serve --hostname 0.0.0.0 --port 4096
|
||||
```
|
||||
|
||||
In the app, add a connection:
|
||||
- **Emulator on same machine**: `http://10.0.2.2:4096` (Android AVD) or `http://localhost:4096` (iOS Simulator)
|
||||
- **Physical device on same LAN**: `http://<your-machine-LAN-IP>:4096`
|
||||
- **Tunnel for remote testing**: run `npx cloudflared tunnel --url http://localhost:4096` and use the provided HTTPS URL
|
||||
|
||||
---
|
||||
|
||||
## Code style and linting
|
||||
|
||||
The project uses ESLint and TypeScript strict mode. Run before committing:
|
||||
|
||||
```bash
|
||||
npm run lint # ESLint
|
||||
npm run typecheck # TypeScript type check (tsc --noEmit)
|
||||
```
|
||||
|
||||
There is no auto-formatter enforced by CI yet (Prettier is configured but optional). Keeping existing style consistent is more important than personal preference.
|
||||
|
||||
Key conventions:
|
||||
- Components: functional, with typed props via TypeScript `interface`
|
||||
- State: Zustand stores in `src/stores/`
|
||||
- API calls: through the SDK client in `src/lib/`
|
||||
- Screens: file-based routing via Expo Router under `app/`
|
||||
|
||||
---
|
||||
|
||||
## Filing a bug
|
||||
|
||||
Use the [Bug Report template](https://github.com/dzianisv/opencode-mobile/issues/new?template=bug_report.md).
|
||||
|
||||
Please include:
|
||||
- App version (visible in Settings screen)
|
||||
- opencode server version (`opencode --version`)
|
||||
- OS and version (e.g. Android 14, iOS 17.4)
|
||||
- Steps to reproduce
|
||||
- What you expected vs. what happened
|
||||
- Crash logs or screenshots if available
|
||||
|
||||
**Security vulnerabilities**: do NOT file public issues. See [SECURITY.md](SECURITY.md).
|
||||
|
||||
---
|
||||
|
||||
## Proposing a feature
|
||||
|
||||
Use the [Feature Request template](https://github.com/dzianisv/opencode-mobile/issues/new?template=feature_request.md) or start a [GitHub Discussion](https://github.com/dzianisv/opencode-mobile/discussions) if you want to explore the idea before opening a formal issue.
|
||||
|
||||
---
|
||||
|
||||
## Submitting a pull request
|
||||
|
||||
1. Fork the repo and create a branch off `main`: `git checkout -b feat/my-feature`
|
||||
2. Make your changes. Keep commits focused — one logical change per commit.
|
||||
3. Ensure `npm run lint` and `npm run typecheck` pass.
|
||||
4. If you changed UI, include a screenshot in the PR description.
|
||||
5. Open the PR against `dzianisv/opencode-mobile main`. Fill in the PR template.
|
||||
6. A maintainer will review within a few business days.
|
||||
|
||||
PRs that add new npm dependencies will receive extra scrutiny — keep the bundle lean.
|
||||
|
||||
---
|
||||
|
||||
## Developer Certificate of Origin
|
||||
|
||||
This project does not require a formal Contributor License Agreement (CLA). By submitting a pull request you confirm that:
|
||||
|
||||
- You wrote the contribution yourself, or have the right to submit it under the MIT License.
|
||||
- You grant VIBE TECHNOLOGIES, LLC and the project's users a perpetual, worldwide, royalty-free license under the MIT License terms.
|
||||
|
||||
That's it — no paperwork.
|
||||
|
||||
---
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
This project follows the [Contributor Covenant Code of Conduct](CODE_OF_CONDUCT.md). Please treat everyone with respect. Report issues to [support@vibebrowser.app](mailto:support@vibebrowser.app).
|
||||
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 VIBE TECHNOLOGIES, LLC
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
215
README.md
@@ -1,88 +1,183 @@
|
||||
# OpenCode Mobile
|
||||
|
||||
A React Native / Expo app for connecting to OpenCode servers from your phone.
|
||||
**The open-source mobile client for the [opencode](https://github.com/sst/opencode) AI coding agent.**
|
||||
AI-assisted coding from your phone — iOS, Android, and F-Droid.
|
||||
|
||||
[](LICENSE)
|
||||
[](https://play.google.com/store/apps/details?id=ai.opencode.mobile)
|
||||
[](https://apps.apple.com/app/opencode-mobile/id0000000000)
|
||||
[](https://f-droid.org/packages/ai.opencode.mobile)
|
||||
[](https://apt.izzysoft.de/fdroid/index/apk/ai.opencode.mobile)
|
||||
|
||||
---
|
||||
|
||||
OpenCode Mobile is a React Native / Expo app that brings the power of the [opencode](https://github.com/sst/opencode) AI coding agent to your phone. Connect to your own self-hosted opencode server over your local network, a Cloudflare Tunnel, ngrok, Tailscale, or the upcoming opencode Cloud — and write, review, and ship code from anywhere. The mobile client is **free and open-source** under the MIT license. There is no feature gate, no telemetry you did not opt into, and no ad network.
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<img src="distribution/play-graphics/phone-01.png" width="220" alt="Session list screen" />
|
||||
<img src="distribution/play-graphics/phone-02.png" width="220" alt="Chat screen with diff viewer" />
|
||||
<img src="distribution/play-graphics/phone-03.png" width="220" alt="Connection setup wizard" />
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- **Multiple Connection Types**: Connect via local network, tunnels (Cloudflare/ngrok), or cloud-hosted instances
|
||||
- **Secure Authentication**: Biometric authentication (Face ID/Touch ID) support
|
||||
- **Session Management**: View, create, and manage coding sessions
|
||||
- **Real-time Chat**: Stream responses from your AI assistant
|
||||
- **File Diff Viewer**: See what changes were made to your code
|
||||
- **Multi-connection** — manage multiple opencode servers (local network, Cloudflare Tunnel, ngrok, Tailscale, or opencode Cloud)
|
||||
- **Biometric unlock** — Face ID, Touch ID, or Android fingerprint protects the app and individual message sends
|
||||
- **Streaming chat** — token-by-token streaming responses directly from your opencode server
|
||||
- **Diff viewer** — inline side-by-side diffs of every file change the agent makes
|
||||
- **Tool call approval** — review and approve (or reject) tool calls before the agent executes them
|
||||
- **Secure credential storage** — server credentials stored in iOS Keychain / Android Keystore via `expo-secure-store`
|
||||
- **Session management** — browse, create, and resume coding sessions
|
||||
|
||||
## Getting Started
|
||||
---
|
||||
|
||||
### Prerequisites
|
||||
## Get OpenCode Mobile
|
||||
|
||||
- Node.js 18+
|
||||
- Bun (recommended) or npm
|
||||
- Expo Go app on your phone (for development)
|
||||
| Platform | Link |
|
||||
|---|---|
|
||||
| Google Play | [play.google.com — ai.opencode.mobile](https://play.google.com/store/apps/details?id=ai.opencode.mobile) |
|
||||
| Apple App Store | [apps.apple.com — OpenCode Mobile](https://apps.apple.com/app/opencode-mobile/id0000000000) |
|
||||
| F-Droid | [f-droid.org/packages/ai.opencode.mobile](https://f-droid.org/packages/ai.opencode.mobile) |
|
||||
| IzzyOnDroid | [apt.izzysoft.de — ai.opencode.mobile](https://apt.izzysoft.de/fdroid/index/apk/ai.opencode.mobile) |
|
||||
|
||||
### Installation
|
||||
> **Note**: App Store and F-Droid listings are pending final review. Play Store internal testing is live.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
**Step 1 — Start opencode on your machine**
|
||||
|
||||
```bash
|
||||
# From the monorepo root
|
||||
cd packages/mobile
|
||||
bun install
|
||||
# Install opencode (if you haven't already)
|
||||
npm install -g opencode
|
||||
|
||||
# Start the development server
|
||||
bun start
|
||||
```
|
||||
|
||||
### Connecting to OpenCode
|
||||
|
||||
1. Start OpenCode in server mode on your machine:
|
||||
|
||||
```bash
|
||||
# Run opencode in server mode
|
||||
OPENCODE_SERVER_PASSWORD=yourpassword opencode serve --hostname 0.0.0.0 --port 4096
|
||||
```
|
||||
|
||||
2. Open the app and add a connection:
|
||||
- **Local Network**: Use your machine's local IP (e.g., `http://192.168.1.100:4096`)
|
||||
- **Tunnel**: Set up a Cloudflare Tunnel or ngrok and use the tunnel URL
|
||||
- **Cloud**: Connect to a hosted OpenCode instance
|
||||
**Step 2 — Install OpenCode Mobile** from any store above (or build from source — see [CONTRIBUTING.md](CONTRIBUTING.md)).
|
||||
|
||||
## Building for Production
|
||||
**Step 3 — Add a connection in the app**
|
||||
|
||||
### iOS
|
||||
Open the app, tap **Add Connection**, and choose your connection type:
|
||||
|
||||
```bash
|
||||
bun run ios
|
||||
# or
|
||||
eas build --platform ios
|
||||
```
|
||||
- **Local network** — your machine's LAN IP, e.g. `http://192.168.1.100:4096`
|
||||
- **Tunnel** — a Cloudflare Tunnel or ngrok URL, e.g. `https://my-opencode.trycloudflare.com`
|
||||
- **Tailscale** — your machine's Tailscale IP, e.g. `http://100.x.x.x:4096`
|
||||
- **opencode Cloud** *(coming soon)* — one-tap managed hosting, no server to run
|
||||
|
||||
### Android
|
||||
Enter the password you set in Step 1, tap **Connect**, and you're in.
|
||||
|
||||
```bash
|
||||
bun run android
|
||||
# or
|
||||
eas build --platform android
|
||||
```
|
||||
---
|
||||
|
||||
## Security
|
||||
## How It Works
|
||||
|
||||
- Credentials are stored securely using `expo-secure-store` (iOS Keychain / Android Keystore)
|
||||
- Optional biometric authentication for app access
|
||||
- Optional biometric confirmation for sending messages
|
||||
- All traffic should use HTTPS for non-local connections
|
||||
|
||||
## Architecture
|
||||
OpenCode Mobile is a thin client. It speaks the opencode HTTP + SSE API: listing sessions, sending messages, streaming responses, and subscribing to file-change events. All AI model calls are handled by your opencode server — you bring your own API keys (OpenAI, Anthropic, etc.) and the app never touches them. The app never proxies your code or conversation through our servers.
|
||||
|
||||
```
|
||||
packages/mobile/
|
||||
├── app/ # Expo Router screens
|
||||
│ ├── (tabs)/ # Tab navigation
|
||||
│ ├── session/ # Session screens
|
||||
│ └── connection/ # Connection management
|
||||
├── src/
|
||||
│ ├── components/ # Reusable components
|
||||
│ ├── hooks/ # Custom hooks
|
||||
│ ├── lib/ # SDK client & types
|
||||
│ └── stores/ # Zustand state stores
|
||||
└── assets/ # App icons & images
|
||||
┌─────────────────────────────────────┐
|
||||
│ OpenCode Mobile │
|
||||
│ (React Native / Expo, this repo) │
|
||||
└──────────────┬──────────────────────┘
|
||||
│ HTTP + SSE
|
||||
│ (local network / tunnel / cloud)
|
||||
▼
|
||||
┌─────────────────────────────────────┐
|
||||
│ opencode server │
|
||||
│ (github.com/sst/opencode, MIT) │
|
||||
│ Running on your laptop / VPS │
|
||||
└──────────────┬──────────────────────┘
|
||||
│ API calls
|
||||
▼
|
||||
┌─────────────────────────────────────┐
|
||||
│ Your AI provider │
|
||||
│ (OpenAI / Anthropic / Gemini / …) │
|
||||
│ Your keys, your bill │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Project Status
|
||||
|
||||
**Current version: v0.2.3**
|
||||
|
||||
| Feature | Status |
|
||||
|---|---|
|
||||
| Multi-connection management | Stable |
|
||||
| Session list + creation | Stable |
|
||||
| Streaming chat | Stable |
|
||||
| Diff viewer | Stable |
|
||||
| Biometric unlock | Stable |
|
||||
| Tool call approval UI | Stable |
|
||||
| Sentry crash reporting (opt-in) | Stable |
|
||||
| Cloudflare / ngrok tunnel wizard | Beta |
|
||||
| opencode Cloud one-tap connect | Planned |
|
||||
| iPad / tablet layout | Planned |
|
||||
| Offline session history | Planned |
|
||||
|
||||
---
|
||||
|
||||
## Supporters and Sponsors
|
||||
|
||||
OpenCode Mobile is built and maintained by [VIBE TECHNOLOGIES, LLC](https://opencode.vibebrowser.app). GitHub Sponsors help cover Sentry, EAS Build, and CI costs (~$60/month). The opencode Cloud hosted backend (planned, $10/mo) is the long-term revenue model.
|
||||
|
||||
If OpenCode Mobile saves you time, consider sponsoring:
|
||||
|
||||
**[github.com/sponsors/VibeTechnologies](https://github.com/sponsors/VibeTechnologies)**
|
||||
|
||||
| Tier | Price | Perk |
|
||||
|---|---|---|
|
||||
| Supporter | $5/mo | Your name in `SUPPORTERS.md` |
|
||||
| Backer | $15/mo | Name + early access to opencode Cloud beta |
|
||||
| Business | $50/mo | Logo on [opencode.vibebrowser.app](https://opencode.vibebrowser.app) + quarterly support call |
|
||||
|
||||
Questions or private support: [support@vibebrowser.app](mailto:support@vibebrowser.app)
|
||||
|
||||
---
|
||||
|
||||
## Roadmap
|
||||
|
||||
Tracked on the [GitHub Projects board](https://github.com/dzianisv/opencode-mobile/projects) and in the [open milestones](https://github.com/dzianisv/opencode-mobile/milestones).
|
||||
|
||||
Near-term priorities:
|
||||
- opencode Cloud one-tap connect + managed hosting
|
||||
- F-Droid mainline acceptance (FCM audit + reproducible build verification)
|
||||
- Tunnel setup wizard (Cloudflare / ngrok / Tailscale)
|
||||
- iPad / tablet layout
|
||||
- Offline session history cache
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
This is part of the OpenCode monorepo. See the root README for contribution guidelines.
|
||||
We welcome bug reports, feature requests, and pull requests. See [CONTRIBUTING.md](CONTRIBUTING.md) for how to set up a dev environment and the contribution process.
|
||||
|
||||
---
|
||||
|
||||
## Privacy
|
||||
|
||||
OpenCode Mobile does not collect personal data. Optional Sentry crash reporting (opt-in, off by default) sends anonymised crash traces to Sentry. No analytics SDKs are bundled. Credentials are stored exclusively on-device in the OS keystore.
|
||||
|
||||
Full privacy policy: [opencode.vibebrowser.app/privacy](https://opencode.vibebrowser.app/privacy)
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
MIT — see [LICENSE](LICENSE).
|
||||
|
||||
Copyright (c) 2026 VIBE TECHNOLOGIES, LLC
|
||||
|
||||
---
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
- [sst/opencode](https://github.com/sst/opencode) — the AI coding agent this app connects to (MIT)
|
||||
- [Expo](https://expo.dev) — the React Native toolchain powering the app
|
||||
- Every contributor who filed a bug, opened a PR, or starred the repo
|
||||
|
||||
58
app.json
@@ -6,22 +6,74 @@
|
||||
"orientation": "portrait",
|
||||
"scheme": "opencode",
|
||||
"userInterfaceStyle": "automatic",
|
||||
"icon": "./assets/icon.png",
|
||||
"splash": {
|
||||
"image": "./assets/splash-icon.png",
|
||||
"resizeMode": "contain",
|
||||
"backgroundColor": "#0F172A"
|
||||
},
|
||||
"ios": {
|
||||
"supportsTablet": true,
|
||||
"bundleIdentifier": "ai.opencode.mobile",
|
||||
"buildNumber": "1",
|
||||
"entitlements": {
|
||||
"aps-environment": "production"
|
||||
},
|
||||
"infoPlist": {
|
||||
"NSFaceIDUsageDescription": "Use Face ID to unlock OpenCode and access your sessions securely."
|
||||
"NSFaceIDUsageDescription": "Use Face ID to unlock OpenCode and access your sessions securely.",
|
||||
"NSSpeechRecognitionUsageDescription": "OpenCode uses speech recognition so you can dictate prompts to your AI coding agent hands-free.",
|
||||
"NSMicrophoneUsageDescription": "OpenCode uses the microphone to capture your voice when using speech-to-text input.",
|
||||
"NSPhotoLibraryUsageDescription": "OpenCode can attach images from your photo library to messages to your AI coding agent.",
|
||||
"NSCameraUsageDescription": "OpenCode can capture images with your camera and attach them to messages to your AI coding agent.",
|
||||
"NSAppTransportSecurity": {
|
||||
"NSAllowsArbitraryLoads": true,
|
||||
"NSAllowsArbitraryLoadsInWebContent": false
|
||||
},
|
||||
"ITSAppUsesNonExemptEncryption": false
|
||||
}
|
||||
},
|
||||
"android": {
|
||||
"package": "ai.opencode.mobile",
|
||||
"usesCleartextTraffic": true
|
||||
"versionCode": 1,
|
||||
"usesCleartextTraffic": true,
|
||||
"adaptiveIcon": {
|
||||
"foregroundImage": "./assets/adaptive-icon.png",
|
||||
"backgroundColor": "#0F172A"
|
||||
}
|
||||
},
|
||||
"web": {
|
||||
"bundler": "metro",
|
||||
"output": "static"
|
||||
},
|
||||
"plugins": ["expo-router", "expo-secure-store", "expo-local-authentication", "@sentry/react-native/expo"],
|
||||
"plugins": [
|
||||
"expo-router",
|
||||
"expo-secure-store",
|
||||
"expo-local-authentication",
|
||||
"@sentry/react-native/expo",
|
||||
[
|
||||
"expo-notifications",
|
||||
{
|
||||
"icon": "./assets/icon.png",
|
||||
"color": "#ffffff",
|
||||
"sounds": []
|
||||
}
|
||||
],
|
||||
[
|
||||
"expo-image-picker",
|
||||
{
|
||||
"photosPermission": "OpenCode can attach images from your photo library to messages to your AI coding agent.",
|
||||
"cameraPermission": "OpenCode can capture images with your camera and attach them to messages to your AI coding agent.",
|
||||
"microphonePermission": false
|
||||
}
|
||||
],
|
||||
[
|
||||
"expo-speech-recognition",
|
||||
{
|
||||
"microphonePermission": "OpenCode uses the microphone to capture your voice when using speech-to-text input.",
|
||||
"speechRecognitionPermission": "OpenCode uses speech recognition so you can dictate prompts to your AI coding agent hands-free."
|
||||
}
|
||||
]
|
||||
],
|
||||
"experiments": {
|
||||
"typedRoutes": true,
|
||||
"reactCompiler": true
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
granted as notificationsGranted,
|
||||
} from "../../src/lib/notifications"
|
||||
import type { Category } from "../../src/lib/notifications"
|
||||
import { hasTelemetryConsent, setTelemetryConsent } from "../../src/lib/telemetry"
|
||||
|
||||
function SettingRow({
|
||||
icon,
|
||||
@@ -73,6 +74,15 @@ export default function SettingsScreen() {
|
||||
const { notifications, setNotification } = useSettings()
|
||||
const [osGranted, setOsGranted] = useState<boolean | null>(null)
|
||||
|
||||
// Telemetry consent: hasTelemetryConsent() returns null (unknown), true, or false.
|
||||
// We initialise local state from in-memory value; updates call setTelemetryConsent().
|
||||
const [crashReporting, setCrashReporting] = useState<boolean>(hasTelemetryConsent() ?? false)
|
||||
|
||||
const handleCrashReportingToggle = useCallback(async (value: boolean) => {
|
||||
setCrashReporting(value)
|
||||
await setTelemetryConsent(value)
|
||||
}, [])
|
||||
|
||||
// Check OS permission state on first toggle attempt
|
||||
const handleToggle = useCallback(
|
||||
async (category: Category, enabled: boolean) => {
|
||||
@@ -173,6 +183,30 @@ export default function SettingsScreen() {
|
||||
)}
|
||||
</SettingSection>
|
||||
|
||||
<SettingSection title="Privacy" isDark={isDark}>
|
||||
<SettingRow
|
||||
icon="shield-checkmark"
|
||||
label="Crash Reporting"
|
||||
description="Share anonymous crash reports via Sentry to help improve OpenCode. No code or prompts are included."
|
||||
isDark={isDark}
|
||||
right={
|
||||
<Switch
|
||||
value={crashReporting}
|
||||
onValueChange={handleCrashReportingToggle}
|
||||
trackColor={{ false: "#767577", true: "#22c55e" }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<SettingRow
|
||||
icon="document-text"
|
||||
label="Privacy Policy"
|
||||
description="What data we collect and how"
|
||||
isDark={isDark}
|
||||
onPress={() => Linking.openURL("https://www.vibebrowser.app/opencode-mobile/privacy")}
|
||||
right={<Ionicons name="open-outline" size={20} color={isDark ? "#666666" : "#999999"} />}
|
||||
/>
|
||||
</SettingSection>
|
||||
|
||||
<SettingSection title="About" isDark={isDark}>
|
||||
<SettingRow icon="information-circle" label="Version" description="1.0.0" isDark={isDark} />
|
||||
<SettingRow
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef } from "react"
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { Stack, router } from "expo-router"
|
||||
import { StatusBar } from "expo-status-bar"
|
||||
import { useColorScheme, View, ActivityIndicator } from "react-native"
|
||||
@@ -12,11 +12,10 @@ import { useCatalog } from "../src/stores/catalog"
|
||||
import { useSettings } from "../src/stores/settings"
|
||||
import { AuthGate } from "../src/components/AuthGate"
|
||||
import { ErrorBoundary } from "../src/components/ErrorBoundary"
|
||||
import { TelemetryConsentModal } from "../src/components/TelemetryConsentModal"
|
||||
import * as notifications from "../src/lib/notifications"
|
||||
import { addBreadcrumb, initSentry, wrap } from "../src/lib/sentry"
|
||||
|
||||
initSentry()
|
||||
addBreadcrumb({ category: "app.lifecycle", message: "app started" })
|
||||
import { addBreadcrumb, wrap } from "../src/lib/sentry"
|
||||
import { loadTelemetryConsent, setTelemetryConsent } from "../src/lib/telemetry"
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
@@ -28,6 +27,9 @@ function RootLayout() {
|
||||
const { loadConnections, isLoading: connectionsLoading, client } = useConnections()
|
||||
const sseStarted = useRef(false)
|
||||
|
||||
// Telemetry consent state: null = loading, 'unknown' = show modal, else decided
|
||||
const [consentState, setConsentState] = useState<"loading" | "unknown" | "decided">("loading")
|
||||
|
||||
useEffect(() => {
|
||||
initAuth()
|
||||
loadConnections()
|
||||
@@ -37,9 +39,32 @@ function RootLayout() {
|
||||
notifications.configure(() => useSettings.getState().notifications)
|
||||
|
||||
// Navigate to session when user taps a notification
|
||||
return notifications.onTap((data) => {
|
||||
const unsubNotifications = notifications.onTap((data) => {
|
||||
router.push(`/session/${data.sessionId}`)
|
||||
})
|
||||
|
||||
// Load telemetry consent — initialise Sentry only if previously granted
|
||||
loadTelemetryConsent()
|
||||
.then((state) => {
|
||||
if (state === "granted") {
|
||||
import("../src/lib/sentry").then(({ initSentry }) => {
|
||||
initSentry()
|
||||
addBreadcrumb({ category: "app.lifecycle", message: "app started" })
|
||||
})
|
||||
setConsentState("decided")
|
||||
} else if (state === "denied") {
|
||||
addBreadcrumb({ category: "app.lifecycle", message: "app started (telemetry off)" })
|
||||
setConsentState("decided")
|
||||
} else {
|
||||
setConsentState("unknown")
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// SecureStore unavailable — show modal so user can decide
|
||||
setConsentState("unknown")
|
||||
})
|
||||
|
||||
return unsubNotifications
|
||||
}, [])
|
||||
|
||||
// Connect/disconnect SSE and load catalog when client changes
|
||||
@@ -60,7 +85,7 @@ function RootLayout() {
|
||||
}
|
||||
}, [client])
|
||||
|
||||
const isLoading = authLoading || connectionsLoading
|
||||
const isLoading = authLoading || connectionsLoading || consentState === "loading"
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -122,6 +147,18 @@ function RootLayout() {
|
||||
</QueryClientProvider>
|
||||
</BottomSheetModalProvider>
|
||||
</GestureHandlerRootView>
|
||||
{/* Telemetry consent modal — shown once on first launch */}
|
||||
<TelemetryConsentModal
|
||||
visible={consentState === "unknown"}
|
||||
onAllow={async () => {
|
||||
await setTelemetryConsent(true)
|
||||
setConsentState("decided")
|
||||
}}
|
||||
onDecline={async () => {
|
||||
await setTelemetryConsent(false)
|
||||
setConsentState("decided")
|
||||
}}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
useColorScheme,
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
Linking,
|
||||
} from "react-native"
|
||||
import { router } from "expo-router"
|
||||
import { Ionicons } from "@expo/vector-icons"
|
||||
@@ -33,6 +34,7 @@ export default function AddConnectionScreen() {
|
||||
const [username, setUsername] = useState("")
|
||||
const [password, setPassword] = useState("")
|
||||
const [isConnecting, setIsConnecting] = useState(false)
|
||||
const [waitlistEmail, setWaitlistEmail] = useState("")
|
||||
|
||||
const buildUrl = () => {
|
||||
if (mode === "advanced") return url.trim()
|
||||
@@ -135,6 +137,13 @@ export default function AddConnectionScreen() {
|
||||
router.back()
|
||||
}
|
||||
|
||||
const handleJoinWaitlist = () => {
|
||||
const email = waitlistEmail.trim()
|
||||
const subject = encodeURIComponent("OpenCode Connect Waitlist")
|
||||
const body = encodeURIComponent(email ? `Sign me up!\n\nEmail: ${email}` : "Sign me up!")
|
||||
Linking.openURL(`mailto:connect@vibebrowser.app?subject=${subject}&body=${body}`)
|
||||
}
|
||||
|
||||
// Quick connect mode - simplified
|
||||
if (mode === "quick") {
|
||||
return (
|
||||
@@ -235,6 +244,40 @@ export default function AddConnectionScreen() {
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* OpenCode Connect — Coming Soon */}
|
||||
<View style={[styles.connectCard, isDark && styles.connectCardDark]}>
|
||||
<View style={styles.connectCardHeader}>
|
||||
<Ionicons name="cloud-done-outline" size={28} color="#6366f1" />
|
||||
<View style={styles.connectCardTitles}>
|
||||
<Text style={[styles.connectCardTitle, isDark && styles.textDark]}>OpenCode Connect</Text>
|
||||
<View style={styles.connectCardBadge}>
|
||||
<Text style={styles.connectCardBadgeText}>Coming Soon</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<Text style={[styles.connectCardDesc, isDark && styles.hintDark]}>
|
||||
Bridge your phone to your opencode server — no tunnel setup, no firewall config. One-tap connect from
|
||||
anywhere.
|
||||
</Text>
|
||||
<TextInput
|
||||
style={[styles.input, isDark && styles.inputDark, { marginTop: 12 }]}
|
||||
placeholder="your@email.com"
|
||||
placeholderTextColor={isDark ? "#666666" : "#999999"}
|
||||
value={waitlistEmail}
|
||||
onChangeText={setWaitlistEmail}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
keyboardType="email-address"
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={styles.waitlistButton}
|
||||
onPress={handleJoinWaitlist}
|
||||
>
|
||||
<Ionicons name="mail-outline" size={16} color="#ffffff" />
|
||||
<Text style={styles.waitlistButtonText}>Join Waitlist</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Advanced mode link */}
|
||||
<TouchableOpacity style={styles.advancedLink} onPress={() => setMode("advanced")}>
|
||||
<Text style={[styles.advancedLinkText, isDark && styles.hintDark]}>
|
||||
@@ -573,4 +616,64 @@ const styles = StyleSheet.create({
|
||||
marginTop: 32,
|
||||
marginBottom: 8,
|
||||
},
|
||||
connectCard: {
|
||||
backgroundColor: "#f0f0ff",
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
marginTop: 24,
|
||||
borderWidth: 1,
|
||||
borderColor: "#c7d2fe",
|
||||
},
|
||||
connectCardDark: {
|
||||
backgroundColor: "#1e1b4b",
|
||||
borderColor: "#3730a3",
|
||||
},
|
||||
connectCardHeader: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 12,
|
||||
marginBottom: 8,
|
||||
},
|
||||
connectCardTitles: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
flexWrap: "wrap",
|
||||
},
|
||||
connectCardTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: "700",
|
||||
color: "#0a0a0a",
|
||||
},
|
||||
connectCardBadge: {
|
||||
backgroundColor: "#6366f1",
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 2,
|
||||
borderRadius: 4,
|
||||
},
|
||||
connectCardBadgeText: {
|
||||
color: "#ffffff",
|
||||
fontSize: 11,
|
||||
fontWeight: "600",
|
||||
},
|
||||
connectCardDesc: {
|
||||
fontSize: 13,
|
||||
color: "#666666",
|
||||
lineHeight: 20,
|
||||
},
|
||||
waitlistButton: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 8,
|
||||
padding: 12,
|
||||
borderRadius: 8,
|
||||
backgroundColor: "#6366f1",
|
||||
marginTop: 12,
|
||||
},
|
||||
waitlistButtonText: {
|
||||
fontSize: 15,
|
||||
fontWeight: "600",
|
||||
color: "#ffffff",
|
||||
},
|
||||
})
|
||||
|
||||
BIN
assets/adaptive-icon.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
assets/icon-appstore.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
assets/icon.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
assets/splash-icon.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
64
distribution/SIGNING-KEY-FINGERPRINTS.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# Signing Key Fingerprints — opencode-mobile
|
||||
|
||||
All distribution channels (Google Play, F-Droid, IzzyOnDroid) use the same
|
||||
signing key. This allows users to update in-place across stores.
|
||||
|
||||
Keystore: `keystores/production-release.jks`
|
||||
Key alias: (see Bitwarden item `KEYSTORE_INFO` in project folder `opencode-mobile`)
|
||||
|
||||
---
|
||||
|
||||
## SHA-256 Certificate Fingerprint
|
||||
|
||||
**Colon-separated (keytool / apksigner display format):**
|
||||
|
||||
```
|
||||
0C:25:9D:94:E0:FF:EA:5D:63:19:61:4B:22:9D:...:DF:6A:A0:99
|
||||
```
|
||||
|
||||
Full value (for file reference only — middle bytes intentionally omitted in reports):
|
||||
```
|
||||
0C:25:9D:94:E0:FF:EA:5D:63:19:61:4B:22:9D:4B:6B:DC:22:DE:1F:56:E3:8E:76:94:83:98:D2:DF:6A:A0:99
|
||||
```
|
||||
|
||||
**Lowercase without colons (F-Droid `AllowedAPKSigningKeys` format):**
|
||||
|
||||
```
|
||||
0c259d94e0ffea5d6319614b229d4b6bdc22de1f56e38e769483 98d2df6aa099
|
||||
```
|
||||
|
||||
(Remove the space — it is split here only for readability.)
|
||||
|
||||
---
|
||||
|
||||
## How to verify
|
||||
|
||||
```bash
|
||||
# From keystore
|
||||
keytool -list -v \
|
||||
-keystore keystores/production-release.jks \
|
||||
-storepass <STOREPASS> \
|
||||
| grep "SHA256:"
|
||||
|
||||
# From a signed APK
|
||||
apksigner verify --print-certs path/to/app-release.apk | grep SHA-256
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage in submissions
|
||||
|
||||
| Channel | Format needed | Location |
|
||||
|---------|--------------|----------|
|
||||
| F-Droid `AllowedAPKSigningKeys` | lowercase hex, no colons | `distribution/fdroid-submission/metadata.yml` |
|
||||
| IzzyOnDroid inclusion request | colon-separated | `distribution/izzyondroid-submission/INCLUSION-REQUEST.md` |
|
||||
| Google Play App Signing | uploaded via console | (Play manages separately if App Signing enabled) |
|
||||
|
||||
---
|
||||
|
||||
## Key backup
|
||||
|
||||
The keystore is gitignored. Backup location: Bitwarden project folder `opencode-mobile`,
|
||||
item `KEYSTORE_PRODUCTION_JKS`. Keep a second offline backup.
|
||||
|
||||
**Never commit the keystore or its password to git.**
|
||||
BIN
distribution/app-store-graphics/ipad-129/01.png
Normal file
|
After Width: | Height: | Size: 183 KiB |
BIN
distribution/app-store-graphics/ipad-129/02.png
Normal file
|
After Width: | Height: | Size: 190 KiB |
BIN
distribution/app-store-graphics/iphone-65/01.png
Normal file
|
After Width: | Height: | Size: 110 KiB |
BIN
distribution/app-store-graphics/iphone-65/02.png
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
distribution/app-store-graphics/iphone-65/03.png
Normal file
|
After Width: | Height: | Size: 87 KiB |
BIN
distribution/app-store-graphics/iphone-67/01.png
Normal file
|
After Width: | Height: | Size: 114 KiB |
BIN
distribution/app-store-graphics/iphone-67/02.png
Normal file
|
After Width: | Height: | Size: 115 KiB |
BIN
distribution/app-store-graphics/iphone-67/03.png
Normal file
|
After Width: | Height: | Size: 89 KiB |
349
distribution/app-store-listing.md
Normal file
@@ -0,0 +1,349 @@
|
||||
# App Store Listing — OpenCode (iOS)
|
||||
|
||||
Copy-paste reference for App Store Connect. Use this once the Apple Developer Program enrollment is approved and the app is created in App Store Connect.
|
||||
|
||||
---
|
||||
|
||||
## App Information
|
||||
|
||||
### App Name (max 30 chars)
|
||||
|
||||
```
|
||||
OpenCode
|
||||
```
|
||||
(8 chars)
|
||||
|
||||
### Subtitle (max 30 chars)
|
||||
|
||||
```
|
||||
AI Coding Agent, In Your Pocket
|
||||
```
|
||||
(31 chars — trim to:)
|
||||
|
||||
```
|
||||
AI Coding Agent in Your Pocket
|
||||
```
|
||||
(30 chars exactly)
|
||||
|
||||
### Promotional Text (max 170 chars — appears above description, can be updated without a new review)
|
||||
|
||||
```
|
||||
Drive your self-hosted AI coding agent from anywhere. Connect to opencode on your workstation and review, approve, and guide AI-generated code changes in real time.
|
||||
```
|
||||
(165 chars)
|
||||
|
||||
### Description (max 4000 chars)
|
||||
|
||||
```
|
||||
OpenCode is an open-source mobile client for the opencode AI coding agent (github.com/sst/opencode). Connect to your self-hosted opencode server and drive AI-powered coding sessions directly from your iPhone or iPad.
|
||||
|
||||
KEY FEATURES
|
||||
|
||||
• Multiple connection types — local network (Wi-Fi), secure tunnels (Cloudflare Tunnel, ngrok), or cloud-hosted opencode instances
|
||||
• Biometric unlock — Face ID / Touch ID to keep your sessions private
|
||||
• Real-time streaming chat — watch your AI agent think and respond live as it works through your code
|
||||
• File diff viewer — see exactly what code changes the agent proposes before you accept them
|
||||
• Multi-session management — start, resume, and switch between coding sessions
|
||||
• Tool call approval — review and approve file writes, shell commands, and other actions before the agent executes them on your code
|
||||
|
||||
WHO IT'S FOR
|
||||
|
||||
• Developers who run opencode on their workstation or a server and want to check in from their phone
|
||||
• Engineers away from their desk who want to guide long-running AI coding sessions
|
||||
• Teams who self-host AI dev tools and want a polished mobile companion
|
||||
|
||||
WHAT IT IS NOT
|
||||
|
||||
• Not an AI model — you bring your own opencode server (which connects to Claude, GPT-4, Gemini, or local models via your API keys)
|
||||
• Not a code editor — pairs with your existing IDE and terminal workflow
|
||||
• Not a subscription service — the app is free and open source
|
||||
|
||||
OPEN SOURCE
|
||||
|
||||
OpenCode Mobile is MIT licensed. Source code, issue tracker, and community discussion:
|
||||
https://github.com/dzianisv/opencode-mobile
|
||||
|
||||
PRIVACY
|
||||
|
||||
OpenCode Mobile does not collect your code, prompts, or AI responses. All AI traffic goes directly from the app to YOUR opencode server — never through our infrastructure. We use Sentry for crash reporting only (no personally identifiable information, no message content). See our privacy policy for full details.
|
||||
|
||||
SELF-HOSTED FIRST
|
||||
|
||||
OpenCode requires you to run an opencode server:
|
||||
1. Install: npm install -g opencode-ai
|
||||
2. Run: opencode serve
|
||||
3. Connect the app to the server URL
|
||||
|
||||
If you cannot self-host, contact support@vibebrowser.app and we will provide a temporary review endpoint.
|
||||
|
||||
SUPPORT
|
||||
|
||||
Email: support@vibebrowser.app
|
||||
Issues: https://github.com/dzianisv/opencode-mobile/issues
|
||||
```
|
||||
|
||||
(Character count: ~1,750 — well under 4,000 limit. Add more feature detail or FAQ if desired.)
|
||||
|
||||
---
|
||||
|
||||
## Keywords (max 100 chars, comma-separated)
|
||||
|
||||
```
|
||||
opencode,AI coding,developer,LLM,Claude,GPT,coding agent,self-hosted,mobile dev,terminal
|
||||
```
|
||||
(89 chars)
|
||||
|
||||
Alternative / supplemental terms to rotate in A/B: `code review`, `AI assistant`, `remote dev`, `SSH`, `copilot`
|
||||
|
||||
---
|
||||
|
||||
## URLs
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Support URL | https://www.vibebrowser.app/ |
|
||||
| Marketing URL | https://github.com/dzianisv/opencode-mobile |
|
||||
| Privacy Policy URL | https://opencode.vibebrowser.app/privacy |
|
||||
|
||||
---
|
||||
|
||||
## App Category
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Primary Category | Developer Tools |
|
||||
| Secondary Category | Productivity |
|
||||
|
||||
---
|
||||
|
||||
## Pricing & Availability
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Price | Free |
|
||||
| In-App Purchases | None |
|
||||
| Availability | All territories (no regional restrictions) |
|
||||
|
||||
---
|
||||
|
||||
## Age Rating Questionnaire
|
||||
|
||||
Apple uses a questionnaire to assign an age rating. Answer as follows for OpenCode Mobile:
|
||||
|
||||
| Question | Answer | Notes |
|
||||
|---|---|---|
|
||||
| Cartoon or fantasy violence | None | Dev tool — no violence content |
|
||||
| Realistic violence | None | |
|
||||
| Prolonged graphic violence | None | |
|
||||
| Sexual content or nudity | None | |
|
||||
| Profanity or crude humor | None | |
|
||||
| Mature or suggestive themes | None | |
|
||||
| Horror/fear-inducing content | None | |
|
||||
| Medical/treatment information | None | |
|
||||
| Alcohol, tobacco, drugs | None | |
|
||||
| Gambling | None | |
|
||||
| Contests | None | |
|
||||
| Unrestricted web access | No | App connects to user-specified servers only, no general browser |
|
||||
| Sharing location data | No | |
|
||||
| User-generated content | No | Chat is between user and their own AI backend, not user-to-user |
|
||||
|
||||
**Expected rating: 4+** (no objectionable content; developer tool)
|
||||
|
||||
---
|
||||
|
||||
## Privacy Nutrition Label (App Privacy)
|
||||
|
||||
Apple requires you to declare what data the app collects. Fill the App Privacy section in App Store Connect as follows.
|
||||
|
||||
### Data Not Collected
|
||||
|
||||
OpenCode Mobile does NOT collect any of the following:
|
||||
- Name, Email address, Phone number, Physical address, Other contact info
|
||||
- Health/fitness data
|
||||
- Financial info, payment info
|
||||
- Precise or coarse location
|
||||
- Contacts
|
||||
- Emails or text messages
|
||||
- Photos or videos
|
||||
- Audio data
|
||||
- Gameplay content
|
||||
- Customer support data
|
||||
- Browsing history, search history
|
||||
- Sensitive info
|
||||
- User content (code, prompts, AI responses are not sent to our servers)
|
||||
- Identifiers (User ID, Device ID — Sentry uses an installation-scoped anonymous ID, see below)
|
||||
|
||||
### Data Linked to You: None
|
||||
|
||||
### Data Not Linked to You
|
||||
|
||||
| Data Type | Category | Purpose | Optional? |
|
||||
|---|---|---|---|
|
||||
| Crash Data | Diagnostics | App functionality | No — always on (see note) |
|
||||
| Performance Data | Diagnostics | App functionality | No — always on (see note) |
|
||||
| Other Diagnostic Data | Diagnostics | App functionality | No |
|
||||
|
||||
**Explanation**: Sentry crash reporting sends device model, OS version, app version, and stack traces. Sentry assigns an anonymous installation ID (not linked to any Apple ID or personal information). No user-generated content (code, prompts, responses) is ever sent.
|
||||
|
||||
**Sentry opt-in status**: As of v0.2.3, Sentry is **always on** when a DSN is configured. If you add a settings toggle for Sentry consent (planned), change Optional? to "Yes" and add a note that users who decline are in the "Data Not Collected" category. In App Store Connect, once opt-in is implemented, this section can be removed or marked optional.
|
||||
|
||||
**"Are you or your third-party partners using this data to track users?"**: No
|
||||
|
||||
**App Tracking Transparency (ATT)**: OpenCode Mobile does **not** use the ATT framework (`AppTrackingTransparency`) and does **not** access the IDFA (Identifier for Advertisers). No ad networks or cross-app tracking SDKs are present. No ATT permission prompt is shown to users.
|
||||
|
||||
---
|
||||
|
||||
## Export Compliance
|
||||
|
||||
Apple asks about encryption during submission. Answer:
|
||||
|
||||
| Question | Answer |
|
||||
|---|---|
|
||||
| Does the app use encryption beyond what is in the operating system? | No |
|
||||
| Does the app qualify for any exemptions? | N/A |
|
||||
|
||||
**Rationale**: OpenCode Mobile uses only standard HTTPS/TLS provided by iOS networking APIs (URLSession via React Native's fetch). It does not implement any custom cryptographic algorithms. Per US Export Administration Regulations (EAR), apps using only standard OS-provided encryption and that do not modify the encryption are exempt from ERN requirements. Source: https://developer.apple.com/documentation/security/complying_with_encryption_export_regulations
|
||||
|
||||
**Action**: In App Store Connect > App Information > Export Compliance, select "No" when asked if the app uses non-exempt encryption.
|
||||
|
||||
---
|
||||
|
||||
## App Review Notes — ATS Justification
|
||||
|
||||
The app's `Info.plist` sets `NSAllowsArbitraryLoads: true` in `NSAppTransportSecurity`. Apple App Store review may ask for justification. Paste the following in the **App Review Notes** field in App Store Connect, or in the reviewer communication:
|
||||
|
||||
---
|
||||
|
||||
**Justification for NSAllowsArbitraryLoads:**
|
||||
|
||||
OpenCode is a developer tool that connects to user-self-hosted opencode CLI servers. These servers typically run on `localhost` or a LAN address (e.g. `http://192.168.1.100:4096`) over plain HTTP. Requiring TLS would prevent the app from functioning as designed for the dev-tool use case, because:
|
||||
|
||||
1. The opencode server (`opencode serve`) serves over plain HTTP by default. Requiring the user to configure TLS certificates for a localhost developer tool is an unreasonable burden.
|
||||
2. The connection targets are the user's **own** machines, not third-party services. There is no user-to-user data exchange and no third-party server communication through this code path.
|
||||
3. `NSAllowsArbitraryLoadsInWebContent` is explicitly set to `false` — we only relax ATS for the app's own network calls to the user-configured backend.
|
||||
|
||||
This is exactly the use case Apple's own documentation acknowledges when describing developer tools and enterprise apps that connect to custom internal servers. Reference: https://developer.apple.com/documentation/bundleresources/information_property_list/nsapptransportsecurity
|
||||
|
||||
We do not weaken security for any user-facing content delivered by third parties. If the reviewer requires additional justification or a demonstration, we are happy to provide a screen recording or a live server endpoint.
|
||||
|
||||
---
|
||||
|
||||
## App Review Information
|
||||
|
||||
### Sign-in Required
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Sign-in required? | No — the app does not have user accounts |
|
||||
|
||||
### App Access Notes
|
||||
|
||||
Paste the following in the "Notes" field of the App Review Information section:
|
||||
|
||||
```
|
||||
OpenCode Mobile requires the reviewer to connect to an opencode server.
|
||||
|
||||
OPTION A — We provide a hosted review endpoint:
|
||||
Contact support@vibebrowser.app before the review window and we will email a temporary server URL valid for 72 hours.
|
||||
|
||||
OPTION B — Self-host (5 minutes):
|
||||
1. Install Node.js 20+ on any macOS/Linux machine
|
||||
2. Run: npm install -g opencode-ai
|
||||
3. Run: opencode serve --hostname 0.0.0.0
|
||||
4. The terminal prints a URL like: http://192.168.x.x:4096
|
||||
5. In the app: tap "Add Connection" → enter that URL → tap "Connect"
|
||||
6. Tap "New Session" → type any prompt (e.g. "list files in current directory")
|
||||
7. Observe streaming response and tool call approval flow
|
||||
|
||||
Note: The --hostname 0.0.0.0 flag makes the server listen on all interfaces
|
||||
so the iOS device (or simulator) on the same Wi-Fi network can reach it.
|
||||
Without it, the server only accepts connections from localhost.
|
||||
|
||||
The app has no hidden features or paywalls. All functionality is accessible after connecting to a server.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Graphic Assets — Required Before Submission
|
||||
|
||||
All icons and screenshots must be provided before submitting for review.
|
||||
|
||||
### App Icon
|
||||
|
||||
| Asset | Size | Format | Notes |
|
||||
|---|---|---|---|
|
||||
| App Icon | 1024×1024 px | PNG, no alpha channel | Used for App Store; Xcode fans out all other sizes |
|
||||
|
||||
The 1024×1024 icon must NOT have rounded corners (Apple applies them). No transparency.
|
||||
|
||||
Current status: `assets/icon.json` is a placeholder — **real PNG required before submission**.
|
||||
|
||||
### iPhone Screenshots (REQUIRED)
|
||||
|
||||
Minimum 1 screenshot per device class. Recommended: 3–5 showing key flows.
|
||||
|
||||
| Device | Resolution | Size name in App Store Connect |
|
||||
|---|---|---|
|
||||
| iPhone 6.7" (iPhone 16 Pro Max / 15 Plus) | 1320×2868 or 1290×2796 | 6.7" Super Retina XDR Display |
|
||||
| iPhone 6.5" (iPhone 14 Plus / 11 Pro Max) | 1242×2688 | 6.5" Super Retina XDR Display |
|
||||
| iPhone 5.5" (iPhone 8 Plus) | 1242×2208 | 5.5" Retina HD Display |
|
||||
|
||||
Note: As of 2024, Apple only requires 6.7" and 6.5" for new submissions. 5.5" is optional but recommended for coverage.
|
||||
|
||||
Suggested screenshot subjects:
|
||||
1. Connection setup screen (add server URL)
|
||||
2. Active chat session — streaming AI response
|
||||
3. File diff view — seeing a code change
|
||||
4. Tool approval dialog
|
||||
5. Session list / multi-session view
|
||||
6. Biometric unlock (if possible to screenshot without triggering auth)
|
||||
|
||||
### iPad Screenshots (REQUIRED for Universal apps)
|
||||
|
||||
Since `supportsTablet: true`, iPad screenshots are required.
|
||||
|
||||
| Device | Resolution | Size name in App Store Connect |
|
||||
|---|---|---|
|
||||
| iPad 12.9" (iPad Pro 6th gen) | 2048×2732 | 12.9" iPad Pro (6th gen) |
|
||||
| iPad 11" (iPad Pro M4) | 1668×2388 | 11" iPad Pro (M4) |
|
||||
|
||||
Minimum 1 per device class required. iPad screenshots can be the same content as iPhone.
|
||||
|
||||
### Preview Videos (Optional)
|
||||
|
||||
- Max 30 seconds
|
||||
- Same resolution as screenshots for each device class
|
||||
- MP4 or MOV
|
||||
- No audio required
|
||||
|
||||
---
|
||||
|
||||
## TestFlight Beta App Description (shown to TestFlight testers)
|
||||
|
||||
```
|
||||
OpenCode Mobile — iOS beta
|
||||
|
||||
Drive your self-hosted opencode AI coding agent from your iPhone or iPad. Connect to opencode running on your workstation (or any server) and guide AI coding sessions remotely.
|
||||
|
||||
This is an early beta. Please report issues via the TestFlight feedback button or email support@vibebrowser.app.
|
||||
|
||||
To use: you need opencode running somewhere accessible (local Wi-Fi, Tailscale, Cloudflare Tunnel, etc). Run `opencode serve` and enter the server URL in the app.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pending Before First Submission
|
||||
|
||||
- [ ] Apple Developer Program enrollment approved (D-U-N-S 142059652, VIBE TECHNOLOGIES LLC)
|
||||
- [ ] App Store Connect app record created (bundle ID: ai.opencode.mobile)
|
||||
- [ ] App icon 1024×1024 PNG (no alpha, no rounded corners)
|
||||
- [ ] iPhone screenshots (6.7" minimum; 6.5" strongly recommended)
|
||||
- [ ] iPad screenshots (12.9" minimum)
|
||||
- [ ] Privacy policy live at https://opencode.vibebrowser.app/privacy
|
||||
- [ ] App Store Connect API key created (for CI — Key ID, Issuer ID, .p8 file)
|
||||
- [ ] Apple Distribution certificate + provisioning profile (or use EAS managed signing)
|
||||
- [ ] Export compliance answered (No to custom encryption)
|
||||
- [ ] App privacy nutrition label filled
|
||||
- [ ] Age rating questionnaire completed (expected: 4+)
|
||||
- [ ] TestFlight internal group created
|
||||
- [ ] Review notes prepared with temporary server URL or self-host instructions
|
||||
137
distribution/fdroid-submission/REPRODUCIBLE-BUILD-NOTES.md
Normal file
@@ -0,0 +1,137 @@
|
||||
# Reproducible Build Notes — ai.opencode.mobile
|
||||
|
||||
F-Droid's modern `AllowedAPKSigningKeys` path requires that F-Droid's build
|
||||
server can compile the same APK and arrive at a binary that matches the
|
||||
pre-signed APK we supply via GitHub releases. Any non-determinism in the build
|
||||
will break this verification.
|
||||
|
||||
---
|
||||
|
||||
## Issues found (2026-05-24)
|
||||
|
||||
### 1. Kotlin error log files tracked in git — MEDIUM
|
||||
|
||||
**Files committed:**
|
||||
```
|
||||
android/.kotlin/errors/errors-1779181311003.log
|
||||
android/.kotlin/errors/errors-1779181311094.log
|
||||
```
|
||||
|
||||
**Problem:** These log files contain absolute host paths:
|
||||
```
|
||||
While analysing /home/azureuser/workspace/opencode-mobile/node_modules/...
|
||||
```
|
||||
|
||||
When F-Droid builds from source on their server, these log files will not
|
||||
exist (or will contain different paths). Since they are tracked in git and
|
||||
checked out during the build, they could cause differing build outputs
|
||||
if the Kotlin compiler reads or embeds them. More practically, they make
|
||||
the source tree non-portable — a smell that will draw reviewer attention.
|
||||
|
||||
**Recommended fix:** Add `.kotlin/` to `android/.gitignore`:
|
||||
```
|
||||
# android/.gitignore (add this line)
|
||||
.kotlin/
|
||||
```
|
||||
|
||||
Then remove the tracked files:
|
||||
```bash
|
||||
git rm -r --cached android/.kotlin/
|
||||
git commit -m "chore: untrack kotlin error log files from android/.kotlin/"
|
||||
```
|
||||
|
||||
This is a trivial fix. Do it before filing the F-Droid MR.
|
||||
|
||||
---
|
||||
|
||||
### 2. android/ directory tracked in git — LOW (expected but notable)
|
||||
|
||||
`expo prebuild` regenerates `android/` from `app.json` and `package.json`.
|
||||
F-Droid's build metadata uses `npx expo prebuild` as a `prebuild:` step,
|
||||
which means F-Droid rebuilds `android/` from scratch on their server.
|
||||
|
||||
The tracked `android/app/build.gradle` and other generated files must match
|
||||
what `expo prebuild` produces. If the Expo SDK version drifts between what is
|
||||
committed and what npm installs, the build will fail.
|
||||
|
||||
**Mitigation already in place:** `package-lock.json` is committed, which pins
|
||||
all npm dependency versions. The F-Droid metadata `Builds:` step uses
|
||||
`npm install --legacy-peer-deps` which respects `package-lock.json`.
|
||||
|
||||
**Residual risk:** If `expo prebuild` is non-deterministic (e.g., writes the
|
||||
current date/time into generated files), subsequent runs will produce different
|
||||
outputs. This is unlikely but should be verified by running prebuild twice and
|
||||
comparing outputs:
|
||||
```bash
|
||||
npx expo prebuild --platform android --non-interactive --clean
|
||||
git diff android/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Hermes bytecode embedding — LOW
|
||||
|
||||
The React Native Hermes engine compiles the JavaScript bundle to Hermes bytecode
|
||||
at build time. The bytecode format is versioned but should be deterministic for
|
||||
the same JS source + Hermes version. The Hermes version is pinned via
|
||||
`react-native` in `package-lock.json`, so this is low risk.
|
||||
|
||||
---
|
||||
|
||||
### 4. PNG crunching — LOW
|
||||
|
||||
`build.gradle` has `crunchPngs true` for release builds. PNG crunching via aapt2
|
||||
is generally deterministic but can vary across aapt2 versions. F-Droid's build
|
||||
environment may use a different Android build tools version.
|
||||
|
||||
**Mitigation:** Pin `buildToolsVersion` in `android/build.gradle` explicitly
|
||||
rather than relying on the Expo-supplied default. Check via:
|
||||
```bash
|
||||
grep buildToolsVersion android/build.gradle android/app/build.gradle
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. No hardcoded timestamps found — PASS
|
||||
|
||||
Grepped `android/` for `System.currentTimeMillis`, `new Date()`, `buildTime`,
|
||||
`BUILD_DATE`, `UUID.randomUUID()` — no results. This is the most common
|
||||
reproducibility killer and is clean here.
|
||||
|
||||
---
|
||||
|
||||
### 6. No absolute host paths in build files — PASS
|
||||
|
||||
Grepped `android/` `*.gradle` and `*.properties` for `/home/`, `/Users/`,
|
||||
`C:\` — no results in build config files.
|
||||
|
||||
---
|
||||
|
||||
## Priority action items before F-Droid MR
|
||||
|
||||
| Priority | Item | Effort |
|
||||
|----------|------|--------|
|
||||
| HIGH | Add `.kotlin/` to `android/.gitignore` and untrack log files | 5 min |
|
||||
| MEDIUM | Run `expo prebuild` twice, compare output with `git diff` | 15 min |
|
||||
| MEDIUM | Pin `buildToolsVersion` explicitly in `android/build.gradle` | 5 min |
|
||||
| LOW | Verify Hermes bytecode is deterministic (compare two builds) | 30 min |
|
||||
| LOW | Test full reproducible build using F-Droid's Docker build env | Hours |
|
||||
|
||||
---
|
||||
|
||||
## How to test reproducible builds
|
||||
|
||||
F-Droid provides a reproducible build test tool:
|
||||
|
||||
```bash
|
||||
# Install fdroidserver
|
||||
pip install fdroidserver
|
||||
|
||||
# Test reproducibility against a released APK
|
||||
fdroid signatures path/to/app-release.apk
|
||||
|
||||
# Full build test
|
||||
fdroid build ai.opencode.mobile:<versionCode> --verbose
|
||||
```
|
||||
|
||||
See https://f-droid.org/en/docs/Reproducible_Builds/ for the full guide.
|
||||
163
distribution/fdroid-submission/SIZE-OPTIMIZATION.md
Normal file
@@ -0,0 +1,163 @@
|
||||
# APK Size Optimization for F-Droid / IzzyOnDroid
|
||||
|
||||
## Current state
|
||||
|
||||
| Artifact | Size |
|
||||
|----------|------|
|
||||
| Universal AAB (`app-release.aab`) | 58.5 MB |
|
||||
| Per-ABI APK (estimated) | ~20–25 MB |
|
||||
| IzzyOnDroid per-APK limit | 30 MB |
|
||||
|
||||
`bundletool` is not installed in this environment, so per-ABI APK sizes were not
|
||||
measured directly. The estimate above is based on typical Expo/Hermes React Native
|
||||
apps:
|
||||
- Hermes JS engine binary: ~8–10 MB per ABI
|
||||
- React Native native libraries: ~5–8 MB per ABI
|
||||
- JavaScript bundle (ABI-independent): ~6–8 MB
|
||||
- Assets (icons, splash, etc.): ~2–3 MB
|
||||
|
||||
**Conclusion:** arm64-v8a APK is likely ~20–25 MB — within IzzyOnDroid's 30 MB limit.
|
||||
No ABI splits required for the initial submission.
|
||||
|
||||
To verify when bundletool is available:
|
||||
```bash
|
||||
bundletool build-apks \
|
||||
--bundle=android/app/build/outputs/bundle/release/app-release.aab \
|
||||
--output=apks.apks \
|
||||
--mode=universal
|
||||
|
||||
# For per-ABI sizes:
|
||||
bundletool build-apks \
|
||||
--bundle=android/app/build/outputs/bundle/release/app-release.aab \
|
||||
--output=apks-splits.apks \
|
||||
--mode=default
|
||||
|
||||
unzip apks-splits.apks -d apk-splits/
|
||||
ls -lh apk-splits/splits/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## If arm64-v8a APK exceeds 30 MB — fix approach
|
||||
|
||||
### Option A: ABI splits in Expo config plugin (recommended)
|
||||
|
||||
`android/app/build.gradle` is regenerated by `npx expo prebuild` on every CI run,
|
||||
so direct edits are overwritten. The correct approach is an Expo config plugin.
|
||||
|
||||
Create `plugins/withAbiSplits.js`:
|
||||
|
||||
```js
|
||||
const { withAppBuildGradle } = require('@expo/config-plugins');
|
||||
|
||||
module.exports = function withAbiSplits(config) {
|
||||
return withAppBuildGradle(config, (config) => {
|
||||
const contents = config.modResults.contents;
|
||||
// Insert splits block inside android { ... } after defaultConfig
|
||||
if (!contents.includes('splits {')) {
|
||||
config.modResults.contents = contents.replace(
|
||||
/defaultConfig \{/,
|
||||
`splits {
|
||||
abi {
|
||||
reset()
|
||||
enable true
|
||||
universalApk true
|
||||
include "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
|
||||
}
|
||||
}
|
||||
defaultConfig {`
|
||||
);
|
||||
}
|
||||
return config;
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
Register in `app.json`:
|
||||
```json
|
||||
{
|
||||
"expo": {
|
||||
"plugins": [
|
||||
"./plugins/withAbiSplits.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
After `npx expo prebuild`, `android/app/build.gradle` will contain the splits block.
|
||||
Run `./gradlew assembleRelease` — produces separate APKs per ABI.
|
||||
|
||||
For IzzyOnDroid / F-Droid: attach `app-arm64-v8a-release.apk` to the GitHub release.
|
||||
|
||||
### Option B: fdroid Gradle product flavor
|
||||
|
||||
Add a `fdroid` flavor that includes only arm64-v8a:
|
||||
|
||||
```groovy
|
||||
// In the config plugin or directly in build.gradle (pre-prebuild override)
|
||||
android {
|
||||
flavorDimensions "distribution"
|
||||
productFlavors {
|
||||
fdroid {
|
||||
dimension "distribution"
|
||||
ndk {
|
||||
abiFilters "arm64-v8a"
|
||||
}
|
||||
}
|
||||
play {
|
||||
dimension "distribution"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Build with: `./gradlew assembleFdroidRelease`
|
||||
|
||||
This also provides a clean hook for excluding FCM artifacts (see below).
|
||||
|
||||
---
|
||||
|
||||
## FCM / expo-notifications
|
||||
|
||||
`expo-notifications` compiles FCM receiver classes even when only local
|
||||
notifications are used (the app only calls `scheduleNotificationAsync` — no
|
||||
push token retrieval). F-Droid scanner may flag `com.google.firebase:firebase-messaging`
|
||||
as `NonFreeNet` or `NonFreeDep`.
|
||||
|
||||
**Mitigation for F-Droid mainline:** add a `fdroid` product flavor that
|
||||
excludes the FCM artifact:
|
||||
|
||||
```groovy
|
||||
// In config plugin
|
||||
android {
|
||||
flavorDimensions "distribution"
|
||||
productFlavors {
|
||||
fdroid {
|
||||
dimension "distribution"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
configurations.fdroidImplementation {
|
||||
exclude group: 'com.google.firebase', module: 'firebase-messaging'
|
||||
}
|
||||
```
|
||||
|
||||
Or patch `expo-notifications` `build.gradle` via a Gradle init script to
|
||||
replace the FCM dependency with a no-op stub in the `fdroid` flavor.
|
||||
|
||||
**This is not required for IzzyOnDroid** (more tolerant of GMS dependencies
|
||||
if they are not actively invoked). Track the F-Droid reviewer's feedback
|
||||
before investing in this fix — they may accept without it given the
|
||||
local-only usage.
|
||||
|
||||
---
|
||||
|
||||
## Action items (when ready to optimize)
|
||||
|
||||
1. Install bundletool: `sudo apt-get install bundletool` or download JAR from
|
||||
https://github.com/google/bundletool/releases
|
||||
2. Run `build-apks --mode=default` to measure per-ABI sizes
|
||||
3. If arm64-v8a > 30 MB: implement config plugin (Option A above)
|
||||
4. For F-Droid mainline FCM concern: implement `fdroid` product flavor (Option B)
|
||||
5. Document measured sizes in this file once known
|
||||
153
distribution/fdroid-submission/SUBMISSION-CHECKLIST.md
Normal file
@@ -0,0 +1,153 @@
|
||||
# F-Droid Mainline Submission Checklist
|
||||
|
||||
This checklist covers the steps to file a Merge Request against
|
||||
https://gitlab.com/fdroid/fdroiddata to add `ai.opencode.mobile` to the
|
||||
F-Droid main repository.
|
||||
|
||||
**Do NOT start this process until ALL prerequisites are checked.**
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites (must all be true before filing)
|
||||
|
||||
- [ ] First Google Play release is live (proves signing key is in production use)
|
||||
- [ ] Signed APK (not AAB) is attached to a GitHub release tag (e.g. `v0.2.4`)
|
||||
- [ ] Sentry opt-in gate is merged to `main` (avoids `Tracking` anti-feature)
|
||||
- [ ] `expo-notifications` FCM-free flavor exists OR F-Droid team has been pre-warned
|
||||
(see `SIZE-OPTIMIZATION.md` section "FCM Flavor")
|
||||
- [ ] Signing key SHA-256 fingerprint is confirmed in `distribution/SIGNING-KEY-FINGERPRINTS.md`
|
||||
- [ ] Reproducible build has been tested locally (see `REPRODUCIBLE-BUILD-NOTES.md`)
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Prepare the signing key fingerprint
|
||||
|
||||
```bash
|
||||
# Get the colon-separated fingerprint
|
||||
keytool -list -v \
|
||||
-keystore keystores/production-release.jks \
|
||||
-storepass <STOREPASS> \
|
||||
| grep "SHA256:"
|
||||
# Example output:
|
||||
# SHA256: 0C:25:9D:94:E0:FF:EA:5D:63:19:61:4B:22:9D:4B:6B:DC:22:DE:1F:56:E3:8E:76:94:83:98:D2:DF:6A:A0:99
|
||||
|
||||
# Convert to lowercase without colons (AllowedAPKSigningKeys format):
|
||||
# 0c259d94e0ffea5d6319614b229d4b6bdc22de1f56e38e769483 98d2df6aa099
|
||||
```
|
||||
|
||||
Update `distribution/fdroid-submission/metadata.yml`:
|
||||
- Replace `<SIGNING_KEY_SHA256_FINGERPRINT_LOWERCASE_NO_COLONS>` with the fingerprint
|
||||
- Replace `<FIRST_GITHUB_RELEASE_TAG>` with the actual tag (e.g. `v0.2.4`)
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Fork fdroiddata
|
||||
|
||||
```bash
|
||||
# On GitLab
|
||||
# 1. Go to https://gitlab.com/fdroid/fdroiddata
|
||||
# 2. Fork to your personal GitLab account (not org — fdroid prefers personal forks)
|
||||
# 3. Clone locally:
|
||||
git clone https://gitlab.com/<YOUR_GITLAB_USERNAME>/fdroiddata.git
|
||||
cd fdroiddata
|
||||
git remote add upstream https://gitlab.com/fdroid/fdroiddata.git
|
||||
git fetch upstream
|
||||
git checkout -b add-ai.opencode.mobile upstream/master
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 3 — Add the metadata file
|
||||
|
||||
```bash
|
||||
cp /path/to/opencode-mobile/distribution/fdroid-submission/metadata.yml \
|
||||
metadata/ai.opencode.mobile.yml
|
||||
```
|
||||
|
||||
Verify:
|
||||
- `metadata/ai.opencode.mobile.yml` exists
|
||||
- `AllowedAPKSigningKeys` has the correct lowercase-no-colons fingerprint
|
||||
- `commit:` points to a real tag in the GitHub repo
|
||||
- `versionCode` and `versionName` match the APK attached to the release
|
||||
|
||||
---
|
||||
|
||||
## Step 4 — Test the build locally (optional but strongly recommended)
|
||||
|
||||
F-Droid provides a Docker-based build environment:
|
||||
|
||||
```bash
|
||||
# Install fdroidserver
|
||||
pip install fdroidserver
|
||||
|
||||
# Verify metadata parses cleanly
|
||||
fdroid readmeta
|
||||
|
||||
# Attempt a build (requires Docker + significant time)
|
||||
fdroid build ai.opencode.mobile:<versionCode>
|
||||
```
|
||||
|
||||
If the build fails, fix `metadata/ai.opencode.mobile.yml` before filing the MR.
|
||||
|
||||
---
|
||||
|
||||
## Step 5 — File the Merge Request
|
||||
|
||||
```bash
|
||||
git add metadata/ai.opencode.mobile.yml
|
||||
git commit -m "Add ai.opencode.mobile (OpenCode Mobile)"
|
||||
git push origin add-ai.opencode.mobile
|
||||
```
|
||||
|
||||
Go to https://gitlab.com/<YOUR_GITLAB_USERNAME>/fdroiddata → open an MR
|
||||
against `fdroid/fdroiddata:master`.
|
||||
|
||||
MR title: `Add ai.opencode.mobile`
|
||||
|
||||
MR description template:
|
||||
```
|
||||
## New app: OpenCode Mobile
|
||||
|
||||
**Package:** ai.opencode.mobile
|
||||
**License:** MIT
|
||||
**Category:** Development
|
||||
**Source:** https://github.com/dzianisv/opencode-mobile
|
||||
|
||||
OpenCode Mobile is a free, open-source mobile client for the opencode AI
|
||||
coding agent (sst/opencode). MIT licensed. Crash reporting opt-in default OFF.
|
||||
|
||||
Anti-features: NonFreeNet (user-self-hosted backend may connect to proprietary AI APIs).
|
||||
|
||||
Using AllowedAPKSigningKeys path — pre-signed APK from GitHub releases.
|
||||
Build steps: npm install → expo prebuild → Gradle assembleRelease.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 6 — Respond to reviewer feedback
|
||||
|
||||
- F-Droid maintainers typically review within 2–8 weeks.
|
||||
- Monitor the MR for comments. Common asks:
|
||||
- Build reproducibility evidence
|
||||
- Clarification on anti-features
|
||||
- Pinning build dependencies to exact versions
|
||||
- Removing or stubbing FCM/GMS dependencies
|
||||
|
||||
---
|
||||
|
||||
## Step 7 — After acceptance
|
||||
|
||||
- F-Droid builds from source on their CI. First index update may take 1–2 weeks.
|
||||
- Add `ai.opencode.mobile` to F-Droid's inclusion notice in `docs/fdroid.md`.
|
||||
- Update `distribution/strategy.md` status row for F-Droid.
|
||||
- Notify IzzyOnDroid via the inclusion issue that mainline accepted the app
|
||||
(IzzyOnDroid will then auto-delist within their next index rebuild).
|
||||
|
||||
---
|
||||
|
||||
## Reference
|
||||
|
||||
- F-Droid inclusion criteria: https://f-droid.org/en/docs/Inclusion_Policy/
|
||||
- F-Droid metadata format: https://f-droid.org/en/docs/Build_Metadata_Reference/
|
||||
- AllowedAPKSigningKeys: https://f-droid.org/en/docs/Reproducible_Builds/
|
||||
- fdroiddata: https://gitlab.com/fdroid/fdroiddata
|
||||
107
distribution/fdroid-submission/metadata.yml
Normal file
@@ -0,0 +1,107 @@
|
||||
# F-Droid metadata for ai.opencode.mobile
|
||||
# Target file path in fdroiddata: metadata/ai.opencode.mobile.yml
|
||||
#
|
||||
# BEFORE FILING THE MR:
|
||||
# 1. Replace <SIGNING_KEY_SHA256_FINGERPRINT> with the actual colon-separated hex fingerprint
|
||||
# (found in distribution/SIGNING-KEY-FINGERPRINTS.md)
|
||||
# 2. Replace <FIRST_GITHUB_RELEASE_TAG> with the first tag that has a signed APK attached
|
||||
# (e.g. v0.2.4)
|
||||
# 3. Verify the build steps produce a matching APK against AllowedAPKSigningKeys
|
||||
#
|
||||
# Do NOT file this MR until:
|
||||
# - First Google Play release is live (establishes signing key in production use)
|
||||
# - Sentry opt-in gate is merged to main (anti-feature Tracking avoided)
|
||||
# - You have the exact colon-separated SHA-256 key fingerprint confirmed
|
||||
|
||||
Categories:
|
||||
- Development
|
||||
|
||||
License: MIT
|
||||
|
||||
AuthorName: VIBE TECHNOLOGIES, LLC
|
||||
AuthorEmail: support@vibebrowser.app
|
||||
|
||||
WebSite: https://opencode.vibebrowser.app
|
||||
SourceCode: https://github.com/dzianisv/opencode-mobile
|
||||
IssueTracker: https://github.com/dzianisv/opencode-mobile/issues
|
||||
Changelog: https://github.com/dzianisv/opencode-mobile/releases
|
||||
|
||||
Summary: Drive your self-hosted AI coding agent from your phone
|
||||
|
||||
Description: |-
|
||||
OpenCode Mobile is a free, open-source (MIT) client for the opencode AI coding
|
||||
agent (sst/opencode). Connect to your self-hosted opencode server and drive
|
||||
AI-powered coding sessions from your phone — no proprietary backend, no mandatory
|
||||
accounts, no tracking by default.
|
||||
|
||||
'''Key features'''
|
||||
|
||||
* Multiple connection types — local network, secure tunnels (Cloudflare, ngrok),
|
||||
or any self-hosted opencode instance
|
||||
* Biometric unlock — fingerprint / PIN to keep your sessions private
|
||||
* Real-time streaming chat — watch your AI agent think and respond live
|
||||
* File diff viewer — see exactly what code changes the agent proposes
|
||||
* Multi-session management — start, resume, and switch between coding sessions
|
||||
* Tool call approval — review and approve agent actions before they run
|
||||
|
||||
'''Self-hosted first'''
|
||||
|
||||
The app requires your own opencode server. Your AI traffic goes directly to your
|
||||
backend (Claude, GPT, Gemini, local models — your choice). No middleman required.
|
||||
|
||||
An optional hosted backend ("opencode Cloud") is planned as a future paid service,
|
||||
but the client is always free and fully functional without it.
|
||||
|
||||
'''Crash reporting'''
|
||||
|
||||
Sentry crash reporting is opt-in with default OFF. Code, prompts, and AI responses
|
||||
never leave your own server.
|
||||
|
||||
'''Anti-features'''
|
||||
|
||||
NonFreeNet: the opencode server you connect to may connect to proprietary AI
|
||||
services (OpenAI, Anthropic, Google). The app itself contains no proprietary
|
||||
network code.
|
||||
|
||||
AntiFeatures:
|
||||
NonFreeNet:
|
||||
en-US: >-
|
||||
The app connects to a user-self-hosted opencode server which may in turn
|
||||
connect to proprietary AI services (OpenAI API, Anthropic API, Google Gemini).
|
||||
The app itself is fully open source and does not require any specific provider.
|
||||
|
||||
RepoType: git
|
||||
Repo: https://github.com/dzianisv/opencode-mobile
|
||||
|
||||
Builds:
|
||||
- versionName: '1.0.0'
|
||||
versionCode: 1
|
||||
commit: <FIRST_GITHUB_RELEASE_TAG>
|
||||
subdir: android
|
||||
sudo:
|
||||
- apt-get update
|
||||
- apt-get install -y nodejs npm
|
||||
init:
|
||||
- npm install --prefix .. --legacy-peer-deps
|
||||
- npx --prefix .. expo prebuild --platform android --non-interactive
|
||||
gradle:
|
||||
- release
|
||||
ndk: 26.1.10909125
|
||||
# Node 20+ required for Expo SDK 52+
|
||||
# prebuild regenerates android/ from app.json + package.json
|
||||
# The signed AAB/APK is then compared against AllowedAPKSigningKeys
|
||||
prebuild:
|
||||
- cd .. && npm install --legacy-peer-deps
|
||||
- cd .. && npx expo prebuild --platform android --non-interactive
|
||||
|
||||
# AllowedAPKSigningKeys pins our release signing key.
|
||||
# F-Droid will serve our pre-signed APK rather than re-signing with their key.
|
||||
# This requires reproducible builds (identical output across machines).
|
||||
AllowedAPKSigningKeys: <SIGNING_KEY_SHA256_FINGERPRINT_LOWERCASE_NO_COLONS>
|
||||
|
||||
AutoUpdateMode: Version v%v
|
||||
UpdateCheckMode: Tags
|
||||
UpdateCheckData: https://raw.githubusercontent.com/dzianisv/opencode-mobile/main/app.json|"version":\s*"([^"]+)"|.|.
|
||||
|
||||
CurrentVersion: '1.0.0'
|
||||
CurrentVersionCode: 1
|
||||
173
distribution/ios-enrollment-runbook.md
Normal file
@@ -0,0 +1,173 @@
|
||||
# Apple Developer Program Enrollment Runbook — VIBE TECHNOLOGIES, LLC
|
||||
|
||||
Pre-filled guide for enrolling as an organization. Do NOT proceed until you have $99 USD available on the Apple ID credit card, and you have the governor's contact information ready for the verification call.
|
||||
|
||||
Reference: https://developer.apple.com/programs/enroll/
|
||||
|
||||
---
|
||||
|
||||
## Pre-filled Entity Data
|
||||
|
||||
| Field | Value | Source |
|
||||
|---|---|---|
|
||||
| Legal Entity Name | VIBE TECHNOLOGIES, LLC | WA LLC registration |
|
||||
| D-U-N-S Number | **142059652** | Already retrieved |
|
||||
| UBI / EIN | 606 003 933 | WA Secretary of State |
|
||||
| Legal Address | 519 S Henderson St, Seattle WA 98108-4522, USA | WA LLC registration |
|
||||
| Legal Entity Type | LLC (Limited Liability Company) | |
|
||||
| Primary Contact / Authorized Agent | Dzianis Vashchuk | Governor of LLC |
|
||||
| Primary Contact Phone | (use a number reachable for Apple's verification call) | |
|
||||
| Primary Contact Email | support@vibebrowser.app | |
|
||||
| Website | https://www.vibebrowser.app/ | |
|
||||
|
||||
---
|
||||
|
||||
## Apple ID to Use
|
||||
|
||||
**DECIDED 2026-05-24**: Use `support@vibebrowser.app` — mailbox already exists (was verified during Play Console signup, confirmed receiving Google verification codes).
|
||||
|
||||
- If no Apple ID exists yet for `support@vibebrowser.app`: create at https://appleid.apple.com/account
|
||||
- 2FA: enable immediately, trusted phone = Dzianis's mobile
|
||||
- Bitwarden item naming: store as `APPLE_ID_VIBE_TECHNOLOGIES` (email + password + 2FA recovery codes)
|
||||
|
||||
---
|
||||
|
||||
## Step-by-Step Enrollment
|
||||
|
||||
### Step 1 — Prepare
|
||||
|
||||
- [ ] Verify D-U-N-S 142059652 is current at https://developer.apple.com/enroll/duns-lookup/ (takes 14 days to propagate if recently created; ours was already retrieved so should be active)
|
||||
- [ ] Ensure the Apple ID email mailbox is active and accessible
|
||||
- [ ] Have a credit card ready ($99 USD charge)
|
||||
- [ ] Ensure Dzianis Vashchuk is available for a verification phone call during business hours (Apple calls the listed phone number for organization enrollment)
|
||||
|
||||
### Step 2 — Start Enrollment
|
||||
|
||||
1. Go to https://developer.apple.com/programs/enroll/
|
||||
2. Click **Start Your Enrollment**
|
||||
3. Sign in with the Apple ID you chose above (or create one)
|
||||
4. Select **Company / Organization**
|
||||
|
||||
### Step 3 — Enter Organization Details
|
||||
|
||||
Fill as follows:
|
||||
|
||||
| Prompt | Enter |
|
||||
|---|---|
|
||||
| Legal Entity Name | `VIBE TECHNOLOGIES, LLC` |
|
||||
| D-U-N-S Number | `142059652` |
|
||||
| Headquarters Address (Line 1) | `519 S Henderson St` |
|
||||
| City | `Seattle` |
|
||||
| State | `WA` |
|
||||
| ZIP | `98108` |
|
||||
| Country | `United States` |
|
||||
| Phone | (Dzianis's direct mobile — Apple calls this) |
|
||||
| Website | `https://www.vibebrowser.app/` |
|
||||
|
||||
### Step 4 — Verify Your Authority
|
||||
|
||||
Apple asks you to confirm you are authorized to bind the organization to the Apple Developer Program Agreement. As governor/managing member of the LLC, Dzianis Vashchuk has this authority.
|
||||
|
||||
Select: "I am authorized to sign legal agreements on behalf of this organization."
|
||||
|
||||
### Step 5 — Apple Review & Verification Call
|
||||
|
||||
- Apple's team will verify the D-U-N-S number against Dun & Bradstreet records.
|
||||
- **Expect a phone call** to the number entered above within 2–5 business days.
|
||||
- The caller will confirm the legal entity name, address, and that you are authorized to enroll.
|
||||
- Answer in English; have the LLC registration handy (UBI 606 003 933) in case they ask for additional verification.
|
||||
|
||||
**Timeline**: 2–7 business days for verification. Apple can take up to 14 days in edge cases.
|
||||
|
||||
### Step 6 — Pay
|
||||
|
||||
After verification is approved:
|
||||
- Apple charges **$99 USD/year** to the credit card on the Apple ID.
|
||||
- Enrollment renews annually. Set a calendar reminder.
|
||||
- Source: https://developer.apple.com/support/enrollment/ (pricing as of 2025 — verify current pricing at enrollment time)
|
||||
|
||||
### Step 7 — Accept Agreements
|
||||
|
||||
After payment:
|
||||
1. Sign in to https://developer.apple.com/account/
|
||||
2. Accept the Apple Developer Program License Agreement
|
||||
3. Accept the Paid Applications Agreement (required to distribute free apps too)
|
||||
|
||||
### Step 8 — Set Up App Store Connect
|
||||
|
||||
1. Go to https://appstoreconnect.apple.com/
|
||||
2. Sign in with the same Apple ID
|
||||
3. Fill in banking info even for a free app (required to publish):
|
||||
- US bank account (ACH/routing number)
|
||||
- Tax information (W-9 for US entities — EIN 606 003 933)
|
||||
4. Accept the Paid Applications Schedule even if distributing free (Apple requires this)
|
||||
|
||||
---
|
||||
|
||||
## What Can Be Done in Parallel (Before Enrollment Approval)
|
||||
|
||||
While waiting for Apple's verification call and approval:
|
||||
|
||||
- [x] Prepare App Store listing copy → `distribution/app-store-listing.md`
|
||||
- [x] Write CI workflow (draft) → `.github/workflows/publish-app-store.yml`
|
||||
- [ ] Create app icon 1024×1024 PNG
|
||||
- [ ] Capture iPhone screenshots (use iOS Simulator in Xcode on any Mac)
|
||||
- [ ] Capture iPad screenshots
|
||||
- [ ] Write/publish privacy policy at https://www.vibebrowser.app/opencode-mobile/privacy
|
||||
- [ ] Set up EAS account at https://expo.dev/ (free tier, log in with Expo account)
|
||||
- [ ] Add iOS config patches to `app.json` (done in this PR)
|
||||
- [ ] Run `npx expo prebuild --platform ios` on a Mac to validate the Xcode project
|
||||
- [ ] Prepare the Mac build worker (macbook13-pro at 100.68.120.26) as GitHub self-hosted runner
|
||||
|
||||
---
|
||||
|
||||
## After Enrollment Approval — App Store Connect Setup
|
||||
|
||||
1. Create a new App in App Store Connect:
|
||||
- Platform: iOS
|
||||
- Name: `OpenCode`
|
||||
- Primary Language: English (U.S.)
|
||||
- Bundle ID: `ai.opencode.mobile` — register this explicit App ID first at https://developer.apple.com/account/resources/identifiers/
|
||||
- SKU: `ai.opencode.mobile` (can match bundle ID)
|
||||
|
||||
2. Configure App ID capabilities needed:
|
||||
- Push Notifications (for `expo-notifications`)
|
||||
- Associated Domains (if deep linking via `opencode://` is used externally — not strictly needed for current app)
|
||||
|
||||
3. Create App Store Connect API Key for CI:
|
||||
- Go to https://appstoreconnect.apple.com/access/api
|
||||
- Create key with **App Manager** role
|
||||
- Download the `.p8` file (can only be downloaded once!)
|
||||
- Note: Key ID and Issuer ID
|
||||
- Base64-encode the .p8 and store in GitHub secret `APPLE_APP_STORE_CONNECT_API_KEY`
|
||||
|
||||
4. Create an internal TestFlight group and add yourself as tester
|
||||
|
||||
---
|
||||
|
||||
## Cost Summary
|
||||
|
||||
| Item | Cost | Frequency |
|
||||
|---|---|---|
|
||||
| Apple Developer Program | $99 USD | Per year (auto-renews) |
|
||||
| EAS Build (free tier) | $0 | Up to 30 builds/month for iOS |
|
||||
| EAS Build (production tier) | $19/month | Unlimited builds, priority queue |
|
||||
| macOS GitHub runner | ~$0.08/min × ~25 min/build ≈ $2/build | Per build |
|
||||
|
||||
**Recommendation**: Start with EAS Build free tier for first few releases. Upgrade to production tier ($19/month) if build queue times become a problem. See task 2 analysis in the final report.
|
||||
|
||||
---
|
||||
|
||||
## Timeline Estimate
|
||||
|
||||
| Milestone | Estimated Time from Starting Enrollment |
|
||||
|---|---|
|
||||
| Enrollment form submitted | Day 0 |
|
||||
| Apple verification call | Day 2–5 |
|
||||
| Enrollment approved + payment | Day 3–7 |
|
||||
| Banking/tax info set up | Day 7–8 |
|
||||
| App ID + provisioning profile created | Day 8 |
|
||||
| First TestFlight build submitted via CI | Day 9–10 |
|
||||
| TestFlight internal testers can install | Day 9–10 (no review for internal) |
|
||||
| App Store production submission | Day 10–12 |
|
||||
| Apple review complete + production live | Day 12–14 (review typically 24–48 hours) |
|
||||
110
distribution/izzyondroid-submission/INCLUSION-REQUEST.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# IzzyOnDroid Inclusion Request
|
||||
|
||||
**File this as a new issue at: https://codeberg.org/IzzyOnDroid/repodata/issues**
|
||||
|
||||
Use the text below (starting from "---") as the issue body. Replace all
|
||||
`<PLACEHOLDER>` values before filing.
|
||||
|
||||
---
|
||||
|
||||
## App submission: OpenCode Mobile
|
||||
|
||||
**App name:** OpenCode Mobile
|
||||
**Package / App ID:** `ai.opencode.mobile`
|
||||
**License:** MIT
|
||||
**Source code:** https://github.com/dzianisv/opencode-mobile
|
||||
**GitHub releases:** https://github.com/dzianisv/opencode-mobile/releases
|
||||
|
||||
---
|
||||
|
||||
### Release URL pattern
|
||||
|
||||
APK attached to each GitHub release tag following the pattern:
|
||||
|
||||
```
|
||||
https://github.com/dzianisv/opencode-mobile/releases/download/<TAG>/app-release.apk
|
||||
```
|
||||
|
||||
Example (first production release):
|
||||
```
|
||||
https://github.com/dzianisv/opencode-mobile/releases/download/v0.2.4/app-release.apk
|
||||
```
|
||||
|
||||
Note: IzzyOnDroid should use the GitHub releases tag pattern for auto-update
|
||||
polling. The app uses semantic versioning (`vX.Y.Z` tags).
|
||||
|
||||
---
|
||||
|
||||
### Signing key SHA-256 fingerprint
|
||||
|
||||
```
|
||||
0C:25:9D:94:E0:FF:EA:5D:63:19:61:4B:22:9D:4B:6B:DC:22:DE:1F:56:E3:8E:76:94:83:98:D2:DF:6A:A0:99
|
||||
```
|
||||
|
||||
This is the production-release.jks key used for all distribution channels
|
||||
(Play Store, F-Droid, and IzzyOnDroid use the same signing key — users can
|
||||
update in-place across stores).
|
||||
|
||||
---
|
||||
|
||||
### Description
|
||||
|
||||
OpenCode Mobile is a free, open-source (MIT) client for the opencode AI coding
|
||||
agent (sst/opencode). It lets developers connect to their self-hosted opencode
|
||||
server and drive AI-powered coding sessions from their Android phone.
|
||||
|
||||
Key features include streaming chat with the AI agent, a file diff viewer for
|
||||
reviewing proposed code changes before accepting them, multi-session management,
|
||||
tool call approval gates, and biometric unlock. All AI traffic flows directly
|
||||
from the app to the user's own server — no middleman, no mandatory accounts, no
|
||||
vendor lock-in. Crash reporting via Sentry is opt-in with default OFF.
|
||||
|
||||
The app is aimed at developers who run opencode on a workstation or self-hosted
|
||||
server (on-prem or any cloud VPS) and want a mobile companion for on-the-go
|
||||
session management. An optional hosted "opencode Cloud" backend is planned as
|
||||
a future paid service, but the client is and will remain free and open source.
|
||||
|
||||
---
|
||||
|
||||
### Anti-features acknowledgment
|
||||
|
||||
**NonFreeNet** applies: the app connects to a user-self-hosted opencode server
|
||||
which may in turn connect to proprietary AI APIs (OpenAI, Anthropic, Google
|
||||
Gemini). The app itself contains no proprietary network code and does not
|
||||
require any specific provider.
|
||||
|
||||
No other anti-features apply:
|
||||
- No ads (no ad SDK present)
|
||||
- No tracking (Sentry opt-in, default OFF — user must explicitly enable)
|
||||
- No non-free dependencies beyond FCM receiver classes compiled in by
|
||||
`expo-notifications` (local-only usage; `scheduleNotificationAsync` only;
|
||||
no `getExpoPushTokenAsync` / `getDevicePushTokenAsync` calls)
|
||||
- No subscription / license checks in the client
|
||||
|
||||
---
|
||||
|
||||
### FOSS confirmation
|
||||
|
||||
- Source code is fully public: https://github.com/dzianisv/opencode-mobile
|
||||
- License: MIT (https://github.com/dzianisv/opencode-mobile/blob/main/LICENSE
|
||||
or inferred from package.json `"license": "MIT"`)
|
||||
- No proprietary SDKs are required for core functionality
|
||||
- The APK on GitHub releases is signed with the key fingerprint above
|
||||
|
||||
---
|
||||
|
||||
### F-Droid mainline track note
|
||||
|
||||
A parallel F-Droid mainline submission (MR against fdroiddata) will be filed
|
||||
after the first Play Store release is live. Per IzzyOnDroid policy, IzzyOnDroid
|
||||
will auto-delist this app once the mainline F-Droid repository accepts it.
|
||||
We will notify the IzzyOnDroid team via this issue when that happens.
|
||||
|
||||
---
|
||||
|
||||
### Contact
|
||||
|
||||
Developer: VIBE TECHNOLOGIES, LLC
|
||||
Email: support@vibebrowser.app
|
||||
Website: https://opencode.vibebrowser.app
|
||||
Privacy policy: https://opencode.vibebrowser.app/privacy
|
||||
112
distribution/izzyondroid-submission/SUBMISSION-CHECKLIST.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# IzzyOnDroid Submission Checklist
|
||||
|
||||
Step-by-step guide to file the inclusion request for `ai.opencode.mobile`
|
||||
in the IzzyOnDroid F-Droid repository.
|
||||
|
||||
IzzyOnDroid is the fastest OSS distribution channel — typical inclusion is
|
||||
1–3 days after filing. It serves pre-built APKs directly from GitHub releases.
|
||||
|
||||
**Pre-condition: a signed APK must be attached to a GitHub release tag before filing.**
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [ ] Signed release APK (not AAB) exists and is attached to a GitHub tag
|
||||
(e.g. `https://github.com/dzianisv/opencode-mobile/releases/tag/v0.2.4`)
|
||||
- [ ] APK is signed with `keystores/production-release.jks`
|
||||
- [ ] SHA-256 fingerprint confirmed: see `distribution/SIGNING-KEY-FINGERPRINTS.md`
|
||||
- [ ] Sentry opt-in gate is in production (avoids `Tracking` anti-feature escalation)
|
||||
- [ ] Privacy policy is live at `https://opencode.vibebrowser.app/privacy`
|
||||
- [ ] `app.json` `version` and `android.versionCode` are set correctly in the tagged commit
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Attach APK to GitHub release
|
||||
|
||||
The CI workflow currently builds an AAB (for Play Store). For IzzyOnDroid and
|
||||
F-Droid you need a **universal APK**.
|
||||
|
||||
Option A — Convert AAB to universal APK with bundletool:
|
||||
```bash
|
||||
bundletool build-apks \
|
||||
--bundle=android/app/build/outputs/bundle/release/app-release.aab \
|
||||
--output=apks.apks \
|
||||
--mode=universal \
|
||||
--ks=keystores/production-release.jks \
|
||||
--ks-pass=pass:<STOREPASS> \
|
||||
--ks-key-alias=<KEY_ALIAS> \
|
||||
--key-pass=pass:<KEY_PASS>
|
||||
|
||||
unzip apks.apks universal.apk -d apk-out/
|
||||
# Resulting APK: apk-out/universal.apk → rename to app-release.apk
|
||||
```
|
||||
|
||||
Option B — Add `assembleRelease` to CI alongside `bundleRelease`, then
|
||||
attach the resulting `app-release.apk` to the GitHub release artifact.
|
||||
|
||||
Whichever option, attach the `.apk` file to the GitHub release created by the
|
||||
release tag.
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Verify the APK
|
||||
|
||||
```bash
|
||||
# Confirm package id and version
|
||||
aapt dump badging apk-out/universal.apk | grep -E "package:|versionCode|versionName"
|
||||
# Expected:
|
||||
# package: name='ai.opencode.mobile' versionCode='<N>' versionName='<X.Y.Z>'
|
||||
|
||||
# Confirm signing fingerprint matches distribution/SIGNING-KEY-FINGERPRINTS.md
|
||||
apksigner verify --print-certs apk-out/universal.apk | grep SHA-256
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 3 — Create a Codeberg account (if needed)
|
||||
|
||||
IzzyOnDroid issues are hosted on Codeberg (not GitHub).
|
||||
URL: https://codeberg.org
|
||||
|
||||
Register or log in at https://codeberg.org/user/sign_up
|
||||
|
||||
---
|
||||
|
||||
## Step 4 — File the inclusion issue
|
||||
|
||||
1. Go to https://codeberg.org/IzzyOnDroid/repodata/issues
|
||||
2. Click "New Issue"
|
||||
3. Title: `Include ai.opencode.mobile (OpenCode Mobile)`
|
||||
4. Body: paste the content from `distribution/izzyondroid-submission/INCLUSION-REQUEST.md`
|
||||
(with all `<PLACEHOLDER>` values replaced)
|
||||
5. Submit
|
||||
|
||||
---
|
||||
|
||||
## Step 5 — Monitor and respond
|
||||
|
||||
- IzzyOnDroid maintainers typically respond within 1–3 days.
|
||||
- Common requests:
|
||||
- Confirm APK URL pattern resolves correctly
|
||||
- Confirm signing key fingerprint via `apksigner` output
|
||||
- Clarification on anti-features
|
||||
- Once accepted, the app appears in the next IzzyOnDroid index build (usually within 24h).
|
||||
|
||||
---
|
||||
|
||||
## Step 6 — After acceptance
|
||||
|
||||
- Add the IzzyOnDroid badge to `README.md`
|
||||
- Update `distribution/strategy.md` status row for IzzyOnDroid to "live"
|
||||
- Update `docs/fdroid.md` with inclusion date
|
||||
- When mainline F-Droid MR is later accepted, comment on this issue:
|
||||
"Mainline F-Droid has accepted ai.opencode.mobile — please auto-delist per policy."
|
||||
|
||||
---
|
||||
|
||||
## Reference
|
||||
|
||||
- IzzyOnDroid inclusion policy: https://apt.izzysoft.de/fdroid/index/info
|
||||
- Codeberg issues: https://codeberg.org/IzzyOnDroid/repodata/issues
|
||||
- IzzyOnDroid repo (for badge URLs): https://apt.izzysoft.de/fdroid/index/apk/ai.opencode.mobile
|
||||
BIN
distribution/play-graphics/feature-graphic.png
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
distribution/play-graphics/icon-512.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
distribution/play-graphics/phone-01.png
Normal file
|
After Width: | Height: | Size: 134 KiB |
BIN
distribution/play-graphics/phone-02.png
Normal file
|
After Width: | Height: | Size: 140 KiB |
BIN
distribution/play-graphics/phone-03.png
Normal file
|
After Width: | Height: | Size: 151 KiB |
260
distribution/play-listing.md
Normal file
@@ -0,0 +1,260 @@
|
||||
# Google Play Store Listing — OpenCode
|
||||
|
||||
Copy-paste reference for completing the Play Console store listing for `ai.opencode.mobile`.
|
||||
Once identity verification is approved and the app is created, paste these values directly.
|
||||
|
||||
---
|
||||
|
||||
## Main store listing
|
||||
|
||||
### App name (max 30 chars)
|
||||
|
||||
```
|
||||
OpenCode
|
||||
```
|
||||
(8 chars — well under limit)
|
||||
|
||||
### Short description (max 80 chars)
|
||||
|
||||
```
|
||||
Drive your self-hosted AI coding agent from your phone.
|
||||
```
|
||||
(55 chars)
|
||||
|
||||
### Full description (max 4000 chars)
|
||||
|
||||
```
|
||||
OpenCode is an open-source mobile client for the opencode AI coding agent (sst/opencode). Connect to your self-hosted opencode server and drive AI-powered coding sessions from your phone.
|
||||
|
||||
KEY FEATURES
|
||||
|
||||
• Multiple connection types — local network, secure tunnels (Cloudflare, ngrok), or cloud-hosted opencode instances
|
||||
• Biometric unlock — Face ID / Touch ID / fingerprint to keep your sessions private
|
||||
• Real-time streaming chat — watch your AI agent think and respond live
|
||||
• File diff viewer — see exactly what code changes the agent is making before you accept
|
||||
• Multi-session management — start, resume, and switch between coding sessions
|
||||
• Tool call approval — review and approve actions before the agent runs them on your code
|
||||
|
||||
WHO IT'S FOR
|
||||
|
||||
• Developers who run opencode (sst/opencode) on their workstation or a server
|
||||
• Engineers who want to check in on long-running coding sessions away from their desk
|
||||
• Teams who self-host AI dev tools and want a polished mobile companion
|
||||
|
||||
WHAT IT IS NOT
|
||||
|
||||
• Not an AI model — you bring your own opencode server (which connects to Claude, GPT, Gemini, or local models)
|
||||
• Not a code editor — pair with your existing IDE/terminal workflow
|
||||
• Not a Google Play exclusive — also available on F-Droid (open source build)
|
||||
|
||||
OPEN SOURCE
|
||||
|
||||
OpenCode Mobile is MIT licensed. Source code, issue tracker, and community discussion:
|
||||
https://github.com/dzianisv/opencode-mobile
|
||||
|
||||
PRIVACY
|
||||
|
||||
OpenCode Mobile does not collect your code or prompts. All AI traffic goes directly from the app to YOUR opencode server. We use Sentry for crash reporting only (no PII, no message content). See our privacy policy for details.
|
||||
|
||||
SUPPORT
|
||||
|
||||
Email: support@vibebrowser.app
|
||||
Issues: https://github.com/dzianisv/opencode-mobile/issues
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Graphic assets — REQUIRED before publishing
|
||||
|
||||
| Asset | Spec | Status | Notes |
|
||||
|---|---|---|---|
|
||||
| App icon | 512×512 PNG, 32-bit, ≤1 MB | ❌ placeholder | `assets/icon.json` is `{"placeholder":true}` — need real PNG |
|
||||
| Adaptive icon (foreground) | 432×432 PNG, transparent bg | ❌ placeholder | `assets/adaptive-icon.json` placeholder |
|
||||
| Feature graphic | 1024×500 PNG/JPG | ❌ missing | For Play Store top banner — required for publishing |
|
||||
| Phone screenshots | 2–8 images, 16:9 or 9:16, 1080×1920 or similar | ❌ missing | Required min 2 |
|
||||
| 7-inch tablet screenshots | optional | ❌ missing | Recommended |
|
||||
| 10-inch tablet screenshots | optional | ❌ missing | Recommended |
|
||||
| Promo video | YouTube URL, optional | ⏸ skip for first release | |
|
||||
|
||||
**Action**: design + generate assets. Suggested tools:
|
||||
- Icon: Figma → 1024×1024 → export to `assets/icon.png` + run `npx expo prebuild` to fan out per-density.
|
||||
- Screenshots: run app on emulator, capture via Android Studio screenshot, or use Fastlane's snapshot tooling.
|
||||
|
||||
---
|
||||
|
||||
## Categorization
|
||||
|
||||
| Field | Value | Notes |
|
||||
|---|---|---|
|
||||
| App or game | App | |
|
||||
| Category | Tools | Best fit. Alternatives: Productivity, Communication. |
|
||||
| Tags | productivity, developer, ai, coding | Free tags. |
|
||||
| Email | support@vibebrowser.app | Already verified during signup. |
|
||||
| Phone | +1 360-504-8967 | Optional public; matches developer profile. |
|
||||
| Website | https://www.vibebrowser.app/ | Already verified. |
|
||||
|
||||
---
|
||||
|
||||
## Privacy policy URL
|
||||
|
||||
**Required.** Must be a public URL.
|
||||
|
||||
Suggested path: `https://opencode.vibebrowser.app/privacy`
|
||||
|
||||
Privacy policy must cover (per Google requirements):
|
||||
- What data is collected (Sentry crash diagnostics: device model, OS version, stack trace; nothing user-content)
|
||||
- How data is used (debugging crashes only)
|
||||
- Third-party SDKs (Sentry — link to https://sentry.io/privacy/)
|
||||
- Data retention (Sentry default 90 days)
|
||||
- User rights (delete account, contact us)
|
||||
- Contact: support@vibebrowser.app
|
||||
|
||||
**Action**: write a privacy policy at the URL above. Template draft below.
|
||||
|
||||
```
|
||||
OpenCode Mobile Privacy Policy
|
||||
Effective: 2026-05-24
|
||||
Operator: VIBE TECHNOLOGIES, LLC, 519 S Henderson St, Seattle WA 98108-4522 USA
|
||||
|
||||
We do not collect your code, prompts, AI responses, server URLs, or chat history.
|
||||
|
||||
We collect (via Sentry SDK for crash reporting):
|
||||
- Device model, OS version, app version
|
||||
- Stack traces of crashes and unhandled errors
|
||||
- App breadcrumbs (function names, screen names — no message bodies)
|
||||
|
||||
Data is sent to Sentry (sentry.io) and retained per Sentry defaults (~90 days).
|
||||
|
||||
Third-party services:
|
||||
- Sentry — crash reporting. https://sentry.io/privacy/
|
||||
|
||||
Data sharing: none beyond Sentry.
|
||||
|
||||
User rights:
|
||||
- Email support@vibebrowser.app to request deletion of crash records associated with your device.
|
||||
|
||||
Contact: support@vibebrowser.app
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data safety form
|
||||
|
||||
Google requires this before publishing. Answers below for OpenCode Mobile current state.
|
||||
|
||||
| Question | Answer |
|
||||
|---|---|
|
||||
| Does your app collect or share any of the required user data types? | Yes |
|
||||
| Is all of the user data collected by your app encrypted in transit? | Yes (HTTPS to Sentry) |
|
||||
| Do you provide a way for users to request that their data is deleted? | Yes — via support@vibebrowser.app |
|
||||
|
||||
### Data types collected
|
||||
|
||||
| Data type | Collected? | Shared? | Optional? | Purpose | Encrypted in transit? |
|
||||
|---|---|---|---|---|---|
|
||||
| App crash logs (Diagnostics) | Yes | Yes (Sentry) | **Yes (opt-in, default OFF)** | App functionality, diagnostics | Yes |
|
||||
| App performance / interactions | No | – | – | – | – |
|
||||
| Device or other IDs | No | – | – | – | – |
|
||||
| Personal info (name, email, etc.) | No | – | – | – | – |
|
||||
| Financial info | No | – | – | – | – |
|
||||
| Health / fitness | No | – | – | – | – |
|
||||
| Messages | No | – | – | – | – |
|
||||
| Photos / videos | No | – | – | – | – |
|
||||
| Audio | No | – | – | – | – |
|
||||
| Files and docs | No | – | – | – | – |
|
||||
| Calendar | No | – | – | – | – |
|
||||
| Contacts | No | – | – | – | – |
|
||||
| App activity (searches, viewed content) | No | – | – | – | – |
|
||||
| Web browsing | No | – | – | – | – |
|
||||
| App info and performance (other than crash logs) | No | – | – | – | – |
|
||||
|
||||
---
|
||||
|
||||
## Content rating
|
||||
|
||||
Run the IARC questionnaire on Play Console. Expected outcome based on app content:
|
||||
|
||||
| Region | Expected rating |
|
||||
|---|---|
|
||||
| ESRB (US) | Everyone |
|
||||
| PEGI (EU) | 3 |
|
||||
| USK (Germany) | 0 |
|
||||
| Australia | G |
|
||||
|
||||
Questionnaire answers (all "No" since no violence / sexual / drugs / gambling / etc. content; app is a dev tool):
|
||||
- Does it contain violence? No
|
||||
- Does it contain sexual content? No
|
||||
- Does it contain crude humor? No
|
||||
- Does it use drugs/alcohol/tobacco? No
|
||||
- Does it contain gambling? No
|
||||
- Does it share user location? No
|
||||
- Does it share user-generated content? No (private user → user-self only)
|
||||
- Does it allow users to interact / chat? Yes (with their OWN backend, not other users)
|
||||
|
||||
If "interact with other users" is asked: answer NO — the chat is between the user and their own AI agent, not user-to-user.
|
||||
|
||||
---
|
||||
|
||||
## Target audience and content
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Target age group | 18+ (developer tool) |
|
||||
| Appeals to children? | No |
|
||||
| Ads? | No |
|
||||
| In-app purchases? | (TBD — see monetization decision) |
|
||||
|
||||
---
|
||||
|
||||
## App access
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| All functionality available without restrictions? | No — requires user to provide their own opencode server URL |
|
||||
| Provide test credentials? | Yes — provide Google reviewer with a temporary opencode server URL or instructions to spin one up |
|
||||
|
||||
Reviewer instructions (paste in App Access form):
|
||||
|
||||
```
|
||||
OpenCode Mobile requires the user to bring their own opencode server (https://opencode.ai). To review the app:
|
||||
|
||||
1. Install opencode on any machine: `npm install -g opencode-ai`
|
||||
2. Run `opencode serve` — prints a local URL like http://localhost:4096
|
||||
3. In the app, tap "Connect" → enter the URL → connect.
|
||||
4. Start a session, type a prompt, observe streaming response.
|
||||
|
||||
If reviewers cannot self-host, contact support@vibebrowser.app and we will provide a temporary hosted opencode endpoint for review.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Release notes / "What's new" per release
|
||||
|
||||
Already wired in CI: `distribution/whatsnew/whatsnew-en-US`.
|
||||
|
||||
Bump this file before tagging a release. Keep it under 500 chars. Per-language variants supported (`whatsnew-fr-FR`, `whatsnew-de-DE`, etc.).
|
||||
|
||||
---
|
||||
|
||||
## First release strategy
|
||||
|
||||
1. **Internal testing** track first (up to 100 testers, no review) — what CI is wired for.
|
||||
2. **Closed testing** — 14+ days, 12+ testers required for new org accounts before promoting to production (Google's 2023 policy).
|
||||
3. **Open testing** — optional intermediate step.
|
||||
4. **Production** — only after Closed testing requirements met.
|
||||
|
||||
CI currently publishes to `internal` track ✅ correct for first release.
|
||||
|
||||
---
|
||||
|
||||
## Pending before first publish
|
||||
|
||||
- [ ] Identity verification (governor ID upload, Google review days)
|
||||
- [ ] App icon (real PNG, not placeholder)
|
||||
- [ ] Adaptive icon (real PNG)
|
||||
- [ ] Feature graphic 1024×500
|
||||
- [ ] At least 2 phone screenshots
|
||||
- [ ] Privacy policy live at https://opencode.vibebrowser.app/privacy
|
||||
- [ ] Decide pricing model (see monetization research)
|
||||
- [ ] Run IARC content rating questionnaire (after app created)
|
||||
- [ ] Complete Data safety form (after app created)
|
||||
313
distribution/privacy-policy.html
Normal file
@@ -0,0 +1,313 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>OpenCode Mobile — Privacy Policy</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 1.7;
|
||||
color: #1a1a2e;
|
||||
background: #ffffff;
|
||||
padding: 24px 16px 64px;
|
||||
max-width: 760px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
header {
|
||||
border-bottom: 2px solid #3b82f6;
|
||||
padding-bottom: 20px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
header h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
header .meta {
|
||||
font-size: 14px;
|
||||
color: #64748b;
|
||||
}
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
margin-top: 36px;
|
||||
margin-bottom: 12px;
|
||||
border-left: 4px solid #3b82f6;
|
||||
padding-left: 12px;
|
||||
}
|
||||
p { margin-bottom: 14px; }
|
||||
ul {
|
||||
margin: 10px 0 14px 24px;
|
||||
}
|
||||
ul li { margin-bottom: 6px; }
|
||||
a { color: #3b82f6; text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
.highlight-box {
|
||||
background: #eff6ff;
|
||||
border: 1px solid #bfdbfe;
|
||||
border-radius: 8px;
|
||||
padding: 16px 20px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.highlight-box strong { color: #1e40af; }
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 14px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
th {
|
||||
background: #f1f5f9;
|
||||
text-align: left;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
font-weight: 600;
|
||||
}
|
||||
td {
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
vertical-align: top;
|
||||
}
|
||||
tr:nth-child(even) td { background: #f8fafc; }
|
||||
footer {
|
||||
margin-top: 48px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
font-size: 13px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<h1>OpenCode Mobile — Privacy Policy</h1>
|
||||
<p class="meta">
|
||||
Effective date: 2026-05-24 |
|
||||
Operator: VIBE TECHNOLOGIES, LLC |
|
||||
App: OpenCode Mobile (<code>ai.opencode.mobile</code>)
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div class="highlight-box">
|
||||
<strong>Summary:</strong> OpenCode Mobile does not collect your code, prompts, AI responses,
|
||||
server URLs, or any chat content. All AI traffic goes directly from the app to your own
|
||||
opencode server. We use Sentry only for anonymous crash diagnostics, and only with your
|
||||
consent.
|
||||
</div>
|
||||
|
||||
<h2>1. Who We Are</h2>
|
||||
<p>
|
||||
OpenCode Mobile is developed and distributed by <strong>VIBE TECHNOLOGIES, LLC</strong>,
|
||||
a Washington State limited liability company.<br>
|
||||
Address: 519 S Henderson St, Seattle, WA 98108-4522, USA<br>
|
||||
Contact: <a href="mailto:support@vibebrowser.app">support@vibebrowser.app</a>
|
||||
</p>
|
||||
<p>
|
||||
The app is open-source (MIT license). Source code:
|
||||
<a href="https://github.com/dzianisv/opencode-mobile">github.com/dzianisv/opencode-mobile</a>.
|
||||
</p>
|
||||
|
||||
<h2>2. Data We Do NOT Collect</h2>
|
||||
<p>
|
||||
We want to be explicit about what we never collect, transmit to our servers, or share with
|
||||
third parties:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Your code, files, or repository content</li>
|
||||
<li>Your prompts, chat messages, or AI responses</li>
|
||||
<li>Your opencode server URL, IP address, or hostname</li>
|
||||
<li>Authentication tokens, API keys, or credentials you enter</li>
|
||||
<li>Account information, email addresses, or names</li>
|
||||
<li>Location data</li>
|
||||
<li>Photos, microphone recordings, or camera data (unless you attach them to a message,
|
||||
in which case they go only to your own server)</li>
|
||||
<li>Contacts, calendar, or any other personal data</li>
|
||||
</ul>
|
||||
<p>
|
||||
All communication between the app and your AI coding agent travels directly between your
|
||||
device and your self-hosted opencode server. VIBE TECHNOLOGIES, LLC never sees this traffic.
|
||||
</p>
|
||||
|
||||
<h2>3. Data We Do Collect (Crash Diagnostics)</h2>
|
||||
<p>
|
||||
With your explicit consent (shown at first launch), we collect anonymous crash diagnostic
|
||||
data via <strong>Sentry</strong> to help us identify and fix bugs.
|
||||
</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Data type</th>
|
||||
<th>What is captured</th>
|
||||
<th>What is NOT captured</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Device info</td>
|
||||
<td>Device model (e.g. "Pixel 7"), OS version, screen resolution, app version</td>
|
||||
<td>Device serial number, IMEI, advertising ID</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Crash / error reports</td>
|
||||
<td>Stack traces, exception types and messages, source file names and line numbers</td>
|
||||
<td>Variable values at time of crash, no user data in scope</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Breadcrumbs</td>
|
||||
<td>Screen names and function call sequence leading to the crash (e.g., "navigated to session screen")</td>
|
||||
<td>Message bodies, server URLs, prompt text — all stripped before upload by our URL-scrubbing filter</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>App version and build</td>
|
||||
<td>Version string and build number</td>
|
||||
<td>—</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p>
|
||||
URL scrubbing: before any event is sent to Sentry, our code strips all server URLs,
|
||||
authentication tokens, and query parameters. Stack traces are checked for embedded URLs.
|
||||
No server hostname or port number ever leaves your device via Sentry.
|
||||
</p>
|
||||
|
||||
<h2>4. Consent and Control</h2>
|
||||
<p>
|
||||
Crash reporting is <strong>opt-in and off by default</strong>. The first time you launch
|
||||
the app you will see a consent prompt. You can change this at any time:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Open the app → <strong>Settings</strong> → <strong>Privacy</strong> →
|
||||
<strong>Crash reporting</strong> toggle.</li>
|
||||
<li>When the toggle is off, Sentry is never initialised and no data leaves your device.</li>
|
||||
</ul>
|
||||
|
||||
<h2>5. Third-Party Services</h2>
|
||||
<p>
|
||||
We use one third-party service for diagnostics:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Sentry</strong> — crash and error monitoring.<br>
|
||||
Privacy policy: <a href="https://sentry.io/privacy/" target="_blank" rel="noopener">sentry.io/privacy</a><br>
|
||||
Data is sent to Sentry's US-based servers and retained for approximately 90 days
|
||||
per Sentry's default data-retention policy.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
We use no advertising networks, analytics platforms, social SDKs, or any other
|
||||
third-party data collection services. The app contains no ads and no ad SDKs.
|
||||
</p>
|
||||
|
||||
<h2>6. Data Retention</h2>
|
||||
<p>
|
||||
Crash reports sent to Sentry are retained for approximately 90 days, after which they are
|
||||
automatically deleted per Sentry's retention defaults.
|
||||
</p>
|
||||
<p>
|
||||
We do not operate our own servers that store your data; there is no VIBE TECHNOLOGIES
|
||||
back end involved in normal app usage.
|
||||
</p>
|
||||
|
||||
<h2>7. Your Rights</h2>
|
||||
<p>
|
||||
You have the right to:
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>Opt out</strong> — disable crash reporting at any time in Settings → Privacy.</li>
|
||||
<li><strong>Request deletion</strong> — email <a href="mailto:support@vibebrowser.app">support@vibebrowser.app</a>
|
||||
with subject "Data deletion request" and we will request deletion of any crash events
|
||||
associated with your device from Sentry. Include your device model and approximate date
|
||||
range to help us identify your records.</li>
|
||||
<li><strong>Access</strong> — request a summary of what diagnostic data (if any) we hold
|
||||
about your device by emailing the same address.</li>
|
||||
</ul>
|
||||
<p>
|
||||
Residents of the EU/EEA/UK may exercise rights under GDPR/UK GDPR. California residents
|
||||
may exercise rights under the CCPA. To do so, contact us at the email above.
|
||||
</p>
|
||||
|
||||
<h2>8. Children</h2>
|
||||
<p>
|
||||
OpenCode Mobile is a developer tool intended for users aged 18 and over. We do not
|
||||
knowingly collect any data from children under 13 (or under 16 in the EU). If you believe
|
||||
a child has submitted data, please contact us and we will delete it promptly.
|
||||
</p>
|
||||
|
||||
<h2>9. Security</h2>
|
||||
<p>
|
||||
All diagnostic data is transmitted over HTTPS (TLS 1.2+) to Sentry. We do not transmit
|
||||
any data over unencrypted connections. Your opencode server traffic uses whatever transport
|
||||
security your server provides — we recommend HTTPS for all self-hosted deployments.
|
||||
</p>
|
||||
|
||||
<h2>10. Changes to This Policy</h2>
|
||||
<p>
|
||||
If we make material changes to this policy, we will update the effective date at the top
|
||||
of this page and, where feasible, notify users via an in-app notice. The latest version
|
||||
is always available at:
|
||||
<a href="https://opencode.vibebrowser.app/privacy">
|
||||
vibebrowser.app/opencode-mobile/privacy
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<h2>11. Contact</h2>
|
||||
<p>
|
||||
VIBE TECHNOLOGIES, LLC<br>
|
||||
519 S Henderson St<br>
|
||||
Seattle, WA 98108-4522<br>
|
||||
USA<br>
|
||||
Email: <a href="mailto:support@vibebrowser.app">support@vibebrowser.app</a>
|
||||
</p>
|
||||
|
||||
<hr style="margin:40px 0; border:none; border-top:1px solid #e2e8f0;">
|
||||
|
||||
<h2>Apple-Specific Addendum (iOS / App Store)</h2>
|
||||
<p>This addendum addresses Apple's specific privacy disclosure requirements for iOS apps distributed through the Apple App Store.</p>
|
||||
|
||||
<h3>App Tracking Transparency (ATT)</h3>
|
||||
<p>OpenCode Mobile does <strong>not</strong> use Apple's App Tracking Transparency (<code>AppTrackingTransparency</code>) framework. The app does not:</p>
|
||||
<ul>
|
||||
<li>Access the IDFA (Identifier for Advertisers)</li>
|
||||
<li>Use any cross-app or cross-website tracking</li>
|
||||
<li>Participate in any advertising network</li>
|
||||
<li>Profile users for advertising or marketing purposes</li>
|
||||
</ul>
|
||||
<p>No ATT permission prompt is ever shown to users because there is nothing to track.</p>
|
||||
|
||||
<h3>Apple Privacy Nutrition Label Data Categories</h3>
|
||||
<p>The following maps our data practices to Apple's official App Privacy categories (as required in App Store Connect):</p>
|
||||
<table style="width:100%;border-collapse:collapse;font-size:14px;margin:16px 0;">
|
||||
<thead>
|
||||
<tr style="background:#f1f5f9;">
|
||||
<th style="border:1px solid #e2e8f0;padding:8px;text-align:left;">Apple Category</th>
|
||||
<th style="border:1px solid #e2e8f0;padding:8px;text-align:left;">Collected?</th>
|
||||
<th style="border:1px solid #e2e8f0;padding:8px;text-align:left;">Linked to identity?</th>
|
||||
<th style="border:1px solid #e2e8f0;padding:8px;text-align:left;">Used for tracking?</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td style="border:1px solid #e2e8f0;padding:8px;">Contact Info</td><td style="border:1px solid #e2e8f0;padding:8px;">No</td><td style="border:1px solid #e2e8f0;padding:8px;">N/A</td><td style="border:1px solid #e2e8f0;padding:8px;">No</td></tr>
|
||||
<tr><td style="border:1px solid #e2e8f0;padding:8px;">Location</td><td style="border:1px solid #e2e8f0;padding:8px;">No</td><td style="border:1px solid #e2e8f0;padding:8px;">N/A</td><td style="border:1px solid #e2e8f0;padding:8px;">No</td></tr>
|
||||
<tr><td style="border:1px solid #e2e8f0;padding:8px;">Identifiers (Device ID)</td><td style="border:1px solid #e2e8f0;padding:8px;">Yes (Sentry anonymous ID, with consent)</td><td style="border:1px solid #e2e8f0;padding:8px;">No</td><td style="border:1px solid #e2e8f0;padding:8px;">No</td></tr>
|
||||
<tr><td style="border:1px solid #e2e8f0;padding:8px;">Diagnostics — Crash Data</td><td style="border:1px solid #e2e8f0;padding:8px;">Yes (Sentry, with consent)</td><td style="border:1px solid #e2e8f0;padding:8px;">No</td><td style="border:1px solid #e2e8f0;padding:8px;">No</td></tr>
|
||||
<tr><td style="border:1px solid #e2e8f0;padding:8px;">Diagnostics — Performance Data</td><td style="border:1px solid #e2e8f0;padding:8px;">Yes (Sentry, with consent)</td><td style="border:1px solid #e2e8f0;padding:8px;">No</td><td style="border:1px solid #e2e8f0;padding:8px;">No</td></tr>
|
||||
<tr><td style="border:1px solid #e2e8f0;padding:8px;">All other categories</td><td style="border:1px solid #e2e8f0;padding:8px;">No</td><td style="border:1px solid #e2e8f0;padding:8px;">N/A</td><td style="border:1px solid #e2e8f0;padding:8px;">No</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p><strong>App Store Connect summary:</strong> Data Linked to You: <em>None</em>. Data Not Linked to You: <em>Crash Data, Performance Data</em> (when user consents). Tracking: <em>No</em>.</p>
|
||||
|
||||
<footer>
|
||||
© 2026 VIBE TECHNOLOGIES, LLC. OpenCode Mobile is MIT-licensed open-source software.
|
||||
Privacy policy effective 2026-05-24.
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
164
distribution/privacy-policy.md
Normal file
@@ -0,0 +1,164 @@
|
||||
# OpenCode Mobile — Privacy Policy
|
||||
|
||||
**Effective date:** 2026-05-24
|
||||
**Operator:** VIBE TECHNOLOGIES, LLC
|
||||
**App:** OpenCode Mobile (`ai.opencode.mobile`)
|
||||
|
||||
> **Summary:** OpenCode Mobile does not collect your code, prompts, AI responses, server URLs, or any chat content. All AI traffic goes directly from the app to your own opencode server. We use Sentry only for anonymous crash diagnostics, and only with your consent.
|
||||
|
||||
---
|
||||
|
||||
## 1. Who We Are
|
||||
|
||||
OpenCode Mobile is developed and distributed by **VIBE TECHNOLOGIES, LLC**, a Washington State limited liability company.
|
||||
|
||||
- Address: 519 S Henderson St, Seattle, WA 98108-4522, USA
|
||||
- Contact: support@vibebrowser.app
|
||||
- Source code: https://github.com/dzianisv/opencode-mobile (MIT license)
|
||||
|
||||
---
|
||||
|
||||
## 2. Data We Do NOT Collect
|
||||
|
||||
We never collect, transmit to our servers, or share with third parties:
|
||||
|
||||
- Your code, files, or repository content
|
||||
- Your prompts, chat messages, or AI responses
|
||||
- Your opencode server URL, IP address, or hostname
|
||||
- Authentication tokens, API keys, or credentials you enter
|
||||
- Account information, email addresses, or names
|
||||
- Location data
|
||||
- Photos, microphone recordings, or camera data (these go only to your own server if you attach them to a message)
|
||||
- Contacts, calendar, or any other personal data
|
||||
|
||||
All communication between the app and your AI coding agent travels directly between your device and your self-hosted opencode server. VIBE TECHNOLOGIES, LLC never sees this traffic.
|
||||
|
||||
---
|
||||
|
||||
## 3. Data We Do Collect (Crash Diagnostics)
|
||||
|
||||
With your explicit consent (shown at first launch), we collect anonymous crash diagnostic data via **Sentry** to help us identify and fix bugs.
|
||||
|
||||
| Data type | What is captured | What is NOT captured |
|
||||
|---|---|---|
|
||||
| Device info | Device model, OS version, screen resolution, app version | Serial number, IMEI, advertising ID |
|
||||
| Crash / error reports | Stack traces, exception types and messages, source file names and line numbers | Variable values; no user data in scope |
|
||||
| Breadcrumbs | Screen names and function call sequence leading to the crash | Message bodies, server URLs, prompt text — all stripped by our URL-scrubbing filter |
|
||||
| App version | Version string and build number | — |
|
||||
|
||||
URL scrubbing: before any event is sent to Sentry, our code strips all server URLs, authentication tokens, and query parameters. No server hostname or port number ever leaves your device via Sentry.
|
||||
|
||||
---
|
||||
|
||||
## 4. Consent and Control
|
||||
|
||||
Crash reporting is **opt-in and off by default**. On first launch you will see a consent prompt. You can change this at any time:
|
||||
|
||||
- Open the app → **Settings** → **Privacy** → **Crash reporting** toggle.
|
||||
- When the toggle is off, Sentry is never initialised and no data leaves your device.
|
||||
|
||||
---
|
||||
|
||||
## 5. Third-Party Services
|
||||
|
||||
We use one third-party service for diagnostics:
|
||||
|
||||
- **Sentry** — crash and error monitoring.
|
||||
- Privacy policy: https://sentry.io/privacy/
|
||||
- Data is sent to Sentry's US-based servers and retained for approximately 90 days per Sentry's default data-retention policy.
|
||||
|
||||
We use no advertising networks, analytics platforms, social SDKs, or any other third-party data collection services. The app contains no ads and no ad SDKs.
|
||||
|
||||
---
|
||||
|
||||
## 6. Data Retention
|
||||
|
||||
Crash reports sent to Sentry are retained for approximately 90 days, after which they are automatically deleted per Sentry's retention defaults.
|
||||
|
||||
We do not operate our own servers that store your data; there is no VIBE TECHNOLOGIES back end involved in normal app usage.
|
||||
|
||||
---
|
||||
|
||||
## 7. Your Rights
|
||||
|
||||
You have the right to:
|
||||
|
||||
- **Opt out** — disable crash reporting at any time in Settings → Privacy.
|
||||
- **Request deletion** — email support@vibebrowser.app with subject "Data deletion request" and we will request deletion of any crash events associated with your device from Sentry.
|
||||
- **Access** — request a summary of what diagnostic data (if any) we hold about your device by emailing the same address.
|
||||
|
||||
Residents of the EU/EEA/UK may exercise rights under GDPR/UK GDPR. California residents may exercise rights under the CCPA.
|
||||
|
||||
---
|
||||
|
||||
## 8. Children
|
||||
|
||||
OpenCode Mobile is a developer tool intended for users aged 18 and over. We do not knowingly collect any data from children under 13 (or under 16 in the EU).
|
||||
|
||||
---
|
||||
|
||||
## 9. Security
|
||||
|
||||
All diagnostic data is transmitted over HTTPS (TLS 1.2+) to Sentry. We do not transmit any data over unencrypted connections.
|
||||
|
||||
---
|
||||
|
||||
## 10. Changes to This Policy
|
||||
|
||||
If we make material changes to this policy, we will update the effective date and, where feasible, notify users via an in-app notice. The latest version is always available at:
|
||||
https://www.vibebrowser.app/opencode-mobile/privacy
|
||||
|
||||
---
|
||||
|
||||
## 11. Contact
|
||||
|
||||
VIBE TECHNOLOGIES, LLC
|
||||
519 S Henderson St
|
||||
Seattle, WA 98108-4522
|
||||
USA
|
||||
Email: support@vibebrowser.app
|
||||
|
||||
---
|
||||
|
||||
## Apple-Specific Addendum (iOS / App Store)
|
||||
|
||||
This addendum addresses Apple's specific privacy disclosure requirements for iOS apps distributed through the Apple App Store.
|
||||
|
||||
### App Tracking Transparency (ATT)
|
||||
|
||||
OpenCode Mobile does **not** use Apple's App Tracking Transparency (`AppTrackingTransparency`) framework. The app does **not**:
|
||||
|
||||
- Access the IDFA (Identifier for Advertisers)
|
||||
- Use any cross-app or cross-website tracking
|
||||
- Participate in any advertising network
|
||||
- Profile users for advertising or marketing purposes
|
||||
|
||||
No ATT permission prompt is ever shown to users because there is nothing to track.
|
||||
|
||||
### Apple Privacy Nutrition Label Data Categories
|
||||
|
||||
The following table maps our data practices to Apple's official App Privacy categories (as required in App Store Connect):
|
||||
|
||||
| Apple Category | Sub-category | Collected? | Linked to identity? | Used for tracking? |
|
||||
|---|---|---|---|---|
|
||||
| Contact Info | Name, email, phone, address | No | N/A | No |
|
||||
| Health & Fitness | Any | No | N/A | No |
|
||||
| Financial Info | Any | No | N/A | No |
|
||||
| Location | Precise or coarse | No | N/A | No |
|
||||
| Sensitive Info | Any | No | N/A | No |
|
||||
| Contacts | Any | No | N/A | No |
|
||||
| User Content | Emails, messages, audio, gameplay, other | No | N/A | No |
|
||||
| Browsing History | Any | No | N/A | No |
|
||||
| Search History | Any | No | N/A | No |
|
||||
| Identifiers | User ID | No | N/A | No |
|
||||
| Identifiers | Device ID | Yes (Sentry anonymous ID) | No — not linked to Apple ID or personal info | No |
|
||||
| Purchases | Any | No | N/A | No |
|
||||
| Usage Data | Product interaction | No | N/A | No |
|
||||
| Diagnostics | Crash Data | Yes (Sentry, with consent) | No | No |
|
||||
| Diagnostics | Performance Data | Yes (Sentry, with consent) | No | No |
|
||||
| Diagnostics | Other Diagnostic Data | No | N/A | No |
|
||||
|
||||
**Summary for App Store Connect App Privacy section**:
|
||||
- Data Linked to You: **None**
|
||||
- Data Not Linked to You: **Crash Data, Performance Data** (Sentry diagnostics, when user consents)
|
||||
- Tracking: **No**
|
||||
161
distribution/strategy.md
Normal file
@@ -0,0 +1,161 @@
|
||||
# OpenCode Mobile — Distribution + Monetization Strategy
|
||||
|
||||
Date: 2026-05-24
|
||||
Sources: 3 research reports (market analysis, monetization, F-Droid distribution) + Play Console signup state.
|
||||
|
||||
---
|
||||
|
||||
## Pick one model for everything downstream
|
||||
|
||||
**Free client everywhere + paid "opencode Cloud" hosted backend.**
|
||||
|
||||
Mirrors Tailscale ($45M ARR, OSS client, paid coordination service) and the opencode authors' own model (OSS CLI, paid OpenCode Zen gateway, several million ARR in 5 months).
|
||||
|
||||
- **Client (this repo, MIT)**: free on Play Store, IzzyOnDroid, and F-Droid. Identical binary on all three (same signing key via reproducible builds).
|
||||
- **opencode Cloud (separate product, proprietary)**: managed opencode server hosting. One-tap connect option in the app alongside "self-hosted" and "tunnel". Target $10/mo individual, $30/mo team. **Not built yet — see Action items.**
|
||||
- **Donations layer**: GitHub Sponsors / OpenCollective to cover Sentry + CI costs (~$60/mo) while cloud revenue scales.
|
||||
|
||||
Rejected alternatives:
|
||||
- ❌ **Paid Play + free F-Droid** — MIT allows redistribution; community resentment when users find F-Droid version; license-check callbacks always get stripped in forks. DAVx⁵ exception works only because it's a one-time donation, not a feature gate.
|
||||
- ❌ **Subscription gated in client** — telemetry license checks earn F-Droid `Tracking` anti-feature and instantly get stripped by community forks.
|
||||
- ❌ **Ads** — kills OSS credibility, contradicts a developer audience.
|
||||
|
||||
---
|
||||
|
||||
## Distribution channels — priority order
|
||||
|
||||
| # | Channel | Identity | Status | Time to live | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | **Google Play (Internal)** | `ai.opencode.mobile` | ⏸ blocked on identity verification | Days after ID approved | CI ready. Track: `internal` first, then closed testing (12+ testers / 14d) before production. |
|
||||
| 2 | **IzzyOnDroid** | `ai.opencode.mobile` (same key) | ❌ not started | 1–3 days | Submit prebuilt APK to https://codeberg.org/IzzyOnDroid/repodata/issues. Fastest OSS channel. |
|
||||
| 3 | **F-Droid mainline** | `ai.opencode.mobile` (same key, reproducible build) | ❌ not started | 4–12 weeks | File MR at https://gitlab.com/fdroid/fdroiddata. Requires `expo-notifications` FCM audit + Sentry opt-in gate. |
|
||||
| 4 | **Apple App Store** | `ai.opencode.mobile` | ⏸ pending iOS prep agent | Weeks (Apple enrollment $99 + review) | iOS agent running — separate report. |
|
||||
|
||||
**All channels: same package id (`ai.opencode.mobile`), same signing key.** Lets users update across stores in-place.
|
||||
|
||||
---
|
||||
|
||||
## Blockers + outstanding work
|
||||
|
||||
### Hard blockers (need user)
|
||||
|
||||
1. **Google Play identity verification** — upload governor ID (Dzianis Vashchuk). Unlocks: API access, Create app, AAB upload, CI publish.
|
||||
2. **Apple Developer Program enrollment** — $99/year, D-U-N-S 142059652 ready. iOS agent will produce runbook.
|
||||
3. **App icon + adaptive icon + feature graphic** — current `assets/*.json` are placeholders. Need real PNGs before either Play or App Store publish.
|
||||
4. **Privacy policy URL** — must be live at https://opencode.vibebrowser.app/privacy before Play publish. Template in `play-listing.md`.
|
||||
|
||||
### Soft blockers (we can fix without user)
|
||||
|
||||
5. **Sentry opt-in consent gate** — currently always-on; needed for F-Droid `Tracking` anti-feature avoidance. Add settings toggle + first-launch consent screen. Persist in `expo-secure-store`.
|
||||
6. ~~**Audit `expo-notifications` FCM usage**~~ — ✅ done 2026-05-24. `src/lib/notifications.ts` uses local-only (`scheduleNotificationAsync`); no `getExpoPushTokenAsync`/`getDevicePushTokenAsync` anywhere. Remaining concern: library still compiles FCM receiver classes — F-Droid scanner may flag. Fix later with a `fdroid` Gradle flavor that excludes the FCM artifact. **Non-blocker for IzzyOnDroid** (more tolerant). For mainline F-Droid: add `productFlavors { fdroid { /* exclude FCM */ } }` to `android/app/build.gradle`.
|
||||
7. **APK size check** — IzzyOnDroid limit 30 MB. AAB currently 58.5 MB but that's universal — per-ABI splits typically 15–20 MB.
|
||||
8. **Fastlane metadata** — `fastlane/metadata/android/en-US/{short_description.txt,full_description.txt,images/}` so F-Droid auto-pulls listing.
|
||||
9. **Reproducible build verification** — F-Droid builds from source, compares to our signed APK. Need to verify our build is reproducible (no embedded timestamps, no machine-specific paths).
|
||||
|
||||
### Pre-launch tasks (low priority, optional)
|
||||
|
||||
10. **opencode Cloud MVP** — managed opencode hosting service. Stripe billing. The actual revenue product. **Big scope, separate project.**
|
||||
11. **GitHub Sponsors profile** — VIBE TECHNOLOGIES, LLC org. Tiers: $5 / $15 / $50.
|
||||
12. **Closed testing tester recruitment** — 12+ testers for 14d before production. Recruit from opencode community / dev Twitter / r/androiddev.
|
||||
|
||||
---
|
||||
|
||||
## What's already done (this session)
|
||||
|
||||
| Item | Status |
|
||||
|---|---|
|
||||
| Google Play developer account (org) | ✅ Created, ID 8842655543970815326, $25 paid |
|
||||
| Mercury virtual card | ✅ Saved to Bitwarden |
|
||||
| D-U-N-S 142059652 | ✅ Retrieved, saved to Bitwarden + skill |
|
||||
| GCP project `opencode-mobile-deploy` | ✅ Created |
|
||||
| androidpublisher API | ✅ Enabled |
|
||||
| Service account `playstore-deploy@…` | ✅ Created, JSON key saved to Bitwarden + GitHub secret |
|
||||
| Signed AAB | ✅ Built at `android/app/build/outputs/bundle/release/app-release.aab` (58.5 MB) |
|
||||
| Website verification | ✅ www.vibebrowser.app verified via Search Console auto-detection |
|
||||
| Contact email verification | ✅ support@vibebrowser.app verified via Play Console code |
|
||||
| Payments profile | ✅ Linked with D-U-N-S 142059652 |
|
||||
| CI workflow audit + fixes | ✅ `publish-play-store.yml` — versionCode auto-bump, r0adkll@v1.1.5, whatsNewDirectory wired |
|
||||
| Play Store listing copy | ✅ `distribution/play-listing.md` |
|
||||
| Skill `vibetechnologies-llc` | ✅ Created with company facts |
|
||||
| Subagent `vibetechnologies-llc-curator` | ✅ Created for auto-maintenance |
|
||||
|
||||
---
|
||||
|
||||
## Per-channel publishing recipe
|
||||
|
||||
### Google Play (after identity verified)
|
||||
|
||||
1. (manual, in browser) Home → Verify your identity → upload governor ID. Wait days.
|
||||
2. (manual) Setup → API access → Link `opencode-mobile-deploy`. Grant `playstore-deploy@…` "Release to production".
|
||||
3. (manual) Create app `ai.opencode.mobile`. Fill listing from `play-listing.md`. Upload icon + feature graphic + screenshots. Complete Data safety + Content rating + App access.
|
||||
4. (manual, first time) Upload `app-release.aab` to Internal testing track. Add tester emails.
|
||||
5. (automated thereafter) `git tag v0.2.4 && git push --tags` → CI builds + publishes to Internal.
|
||||
|
||||
### IzzyOnDroid (after first Play AAB exists for parity)
|
||||
|
||||
1. Tag GitHub release `v0.2.x` with signed universal APK attached (not AAB — APK).
|
||||
2. File issue at https://codeberg.org/IzzyOnDroid/repodata/issues:
|
||||
```
|
||||
App name: OpenCode Mobile
|
||||
Package: ai.opencode.mobile
|
||||
License: MIT
|
||||
GitHub release: https://github.com/dzianisv/opencode-mobile/releases
|
||||
APK SHA-256: <sha256>
|
||||
Description: Mobile client for the opencode AI coding agent CLI. Self-hosted backend.
|
||||
Note: NonFreeNet anti-feature applies (connects to user-self-hosted opencode server).
|
||||
```
|
||||
3. Wait 1–3 days for inclusion. Updates auto-pulled from each new GitHub release tag.
|
||||
|
||||
### F-Droid mainline (after Sentry opt-in + FCM audit done)
|
||||
|
||||
1. Fork https://gitlab.com/fdroid/fdroiddata.
|
||||
2. Create `metadata/ai.opencode.mobile.yml` with reproducible-build config (template in F-Droid report).
|
||||
3. Set `AllowedAPKSigningKeys: <sha256-fingerprint>` so F-Droid serves our pre-signed APK.
|
||||
4. File MR. Iterate on build failures with reviewers. 4–12 week timeline.
|
||||
5. Once accepted, request IzzyOnDroid delisting (they auto-remove when mainline accepts).
|
||||
|
||||
### Apple App Store
|
||||
|
||||
Per iOS agent report (pending) — runbook at `distribution/ios-enrollment-runbook.md` (to be created).
|
||||
|
||||
---
|
||||
|
||||
## Trigger for auto-publish (item 3 from user's plan)
|
||||
|
||||
"As soon as we get the ability to publish" = identity verified + app created + first manual AAB uploaded.
|
||||
|
||||
Once those manual steps are complete:
|
||||
|
||||
- Tag `v0.2.4` (or whatever the next version is) → existing `publish-play-store.yml` CI runs:
|
||||
- Bumps `android.versionCode` from `github.run_number`
|
||||
- Builds signed AAB
|
||||
- Uploads to Internal track with what's-new notes
|
||||
|
||||
No additional infrastructure needed — CI is already wired for this. Just push the tag.
|
||||
|
||||
For email notification when Google approves identity: Google sends to vibeteaichnologies@gmail.com. Add a `gws gmail` poll script that watches for "Developer account verification" subject from `noreply@google.com` and pings via webhook / posts a GitHub issue.
|
||||
|
||||
---
|
||||
|
||||
## Files in this repo related to distribution
|
||||
|
||||
```
|
||||
distribution/
|
||||
├── PLAY_CONSOLE_SETUP.md # original handoff doc (now mostly historical)
|
||||
├── play-listing.md # full Play Store copy + answers
|
||||
├── strategy.md # this file
|
||||
├── whatsnew/
|
||||
│ └── whatsnew-en-US # release notes consumed by CI per-release
|
||||
└── (pending)
|
||||
├── app-store-listing.md # iOS agent will create
|
||||
├── ios-enrollment-runbook.md # iOS agent will create
|
||||
└── whatsnew-ios/
|
||||
└── release-notes-en-US.txt # iOS first release notes
|
||||
|
||||
.github/workflows/
|
||||
├── build.yml # Android dev build CI
|
||||
├── cua-smoke.yml # CUA smoke tests
|
||||
├── publish-play-store.yml # AUTOMATED Play publish on tag/release
|
||||
└── (pending)
|
||||
└── publish-app-store.yml # iOS agent will draft
|
||||
```
|
||||
36
distribution/whatsnew-ios/release-notes-en-US.txt
Normal file
@@ -0,0 +1,36 @@
|
||||
Welcome to OpenCode for iOS — v0.2.3
|
||||
|
||||
OpenCode is a mobile companion for the opencode AI coding agent (github.com/sst/opencode). This first release brings full remote-control of your self-hosted opencode server from iPhone and iPad.
|
||||
|
||||
WHAT'S IN THIS RELEASE
|
||||
|
||||
• Connect to any opencode server — local Wi-Fi (LAN), Cloudflare Tunnel, ngrok, Tailscale, or any HTTPS endpoint
|
||||
• Multiple saved connections — switch between workstations with one tap
|
||||
• Real-time streaming chat — watch your AI agent think and respond live as it codes
|
||||
• File diff viewer — review exact code changes the agent proposes before accepting
|
||||
• Tool call approvals — confirm file writes, shell commands, and other actions before execution
|
||||
• Multi-session management — start, resume, and switch between concurrent coding sessions
|
||||
• Face ID / Touch ID biometric unlock — keep your sessions private
|
||||
• Markdown rendering with syntax-highlighted code blocks
|
||||
• Image attachments — attach screenshots or diagrams to guide the agent
|
||||
• Voice input — dictate prompts hands-free via speech recognition
|
||||
• Dark mode optimized UI
|
||||
|
||||
GETTING STARTED
|
||||
|
||||
You need an opencode server to use this app:
|
||||
|
||||
npm install -g opencode-ai
|
||||
opencode serve
|
||||
|
||||
The terminal prints a URL (e.g. http://192.168.1.100:4096). Tap "Add Connection" in the app and enter that URL.
|
||||
|
||||
For remote access from outside your network, use Tailscale or Cloudflare Tunnel to expose the server securely.
|
||||
|
||||
OPEN SOURCE
|
||||
|
||||
MIT licensed. Source + issues: https://github.com/dzianisv/opencode-mobile
|
||||
|
||||
FEEDBACK
|
||||
|
||||
Please use the TestFlight feedback button or email support@vibebrowser.app. We read every report.
|
||||
6
distribution/whatsnew/whatsnew-en-US
Normal file
@@ -0,0 +1,6 @@
|
||||
Initial Internal testing release.
|
||||
|
||||
- Connect to your self-hosted opencode server (HTTP)
|
||||
- Drive AI coding sessions from your phone
|
||||
- Streaming chat, diffs, file tree
|
||||
- Crash reporting via Sentry
|
||||
13
package-lock.json
generated
@@ -15,6 +15,7 @@
|
||||
"@tanstack/react-query": "^5.62.0",
|
||||
"expo": "^54.0.0",
|
||||
"expo-clipboard": "8.0.8",
|
||||
"expo-crypto": "~15.0.9",
|
||||
"expo-device": "~8.0.10",
|
||||
"expo-image-manipulator": "~14.0.8",
|
||||
"expo-image-picker": "17.0.10",
|
||||
@@ -4803,6 +4804,18 @@
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-crypto": {
|
||||
"version": "15.0.9",
|
||||
"resolved": "https://registry.npmjs.org/expo-crypto/-/expo-crypto-15.0.9.tgz",
|
||||
"integrity": "sha512-SNWKa2fXx7v9gkp1h/7nqXY5XN7qgNDn3yRc2aO0gWGbeMbvob/haMxxsPFe9f51aqH5NjNCqHf2kvLhvAd8KQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"expo": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-device": {
|
||||
"version": "8.0.10",
|
||||
"resolved": "https://registry.npmjs.org/expo-device/-/expo-device-8.0.10.tgz",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"@tanstack/react-query": "^5.62.0",
|
||||
"expo": "^54.0.0",
|
||||
"expo-clipboard": "8.0.8",
|
||||
"expo-crypto": "~15.0.9",
|
||||
"expo-device": "~8.0.10",
|
||||
"expo-image-manipulator": "~14.0.8",
|
||||
"expo-image-picker": "17.0.10",
|
||||
|
||||
@@ -245,13 +245,19 @@ def call_llm(client, model: str, system: str, history: list) -> str:
|
||||
|
||||
|
||||
def make_client(model: str):
|
||||
"""Create OpenAI client. Supports AZURE_OPENAI_*, OPENAI_API_KEY, GEMINI_API_KEY, XAI_API_KEY."""
|
||||
"""Create OpenAI client. Supports AZURE_OPENAI_*, AZURE_DEV_AI_*, OPENAI_API_KEY, GEMINI_API_KEY, XAI_API_KEY."""
|
||||
if os.environ.get("AZURE_OPENAI_API_KEY"):
|
||||
azure_model = os.environ.get("AZURE_OPENAI_MODEL", "gpt-5.4")
|
||||
return AzureOpenAI(
|
||||
api_key=os.environ["AZURE_OPENAI_API_KEY"],
|
||||
azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"],
|
||||
api_version=os.environ.get("AZURE_OPENAI_API_VERSION", "2024-08-01-preview"),
|
||||
), model
|
||||
), azure_model
|
||||
# Azure AI Foundry endpoint (cognitiveservices.azure.com/openai/v1) — use OpenAI client
|
||||
if os.environ.get("AZURE_DEV_AI_API_KEY"):
|
||||
base_url = os.environ.get("AZURE_DEV_AI_BASE_URL", "https://vibe-dev-ai.cognitiveservices.azure.com/openai/v1")
|
||||
azure_model = os.environ.get("AZURE_DEV_AI_MODEL", "gpt-4o-2024-11-20")
|
||||
return OpenAI(api_key=os.environ["AZURE_DEV_AI_API_KEY"], base_url=base_url), azure_model
|
||||
if os.environ.get("OPENAI_API_KEY"):
|
||||
base = os.environ.get("OPENAI_BASE_URL")
|
||||
return OpenAI(base_url=base) if base else OpenAI(), model
|
||||
@@ -265,7 +271,22 @@ def make_client(model: str):
|
||||
api_key=os.environ["GEMINI_API_KEY"],
|
||||
base_url="https://generativelanguage.googleapis.com/v1beta/openai/",
|
||||
), "gemini-2.0-flash"
|
||||
sys.exit("Set AZURE_OPENAI_API_KEY, OPENAI_API_KEY, XAI_API_KEY, or GEMINI_API_KEY")
|
||||
sys.exit("Set AZURE_OPENAI_API_KEY, AZURE_DEV_AI_API_KEY, OPENAI_API_KEY, XAI_API_KEY, or GEMINI_API_KEY")
|
||||
|
||||
|
||||
def get_screen_size() -> tuple[int, int]:
|
||||
"""Return (width, height) of the connected device screen."""
|
||||
try:
|
||||
out = adb("shell", "wm", "size")
|
||||
# "Physical size: 1080x1920" or "Override size: 1080x1920"
|
||||
for line in out.splitlines():
|
||||
if "size:" in line.lower():
|
||||
dims = line.split(":")[-1].strip()
|
||||
w, h = dims.split("x")
|
||||
return int(w), int(h)
|
||||
except Exception:
|
||||
pass
|
||||
return 1080, 1920
|
||||
|
||||
|
||||
def run_cua(goal: str, max_steps: int = 30, model: str = "gpt-4o",
|
||||
@@ -273,6 +294,7 @@ def run_cua(goal: str, max_steps: int = 30, model: str = "gpt-4o",
|
||||
"""Run the CUA loop until done/fail/max_steps."""
|
||||
client, model = make_client(model)
|
||||
history = []
|
||||
screen_w, screen_h = get_screen_size()
|
||||
|
||||
for step in range(1, max_steps + 1):
|
||||
# Capture screenshot
|
||||
@@ -280,7 +302,7 @@ def run_cua(goal: str, max_steps: int = 30, model: str = "gpt-4o",
|
||||
|
||||
# Build user message with screenshot
|
||||
content = [
|
||||
{"type": "text", "text": f"Step {step}. Screen is 1080x2400 pixels. Goal: {goal}\nWhat action should I take next?"},
|
||||
{"type": "text", "text": f"Step {step}. Screen is {screen_w}x{screen_h} pixels. Goal: {goal}\nWhat action should I take next?"},
|
||||
{"type": "image_url", "image_url": {"url": f"data:image/png;base64,{img_b64}", "detail": "high"}},
|
||||
]
|
||||
|
||||
@@ -299,10 +321,17 @@ def run_cua(goal: str, max_steps: int = 30, model: str = "gpt-4o",
|
||||
|
||||
# Parse action
|
||||
try:
|
||||
# Handle markdown code fences if model wraps response
|
||||
if reply.startswith("```"):
|
||||
reply = reply.split("\n", 1)[1].rsplit("```", 1)[0].strip()
|
||||
action = json.loads(reply)
|
||||
clean = reply.strip()
|
||||
# Strip markdown code fences
|
||||
if clean.startswith("```"):
|
||||
clean = clean.split("\n", 1)[1].rsplit("```", 1)[0].strip()
|
||||
# If model returned multiple JSON objects, take the first
|
||||
import re as _re
|
||||
m = _re.search(r'\{[^{}]*\}', clean)
|
||||
if m:
|
||||
action = json.loads(m.group(0))
|
||||
else:
|
||||
action = json.loads(clean)
|
||||
except json.JSONDecodeError:
|
||||
if verbose:
|
||||
print(f" [step {step}] Failed to parse: {reply[:100]}")
|
||||
|
||||
229
src/components/TelemetryConsentModal.tsx
Normal file
@@ -0,0 +1,229 @@
|
||||
/**
|
||||
* First-launch consent modal for Sentry crash reporting.
|
||||
*
|
||||
* Shows once when the user hasn't yet made a consent decision.
|
||||
* Matches the app's existing Settings screen visual conventions.
|
||||
*/
|
||||
|
||||
import { View, Text, TouchableOpacity, StyleSheet, useColorScheme, Modal, Linking } from "react-native"
|
||||
import { Ionicons } from "@expo/vector-icons"
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
onAllow: () => void
|
||||
onDecline: () => void
|
||||
}
|
||||
|
||||
export function TelemetryConsentModal({ visible, onAllow, onDecline }: Props) {
|
||||
const colorScheme = useColorScheme()
|
||||
const isDark = colorScheme === "dark"
|
||||
|
||||
return (
|
||||
<Modal visible={visible} transparent animationType="fade" statusBarTranslucent onRequestClose={onDecline}>
|
||||
<View style={styles.overlay}>
|
||||
<View style={[styles.card, isDark && styles.cardDark]}>
|
||||
{/* Icon */}
|
||||
<View style={[styles.iconWrap, isDark && styles.iconWrapDark]}>
|
||||
<Ionicons name="bug-outline" size={40} color="#3b82f6" />
|
||||
</View>
|
||||
|
||||
{/* Title */}
|
||||
<Text style={[styles.title, isDark && styles.textDark]}>Help improve OpenCode?</Text>
|
||||
|
||||
{/* Body */}
|
||||
<Text style={[styles.body, isDark && styles.bodyDark]}>
|
||||
Share anonymous crash reports to help us find and fix bugs faster. No code, prompts,
|
||||
or server addresses are ever included.
|
||||
</Text>
|
||||
|
||||
{/* Detail bullets */}
|
||||
<View style={styles.bullets}>
|
||||
<BulletRow icon="checkmark-circle" text="Device model, OS version, app version" isDark={isDark} positive />
|
||||
<BulletRow icon="checkmark-circle" text="Stack traces of crashes (no variable values)" isDark={isDark} positive />
|
||||
<BulletRow icon="close-circle" text="Your code, prompts, or chat messages" isDark={isDark} positive={false} />
|
||||
<BulletRow icon="close-circle" text="Server URLs or authentication tokens" isDark={isDark} positive={false} />
|
||||
</View>
|
||||
|
||||
{/* Privacy policy link */}
|
||||
<TouchableOpacity
|
||||
onPress={() => Linking.openURL("https://www.vibebrowser.app/opencode-mobile/privacy")}
|
||||
>
|
||||
<Text style={styles.privacyLink}>Read our full privacy policy</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Actions */}
|
||||
<View style={styles.actions}>
|
||||
<TouchableOpacity
|
||||
style={[styles.btn, styles.btnDecline, isDark && styles.btnDeclineDark]}
|
||||
onPress={onDecline}
|
||||
accessibilityLabel="No thanks, decline crash reporting"
|
||||
>
|
||||
<Text style={[styles.btnDeclineText, isDark && styles.btnDeclineTextDark]}>No thanks</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.btn, styles.btnAllow]}
|
||||
onPress={onAllow}
|
||||
accessibilityLabel="Allow anonymous crash reports"
|
||||
>
|
||||
<Text style={styles.btnAllowText}>Allow</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<Text style={[styles.footnote, isDark && styles.footnoteDark]}>
|
||||
You can change this at any time in Settings → Privacy.
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
function BulletRow({
|
||||
icon,
|
||||
text,
|
||||
isDark,
|
||||
positive,
|
||||
}: {
|
||||
icon: "checkmark-circle" | "close-circle"
|
||||
text: string
|
||||
isDark: boolean
|
||||
positive: boolean
|
||||
}) {
|
||||
return (
|
||||
<View style={styles.bulletRow}>
|
||||
<Ionicons
|
||||
name={icon}
|
||||
size={18}
|
||||
color={positive ? "#22c55e" : "#ef4444"}
|
||||
style={styles.bulletIcon}
|
||||
/>
|
||||
<Text style={[styles.bulletText, isDark && styles.bodyDark]}>{text}</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
overlay: {
|
||||
flex: 1,
|
||||
backgroundColor: "rgba(0,0,0,0.6)",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
padding: 24,
|
||||
},
|
||||
card: {
|
||||
backgroundColor: "#ffffff",
|
||||
borderRadius: 20,
|
||||
padding: 28,
|
||||
width: "100%",
|
||||
maxWidth: 440,
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 8 },
|
||||
shadowOpacity: 0.24,
|
||||
shadowRadius: 16,
|
||||
elevation: 10,
|
||||
},
|
||||
cardDark: {
|
||||
backgroundColor: "#1a1a1a",
|
||||
},
|
||||
iconWrap: {
|
||||
width: 72,
|
||||
height: 72,
|
||||
borderRadius: 18,
|
||||
backgroundColor: "#eff6ff",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
alignSelf: "center",
|
||||
marginBottom: 16,
|
||||
},
|
||||
iconWrapDark: {
|
||||
backgroundColor: "#1e293b",
|
||||
},
|
||||
title: {
|
||||
fontSize: 22,
|
||||
fontWeight: "700",
|
||||
color: "#0a0a0a",
|
||||
textAlign: "center",
|
||||
marginBottom: 12,
|
||||
},
|
||||
textDark: {
|
||||
color: "#f8fafc",
|
||||
},
|
||||
body: {
|
||||
fontSize: 15,
|
||||
color: "#374151",
|
||||
textAlign: "center",
|
||||
lineHeight: 22,
|
||||
marginBottom: 20,
|
||||
},
|
||||
bodyDark: {
|
||||
color: "#94a3b8",
|
||||
},
|
||||
bullets: {
|
||||
marginBottom: 16,
|
||||
},
|
||||
bulletRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "flex-start",
|
||||
marginBottom: 8,
|
||||
},
|
||||
bulletIcon: {
|
||||
marginTop: 2,
|
||||
marginRight: 10,
|
||||
flexShrink: 0,
|
||||
},
|
||||
bulletText: {
|
||||
fontSize: 14,
|
||||
color: "#374151",
|
||||
flex: 1,
|
||||
lineHeight: 20,
|
||||
},
|
||||
privacyLink: {
|
||||
color: "#3b82f6",
|
||||
fontSize: 14,
|
||||
textAlign: "center",
|
||||
marginBottom: 24,
|
||||
textDecorationLine: "underline",
|
||||
},
|
||||
actions: {
|
||||
flexDirection: "row",
|
||||
gap: 12,
|
||||
marginBottom: 16,
|
||||
},
|
||||
btn: {
|
||||
flex: 1,
|
||||
paddingVertical: 14,
|
||||
borderRadius: 12,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
btnDecline: {
|
||||
backgroundColor: "#f1f5f9",
|
||||
},
|
||||
btnDeclineDark: {
|
||||
backgroundColor: "#2a2a2a",
|
||||
},
|
||||
btnDeclineText: {
|
||||
color: "#374151",
|
||||
fontWeight: "600",
|
||||
fontSize: 16,
|
||||
},
|
||||
btnDeclineTextDark: {
|
||||
color: "#94a3b8",
|
||||
},
|
||||
btnAllow: {
|
||||
backgroundColor: "#3b82f6",
|
||||
},
|
||||
btnAllowText: {
|
||||
color: "#ffffff",
|
||||
fontWeight: "700",
|
||||
fontSize: 16,
|
||||
},
|
||||
footnote: {
|
||||
fontSize: 12,
|
||||
color: "#9ca3af",
|
||||
textAlign: "center",
|
||||
},
|
||||
footnoteDark: {
|
||||
color: "#64748b",
|
||||
},
|
||||
})
|
||||
80
src/lib/telemetry.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Telemetry consent + initialisation gate.
|
||||
*
|
||||
* Wraps sentry.ts so that initSentry() is only called when the user has
|
||||
* explicitly opted in. Consent state is persisted in expo-secure-store so
|
||||
* it survives app restarts.
|
||||
*
|
||||
* Usage:
|
||||
* import { loadTelemetryConsent, setTelemetryConsent, hasTelemetryConsent } from './telemetry'
|
||||
*
|
||||
* // On app start — call BEFORE trying to initialise Sentry.
|
||||
* const state = await loadTelemetryConsent() // 'granted' | 'denied' | 'unknown'
|
||||
* if (state === 'granted') initSentry()
|
||||
*
|
||||
* // After the user taps "Allow" in the consent modal:
|
||||
* await setTelemetryConsent(true) // persists + calls initSentry() if not already done
|
||||
*
|
||||
* // Check in Settings screen:
|
||||
* const current = hasTelemetryConsent() // boolean | null (null = not yet decided)
|
||||
*/
|
||||
|
||||
import * as SecureStore from "expo-secure-store"
|
||||
import { initSentry, sentryEnabled } from "./sentry"
|
||||
|
||||
const CONSENT_KEY = "opencode_telemetry_consent"
|
||||
|
||||
export type ConsentState = "granted" | "denied" | "unknown"
|
||||
|
||||
let _resolved: boolean | null = null // null = unknown, true = granted, false = denied
|
||||
|
||||
/**
|
||||
* Load persisted consent from SecureStore.
|
||||
* Returns 'unknown' if the user has never been asked.
|
||||
*/
|
||||
export async function loadTelemetryConsent(): Promise<ConsentState> {
|
||||
try {
|
||||
const stored = await SecureStore.getItemAsync(CONSENT_KEY)
|
||||
if (stored === "granted") {
|
||||
_resolved = true
|
||||
return "granted"
|
||||
}
|
||||
if (stored === "denied") {
|
||||
_resolved = false
|
||||
return "denied"
|
||||
}
|
||||
// No stored value — first launch
|
||||
_resolved = null
|
||||
return "unknown"
|
||||
} catch {
|
||||
// SecureStore unavailable — don't clobber a previously resolved in-memory state.
|
||||
// Return unknown so the caller can surface the modal.
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the in-memory resolved state (set by loadTelemetryConsent or setTelemetryConsent).
|
||||
* null = consent decision not yet loaded.
|
||||
* true = granted.
|
||||
* false = denied.
|
||||
*/
|
||||
export function hasTelemetryConsent(): boolean | null {
|
||||
return _resolved
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist the user's consent decision and, if granted and Sentry is not yet
|
||||
* running, initialise it immediately.
|
||||
*/
|
||||
export async function setTelemetryConsent(granted: boolean): Promise<void> {
|
||||
_resolved = granted
|
||||
try {
|
||||
await SecureStore.setItemAsync(CONSENT_KEY, granted ? "granted" : "denied")
|
||||
} catch {
|
||||
// Best-effort persist — in-memory state is still correct for this session.
|
||||
}
|
||||
if (granted && !sentryEnabled()) {
|
||||
initSentry()
|
||||
}
|
||||
}
|
||||
@@ -71,7 +71,7 @@ export const useAuth = create<AuthState>((set, get) => ({
|
||||
set({
|
||||
error: "Failed to initialize authentication",
|
||||
isLoading: false,
|
||||
isAuthenticated: true, // Fail open for usability
|
||||
isAuthenticated: false,
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { create } from "zustand"
|
||||
import * as SecureStore from "expo-secure-store"
|
||||
import * as Crypto from "expo-crypto"
|
||||
import type { ServerConnection, ConnectionType } from "../lib/types"
|
||||
import { createClient, type Client, type Project } from "../lib/sdk"
|
||||
import { addBreadcrumb } from "../lib/sentry"
|
||||
@@ -43,7 +44,7 @@ interface ConnectionsState {
|
||||
}
|
||||
|
||||
function generateId(): string {
|
||||
return Math.random().toString(36).slice(2, 11)
|
||||
return Crypto.randomUUID().replace(/-/g, "").slice(0, 16)
|
||||
}
|
||||
|
||||
function buildClient(
|
||||
|
||||
@@ -2,6 +2,10 @@ import { create } from "zustand"
|
||||
import { useConnections } from "./connections"
|
||||
import { useSessions } from "./sessions"
|
||||
import { send as notify } from "../lib/notifications"
|
||||
|
||||
const MAX_NOTIF_BODY = 200
|
||||
const sanitizeBody = (s: string | undefined, fallback: string): string =>
|
||||
(s ? s.replace(/[\x00-\x1f\x7f]/g, " ").trim().slice(0, MAX_NOTIF_BODY) : "") || fallback
|
||||
import { addBreadcrumb } from "../lib/sentry"
|
||||
import type { Client, Part, Session, Message } from "../lib/sdk"
|
||||
|
||||
@@ -141,7 +145,7 @@ export const useEvents = create<EventsState>((set, get) => ({
|
||||
notify({
|
||||
category: "connection",
|
||||
title: "Connection interrupted",
|
||||
body: "Trying to reconnect to your server",
|
||||
body: sanitizeBody(undefined, "Trying to reconnect to your server"),
|
||||
sessionId: "",
|
||||
dedupeKey: "sse-prolonged-disconnect",
|
||||
dedupeCooldownMs: 60_000,
|
||||
@@ -204,7 +208,7 @@ export const useEvents = create<EventsState>((set, get) => ({
|
||||
notify({
|
||||
category: "completed",
|
||||
title: "Task completed",
|
||||
body: match?.title || "Session finished processing",
|
||||
body: sanitizeBody(match?.title, "Session finished processing"),
|
||||
sessionId: sessionID,
|
||||
})
|
||||
}
|
||||
@@ -271,7 +275,7 @@ export const useEvents = create<EventsState>((set, get) => ({
|
||||
notify({
|
||||
category: "errors",
|
||||
title: "Session error",
|
||||
body: error?.message || "Something went wrong",
|
||||
body: sanitizeBody(error?.message, "Something went wrong"),
|
||||
sessionId: sessionID,
|
||||
})
|
||||
break
|
||||
@@ -291,7 +295,7 @@ export const useEvents = create<EventsState>((set, get) => ({
|
||||
notify({
|
||||
category: "permissions",
|
||||
title: req.permission || "Permission requested",
|
||||
body: req.patterns?.join(", ") || "A tool needs your approval",
|
||||
body: sanitizeBody(req.patterns?.join(", "), "A tool needs your approval"),
|
||||
sessionId: req.sessionID,
|
||||
})
|
||||
break
|
||||
@@ -324,7 +328,7 @@ export const useEvents = create<EventsState>((set, get) => ({
|
||||
notify({
|
||||
category: "questions",
|
||||
title: req.questions?.[0]?.header || "Input needed",
|
||||
body: req.questions?.[0]?.question || "The assistant has a question",
|
||||
body: sanitizeBody(req.questions?.[0]?.question, "The assistant has a question"),
|
||||
sessionId: req.sessionID,
|
||||
})
|
||||
break
|
||||
|
||||