t3-code-android-nightly/docs/operations/mobile-app-store-screenshots.md
Julius Marminge 9c524d5771
fix(mobile): capture a lit 6.9-inch lock screen in the agent-activity showcase (#13522)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-24 17:18:05 -07:00

208 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Mobile app-store screenshot harness
> For maintainers. Using T3 Code? See [docs/user](../user/).
The screenshot harness runs the real mobile application against three disposable local T3
environments. It creates an isolated base directory and server for each environment, real Git
projects with deterministic content, seeded orchestration projections, and persisted terminal
history. The app pairs with every server through its normal connection flow and React Navigation
opens the production Home, Thread, ThreadTerminal, ThreadReview, and SettingsEnvironments routes.
No screenshot-specific screen recreates application UI. `EXPO_PUBLIC_SHOWCASE=1` only enables the
non-rendering pairing/readiness coordinator, disables terminal autofocus so captures do not contain
the software keyboard, and supplies deterministic T3 Connect discovery rows to the real
Environments screen. The local environment cards always come from real paired servers.
## Capture the default matrix
From the repository root:
pnpm screenshots:mobile
The command:
1. Creates three temporary T3 base directories and starts a local server for each on an available
port.
2. Creates T3 Code, React, and Linux Git repositories with recognizable favicons, feature branches,
and a deterministic T3 Code review diff.
3. Seeds each server's migrated SQLite database with playful threads, messages, activities, and
terminal history, then adds two persisted mobile-outbox tasks waiting to send.
4. Starts an isolated Metro server, builds the selected native apps, and boots each device.
5. Pairs each clean app installation with Moonbase Terminal, Suspense Station, and Kernel Cabin.
6. Navigates to the real application route for every requested scene.
7. Sets the requested system appearance and palette, normalizes status bars, converts captures to 24-bit RGB PNGs without alpha, and
validates dimensions, aspect ratio, file size, and screenshot count before succeeding.
8. Writes store-ready folders beneath `artifacts/app-store/screenshots/` that can be uploaded
directly to App Store Connect or Google Play Console.
The servers, Metro, temporary root directory, and devices started by the runner are cleaned up after
capture. Pass `--keep-running` to retain them for inspection; the runner prints the base-directory
paths and server ports.
Captures wait for the real environment snapshot to hydrate and for the requested route to become
active. Both platforms record readiness in the simulator/emulator app container. A final settle
delay allows native terminal and Git review data to finish rendering.
A full capture regenerates the selected native project with Expo's clean production prebuild before
building it. Use --skip-build for repeated captures after the first build.
The harness uses fixed Metro port `8199`, which separates it from Expo's normal default port but is
shared across every checkout. The readiness check only verifies that the port is open; it does not
verify process ownership. Concurrent screenshot harnesses in different worktrees can therefore
collide or attach to the wrong Metro process.
Every configured device defaults to dark appearance and the `t3-code` palette, so plain
`pnpm screenshots:mobile` produces 35 dark PNGs. Pass `--appearance light`, `--appearance dark`, or
`--appearance both` to override the configured appearance; `both` produces 70 PNGs.
Pass `--theme <id>` (repeatable) or `--theme all` to capture the app's other palettes: `t3-code`,
`t3-chat`, `grove`, `ocean`, `ember`, and `iris`. The runner hands the palette to the app as a launch
argument, the app applies it to both color schemes, and a scene only reports itself ready once the
requested palette is active — so a capture can never show the previous theme. `--theme all`
multiplies the run by six; only the native build is shared.
The default matrix is:
| Output folder | Capture target | Upload dimensions | Store slot |
| ------------------------------------- | ---------------------------- | ----------------- | ----------------------------------------- |
| `apple/iphone-6.9/dark/t3-code/` | disposable iPhone 17 Pro Max | 1320×2868 | App Store Connect iPhone 6.9-inch |
| `apple/iphone-6.5/dark/t3-code/` | disposable iPhone 14 Plus | 1284×2778 | App Store Connect iPhone 6.5-inch |
| `apple/ipad-13/dark/t3-code/` | iPad Pro 13-inch (M5) | 2752×2064 | App Store Connect iPad 13-inch, landscape |
| `google-play/phone/dark/t3-code/` | Pixel AVD at 420 dpi | 1080×1920 | Google Play phone, portrait 9:16 |
| `google-play/tablet-7/dark/t3-code/` | Pixel AVD at 600dp width | 1080×1920 | Google Play 7-inch tablet, portrait 9:16 |
| `google-play/tablet-10/dark/t3-code/` | Pixel AVD at 800dp width | 1440×2560 | Google Play 10-inch tablet, portrait 9:16 |
Each target captures thread, terminal, review, thread list, and environments, and every target but
the iPad also captures agent activity. Each palette folder's five or six screenshots satisfy the configured Apple limit of 1–10, Google
phone requirement of 2–8, and Google tablet recommendation/slot minimum of 4 with a maximum of 8.
Every palette gets its own leaf folder so one upload slot never mixes themes and each folder keeps a
store-legal screenshot count.
The agent-activity scene shows what a user sees away from the app. The app stages the same Live
Activity (iOS) or ongoing Live Update (Android) the relay would publish for four seeded threads. On
iOS the runner then locks the simulator and pushes the matching approval alert with `simctl push`;
on Android the staged update carries the alert and the runner opens the notification shade. Locking
the simulator and answering the notification permission prompt use
[AXe](https://github.com/cameroncooke/AXe), so install it (`brew tap cameroncooke/axe && brew
install axe`) or set `AXE_PATH` before capturing iOS. The iPad skips the scene because the lock
screen does not follow the app's landscape self-rotation.
The generated tree is deliberately aligned with the store upload fields:
artifacts/app-store/screenshots/
├── apple/
│ ├── iphone-6.9/dark/t3-code/{thread,terminal,review,threads,environments,agent-activity}.png
│ ├── iphone-6.5/dark/t3-code/{thread,terminal,review,threads,environments,agent-activity}.png
│ └── ipad-13/dark/t3-code/{thread,terminal,review,threads,environments}.png
└── google-play/
├── phone/dark/t3-code/{thread,terminal,review,threads,environments,agent-activity}.png
├── tablet-7/dark/t3-code/{thread,terminal,review,threads,environments,agent-activity}.png
└── tablet-10/dark/t3-code/{thread,terminal,review,threads,environments,agent-activity}.png
A light-only run writes the same tree under `light/`; `--appearance both` writes both appearance
folders, and each requested theme adds a sibling folder next to `t3-code/`.
Edit [mobile-showcase.config.ts](../../scripts/mobile-showcase.config.ts) to change simulator or AVD
names, light/dark appearance, default palette, iOS orientation, scenes, output directory, capture
delay, Android ABI, or viewport. The selectable palette ids come from `MOBILE_THEME_IDS` in
[themePalettes.ts](../../packages/shared/src/themePalettes.ts), so the harness and the app's
appearance settings can never drift apart.
## Capture in GitHub Actions
Run the `Mobile Showcase Screenshots` workflow from GitHub's Actions tab, choose `all`, `ios`, or
`android`, select `light`, `dark`, or `both`, and pick a palette (or `all`, which raises each job's
timeout from 60 to 300 minutes). The default dispatch captures both appearances of the `t3-code`
palette and runs iOS and Android concurrently: iPhone and iPad capture on a
12-vCPU Blacksmith macOS runner, while Android phone, 7-inch tablet, and 10-inch tablet capture on a
16-vCPU Blacksmith Linux runner with a KVM-accelerated x86_64 emulator.
Every job uploads its PNGs even when capture fails, which makes partial runs useful for diagnosis.
The separate validation step is success-gated: it runs before upload only when capture succeeds. If
capture fails, the `always()` upload still publishes partial PNGs without re-validating them.
Download `app-store-connect-screenshots` and `google-play-screenshots` from the workflow run's
Artifacts section. Artifacts are retained for 14 days.
The workflow uses the same checked-in device and scene matrix as local capture. Android remains
ARM64 by default for local Apple Silicon development; CI sets `T3_SHOWCASE_ANDROID_ABI=x86_64` so the
debug APK matches its accelerated emulator.
## Fast iteration
Capture one scene or device:
pnpm screenshots:mobile --device iphone-6.9 --scene thread
pnpm screenshots:mobile --platform android --scene review
Override the configured appearance or capture both variants:
pnpm screenshots:mobile --appearance light
pnpm screenshots:mobile --appearance dark
pnpm screenshots:mobile --appearance both
Capture other palettes:
pnpm screenshots:mobile --device iphone-6.9 --theme ocean
pnpm screenshots:mobile --device iphone-6.9 --theme ocean --theme ember
pnpm screenshots:mobile --device iphone-6.9 --theme all
Reuse the native build and retain the disposable environment:
pnpm screenshots:mobile --device ipad-13 --skip-build --keep-running
By default, let the screenshot runner start Metro on port `8199`. To keep Metro in a separate
terminal, start it with the same showcase environment and explicit harness port:
cd apps/mobile
APP_VARIANT=development EXPO_PUBLIC_SHOWCASE=1 pnpm exec expo start --dev-client --port 8199
Then run the capture from the repository root:
pnpm screenshots:mobile --skip-build --skip-metro --device iphone-6.9
`pnpm --filter @t3tools/mobile showcase` starts Expo on its normal port, so it is not compatible with
the harness's `--skip-metro` mode.
List the matrix and flags:
pnpm screenshots:mobile --list
Validate existing files without starting Metro, servers, simulators, or emulators:
pnpm screenshots:mobile --validate-only
pnpm screenshots:mobile --platform ios --validate-only
## Customize the seeded environment
- Project repository, thread projections, conversation, terminal transcript, and Git changes:
[mobile-showcase-environment.ts](../../scripts/mobile-showcase-environment.ts)
- Device and capture matrix:
[mobile-showcase.config.ts](../../scripts/mobile-showcase.config.ts)
- Simulator/emulator orchestration:
[mobile-showcase.ts](../../scripts/mobile-showcase.ts)
Fixture timestamps are generated relative to capture startup so every route shows stable relative
labels while the server still receives valid current data. The same deterministic three-environment
ensemble serves iPhone, iPad, Android phone, and Android tablet captures; responsive differences
come entirely from the production app layout.
The Pending rows use the production offline outbox and point at the real T3 Code and React fixture
projects. Showcase coordination holds those two entries in the outbox for capture, just like a task
currently open for editing, so reconnecting the seeded environments cannot deliver and remove them
before the screenshot is taken.
The Environments capture presents the three local fixture transports as a Tailscale HTTPS hostname,
a Helsinki VPS hostname, and a Tailnet IPv4 address. This display-only substitution keeps the cards
remote-first while the harness retains reliable loopback connections to its ephemeral servers.
## Local prerequisites
- iOS: Xcode command-line tools, the configured simulator runtimes, and installed CocoaPods.
- Android: SDK resolution checks `ANDROID_HOME`, then `ANDROID_SDK_ROOT`, then defaults to
`$HOME/Library/Android/sdk` on macOS or `$HOME/Android/Sdk` on other platforms. The resolved SDK
must provide `adb` and `emulator`, and the configured AVD must exist.
The harness is the source of truth for upload dimensions; do not resize its output. If store rules
change, update the target's `storeAsset` specification. Capture fails when a PNG is the wrong size,
has alpha, is not 8-bit RGB, exceeds the configured file-size limit, violates Google Play's 9:16
shape/bounds, or leaves a full output set below its store minimum.