| 1 | # Mobile app-store screenshot harness |
| 2 | |
| 3 | > For maintainers. Using T3 Code? See [docs/user](../user/). |
| 4 | |
| 5 | The screenshot harness runs the real mobile application against three disposable local T3 |
| 6 | environments. It creates an isolated base directory and server for each environment, real Git |
| 7 | projects with deterministic content, seeded orchestration projections, and persisted terminal |
| 8 | history. The app pairs with every server through its normal connection flow and React Navigation |
| 9 | opens the production Home, Thread, ThreadTerminal, ThreadReview, and SettingsEnvironments routes. |
| 10 | |
| 11 | No screenshot-specific screen recreates application UI. `EXPO_PUBLIC_SHOWCASE=1` only enables the |
| 12 | non-rendering pairing/readiness coordinator, disables terminal autofocus so captures do not contain |
| 13 | the software keyboard, and supplies deterministic T3 Connect discovery rows to the real |
| 14 | Environments screen. The local environment cards always come from real paired servers. |
| 15 | |
| 16 | ## Capture the default matrix |
| 17 | |
| 18 | From the repository root: |
| 19 | |
| 20 | pnpm screenshots:mobile |
| 21 | |
| 22 | The command: |
| 23 | |
| 24 | 1. Creates three temporary T3 base directories and starts a local server for each on an available |
| 25 | port. |
| 26 | 2. Creates T3 Code, React, and Linux Git repositories with recognizable favicons, feature branches, |
| 27 | and a deterministic T3 Code review diff. |
| 28 | 3. 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. |
| 30 | 4. Starts an isolated Metro server, builds the selected native apps, and boots each device. |
| 31 | 5. Pairs each clean app installation with Moonbase Terminal, Suspense Station, and Kernel Cabin. |
| 32 | 6. Navigates to the real application route for every requested scene. |
| 33 | 7. 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. |
| 35 | 8. Writes store-ready folders beneath `artifacts/app-store/screenshots/` that can be uploaded |
| 36 | directly to App Store Connect or Google Play Console. |
| 37 | |
| 38 | The servers, Metro, temporary root directory, and devices started by the runner are cleaned up after |
| 39 | capture. Pass `--keep-running` to retain them for inspection; the runner prints the base-directory |
| 40 | paths and server ports. |
| 41 | |
| 42 | Captures wait for the real environment snapshot to hydrate and for the requested route to become |
| 43 | active. Both platforms record readiness in the simulator/emulator app container. A final settle |
| 44 | delay allows native terminal and Git review data to finish rendering. |
| 45 | |
| 46 | A full capture regenerates the selected native project with Expo's clean production prebuild before |
| 47 | building it. Use --skip-build for repeated captures after the first build. |
| 48 | |
| 49 | The harness uses fixed Metro port `8199`, which separates it from Expo's normal default port but is |
| 50 | shared across every checkout. The readiness check only verifies that the port is open; it does not |
| 51 | verify process ownership. Concurrent screenshot harnesses in different worktrees can therefore |
| 52 | collide or attach to the wrong Metro process. |
| 53 | |
| 54 | Every 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 | |
| 58 | Pass `--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 |
| 60 | argument, the app applies it to both color schemes, and a scene only reports itself ready once the |
| 61 | requested palette is active — so a capture can never show the previous theme. `--theme all` |
| 62 | multiplies the run by six; only the native build is shared. |
| 63 | |
| 64 | The 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 | |
| 75 | Each target captures thread, terminal, review, thread list, and environments, and every target but |
| 76 | the iPad also captures agent activity. Each palette folder's five or six screenshots satisfy the configured Apple limit of 1–10, Google |
| 77 | phone requirement of 2–8, and Google tablet recommendation/slot minimum of 4 with a maximum of 8. |
| 78 | Every palette gets its own leaf folder so one upload slot never mixes themes and each folder keeps a |
| 79 | store-legal screenshot count. |
| 80 | |
| 81 | The agent-activity scene shows what a user sees away from the app. The app stages the same Live |
| 82 | Activity (iOS) or ongoing Live Update (Android) the relay would publish for four seeded threads. On |
| 83 | iOS the runner then locks the simulator and pushes the matching approval alert with `simctl push`; |
| 84 | on Android the staged update carries the alert and the runner opens the notification shade. Locking |
| 85 | the simulator and answering the notification permission prompt use |
| 86 | [AXe](https://github.com/cameroncooke/AXe), so install it (`brew tap cameroncooke/axe && brew |
| 87 | install axe`) or set `AXE_PATH` before capturing iOS. The iPad skips the scene because the lock |
| 88 | screen does not follow the app's landscape self-rotation. |
| 89 | |
| 90 | The 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 | |
| 102 | A light-only run writes the same tree under `light/`; `--appearance both` writes both appearance |
| 103 | folders, and each requested theme adds a sibling folder next to `t3-code/`. |
| 104 | |
| 105 | Edit [mobile-showcase.config.ts](../../scripts/mobile-showcase.config.ts) to change simulator or AVD |
| 106 | names, light/dark appearance, default palette, iOS orientation, scenes, output directory, capture |
| 107 | delay, 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 |
| 109 | appearance settings can never drift apart. |
| 110 | |
| 111 | ## Capture in GitHub Actions |
| 112 | |
| 113 | Run 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 |
| 115 | timeout from 60 to 300 minutes). The default dispatch captures both appearances of the `t3-code` |
| 116 | palette and runs iOS and Android concurrently: iPhone and iPad capture on a |
| 117 | 12-vCPU Blacksmith macOS runner, while Android phone, 7-inch tablet, and 10-inch tablet capture on a |
| 118 | 16-vCPU Blacksmith Linux runner with a KVM-accelerated x86_64 emulator. |
| 119 | |
| 120 | Every job uploads its PNGs even when capture fails, which makes partial runs useful for diagnosis. |
| 121 | The separate validation step is success-gated: it runs before upload only when capture succeeds. If |
| 122 | capture fails, the `always()` upload still publishes partial PNGs without re-validating them. |
| 123 | Download `app-store-connect-screenshots` and `google-play-screenshots` from the workflow run's |
| 124 | Artifacts section. Artifacts are retained for 14 days. |
| 125 | |
| 126 | The workflow uses the same checked-in device and scene matrix as local capture. Android remains |
| 127 | ARM64 by default for local Apple Silicon development; CI sets `T3_SHOWCASE_ANDROID_ABI=x86_64` so the |
| 128 | debug APK matches its accelerated emulator. |
| 129 | |
| 130 | ## Fast iteration |
| 131 | |
| 132 | Capture one scene or device: |
| 133 | |
| 134 | pnpm screenshots:mobile --device iphone-6.9 --scene thread |
| 135 | pnpm screenshots:mobile --platform android --scene review |
| 136 | |
| 137 | Override 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 | |
| 143 | Capture 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 | |
| 149 | Reuse the native build and retain the disposable environment: |
| 150 | |
| 151 | pnpm screenshots:mobile --device ipad-13 --skip-build --keep-running |
| 152 | |
| 153 | By default, let the screenshot runner start Metro on port `8199`. To keep Metro in a separate |
| 154 | terminal, 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 | |
| 159 | Then 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 |
| 164 | the harness's `--skip-metro` mode. |
| 165 | |
| 166 | List the matrix and flags: |
| 167 | |
| 168 | pnpm screenshots:mobile --list |
| 169 | |
| 170 | Validate 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 | |
| 184 | Fixture timestamps are generated relative to capture startup so every route shows stable relative |
| 185 | labels while the server still receives valid current data. The same deterministic three-environment |
| 186 | ensemble serves iPhone, iPad, Android phone, and Android tablet captures; responsive differences |
| 187 | come entirely from the production app layout. |
| 188 | |
| 189 | The Pending rows use the production offline outbox and point at the real T3 Code and React fixture |
| 190 | projects. Showcase coordination holds those two entries in the outbox for capture, just like a task |
| 191 | currently open for editing, so reconnecting the seeded environments cannot deliver and remove them |
| 192 | before the screenshot is taken. |
| 193 | |
| 194 | The Environments capture presents the three local fixture transports as a Tailscale HTTPS hostname, |
| 195 | a Helsinki VPS hostname, and a Tailnet IPv4 address. This display-only substitution keeps the cards |
| 196 | remote-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 | |
| 205 | The harness is the source of truth for upload dimensions; do not resize its output. If store rules |
| 206 | change, update the target's `storeAsset` specification. Capture fails when a PNG is the wrong size, |
| 207 | has alpha, is not 8-bit RGB, exceeds the configured file-size limit, violates Google Play's 9:16 |
| 208 | shape/bounds, or leaves a full output set below its store minimum. |