Agent skill

desktop-expert

Stars 1,494
Forks 189

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, jvmMain source set structure
  • gradle-expert: All build.gradle.kts issues, dependency conflicts
  • compose-expert: General Compose patterns, @Composable best 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:

kotlin
// 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 window
  • rememberWindowState() manages size/position
  • onCloseRequest handles window close

See: desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt:87-138


2. Window Management

WindowState

kotlin
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

kotlin
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

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

kotlin
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

kotlin
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.

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

kotlin
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

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

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

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

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

  1. Mouse/touch interaction
  2. Keyboard shortcut (if frequent)
  3. Tooltip showing shortcut
kotlin
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:

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

kotlin
// 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, MenuBar
  • Main.kt:183-264 - NavigationRail pattern
  • build.gradle.kts:45-73 - Desktop packaging config

10. Packaging & Distribution

Build Configuration

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

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

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

kotlin
// 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 catalog
  • references/desktop-navigation.md - NavigationRail vs BottomNav patterns
  • references/keyboard-shortcuts.md - Standard shortcuts by OS
  • references/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:

  1. Should this be shared or desktop-only?

    • Business logic → Share in commonMain
    • Navigation/layout → Keep in desktopApp/jvmMain
  2. Does this need OS-specific behavior?

    • Keyboard shortcuts → Yes (Cmd vs Ctrl)
    • File paths → Yes (separators)
    • Icons → Yes (per-OS formats)
  3. Is there a desktop UX convention?

    • Check MenuBar standards
    • Consider keyboard-first design
    • Tooltips for all actions
  4. Does this need gradle-expert?

    • Any build.gradle.kts changes → Delegate
    • Packaging/distribution issues → Delegate

Anti-Patterns

Hardcoding Ctrl everywhere

kotlin
// Main.kt:105 - Current issue
shortcut = KeyShortcut(Key.N, ctrl = true)  // Wrong on macOS

OS-aware shortcuts

kotlin
shortcut = DesktopShortcuts.primary(Key.N)

Using Android navigation on Desktop

kotlin
Scaffold(bottomBar = { BottomNavigationBar() })  // Wrong for desktop

NavigationRail for desktop

kotlin
Row {
    NavigationRail { /* ... */ }
    MainContent()
}

No keyboard shortcuts

kotlin
IconButton(onClick = { refresh() }) {
    Icon(Icons.Default.Refresh, "Refresh")
}

Shortcuts + tooltips

kotlin
IconButton(
    onClick = { refresh() },
    modifier = Modifier.tooltipArea("Refresh (Cmd+R)")
) {
    Icon(Icons.Default.Refresh, "Refresh")
}

Next Steps

When implementing desktop features:

  1. Read references/desktop-compose-apis.md for API catalog
  2. Check references/keyboard-shortcuts.md for standard shortcuts
  3. Reference Main.kt:87-264 for current patterns
  4. Test on all 3 platforms (macOS, Windows, Linux) if possible
  5. Delegate build issues to gradle-expert
  6. Share UI components via compose-expert, not desktop-expert

Version: 1.0.0 Last Updated: 2025-12-30 Codebase Reference: AmethystMultiplatform commit 258c4e011

Expand your agent's capabilities with these related and highly-rated skills.

vitorpamplona/amethyst

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

1,494 189
Explore
vitorpamplona/amethyst

quartz-integration

1,494 189
Explore
vitorpamplona/amethyst

android-expert

1,494 189
Explore
vitorpamplona/amethyst

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).

1,494 189
Explore
vitorpamplona/amethyst

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

1,494 189
Explore
vitorpamplona/amethyst

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.

1,494 189
Explore

Didn't find tool you were looking for?

Be as detailed as possible for better results