feat(demo): add offline demo mode for zero-server activation (#108)

Installers with no self-hosted opencode server hit a dead end at the
empty Sessions state, contributing to ~0% 7-day retention. Adds a
fully offline, scripted /demo route reusing the real chat components
(MessageBubble, ToolCallCard/DiffView, PermissionPrompt) so new users
can see what opencode does before connecting anything, then funnels
them to Connect / the setup guide.

- src/lib/demo-script.ts: pure, hardcoded Message/Part fixture builder
  (no RN/store/network imports) — the isolation guarantee.
- app/demo.tsx: new /demo route rendering the scripted conversation
  via useMemo'd local state only; permission reply is local setState,
  never sessionClient.permission.reply().
- app/(tabs)/index.tsx: "Try a demo" button added to the no-connection
  empty state, placed after the existing add-connection-button so its
  position/testID for existing Maestro flows is unchanged.
- .maestro/flows/demo.yaml: new E2E flow covering the empty-state CTA
  through conversation, diff expand, permission approve, and the CTA
  reaching the real connect form.
- scripts/run-e2e-flows.sh: registers demo in NEWER_FLOWS (non-blocking)
  so it actually runs in CI.
- i18n: new sessionsList.empty.tryDemoButton and demo.* keys added to
  both en.json and zh-Hans.json (catalog-parity verified).

npm run typecheck: clean. npm test: 175/175 passing.

Co-authored-by: engineer <engineer@macbookpro.lan>
This commit is contained in:
Den
2026-07-17 19:04:58 -07:00
committed by GitHub
parent dfc38b3276
commit c9ec92d8b7
8 changed files with 514 additions and 1 deletions

94
.maestro/flows/demo.yaml Normal file
View File

@@ -0,0 +1,94 @@
appId: cc.agentlabs.opencode
name: Demo mode - offline scripted walkthrough (no server, zero-server activation)
---
# Covers the "Try a demo" activation path added for the ~0% 7-day retention
# problem: an installer with no self-hosted `opencode serve` server used to
# hit a dead end at the no-connection empty state (see activation-positive.yaml
# / activation-negative-401.yaml for the "has a server" paths). This flow
# never starts a mock opencode server and never taps add-connection-button —
# app/demo.tsx (src/lib/demo-script.ts) is fully offline, hardcoded, local
# data rendered through the same chat components a real session uses.
#
# Verifies: the empty-state CTA exists and doesn't disturb the pre-existing
# add-connection-button (same testID, same position — activation-positive.yaml
# still taps it first), the demo screen renders a scripted conversation
# (reasoning -> tool call -> diff -> permission prompt) through the real
# MessageBubble/ToolCallCard/DiffView/PermissionPrompt components, the
# permission step responds locally (no network — there is no server to talk
# to), and the closing CTA actually routes to the real connect form
# (app/connection/add.tsx), not a fake/dead link.
- launchApp:
clearState: true
- assertVisible:
id: "telemetry-consent-card"
- tapOn:
id: "telemetry-decline-button"
- assertVisible:
text: "No Connection"
- takeScreenshot: demo-S1_empty_state
# add-connection-button must still be the first/only thing tapped here — this
# flow only ever taps it in this reachability check (visible, not tapped),
# to lock in that its position wasn't disturbed by the new button.
- assertVisible:
id: "add-connection-button"
- tapOn:
id: "try-demo-button"
- assertVisible:
id: "demo-screen"
- assertVisible:
id: "demo-banner"
- takeScreenshot: demo-S2_demo_screen_opened
# Scripted conversation renders through the real chat components.
- assertVisible:
id: "chat-bubble-user"
- assertVisible:
text: "login button"
- assertVisible:
id: "chat-bubble-assistant"
- assertVisible:
text: "Thinking"
- takeScreenshot: demo-S3_conversation
# Tool call card is collapsed by default (same behavior as a real session) —
# expand it to reveal the diff.
- scrollUntilVisible:
element:
text: "Edit src/components/LoginButton.tsx"
direction: DOWN
timeout: 10000
- tapOn:
text: "Edit src/components/LoginButton.tsx"
- assertVisible:
id: "diff-view-scroll"
- takeScreenshot: demo-S4_diff_view
# Permission prompt (approve/permission step) — replying only updates local
# demo state, never a real sessionClient.permission.reply() network call.
- scrollUntilVisible:
element:
text: "Permission Required"
direction: DOWN
timeout: 10000
- takeScreenshot: demo-S5_permission_prompt
- tapOn:
text: "Allow"
- assertVisible:
text: "Tests passed"
- takeScreenshot: demo-S6_permission_approved
# Closing CTA routes to the REAL connect form, not a dead link.
- scrollUntilVisible:
element:
id: "demo-connect-button"
direction: DOWN
timeout: 10000
- takeScreenshot: demo-S7_cta
- tapOn:
id: "demo-connect-button"
- assertVisible:
id: "connect-ip-input"
- takeScreenshot: demo-S8_connect_form_reached