You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
Music/docs/superpowers/plans/2026-05-30-add-to-new-playl...

13 KiB

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.swiftModify. Add createPlaylistAndAddTrack(name:track:) orchestration method.
  • MusicTests/PlaylistViewModelTests.swiftCreate. Unit test for the new method.
  • Music/Models/TrackContextMenuConfig.swiftModify. Add onAddToNewPlaylist optional closure (+ init param, default nil).
  • Music/Views/TrackContextMenuModifier.swiftModify. Add "New Playlist…" button + relax the submenu visibility guard.
  • Music/ContentView.swiftModify. 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:

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:

xcodebuild test -scheme Music -destination 'platform=macOS' -only-testing:MusicTests/PlaylistViewModelTests

Expected: build/compile failurevalue 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):

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

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

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

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

        self.onAddToNewPlaylist = onAddToNewPlaylist
  • Step 3: Build to verify it compiles

Run:

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:

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

                if !config.playlists.isEmpty {
                    Menu("Add to Playlist") {
                        ForEach(config.playlists) { playlist in
                            Button(playlist.name) {
                                config.onAddToPlaylist(track, playlist)
                            }
                        }
                    }
                }

with:

                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:

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:

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

            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:

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

xcodebuild build -scheme Music -destination 'platform=macOS'

Expected: BUILD SUCCEEDED.

  • Step 5: Manual verification

Launch the app. Right-click a track → Add to PlaylistNew 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:
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.