Agent skill
desktop-expert
Install this agent skill to your Project
npx add-skill https://github.com/vitorpamplona/amethyst/tree/main/.claude/skills/desktop-expert
SKILL.md
Desktop Expert
Expert in Compose Multiplatform Desktop development for AmethystMultiplatform. Covers Desktop-specific APIs, OS conventions, navigation patterns, and UX principles.
When to Use This Skill
Auto-invoke when:
- Working with
desktopApp/module files - Using Desktop-only APIs:
Window,Tray,MenuBar,Dialog - Implementing keyboard shortcuts, menu systems
- Desktop navigation (NavigationRail, multi-window)
- File system operations (file pickers, drag-drop)
- OS-specific behavior (macOS, Windows, Linux)
- Desktop UX patterns (keyboard-first, tooltips)
Delegate to:
- kotlin-multiplatform: Shared code questions,
jvmMainsource set structure - gradle-expert: All
build.gradle.ktsissues, dependency conflicts - compose-expert: General Compose patterns,
@Composablebest practices, Material3
Scope
In scope:
- Desktop-only Compose APIs
- Window management, positioning, state
- MenuBar + keyboard shortcuts (OS-specific)
- System Tray integration
- Desktop navigation patterns (NavigationRail)
- File dialogs, Desktop.getDesktop()
- OS conventions (macOS vs Windows vs Linux)
- Desktop UX principles
Out of scope:
- Build configuration → gradle-expert
- Shared composables → compose-expert
- KMP structure → kotlin-multiplatform
1. Desktop Entry Point
application {} DSL
Desktop apps start with the application {} block:
// desktopApp/src/jvmMain/kotlin/Main.kt
fun main() = application {
val windowState = rememberWindowState(
width = 1200.dp,
height = 800.dp,
position = WindowPosition.Aligned(Alignment.Center)
)
Window(
onCloseRequest = ::exitApplication,
state = windowState,
title = "Amethyst"
) {
MenuBar { /* ... */ }
App()
}
}
Key points:
application {}is the root composable (JVM-only)Window()creates the main windowrememberWindowState()manages size/positiononCloseRequesthandles window close
See: desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt:87-138
2. Window Management
WindowState
val windowState = rememberWindowState(
width = 1200.dp,
height = 800.dp,
position = WindowPosition.Aligned(Alignment.Center)
)
Window(
state = windowState,
title = "My App",
resizable = true,
onCloseRequest = ::exitApplication
) {
// Content
}
Multiple Windows
fun main() = application {
var showSettings by remember { mutableStateOf(false) }
Window(onCloseRequest = ::exitApplication, title = "Main") {
Button(onClick = { showSettings = true }) {
Text("Open Settings")
}
}
if (showSettings) {
Window(
onCloseRequest = { showSettings = false },
title = "Settings"
) {
// Settings UI
}
}
}
Pattern: Use state to control window visibility conditionally.
3. MenuBar System
Basic MenuBar
Window(onCloseRequest = ::exitApplication, title = "App") {
MenuBar {
Menu("File") {
Item("New Note", onClick = { /* ... */ })
Separator()
Item("Quit", onClick = ::exitApplication)
}
Menu("Edit") {
Item("Copy", onClick = { /* ... */ })
Item("Paste", onClick = { /* ... */ })
}
}
App()
}
Keyboard Shortcuts (OS-Aware)
Current issue: Main.kt hardcodes ctrl = true (Main.kt:105, 111, 117, 122, 123).
OS-specific shortcuts:
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyShortcut
// Detect OS
val isMacOS = System.getProperty("os.name").lowercase().contains("mac")
MenuBar {
Menu("File") {
Item(
"New Note",
shortcut = if (isMacOS) {
KeyShortcut(Key.N, meta = true) // Cmd+N on macOS
} else {
KeyShortcut(Key.N, ctrl = true) // Ctrl+N on Win/Linux
},
onClick = { /* ... */ }
)
Item(
"Settings",
shortcut = if (isMacOS) {
KeyShortcut(Key.Comma, meta = true) // Cmd+, on macOS
} else {
KeyShortcut(Key.Comma, ctrl = true) // Ctrl+, on Win/Linux
},
onClick = { /* ... */ }
)
Separator()
Item(
"Quit",
shortcut = if (isMacOS) {
KeyShortcut(Key.Q, meta = true) // Cmd+Q on macOS
} else {
KeyShortcut(Key.Q, ctrl = true) // Ctrl+Q on Win/Linux
},
onClick = ::exitApplication
)
}
}
Standard shortcuts:
| Action | macOS | Windows/Linux |
|---|---|---|
| New | Cmd+N | Ctrl+N |
| Open | Cmd+O | Ctrl+O |
| Save | Cmd+S | Ctrl+S |
| Quit | Cmd+Q | Ctrl+Q (Alt+F4) |
| Settings | Cmd+, | Ctrl+, |
| Copy | Cmd+C | Ctrl+C |
| Paste | Cmd+V | Ctrl+V |
| Undo | Cmd+Z | Ctrl+Z |
See: references/keyboard-shortcuts.md for full list.
4. System Tray
Basic Tray
application {
var isVisible by remember { mutableStateOf(true) }
Tray(
icon = painterResource("icon.png"),
onAction = { isVisible = true },
menu = {
Item("Show", onClick = { isVisible = true })
Separator()
Item("Quit", onClick = ::exitApplication)
}
)
if (isVisible) {
Window(
onCloseRequest = { isVisible = false }, // Minimize to tray
title = "App"
) {
// Content
}
}
}
Pattern: Hide window to tray instead of closing.
Current status: Not implemented in Main.kt. Planned feature.
5. Desktop Navigation Patterns
NavigationRail (Current Pattern)
Desktop uses NavigationRail (vertical sidebar) instead of Android's bottom navigation.
Row(Modifier.fillMaxSize()) {
// Sidebar
NavigationRail(
modifier = Modifier.width(80.dp).fillMaxHeight(),
containerColor = MaterialTheme.colorScheme.surfaceVariant
) {
NavigationRailItem(
icon = { Icon(Icons.Default.Home, "Feed") },
label = { Text("Feed") },
selected = currentScreen == AppScreen.Feed,
onClick = { currentScreen = AppScreen.Feed }
)
// More items...
}
VerticalDivider()
// Main content area
Box(Modifier.weight(1f).fillMaxHeight()) {
when (currentScreen) {
AppScreen.Feed -> FeedScreen()
// Other screens...
}
}
}
See: Main.kt:191-264
Why NavigationRail?
- Desktop has horizontal space (1200+ dp width)
- Vertical sidebar is standard desktop pattern
- Always visible (no tabs hidden)
- Icon + label both visible
Android comparison:
- Android:
BottomNavigationBar(horizontal, bottom) - Desktop:
NavigationRail(vertical, left)
Multi-Pane Layouts
Desktop can leverage wide screens:
Row {
// Left: Navigation
NavigationRail { /* ... */ }
// Center: Main content
Box(Modifier.weight(0.6f)) {
FeedScreen()
}
// Right: Details pane (desktop only)
if (selectedNote != null) {
VerticalDivider()
Box(Modifier.weight(0.4f)) {
NoteDetailPane(selectedNote)
}
}
}
See: references/desktop-navigation.md
6. File System Integration
File Dialogs
// File picker (load)
val fileDialog = FileDialog(Frame(), "Select file", FileDialog.LOAD)
fileDialog.isVisible = true
val filePath = fileDialog.file?.let { "${fileDialog.directory}$it" }
// File picker (save)
val saveDialog = FileDialog(Frame(), "Save file", FileDialog.SAVE)
saveDialog.isVisible = true
val savePath = saveDialog.file?.let { "${saveDialog.directory}$it" }
Note: Compose Desktop doesn't have native file picker composable yet. Use AWT FileDialog.
Open External URLs
// jvmMain actual implementation
actual fun openExternalUrl(url: String) {
if (Desktop.isDesktopSupported()) {
Desktop.getDesktop().browse(URI(url))
}
}
Pattern: Define expect in commonMain, implement actual in jvmMain.
Drag & Drop (Future)
// Compose Desktop drag-drop (experimental)
Box(
modifier = Modifier
.onExternalDrag(
onDragStart = { /* ... */ },
onDrag = { /* ... */ },
onDragExit = { /* ... */ },
onDrop = { state ->
val dragData = state.dragData
// Handle dropped files
}
)
) {
Text("Drop files here")
}
7. OS-Specific Conventions
Platform Detection
val osName = System.getProperty("os.name").lowercase()
val isMacOS = osName.contains("mac")
val isWindows = osName.contains("win")
val isLinux = osName.contains("nux") || osName.contains("nix")
Menu Bar Placement
| OS | Behavior |
|---|---|
| macOS | System-wide menu bar at top of screen |
| Windows | In-window menu bar |
| Linux | Varies by desktop environment |
Compose Desktop MenuBar adapts automatically.
Keyboard Modifier Keys
| Modifier | macOS | Windows/Linux |
|---|---|---|
| Primary | meta = true (Cmd) |
ctrl = true |
| Secondary | ctrl = true |
alt = true |
| Shift | shift = true |
shift = true |
Best practice: Detect OS and use appropriate modifier.
System Tray Behavior
| OS | Tray Location |
|---|---|
| macOS | Top-right menu bar |
| Windows | Bottom-right taskbar |
| Linux | Top panel (varies) |
8. Desktop UX Principles
Keyboard-First Design
Every action should have:
- Mouse/touch interaction
- Keyboard shortcut (if frequent)
- Tooltip showing shortcut
IconButton(
onClick = { /* refresh */ },
modifier = Modifier.tooltipArea(
tooltip = {
Text("Refresh (${if (isMacOS) "Cmd" else "Ctrl"}+R)")
}
)
) {
Icon(Icons.Default.Refresh, "Refresh")
}
Tooltip Best Practices
- Show keyboard shortcut in tooltip
- Use native modifier name (Cmd vs Ctrl)
- Brief description + shortcut
Context Menus
Right-click should show context menu:
// Future: Compose Desktop context menu API
Box(
modifier = Modifier.contextMenuArea(
items = {
listOf(
ContextMenuItem("Copy") { /* ... */ },
ContextMenuItem("Paste") { /* ... */ }
)
}
)
) {
// Content
}
Current: Use popup or custom implementation.
Window State Persistence
Save/restore window size/position:
// Save on close
windowState.size // DpSize
windowState.position // WindowPosition
// Restore on launch
val savedWidth = preferences.getInt("window.width", 1200)
val savedHeight = preferences.getInt("window.height", 800)
val windowState = rememberWindowState(
width = savedWidth.dp,
height = savedHeight.dp
)
9. Desktop Module Structure
desktopApp/
├── build.gradle.kts # Desktop-only build config
└── src/
└── jvmMain/
├── kotlin/
│ └── com/vitorpamplona/amethyst/desktop/
│ ├── Main.kt # Entry point, Window, MenuBar
│ ├── network/
│ │ ├── DesktopHttpClient.kt
│ │ └── DesktopRelayConnectionManager.kt
│ └── ui/
│ ├── FeedScreen.kt # Desktop screen layouts
│ └── LoginScreen.kt
└── resources/
├── icon.icns # macOS icon
├── icon.ico # Windows icon
└── icon.png # Linux icon
Key files:
Main.kt:87-138-application {},Window,MenuBarMain.kt:183-264- NavigationRail patternbuild.gradle.kts:45-73- Desktop packaging config
10. Packaging & Distribution
Build Configuration
// desktopApp/build.gradle.kts
compose.desktop {
application {
mainClass = "com.vitorpamplona.amethyst.desktop.MainKt"
nativeDistributions {
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
packageName = "Amethyst"
packageVersion = "1.0.0"
macOS {
bundleID = "com.vitorpamplona.amethyst.desktop"
iconFile.set(project.file("src/jvmMain/resources/icon.icns"))
}
windows {
iconFile.set(project.file("src/jvmMain/resources/icon.ico"))
menuGroup = "Amethyst"
}
linux {
iconFile.set(project.file("src/jvmMain/resources/icon.png"))
}
}
}
}
See: desktopApp/build.gradle.kts:45-73
Gradle Tasks
# Run desktop app
./gradlew :desktopApp:run
# Package for distribution
./gradlew :desktopApp:packageDmg # macOS
./gradlew :desktopApp:packageMsi # Windows
./gradlew :desktopApp:packageDeb # Linux
Delegate packaging issues to gradle-expert.
Common Patterns
Pattern: OS-Aware Shortcuts Helper
// commons/src/jvmMain/kotlin/shortcuts/ShortcutUtils.kt
object DesktopShortcuts {
private val isMacOS = System.getProperty("os.name")
.lowercase().contains("mac")
fun primary(key: Key) = if (isMacOS) {
KeyShortcut(key, meta = true)
} else {
KeyShortcut(key, ctrl = true)
}
fun primaryShift(key: Key) = if (isMacOS) {
KeyShortcut(key, meta = true, shift = true)
} else {
KeyShortcut(key, ctrl = true, shift = true)
}
val modifierName = if (isMacOS) "Cmd" else "Ctrl"
}
// Usage in MenuBar
Item(
"New Note",
shortcut = DesktopShortcuts.primary(Key.N),
onClick = { /* ... */ }
)
Pattern: Shared Composables, Platform Layouts
// commons/commonMain - Shared NoteCard
@Composable
fun NoteCard(note: NoteDisplayData) {
// Business logic, UI component (shared)
}
// desktopApp/jvmMain - Desktop layout
@Composable
fun FeedScreen() {
Column {
FeedHeader(/* ... */) // Shared from commons
LazyColumn {
items(notes) { note ->
NoteCard(note) // Shared composable
}
}
}
}
// amethyst/androidMain - Android layout
@Composable
fun FeedScreen() {
Scaffold(
bottomBar = { BottomNavigationBar() } // Android-specific
) {
LazyColumn {
items(notes) { note ->
NoteCard(note) // Same shared composable
}
}
}
}
Philosophy: Share UI components (cards, buttons), keep navigation/layout platform-specific.
Resources
Official Documentation
Bundled References
references/desktop-compose-apis.md- Complete Desktop API catalogreferences/desktop-navigation.md- NavigationRail vs BottomNav patternsreferences/keyboard-shortcuts.md- Standard shortcuts by OSreferences/os-detection.md- Platform detection patterns
Codebase Examples
- Main.kt:87-138 - Window, MenuBar entry point
- Main.kt:183-264 - NavigationRail pattern
- FeedScreen.kt:49-136 - Desktop screen layout
- LoginScreen.kt:44-97 - Centered desktop login
Questions to Ask
When working on desktop features:
-
Should this be shared or desktop-only?
- Business logic → Share in
commonMain - Navigation/layout → Keep in
desktopApp/jvmMain
- Business logic → Share in
-
Does this need OS-specific behavior?
- Keyboard shortcuts → Yes (Cmd vs Ctrl)
- File paths → Yes (separators)
- Icons → Yes (per-OS formats)
-
Is there a desktop UX convention?
- Check MenuBar standards
- Consider keyboard-first design
- Tooltips for all actions
-
Does this need gradle-expert?
- Any
build.gradle.ktschanges → Delegate - Packaging/distribution issues → Delegate
- Any
Anti-Patterns
❌ Hardcoding Ctrl everywhere
// Main.kt:105 - Current issue
shortcut = KeyShortcut(Key.N, ctrl = true) // Wrong on macOS
✅ OS-aware shortcuts
shortcut = DesktopShortcuts.primary(Key.N)
❌ Using Android navigation on Desktop
Scaffold(bottomBar = { BottomNavigationBar() }) // Wrong for desktop
✅ NavigationRail for desktop
Row {
NavigationRail { /* ... */ }
MainContent()
}
❌ No keyboard shortcuts
IconButton(onClick = { refresh() }) {
Icon(Icons.Default.Refresh, "Refresh")
}
✅ Shortcuts + tooltips
IconButton(
onClick = { refresh() },
modifier = Modifier.tooltipArea("Refresh (Cmd+R)")
) {
Icon(Icons.Default.Refresh, "Refresh")
}
Next Steps
When implementing desktop features:
- Read
references/desktop-compose-apis.mdfor API catalog - Check
references/keyboard-shortcuts.mdfor standard shortcuts - Reference Main.kt:87-264 for current patterns
- Test on all 3 platforms (macOS, Windows, Linux) if possible
- Delegate build issues to gradle-expert
- Share UI components via compose-expert, not desktop-expert
Version: 1.0.0 Last Updated: 2025-12-30 Codebase Reference: AmethystMultiplatform commit 258c4e011
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
find-non-lambda-logs
Use when auditing or migrating Log calls to lambda overloads, after adding new logging, or checking for string interpolation in Log.d/i/w/e calls that waste allocations when the log level is filtered out
quartz-integration
android-expert
compose-expert
Advanced Compose Multiplatform UI patterns for shared composables. Use when working with visual UI components, state management patterns (remember, derivedStateOf, produceState), recomposition optimization (@Stable/@Immutable visual usage), Material3 theming, custom ImageVector icons, or determining whether to share UI in commonMain vs keep platform-specific. Delegates navigation to android-expert/desktop-expert. Complements kotlin-expert (handles Kotlin language aspects of state/annotations).
find-missing-translations
Use when comparing Android strings.xml locale files to find untranslated string resources, missing translation keys, or preparing translation work for a specific language
kotlin-multiplatform
Platform abstraction decision-making for Amethyst KMP project. Guides when to abstract vs keep platform-specific, source set placement (commonMain, jvmAndroid, platform-specific), expect/actual patterns. Covers primary targets (Android, JVM/Desktop, iOS) with web/wasm future considerations. Integrates with gradle-expert for dependency issues. Triggers on: abstraction decisions ("should I share this?"), source set placement questions, expect/actual creation, build.gradle.kts work, incorrect placement detection, KMP dependency suggestions.
Didn't find tool you were looking for?