1# Mobile app-store screenshot harness
2
3> For maintainers. Using T3 Code? See [docs/user](../user/).
4
5The screenshot harness runs the real mobile application against three disposable local T3
6environments. It creates an isolated base directory and server for each environment, real Git
7projects with deterministic content, seeded orchestration projections, and persisted terminal
8history. The app pairs with every server through its normal connection flow and React Navigation
9opens the production Home, Thread, ThreadTerminal, ThreadReview, and SettingsEnvironments routes.
10
11No screenshot-specific screen recreates application UI. `EXPO_PUBLIC_SHOWCASE=1` only enables the
12non-rendering pairing/readiness coordinator, disables terminal autofocus so captures do not contain
13the software keyboard, and supplies deterministic T3 Connect discovery rows to the real
14Environments screen. The local environment cards always come from real paired servers.
15
16## Capture the default matrix
17
18From the repository root:
19
20 pnpm screenshots:mobile
21
22The command:
23
241. Creates three temporary T3 base directories and starts a local server for each on an available
25 port.
262. Creates T3 Code, React, and Linux Git repositories with recognizable favicons, feature branches,
27 and a deterministic T3 Code review diff.
283. Seeds each server's migrated SQLite database with playful threads, messages, activities, and
29 terminal history, then adds two persisted mobile-outbox tasks waiting to send.
304. Starts an isolated Metro server, builds the selected native apps, and boots each device.
315. Pairs each clean app installation with Moonbase Terminal, Suspense Station, and Kernel Cabin.
326. Navigates to the real application route for every requested scene.
337. Sets the requested system appearance and palette, normalizes status bars, converts captures to 24-bit RGB PNGs without alpha, and
34 validates dimensions, aspect ratio, file size, and screenshot count before succeeding.
358. Writes store-ready folders beneath `artifacts/app-store/screenshots/` that can be uploaded
36 directly to App Store Connect or Google Play Console.
37
38The servers, Metro, temporary root directory, and devices started by the runner are cleaned up after
39capture. Pass `--keep-running` to retain them for inspection; the runner prints the base-directory
40paths and server ports.
41
42Captures wait for the real environment snapshot to hydrate and for the requested route to become
43active. Both platforms record readiness in the simulator/emulator app container. A final settle
44delay allows native terminal and Git review data to finish rendering.
45
46A full capture regenerates the selected native project with Expo's clean production prebuild before
47building it. Use --skip-build for repeated captures after the first build.
48
49The harness uses fixed Metro port `8199`, which separates it from Expo's normal default port but is
50shared across every checkout. The readiness check only verifies that the port is open; it does not
51verify process ownership. Concurrent screenshot harnesses in different worktrees can therefore
52collide or attach to the wrong Metro process.
53
54Every configured device defaults to dark appearance and the `t3-code` palette, so plain
55`pnpm screenshots:mobile` produces 35 dark PNGs. Pass `--appearance light`, `--appearance dark`, or
56`--appearance both` to override the configured appearance; `both` produces 70 PNGs.
57
58Pass `--theme <id>` (repeatable) or `--theme all` to capture the app's other palettes: `t3-code`,
59`t3-chat`, `grove`, `ocean`, `ember`, and `iris`. The runner hands the palette to the app as a launch
60argument, the app applies it to both color schemes, and a scene only reports itself ready once the
61requested palette is active — so a capture can never show the previous theme. `--theme all`
62multiplies the run by six; only the native build is shared.
63
64The default matrix is:
65
66| Output folder | Capture target | Upload dimensions | Store slot |
67| ------------------------------------- | ---------------------------- | ----------------- | ----------------------------------------- |
68| `apple/iphone-6.9/dark/t3-code/` | disposable iPhone 17 Pro Max | 1320×2868 | App Store Connect iPhone 6.9-inch |
69| `apple/iphone-6.5/dark/t3-code/` | disposable iPhone 14 Plus | 1284×2778 | App Store Connect iPhone 6.5-inch |
70| `apple/ipad-13/dark/t3-code/` | iPad Pro 13-inch (M5) | 2752×2064 | App Store Connect iPad 13-inch, landscape |
71| `google-play/phone/dark/t3-code/` | Pixel AVD at 420 dpi | 1080×1920 | Google Play phone, portrait 9:16 |
72| `google-play/tablet-7/dark/t3-code/` | Pixel AVD at 600dp width | 1080×1920 | Google Play 7-inch tablet, portrait 9:16 |
73| `google-play/tablet-10/dark/t3-code/` | Pixel AVD at 800dp width | 1440×2560 | Google Play 10-inch tablet, portrait 9:16 |
74
75Each target captures thread, terminal, review, thread list, and environments, and every target but
76the iPad also captures agent activity. Each palette folder's five or six screenshots satisfy the configured Apple limit of 1–10, Google
77phone requirement of 2–8, and Google tablet recommendation/slot minimum of 4 with a maximum of 8.
78Every palette gets its own leaf folder so one upload slot never mixes themes and each folder keeps a
79store-legal screenshot count.
80
81The agent-activity scene shows what a user sees away from the app. The app stages the same Live
82Activity (iOS) or ongoing Live Update (Android) the relay would publish for four seeded threads. On
83iOS the runner then locks the simulator and pushes the matching approval alert with `simctl push`;
84on Android the staged update carries the alert and the runner opens the notification shade. Locking
85the simulator and answering the notification permission prompt use
86[AXe](https://github.com/cameroncooke/AXe), so install it (`brew tap cameroncooke/axe && brew
87install axe`) or set `AXE_PATH` before capturing iOS. The iPad skips the scene because the lock
88screen does not follow the app's landscape self-rotation.
89
90The generated tree is deliberately aligned with the store upload fields:
91
92 artifacts/app-store/screenshots/
93 ├── apple/
94 │ ├── iphone-6.9/dark/t3-code/{thread,terminal,review,threads,environments,agent-activity}.png
95 │ ├── iphone-6.5/dark/t3-code/{thread,terminal,review,threads,environments,agent-activity}.png
96 │ └── ipad-13/dark/t3-code/{thread,terminal,review,threads,environments}.png
97 └── google-play/
98 ├── phone/dark/t3-code/{thread,terminal,review,threads,environments,agent-activity}.png
99 ├── tablet-7/dark/t3-code/{thread,terminal,review,threads,environments,agent-activity}.png
100 └── tablet-10/dark/t3-code/{thread,terminal,review,threads,environments,agent-activity}.png
101
102A light-only run writes the same tree under `light/`; `--appearance both` writes both appearance
103folders, and each requested theme adds a sibling folder next to `t3-code/`.
104
105Edit [mobile-showcase.config.ts](../../scripts/mobile-showcase.config.ts) to change simulator or AVD
106names, light/dark appearance, default palette, iOS orientation, scenes, output directory, capture
107delay, Android ABI, or viewport. The selectable palette ids come from `MOBILE_THEME_IDS` in
108[themePalettes.ts](../../packages/shared/src/themePalettes.ts), so the harness and the app's
109appearance settings can never drift apart.
110
111## Capture in GitHub Actions
112
113Run the `Mobile Showcase Screenshots` workflow from GitHub's Actions tab, choose `all`, `ios`, or
114`android`, select `light`, `dark`, or `both`, and pick a palette (or `all`, which raises each job's
115timeout from 60 to 300 minutes). The default dispatch captures both appearances of the `t3-code`
116palette and runs iOS and Android concurrently: iPhone and iPad capture on a
11712-vCPU Blacksmith macOS runner, while Android phone, 7-inch tablet, and 10-inch tablet capture on a
11816-vCPU Blacksmith Linux runner with a KVM-accelerated x86_64 emulator.
119
120Every job uploads its PNGs even when capture fails, which makes partial runs useful for diagnosis.
121The separate validation step is success-gated: it runs before upload only when capture succeeds. If
122capture fails, the `always()` upload still publishes partial PNGs without re-validating them.
123Download `app-store-connect-screenshots` and `google-play-screenshots` from the workflow run's
124Artifacts section. Artifacts are retained for 14 days.
125
126The workflow uses the same checked-in device and scene matrix as local capture. Android remains
127ARM64 by default for local Apple Silicon development; CI sets `T3_SHOWCASE_ANDROID_ABI=x86_64` so the
128debug APK matches its accelerated emulator.
129
130## Fast iteration
131
132Capture one scene or device:
133
134 pnpm screenshots:mobile --device iphone-6.9 --scene thread
135 pnpm screenshots:mobile --platform android --scene review
136
137Override the configured appearance or capture both variants:
138
139 pnpm screenshots:mobile --appearance light
140 pnpm screenshots:mobile --appearance dark
141 pnpm screenshots:mobile --appearance both
142
143Capture other palettes:
144
145 pnpm screenshots:mobile --device iphone-6.9 --theme ocean
146 pnpm screenshots:mobile --device iphone-6.9 --theme ocean --theme ember
147 pnpm screenshots:mobile --device iphone-6.9 --theme all
148
149Reuse the native build and retain the disposable environment:
150
151 pnpm screenshots:mobile --device ipad-13 --skip-build --keep-running
152
153By default, let the screenshot runner start Metro on port `8199`. To keep Metro in a separate
154terminal, start it with the same showcase environment and explicit harness port:
155
156 cd apps/mobile
157 APP_VARIANT=development EXPO_PUBLIC_SHOWCASE=1 pnpm exec expo start --dev-client --port 8199
158
159Then run the capture from the repository root:
160
161 pnpm screenshots:mobile --skip-build --skip-metro --device iphone-6.9
162
163`pnpm --filter @t3tools/mobile showcase` starts Expo on its normal port, so it is not compatible with
164the harness's `--skip-metro` mode.
165
166List the matrix and flags:
167
168 pnpm screenshots:mobile --list
169
170Validate existing files without starting Metro, servers, simulators, or emulators:
171
172 pnpm screenshots:mobile --validate-only
173 pnpm screenshots:mobile --platform ios --validate-only
174
175## Customize the seeded environment
176
177- Project repository, thread projections, conversation, terminal transcript, and Git changes:
178 [mobile-showcase-environment.ts](../../scripts/mobile-showcase-environment.ts)
179- Device and capture matrix:
180 [mobile-showcase.config.ts](../../scripts/mobile-showcase.config.ts)
181- Simulator/emulator orchestration:
182 [mobile-showcase.ts](../../scripts/mobile-showcase.ts)
183
184Fixture timestamps are generated relative to capture startup so every route shows stable relative
185labels while the server still receives valid current data. The same deterministic three-environment
186ensemble serves iPhone, iPad, Android phone, and Android tablet captures; responsive differences
187come entirely from the production app layout.
188
189The Pending rows use the production offline outbox and point at the real T3 Code and React fixture
190projects. Showcase coordination holds those two entries in the outbox for capture, just like a task
191currently open for editing, so reconnecting the seeded environments cannot deliver and remove them
192before the screenshot is taken.
193
194The Environments capture presents the three local fixture transports as a Tailscale HTTPS hostname,
195a Helsinki VPS hostname, and a Tailnet IPv4 address. This display-only substitution keeps the cards
196remote-first while the harness retains reliable loopback connections to its ephemeral servers.
197
198## Local prerequisites
199
200- iOS: Xcode command-line tools, the configured simulator runtimes, and installed CocoaPods.
201- Android: SDK resolution checks `ANDROID_HOME`, then `ANDROID_SDK_ROOT`, then defaults to
202 `$HOME/Library/Android/sdk` on macOS or `$HOME/Android/Sdk` on other platforms. The resolved SDK
203 must provide `adb` and `emulator`, and the configured AVD must exist.
204
205The harness is the source of truth for upload dimensions; do not resize its output. If store rules
206change, update the target's `storeAsset` specification. Capture fails when a PNG is the wrong size,
207has alpha, is not 8-bit RGB, exceeds the configured file-size limit, violates Google Play's 9:16
208shape/bounds, or leaves a full output set below its store minimum.