--- phase: 17-single-track-edit plan: 01 type: execute wave: 1 depends_on: [] files_modified: - backend/tagwriter/pipeline.go - backend/frontendutil/frontendutil.go - frontend/src/store/library-store.ts - frontend/src/components/track-list/track-list.ts - frontend/src/components/queue-panel/queue-panel.ts - frontend/src/components/cover-grid/cover-grid.ts - frontend/src/components/playlist-details/playlist-details.ts autonomous: true requirements: - EDIT-01 - EDIT-04 must_haves: truths: - "WriteTrackTagsByPath accepts a file path string and TagChanges, resolves the track ID internally, and delegates to WriteTrackTags" - "ImageFilePicker opens a native file dialog filtered to JPEG/PNG and returns the selected file path" - "After a successful tag write, the library store invalidates all caches and re-fetches data so all views reflect the new metadata" - "Right-clicking any single track in track-list, queue-panel, cover-grid, or playlist-details shows 'Track Details' in the context menu regardless of selection state" artifacts: - path: "backend/tagwriter/pipeline.go" provides: "WriteTrackTagsByPath method on TagWriter" contains: "func (tw *TagWriter) WriteTrackTagsByPath" - path: "backend/frontendutil/frontendutil.go" provides: "ImageFilePicker method for cover art selection" contains: "func (fe *FrontendUtil) ImageFilePicker" - path: "frontend/src/store/library-store.ts" provides: "TrackMetadataChanged event handler calling invalidate()" contains: "TrackMetadataChanged" - path: "frontend/src/components/track-list/track-list.ts" provides: "Track Details context menu item visible for any right-clicked track" - path: "frontend/src/components/queue-panel/queue-panel.ts" provides: "Track Details context menu item visible for any right-clicked track" key_links: - from: "frontend/src/store/library-store.ts" to: "backend events" via: "EventsOn(Events.TrackMetadataChanged)" pattern: "EventsOn.*TrackMetadataChanged" - from: "backend/tagwriter/pipeline.go" to: "backend/database" via: "GetAudioFileByPath query" pattern: "GetAudioFileByPath" --- Wire the backend bridge methods and frontend plumbing needed for single-track tag editing. Purpose: Phase 17 builds on the WriteTrackTags pipeline from Phase 16. The frontend identifies tracks by `FilePath` but WriteTrackTags requires `trackID int64`. This plan adds a path-based wrapper, a cover art file picker, the library store event handler that refreshes views after edits, and removes the selection-count gate on the "Track Details" context menu item. Output: Backend methods ready for frontend consumption, library store reacts to tag write events, context menu accessible from any track context. @/home/caleb/.config/opencode/get-shit-done/workflows/execute-plan.md @/home/caleb/.config/opencode/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/17-single-track-edit/17-CONTEXT.md From backend/tagwriter/pipeline.go: ```go type TagWriter struct { logger *slog.Logger db *database.DB ctx context.Context player PlayerStopper library PipelineLocker } func (tw *TagWriter) WriteTrackTags(trackID int64, changes TagChanges) error ``` From backend/tagwriter/tagwriter.go: ```go type TagChanges map[string]any const ( FieldTitle = "title" FieldArtist = "artist" FieldAlbum = "album" FieldAlbumArtist = "album_artist" FieldGenre = "genre" FieldYear = "year" FieldTrackNumber = "track_number" FieldDiscNumber = "disc_number" FieldComposer = "composer" FieldCoverArt = "cover_art" ) ``` From backend/database/sql/sqlcgen/audio_files.sql.go: ```go func (q *Queries) GetAudioFileByPath(ctx context.Context, filePath string) (AudioFile, error) ``` From backend/frontendutil/frontendutil.go: ```go type FrontendUtil struct { ctx context.Context } func (fe *FrontendUtil) DirectoryPicker() (string, error) func (fe *FrontendUtil) PlaylistFilePicker() ([]string, error) ``` From frontend/src/store/library-store.ts: ```typescript class LibraryStore { private invalidate(): void { ... } // Currently listens for: LibraryScanComplete, LibraryRemoved, LibraryAdded, LibraryRenamed // Does NOT listen for TrackMetadataChanged } ``` From frontend/src/events.ts: ```typescript export const Events = { TrackMetadataChanged: "TrackMetadataChanged", // ... } as const; ``` Context menu pattern (track-list.ts line ~1966): ```typescript ${this.selection.selectionCount === 1 ? html` this.onContextMenuAction('track-details')}> Track Details ` : nothing} ``` queue-panel.ts uses the same pattern at line ~1553. cover-grid.ts and playlist-details.ts conditionally show Track Details only for single track context. Task 1: Add WriteTrackTagsByPath and ImageFilePicker backend methods backend/tagwriter/pipeline.go, backend/frontendutil/frontendutil.go **In `backend/tagwriter/pipeline.go`**, add a new method `WriteTrackTagsByPath` directly below the existing `WriteTrackTags` method: ```go // WriteTrackTagsByPath resolves a file path to its audio_file.id and // delegates to WriteTrackTags. This is the frontend-facing entry // point since the frontend identifies tracks by FilePath. func (tw *TagWriter) WriteTrackTagsByPath(filePath string, changes TagChanges) error { ctx := context.Background() audioFile, err := tw.db.Queries.GetAudioFileByPath(ctx, filePath) if err != nil { return fmt.Errorf("resolve track by path %q: %w", filePath, err) } return tw.WriteTrackTags(audioFile.ID, changes) } ``` This uses the existing `GetAudioFileByPath` sqlc query (already generated) to look up the `audio_files.id` from `file_path`, then delegates to the existing `WriteTrackTags` pipeline. **In `backend/frontendutil/frontendutil.go`**, add a new method `ImageFilePicker` below `PlaylistFilePicker`: ```go // ImageFilePicker opens a file selection dialog filtered to image // files (JPEG, PNG). Returns the selected file path, or empty // string if the user cancelled. func (fe *FrontendUtil) ImageFilePicker() (string, error) { file, err := runtime.OpenFileDialog( fe.ctx, runtime.OpenDialogOptions{ Title: "Select Cover Art", Filters: []runtime.FileFilter{ { DisplayName: "Image Files (*.jpg, *.jpeg, *.png)", Pattern: "*.jpg;*.jpeg;*.png", }, }, }, ) if err != nil { return "", fmt.Errorf("could not open file dialog: %w", err) } return file, nil } ``` After adding both methods, run `make generate` to regenerate Wails TypeScript bindings (this will create the `WriteTrackTagsByPath` and `ImageFilePicker` bindings in `frontend/wailsjs/go/`). Ensure both methods follow codebase conventions: doc comments ending with periods, error wrapping with `%w`, `fmt.Errorf` context. `go build -tags webkit2_41 ./...` compiles without errors. `make generate` succeeds and creates new TypeScript bindings. `rg "WriteTrackTagsByPath" frontend/wailsjs/go/tagwriter/` shows the generated binding. `rg "ImageFilePicker" frontend/wailsjs/go/frontendutil/` shows the generated binding. WriteTrackTagsByPath method exists on TagWriter, resolves filePath→trackID via GetAudioFileByPath, delegates to WriteTrackTags. ImageFilePicker method exists on FrontendUtil, opens native file dialog filtered to JPEG/PNG, returns selected path. Both have TypeScript bindings generated. Task 2: Add TrackMetadataChanged handler and fix context menu conditions frontend/src/store/library-store.ts, frontend/src/components/track-list/track-list.ts, frontend/src/components/queue-panel/queue-panel.ts, frontend/src/components/cover-grid/cover-grid.ts, frontend/src/components/playlist-details/playlist-details.ts **In `frontend/src/store/library-store.ts`**, add a `TrackMetadataChanged` event listener in the constructor, after the existing `LibraryRenamed` listener: ```typescript EventsOn(Events.TrackMetadataChanged, () => { this.invalidate(); }); ``` This causes a full cache invalidation + re-fetch of all library data (tracks, albums, artists, genres) whenever any track's tags are written. Full reload is acceptable per the CONTEXT.md decision: "Full reload is acceptable because editing is a low-frequency operation." **In `frontend/src/components/track-list/track-list.ts`**, find the context menu rendering where "Track Details" is conditionally shown (around line 1966). Change the condition from `this.selection.selectionCount === 1` to always show the item. The item should appear when right-clicking any track. Per CONTEXT.md: "'Track Details' should appear in the context menu when right-clicking any track, regardless of selection state." Replace: ```typescript ${this.selection.selectionCount === 1 ? html` this.onContextMenuAction('track-details')}> ``` With: ```typescript ${html` this.onContextMenuAction('track-details')}> ``` Remove the corresponding `: nothing}` closing. When `track-details` action is triggered with multiple selections, use the first selected track (or the right-clicked track). Check how `onContextMenuAction` resolves the target — it should use the context menu target row's `FilePath`, not require exactly 1 selection. **In `frontend/src/components/queue-panel/queue-panel.ts`**, apply the same fix: remove the `selectionCount === 1` condition around the "Track Details" context menu item (around line 1553). The queue always has a specific right-click target (the clicked track row), so Track Details should always be available. **In `frontend/src/components/cover-grid/cover-grid.ts`**, find the Track Details context menu item condition (it checks `contextMenuTarget.kind === 'track' && selectedTracks.size === 1`). Change to only check `contextMenuTarget.kind === 'track'` — the dialog opens for the right-clicked track regardless of multi-selection. **In `frontend/src/components/playlist-details/playlist-details.ts`**, apply the same fix: remove the `selectionCount === 1` condition for the "Track Details" menu item. For all 4 components: when Track Details is activated with multiple tracks selected, the `openTrackDetails` method should open details for the first selected track (or the context-menu-target track). Review each component's `openTrackDetails` to ensure it works with the right-clicked track, not the full selection. `pnpm run typecheck` in frontend/ passes. `rg "TrackMetadataChanged" frontend/src/store/library-store.ts` shows the new event handler. `rg "selectionCount === 1" frontend/src/components/track-list/track-list.ts frontend/src/components/queue-panel/queue-panel.ts` returns no matches (condition removed). TrackMetadataChanged event handler added to LibraryStore — after a tag write, all caches are invalidated and views refresh. "Track Details" context menu item appears when right-clicking any track in all 4 views (track-list, queue-panel, cover-grid, playlist-details) regardless of how many tracks are selected. - `go build -tags webkit2_41 ./...` compiles - `pnpm run typecheck` (in frontend/) passes - `make generate` succeeds - WriteTrackTagsByPath binding exists in `frontend/wailsjs/go/tagwriter/` - ImageFilePicker binding exists in `frontend/wailsjs/go/frontendutil/` - LibraryStore listens for TrackMetadataChanged - "Track Details" context menu item no longer gated on single selection Backend methods ready for Plan 02 to call from the track-details dialog. Library store will automatically refresh all views when tag writes complete. Context menu shows "Track Details" for any right-clicked track. After completion, create `.planning/phases/17-single-track-edit/17-01-SUMMARY.md`