Files
amethyst/.claude/CLAUDE.md
T
Claude 9b6d65c9f1 docs: require running material-symbols-subset.sh when adding new icons
The bundled Material Symbols font ships only the codepoints referenced
from MaterialSymbols.kt. Without regenerating the subset, newly added
icons render as tofu at runtime. Document this as a mandatory step in
CLAUDE.md so agents pick it up automatically.
2026-05-20 19:00:38 +00:00

16 KiB

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-<slug>.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 <verb> 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 <number> - 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:

    # 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

# 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:

// 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

// 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:

./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-<feature> or fix/desktop-<issue>
  • Commits: Conventional commits (feat:, fix:, etc.)
  • Never use --no-verify

Resources