crabidy/plan/summary.md

12 KiB
Raw Blame History

Implementation summaries

fs-provider (2026-07-21)

Built per plan/fs-provider.md: a second media provider (crate fsdy, /fs) that walks one configured root directory and treats *.track.toml files as serialized track nodes — metadata plus exactly one playable reference: a local audio file (absolute or relative to the track file), an http(s) URL, or a crabidy-internal link. The wire types are unchanged (architecture D1): the only new datastructure is the on-disk TOML schema. Link tracks rewrite Track.path to the target at listing time (D2), so playback routes to the owning provider through the orchestrator's existing prefix routing with zero new mechanisms; links into /fs are rejected at parse time, making chains impossible. Directories list sorted and queue via the default chunked resolve walk; client paths are decoded and validated in a single helper so they cannot escape the root; symlinks, hidden entries, and broken files are skipped with warnings. ProviderOrchestrator gained an optional fs client (non-fatal init from fsdy.toml, default root dirs::audio_dir()) and /fs routing arms in every trait method. No player or TUI changes were needed. All 91 workspace tests green (15 new in fsdy); every gate in quality/fs-provider.md checked. A temporary live probe (removed after passing) built a real tree whose link track pointed at a track fetched from the live Tidal API: listing order held, the link path was rewritten, and the target resolved a stream URL — the full D2 story end-to-end.

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

Deviations from plan / architecture (fs-provider)

  • TrackFileError::UrlScheme carries only the scheme, not the URL: the parse error ends up in skip-warnings, and a private stream URL may embed a token (quality gate "no file contents in logs"). The architecture's schema and behavior are otherwise as designed.
  • The live probe ran at the provider layer, not against a running server (no interactive terminal/audio device here, same as previous features): fsdy and tidaldy clients driven directly, mimicking the orchestrator's routing exactly. It also had to fetch its link target first — the well-known id from the progressive-queueing probe is an album path, and a link must point at a track.
  • get_lib_node on a track path is MalformedPath — implicit in the design, made explicit so the default resolve walk can never mistake a track file for a directory.
  • Environment note: builds/tests again ran with a session-local CARGO_TARGET_DIR (owner-built artifacts in target/); no repo change.

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.