82f8bc62a0
- 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>
6.6 KiB
6.6 KiB
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)
@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
// 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
// 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
// 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
-
Composable Design
- Create shareable UI components
- Implement Material3 design
- Handle responsive layouts
-
Desktop UI Patterns
- Window management (size, position, multi-window)
- Menu bars and context menus
- System tray integration
- Keyboard shortcuts
- Native file dialogs
-
Navigation Architecture
- Screen-based navigation
- Platform-specific shells (sidebar vs bottom nav)
- Deep linking patterns
-
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