soulseek: nav sections, search results subpanel, stable client deps

- split into nav (vertical section menu) + view panels via soulseek:section channel
- search view: history list + input, opens results subpanel for a past search
- search results load stored responses (no re-run), session-cached, ranked
- remove client from effect/callback deps (useClient is a fresh object per render)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-29 18:19:39 +00:00
co-authored by Claude Opus 4.8
parent 2a509646e7
commit c1594594f5
10 changed files with 511 additions and 222 deletions
@@ -4,15 +4,15 @@ import { WorkspaceView } from 'officerdev';
import { useDashboardState } from 'state/useDashboardState';
import { defaultLayout } from './defaultLayout';
// /soulseek uses the Workspace/Panel system (like /chat and /music): two panels — the search browser
// (soulseek-search) and the download queue (soulseek-transfers) — coordinating via the 'soulseek:refresh'
// panel channel. Both talk to slskd through the /api/slskd auth proxy.
// /soulseek uses the Workspace/Panel system (like /chat and /music): a section nav (soulseek-nav) on
// the left and a section view (soulseek-view) on the right, coordinating via the 'soulseek:section'
// channel. Both talk to slskd through the /api/slskd auth proxy.
const ALLOWED_APP_TYPES = new Set<string | null>(['soulseek-search', 'soulseek-transfers', null]);
const ALLOWED_APP_TYPES = new Set<string | null>(['soulseek-nav', 'soulseek-view', null]);
function normalizeLayout(node: LayoutNode): LayoutNode {
if (node.type === 'panel') {
return ALLOWED_APP_TYPES.has(node.appType) ? node : { ...node, appType: 'soulseek-search' };
return ALLOWED_APP_TYPES.has(node.appType) ? node : { ...node, appType: 'soulseek-view' };
}
const children = node.children.map((c) => {
const fixed = normalizeLayout(c.node);
@@ -23,7 +23,9 @@ function normalizeLayout(node: LayoutNode): LayoutNode {
}
export const SoulseekScreen = () => {
const rawWorkspace = useDashboardState<LayoutNode>('screens/soulseek', defaultLayout);
// v2: nav + view (replaced the earlier search + transfers split) — new key so the old persisted
// layout doesn't resurrect as two mismatched panels.
const rawWorkspace = useDashboardState<LayoutNode>('screens/soulseek-v2', defaultLayout);
const workspace = useMemo(() => {
const fixed = normalizeLayout(rawWorkspace.value);
@@ -5,7 +5,7 @@ export const defaultLayout: LayoutNode = {
id: 'soulseek-root',
direction: 'horizontal',
children: [
{ node: { type: 'panel', id: 'soulseek-search', appType: 'soulseek-search' }, size: 55 },
{ node: { type: 'panel', id: 'soulseek-transfers', appType: 'soulseek-transfers' }, size: 45 },
{ node: { type: 'panel', id: 'soulseek-nav', appType: 'soulseek-nav' }, size: 22 },
{ node: { type: 'panel', id: 'soulseek-view', appType: 'soulseek-view' }, size: 78 },
],
};