session.list() now fetches GET /experimental/session (all sessions across
every directory) and falls back to the legacy directory-scoped GET /session
only on 404 (older servers). A directory-less /session is directory-scoped and
returns [] when the active dir has no sessions, so the Recent Sessions list was
empty unless the user first picked a folder.
Global shaping (roots filter, title search, sort by time.updated desc, limit)
moved to a pure, unit-tested src/lib/session-list.ts (no expo/fetch import) with
10 node --test cases.
Co-authored-by: dzianisv <engineer@gray-knight-m1.local>
1. buildRequestHeaders: UTF-8-encode Basic-auth credentials before btoa()
so non-ASCII usernames/passwords don't throw (Hermes' btoa is Latin1-only
and the throw was an unhandled rejection that hung the connect spinner).
2. diagnostics classify(): check root.ok (server reachable) before
!internet.ok, so a reachable-but-failing server (e.g. wrong auth) is no
longer misdiagnosed as "no internet" just because the public-internet
probe also failed (captive portal, Tailscale-only network, etc).
3. sdk.ts createClient: strip trailing slashes from baseUrl once, so a
trailing-slash URL from Advanced mode / Edit screen doesn't produce a
double slash on every request path.
4. add.tsx / [id].tsx: wrap addConnection/updateConnection in try/catch so
a SecureStore failure after a successful test resets the spinner and
shows an alert instead of hanging forever. Adds
connection.shared.alerts.saveFailedTitle/saveFailedMessage (en + zh-Hans).
5. add.tsx / [id].tsx: build the diagnostics probe's auth with buildAuth()
instead of a hand-rolled expression, so the probe reproduces the real
request's credentials (previously Quick Connect's password-only case
sent no auth to the probe at all).
6. add.tsx handleQuickConnect: stop sending the shared `username` state,
which could carry a stray value typed earlier in Advanced mode and
silently override the "opencode" default after "Back to Quick".
Claude-Session: https://claude.ai/code/session_01T12AhSnQVrSxNnvwfCx2z6
Co-authored-by: engineer <engineer@macbookpro.lan>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(onboarding): clarify opencode-serve requirement and fail connect tests fast
New users bounce at ~0% 7-day retention because nothing tells them the app
needs a computer running `opencode serve` on the same network/Tailscale, and
a bad IP hangs for the full 30s request timeout before failing.
- Rewrite the no-connection empty state subtitle and add a "How to set up a
server" link to the setup guide (app/(tabs)/index.tsx, src/lib/links.ts).
- Surface the opencode-serve prerequisite as a one-line notice at the top of
the Quick Connect form, above the existing detailed help box
(app/connection/add.tsx).
- Give the interactive connection test (testConnection) its own 12s timeout
via an optional Client.global.health(timeoutMs) parameter, instead of
reusing the general 30s REQUEST_TIMEOUT_MS used for real session traffic
(src/lib/sdk.ts, src/stores/connections.ts).
- Mirror all new/changed strings in the zh-Hans catalog; catalog-parity test
keeps them in sync.
* docs(distribution): add retention analysis motivating first-run fixes
Diagnoses ~0% D7 retention as product-shape (no path to value without a
self-hosted server, no demo mode, store copy sets no expectation). Ranks
fixes and isolates the two owner-only strategic calls (store-copy honesty,
hosted OpenCode Connect).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T12AhSnQVrSxNnvwfCx2z6
* fix(onboarding): drop connect-screen prerequisite notice (kept off-screen the submit button in E2E)
The added notice pushed connect-submit-button below the fold, breaking the
Maestro activation-positive flow (and the other flows sharing the connect
prelude). The empty state already sets the opencode-serve expectation one
screen earlier, so this notice was redundant. Empty-state guidance + guide
link and the fast-fail connect timeout are unaffected and retained.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T12AhSnQVrSxNnvwfCx2z6
---------
Co-authored-by: engineer <engineer@macbookpro.lan>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The directory browser could only descend from a manually-typed path
since the SDK had no way to enumerate the server's filesystem roots
(multiple drives on Windows, mount points, home dir). Add
file.roots() to sdk.ts (GET /file/roots, added server-side in
dzianisv/opencode#238) and show the results as pinned top-level chips
in DirectoryBrowserSheet that jump straight into that root.
Degrades gracefully: older servers 404 on the new endpoint, which the
SDK turns into null, normalizeRoots() turns into an empty list, and
the browser just shows no chips — manual "Jump to path" entry keeps
working exactly as before.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Adds diagnostics-only instrumentation to attribute the Activation E2E
positive flow's "SSE reply never renders" failure (issue #90 mode B)
between (a) expo/fetch not streaming the SSE response on the Android
release APK, vs (b) a mock-side broadcast bug. Does not change app
behavior or fix the root cause — #90 stays open pending the next CI
run's enriched logs.
- tests/fixtures/mock-opencode-server.ts: per-request logging
(method/path/status), per-SSE-connection connect/disconnect logging
with live client count, per-broadcast event-type + client-count
logging, and a 2s SSE heartbeat comment so client-side silence
becomes unambiguous.
- src/lib/sdk.ts global.events(): logs on the first successful
reader.read() that returns data, and when the stream loop ends —
proves/disproves whether expo/fetch ever delivers a byte.
- scripts/run-e2e-flows.sh: the emulator->mock reachability probe used
toybox wget/nc, which don't work reliably on the API-28 image.
Replaced with a probe chain (curl, wget, mksh /dev/tcp, nc, then a
host-side fallback) that writes a clear PASS/FAIL/UNKNOWN verdict to
artifacts/diag/probe.txt without blocking the flow.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(auth): stop infinite SSE retry on 401/403 and surface auth failures
Root cause (Sentry OPENCODE-MOBILE-1, 309 events / 65 users): auth is static
HTTP Basic and no code path treated 401 specially. The SSE reconnect loop in
events.ts retried on a fixed backoff regardless of cause, so a bad password
spammed Sentry and drained battery forever with zero user feedback.
Advanced-mode connection save also had no pre-flight check and silently
persisted bad credentials as the active connection.
- src/lib/api-error.ts: new pure ApiAuthError/isAuthStatus/isAuthError module
(node --test covered) so 401/403 are distinguishable from other failures.
- src/lib/sdk.ts: request()/events() now throw ApiAuthError for 401/403
instead of a generic Error.
- src/stores/events.ts: the SSE loop stops retrying on an auth error and sets
a new `authError` flag instead of reconnecting forever; other errors keep
the existing backoff. Fires connection_failed (source: sse, error_class:
unauthorized) so it's visible in the existing funnel.
- app/(tabs)/index.tsx: sessions screen shows an "Authentication Failed"
state with a link to the connection edit screen when authError is set.
- app/connection/[id].tsx: saving edited credentials for the active
connection now reconnects SSE immediately instead of requiring an app
restart.
- app/connection/add.tsx: Advanced-mode save now runs the same testConnection
pre-flight as Quick Connect and shows the same "Connection Failed" alert
(with diagnostics/share-report) instead of silently saving bad credentials.
Closes#76
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NJKAQ6HAikWGQK7PGZ5Y4E
* fix(auth): add Retry button on 401 error state, widen ConnectionTestSource
- Authentication Failed screen now offers Retry alongside Check
Credentials, calling events store's connect() directly to restart
the SSE state machine on transient 401s without leaving the app.
- Widen ConnectionTestSource to include 'sse' (events.ts:389's
connection_failed track call) and note the activation funnel only
filters on source=onboarding.
Addresses PR #79 review follow-ups.
---------
Co-authored-by: engineer <engineer@gray-knight-m1.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat: edit/revert sent messages via server revert API
Wires the mobile client up to the opencode server's session.revert /
session.unrevert endpoints (the same primitive the desktop TUI uses to
edit the last message). Long-press a user message bubble -> "Edit
message" reverts it server-side and prefills the composer with its
text; a banner offers Undo while the revert is pending (it's only
cleaned up server-side on the next prompt). Degrades gracefully with
an alert on older servers that 404 the /revert route.
Closes#56
* fix(revert): address code review findings on edit/revert message flow
- Confirm before overwriting an in-progress composer draft when editing
a sent message (F2)
- Restore reverted message's file attachments into the composer, not
just its text (F3)
- Distinguish 401/403 from other revert failures with an accurate
"Authentication failed" message instead of a generic one (F4)
- Exclude optimistic "temp-" message IDs from the revert cutoff
comparison so concurrently-sent messages aren't hidden (F9)
---------
Co-authored-by: engineer <engineer@gray-knight-m1.local>
* 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>
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.
Extract the SSE chunk-buffering from sdk.ts into a pure SSEParser (sdk.ts delegates;
behavior unchanged) and pin the framing rules that are easy to break when an event
splits across network reads: partial trailing lines held until completed, frames
reassembled across 2-3 reads, [DONE] sentinel and empty/non-data lines filtered,
each frame emitted exactly once. typecheck clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extract the auth + directory-header encoding from sdk.ts into a pure, dep-free
headers.ts (sdk.ts now delegates — behavior unchanged) so the connection-critical
logic is unit-testable without expo/fetch. Add 8 header tests (ASCII passthrough,
non-ASCII/CJK percent-encoding stays header-safe, Basic auth, empty-dir handling)
and 8 logbuffer tests (serialization incl. circular-ref fallback, 200-entry ring
cap, copy semantics, formatting). typecheck clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>