131 lines
7.1 KiB
Markdown
131 lines
7.1 KiB
Markdown
# Plan — search via creatable nodes
|
|
|
|
Ordered tasks for the `implement` stage. Inputs: `architecture/search.md`,
|
|
stubs across all five crates, gates in `quality/search.md`. Tests:
|
|
`devenv shell -- cargo test -p crabidy-core -p tidaldy -p cbd-tui`
|
|
(11 failing at plan time = the target). Note: use a session-local
|
|
`CARGO_TARGET_DIR` if `target/` contains root-owned artifacts from the
|
|
repo owner's builds.
|
|
|
|
## 1. Path segment encoding (crabidy-core)
|
|
|
|
- [x] Add the `percent-encoding` crate (workspace dependency; check the
|
|
nixpkgs/devenv side is unaffected — pure Rust). Implement
|
|
`encode_segment` (encode `/`, `%`, whitespace, controls and everything
|
|
non-alphanumeric-unreserved via an `AsciiSet`) and `decode_segment`
|
|
(lossy UTF-8 decode). **Verify**: the three `crabidy-core` tests pass
|
|
(`encode_segment_round_trips_arbitrary_text`,
|
|
`encoded_segments_are_path_safe`, `decode_segment_is_lossy_not_panicky`);
|
|
gates "encode/decode are total", "only percent-encoding added".
|
|
|
|
## 2. Verify the search payload shape (tidaldy, live API)
|
|
|
|
- [x] Before typing the models: run the existing explorer helper
|
|
(`Client::search`) once against the live API (needs the local tidal
|
|
config; if unavailable, consult the response shapes used by other tdl
|
|
clients and mark the gate as verified-by-proxy). Confirm whether
|
|
`search/tracks|artists|albums` return the same `Page<T>` item shapes as
|
|
the library endpoints; put any deviation into `tidaldy::models`.
|
|
**Verify**: gate "models verified against the live API".
|
|
|
|
## 3. Typed search requests (tidaldy)
|
|
|
|
- [x] Implement `search_tracks/search_artists/search_albums`: single
|
|
`make_request` with `query`, `limit=SEARCH_RESULT_LIMIT`, `offset=0`,
|
|
decoding a `Page<T>`; no pagination loop. **Verify**: gate "first page
|
|
only"; unit-testable only against live API (leave network test
|
|
`#[ignore]` like the existing one).
|
|
|
|
## 4. Search subtree in the provider (tidaldy)
|
|
|
|
- [x] `is_track_path`: include `TidalPath::SearchTrack`; `track_id_from_path`
|
|
already falls out (its match is on the parsed variant — extend it).
|
|
**Verify**: `search_track_paths_are_track_paths`,
|
|
`track_id_is_extracted_from_search_track_paths` pass.
|
|
- [x] `get_lib_node` `Search` arm: children = one entry per stored term
|
|
(path `join_path(path, encode_segment(term))`, title = raw term,
|
|
`is_queable: false`), node `is_creatable: true`. Do not require
|
|
`user_id` for search paths (move the `get_user_id` gate into the arms
|
|
that need it — search must work even if the user id is missing).
|
|
**Verify**: gate "search node lists terms"; ignored network test
|
|
extended by hand.
|
|
- [x] `get_lib_node` `SearchTerm` arm: decode the term; implicitly register
|
|
unknown terms (stale-client recovery); fetch the three categories
|
|
concurrently (`tokio::join!`); node = tracks from `search_tracks`
|
|
(`to_proto(path)`), children = artists (`Artist: <name>` →
|
|
`/tidal/artists/<id>`, queueable) then albums (`Album: <title>` →
|
|
`/tidal/artists/<artist-id>/<album-id>`, queueable), `is_queable:
|
|
false`, `is_creatable: false`. **Verify**: gates "term node" +
|
|
"canonical children"; behavior exercised end-to-end in task 9.
|
|
- [x] `create_lib_node`: trim title → `InvalidInput` if empty; parent must
|
|
parse to `TidalPath::Search` else `NotSupported`; store raw term
|
|
idempotently (no duplicates, lock not held across await); return
|
|
`self.get_lib_node(term_path)`. **Verify**: gate "create semantics";
|
|
add non-network unit tests: create with bad parent / empty title on an
|
|
offline client returns the right errors (no API call happens before
|
|
validation).
|
|
|
|
## 5. Server plumbing (crabidy-server)
|
|
|
|
- [x] `ProviderOrchestrator::create_lib_node`: `/tidal`-prefixed parent →
|
|
`tidal_client.create_lib_node`, anything else `NotSupported` (warn like
|
|
the other routes). **Verify**: gate "orchestrator routing".
|
|
- [x] `rpc.rs create_library_node`: bounded(1) rendezvous with
|
|
`ProviderCommand::CreateLibraryNode`; map errors `NotSupported` →
|
|
`failed_precondition`, `InvalidInput` → `invalid_argument`, rest →
|
|
`internal`. **Verify**: gate "error mapping"; `cargo check`.
|
|
|
|
## 6. TUI input overlay (cbd-tui)
|
|
|
|
- [x] `App::dispatch(LibraryCreateNode)`: open
|
|
`InputState { parent_path: library.path(), buffer: "" }` only when
|
|
`library.is_creatable()`. **Verify**:
|
|
`create_node_only_opens_input_on_creatable_nodes`.
|
|
- [x] `App::handle_input_key`: Esc cancels; Enter trims + sends
|
|
`MessageFromUi::CreateNode` (empty → just close); Backspace pops;
|
|
`KeyCode::Char(c)` appends regardless of SHIFT; all else ignored.
|
|
**Verify**: `input_appends_and_backspace_pops`,
|
|
`esc_cancels_without_sending`, `enter_submits_trimmed_title_and_closes`,
|
|
`enter_on_empty_input_closes_without_sending`.
|
|
- [x] Event loop (`main.rs run_ui`): when `app.input.is_some()`, route the
|
|
key to `app.handle_input_key(key)` and skip `bindings::lookup`
|
|
entirely. **Verify**: gate "input mode bypasses bindings" (read the
|
|
loop; also confirm `q` cannot quit while typing).
|
|
|
|
## 7. TUI library rendering (cbd-tui)
|
|
|
|
- [x] `Library::update`: apply empty nodes when `node.is_creatable` (keep the
|
|
skip for empty non-creatable nodes). **Verify**:
|
|
`empty_creatable_nodes_are_enterable`.
|
|
- [x] Mark creatable children in the list (suffix marker, `COLOR_SECONDARY`)
|
|
— requires keeping `is_creatable` on `UiItem`; hint in the pane title
|
|
when the open node is creatable (e.g. `search — % to add`).
|
|
**Verify**: gate "creatable marked in UI" (read render code).
|
|
- [x] Render the input overlay: bottom line of the library pane showing
|
|
`new node: <buffer>▏` while `input.is_some()`. **Verify**: gate "input
|
|
overlay renders"; add a `TestBackend` test asserting the buffer text
|
|
appears while open (mirror the help-modal test helpers).
|
|
|
|
## 8. TUI ↔ server wiring (cbd-tui)
|
|
|
|
- [x] `RpcClient::create_library_node`: send request, evict
|
|
`library_node_cache` entry for `parent_path`, insert the returned node,
|
|
return it. **Verify**: gate "cache eviction" (read; the cache is
|
|
private — no test seam without refactoring, keep it a gate).
|
|
- [x] `main.rs poll`: `CreateNode` arm calls the client method; on success
|
|
`ReplaceLibraryNode(node)`; on failure log and leave the UI unchanged
|
|
(no panic — replace the stub's `todo!`). **Verify**: gate "create
|
|
failures surface as errors, never a panic".
|
|
|
|
## 9. End-to-end + gates sweep
|
|
|
|
- [x] Run the real stack (`crabidy-server` + `cbd-tui`, needs tidal login):
|
|
enter `/tidal/search`, `%`, type a term, Enter; results appear; queue a
|
|
track result; dive into an artist result. If no login is available,
|
|
exercise create/list/error paths against the offline client instead
|
|
and note it in the summary. **Verify**: architecture flow diagram
|
|
matches reality.
|
|
- [x] Sweep: no `todo!()` left (workspace grep), fmt + clippy + tests green,
|
|
every `quality/search.md` box checked, docs updated where behavior
|
|
shifted. Append the outcome + deviations to `plan/summary.md`.
|