Files
yellowjacket/.planning/phases/17-single-track-edit/17-01-PLAN.md
T

12 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
17-single-track-edit 01 execute 1
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
true
EDIT-01
EDIT-04
truths artifacts key_links
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
path provides contains
backend/tagwriter/pipeline.go WriteTrackTagsByPath method on TagWriter func (tw *TagWriter) WriteTrackTagsByPath
path provides contains
backend/frontendutil/frontendutil.go ImageFilePicker method for cover art selection func (fe *FrontendUtil) ImageFilePicker
path provides contains
frontend/src/store/library-store.ts TrackMetadataChanged event handler calling invalidate() TrackMetadataChanged
path provides
frontend/src/components/track-list/track-list.ts Track Details context menu item visible for any right-clicked track
path provides
frontend/src/components/queue-panel/queue-panel.ts Track Details context menu item visible for any right-clicked track
from to via pattern
frontend/src/store/library-store.ts backend events EventsOn(Events.TrackMetadataChanged) EventsOn.*TrackMetadataChanged
from to via pattern
backend/tagwriter/pipeline.go backend/database GetAudioFileByPath query 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.

<execution_context> @/home/caleb/.config/opencode/get-shit-done/workflows/execute-plan.md @/home/caleb/.config/opencode/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/17-single-track-edit/17-CONTEXT.md

From backend/tagwriter/pipeline.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:

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:

func (q *Queries) GetAudioFileByPath(ctx context.Context, filePath string) (AudioFile, error)

From backend/frontendutil/frontendutil.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:

class LibraryStore {
    private invalidate(): void { ... }
    // Currently listens for: LibraryScanComplete, LibraryRemoved, LibraryAdded, LibraryRenamed
    // Does NOT listen for TrackMetadataChanged
}

From frontend/src/events.ts:

export const Events = {
    TrackMetadataChanged: "TrackMetadataChanged",
    // ...
} as const;

Context menu pattern (track-list.ts line ~1966):

${this.selection.selectionCount === 1
    ? html`<wa-dropdown-item @click=${() => this.onContextMenuAction('track-details')}>
          <wa-icon slot="icon" name="circle-info"></wa-icon>
          Track Details
      </wa-dropdown-item>` : 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:
// 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:

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

${this.selection.selectionCount === 1
    ? html`<wa-dropdown-item @click=${() => this.onContextMenuAction('track-details')}>

With:

${html`<wa-dropdown-item @click=${() => 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

<success_criteria> 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. </success_criteria>

After completion, create `.planning/phases/17-single-track-edit/17-01-SUMMARY.md`