diff --git a/CLAUDE.md b/CLAUDE.md index 6d4de067..a788bdbe 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -25,7 +25,7 @@ Long-running and privileged work lives in **sidecars**: separate processes that `/api/sidecar/register` and are tracked in `src/servers/sidecar-registry.ts`. PM2 runs them (`ecosystem.config.cjs`): `officer` (the server), `officer-anthropic-proxy`, `officer-agent`, `officer-opencode`, `officer-email`, `officer-pty`, `officer-vnc`, `officer-music`, `officer-vault`, -`officer-slskd`. +`officer-slskd`, `officer-headscale`. **`officer-anthropic-proxy` and `officer-agent` are not the same thing.** The proxy holds the Anthropic credential and forwards API traffic; the agent is the process that spawns `claude`. They were one entry @@ -205,14 +205,49 @@ Worked examples: `/soulseek`, `/music`, `/chat`. that pins `appType`s to an allow-list. Panels are windowed apps under `src/workspaces/officerdev/src/apps//`, each exporting `appRegistryMetas` (`{ key, name, icon, component, availableOnPanel: false }`) and registered in `AppRegistry.tsx`. - Panels coordinate via `usePanelChannel`. + The Workspace/Panel framework is orthogonal to routing — it contains no route navigation, and panels + live inside the Route element tree, so they can call `useParams`/`useSearchParams` directly. - **Page title by route.** Add a rule to `RULES` in `src/apps/officer-web/state/usePageTitle.ts` (`{ match: (p) => p.startsWith('/'), title: '' }`, most-specific first); - `usePageTitleSync` does the rest. + `usePageTitleSync` does the rest. `startsWith` means nested routes are covered. + +### The URL is the source of truth for selection + +`docs/navigation-audit.md` is the authority here — read it before building a screen that selects +things. It names the **"opaque click" anti-pattern**: an element that opens something addressable but +keeps the id in an onClick closure instead of the DOM, leaves the URL unchanged, holds the selection +in `usePanelChannel`/`useGlobal`, and has no anchor semantics (no cmd-click, no middle-click, not +link-focusable). Half the app still does this; none of the new code should. + +- **Addressable state goes in the URL** (`useParams` / `useSearchParams`), never in a channel. + `usePanelChannel` is for genuine signals and refresh buses (`preview:refresh`, + `files:refresh-signal`, `SLSKD_REFRESH_CHANNEL`, `MUSIC_RESYNC_CHANNEL`, `FILE_VIEWER_CHANNEL`). + "Which thing is open" is a URL. Panels each read the URL rather than passing it between themselves. +- **Rows and nav items are real links.** `` for rows (exemplar: the `/chat` session list, + `f35c145`); **react-router's ``** for nav chrome, so active state comes from the router. + The hand-rolled `isActive` in `Dock`/`Header` is scheduled for replacement (audit Phase 4) — don't + copy it. A disabled entry renders as a ``; a disabled `` is not a thing. A control that + *mutates* rather than navigates stays a `