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.swift— Modify. AddcreatePlaylistAndAddTrack(name:track:)orchestration method.MusicTests/PlaylistViewModelTests.swift— Create. Unit test for the new method.Music/Models/TrackContextMenuConfig.swift— Modify. AddonAddToNewPlaylistoptional closure (+ init param, defaultnil).Music/Views/TrackContextMenuModifier.swift— Modify. Add "New Playlist…" button + relax the submenu visibility guard.Music/ContentView.swift— Modify. New@Statefor the pending track, wireonAddToNewPlaylist, present the name alert.
Task 1: PlaylistViewModel.createPlaylistAndAddTrack
Files:
-
Test:
MusicTests/PlaylistViewModelTests.swift(create) -
Modify:
Music/ViewModels/PlaylistViewModel.swift(add method afteraddTrack, ~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 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):
@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. byplaylistNameInput/showNewPlaylistAlert), wire the closure intrackContextMenuConfig(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 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:
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:)andonAddToNewPlaylistare used identically across Tasks 1–4;newPlaylistTrack/newPlaylistNameInputare consistent within Task 4.