---
name: desktop-expert
description: Compose Multiplatform Desktop patterns for the `desktopApp/` module. Use when working with (1) Desktop-only APIs (Window, WindowState, Tray, MenuBar, Dialog), (2) keyboard shortcuts and menu systems with OS-aware conventions (Cmd vs Ctrl, isMacOS branching), (3) desktop navigation (NavigationRail/sidebar vs Android bottom nav, multi-window), (4) file system integration (file pickers, drag-and-drop, Desktop.getDesktop()), (5) OS-specific behavior on macOS/Windows/Linux, (6) desktop UX principles (keyboard-first, tooltips). Delegates shared composables to compose-expert, build/packaging to gradle-expert, and source-set structure to kotlin-multiplatform.
---

# 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` — 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

```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 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:**

```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...
        }
    }
}
```

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

```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:**
- `desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt` — `fun main()` L172, `application {` L186, `Window` L229, `MenuBar` L234 (OS-aware shortcuts begin at L239)
- `desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/deck/SinglePaneLayout.kt` — NavigationRail at L97
- `desktopApp/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

```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
- [Desktop-only API | Kotlin Multiplatform](https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-desktop-components.html)
- [Top-level windows management](https://kotlinlang.org/docs/multiplatform/compose-desktop-top-level-windows-management.html)
- [Tray/MenuBar Tutorial](https://github.com/JetBrains/compose-multiplatform/blob/master/tutorials/Tray_Notifications_MenuBar_new/README.md)

### 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` — Window + MenuBar entry point (`application` L186, `Window` L229, `MenuBar` L234)
- `ui/deck/SinglePaneLayout.kt` — NavigationRail at L97
- `ui/deck/DeckLayout.kt` / `WorkspaceManager.kt` — multi-pane workspace
- `ui/feed/` — Desktop feed screens
- `ui/login/` — 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
// 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)**
```kotlin
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**
```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` (entry point L172-L450+) and `ui/deck/SinglePaneLayout.kt` (NavigationRail) 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
