crabidy/plan/summary.md

9.1 KiB
Raw Blame History

Implementation summaries

progressive-queueing (2026-07-21)

Built per plan/progressive-queueing.md: queueing a large nested collection now fills the queue progressively instead of freezing until the full resolve. ProviderClient gained resolve_tracks_into (chunk-streaming over a bounded channel; sender-drop = done, receiver-drop = cancel) with a default pre-order walk; tidaldy overrides it so playlists and albums emit one chunk per fetched 50-track page. The playback loop registers a pending op per queue command, spawns a forwarder, applies chunks on the loop (single-writer preserved), broadcasts after every chunk, and starts playback with the first chunk that makes a track current. Replace/Clear cancel in-flight resolves down to the HTTP fetch. The wire gained Queue.resolving = 4 (additive); the TUI renders an animated one-to-three dots pseudo-item after the last queue row while it is set. All 76 workspace tests green; every gate in quality/progressive-queueing.md checked. Verified against the live Tidal API with a temporary ignored probe (removed after passing): a 71-album artist streamed its first 19-track chunk (first album, listing order) while the walk was still running, and dropping the receiver mid-stream ended the resolve cleanly in 1.8 s instead of draining the discography.

The whole feature ran autonomously per standing instruction; decisions are recorded in architecture/progressive-queueing.md (options + rationale).

Deviations from plan / architecture (progressive-queueing)

  • make_paginated_request_into became stream_track_pages_into: the planned generic AsyncFnMut page sink dies on a rustc "implementation of Send is not general enough" limitation inside async_trait methods. The concrete method (fixed Track item type, proto mapping and channel send inlined) sidesteps it with the same page-loop and cancellation semantics.
  • Zero-track warning lives in the forwarder, not finish_resolve: the forwarder sees each path and its chunk count, so the existing per-path "resolved to no playable tracks" message survives verbatim; the planned op-level warning would have had to smuggle paths into PendingResolve.
  • Fixed alongside (user-reported): Enter on a non-queueable library item used to blank the queue while audio kept playing. Two causes, both fixed: Library::get_selected now gates the bare selection on is_queable (marks were already gated), and a replace that resolves to zero tracks no longer touches the queue at all — structurally, since the queue is only mutated by arriving chunks. Regression test queue_ops_ignore_non_queueable_selections.
  • Queue (play-next) captures the current position when the command arrives, not per chunk: chunks of one op stay contiguous after the track the user was on when they pressed the key, even if playback advances mid-resolve.
  • Live probe scope: the first full-discography probe was cut short (hundreds of album fetches for no extra signal) and replaced by a receive-two-chunks-then-cancel probe — which also exercises mid-stream cancellation against the live API, which the drain-everything version could not.
  • Environment note: builds/tests again ran with a session-local CARGO_TARGET_DIR (owner-built artifacts in target/); no repo change.

node-editing (2026-07-20)

Built per plan/node-editing.md: search-term nodes (created via %) are now modifiable — e opens the input overlay prefilled with the current title and renames (re-running the search; merge on title collision), d deletes without confirmation (documented decision, architecture/node-editing.md D4). Capabilities travel as LibraryNodeChild.is_editable/is_deletable (fields 5/6, child-only — no consumer for node-level copies), surfaced as a [ed] marker; two new rpcs RenameLibraryNode (returns the renamed node, TUI navigates into it) and DeleteLibraryNode (returns the refreshed parent). All 59 workspace tests green; every gate in quality/node-editing.md checked. Verified against the live Tidal API with a temporary ignored probe (removed after passing): create beatles → rename to rolling stones (in-place, 20 tracks / 40 children) → a track queued under the old term still resolved a stream URL → delete emptied the listing.

The whole feature ran autonomously per standing instruction; decisions are recorded in architecture/node-editing.md (options + rationale per topic).

Deviations from plan / architecture (node-editing)

  • Self-rename bug caught by the gate tests: the first rename_search_term implementation deleted a term renamed to itself (the merge branch removed the "old" slot). Fixed with an explicit old != new guard; the architecture text ("merge on collision") now implicitly means distinct titles.
  • pane_bindings_only_match_their_own_pane (help-modal suite) updated: it asserted plain d is unbound in the library — now it is LibraryDeleteNode by design; the test's queue-only example key moved to c.
  • delete_library_node also maps InvalidInputinvalid_argument although no provider raises it for delete today — keeps the error contract uniform across the three node-mutation rpcs.
  • End-to-end check ran at the provider layer (as with search): no interactive terminal/audio device in this environment; the gRPC handler and TUI layers above it are covered by unit tests and review.
  • Environment note: builds/tests again ran with a session-local CARGO_TARGET_DIR (owner-built artifacts in target/); no repo change.

search (2026-07-20)

Built per plan/search.md: % inside /tidal/search opens a one-line input; the term becomes a persistent (per-process) tree node holding Tidal search results — 20 tracks queueable in place, plus artist/album results as canonical /tidal/artists/... children. Creatable nodes carry an is_creatable flag end-to-end (proto → provider → TUI marker [%] + pane hint). All 48 workspace tests green; every gate in quality/search.md checked. Verified against the live Tidal API: payload shapes match the existing models (probe kept as the ignored probe_search_shapes test), and a full create→list→resolve-URL round trip succeeded (beatles → 20 tracks / 40 children, idempotent, playable stream URL from a search-track path).

The whole feature ran autonomously on user instruction; decisions were taken without mid-stage confirmation and recorded in architecture/search.md (options + decision per topic).

  • Library::update now concatenates tracks and children (tracks first). The old code showed tracks instead of children, which would have hidden the artist/album results on term nodes — architecture assumed both would render. Existing nodes are unaffected (they only ever carry one kind).
  • Search categories degrade independently: a failing category logs and contributes nothing; only all three failing is a FetchError. The plan did not specify partial-failure behavior.
  • get_lib_node no longer requires a user id up front — the gate moved into the favorites arms (planned), which also means create_lib_node validation works fully offline (used by the new unit tests).
  • End-to-end check ran at the provider layer (temporary ignored test, removed after passing) rather than driving the full TUI + server — no interactive terminal/audio device in this environment. The gRPC handler and TUI layers above it are covered by unit tests and review.
  • Environment note: target/ contains owner-built artifacts not writable by this agent's user; builds/tests ran with a session-local CARGO_TARGET_DIR. No repo change involved.

help-modal (2026-07-20)

Built per plan/help-modal.md: app/bindings.rs (declarative BINDINGS table + lookup + key_label), app/help.rs (overlay), the App::dispatch/DispatchResult seam, and the rewired event loop in main.rs. All 20 tests pass; every gate in quality/help-modal.md checked.

Deviations from plan / architecture (help-modal)

  • Two-column modal layout. The architecture assumed a single-column list; the full table is ~50 rows and would not fit even a 100×40 frame. The modal renders Global in the left column and Library + Queue stacked in the right column, with the close keys as a footer line (Close help: ?, Esc, q) derived from the Scope::Help bindings instead of a fourth listed group. The open question "scroll vs truncate" stays resolved as truncate — but after the column split the content fits ~34×94, so truncation only kicks in on genuinely small terminals.
  • Scope derives Hash (not in the stub) so the chord-uniqueness test can use a HashSet.
  • QueueInsertHere description reworded to "Insert library selection after this track": crabidy-server's insert_tracks splices at position + 1. Same check confirmed the planned "Queue selection after current track" wording for LibraryQueueNext.
  • main.rs passes tx to App::new without the now-unneeded clone; the KeyCode/KeyModifiers/UiFocus/StatefulList imports moved out with the old match.