# Amethyst ## Project Overview Amethyst is a Nostr Client for Android that was made for Android-only and has been slowly switching over to a Kotlin Multiplatform project. The main modules are: `quartz`, `commons`, `amethyst`, `desktopApp`, `cli`, plus the audio-rooms transport stack `quic` + `nestsClient`. Quartz should contain implementations of Nostr specifications and utilities to help implement them. Commons stores shared code between Amethyst Android (`amethyst`) and Amethyst Desktop (`desktopApp`). The Desktop App is designed to be mouse first and so uses a completely different screen and navigation architecture while sharing the back end components with the android counterpart. `cli` ships `amy`, a non-interactive JVM command-line client that drives the same `quartz` + `commons` code — used by humans, agents, and interop tests. `quic` is a from-scratch pure-Kotlin QUIC v1 + HTTP/3 + WebTransport client (no JNI, no BouncyCastle), built because no Android-compatible Java QUIC library exists. `nestsClient` runs the audio-room protocol on top of `:quic` for the NIP-53 audio-rooms feature. It implements both IETF `draft-ietf-moq-transport-17` (under `moq/`) and **moq-lite Lite-03** (kixelated's variant, under `moq/lite/`); the production listener AND speaker paths both run on moq-lite to interop with the nostrnests reference relay. The IETF code is kept as a reference + unit-test implementation for any future IETF target; see `nestsClient/plans/2026-04-26-moq-lite-gap.md`. ## Architecture ``` amethyst/ ├── quartz/ # Nostr KMP library (protocol only, no UI) │ └── src/ │ ├── commonMain/ # Shared Nostr protocol, data models │ ├── androidMain/ # Android-specific (crypto, storage) │ ├── jvmMain/ # Desktop JVM-specific │ └── iosMain/ # iOS-specific ├── commons/ # Shared UI components (convert to KMP) │ └── src/ │ ├── commonMain/ # Shared composables, icons, state │ ├── androidMain/ # Android-specific UI utilities │ └── jvmMain/ # Desktop-specific UI utilities ├── quic/ # Pure-Kotlin QUIC v1 + HTTP/3 + WebTransport (audio-rooms transport) │ └── src/ │ ├── commonMain/ # Protocol, frame/packet codecs, TLS state machine │ ├── jvmAndroid/ # JCA-backed AEAD + UDP socket actuals │ └── commonTest/ # RFC vector + adversarial tests ├── nestsClient/ # Audio-room client (IETF MoQ-transport today; moq-lite phase pending) │ └── src/ │ ├── commonMain/ # MoQ session, NestsListener, audio glue │ └── jvmAndroid/ # Opus encode/decode, AudioRecord/AudioTrack ├── desktopApp/ # Desktop JVM application (layouts, navigation) ├── amethyst/ # Android app (layouts, navigation) └── cli/ # Amy — non-interactive CLI (JVM only, no Compose) ``` **Sharing Philosophy:** - `quartz/` = Nostr business logic, protocol, data (no UI) - `commons/` = Shared UI components, icons, composables, flows and ViewModels - `quic/` = Transport library (QUIC + HTTP/3 + WebTransport); reusable for any KMP project that needs MoQ. Has no Android-framework dependencies. - `nestsClient/` = MoQ + audio-rooms client; takes `:quic` as transport, Quartz for crypto, `MediaCodec` / `AudioRecord` / `AudioTrack` for audio. - `amethyst/` & `desktopApp/` = Platform-native layouts and navigation - `cli/` = Thin assembly layer over `quartz/` + `commons/` (no new logic allowed) **Plans per module:** design docs for new subsystems live in the owning module's `plans/YYYY-MM-DD-.md` (e.g. `cli/plans/`, `commons/plans/`). The global `docs/plans/` folder is frozen — don't add new plans there. ## Tech Stack | Layer | Technology | |-------|------------| | **Core** | Quartz (Nostr KMP) | | **UI** | Compose Multiplatform 1.10.3 | | **Async** | kotlinx.coroutines + Flow | | **Network** | OkHttp (JVM) | | **Serialization** | Jackson | | **DI** | Manual / Koin | | **Build** | Gradle 8.x, Kotlin 2.3.20 | ## Skills Specialized skills provide domain expertise with bundled resources and patterns: | Skill | Expertise | When to Use | |-------|-----------|-------------| | `nostr-expert` | Nostr protocol (Quartz library) | Event types, NIPs, tags, signing, Bech32, NIP-44, LargeCache | | `kotlin-expert` | Advanced Kotlin patterns | StateFlow, sealed classes, @Immutable, DSLs, common utilities | | `kotlin-coroutines` | Advanced async patterns | supervisorScope, callbackFlow, relay pools, testing | | `kotlin-multiplatform` | Platform abstraction | expect/actual, source sets, sharing decisions | | `compose-expert` | Shared UI components | Material3, state hoisting, recomposition, rich-text parsing | | `android-expert` | Android platform | Navigation, permissions, lifecycle, Material3, Coil image loading | | `desktop-expert` | Desktop platform | Window, MenuBar, keyboard shortcuts, DeckLayout | | `gradle-expert` | Build system | Dependencies, versioning, packaging, optimization | | `account-state` | `Account` + `LocalCache` | Per-user StateFlows, event store, adding account-scoped settings | | `relay-client` | Subscriptions & filter assembly | `ComposeSubscriptionManager`, assemblers, preloaders, EOSE | | `feed-patterns` | Feeds & DAL | `FeedFilter`, `AdditiveComplexFeedFilter`, `FeedViewModel` family | | `auth-signers` | `NostrSigner` implementations | Local, NIP-46 bunker, NIP-55 Android external signer | | `quartz-integration` | Quartz as an external library | Gradle setup, `NostrClient`, `KeyPair`, for external projects | | `amy-expert` | Amy CLI (`cli/` module) | Adding `amy ` commands, JSON output contract, extracting logic from `amethyst/` into `commons/` so CLI can call it | | `find-missing-translations` | Utility | Extract untranslated Android strings | | `find-non-lambda-logs` | Utility | Audit Log calls for lambda overloads | ### Technique-layer skills (Compose / Kotlin best practices) These are general Compose/Kotlin decision-framework skills (vendored from `chrisbanes/skills`). The skills above are **codebase-oriented** ("where is X in Amethyst, what pattern do we use"); these are **technique-oriented** ("what is the correct Compose/Kotlin design here"). They complement — not replace — the codebase skills: e.g. `compose-expert` tells you where shared composables live, `compose-slot-api-pattern` tells you how to shape their public API. | Skill | Expertise | Complements | |-------|-----------|-------------| | `compose-recomposition-performance` | Router: which recomposition axis is the problem | `compose-expert` | | `compose-stability-diagnostics` | Compiler reports, strong skipping, `ImmutableList` at UI boundaries | `compose-expert`, `kotlin-expert` | | `compose-state-deferred-reads` | Phase-aware state reads, block-form modifiers, provider lambdas | `compose-expert` | | `compose-slot-api-pattern` | `@Composable` slot design for reusable components | `compose-expert` | | `compose-modifier-and-layout-style` | `modifier` parameter conventions, chain construction, conditional hoisting | `compose-expert` | | `compose-side-effects` | `LaunchedEffect`/`DisposableEffect`/`SideEffect`, keys, `rememberUpdatedState` | `compose-expert` | | `compose-state-holder-ui-split` | State-holder vs plain-UI composable split | `compose-expert`, `feed-patterns` | | `kotlin-flow-state-event-modeling` | StateFlow/SharedFlow/Channel choice, sentinels, `stateIn`, `update {}` | `kotlin-expert` | | `kotlin-coroutines-structured-concurrency` | Stored-scope anti-pattern, `suspend` boundaries, `runBlocking`, cancellation | `kotlin-coroutines` | | `kotlin-types-value-class` | `@JvmInline value class` vs `data class`, Compose stability | `kotlin-expert` | ## Workflow **When you ask for a feature:** 1. **Quick skill assessment** - I identify which skills are relevant 2. **Propose which skills** - I present which skills I'll use for the task 3. **Get approval** - You review and approve (or adjust) the skill selection 4. **Review plan using approved skills** - I invoke the approved skills to create detailed implementation plan 5. **Execute with skills** - Skills collaborate to implement the feature **Example:** ``` You: "Add video support to notes" Me: "I'll use: - /nostr-expert (NIP-71 video events) - /compose-expert (video player UI) - /android-expert (platform video APIs) Proceed?" You: "yes" Me: [invokes skills to create plan] "Plan from skills: 1. nostr-expert: Use NIP-71 kind 34235 for video events... 2. compose-expert: Create VideoPlayer composable in commons... 3. android-expert: Use ExoPlayer for Android... Proceed with implementation?" You: "yes" Me: [implements using skill guidance] ``` ## Commands - `/desktop-run` - Build and run desktop app - `/nip ` - Get NIP implementation guidance ## Feature Workflow **CRITICAL: Always check existing implementations first before creating new code!** When picking up a new task or feature, follow this process: ### Step 0: Survey Existing Implementation (MANDATORY) **Before writing ANY code, thoroughly audit ALL modules:** 1. **Search for existing implementations across all modules:** ```bash # Search in quartz for protocol/business logic grep -r "class.*Manager\|object.*Cache\|class.*Filter" quartz/src/commonMain/ # Search in commons for UI components grep -r "@Composable.*Card\|@Composable.*View\|@Composable.*Dialog" commons/src/ # Search in amethyst for Android patterns grep -r "class.*ViewModel\|class.*Account\|class.*State" amethyst/src/main/java/ # Search for specific functionality grep -r "fun isFollowing\|fun subscribe\|fun getMetadata" {quartz,commons,amethyst}/src/ ``` 2. **Understand existing architecture patterns:** - Event stores and caching systems - State management patterns (StateFlow, mutable states) - ViewModel patterns and lifecycle handling - Filter builders and relay subscription patterns - UI component hierarchies 3. **Key principle:** Most logic already exists! Your job is to: - **Reuse** existing protocol/business logic from quartz - **Extract** shareable UI components AND ViewModels from amethyst to commons - Create **platform-specific** layouts/navigation for Desktop - **NOT** duplicate existing managers, caches, or state systems 4. **Document findings in implementation plan as a matrix:** | File/Component | Status | Location | Action | |----------------|--------|----------|--------| | FilterBuilders | ✅ Exists | quartz/relay/filters/ | Reuse as-is | | NoteCard | 📦 Extract | amethyst/ui/note/ → commons/ | Extract to commons | | HomeFeedViewModel | 📦 Extract | amethyst/ → commons/commonMain/viewmodels/ | Extract to commons | | ProfileCache | ⚠️ Avoid | N/A | Already in User/Account pattern | **Legend:** - ✅ **Reuse** - Exists and can be used directly - 📦 **Extract** - Exists in Android, needs extraction to commons - 🆕 **New** - Doesn't exist, needs creation (platform-specific only) - ⚠️ **Avoid** - Duplicate functionality, use existing pattern instead ### Step 1: Analyze Android Implementation After surveying (Step 0), deeply examine the Android implementation: 1. Find the relevant feature/component in `amethyst/` module 2. Understand the current implementation patterns 3. Identify dependencies and integrations 4. Map out what code can be shared vs platform-specific ### Step 2: Create Implementation Plan Before coding, create a plan that categorizes work into three buckets: | Category | Description | Location | |----------|-------------|----------| | **Android-Specific** | Platform-native layouts, navigation patterns | `amethyst/`, `androidMain/` | | **Reusable (Shared)** | Business logic, UI components, **ViewModels**, state management | `quartz/commonMain/`, `commons/commonMain/` | | **Desktop-Specific** | Desktop-native layouts, navigation patterns, platform APIs | `desktopApp/`, `jvmMain/` | ### Step 3: Code Sharing Strategy **Share:** - Business logic and data models → `quartz/commonMain/` - Major UI components (cards, lists, dialogs) → `commons/commonMain/` - **ViewModels** (state, business logic) → `commons/commonMain/viewmodels/` - Icons and visual assets → `commons/commonMain/` **Keep Platform-Native:** - **Screen composables** (layout, scaffolding) - Desktop uses `Window`, Android uses `Activity` - Navigation patterns (sidebar vs bottom nav) - Platform-specific interactions (gestures, keyboard shortcuts) - System integrations (notifications, file pickers) **Rationale:** ViewModels contain platform-agnostic state management (StateFlow/SharedFlow) and business logic. Screens consume ViewModels but render differently (Desktop sidebar + content area vs Android bottom nav). ### Step 4: Extract Shared Components When extracting UI components: 1. Identify reusable composables in Android code 2. Move to `commons/commonMain/` (consult `/compose-expert` for patterns) 3. Create expect/actual declarations for platform-specific behavior (consult `/kotlin-multiplatform`) 4. Update both Android and Desktop to use shared component **Note:** `quartz/` is protocol-only (no composables). Shared UI goes in `commons/` after converting it to KMP. ## Build Commands ```bash # Run desktop app ./gradlew :desktopApp:run # Run Android app ./gradlew :amethyst:installDebug # Build Quartz for all targets ./gradlew :quartz:build # Run tests ./gradlew test # Format code ./gradlew spotlessApply ``` ## Quartz KMP Structure The Quartz library uses expect/actual for platform-specific implementations: ```kotlin // commonMain - shared protocol logic expect class CryptoProvider { fun sign(message: ByteArray, privateKey: ByteArray): ByteArray fun verify(message: ByteArray, signature: ByteArray, publicKey: ByteArray): Boolean } // androidMain - uses secp256k1-kmp-jni-android actual class CryptoProvider { /* Android implementation */ } // jvmMain - uses secp256k1-kmp-jni-jvm actual class CryptoProvider { /* JVM implementation */ } ``` ## Key Patterns ### Platform Abstraction ```kotlin // commonMain expect fun openExternalUrl(url: String) // androidMain actual fun openExternalUrl(url: String) { context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) } // jvmMain (Desktop) actual fun openExternalUrl(url: String) { Desktop.getDesktop().browse(URI(url)) } ``` ## Icons The Material Symbols font bundled at `commons/src/commonMain/composeResources/font/material_symbols_outlined.ttf` is a **subset** that only contains the glyphs referenced from `commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/symbols/MaterialSymbols.kt`. **MANDATORY:** Whenever you add a new icon — i.e. introduce a `MaterialSymbol("\uXXXX")` codepoint that wasn't already referenced anywhere in `MaterialSymbols.kt` — you MUST regenerate the subset font by running: ```bash ./tools/material-symbols-subset/subset.sh ``` Commit the regenerated `material_symbols_outlined.ttf` alongside your `MaterialSymbols.kt` change. Without this step the new icon renders as tofu (□) at runtime because the glyph is not in the bundled font. Reusing a codepoint already present in `MaterialSymbols.kt` does NOT require regenerating. See `tools/material-symbols-subset/README.md` for details and prerequisites (`pip install fonttools brotli`). ## Code Formatting After completing any task that modifies Kotlin files, always run: ``` ./gradlew spotlessApply ``` Do this before considering the task complete. ### Kotlin Style - **Never write fully-qualified class names inline in function bodies.** Add an `import` for the class and reference it by its simple name. Write `Event` (with `import com.vitorpamplona.quartz...Event`), not `com.vitorpamplona.quartz...Event` in the middle of code. - The only acceptable inline fully-qualified names are: a genuine name collision (prefer `import ... as Alias` instead), or where the language requires it. Comments, KDoc, and string literals are exempt. ### Navigation Shell - **Desktop**: Sidebar + main content area - **Android**: Bottom navigation ## Git Workflow - Branch: `feat/desktop-` or `fix/desktop-` - Commits: Conventional commits (`feat:`, `fix:`, etc.) - Never use `--no-verify` ## Resources - [Nostr NIPs](https://github.com/nostr-protocol/nips) - [Compose Multiplatform](https://www.jetbrains.com/compose-multiplatform/) - [KMP Documentation](https://kotlinlang.org/docs/multiplatform.html)