Commit Graph

283 Commits

Author SHA1 Message Date
Den
0bedad366b feat(feedback): deliver shared diagnostic reports to Chatwoot support inbox (#88)
* feat(feedback): deliver shared diagnostic reports to Chatwoot support inbox

Wire shareReport() to the Chatwoot public client API
(/public/api/v1/inboxes/{inbox_identifier}) so user-shared diagnostic
reports also reach the OpenCode Mobile Feedback inbox.

- New src/lib/chatwoot.ts: dependency-injected, node-testable client —
  anonymous contact -> conversation -> message. Ships only the inbox
  identifier (EXPO_PUBLIC_CHATWOOT_INBOX_IDENTIFIER); never an
  account api_access_token. Contact source_id persisted via
  SecureStore for conversation continuity; stale id recreated on 404.
- Delivery is gated on the same telemetry consent flag as
  Sentry/PostHog and is best-effort (share sheet never blocks on it).
- Reports are scrubbed before leaving the device: all URLs and every
  occurrence of the target host redacted (new redactHostAndUrls in
  scrub.ts).
- CI: pass EXPO_PUBLIC_CHATWOOT_INBOX_IDENTIFIER in build and
  Play-publish workflows. Deliberately NOT added to the F-Droid
  workflow to avoid widening reproducible-build divergence (#86).
- Consent modal copy discloses support-inbox delivery.

Closes #85

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(feedback): close host-leak gaps in support-report scrubbing

Security review findings on the Chatwoot delivery path:

- Log-buffer lines record server hosts without a scheme, which the
  URL regex never matches, and crash reports carry no host of their
  own — so bare hostnames could reach the support inbox. Track every
  host probed this session and redact them all in the support copy.
- Redact bare IPv4 addresses as a catch-all for hosts never parsed.
- Resolve telemetry consent from SecureStore when a report is shared
  before startup finished loading it, instead of silently dropping.
- Move redactHostAndUrls tests to scrub.test.ts alongside the module.

Refs #85

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 23:08:40 -07:00
Den
004fa13795 chore(store): refresh Play listing — drop dated 'GPT-4', ASO keyword pass (#83)
* chore(store): refresh Play listing — drop dated "GPT-4", ASO keyword pass

distribution/play-listing.md is the canonical copy-paste source for the
Play Console listing (per distribution/strategy.md). It still named
"GPT-4" and was missing the directory picker and reasoning-effort
features shipped since. Rewrite title/short description/full
description to be model-agnostic ("Claude, GPT, Gemini, or any other
model") so it stops dating itself, add the two new feature bullets, a
connection-options section, and screenshot captions.

Also fixes the same stale "GPT-4" mention in the orphaned
distribution/play-store-listing.md (marked superseded — it was never
merged back into play-listing.md), distribution/aso-audit.md's
recommended-copy example, and distribution/app-store-listing.md for
cross-store consistency.

fastlane/metadata/android/en-US/full_description.txt is untouched: it
already had no stale model name, and it is shared with the F-Droid
auto-pull (plain text + AntiFeatures disclosure required by mainline
F-Droid) so it must not get Play-style HTML/marketing copy. Only
title.txt and short_description.txt (Play/App-store-only fields, not
read by F-Droid's metadata.yml which sets AutoName explicitly) were
aligned with the new short description.

* docs(store): correct full-description char count annotation (3410→3366)

---------

Co-authored-by: engineer <engineer@gray-knight-m1.local>
2026-07-16 19:08:29 -07:00
Den
e3fb8fa431 fix(site): use support@agentlabs.cc for opencode-mobile support contact (#84)
The website's Support, Privacy, and Terms pages still showed
support@vibebrowser.app — leftover boilerplate from the parent
VibeBrowser product. App store listings, the privacy policy source
(distribution/privacy-policy.html), F-Droid metadata, and the in-app
mailto already use support@agentlabs.cc, so this brings the website
in line with the canonical support address.

Changed:
- website/app/support/page.tsx: mailto link + two visible address strings
- website/app/privacy/page.tsx: fallback contact string shown only if
  distribution/privacy-policy.html fails to load
- website/app/terms/page.tsx: Section 7 (No Support Obligation) and
  Section 14 (Contact) mailto links

Left untouched: AGENTS.md's VIBEBROWSER_REMOTE_URL example (a real
VibeBrowser relay URL, unrelated product) and historical planning notes
(context.md, HANDOFF.md, .autopilot/state.md) documenting the prior
opencode.vibebrowser.app/privacy hosting decision — those are logs of
past decisions, not live support/contact surfaces.

Co-authored-by: engineer <engineer@gray-knight-m1.local>
2026-07-16 19:07:30 -07:00
engineer
c20f470545 chore(ios): switch Apple ID to vibeteaichnologies@gmail.com (owner decision 2026-07-17) 2026-07-16 18:13:26 -07:00
Den
142518866b fix(metrics): repair review triage — correct secret wiring, privacy-safe aggregated issues. Closes #61. Refs #60. (#78)
* fix(metrics): repair review triage — correct secret wiring, privacy-safe aggregated issues

- triage-reviews.yml read secrets.GOOGLE_SERVICE_ACCOUNT_JSON, which doesn't
  exist; map the real PLAY_STORE_SERVICE_ACCOUNT_JSON secret onto the env var
  the script expects.
- triage-reviews.py rewritten to maintain a single sanitized, deduped
  "Play Store Review Triage" issue instead of one public issue per review.
  The old version leaked reviewer full names and verbatim review text into
  public GitHub issues and spammed the tracker. The new version aggregates
  actionable (<=3 star) reviews into one issue with rating counts, a
  word-frequency theme summary (no quoted sentences), and opaque review_id
  references for Play Console lookup. An embedded HTML comment marker
  (matching the product-intelligence.mjs pattern) holds the current
  actionable review_id set so runs update in place and skip entirely when
  nothing changed.
- product-intelligence.yml referenced the nonexistent
  SENTRY_PRODUCT_INTELLIGENCE_TOKEN secret, causing the daily cron to fail
  silently (#60). Fall back to SENTRY_AUTH_TOKEN when the dedicated
  read-only token isn't configured.
- docs/playstore.md: document that Play Console is still the only trusted
  source for acquisition/uninstall metrics (product-intelligence.mjs defers
  this), and that review-based signals are sourced via the Android
  Publisher API through PLAY_STORE_SERVICE_ACCOUNT_JSON.

Closes #61. Refs #60.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NJKAQ6HAikWGQK7PGZ5Y4E

* fix(triage): fail visibly when GOOGLE_SERVICE_ACCOUNT_JSON is missing

Review finding on PR #78: env_client() exited 0 on missing credentials,
so the scheduled workflow would report success while silently doing
nothing — contradicting issue #61's 'missing credentials fail visibly'
done-criteria.

---------

Co-authored-by: engineer <engineer@gray-knight-m1.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 18:00:41 -07:00
Den
819996f5fa fix(sessions): show all sessions across all projects (closes #48) (#50)
* fix(sessions): load all sessions across projects, not just active directory

Closes #48

Root cause: loadSessions() used connState.client which carries the
active connection's directory as x-opencode-directory header. The server
filters sessions by that directory, so only the current project's sessions
were visible.

Fix: call clientForDirectory(undefined) to get a no-header client.
The server then returns sessions from all projects.

The session row UI already showed a directory badge (shortDir from
session.directory), so no UI change is needed — each session already
displays its project folder name.

* fix(sessions): preserve directory when opening rows

Carry each listed session directory into the route so selection, messages, and follow-up operations use the matching project client.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-16 17:33:00 -07:00
Den
c1736bd426 feat: add reasoning effort picker to session screen (#47) (#51)
* feat(#49): improve project picker with recents + server projects

- New Session modal now shows:
  - Current project as tappable row (tap to create session immediately)
  - Recent Projects section: list of previously used dirs as tappable rows
  - Server Projects section: projects known to opencode server (from /project API)
  - Manual path input as fallback (unchanged behavior)
- Modal body is now scrollable to handle long lists
- All selection paths call addRecentDirectory to keep recents up to date
- TypeScript clean (pre-existing VariantPicker.tsx error unrelated)

* feat: add reasoning effort (variant) picker to session screen (#47)

- Add VariantPicker bottom sheet component (low/medium/high/auto)
- Add variant state to catalog store, reset on model change
- Pass variant through sendMessage -> sdk.session.prompt()
- Add reasoning chip to toolbar, shown only for models with variants
- Parse model.variants from provider API response in catalog and sdk types

API field: variant in POST /session/:id/prompt_async
Server maps variant -> reasoningEffort via model variant config

* fix(models): preserve reasoning effort across messages

Reset the selected variant only when the provider/model pair actually changes, including catalog reloads and agent-driven model switches.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-16 17:32:56 -07:00
Den
36421b4d30 fix(sessions): hide misleading "0 files" in session list (#75)
session.summary is always a truthy object with files defaulting to 0
until the server populates real counts, so the session list always
rendered "· 0 files" regardless of actual changes. SessionInfo.tsx
already guards on `summary.files > 0`; apply the same guard in the
session list so it no longer shows a count that is never accurate.

Note: this does not make the count itself accurate — session.summary
is populated server-side and the opencode server currently never
updates it after a session runs. Showing correct counts needs either
a server-side fix or client-side aggregation from session message
parts; this PR only removes the always-wrong "0 files" text.

Partially addresses #55

Co-authored-by: engineer <engineer@macbookpro.lan>
2026-07-16 17:32:53 -07:00
Den
8c4b7a6239 fix(android): keyboard covers text input on chat screen (#70)
The KeyboardAvoidingView used behavior='height' on Android, which
conflicts with the native android:windowSoftInputMode='adjustResize'
set in AndroidManifest.xml. This causes the keyboard to overlap the
text input instead of pushing it up.

Fix: use behavior={undefined} on Android, letting the native
adjustResize handle keyboard avoidance — the recommended approach.

Closes #53

(cherry picked from commit 5d2380b794a240e7ee9b72a95b1b5161403704d2)

Co-authored-by: cloph <128580843+cloph-dsp@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-16 17:32:49 -07:00
engineer
3a724eb818 merge: test/activation-e2e — Maestro activation E2E + mock opencode server (reviewed: APPROVE after fixes) 2026-07-16 17:24:32 -07:00
engineer
7ca5d2eb19 test(activation): address code-review findings on E2E flows, mock, CI
Review fixes (REQUEST_CHANGES round 1):

1. HIGH activation-negative-401.yaml: after dismissing the "Connection
   Failed" alert the app stays on the Add-Connection modal
   (handleQuickConnect's failure branch never calls router.back()), so the
   old `text: "No Connection"` assertion (Sessions-tab empty state) could
   never pass. Now asserts connect-submit-button is still visible instead.

2. MEDIUM mock-opencode-server.ts: prompt_async now parses the request
   body, persists the USER's message, and broadcasts it (message.updated +
   message.part.updated) BEFORE the canned assistant reply — matching real
   server behavior. Without this, the app's handleEvent strips the
   optimistic temp- user message when the assistant's message.updated
   arrives and the sent message vanishes from the transcript.
   activation-positive.yaml now also asserts chat-bubble-user and the
   user's message text are visible after the reply lands, so that
   regression class is actually covered.

3. MEDIUM activation-e2e.yml: timeout-minutes 15 -> 60. The job runs the
   same npm install + prebuild + assembleRelease + emulator pipeline that
   cua-smoke.yml budgets 60 min for (emulator-boot-timeout alone is 10 min).

4. MEDIUM activation-e2e.yml: replicated cua-smoke.yml's "Purge stale
   generated sources" step — the Gradle cache key/restore-keys are shared
   with that workflow, so the stale-autolinking-tree failure mode
   (compileReleaseJavaWithJavac against the old package id) applies here too.

Verified locally: tsc --noEmit clean; npm test 81/81 pass; all three
touched YAML files parse valid; mock server exercised standalone —
full prompt cycle confirms GET /session/:id/message returns BOTH user
and assistant messages, SSE order is message.updated(user) ->
message.part.updated(user) -> busy -> message.updated(assistant) ->
message.part.updated(assistant) -> idle, user events carry the
sessionID/messageID fields handleEvent filters on, and --fail-auth
mode returns 401. Still no emulator run in this environment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NJKAQ6HAikWGQK7PGZ5Y4E
2026-07-16 17:19:46 -07:00
engineer
b4483887ec merge: feat/activation-analytics — consent-gated PostHog activation funnel (reviewed: APPROVE after fixes) 2026-07-16 16:09:27 -07:00
engineer
c3cac2b8e5 fix(analytics): address review findings on activation-funnel events
- app_opened now also fires on the consent-grant transition (modal Allow /
  Settings toggle), not just cold start with prior consent — the true first
  session was emitting nothing and session 2 got mislabeled is_first_open.
  trackAppOpened() is guarded once-per-JS-session so revoke->regrant cannot
  double-count.
- testConnection() takes a source ('onboarding' | 'edit_test') carried on
  connection_attempted/succeeded/failed so the funnel can filter out the
  edit screen's repeat-tester noise.
- Aborted runs no longer count: abortedSessions set (in sessions.ts, read by
  events.ts which already imports it — no new import cycle), marked after a
  successful abort call, cleared on busy, and checked on busy->idle for BOTH
  response_received and recordSuccessfulSession().
- Consent revocation now DROPS buffered events instead of flushing them:
  PostHog's optOut() only blocks new captures and shutdown() drains the queue
  over the network, so ConsentGatedPostHog overrides the public fetch()
  transport to answer with a synthetic 200 post-revoke — shutdown clears the
  persisted queue and timers with zero bytes leaving the device. Re-grant
  calls optIn() to clear the persisted SDK opt-out flag.
- classifyConnectionError extracted to pure analytics-classify.ts with
  node --test coverage (same pattern as store-review-policy).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NJKAQ6HAikWGQK7PGZ5Y4E
2026-07-16 16:04:54 -07:00
engineer
f16dd91d88 merge: feat/directory-picker — browsable server directory picker (#49/#57) (reviewed: APPROVE after fixes) 2026-07-16 15:59:34 -07:00
engineer
ec0a0a04d3 merge: fix/sentry-sourcemaps — metro debug-ID injection + release/dist alignment (reviewed: APPROVE) 2026-07-16 15:58:45 -07:00
engineer
7e9b3981c3 fix(directory-picker): address review — modal layering, root nav, stale state
- HIGH: the "Browse Folders..." entry in the New Session RN <Modal> expanded
  a sibling BottomSheet, which a native Modal always covers (a
  BottomSheetModal through the root portal would be covered too), so the
  primary entry point was invisible/untouchable. The modal is now closed
  before the sheet expands and restored on cancel via a new onDismiss
  callback (restoreNewSessionOnDismiss ref); picking a folder proceeds to
  session creation without reopening the modal.

- MEDIUM: parentOf("/") returned "/" so Up at the POSIX root looped forever;
  it now returns null at "/", "\" and Windows drive roots alike, disabling
  the Up button there.

- LOW: opening the sheet with no known start directory (server home not
  loaded yet) showed the previous open's stale entries; it now clears state,
  invalidates in-flight loads, and shows an "Enter a path above to start
  browsing" empty state. Sheet init also no longer re-runs on snap-point
  drags (wasOpen guard).

- Extracted the pure path helpers (stripTrailingSlash/parentOf/nameOf) into
  src/lib/path-utils.ts (no RN imports) with node --test coverage for POSIX
  root, Windows drive roots, trailing slashes, and backslash paths.

typecheck clean; 97/97 tests pass (16 new).
2026-07-16 15:57:31 -07:00
engineer
027c529ce5 merge main (feat/feedback-automation) into feat/activation-analytics
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NJKAQ6HAikWGQK7PGZ5Y4E
2026-07-16 15:57:13 -07:00
engineer
01dd0191b3 test(activation): add Maestro E2E coverage for the activation flow
Adds deterministic end-to-end coverage for first-open -> telemetry consent
-> server URL entry -> connect -> send first message -> receive reply,
targeting the 0%-7-day-retention investigation (GitHub issue #76).

- tests/fixtures/mock-opencode-server.ts: dependency-free HTTP+SSE stub
  matching the REAL client protocol (src/lib/sdk.ts) — REST + a single
  long-lived GET /global/event SSE stream, no WebSocket. Supports a
  --fail-auth mode that 401s every request to exercise the connect-time
  auth-failure class.
- .maestro/flows/activation-positive.yaml: consent -> quick connect ->
  new session -> send message -> assert streamed reply renders, with a
  screenshot at every step (positive-S1..S8).
- .maestro/flows/activation-negative-401.yaml: same setup against the
  --fail-auth server, asserts Quick Connect's existing "Connection Failed"
  alert is shown (not silently swallowed) and that the connection is not
  saved. Flags in comments that Advanced-mode Save (handleAdvancedSave)
  still has no testConnection() check and is a known, uncovered gap.
- testID props added (no restructuring) to the screens/components the
  flows drive: TelemetryConsentModal, connection/add.tsx, tabs/index.tsx,
  session/[id].tsx, MessageBubble.
- .github/workflows/activation-e2e.yml: new CI job — Android emulator via
  reactivecircus/android-emulator-runner, builds the debug-signed APK,
  starts both mock server instances, runs both Maestro flows, uploads
  screenshots via actions/upload-artifact. Kept separate from the existing
  vision-driven cua-smoke.yml, which needs a live server + LLM and isn't
  suited to tight deterministic regression assertions.
- .gitignore: Maestro takeScreenshot output is never committed.

Verified locally: mock server exercised standalone via curl (health,
project/current, path, session create, SSE event ordering, message
persistence) in both normal and --fail-auth modes; both Maestro flow
files validated as well-formed YAML; tsc --noEmit clean on all changed
files. No lint script exists in this repo (N/A). Full emulator execution
was not run — no Android SDK/emulator available in this environment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NJKAQ6HAikWGQK7PGZ5Y4E
2026-07-16 15:56:47 -07:00
engineer
f2b3e5d0a8 merge: feat/feedback-automation — cron product-intelligence, review-triage workflow, store-review prompt (reviewed, 2 findings fixed) 2026-07-16 15:54:07 -07:00
engineer
d4555d45b4 fix(feedback): don't count errored sessions; mark review asked before requesting
Review findings on the store-review prompt:

1. SessionStatus has no error variant and session.error never touches
   sessionStatus, so an errored session still ends busy -> idle and was
   counted as a success — potentially burning the once-ever review prompt
   on a failed run. Track an erroredSessions set: mark in the
   session.error handler, clear when the session goes busy again (new
   run) and on disconnect, and skip recordSuccessfulSession() on the
   busy -> idle transition if the session errored.

2. ASKED_KEY was persisted only after requestReview() resolved. On iOS
   requestReview() can throw (MissingCurrentWindowSceneException while
   backgrounded — likely, since sessions often complete in background),
   which would retry the prompt on later successes, violating the
   "at most once, ever" contract. Persist ASKED_KEY before calling
   requestReview(); a failed attempt consumes the one shot.
2026-07-16 15:53:24 -07:00
engineer
42ceea3e1f fix(sentry): repair Android source-map upload (debug IDs + release/dist match)
Releases 0.4.3-0.4.7 uploaded zero source-map files to Sentry, leaving every
JS frame unsymbolicated (app:///index.android.bundle:1). Root-caused two
independent bugs:

1. No metro.config.js existed, so Metro never ran Sentry's debug-ID
   injection. Without an embedded debug ID, sentry.gradle's upload task
   falls back to matching source maps to events by release/dist string
   alone (see has-sourcemap-debugid.js check in sentry.gradle) - and that
   fallback was broken (see #2). Added metro.config.js wrapping Expo's
   default config with getSentryExpoConfig from @sentry/react-native/metro,
   the officially documented path for Expo + debug-ID symbolication.

   The installed @sentry/react-native@6.14.0 could not actually bundle with
   this enabled: its metro integration does a hard `require("metro/src/lib/
   countLines")`, a deep path metro 0.83.x (bundled by Expo SDK 54) no
   longer exposes via its package.json `exports` map, crashing every build.
   Bumped to ~6.22.0 (package.json:18), which vendors countLines and adds
   metro/private/* fallbacks for other deep metro imports. Verified via a
   real `npx expo export:embed` run: bundle and source map now share a
   matching `debugId`.

2. sentry.gradle's default release/dist for the upload is
   `${applicationId}@${versionName}+${versionCode}` (computed from
   android/app/build.gradle), which never matched what Sentry.init() reports
   at runtime (`opencode-mobile@${app.json version}`, src/lib/sentry.ts:33-34).
   Every source map was therefore filed under a release Sentry never
   queries. Added a "Set Sentry release identifiers" step to build.yml,
   publish-play-store.yml, and publish-fdroid.yml that exports
   SENTRY_RELEASE/SENTRY_DIST from app.json's version before the Gradle
   build step, forcing an exact match.

Also filled in organization/project on the `@sentry/react-native/expo`
plugin in app.json (previously a bare string, which only warned "Missing
config for organization, project" and relied on env-var fallback) so
android/sentry.properties is generated deterministically instead of by
accident/history.

Verified locally (no push - GitHub is down, consolidating to local main):
- npx expo export:embed (real Metro bundle) succeeds and embeds a matching
  debugId in both index.android.bundle and its .map
- npm run typecheck: clean
- npm test: 81/81 passing
- Full ./gradlew Android build not verified: this machine has no
  ANDROID_HOME/SDK and a JDK/Gradle-wrapper version mismatch unrelated to
  this change; CI's Java 17 + Android SDK toolchain is unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NJKAQ6HAikWGQK7PGZ5Y4E
2026-07-16 15:51:31 -07:00
engineer
f0a7c1d868 feat: add browsable directory picker for new sessions and project switch
Users had to type an absolute server path on a phone keyboard to pick a
working directory (#49 "Choose project UIX"), and #57 reports that
only the default-drive project is ever discoverable. #52 already added
recents + client.project.list() as flat pickers, but there was still no
way to browse into subdirectories or discover paths the server hadn't
already indexed as a "project" — the only fallback was manual typing.

The opencode server already exposes a scoped filesystem-listing endpoint
(GET /file, handled in file.ts/handlers/file.ts) that resolves relative
to whatever directory the request is scoped to (header or query param) —
no new server endpoint is needed. Add file.list() to the mobile SDK
client and a new DirectoryBrowserSheet that lists subdirectories one
level at a time (via clientForDirectory(dir) + file.list({path: "."})),
supports "up" navigation, and a manual jump-to-path field. Wire it into
both the "new session" modal and the existing DirectorySwitcher, so
recents/manual entry remain available as a fallback alongside browsing.

Residual gap: there's still no "list available drives" API, so Windows
users with projects on D:, E:, etc. still need to type the drive root
once (it's then remembered via recents) — a full fix for #57 would need
a small server-side addition to enumerate mounted volumes.
2026-07-16 15:48:38 -07:00
engineer
ace8c19816 feat(analytics): add consent-gated activation-funnel analytics via PostHog
Installs are up 615% but 7-day retention is ~0% and we had no analytics SDK
to see where users drop off. Adds a thin PostHog wrapper (src/lib/analytics.ts)
that tracks app_opened, connection_form_submitted, connection_attempted,
connection_succeeded/failed (with a coarse error_class, e.g. the known 401
auth bug), message_sent, and response_received.

PostHog was chosen over Aptabase for its GMS-free JS-only RN SDK (fine for
the F-Droid/no-Firebase build), EU-hosted/self-host option, and generous
free tier. Analytics shares the exact same consent flag as Sentry
(telemetry.ts now gates both) so zero network calls happen without explicit
opt-in.

Requires a new EXPO_PUBLIC_POSTHOG_KEY CI secret (wired into build.yml,
publish-fdroid.yml, publish-play-store.yml, and documented in
publish-app-store.yml alongside the existing Sentry secrets).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NJKAQ6HAikWGQK7PGZ5Y4E
2026-07-16 15:48:16 -07:00
engineer
d3ee3d9e82 feat(feedback): prompt for a store review after successful sessions
Add expo-store-review (SDK 54-matched via `expo install`) and wire a
one-time in-app rating prompt into the SSE busy->idle "session completed"
transition in stores/events.ts — the same signal that already drives the
"Task completed" notification, so it only fires on genuine success, never
on session.error.

State (success count, one-time "asked" flag) persists in expo-secure-store,
mirroring the consent pattern in telemetry.ts. The threshold check is split
into store-review-policy.ts, free of expo imports, so it's unit-testable
with plain `node --test` (same split as buildAuth in auth.ts).

F-Droid/Play-Services-absent safety comes from the library itself:
StoreReview.isAvailableAsync() resolves false there, so requestReview() is
never called and there's no store-URL fallback configured in app.json.
2026-07-16 15:46:28 -07:00
engineer
e72e82df8e feat(ci): add Play Store review triage workflow
scripts/triage-reviews.py was fully written but had no workflow, so it
never ran. Add a daily 07:00 UTC cron (staggered after product-intelligence)
plus workflow_dispatch, with Python 3.12 + the Android Publisher API client
deps the script imports, and GOOGLE_SERVICE_ACCOUNT_JSON / GH_TOKEN passed
through as named secrets.

Also fix a stale doc-string reference: the issue body linked to a
non-existent monitor-reviews.yml; point it at the workflow actually created.
2026-07-16 15:46:21 -07:00
engineer
14a130cf66 feat(ci): schedule daily product-intelligence run
The workflow existed with only workflow_dispatch, so it never ran on its
own. Add a daily 06:00 UTC cron alongside the manual trigger.
2026-07-16 15:46:16 -07:00
Den
a5723bf087 feat: improve project picker with recents and server projects (#52)
Adds recent and server-project discovery to the new-session directory picker. Reviewed against current main; Android, iOS Simulator, and mandatory CUA checks are green.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-15 12:06:46 -07:00
Den
5c14ce0a5d feat: add daily product intelligence and versioned site assets (#64)
Adds privacy-safe aggregate product intelligence, reviewed/versioned website assets, and a dispatch-only rollout until the dedicated Sentry token is verified. Independent review blockers were fixed in 8bc47e4; app checks, website production build, Android CI, and iOS CI are green.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-15 11:59:40 -07:00
Den
cbe00c3222 feat: prepare iOS build and TestFlight CI (#66)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-14 11:16:25 -07:00
Dennis V
d1071b2a44 fix(ios): close final release review blockers
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-14 17:49:53 +00:00
Dennis V
c287ecad70 docs(agents): switch browser automation to VibeBrowser
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-14 16:56:56 +00:00
Dennis V
f9b18a06f3 fix(privacy): stop telemetry on consent revocation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-14 06:30:09 +00:00
Dennis V
679475d0a4 docs(site): publish iOS implementation progress
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-14 06:16:36 +00:00
Dennis V
791588647b feat(ios): add native build and TestFlight automation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-14 06:16:36 +00:00
Dennis V
d546c41e22 fix(cua): connect phase — verify by connection entry appearing, not by reading tiny URL text 2026-06-24 08:48:42 +00:00
Dennis V
378e6ecf0a fix(cua): connect phase — app uses separate IP + Port fields, button is 'Connect' below scroll 2026-06-24 08:23:34 +00:00
Dennis V
28f8182f09 fix(cua): session_list phase — tapping connection only sets active, must then navigate to Sessions tab 2026-06-24 08:00:11 +00:00
Dennis V
2ed26dcbe2 fix(cua): increase default max_steps_per_phase to 25 — connect phase needs more room for first-launch UI navigation 2026-06-24 07:34:21 +00:00
Dennis V
00409c71b4 fix(ci): use temp script file to avoid sh -c quoting issues with CUA dispatch 2026-06-24 07:07:29 +00:00
Dennis V
1f59602fce fix(ci): fix shell syntax in emulator runner script — avoid complex elfi chain, use flat script 2026-06-24 06:42:18 +00:00
Dennis V
00378ba7ed fix(ci): YAML syntax error — double-quotes in GH expression default value; add no-tap instruction to showcase typescript phase 2026-06-24 06:15:51 +00:00
Dennis V
d413d5f927 docs: add --e2e and --query to CI dispatch + AGENTS.md
cua-smoke.yml:
  - Add scenario/query/e2e_* workflow_dispatch inputs
  - Runner step dispatches to --query / --e2e / --showcase based on inputs
  - Upload /tmp/cua_eval_report.json as artifact (--query output)

AGENTS.md:
  - Document all 3 run modes: --showcase, --e2e, --query
  - List available models on dev server (deepseek-v4-flash-free etc.)
  - Add dispatch inputs reference for CI
  - Add --e2e / --query to 'when to run' guidance

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-24 04:57:03 +00:00
Dennis V
a305936f2b feat(cua): add --e2e and --query modes with structured evaluation
--e2e mode: full end-to-end coding task scenario
  - connect → long-press FAB to create session in custom project dir
  - select AI model via model picker (hint substring match)
  - submit coding task → DETERMINISTIC API poll for session idle
  - DETERMINISTIC API message scan for target filename
  - DETERMINISTIC ADB uiautomator check for filename in UI
  - LLM screenshot + visual evaluation summary

--query mode: natural-language test description → structured test run
  - LLM planner converts the query into JSON phases + deterministic checks
  - Executes each phase via the CUA loop (with critical/informational split)
  - Runs deterministic checks: ui_text | session_idle | file_created
  - LLM evaluator produces scored JSON report: overall/score/phases/recommendations

New helpers:
  - wait_for_session_idle(): polls GET /session until status==idle (no LLM)
  - check_session_file_created(): scans session messages API for filename
  - _api_base(): translates emulator host route for host-side API calls
  - run_scenario_hello_world_e2e(): 8-phase hardcoded e2e scenario
  - run_query_test(): planner → execute → evaluator pipeline

Also adds hello_world_e2e to --scenarios catalog for named invocation.

Usage:
  # Hardcoded e2e:
  python scripts/android-cua-smoke.py --e2e --opencode-url http://100.108.64.76:4096

  # Natural-language query:
  python scripts/android-cua-smoke.py --query \
    'Open android app. Connect to server. Open ~/workspace/opencode-mobile. \
     Choose deepseek model. Ask to write hello_world.py. Validate it was created.'

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-24 03:53:01 +00:00
Dennis V
4c2f6793de feat: add Vercel analytics to website + Play Store review triage script
- docs-site/index.html: add Vercel Analytics + Speed Insights CDN scripts
  (only fires on opencode.agentlabs.cc served via Vercel, not GitHub Pages)
- scripts/triage-reviews.py: fetch recent Play Store reviews via Android
  Publisher API, create GitHub issues for ≤3★ reviews not yet tracked

Run review triage manually on VM:
  DAYS_BACK=7 GOOGLE_SERVICE_ACCOUNT_JSON=... GH_TOKEN=... python3 scripts/triage-reviews.py

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-23 20:18:15 +00:00
Dennis V
4f10cd4ccf assets: expand to 7 screenshots covering full PRD app flow
Shows every major screen: connect, sessions list, chat input,
tool calls streaming, file writes, completed result, settings/model.

01 - Add connection screen (onboarding)
02 - Sessions list loaded from server
03 - Session chat view with message sent
04 - AI tool calls streaming (reading files)
05 - AI writing TypeScript files to disk
06 - Completed session — hello.ts created
07 - Settings + model selection

Website updated to show all 7 in a tighter grid (max-width 260px).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-23 17:31:57 +00:00
Dennis V
c1ced308cc assets: update screenshots and demo from CUA run 28041009456 (v0.4.7)
Fresh from successful onboarding showcase: connect → sessions → TypeScript
coding task → verify → settings. All critical phases PASS.

Screenshots:
  01: Sessions list loaded after connecting
  02: TypeScript hello.ts completed successfully (AI output visible)
  03: Sessions reload after navigation back

Demo: 10x speed (168s → 31s), 116KB mp4 + 1.2MB gif fallback.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-23 17:06:07 +00:00
Dennis V
7c4fdbdb57 assets: update Play Store phone screenshots from CUA run 28013924527
- 01.png: sessions list view
- 02.png: active TypeScript session (code generation)
- 03.png: settings/model selection screen

Uploaded to cc.agentlabs.opencode Play Console (en-US, phoneScreenshots).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-23 16:42:59 +00:00
Dennis V
3882e0d255 assets: update screenshots from CUA run 28013924527
- 01.png: sessions list view
- 02.png: active TypeScript session (code generation in progress)
- 03.png: settings/model selection screen

Source: CI run 28013924527 (cua-smoke.yml), upscaled 4x (1280×2560)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-23 16:41:15 +00:00
Dennis V
cc0f5217b1 release: bump to v0.4.7 (versionCode 34)
SSE disconnect banner, backgrounded permission notifications,
scrubUrl unit tests (13 tests, 81/81 suite green).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-23 16:39:43 +00:00
Dennis V
029bf2be04 fix(notifications): use user-friendly title and dedup keys for permission/question notifications
- Change permission notification title from `req.permission || 'Permission requested'`
  to the user-friendly 'Agent needs approval'; permission type + patterns now appear
  in the body (e.g. 'bash: echo hello') for context.
- Add `dedupeKey: `perm-${req.id}`` and `dedupeKey: `question-${req.id}``
  (60 s cooldown) to both events so a SSE reconnect after disconnect() clears state
  can't fire a second notification for the same pending request.
- Fix stale CUA-test comment that claimed 'Agent needs approval' did not exist;
  fallback assertion already matched correct title; update the comment to reflect
  the real events.ts behavior.

Closes #39

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-23 16:33:27 +00:00