
Desktop Expert
FreeMaster Compose Multiplatform Desktop development.
Free · Opens the source repo
What Desktop Expert does
Desktop Expert is a specialized skill designed for developers working with Compose Multiplatform Desktop applications, particularly within the Amethyst framework. It provides comprehensive guidance on utilizing desktop-specific APIs, including Window management, MenuBar integration, and system Tray functionalities. This skill is essential for those looking to implement OS-aware conventions and navigation patterns that enhance user experience across macOS, Windows, and Linux environments.
The skill focuses on various aspects of desktop application development. It covers how to manage application windows, including their states, sizes, and positions, as well as how to implement a MenuBar that adapts to different operating systems. Developers can utilize the provided patterns for creating multiple windows and managing their visibility based on user interactions. Furthermore, the skill emphasizes the importance of keyboard shortcuts and menu systems that align with OS conventions, ensuring a seamless experience for users regardless of their platform.
In addition to window and menu management, Desktop Expert addresses file system integration, such as file pickers and drag-and-drop functionality, which are crucial for desktop applications. The skill also provides insights into desktop UX principles, promoting a keyboard-first approach and the use of tooltips to enhance usability. By leveraging this skill, developers can create applications that not only function well but also adhere to best practices in desktop application design.
Overall, Desktop Expert is a valuable resource for developers who want to deepen their understanding of Compose Multiplatform Desktop development. It serves as a guide for implementing desktop-specific features while delegating other concerns to related skills like gradle-expert and compose-expert, making it a well-rounded addition to any developer's toolkit.
When to use it
Use this skill when developing applications within the `desktopApp/` module that require desktop-specific features and OS-aware functionality.
When not to use it
This skill is not suitable for web or mobile application development, nor does it cover build configuration or shared composables.
What you can build with it
Implementing a MenuBar
Use Desktop Expert to create a responsive MenuBar that adapts shortcuts based on the user's operating system.
Managing Multiple Windows
Leverage the skill to control the visibility of multiple windows in your desktop application based on user actions.
Integrating File System Features
Utilize Desktop Expert for implementing file pickers and drag-and-drop functionality in your desktop applications.
How to install Desktop Expert
View source1. Install with the skills CLI
npx skills add vitorpamplona/amethyst/desktop-expert --agent claude-code2. Or install it manually
Download the skill folder and drop it into ~/.claude/skills/ for all projects, or .claude/skills/ to scope it to one repo. Restart Claude Code so it picks up the new skill.
Anthropic's agentic coding CLI, and the reference implementation of Agent Skills. Drop a skill folder into ~/.claude/skills and Claude Code loads it automatically whenever a task matches the skill's description. Claude Code docs
Inside SKILL.md
Written by vitorpamplonaDesktop 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 — grep for fun main(), application {, the top-level Window, and MenuBar { (the file is large and line numbers drift; navigate by symbol).
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 state: Main.kt already branches on isMacOS (declared at L120) for every menu shortcut — if (isMacOS) { KeyShortcut(..., meta = true) } else { KeyShortcut(..., ctrl = true) } (see L239, L249, L286, L313, L325, L335, L347, L358, L374, L384, L400, L416, L449). When adding a new shortcut, follow the same branching pattern rather than hardcoding ctrl = true.
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...
}
}
}
In Amethyst Desktop: the sidebar is the custom MainSidebar composable in desktopApp/.../ui/deck/DeckSidebar.kt, instantiated from Main.kt and shared by both layout modes (SinglePaneLayout and the multi-pane DeckLayout alongside it). It is hand-rolled, not Material's NavigationRail — use NavigationRail only for new, simpler cases.
Why a left sidebar?
- 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: left vertical sidebar (
MainSidebar)
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:
desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt—fun main()L172,application {L186,WindowL229,MenuBarL234 (OS-aware shortcuts begin at L239)desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/deck/SinglePaneLayout.kt— NavigationRail at L97desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/deck/— DeckLayout, WorkspaceManager, DeckState (multi-pane)desktopApp/build.gradle.kts— desktop packaging config (DMG/MSI/DEB)
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— Window + MenuBar entry point (applicationL186,WindowL229,MenuBarL234)ui/deck/SinglePaneLayout.kt— NavigationRail at L97ui/deck/DeckLayout.kt/WorkspaceManager.kt— multi-pane workspaceui/feed/— Desktop feed screensui/login/— 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
// Do NOT do this in a new shortcut:
shortcut = KeyShortcut(Key.N, ctrl = true) // Wrong on macOS
✅ OS-aware shortcuts (the pattern Main.kt already uses)
shortcut = if (isMacOS) {
KeyShortcut(Key.N, meta = true) // Cmd+N on macOS
} else {
KeyShortcut(Key.N, ctrl = true) // Ctrl+N on Win/Linux
}
❌ 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(entry point L172-L450+) andui/deck/SinglePaneLayout.kt(NavigationRail) 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
Frequently asked questions about Desktop Expert
Similar skills
Playwright Component Testing
Test React and Vue components in isolation with Playwright.
Fluent UI Blazor
Integrate Fluent UI components in Blazor applications effortlessly.
Build MCP App
Create interactive UI widgets for MCP servers.
Web Design Reviewer
Identify and fix design issues in websites efficiently.
Markstream Install
Seamlessly integrate Markstream for Markdown rendering.
GSAP & Framer Scroll Animation
Create advanced scroll animations effortlessly.
