# Editable and deletable library nodes ## Context and problem statement The search feature (`architecture/search.md`) introduced *creatable* nodes: `%` under `/tidal/search` turns a typed term into a tree node holding search results. Those nodes are currently immutable — a typo'd term can only be abandoned, and stale terms accumulate for the lifetime of the server process. This feature makes such nodes *modifiable*: in the library pane, `e` renames the selected node (for a search term: re-runs the search under the new term) and `d` deletes it. Modifiability is a per-node capability advertised by the provider, exactly like `is_creatable` — the TUI never hardcodes which paths support what. Run autonomously per standing user instruction; every decision below records the options considered and the rationale. ## Assumptions - "Nodes that got created via `%`" are today exactly the search-term nodes under `/tidal/search`; the design must not special-case them (playlists are the obvious future candidate), but they are the only provider implementation in this iteration. - Editing means **renaming** (the title is the only user-supplied property a node has). For a search term, the title *is* the query, so a rename re-runs the search. - `e`/`d` act on the **selected item in the library list** (the same selection model as queueing), not on the currently-open node. - Proto changes must stay wire-compatible (additive fields/rpcs only), as with the search feature. ## Decisions ### D1 — Two capability flags, on the child only Options considered: 1. One `is_modifiable` flag implying both rename and delete. 2. Two flags `is_editable` / `is_deletable`. 3. Flags on both `LibraryNodeChild` and `LibraryNode` (mirroring `is_creatable`). **Decision: (2), child-only** — `LibraryNodeChild.is_editable = 5`, `LibraryNodeChild.is_deletable = 6`. Future node kinds plausibly support only one of the two (a favorites entry may be deletable but not renamable), and an extra bool costs nothing on the wire. Unlike `is_creatable` (consumed for the *open* node: pane hint + `%` target), edit/delete are only ever checked against the selected **child**, so node-level copies would have no consumer — they are left out until something reads them. ### D2 — RPC shapes Options considered: 1. One generic `UpdateLibraryNode` with optional fields + a separate delete. 2. `RenameLibraryNode(path, new_title) → node` and `DeleteLibraryNode(path) → parent node`. **Decision: (2).** Rename is the only edit that exists; a generic update message would be speculative surface area. Both responses carry the node the TUI should display next, following `CreateLibraryNode`: - **Rename returns the renamed node** and the TUI navigates into it — the same "show me the result" behavior as `%` create. The path changes on rename (term is percent-encoded into the path), so returning the node is also what tells the client the new path. - **Delete returns the refreshed parent node** — the user is looking at the parent listing when they press `d`; returning it saves a follow-up `GetLibraryNode` and can never serve a stale cached listing. ### D3 — Provider semantics (tidaldy) - `rename_lib_node(path, new_title)`: `path` must parse to `TidalPath::SearchTerm`, else `NotSupported`. Title is trimmed; empty → `InvalidInput`. The old term is **replaced in place** (keeps its position in the terms list). Renaming to an already-existing term merges: the old entry is removed, the existing one wins — the list never holds duplicates. Renaming a term the server doesn't know (stale client cache, restart) registers the new term — same forgiveness as `get_lib_node` on unknown terms. Returns `get_lib_node(new_path)`. - `delete_lib_node(path)`: `path` must parse to `TidalPath::SearchTerm`, else `NotSupported`. Removing an unknown term succeeds silently — delete is idempotent. Returns the refreshed parent node (`get_lib_node(/tidal/search)`). - Both reuse the `search_terms` `RwLock` discipline: poison-tolerant, never held across an await. - **Queued search tracks survive rename/delete**: a queue entry `/tidal/search//` resolves URLs/metadata from the embedded track id alone; term registration is irrelevant to playback. ### D4 — No delete confirmation (for now) **Options**: confirm prompt (`y`/`n` mini-mode) vs immediate delete. **Decision: immediate.** The only deletable nodes are search terms, which are free to recreate (`%` + retype); a confirmation mode adds a third input state for no protected value. **Open question**: when higher-value nodes (user playlists) become deletable, a confirmation step must be revisited — noted here so the decision is rediscovered. ### D5 — TUI input overlay grows a purpose `InputState` today hardcodes creation (`parent_path` + buffer). It becomes ```rust InputState { purpose: InputPurpose, buffer: String } enum InputPurpose { Create { parent_path }, Rename { path } } ``` - `e` opens the overlay **prefilled with the current title** (append-only editing as before: chars push, Backspace pops); Enter sends `MessageFromUi::RenameNode`, Esc cancels. Overlay label: `rename: ▏` vs `new node: ▏`. - Prefilling means "rename" degrades gracefully to "retype" — no cursor movement is introduced in this iteration (matches the existing overlay). - Submitting an unchanged title is sent anyway; the provider treats it as a no-op rename and returns the node (harmless refresh). - `d` sends `MessageFromUi::DeleteNode { path }` directly (D4). - Both keys are silently ignored when the selected item lacks the flag, mirroring `%` on non-creatable nodes. - List marker: editable/deletable children render a `[e]`, `[d]` or `[ed]` suffix in `COLOR_SECONDARY`, alongside the existing `[%]` for creatable ones. The bindings table gains `e`/`d` in the Library scope (plain `e` and `d` are unbound there today; `d` only exists in Queue scope), so the help modal picks them up automatically. ### D6 — Client cache handling `RpcClient` keeps `library_node_cache`. On rename: evict the **old path** and the **parent**, insert the returned node under its new path. On delete: evict the deleted path and the parent, insert the returned parent. (Same reasoning as create's eviction — a stale `/tidal/search` listing would resurrect the old term in the UI.) ### D7 — Server plumbing Two new `ProviderCommand`s (`RenameLibraryNode`, `DeleteLibraryNode`) with the established bounded(1) reply rendezvous; orchestrator routes `/tidal`-prefixed paths to the tidal client and answers anything else `NotSupported`. gRPC error mapping is identical to create: `NotSupported` → `failed_precondition`, `InvalidInput` → `invalid_argument`, rest → `internal`; no internals leak into `Status` messages. ## Flows ```d2 shape: sequence_diagram user: { shape: person } tui: cbd-tui server: crabidy-server tidal: tidaldy user -> tui: "e on selected [ed] node" tui -> tui: open overlay prefilled with title user -> tui: edit text, Enter tui -> server: RenameLibraryNode(path, new_title) server -> tidal: rename_lib_node tidal -> tidal: replace term in list (merge on collision) tidal -> server: node at new path (fresh search) server -> tui: renamed node tui -> tui: evict old path + parent, show renamed node user -> tui: "d on selected [ed] node" tui -> server: DeleteLibraryNode(path) server -> tidal: delete_lib_node tidal -> tidal: remove term (idempotent) tidal -> server: refreshed parent node server -> tui: parent node tui -> tui: evict path + parent, show parent listing ``` Capability flags travel with every listing: ```d2 direction: right tidaldy: { search_arm: "get_lib_node(/tidal/search)" } proto: "LibraryNodeChild { is_editable=5, is_deletable=6 }" tui: { list: "library list: title [ed]" keys: "e -> rename overlay\nd -> DeleteNode" } tidaldy.search_arm -> proto: term children flagged proto -> tui.list: render marker proto -> tui.keys: gate actions ``` ## Boundaries and risks - **Proto**: additive only — two rpcs, two child fields (5, 6). Old clients ignore the flags and never call the rpcs; old servers reject unknown rpcs with `unimplemented` (tonic default), which the TUI logs without crashing. - **Rename-to-collision** merges terms; the response node is the *existing* term's node. The user sees the results they asked for either way. - **Concurrent clients**: two TUIs editing the same term list race benignly — the list is a `RwLock`-guarded Vec, every operation is atomic under the write lock, and stale views self-heal on the next listing fetch. - **Not in scope**: editing anything but the title; deleting non-search nodes; confirmation UX (D4); persistent search terms (still per-process, as shipped by the search feature).