| Dimension | Shared | Only in A | Only in B | Overlap |
|---|---|---|---|---|
| Sections | 4 | 10 | 11 | 16% |
| Commands | 0 | 2 | 8 | 0% |
| Section tags | 3 | 4 | 3 | 30% |
What each file covers
Sections
4 shared · 10 only in A · 11 only in B- − iOS Package (Native iOS App)
- − Key Architecture Decisions
- − Authentication Flow
- − Data Flow
- − iPad Support
- − From monorepo root:
- − From packages/ios/:
- − Transcript Bundle
- − Key Files
- − Testing
- + Android Package (Native Android App)
- + Overview
- + Key Architecture Rules
- + Transcript
- + Sync and Encryption
- + Persistence
- + Firebase / Notifications
- + Play Store screenshots and video
- + Builds, signing, and CI
- + Agent Guidance
- + Important Files
- Package Structure
- Development
- Prerequisites
- Commands
Commands
0 shared · 2 only in A · 8 only in B- − npm run ios:test:swift
- − npm run ios:build:transcript
- + npm run android:build:transcript
- + npm run android:test:unit
- + npm run android:assemble:debug
- + npm run android:assemble:release
- + npm run android:bundle:release
- + npm run android:screenshots
- + npm run android:walkthrough
- + npm run android:bundle:signed
Section tags
3 shared · 4 only in A · 3 only in B- − test
- − security
- − dependencies
- − monorepo
- + code-style
- + do-not
- + agent-behaviour
- setup
- build
- architecture
Line diff
nimbalyst/nimbalyst · packages/ios/CLAUDE.md
@@ −1 @@
1# iOS Package (Native iOS App)
2
3This package contains the native SwiftUI iOS/iPadOS app for Nimbalyst. It provides a mobile interface for viewing and interacting with AI sessions synced from the desktop Electron app via end-to-end encrypted WebSocket sync.
4
5The app is **pure Swift/SwiftUI** with no Capacitor or web framework dependency. The only web view is `TranscriptWebView` (WKWebView) which renders the rich chat transcript using the same React components as the desktop app.
6
7## Package Structure
8
9```
10packages/ios/
11 NimbalystNative/ # Swift Package - all business logic and UI
12 Sources/
13 App/ # AppState (root observable), ContentView, navigation
14 Auth/ # AuthManager (Stytch OAuth via ASWebAuthenticationSession)
15 Crypto/ # CryptoManager (AES-256-GCM, PBKDF2), KeychainManager
16 Database/ # DatabaseManager (GRDB migrations, queries)
17 Models/ # GRDB record types: Project, Session, Message, QueuedPrompt, SyncState
18 Notifications/ # NotificationManager (push notification registration)
19 Sync/ # SyncManager, WebSocketClient, SyncProtocol types
20 Utils/ # RelativeTimestamp, NimbalystColors
21 Views/ # All SwiftUI views
22 Tests/ # Unit and integration tests (68 tests)
23 Package.swift # Swift Package Manager manifest (GRDB dependency)
24
25 NimbalystApp/ # Xcode app target
26 Sources/ # App entry point (@main), DebugMenu
27 Resources/ # Assets.xcassets (AppIcon, Splash), transcript-dist bundle
28 project.yml # XcodeGen project definition
29
30 CryptoCompatibility/ # CommonCrypto bridging header for PBKDF2 key derivation
31
32 src/transcript/ # React transcript web bundle (loaded in WKWebView)
33 main.tsx # Entry point with Swift <-> JS bridge
34 styles.css # Styles with bundled Material Symbols font
35 fonts/ # Locally bundled Material Symbols TTF
36
37 vite.config.transcript.ts # Vite config for transcript bundle (IIFE format for file://)
38 transcript.html # HTML entry point for Vite build
39 dist-transcript/ # Build output (not committed)
40```
41
42## Key Architecture Decisions
43
44### Authentication Flow
451. QR pairing stores encryption seed + server URL in Keychain
462. Stytch OAuth stores JWT + user ID in Keychain
473. When both paired AND authenticated, managers initialize
484. Encryption key derived from seed + user ID via PBKDF2
49
50### Data Flow
51- **Sync**: WebSocket connection to CollabV3 Durable Object (same server as desktop)
52- **Encryption**: All session data encrypted with AES-256-GCM before transmission
53- **Storage**: GRDB (SQLite) with reactive `ValueObservation` for live UI updates
54- **Transcript**: WKWebView loads bundled React app, communicates via `webkit.messageHandlers.bridge`
55
56### iPad Support
57- `NavigationSplitView` for regular size class (sidebar + detail)
58- `NavigationStack` for compact size class (iPhone)
59
60## Development
61
62### Prerequisites
63- Xcode 16+
64- Node.js 20+ (for transcript bundle)
65- XcodeGen (`brew install xcodegen`)
66
67### Commands
68```bash
69# From monorepo root:
70npm run ios:test:swift # Run all 68 Swift tests
71npm run ios:build:transcript # Build transcript web bundle
72
73# From packages/ios/:
74cd NimbalystNative && swift test # Run tests directly
75cd NimbalystApp && xcodegen generate # Regenerate .xcodeproj
76open NimbalystApp/NimbalystApp.xcodeproj # Open in Xcode
77```
78
79### Transcript Bundle
80The Xcode pre-build script in `project.yml` automatically builds the transcript with Vite and copies it to `Resources/transcript-dist/`. You can also build manually:
81
82```bash
83npm run ios:build:transcript
84```
85
86Output: `dist-transcript/transcript.html` + `dist-transcript/assets/` (JS bundle + Material Symbols font).
87
88After building, copy the output to Xcode resources:
89```bash
90rm -f NimbalystApp/Resources/transcript-dist/assets/transcript-*.js
91cp dist-transcript/transcript.html NimbalystApp/Resources/transcript-dist/transcript.html
92cp dist-transcript/assets/* NimbalystApp/Resources/transcript-dist/assets/
93```
94
95**CRITICAL: React hooks rules in `src/transcript/main.tsx`**
96
97The transcript React app runs inside WKWebView where errors are invisible (cross-origin `window.onerror` reports "Script error." with no details). This makes hooks violations especially dangerous -- the screen goes blank with no diagnostic information.
98
99Rules for editing `TranscriptApp` in `main.tsx`:
100- **All hooks (`useState`, `useRef`, `useCallback`, `useMemo`, `useEffect`) must come BEFORE any early returns.** React requires the same hooks to run in the same order on every render. An early `return` before a hook means that hook runs on some renders but not others, crashing React with "Rendered more hooks than during the previous render."
101- **The `TranscriptErrorBoundary` wraps the app** to catch render errors and display them on screen + report to the native bridge. Do not remove it.
102- **The `postErrorToNative` helper** sends error details through `webkit.messageHandlers.bridge` so they appear in Xcode console logs with full stack traces. Use it in any new try-catch blocks.
103- **Test after any change**: Always rebuild the transcript (`npm run ios:build:transcript`), copy to Xcode resources, and rebuild in Xcode. Vite build success does NOT mean React will render correctly at runtime.
104
105## Key Files
106
107| File | Purpose |
108|------|---------|
109| `Sources/App/AppState.swift` | Root observable object; owns database, crypto, and sync managers |
110| `Sources/Sync/SyncManager.swift` | WebSocket sync with CollabV3; processes index responses and broadcasts |
111| `Sources/Sync/SyncProtocol.swift` | All wire protocol types (Codable structs with CodingKeys) |
112| `Sources/Database/DatabaseManager.swift` | GRDB schema migrations, queries, and project stats refresh |
113| `Sources/Crypto/CryptoManager.swift` | AES-256-GCM encrypt/decrypt, deterministic project ID encryption |
114| `Sources/Views/TranscriptWebView.swift` | WKWebView + Coordinator with JS bridge, TranscriptController |
115| `Sources/Views/SessionDetailView.swift` | Session detail with transcript, scroll-to-top, jump-to-prompt |
116| `Sources/Views/SessionListView.swift` | Time-grouped session list with search and swipe-to-delete |
117| `Sources/Views/ProjectListView.swift` | Project list sorted by last activity with desktop connection indicator |
118| `src/transcript/main.tsx` | React transcript app with `scrollToTop`, `scrollToMessage`, `getPromptList` JS bridge |
119
120## Testing
121- 68 Swift tests covering database, crypto, sync integration, and web view
122- See [TESTING.md](./TESTING.md) for CI/CD pipeline details
123- Tests run on both macOS (via Swift Package Manager) and iOS simulator (via Xcode)
124
nimbalyst/nimbalyst · packages/android/CLAUDE.md
@@ +1 @@
1# Android Package (Native Android App)
2
3This package contains the native Android app for Nimbalyst. It mirrors the iOS native app architecture where practical: a pure native mobile shell with a single embedded web transcript view that renders the shared React transcript bundle.
4
5## Overview
6
7The Android app is:
8
9- **Pure native Android** using Kotlin and Jetpack Compose
10- **Room-backed** for local persistence
11- **WebSocket-synced** with CollabV3 Durable Objects
12- **End-to-end encrypted** using the same seed + user-derived key model as iOS
13- **Transcript-rendered** through a single `WebView` that loads the bundled React transcript UI
14
15Voice agent features are intentionally out of scope for Android.
16
17## Package Structure
18
19```text
20packages/android/
21 app/
22 src/main/java/com/nimbalyst/app/
23 attachments/ # Image attachment preparation/compression
24 auth/ # Auth callback parsing
25 crypto/ # AES-GCM + PBKDF2 key derivation
26 data/ # Room entities, DAOs, repository
27 notifications/ # Android notification + FCM token plumbing
28 pairing/ # QR payload parsing and persistent pairing state
29 sync/ # WebSocket sync manager and wire protocol
30 transcript/ # WebView host and JS bridge
31 ui/ # Compose screens and app shell
32 src/test/ # Unit tests
33 src/transcript/ # Shared React transcript bundle entrypoint/assets
34 scripts/ # Transcript asset sync helpers
35```
36
37## Key Architecture Rules
38
39### Transcript
40
41- The transcript UI lives in `src/transcript/main.tsx` and is bundled into Android assets.
42- `TranscriptWebView.kt` is the Android host. `TranscriptBridge.kt` is the only place JS bridge actions should be decoded and routed.
43- Keep transcript behavior aligned with iOS unless Android-specific UX requires a different path.
44
45### Sync and Encryption
46
47- `SyncManager.kt` owns the device sync lifecycle, room joins, index updates, queued prompt handling, and session control messages.
48- `CryptoManager.kt` must remain wire-compatible with iOS and desktop. Be cautious with any PBKDF2, AES-GCM, or payload format changes.
49- User routing identity and crypto identity are distinct. Do not collapse them back into a single field.
50
51### Persistence
52
53- Room is the source of truth for local Android UI state.
54- Prefer repository/DAO changes over screen-local state duplication.
55- If you add persisted fields, update schema, migrations, and any seed/demo paths together.
56
57### Firebase / Notifications
58
59- `app/google-services.json` is local environment config. Do **not** commit it. The `google-services` Gradle plugin is applied conditionally (only when the file exists), so a build without it stays green and push stays inert.
60- Client push registration lives in `notifications/NotificationManager.kt`.
61- Server push delivery lives in the collab server, which is the sibling `nimbalyst-collab` repository, not this monorepo. Clone it next to this repo at `../nimbalyst-collab` (override with `COLLAB_SERVER_PATH`); collab tests are gated by `RUN_COLLAB_TESTS=1`. See `.github/workflows/ci.yml`. Android push changes usually require coordinated client + server work.
62
63## Development
64
65### Prerequisites
66
67- Android Studio Ladybug / AGP-compatible version for this project
68- JDK 17 for Gradle builds. The project targets `JavaVersion.VERSION_17` and `jvmTarget = "17"`, and Temurin 17 matches CI. A non-17 JDK (e.g. GraalVM) can fail the AGP `jlink` step.
69- Android SDK + emulator tooling
70- Node.js 20+ for transcript bundle builds
71
72### Commands
73
74From the repository root the npm scripts wrap the Gradle tasks:
75
76```bash
77npm run android:build:transcript # build the transcript bundle
78npm run android:test:unit # ./gradlew :app:testDebugUnitTest
79npm run android:assemble:debug # ./gradlew :app:assembleDebug
80npm run android:assemble:release # ./gradlew :app:assembleRelease
81npm run android:bundle:release # ./gradlew :app:bundleRelease
82```
83
84To invoke Gradle directly, point `JAVA_HOME` at a Temurin 17 install (no hard-coded user path):
85
86```bash
87cd packages/android
88JAVA_HOME=/path/to/temurin-17 ./gradlew :app:assembleDebug
89JAVA_HOME=/path/to/temurin-17 ./gradlew :app:testDebugUnitTest
90```
91
92### Play Store screenshots and video
93
94`npm run android:screenshots` and `npm run android:walkthrough` drive an emulator against the debug-only screenshot mode in `app/src/debug/java/com/nimbalyst/app/screenshots/` (inert stub in `app/src/release/`). Never move that code into `src/main` — it seeds demo data and a fake paired state. See [ANDROID_MARKETING_SCREENSHOTS.md](../../docs/ANDROID_MARKETING_SCREENSHOTS.md).
95
96### Builds, signing, and CI
97
98- The `google-services` plugin is applied only when `app/google-services.json` is present, so a build without it succeeds and push stays inert until the file is added.
99- CI can inject Firebase config from the optional `ANDROID_GOOGLE_SERVICES_JSON_BASE64` GitHub secret by decoding it to `app/google-services.json` before the Gradle build.
100- The release `signingConfig` reads the keystore path and credentials from environment variables: `NIMBALYST_ANDROID_KEYSTORE`, `NIMBALYST_ANDROID_KEYSTORE_PASSWORD`, `NIMBALYST_ANDROID_KEY_ALIAS`, `NIMBALYST_ANDROID_KEY_PASSWORD`. When the keystore is absent the release build is simply unsigned. Minification stays off (signed is not the same as minified).
101- CI builds both the APK and Play-ready AAB via `.github/workflows/android-build.yml`, which supplies the keystore and signing secrets to produce signed release artifacts when secrets are present. CI also decodes `google-services.json` from the `ANDROID_GOOGLE_SERVICES_JSON_BASE64` secret and fails a signed build if that secret is missing, so a signed AAB never ships with push silently inert.
102- To build a signed release locally, run `npm run android:bundle:signed` (wraps `scripts/android-bundle-signed.sh`). It pulls all signing secrets from the 1Password item `Nimbalyst Android Signing` (Nimbalyst vault) at build time via `op read`: the upload keystore is fetched to a temp file deleted on exit, and passwords/alias are injected into the Gradle env only. Never commit a keystore — `*.jks`/`*.keystore` are gitignored.
103
104Open `packages/android/` in Android Studio, not the repo root.
105
106## Agent Guidance
107
108- Read the root `CLAUDE.md` before changing this package.
109- Prefer following iOS behavior and naming when implementing cross-platform mobile features.
110- Do not commit secrets or local machine config such as:
111 - `app/google-services.json`
112 - `local.properties`
113 - build outputs
114- If Android Studio reports AGP incompatibility, the correct fix is usually to update Android Studio rather than downgrade AGP/Kotlin.
115- When changing sync protocol behavior, inspect the matching iOS code paths in this repo and the collab server code paths in the sibling `nimbalyst-collab` repository before editing.
116- When changing transcript bridge behavior, update or add Android tests in `app/src/test/` where possible.
117
118## Important Files
119
120| File | Purpose |
121| --- | --- |
122| `app/src/main/java/com/nimbalyst/app/NimbalystApplication.kt` | App-level dependency setup and startup wiring |
123| `app/src/main/java/com/nimbalyst/app/MainActivity.kt` | Activity entry point and deep-link handling |
124| `app/src/main/java/com/nimbalyst/app/ui/NimbalystAndroidApp.kt` | Root Compose app shell and navigation |
125| `app/src/main/java/com/nimbalyst/app/sync/SyncManager.kt` | Core mobile sync lifecycle and message handling |
126| `app/src/main/java/com/nimbalyst/app/sync/SyncProtocol.kt` | Android wire protocol types |
127| `app/src/main/java/com/nimbalyst/app/crypto/CryptoManager.kt` | Encryption and key derivation |
128| `app/src/main/java/com/nimbalyst/app/data/NimbalystDatabase.kt` | Room database definition |
129| `app/src/main/java/com/nimbalyst/app/transcript/TranscriptWebView.kt` | WebView transcript host |
130| `app/src/main/java/com/nimbalyst/app/transcript/TranscriptBridge.kt` | JS/native bridge handler |
131| `src/transcript/main.tsx` | Shared transcript app entry point for Android |
132
@@ −1 +1 @@
1−# iOS Package (Native iOS App)
1+# Android Package (Native Android App)
22
3−This package contains the native SwiftUI iOS/iPadOS app for Nimbalyst. It provides a mobile interface for viewing and interacting with AI sessions synced from the desktop Electron app via end-to-end encrypted WebSocket sync.
3+This package contains the native Android app for Nimbalyst. It mirrors the iOS native app architecture where practical: a pure native mobile shell with a single embedded web transcript view that renders the shared React transcript bundle.
44
5−The app is **pure Swift/SwiftUI** with no Capacitor or web framework dependency. The only web view is `TranscriptWebView` (WKWebView) which renders the rich chat transcript using the same React components as the desktop app.
5+## Overview
66
7+The Android app is:
8+
9+- **Pure native Android** using Kotlin and Jetpack Compose
10+- **Room-backed** for local persistence
11+- **WebSocket-synced** with CollabV3 Durable Objects
12+- **End-to-end encrypted** using the same seed + user-derived key model as iOS
13+- **Transcript-rendered** through a single `WebView` that loads the bundled React transcript UI
14+
15+Voice agent features are intentionally out of scope for Android.
16+
717 ## Package Structure
818
19+```text
20+packages/android/
21+ app/
22+ src/main/java/com/nimbalyst/app/
23+ attachments/ # Image attachment preparation/compression
24+ auth/ # Auth callback parsing
25+ crypto/ # AES-GCM + PBKDF2 key derivation
26+ data/ # Room entities, DAOs, repository
27+ notifications/ # Android notification + FCM token plumbing
28+ pairing/ # QR payload parsing and persistent pairing state
29+ sync/ # WebSocket sync manager and wire protocol
30+ transcript/ # WebView host and JS bridge
31+ ui/ # Compose screens and app shell
32+ src/test/ # Unit tests
33+ src/transcript/ # Shared React transcript bundle entrypoint/assets
34+ scripts/ # Transcript asset sync helpers
935 ```
10−packages/ios/
11− NimbalystNative/ # Swift Package - all business logic and UI
12− Sources/
13− App/ # AppState (root observable), ContentView, navigation
14− Auth/ # AuthManager (Stytch OAuth via ASWebAuthenticationSession)
15− Crypto/ # CryptoManager (AES-256-GCM, PBKDF2), KeychainManager
16− Database/ # DatabaseManager (GRDB migrations, queries)
17− Models/ # GRDB record types: Project, Session, Message, QueuedPrompt, SyncState
18− Notifications/ # NotificationManager (push notification registration)
19− Sync/ # SyncManager, WebSocketClient, SyncProtocol types
20− Utils/ # RelativeTimestamp, NimbalystColors
21− Views/ # All SwiftUI views
22− Tests/ # Unit and integration tests (68 tests)
23− Package.swift # Swift Package Manager manifest (GRDB dependency)
2436
25− NimbalystApp/ # Xcode app target
26− Sources/ # App entry point (@main), DebugMenu
27− Resources/ # Assets.xcassets (AppIcon, Splash), transcript-dist bundle
28− project.yml # XcodeGen project definition
37+## Key Architecture Rules
2938
30− CryptoCompatibility/ # CommonCrypto bridging header for PBKDF2 key derivation
39+### Transcript
3140
32− src/transcript/ # React transcript web bundle (loaded in WKWebView)
33− main.tsx # Entry point with Swift <-> JS bridge
34− styles.css # Styles with bundled Material Symbols font
35− fonts/ # Locally bundled Material Symbols TTF
41+- The transcript UI lives in `src/transcript/main.tsx` and is bundled into Android assets.
42+- `TranscriptWebView.kt` is the Android host. `TranscriptBridge.kt` is the only place JS bridge actions should be decoded and routed.
43+- Keep transcript behavior aligned with iOS unless Android-specific UX requires a different path.
3644
37− vite.config.transcript.ts # Vite config for transcript bundle (IIFE format for file://)
38− transcript.html # HTML entry point for Vite build
39− dist-transcript/ # Build output (not committed)
40−```
45+### Sync and Encryption
4146
42−## Key Architecture Decisions
47+- `SyncManager.kt` owns the device sync lifecycle, room joins, index updates, queued prompt handling, and session control messages.
48+- `CryptoManager.kt` must remain wire-compatible with iOS and desktop. Be cautious with any PBKDF2, AES-GCM, or payload format changes.
49+- User routing identity and crypto identity are distinct. Do not collapse them back into a single field.
4350
44−### Authentication Flow
45−1. QR pairing stores encryption seed + server URL in Keychain
46−2. Stytch OAuth stores JWT + user ID in Keychain
47−3. When both paired AND authenticated, managers initialize
48−4. Encryption key derived from seed + user ID via PBKDF2
51+### Persistence
4952
50−### Data Flow
51−- **Sync**: WebSocket connection to CollabV3 Durable Object (same server as desktop)
52−- **Encryption**: All session data encrypted with AES-256-GCM before transmission
53−- **Storage**: GRDB (SQLite) with reactive `ValueObservation` for live UI updates
54−- **Transcript**: WKWebView loads bundled React app, communicates via `webkit.messageHandlers.bridge`
53+- Room is the source of truth for local Android UI state.
54+- Prefer repository/DAO changes over screen-local state duplication.
55+- If you add persisted fields, update schema, migrations, and any seed/demo paths together.
5556
56−### iPad Support
57−- `NavigationSplitView` for regular size class (sidebar + detail)
58−- `NavigationStack` for compact size class (iPhone)
57+### Firebase / Notifications
5958
59+- `app/google-services.json` is local environment config. Do **not** commit it. The `google-services` Gradle plugin is applied conditionally (only when the file exists), so a build without it stays green and push stays inert.
60+- Client push registration lives in `notifications/NotificationManager.kt`.
61+- Server push delivery lives in the collab server, which is the sibling `nimbalyst-collab` repository, not this monorepo. Clone it next to this repo at `../nimbalyst-collab` (override with `COLLAB_SERVER_PATH`); collab tests are gated by `RUN_COLLAB_TESTS=1`. See `.github/workflows/ci.yml`. Android push changes usually require coordinated client + server work.
62+
6063 ## Development
6164
6265 ### Prerequisites
63−- Xcode 16+
64−- Node.js 20+ (for transcript bundle)
65−- XcodeGen (`brew install xcodegen`)
6666
67+- Android Studio Ladybug / AGP-compatible version for this project
68+- JDK 17 for Gradle builds. The project targets `JavaVersion.VERSION_17` and `jvmTarget = "17"`, and Temurin 17 matches CI. A non-17 JDK (e.g. GraalVM) can fail the AGP `jlink` step.
69+- Android SDK + emulator tooling
70+- Node.js 20+ for transcript bundle builds
71+
6772 ### Commands
68−```bash
69−# From monorepo root:
70−npm run ios:test:swift # Run all 68 Swift tests
71−npm run ios:build:transcript # Build transcript web bundle
7273
73−# From packages/ios/:
74−cd NimbalystNative && swift test # Run tests directly
75−cd NimbalystApp && xcodegen generate # Regenerate .xcodeproj
76−open NimbalystApp/NimbalystApp.xcodeproj # Open in Xcode
77−```
74+From the repository root the npm scripts wrap the Gradle tasks:
7875
79−### Transcript Bundle
80−The Xcode pre-build script in `project.yml` automatically builds the transcript with Vite and copies it to `Resources/transcript-dist/`. You can also build manually:
81−
8276 ```bash
83−npm run ios:build:transcript
77+npm run android:build:transcript # build the transcript bundle
78+npm run android:test:unit # ./gradlew :app:testDebugUnitTest
79+npm run android:assemble:debug # ./gradlew :app:assembleDebug
80+npm run android:assemble:release # ./gradlew :app:assembleRelease
81+npm run android:bundle:release # ./gradlew :app:bundleRelease
8482 ```
8583
86−Output: `dist-transcript/transcript.html` + `dist-transcript/assets/` (JS bundle + Material Symbols font).
84+To invoke Gradle directly, point `JAVA_HOME` at a Temurin 17 install (no hard-coded user path):
8785
88−After building, copy the output to Xcode resources:
8986 ```bash
90−rm -f NimbalystApp/Resources/transcript-dist/assets/transcript-*.js
91−cp dist-transcript/transcript.html NimbalystApp/Resources/transcript-dist/transcript.html
92−cp dist-transcript/assets/* NimbalystApp/Resources/transcript-dist/assets/
87+cd packages/android
88+JAVA_HOME=/path/to/temurin-17 ./gradlew :app:assembleDebug
89+JAVA_HOME=/path/to/temurin-17 ./gradlew :app:testDebugUnitTest
9390 ```
9491
95−**CRITICAL: React hooks rules in `src/transcript/main.tsx`**
92+### Play Store screenshots and video
9693
97−The transcript React app runs inside WKWebView where errors are invisible (cross-origin `window.onerror` reports "Script error." with no details). This makes hooks violations especially dangerous -- the screen goes blank with no diagnostic information.
94+`npm run android:screenshots` and `npm run android:walkthrough` drive an emulator against the debug-only screenshot mode in `app/src/debug/java/com/nimbalyst/app/screenshots/` (inert stub in `app/src/release/`). Never move that code into `src/main` — it seeds demo data and a fake paired state. See [ANDROID_MARKETING_SCREENSHOTS.md](../../docs/ANDROID_MARKETING_SCREENSHOTS.md).
9895
99−Rules for editing `TranscriptApp` in `main.tsx`:
100−- **All hooks (`useState`, `useRef`, `useCallback`, `useMemo`, `useEffect`) must come BEFORE any early returns.** React requires the same hooks to run in the same order on every render. An early `return` before a hook means that hook runs on some renders but not others, crashing React with "Rendered more hooks than during the previous render."
101−- **The `TranscriptErrorBoundary` wraps the app** to catch render errors and display them on screen + report to the native bridge. Do not remove it.
102−- **The `postErrorToNative` helper** sends error details through `webkit.messageHandlers.bridge` so they appear in Xcode console logs with full stack traces. Use it in any new try-catch blocks.
103−- **Test after any change**: Always rebuild the transcript (`npm run ios:build:transcript`), copy to Xcode resources, and rebuild in Xcode. Vite build success does NOT mean React will render correctly at runtime.
96+### Builds, signing, and CI
10497
105−## Key Files
98+- The `google-services` plugin is applied only when `app/google-services.json` is present, so a build without it succeeds and push stays inert until the file is added.
99+- CI can inject Firebase config from the optional `ANDROID_GOOGLE_SERVICES_JSON_BASE64` GitHub secret by decoding it to `app/google-services.json` before the Gradle build.
100+- The release `signingConfig` reads the keystore path and credentials from environment variables: `NIMBALYST_ANDROID_KEYSTORE`, `NIMBALYST_ANDROID_KEYSTORE_PASSWORD`, `NIMBALYST_ANDROID_KEY_ALIAS`, `NIMBALYST_ANDROID_KEY_PASSWORD`. When the keystore is absent the release build is simply unsigned. Minification stays off (signed is not the same as minified).
101+- CI builds both the APK and Play-ready AAB via `.github/workflows/android-build.yml`, which supplies the keystore and signing secrets to produce signed release artifacts when secrets are present. CI also decodes `google-services.json` from the `ANDROID_GOOGLE_SERVICES_JSON_BASE64` secret and fails a signed build if that secret is missing, so a signed AAB never ships with push silently inert.
102+- To build a signed release locally, run `npm run android:bundle:signed` (wraps `scripts/android-bundle-signed.sh`). It pulls all signing secrets from the 1Password item `Nimbalyst Android Signing` (Nimbalyst vault) at build time via `op read`: the upload keystore is fetched to a temp file deleted on exit, and passwords/alias are injected into the Gradle env only. Never commit a keystore — `*.jks`/`*.keystore` are gitignored.
106103
107−| File | Purpose |
108−|------|---------|
109−| `Sources/App/AppState.swift` | Root observable object; owns database, crypto, and sync managers |
110−| `Sources/Sync/SyncManager.swift` | WebSocket sync with CollabV3; processes index responses and broadcasts |
111−| `Sources/Sync/SyncProtocol.swift` | All wire protocol types (Codable structs with CodingKeys) |
112−| `Sources/Database/DatabaseManager.swift` | GRDB schema migrations, queries, and project stats refresh |
113−| `Sources/Crypto/CryptoManager.swift` | AES-256-GCM encrypt/decrypt, deterministic project ID encryption |
114−| `Sources/Views/TranscriptWebView.swift` | WKWebView + Coordinator with JS bridge, TranscriptController |
115−| `Sources/Views/SessionDetailView.swift` | Session detail with transcript, scroll-to-top, jump-to-prompt |
116−| `Sources/Views/SessionListView.swift` | Time-grouped session list with search and swipe-to-delete |
117−| `Sources/Views/ProjectListView.swift` | Project list sorted by last activity with desktop connection indicator |
118−| `src/transcript/main.tsx` | React transcript app with `scrollToTop`, `scrollToMessage`, `getPromptList` JS bridge |
104+Open `packages/android/` in Android Studio, not the repo root.
119105
120−## Testing
121−- 68 Swift tests covering database, crypto, sync integration, and web view
122−- See [TESTING.md](./TESTING.md) for CI/CD pipeline details
123−- Tests run on both macOS (via Swift Package Manager) and iOS simulator (via Xcode)
106+## Agent Guidance
107+
108+- Read the root `CLAUDE.md` before changing this package.
109+- Prefer following iOS behavior and naming when implementing cross-platform mobile features.
110+- Do not commit secrets or local machine config such as:
111+ - `app/google-services.json`
112+ - `local.properties`
113+ - build outputs
114+- If Android Studio reports AGP incompatibility, the correct fix is usually to update Android Studio rather than downgrade AGP/Kotlin.
115+- When changing sync protocol behavior, inspect the matching iOS code paths in this repo and the collab server code paths in the sibling `nimbalyst-collab` repository before editing.
116+- When changing transcript bridge behavior, update or add Android tests in `app/src/test/` where possible.
117+
118+## Important Files
119+
120+| File | Purpose |
121+| --- | --- |
122+| `app/src/main/java/com/nimbalyst/app/NimbalystApplication.kt` | App-level dependency setup and startup wiring |
123+| `app/src/main/java/com/nimbalyst/app/MainActivity.kt` | Activity entry point and deep-link handling |
124+| `app/src/main/java/com/nimbalyst/app/ui/NimbalystAndroidApp.kt` | Root Compose app shell and navigation |
125+| `app/src/main/java/com/nimbalyst/app/sync/SyncManager.kt` | Core mobile sync lifecycle and message handling |
126+| `app/src/main/java/com/nimbalyst/app/sync/SyncProtocol.kt` | Android wire protocol types |
127+| `app/src/main/java/com/nimbalyst/app/crypto/CryptoManager.kt` | Encryption and key derivation |
128+| `app/src/main/java/com/nimbalyst/app/data/NimbalystDatabase.kt` | Room database definition |
129+| `app/src/main/java/com/nimbalyst/app/transcript/TranscriptWebView.kt` | WebView transcript host |
130+| `app/src/main/java/com/nimbalyst/app/transcript/TranscriptBridge.kt` | JS/native bridge handler |
131+| `src/transcript/main.tsx` | Shared transcript app entry point for Android |
124132
