# 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` 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`; 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: ` → `/tidal/artists/`, queueable) then albums (`Album: ` → `/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`.