- 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).
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.
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
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.
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.
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.
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>
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>
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>
- 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>
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>
- 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>
Shows amber 'Reconnecting… (attempt N)' banner when SSE is down.
Shows brief green 'Connected ✓' flash on reconnect (useRef transition
to avoid atomic state reset bug where lastDisconnectAt resets with
reconnectAttempts in the same set() call).
Banner disappears automatically when SSE is stable.
Updates CUA scenario to check for both ASCII and Unicode ellipsis.
Closes#42
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
#39 (backgrounded permission notification):
- Event is permission.asked, not permission.requested (events.ts already calls
notify() for it; send() only fires while backgrounded).
- Assert on APP_PACKAGE (the only token guaranteed in every dumpsys record) plus
the actual copy ('Permission requested' / 'A tool needs your approval') instead
of the non-existent 'Agent needs approval' string.
#42 (SSE disconnect banner):
- reconnectAttempts only zeroes after STABLE_CONNECTION_MS (10s) past a healthy
reconnect, and the pending backoff timer can take up to 15s — so the banner can
linger ~25s. Poll up to 40s for dismissal instead of a fixed 15s sleep to avoid
a false 'still showing' failure.
Refs #39#42
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Rewrite <current_mission>: the Play Store publication blocker is cleared
(cc.agentlabs.opencode live on internal track), website opencode.agentlabs.cc
is live (HTTP 200), and the CUA sessions_reload phase is green (run 28002986180).
Refocus the mission on growth: Play Store promotion, F-Droid mainline, content
marketing, and organic SEO/ASO.
Closes#44
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- §4: path.get() is still live (feeds serverHome → directory switcher ~ expansion);
only the dead session-scoping plumbing (sessionScope.ts) was removed in 472ff8d.
Clarify rather than delete, since the call site is not dead.
- §2: document src/lib/speech.ts (experimental voice input; PRD §7 scope caveat).
- §7: app.json and package.json versions are kept in sync since v0.4.6 (both 0.4.6).
Refs #43
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds three deterministic helpers that use ADB instead of LLM vision,
so pass/fail cannot be hallucinated:
check_ui_text(text) — uiautomator XML dump + grep
check_notification_drawer(text, timeout) — dumpsys notification poll
simulate_network_drop() / restore_network() — svc wifi/data disable
Plus two new feature-test scenarios with deterministic gating:
sse_disconnect_banner (#42)
- ADB cuts WiFi+data, waits, checks UI XML for 'Reconnecting' text
- LLM visual check is supplementary/informational only
- ADB restores network, checks banner disappears
backgrounded_permission_notification (#39)
- ADB backgrounds app (Home key)
- API sends permission-triggering message
- adb dumpsys notification checked for 'Agent needs approval'
- No LLM involved in pass/fail decision
Also adds background_app() / foreground_app() helpers.
Wires new scenarios into --scenarios catalog alongside LLM ones.
Updates --scenarios help text to document both types.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
connect phase: now verifies the connection appears in the list after saving,
not just that the form was dismissed. Prevents false PASS when the LLM
declares connect done before the entry is actually visible.
_precreate_test_session: when external URL (Tailscale) times out from the
CI runner, fall back to localhost:4096 (the runner-local opencode serve).
This ensures the named-session assertion stays active in standard CI runs
while also working when dispatched against a live external server.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When dispatched with opencode_url set (e.g. Tailscale URL), the workflow
skips starting a local opencode server and the model probe, and points the
emulator directly at the external server.
This allows running the full CUA test against a live dev server without
depending on the flaky CI-hosted opencode serve setup.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The probe step computed a SCENARIOS output, but the emulator step runs
--showcase hardcoded and never consumes steps.probe.outputs.SCENARIOS, so
the selection logic was dead. Drop it and keep MODEL_CAPABLE as an
informational signal. Showcase mode handles model-unavailability gracefully
(typescript/verify phases are informational-only).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
sessionScopeDirectory() always returned null and ignored its home arg, so
the scope plumbing in loadSessions/createSession was dead: scopeDir was
always null, listClient always the default client, and the lazy path.get()
fetch fed only that dead branch. Collapse both call sites to use the
connection's default client directly and delete the now-orphaned
sessionScope.ts helper and its test.
serverHome is intentionally KEPT in connections.ts: it is still consumed by
the directory switcher UI (DirectorySwitcher.tsx, app/(tabs)/index.tsx) for
~ path expansion, so it is not dead code.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace stale opencode.vibebrowser.app / www.vibebrowser.app domain refs
with the current agentlabs.cc/opencode branding, and update the privacy
policy package id ai.opencode.mobile -> cc.agentlabs.opencode.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Screenshots from CUA smoke test showing:
- 01: Sessions list loading real sessions from server on connect
- 02: Streaming AI response in a coding session
- 03: Sessions tab after navigating back (sessions_reload regression guard)
Demo: 10x speed video (138s → 14s) of full onboarding flow.
Website now shows <video> with mp4 source + gif fallback.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>