give the soulseek workspace a url

The section is /soulseek/:section — nav entries are NavLinks, the view panel reads the same
URL instead of being told, and the dashboard's tiles and recent searches are real links (a
recent search now opens that search, not the search screen's front page).

The peer went in `?user=<name>` rather than the /soulseek/users/:name the audit sketched: a
second path segment would need a nested route just to keep the nav highlight, and `?search=`
had already set the convention there. That deletes the `soulseek:user` channel and with it a
`{username, nonce}` request the Users panel consumed-once and cleared — the nonce existed so
asking for the same peer twice counted twice. A link is idempotent, so there is nothing to
consume and nothing to disambiguate.

`soulseek:refresh` stays: it is a signal, which is what channels are for.
This commit is contained in:
2026-08-07 11:45:33 +00:00
parent 00332e275a
commit fd923bb9be
12 changed files with 149 additions and 114 deletions
+9 -10
View File
@@ -36,7 +36,7 @@ That fix is the template for the HIGH items below. **Caveat:** the fix only did
## Route map (from `App.tsx`)
**Existing entity routes:** `/chat/:sessionId`, `/jobs/:id`, `/dashboards/:id`, `/projects/:id`, `/email/:emailId`.
**Flat screens:** `/` `/files` `/music` `/soulseek` `/tasks` `/skills` `/processes` `/activity`
**Flat screens:** `/` `/files` `/music` `/tasks` `/skills` `/processes` `/activity`
`/system-monitor` `/task-logs` `/plans` `/terminal` `/desktop` `/browser` `/code-editor`.
The five settings pages are route **pairs** now, not flat screens — `/settings/{profile,ai,system,integrations,user-management}`
plus a `:section` each (M6). There has never been a `/settings/apps`; that entry was wrong when this list
@@ -87,12 +87,13 @@ 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.
- **Soulseek** — an entire workspace with **no URL at all**: `soulseek:section` nav (`SoulseekNav:40`,
`SoulseekDashboard:88/96/110/121`), `soulseek:user` peer channel (`UserMenu:32`, `SoulseekUsers:42`), and
per-panel `useState` for past-search (`SearchView:160`, a textbook `role="button"` div with `s.id` in the
closure), rooms (`SoulseekRooms:153`), conversations (`SoulseekChat:135`). → decide a nested tree
`/soulseek/:section` + `/soulseek/{users/:name, search/:id, rooms/:name, chat/:name}`. **S1 (the section nav)
decides S2S7.**
- ~~**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
signal and not selection. The peer went in the query string rather than `/soulseek/users/:name` as
sketched here, because the section nav has to stay a one-segment `NavLink` — a second path segment would
need a nested route just to keep the highlight, and the query string is what `?search=` already uses.
Still on `useState`: rooms (`SoulseekRooms`) and conversations (`SoulseekChat`).
### 🟡 LOW / borderline
@@ -120,8 +121,6 @@ This is the primary surface to convert to URL-driven selection.
| `SELECTED_PROJECT` (`useGlobal`) | selected project | `/projects/:id` | `ProjectListApp.tsx:45`, `ProjectPreview.tsx:386` (H3) |
| `EMAIL_SELECTED` | open email | `/email/:emailId` | `EmailList.tsx:336`, `EmailScreen.tsx` sync (H5) |
| `music:cwd` | library location (album/artist/folder) | `/music?path=` | `MusicBrowser/MusicDetail/FavoritesView` |
| `soulseek:section` | active Soulseek section | `/soulseek/:section` | `SoulseekNav/View/Dashboard/UserMenu` |
| `soulseek:user` | selected peer | `/soulseek/users/:name` | `UserMenu.tsx:28`, `SoulseekUsers.tsx:34` |
| 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`,
@@ -201,7 +200,7 @@ publishers means changing the chat panel, which is another agent's, so it is wri
### 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).
- [ ] **Soulseek**decide the nested tree `/soulseek/:section` + `/soulseek/{users,search,rooms,chat}/:x`; **S1 decides S2S7** (M-slsk).
- [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`).
+10 -2
View File
@@ -646,8 +646,16 @@ All the same bug: an app guessing "am I being closed?" from an unmount, or payin
key now, so there is nothing left to disagree with.
**SystemMonitor is done too** (audit M10) — `/system-monitor/:scope`, scope list as `NavLink`s,
`monitor:scope` deleted. Smallest of the seven and the same shape as `/photos`.
Remaining here: Music, Soulseek, Email, Browser. **Chat detail (`chat:selected-session`) is
chat-owned — leave it; it is written up in `COMMS/`.**
**Soulseek is done** — `/soulseek/:section`, and the peer moved to `?user=<name>` rather than the
`/soulseek/users/:name` the audit sketched: a second path segment would have needed a nested route
just to keep the nav's `NavLink` highlight, and `?search=` was already establishing the convention
for that workspace. Deleting `soulseek:user` also deleted the nonce it carried — a request stamped
`{username, nonce}`, consumed-once by the Users panel so that returning to the section did not
re-run the lookup, and nonce-stamped so that asking for the *same* peer twice still counted twice.
A link needs none of that: it is idempotent, so there is nothing to consume and nothing to
disambiguate. That is the shape of most of this section's remaining work.
Remaining here: Music, Email, Browser. **Chat detail (`chat:selected-session`) is chat-owned —
leave it; it is written up in `COMMS/`.**
### 5.9 The context has grown an app-config section — _(found 2026-08-07)_
+5 -4
View File
@@ -126,10 +126,11 @@ connection status and the header renders it. Panel-scoped by construction, which
### The thirteen channels, honestly
13 channels, 26 call sites. Six carry selection that should be in the URL`music:cwd` (the entire
library location, so no album is linkable), `soulseek:section` and `soulseek:user` (the whole Soulseek
workspace has no URL), `chat:selected-session`, `monitor:scope`. Two are view toggles where a channel is
genuinely fine (`music:favorites`, `music:lyrics`).
Was 13 channels, 26 call sites. Six carried selection that belongs in the URL, and three of those are
gone: `soulseek:section` and `soulseek:user` (the whole Soulseek workspace now has one) and
`monitor:scope`. Still to move: `music:cwd` — the entire library location, so no album is linkable — and
`chat:selected-session`, which is the chat's to move, not this document's. Two are view toggles where a
channel is genuinely fine (`music:favorites`, `music:lyrics`).
Two are wired to nobody: `preview:refresh` and `chat:active-session` have publishers and no subscribers.
A third, `file-viewer:<panelId>`, had a subscriber and no publisher — it is gone, along with the