put the music library location in the url

/music?path=<rel> replaces the music:cwd channel. Each panel reads the param
itself through useMusicCwd(), so MusicBrowser, MusicDetail and FavoritesView
no longer tell each other where they are, and every drill-in is a <Link>:
library rows, folder rows, album/artist cards, both "up" affordances, the
favorites rows, and the dock's now-playing tile. Track rows stay buttons —
they play, which is a mutation.

A query param rather than a nested route because the location is only one of
the things this screen holds (the lyrics split and the favorites view are the
others), and a splat has to be a route's last segment.

MusicPlayerHost is mounted outside <Routes> and used to write the channel and
then navigate('/music') to make the write visible — the audit's only
navigate-with-a-side-effect. That collapses to one <Link>.

music:resync (a refresh signal) and music:favorites (a view of one panel) stay
channels, deliberately.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-07 12:08:04 +00:00
co-authored by Claude Opus 5
parent 374140d3a6
commit aef8619c6c
8 changed files with 164 additions and 122 deletions
+17 -8
View File
@@ -87,9 +87,15 @@ are good building blocks. The **Workspace/Panel framework** contains **zero** ro
**Music** (M-music) and **Soulseek** (M-slsk) are whole-workspace channel apps — pulled out below because each
is **one design decision** that cascades across many files:
- **Music** — `Music/{MusicBrowser:182,209 · MusicDetail:138,174,234 · FavoritesView:85,107}` hold the entire
library location in `usePanelChannel('music:cwd')`; every drill-in is `setCwd(path)`. No URL, no deep-link to
an album. → back `music:cwd` with `/music?path=<rel>`; rows become `<Link>`s.
- ~~**Music**~~**done.** The library location is `/music?path=<rel>` (`rel` relative to the `Music` root);
`music:cwd` is deleted. Each panel calls `useMusicCwd()` and reads the param itself, so `MusicBrowser`,
`MusicDetail` and `FavoritesView` no longer tell each other anything. Every drill-in is a `<Link>` — library
rows, folder rows, album/artist cards, both "up" affordances, the favorites rows, and the dock's now-playing
tile. Track rows stay `<button>`s: they play, which is a mutation. Two things that were channel-shaped and
stayed channels: `music:favorites` (a view toggle over this one panel) and `music:resync` (a refresh signal).
A query param rather than `/music/*` because the location is only one of the things this screen holds — the
lyrics split and the favorites view are the others — and because a splat has to be a route's last segment.
**Needs runtime test.**
- ~~**Soulseek**~~ — **mostly done.** The section is `/soulseek/:section` (nav entries are `NavLink`s, the
view panel reads the same URL), the peer is `?user=<name>`, and the past-search was already `?search=<id>`.
The `soulseek:section` and `soulseek:user` channels are deleted; `soulseek:refresh` stays, which is a
@@ -114,7 +120,11 @@ is **one design decision** that cascades across many files:
- **Jobs step/iteration** (`Jobs/JobDetail.tsx:234,255`) — intra-detail sub-selection; only if we want `/jobs/:id/:stepKey` deep links.
- **Preview slug** (`Preview/PreviewApp.tsx:23`) — Preview usually derives its target from the surrounding dashboard/project `cwd`; the manual picker is a fallback. Probably leave URL-less.
- **FileBrowser widget** (`FileBrowserWidget/.../BrowseTab.tsx:36`) — compact sidebar widget; keeping its browse cursor local is defensible.
- **Music favorites view** (`music:fav`) & **SystemMonitor scope** — view toggles over tiny fixed sets; channel-state is arguably fine.
- **Music favorites view** — the channel is `music:favorites`, not `music:fav`. **Decided: it stays a channel.**
It is a view of the detail panel rather than a location, it survives no reload worth surviving, and the one
case where its being off-URL shows is handled explicitly: going Home or opening a favorite closes it by hand,
because navigating to where you already are fires no route change. (**SystemMonitor scope** was listed here
as the same kind of thing and was not — it is M10, and it is in the URL.)
- **"New Chat" button** (`ChatHistory/SessionList.tsx:68`) — a create-action that also `navigate('/chat/new')`; could be `<Link to="/chat/new">` if the state-set moves into the route. Its `chat:selected-session` write is part of the H4 cleanup.
---
@@ -130,7 +140,6 @@ This is the primary surface to convert to URL-driven selection.
| `chat:active-cwd` | chat working dir | query param on `/chat` | `ChatDetailPanel.tsx:102`, `SessionList.tsx:14` |
| `SELECTED_DASHBOARD_KEY` (`useGlobal`) | selected dashboard | `/dashboards/:id` | `DashboardListApp.tsx:36`, `DashboardPreview.tsx:287` (H2) |
| `SELECTED_PROJECT` (`useGlobal`) | selected project | `/projects/:id` | `ProjectListApp.tsx:45`, `ProjectPreview.tsx:386` (H3) |
| `music:cwd` | library location (album/artist/folder) | `/music?path=` | `MusicBrowser/MusicDetail/FavoritesView` |
| local `useState` selections | search / room / conversation / peer / open file / preview slug | respective nested routes | `SearchView`, `SoulseekRooms`, `SoulseekChat`, `SoulseekUsers`, `CodeEditor/useEditorState`, `PreviewProvider` |
**Excluded — event-bus / refresh signals, NOT selection:** `files:refresh-signal`,
@@ -163,7 +172,7 @@ publishers means changing the chat panel, which is another agent's, so it is wri
| 10 | `Jobs/JobDetail.tsx:709` | back button | `/jobs` | YES | use `BackButton`/`<Link>` |
| 11 | `ChatHistory/SessionList.tsx:70` | "New Chat" | `/chat/new` | ~ | borderline (LOW) |
| 12 | `components/Combobox.tsx:54` | nav combobox option | `option.href` | YES | M7 |
| 13 | `MusicPlayer/MusicPlayerHost.tsx:251` | open current album (setCwd+nav) | `/music` | ~ | has side-effect |
| ~~13~~ | `MusicPlayer/MusicPlayerHost.tsx:251` | open current album (setCwd+nav) | `/music` | ~ | **Done** — the side-effect was the `setCwd`; with the location in the URL the tile is a plain `<Link>` to `musicPath(albumRel)`. |
| 14 | `FileBrowserWidget/useFileBrowserWidget.ts:53` | open folder in Files | `/files?view=…` | ~ | query nav |
| 15 | `FileBrowserApp/useFileBrowserApp.ts:401` | create dashboard from folder | `/dashboards/new?…` | ~ | action-nav |
| 16 | `FileBrowserApp/useFileBrowserApp.ts:427` | create dashboard from files | `/dashboards/new?…` | ~ | action-nav |
@@ -209,7 +218,7 @@ publishers means changing the chat panel, which is another agent's, so it is wri
- [ ] **M7** Combobox: render `href` options as `<Link>` (`Combobox.tsx:53`).
### Phase 3 — Whole-workspace routing decisions (needs a design call first)
- [ ] **Music** decide `/music?path=` (or nested); back `music:cwd` with the URL; rows `<Link>` (M-music).
- [x] **Music**`/music?path=<rel>`; `music:cwd` deleted; every drill-in (including the dock's now-playing tile, navigate-site 13) is a `<Link>`. `music:favorites` and `music:resync` stay — a view toggle and a refresh signal. **Needs runtime test.**
- [x] **Soulseek**`/soulseek/:section` with the peer in `?user=` and the search already in `?search=`; the two selection channels are deleted. Rooms and conversations are still `useState`. **Needs runtime test.**
- [x] **M10** SystemMonitor scope → `/system-monitor/:scope`; `monitor:scope` channel deleted. **Needs runtime test.**
- [ ] **M9** Plans → `/plans/:name` (`Plans/index.tsx:36`).
@@ -218,7 +227,7 @@ publishers means changing the chat panel, which is another agent's, so it is wri
- [ ] Dock + Header + mobile sheet → react-router `<NavLink>` for active state; drop hand-rolled `isActive` (`Dock.tsx`, `Header.tsx`).
- [ ] "New Chat" → `<Link to="/chat/new">` (`SessionList.tsx:68`) once H4's channel cleanup lands.
- [ ] Jobs back button + any `navigate('/jobs')` → shared `BackButton` (`JobDetail.tsx:709`).
- [ ] Decide/skip: Jobs step deep-link, Preview slug, FileBrowser widget, Music-favorites view toggle. (Browser tabs: **done** — see the LOW section. Monitor scope: **done** as M10, it was not a view toggle.)
- [ ] Decide/skip: Jobs step deep-link, Preview slug, FileBrowser widget. (Browser tabs: **done** — see the LOW section. Monitor scope: **done** as M10, it was not a view toggle. Music favorites: **decided** — stays a channel, reasoning in the LOW section.)
### Cross-cutting for the refactor itself
- [ ] Standardise a URL-as-source-of-truth pattern for panel selection (replace the `usePanelChannel`/`useGlobal`