9.1 KiB
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_intobecamestream_track_pages_into: the planned genericAsyncFnMutpage sink dies on a rustc "implementation ofSendis not general enough" limitation insideasync_traitmethods. The concrete method (fixedTrackitem 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 intoPendingResolve. - 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_selectednow gates the bare selection onis_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 testqueue_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 intarget/); 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_termimplementation deleted a term renamed to itself (the merge branch removed the "old" slot). Fixed with an explicitold != newguard; the architecture text ("merge on collision") now implicitly means distinct titles. pane_bindings_only_match_their_own_pane(help-modal suite) updated: it asserted plaindis unbound in the library — now it isLibraryDeleteNodeby design; the test's queue-only example key moved toc.delete_library_nodealso mapsInvalidInput→invalid_argumentalthough 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 intarget/); 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).
Deviations from plan / architecture (search)
Library::updatenow 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_nodeno longer requires a user id up front — the gate moved into the favorites arms (planned), which also meanscreate_lib_nodevalidation 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-localCARGO_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 theScope::Helpbindings 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. ScopederivesHash(not in the stub) so the chord-uniqueness test can use aHashSet.QueueInsertHeredescription reworded to "Insert library selection after this track":crabidy-server'sinsert_trackssplices atposition + 1. Same check confirmed the planned "Queue selection after current track" wording forLibraryQueueNext.main.rspassestxtoApp::newwithout the now-unneeded clone; theKeyCode/KeyModifiers/UiFocus/StatefulListimports moved out with the old match.