document how the workspace/panel framework works

This commit is contained in:
2026-08-07 04:10:36 +00:00
parent 33d55121cd
commit ce7968ac90
3 changed files with 274 additions and 4 deletions
+77 -4
View File
@@ -4,6 +4,9 @@ Living list. Add items as they are found, tick them as they land, and write the
item rather than deleting it — the reason a thing was done is worth more later than a clean list.
Move anything fully settled to §7.
**How the framework actually works is documented separately, in `workspace-panels.md`** — read that
first if you are new to it. This file is only the defect list and the work queue.
Findings and full reasoning: `COMMS/workspace-panel-framework-analysis-2026-08-07.md`. Every `file:line`
below was opened; DB claims were run against live `officer_dev`. Paths are relative to
`platform/src/workspaces/officerdev/src/` unless they start with `servers/`, `databases/` or `sidecars/`.
@@ -57,6 +60,19 @@ Both are two-line fixes, and without them you cannot tell whether any later fix
branches do not. A null falls to the UPDATE branch and sets a `NOT NULL` column
(`databases/officer_db/src/queries/dashboards.ts:70`) → 23502.
- [ ] **`HostTerminalWrapper` never strips the prefix, so it mints phantom dashboards.** *(found
2026-08-08, latent — `dashboards` is still 0 rows)*
`apps/Terminal/HostTerminalWrapper.tsx:12` is `` `ws-host-terminals-${dashboardId}` `` with **no
regex**, while its sibling `TerminalWrapper.tsx:13-14` correctly matches `^ws-layout-(.+)$` first.
So the key becomes `ws-host-terminals-ws-layout-<id>` or `ws-host-terminals-screens/terminal`, the
dispatcher's `^ws-host-terminals-(.+)$` branch captures that whole string as an id, and
`upsertDashboard` **inserts a row when the id is unknown** (`queries/dashboards.ts:75-86`,
`name: data.name ?? id`). `getAllDashboardState` maps every `dashboards` row into `workspaces`, so
the garbage id surfaces in the Dashboards list as a real dashboard.
Two apps deriving a state key from the same string by two different rules is the actual defect;
the id-shaped-string-as-a-bag problem behind it is §5.9.
Latent only because `officerdev/terminal-host` is in no default layout and hidden from the picker.
- [ ] **Renaming a dashboard resurrects it as a zombie row.**
`apps/Dashboards/DashboardPreview.tsx:316-318` PATCHes `ws-layout-old: null` *and*
`ws-terminals-old: null` together. The first deletes the row; the second then calls
@@ -116,12 +132,16 @@ these.
`databases/CLAUDE.md`'s stance that the schema is the source of truth for *contents*, a CHECK on
`jsonb_typeof(layout) = 'object'` is the cheap half.
- [ ] **A bare number is squatting in a framework namespace.** *(verified in the live DB)*
`apps/Soulseek/shared.ts:12` builds `screens/soulseek-zoom/${panelId}` and passes it to
- [x] **A bare number is squatting in a framework namespace.** *(verified in the live DB)*
`apps/Soulseek/shared.ts:12` built `screens/soulseek-zoom/${panelId}` and passed it to
`useDashboardState<number>`. The server routes `^screens/(.+)$` into `screens.layout`, so:
`user_id 1 | soulseek-zoom/soulseek-view | jsonb_typeof = number`. That namespace belongs to
layouts. Give per-panel scalar prefs their own key family (and a matching dispatcher branch, which
the §2 `else` will otherwise start rejecting).
layouts.
**Resolved `ba664dc`** — Soulseek's private zoom became a framework feature (`LayoutPanel.zoom`),
so the scalar now rides on the layout node inside `screens/soulseek-v2` and needs no key of its
own. The squatting row is gone: `select name from screens` returns 15 rows, none `soulseek-zoom/*`.
This is also the general answer for per-panel scalar prefs — put them on the node, not in a key,
because `useDashboardState` seeds a row per key on mount.
---
@@ -307,6 +327,59 @@ All the same bug: an app guessing "am I being closed?" from an unmount, or payin
(`frontend.tsx:15-21`) with no per-route scoping, and `reset` is called nowhere.
Authority: `docs/navigation-audit.md`.
### 5.9 The context has grown an app-config section — *(found 2026-08-08)*
`WorkspaceContext` is 18 fields, of which the framework itself reads none of the first six. Apps never
touch the framework half, so the abstraction holds in one direction; the leak is entirely outbound.
- [ ] **Delete `initialFilePath` and `defaultFileSort`.** Declared `WorkspaceContext.ts:14-15`, plumbed
through `WorkspaceView.tsx:19-20,30`, read only by
`apps/FileBrowser/FileBrowserApp/FileBrowserPanelWrapper.tsx:7,9,10` — and **set by zero callers**.
`DefaultFileSort` (`{field: 'name'|'size'|'type'|'date'}`) is file-browser vocabulary living in the
framework's type file, and it is re-exported from the barrel (`Workspace/index.ts:27`). Pure leak,
no payoff, entirely deletable.
- [ ] **Move `promptPrefix` onto the component, not the context.** `WorkspaceContext.ts:16` →
`Chat/ChatPanelWrapper.tsx:78` → `useEmbeddableChat.ts:107`. Set by `EmailScreen.tsx:53` and
`BrowserScreen.tsx:35`, each a screen-local ~40-word system prompt. The framework is a courier for
a string only one app understands, and the `components` prop already exists for exactly this —
Email can supply a pre-configured chat by panel id.
- [ ] **`dashboardId` is a bag whose *format* three apps parse.** It is literally `workspace.key`
(`WorkspaceView.tsx:163`). Consumers reverse-engineer meaning from its shape:
`Chat/ChatPanelWrapper.tsx:51-56` does `dashboardId === 'email' || dashboardId === 'screens/email'`
→ email context, and `!startsWith('screens/')` → dashboard context — **so renaming a screen key
silently changes the agent's system context**. `TerminalWrapper.tsx:13-16` regexes it;
`HostTerminalWrapper.tsx:12` doesn't (§2). Give the context the parsed facts
(`{ kind: 'screen'|'dashboard', id }`) instead of the raw key, and the three parsers collapse.
- [ ] **`WorkspaceLayout.tsx:36` silently omits `root`, `initialFilePath`, `defaultFileSort`** — apps
inside a `DashboardPreview` fall through to the `createContext` defaults. Whatever survives the
three items above should be constructed in one place, not twice by hand.
### 5.10 Channel hygiene — *(found 2026-08-08)*
Cheap to fix, and prerequisites for the 5.8 migration rather than alternatives to it.
- [ ] **Four channels are bare string literals with no constant.** `'files:refresh-signal'` (repeated in
4 files), `'chat:selected-session'` (3 files — plus a module-private `CHANNEL` const in a 4th place
that only one of them uses), `'chat:active-session'`, `'preview:refresh'`. A typo silently yields a
fresh channel pinned to `initialData`; nothing errors. Export a constant per channel, next to its
payload type.
- [ ] **Payload types are per-call-site, not per-channel.** `usePanelChannel<T>` takes `T` from each
caller, so a publisher and a subscriber can disagree and nothing checks.
`'files:refresh-signal'` is `number` in all four places by convention only. A
`defineChannel<T>(name)` helper returning a typed hook would fix both this and the item above.
- [ ] **Two write idioms disagree on the same channel.** `files:refresh-signal` is bumped with
`Date.now()` at the Chat sites and `setRefreshSignal((n) => n + 1)` at the FileViewer sites — and
`useGlobal`'s functional form applies against the **render-time** snapshot (`useGlobal.ts:18`), so
two increments in one render window collapse into one. Standardise on the nonce.
(`useLyricsOpen.ts:19-23` already documents avoiding the functional form for this reason.)
- [ ] **`system-settings:run-command` has no writer.** `run-command-channel.ts:6`; the only two writes
(`SystemSettings.tsx:87,133`) are both *clears*, and the sibling ServerSettings sections never
import it. The panel it drives — a terminal that opens with a command pre-loaded — appears
unreachable. Wire it or delete it; add to §8 either way.
- [ ] **`PanelComponentEntry.component` is typed with no props** (`types.ts:60-62`) but
`PanelSlot.tsx:521` passes `panelId` at runtime. `components`-supplied panels get a prop they
cannot see; registry apps get the honest `{ panelId: string }`. One-line type fix.
---
## 6. Decisions needed — not defects, don't guess