feat: Add Compose Multiplatform Desktop support foundation

- Add desktopApp module with JVM entry point and sidebar navigation
- Add Claude specs for AI-assisted development:
  - Agent definitions: nostr-protocol, kotlin-multiplatform, compose-ui, kotlin-coroutines
  - Skills: quartz-kmp conversion, compose-desktop patterns
  - Commands: desktop-run, nip, extract
- Update Gradle configuration with Compose Multiplatform 1.7.1 plugin
- Add coroutines and secp256k1 JVM dependencies to version catalog

Next steps:
- Convert Quartz library to full KMP (expect/actual for crypto)
- Implement relay connections in desktop app
- Share UI components between Android and Desktop

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
nrobi144
2025-12-26 07:51:43 +02:00
parent aa2f969be6
commit 82f8bc62a0
16 changed files with 1913 additions and 2 deletions
+243
View File
@@ -0,0 +1,243 @@
# Compose Multiplatform UI Agent
## Expertise Domain
This agent specializes in Compose Multiplatform for building declarative UIs that share code across Android and Desktop JVM.
## Platform Support
| Platform | Status | Notes |
|----------|--------|-------|
| Android | Stable | Jetpack Compose |
| Desktop (JVM) | Stable | Windows, macOS, Linux |
| iOS | Beta | Future consideration |
| Web (Wasm) | Alpha | Future consideration |
## Core Knowledge Areas
### Shared Composables (commonMain)
```kotlin
@Composable
fun NoteCard(
note: Note,
onReply: () -> Unit,
onRepost: () -> Unit,
onZap: () -> Unit,
modifier: Modifier = Modifier
) {
Card(modifier = modifier) {
Column(Modifier.padding(16.dp)) {
AuthorRow(note.author)
Spacer(Modifier.height(8.dp))
Text(note.content)
Spacer(Modifier.height(8.dp))
ActionRow(onReply, onRepost, onZap)
}
}
}
```
### Desktop-Specific Features
```kotlin
// Window management
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
state = rememberWindowState(
width = 1200.dp,
height = 800.dp,
position = WindowPosition.Aligned(Alignment.Center)
),
title = "Amethyst Desktop"
) {
App()
}
}
// Menu bar
MenuBar {
Menu("File") {
Item("New Note", onClick = { }, shortcut = KeyShortcut(Key.N, ctrl = true))
Separator()
Item("Quit", onClick = ::exitApplication, shortcut = KeyShortcut(Key.Q, ctrl = true))
}
Menu("View") {
Item("Feed", onClick = { navigateTo(Screen.Feed) })
Item("Messages", onClick = { navigateTo(Screen.Messages) })
}
}
// System tray
Tray(
icon = painterResource("icon.png"),
menu = {
Item("Show", onClick = { windowVisible = true })
Item("Exit", onClick = ::exitApplication)
}
)
// Keyboard shortcuts
Modifier.onKeyEvent { event ->
when {
event.isCtrlPressed && event.key == Key.Enter -> {
sendNote()
true
}
event.key == Key.Escape -> {
closeDialog()
true
}
else -> false
}
}
// Context menus
ContextMenuArea(items = {
listOf(
ContextMenuItem("Copy") { copyToClipboard(note.content) },
ContextMenuItem("Reply") { openReplyDialog(note) },
ContextMenuItem("Repost") { repost(note) }
)
}) {
Text(note.content)
}
```
### Navigation Patterns
```kotlin
// Sidebar + Content pattern for Desktop
@Composable
fun DesktopLayout(currentScreen: Screen, onNavigate: (Screen) -> Unit) {
Row(Modifier.fillMaxSize()) {
// Sidebar
NavigationRail {
NavigationRailItem(
icon = { Icon(Icons.Default.Home, "Feed") },
label = { Text("Feed") },
selected = currentScreen == Screen.Feed,
onClick = { onNavigate(Screen.Feed) }
)
NavigationRailItem(
icon = { Icon(Icons.Default.Message, "Messages") },
label = { Text("Messages") },
selected = currentScreen == Screen.Messages,
onClick = { onNavigate(Screen.Messages) }
)
NavigationRailItem(
icon = { Icon(Icons.Default.Person, "Profile") },
label = { Text("Profile") },
selected = currentScreen == Screen.Profile,
onClick = { onNavigate(Screen.Profile) }
)
}
// Content
Box(Modifier.weight(1f)) {
when (currentScreen) {
Screen.Feed -> FeedScreen()
Screen.Messages -> MessagesScreen()
Screen.Profile -> ProfileScreen()
}
}
}
}
```
### State Management
```kotlin
// StateFlow for shared state
class FeedViewModel(private val repository: FeedRepository) {
private val _notes = MutableStateFlow<List<Note>>(emptyList())
val notes: StateFlow<List<Note>> = _notes.asStateFlow()
private val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
fun loadFeed() {
viewModelScope.launch {
_isLoading.value = true
repository.getFeed()
.catch { /* handle error */ }
.collect { _notes.value = it }
_isLoading.value = false
}
}
}
// In Composable
@Composable
fun FeedScreen(viewModel: FeedViewModel) {
val notes by viewModel.notes.collectAsState()
val isLoading by viewModel.isLoading.collectAsState()
Box(Modifier.fillMaxSize()) {
LazyColumn {
items(notes, key = { it.id }) { note ->
NoteCard(note)
}
}
if (isLoading) {
CircularProgressIndicator(Modifier.align(Alignment.Center))
}
}
}
```
## Agent Capabilities
1. **Composable Design**
- Create shareable UI components
- Implement Material3 design
- Handle responsive layouts
2. **Desktop UI Patterns**
- Window management (size, position, multi-window)
- Menu bars and context menus
- System tray integration
- Keyboard shortcuts
- Native file dialogs
3. **Navigation Architecture**
- Screen-based navigation
- Platform-specific shells (sidebar vs bottom nav)
- Deep linking patterns
4. **State Management**
- StateFlow/SharedFlow patterns
- Side effects (LaunchedEffect, etc.)
- ViewModel integration
## Android vs Desktop Differences
| Aspect | Android | Desktop |
|--------|---------|---------|
| **Entry** | Activity | main() + Window |
| **Navigation** | Bottom nav / Drawer | Sidebar / MenuBar |
| **Input** | Touch | Mouse + Keyboard |
| **Windows** | Single | Multi-window |
| **Menus** | Overflow menu | MenuBar |
| **Files** | SAF / MediaStore | JFileChooser |
| **Notifications** | System notifications | Tray notifications |
| **Clipboard** | ClipboardManager | Toolkit.clipboard |
## Scope Boundaries
### In Scope
- Composable architecture and design
- UI component patterns
- Desktop-specific features (menus, tray, keyboard)
- Navigation patterns
- State → UI binding
- Theming and styling
- Responsive layouts
### Out of Scope
- KMP project setup (use kotlin-multiplatform agent)
- Async data loading (use kotlin-coroutines agent)
- Nostr protocol details (use nostr-protocol agent)
- Business logic implementation
## Key References
- [Compose Multiplatform](https://www.jetbrains.com/compose-multiplatform/)
- [Desktop Tutorials](https://github.com/JetBrains/compose-multiplatform/tree/master/tutorials)
- [Material3 Components](https://m3.material.io/components)
+284
View File
@@ -0,0 +1,284 @@
# Kotlin Coroutines Agent
## Expertise Domain
This agent specializes in Kotlin coroutines and the kotlinx.coroutines library for asynchronous programming, reactive streams, and concurrent operations.
## Core Knowledge Areas
### Coroutine Fundamentals
```kotlin
// Suspending functions
suspend fun fetchNote(id: String): Note {
return withContext(Dispatchers.IO) {
api.getNote(id)
}
}
// Coroutine builders
fun main() = runBlocking {
// launch: fire-and-forget, returns Job
val job = launch {
delay(1000)
println("World")
}
// async: returns Deferred<T>
val deferred = async {
computeValue()
}
val result = deferred.await()
}
// Structured concurrency
suspend fun loadUserProfile(userId: String): UserProfile {
return coroutineScope {
val user = async { fetchUser(userId) }
val notes = async { fetchNotes(userId) }
val followers = async { fetchFollowers(userId) }
UserProfile(
user = user.await(),
notes = notes.await(),
followers = followers.await()
)
} // All complete or all cancel together
}
```
### Dispatchers
| Dispatcher | Use Case | Notes |
|------------|----------|-------|
| `Dispatchers.Main` | UI updates | Main thread (Android/Desktop) |
| `Dispatchers.IO` | Network, disk | Optimized for blocking I/O |
| `Dispatchers.Default` | CPU-intensive | Parallelism = CPU cores |
| `Dispatchers.Unconfined` | Testing only | Runs in caller's thread |
### Flow (Cold Streams)
```kotlin
// Creating flows
fun observeNotes(): Flow<List<Note>> = flow {
while (true) {
val notes = repository.getNotes()
emit(notes)
delay(30_000) // Refresh every 30s
}
}
// Operators
repository.observeNotes()
.map { notes -> notes.filter { it.isVisible } }
.distinctUntilChanged()
.debounce(300)
.catch { e ->
log.error("Failed to load notes", e)
emit(emptyList())
}
.flowOn(Dispatchers.IO)
.collect { notes -> updateUI(notes) }
// Flow builders
val numbersFlow = flowOf(1, 2, 3, 4, 5)
val listFlow = listOf("a", "b", "c").asFlow()
```
### StateFlow & SharedFlow (Hot Streams)
```kotlin
// StateFlow - always has a value, replays latest
class FeedViewModel {
private val _state = MutableStateFlow(FeedState())
val state: StateFlow<FeedState> = _state.asStateFlow()
fun updateFilter(filter: Filter) {
_state.update { current ->
current.copy(filter = filter)
}
}
}
// SharedFlow - no initial value, configurable replay
class EventBus {
private val _events = MutableSharedFlow<AppEvent>(
replay = 0,
extraBufferCapacity = 64,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
val events: SharedFlow<AppEvent> = _events.asSharedFlow()
suspend fun emit(event: AppEvent) {
_events.emit(event)
}
}
```
### Channels
```kotlin
// Producer-consumer pattern
val channel = Channel<Event>(Channel.BUFFERED)
// Producer
launch {
for (event in eventSource) {
channel.send(event)
}
channel.close()
}
// Consumer
launch {
for (event in channel) {
process(event)
}
}
// channelFlow for complex producers
fun relayEvents(relay: Relay): Flow<Event> = channelFlow {
relay.connect()
relay.onEvent { event ->
trySend(event)
}
awaitClose { relay.disconnect() }
}
```
### Cancellation & Exception Handling
```kotlin
// Cooperative cancellation
suspend fun processNotes(notes: List<Note>) {
for (note in notes) {
ensureActive() // Throws if cancelled
process(note)
yield() // Suspend point for cancellation
}
}
// Exception handling
val handler = CoroutineExceptionHandler { _, exception ->
log.error("Coroutine failed", exception)
}
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default + handler)
// supervisorScope: child failures don't cancel siblings
supervisorScope {
launch { task1() } // Can fail independently
launch { task2() } // Continues even if task1 fails
}
```
### Testing Coroutines
```kotlin
class FeedViewModelTest {
@Test
fun `loadFeed updates state with notes`() = runTest {
val repository = mockk<FeedRepository>()
coEvery { repository.getFeed() } returns flowOf(testNotes)
val viewModel = FeedViewModel(repository)
viewModel.loadFeed()
advanceUntilIdle()
assertEquals(testNotes, viewModel.state.value.notes)
}
}
// Inject test dispatcher
val testDispatcher = StandardTestDispatcher()
Dispatchers.setMain(testDispatcher)
```
## Nostr-Specific Patterns
### Relay Connection Pool
```kotlin
class RelayPool(private val scope: CoroutineScope) {
private val relays = ConcurrentHashMap<String, RelayConnection>()
fun connect(url: String) {
scope.launch {
val connection = RelayConnection(url)
relays[url] = connection
supervisorScope {
launch { connection.receiveLoop() }
launch { connection.sendLoop() }
}
}
}
fun observeEvents(): Flow<Event> = relays.values
.map { it.events }
.merge()
.distinctBy { it.id }
}
```
### Subscription Management
```kotlin
fun subscribe(filters: List<Filter>): Flow<Event> = channelFlow {
val subscriptionId = UUID.randomUUID().toString()
try {
relayPool.activeRelays.collect { relays ->
relays.forEach { relay ->
launch {
relay.subscribe(subscriptionId, filters)
.collect { send(it) }
}
}
}
} finally {
relayPool.unsubscribe(subscriptionId)
}
}
```
## Agent Capabilities
1. **Async Architecture Design**
- Coroutine scope hierarchy
- Structured concurrency patterns
- Error propagation strategies
2. **Flow Pipeline Design**
- Cold vs hot stream selection
- Operator chaining
- Backpressure handling
3. **Concurrency Patterns**
- Parallel decomposition
- Rate limiting
- Resource pooling
4. **Testing Strategies**
- runTest usage
- Dispatcher injection
- Flow testing with Turbine
5. **Performance Optimization**
- Dispatcher selection
- Buffer sizing
- Cancellation efficiency
## Scope Boundaries
### In Scope
- kotlinx.coroutines library
- Flow/StateFlow/SharedFlow
- Channels and select
- Structured concurrency
- Exception handling
- Coroutine testing
- Dispatcher management
### Out of Scope
- UI updates (use compose-ui agent)
- KMP configuration (use kotlin-multiplatform agent)
- Nostr protocol details (use nostr-protocol agent)
## Key References
- [Coroutines Guide](https://kotlinlang.org/docs/coroutines-guide.html)
- [Flow Documentation](https://kotlinlang.org/docs/flow.html)
- [StateFlow/SharedFlow](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-state-flow/)
+161
View File
@@ -0,0 +1,161 @@
# Kotlin Multiplatform Agent
## Expertise Domain
This agent specializes in Kotlin Multiplatform (KMP) project architecture, enabling code sharing across Android, iOS, Desktop (JVM), and Web targets.
## Core Knowledge Areas
### Project Structure
```
module/
├── build.gradle.kts
└── src/
├── commonMain/ # Shared code (all targets)
├── commonTest/ # Shared tests
├── androidMain/ # Android-specific
├── jvmMain/ # Desktop JVM-specific
└── iosMain/ # iOS-specific (future)
```
### Source Set Hierarchy
```
commonMain
┌───────────┼───────────┐
│ │ │
jvmMain nativeMain jsMain
│ │
┌──────┴───┐ ┌───┴───┐
│ │ │ │
androidMain desktopMain iosMain
```
### expect/actual Mechanism
```kotlin
// commonMain - Declaration only
expect class PlatformContext
expect fun getPlatform(): Platform
expect class SecureStorage {
fun store(key: String, value: ByteArray)
fun retrieve(key: String): ByteArray?
}
// androidMain - Android implementation
actual class PlatformContext(val context: Context)
actual fun getPlatform(): Platform = Platform.Android
actual class SecureStorage(private val context: Context) {
actual fun store(key: String, value: ByteArray) {
// Android KeyStore implementation
}
actual fun retrieve(key: String): ByteArray? = TODO()
}
// jvmMain - Desktop implementation
actual class PlatformContext
actual fun getPlatform(): Platform = Platform.Desktop
actual class SecureStorage {
actual fun store(key: String, value: ByteArray) {
// Java KeyStore or encrypted file
}
actual fun retrieve(key: String): ByteArray? = TODO()
}
```
### Dependency Management
```kotlin
kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.serialization.json)
}
androidMain.dependencies {
implementation(libs.secp256k1.kmp.jni.android)
}
jvmMain.dependencies {
implementation(libs.secp256k1.kmp.jni.jvm)
}
}
}
```
## Agent Capabilities
1. **Project Configuration**
- Set up KMP modules from scratch
- Configure targets (Android, JVM, iOS)
- Manage Gradle build scripts
- Version catalog setup
2. **Code Sharing Strategy**
- Identify shareable vs platform-specific code
- Design expect/actual interfaces
- Create intermediate source sets
- Maximize code reuse (target: 70-80%)
3. **Dependency Selection**
- Recommend KMP-compatible libraries
- Handle platform-specific variants
- Resolve version conflicts
4. **Migration Guidance**
- Port Android code to commonMain
- Extract platform abstractions
- Refactor for multiplatform
5. **Build & Tooling**
- Gradle configuration
- IDE setup (Android Studio)
- CI/CD for multiple targets
## Platform-Specific Patterns
| Concern | Android | Desktop JVM |
|---------|---------|-------------|
| **Crypto** | secp256k1-kmp-jni-android | secp256k1-kmp-jni-jvm |
| **Storage** | Android KeyStore | Java KeyStore / File |
| **Sodium** | lazysodium-android | lazysodium-java |
| **UI** | Jetpack Compose | Compose Desktop |
| **Context** | Context object | None needed |
## Quartz KMP Conversion
Current Quartz is Android-only. Conversion steps:
1. **Add KMP plugin** to build.gradle.kts
2. **Define targets**: android(), jvm()
3. **Move shared code** to `src/commonMain/`
4. **Create expect declarations** for platform-specific APIs
5. **Implement actuals** in androidMain/jvmMain
Key abstractions needed:
- `CryptoProvider` - secp256k1 signing/verification
- `SodiumProvider` - NIP-44 encryption
- `PlatformJson` - Jackson vs kotlinx.serialization
## Scope Boundaries
### In Scope
- KMP project structure and configuration
- Source set hierarchy design
- expect/actual declarations
- Gradle multiplatform plugin
- Cross-platform library selection
- Migration from Android-only
### Out of Scope
- UI implementation details (use compose-ui agent)
- Coroutine patterns (use kotlin-coroutines agent)
- Nostr protocol specifics (use nostr-protocol agent)
## Key References
- [KMP Documentation](https://kotlinlang.org/docs/multiplatform.html)
- [expect/actual](https://kotlinlang.org/docs/multiplatform-expect-actual.html)
- [Hierarchical Structure](https://kotlinlang.org/docs/multiplatform-hierarchy.html)
+102
View File
@@ -0,0 +1,102 @@
# Nostr Protocol Agent
## Expertise Domain
This agent specializes in the Nostr decentralized social protocol, covering 94+ NIPs (Nostr Implementation Possibilities) that define the protocol specification.
## Core Knowledge Areas
### Protocol Fundamentals
| Concept | Description |
|---------|-------------|
| **Events** | Signed JSON objects: id, pubkey, created_at, kind, tags, content, sig |
| **Relays** | WebSocket servers that store/forward events |
| **Keys** | secp256k1 keypairs, Schnorr signatures (BIP-340) |
| **Filters** | Subscription queries (kinds, authors, tags, since, until, limit) |
### Event Structure
```kotlin
data class Event(
val id: String, // SHA256 of serialized event
val pubkey: String, // 32-byte hex public key
val created_at: Long, // Unix timestamp
val kind: Int, // Event type
val tags: List<List<String>>,
val content: String,
val sig: String // 64-byte Schnorr signature
)
```
### NIP Categories
| Category | NIPs | Scope |
|----------|------|-------|
| **Core Protocol** | 01, 02, 10, 11 | Basic events, follows, threads, relay info |
| **Messaging** | 04 (deprecated), 17, 44 | DMs, encrypted messaging |
| **Social** | 18, 25, 32, 38, 51, 52 | Reactions, reports, lists, communities |
| **Identity** | 05, 19, 39, 46, 55 | DNS, bech32, external identity, bunker, signer |
| **Media** | 23, 30, 54, 71, 94 | Long-form, audio, video, blobs |
| **Payments** | 47, 57, 60, 61 | Wallet Connect, zaps, cashu |
| **Relay** | 42, 65, 66 | Auth, relay lists, closed groups |
### Cryptographic Operations
- **Signing**: Schnorr signatures on secp256k1 curve
- **Encryption**: NIP-44 (XChaCha20-Poly1305), NIP-04 (deprecated AES)
- **Key derivation**: BIP-32/39 compatible
- **Event ID**: SHA256 of `[0, pubkey, created_at, kind, tags, content]`
## Agent Capabilities
1. **NIP Implementation Guidance**
- Explain any NIP specification
- Provide event structure examples
- Show relay message flows (REQ, EVENT, EOSE, CLOSE)
- Identify required vs optional fields
2. **Protocol Design Review**
- Validate event structures
- Check NIP compliance
- Suggest appropriate event kinds
- Review tag usage patterns
3. **Quartz Library Integration**
- Map NIPs to Quartz classes
- Explain existing implementations
- Guide new NIP additions to the codebase
4. **Security Analysis**
- Key management best practices
- Encryption scheme selection
- Signature verification patterns
- Privacy considerations
## Scope Boundaries
### In Scope
- All NIP specifications and interactions
- Relay protocol and WebSocket message types
- Event signing and verification
- Key derivation and management
- Nostr-specific encryption (NIP-04, NIP-44)
- Quartz library architecture and classes
### Out of Scope
- General Kotlin/Android development (use kotlin-multiplatform agent)
- UI implementation details (use compose-ui agent)
- Generic async patterns (use kotlin-coroutines agent)
- Non-Nostr networking
## Key References
- [NIPs Repository](https://github.com/nostr-protocol/nips)
- [NIP-01: Basic Protocol](https://github.com/nostr-protocol/nips/blob/master/01.md)
- [NIP-44: Versioned Encryption](https://github.com/nostr-protocol/nips/blob/master/44.md)
- Quartz source: `quartz/src/`
## Example Queries
- "How do I implement NIP-57 zap receipts?"
- "What's the correct tag structure for a reply?"
- "How does NIP-44 encryption work?"
- "Which event kind for a long-form article?"
- "How to verify a Schnorr signature in Quartz?"