# Add "New Playlist…" to the Add-to-Playlist menu — Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Let a user create a new regular playlist from a track's "Add to Playlist" context submenu, name it via a prompt, and have the track added to it on save. **Architecture:** A new "New Playlist…" item in the existing `Menu("Add to Playlist")` (in `TrackContextMenuModifier`) calls a new optional closure on `TrackContextMenuConfig`. `ContentView` owns that closure: it stashes the pending track and presents an `.alert` + `TextField` (mirroring the app's existing New Playlist alert). On save it calls a new `PlaylistViewModel.createPlaylistAndAddTrack(name:track:)`, which creates the regular playlist and adds the track in one step. **Tech Stack:** Swift, SwiftUI, GRDB, Swift Testing (`@Test`), Xcode (`Music` scheme). **Git note:** This project's owner never auto-commits — commits are made by the user via the `/commit` skill. The "Commit" steps below describe the *suggested grouping* of changes for when the user chooses to commit; do **not** run `git commit` yourself. **Spec:** `docs/superpowers/specs/2026-05-30-add-to-new-playlist-design.md` --- ## File Structure - `Music/ViewModels/PlaylistViewModel.swift` — **Modify.** Add `createPlaylistAndAddTrack(name:track:)` orchestration method. - `MusicTests/PlaylistViewModelTests.swift` — **Create.** Unit test for the new method. - `Music/Models/TrackContextMenuConfig.swift` — **Modify.** Add `onAddToNewPlaylist` optional closure (+ init param, default `nil`). - `Music/Views/TrackContextMenuModifier.swift` — **Modify.** Add "New Playlist…" button + relax the submenu visibility guard. - `Music/ContentView.swift` — **Modify.** New `@State` for the pending track, wire `onAddToNewPlaylist`, present the name alert. --- ## Task 1: `PlaylistViewModel.createPlaylistAndAddTrack` **Files:** - Test: `MusicTests/PlaylistViewModelTests.swift` (create) - Modify: `Music/ViewModels/PlaylistViewModel.swift` (add method after `addTrack`, ~line 77) - [ ] **Step 1: Write the failing test** Create `MusicTests/PlaylistViewModelTests.swift`: ```swift import Testing import Foundation @testable import Music @MainActor struct PlaylistViewModelTests { // Verifies createPlaylistAndAddTrack does the full job in one call: // 1. Seed a track into an in-memory DB and build a PlaylistViewModel over it. // 2. Call createPlaylistAndAddTrack with a name and the seeded track. // 3. The returned playlist has the given name and a real (non-nil) id. // 4. The DB shows that playlist now contains exactly the seeded track. // 5. The new playlist is recorded as the last-used playlist. @Test func createPlaylistAndAddTrackCreatesPlaylistAndAddsTrack() throws { // 1. Seed a track and build the view model. let db = try DatabaseService(inMemory: true) var track = Track.fixture(fileURL: "/song.mp3", title: "Song A") try db.insert(&track) let vm = PlaylistViewModel(db: db) // 2. Create a new playlist and add the track in one step. let created = try vm.createPlaylistAndAddTrack(name: "Road Trip", track: track) // 3. The returned playlist is well-formed. #expect(created.id != nil) #expect(created.name == "Road Trip") // 4. The playlist contains exactly the seeded track. let tracks = try db.fetchPlaylistTracks(playlistId: created.id!) #expect(tracks.count == 1) #expect(tracks[0].id == track.id) // 5. The new playlist became the last-used playlist. #expect(vm.lastUsedPlaylistId == created.id) } } ``` - [ ] **Step 2: Run the test to verify it fails** Run: ```bash xcodebuild test -scheme Music -destination 'platform=macOS' -only-testing:MusicTests/PlaylistViewModelTests ``` Expected: **build/compile failure** — `value of type 'PlaylistViewModel' has no member 'createPlaylistAndAddTrack'`. - [ ] **Step 3: Write the minimal implementation** In `Music/ViewModels/PlaylistViewModel.swift`, add this method directly after `addTrack(_:to:)` (after line 77): ```swift @discardableResult func createPlaylistAndAddTrack(name: String, track: Track) throws -> Playlist { let playlist = try db.createPlaylist(name: name) try addTrack(track, to: playlist) return playlist } ``` `db.createPlaylist` returns a `Playlist` with its assigned `id`; the existing `addTrack` inserts the join row and sets `lastUsedPlaylistId`. - [ ] **Step 4: Run the test to verify it passes** Run: ```bash xcodebuild test -scheme Music -destination 'platform=macOS' -only-testing:MusicTests/PlaylistViewModelTests ``` Expected: **PASS** (`createPlaylistAndAddTrackCreatesPlaylistAndAddsTrack`). - [ ] **Step 5: Commit (suggested grouping — leave to the user / `/commit`)** Changed files: `MusicTests/PlaylistViewModelTests.swift`, `Music/ViewModels/PlaylistViewModel.swift`. Suggested message: `feat: add PlaylistViewModel.createPlaylistAndAddTrack` --- ## Task 2: Add `onAddToNewPlaylist` to `TrackContextMenuConfig` This is a pure data struct; the new field is optional with a `nil` default, so existing call sites and `TrackContextMenuConfigTests` keep compiling. No new unit test (the struct just stores a closure). **Files:** - Modify: `Music/Models/TrackContextMenuConfig.swift` - [ ] **Step 1: Add the stored property** In `TrackContextMenuConfig`, add the property after `onAddToQueue` (after line 15): ```swift // nil hides the "New Playlist…" item (e.g. tests that don't supply it). let onAddToNewPlaylist: ((Track) -> Void)? ``` - [ ] **Step 2: Add the init parameter (with default `nil`)** In the explicit `init`, add the parameter after `onAddToQueue` (line 30): ```swift onAddToQueue: ((Track) -> Void)? = nil, onAddToNewPlaylist: ((Track) -> Void)? = nil, onGetInfo: (([Track]) -> Void)? = nil ``` And add the assignment in the body, after `self.onAddToQueue = onAddToQueue` (line 40): ```swift self.onAddToNewPlaylist = onAddToNewPlaylist ``` - [ ] **Step 3: Build to verify it compiles** Run: ```bash xcodebuild build -scheme Music -destination 'platform=macOS' ``` Expected: **BUILD SUCCEEDED** (existing call sites still compile because the new param defaults to `nil`). - [ ] **Step 4: Run the existing config tests to confirm no regression** Run: ```bash xcodebuild test -scheme Music -destination 'platform=macOS' -only-testing:MusicTests/TrackContextMenuConfigTests ``` Expected: **PASS** (all existing tests). - [ ] **Step 5: Commit (suggested grouping — leave to the user / `/commit`)** Changed file: `Music/Models/TrackContextMenuConfig.swift`. Suggested message: `feat: add onAddToNewPlaylist to TrackContextMenuConfig` --- ## Task 3: Add "New Playlist…" to the submenu SwiftUI view code; no unit test (consistent with the codebase). Verify by build. **Files:** - Modify: `Music/Views/TrackContextMenuModifier.swift:30-38` - [ ] **Step 1: Replace the "Add to Playlist" submenu block** Replace the current block (lines 30–38): ```swift if !config.playlists.isEmpty { Menu("Add to Playlist") { ForEach(config.playlists) { playlist in Button(playlist.name) { config.onAddToPlaylist(track, playlist) } } } } ``` with: ```swift if !config.playlists.isEmpty || config.onAddToNewPlaylist != nil { Menu("Add to Playlist") { if let onAddToNewPlaylist = config.onAddToNewPlaylist { Button("New Playlist…") { onAddToNewPlaylist(track) } if !config.playlists.isEmpty { Divider() } } ForEach(config.playlists) { playlist in Button(playlist.name) { config.onAddToPlaylist(track, playlist) } } } } ``` Notes: the button label uses a real ellipsis character `…` (macOS convention for "opens a prompt"). The submenu now appears even when the user has zero playlists, showing just "New Playlist…". The `Divider` only appears when there are existing playlists to separate from. - [ ] **Step 2: Build to verify it compiles** Run: ```bash xcodebuild build -scheme Music -destination 'platform=macOS' ``` Expected: **BUILD SUCCEEDED**. - [ ] **Step 3: Commit (suggested grouping — leave to the user / `/commit`)** Changed file: `Music/Views/TrackContextMenuModifier.swift`. Suggested message: `feat: add "New Playlist…" item to Add to Playlist submenu` --- ## Task 4: Wire the prompt in `ContentView` SwiftUI view code; no unit test. Verify by build + manual check. **Files:** - Modify: `Music/ContentView.swift` — add `@State` (near the other playlist alert state, e.g. by `playlistNameInput`/`showNewPlaylistAlert`), wire the closure in `trackContextMenuConfig` (line 433–434 area), add the alert (by the existing New Playlist alert at line 273). - [ ] **Step 1: Add state for the pending track** Find the existing `@State` declarations for the playlist alerts (the same scope that declares `showNewPlaylistAlert` and `playlistNameInput`). Add nearby: ```swift @State private var newPlaylistTrack: Track? @State private var newPlaylistNameInput = "" ``` (`newPlaylistNameInput` is kept separate from the sidebar's `playlistNameInput` so the two flows can't clobber each other's text.) - [ ] **Step 2: Wire the closure in `trackContextMenuConfig`** In `trackContextMenuConfig` (line 412), add `onAddToNewPlaylist` to the `TrackContextMenuConfig(...)` call. Insert it after the `onAddToQueue:` argument (line 433), before `onGetInfo:`: ```swift onAddToQueue: queueEnabled ? { track in player.addToQueue(track) } : nil, onAddToNewPlaylist: { track in newPlaylistTrack = track }, onGetInfo: { tracks in if !tracks.isEmpty { infoRequest = TrackInfoRequest(tracks: tracks) } } ``` (Wired unconditionally — matches `onAddToPlaylist`, which is also not gated on `queueEnabled`.) - [ ] **Step 3: Add the name-prompt alert** Immediately after the existing New Playlist alert block (after line 283, the closing `}` of `.alert("New Playlist", isPresented: $showNewPlaylistAlert)`), add: ```swift .alert("New Playlist", isPresented: Binding( get: { newPlaylistTrack != nil }, set: { if !$0 { newPlaylistTrack = nil; newPlaylistNameInput = "" } } )) { TextField("Playlist name", text: $newPlaylistNameInput) Button("Cancel", role: .cancel) { newPlaylistNameInput = "" newPlaylistTrack = nil } Button("Create") { let name = newPlaylistNameInput.trimmingCharacters(in: .whitespaces) if !name.isEmpty, let track = newPlaylistTrack { try? playlist.createPlaylistAndAddTrack(name: name, track: track) } newPlaylistNameInput = "" newPlaylistTrack = nil } } ``` - [ ] **Step 4: Build to verify it compiles** Run: ```bash xcodebuild build -scheme Music -destination 'platform=macOS' ``` Expected: **BUILD SUCCEEDED**. - [ ] **Step 5: Manual verification** Launch the app. Right-click a track → **Add to Playlist** → **New Playlist…**. Enter a name, click **Create**. Confirm: - A new playlist with that name appears in the sidebar. - Selecting it shows the track you added. - The sidebar selection did **not** change automatically (stayed on the current view). - Re-open the context menu: "Add to " now appears as the last-used playlist shortcut. - Empty name → clicking Create does nothing (no empty playlist created). - [ ] **Step 6: Commit (suggested grouping — leave to the user / `/commit`)** Changed file: `Music/ContentView.swift`. Suggested message: `feat: prompt for name and add track when creating playlist from menu` --- ## Final verification - [ ] Run the full test target once: ```bash xcodebuild test -scheme Music -destination 'platform=macOS' ``` Expected: **all tests PASS**, including the new `PlaylistViewModelTests`. --- ## Self-review (done while writing this plan) - **Spec coverage:** view-model create+add (Task 1), config closure (Task 2), submenu item + empty-list handling (Task 3), prompt + wiring + behavior decisions: single clicked track, no navigation, empty-name no-op (Task 4). All spec sections map to a task. - **Placeholder scan:** none — every code step shows the exact code. - **Type/name consistency:** `createPlaylistAndAddTrack(name:track:)` and `onAddToNewPlaylist` are used identically across Tasks 1–4; `newPlaylistTrack` / `newPlaylistNameInput` are consistent within Task 4.