stop the dashboard PATCH dispatcher losing writes
Four defects, one shape: a write that returns 200 and lands nowhere.
- Unknown keys were dropped by a chain of `if (match) continue` with no `else`. The three prefixes
CommandTerminalWrapper actually writes — tmux, nvim, claude-code — were among them, so those panel
maps lived in the React Query cache only: every reload minted a fresh uuid and abandoned a running
pty. They now live in a `panel_state` bag on the dashboard row, and an unmatched key 400s.
- `ws-terminals-{id}: null` fell through to an upsert, writing NULL into a NOT NULL column on a live
dashboard and re-INSERTing a deleted one. Renaming a dashboard sends exactly that, paired with
`ws-layout-{id}: null`, so the old slug came back as a zombie row in the dashboards list.
- HostTerminalWrapper and CommandTerminalWrapper built their state key straight from `dashboardId`,
which is a workspace *key* (`ws-layout-<id>`), while TerminalWrapper stripped the prefix. The server
read the un-stripped form back as a dashboard id and created it. One rule now, in state-key.ts.
Verified against the live server: unknown key 400s, the three prefixes round-trip, a null on a live
dashboard is a no-op, and the rename sequence leaves no zombie.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import { useDashboardState } from 'state/useDashboardState';
|
||||
import { useGlobal } from 'hooks/useGlobal';
|
||||
import type { TerminalConnectionState } from './Terminal';
|
||||
import { TerminalView } from './Terminal';
|
||||
import { terminalStateKey } from './state-key';
|
||||
|
||||
const EMPTY_TERMINALS: Record<string, string> = {};
|
||||
|
||||
@@ -15,7 +16,7 @@ type CommandTerminalWrapperProps = {
|
||||
|
||||
export const CommandTerminalWrapper = ({ panelId, command, statePrefix }: CommandTerminalWrapperProps) => {
|
||||
const { dashboardId, cwd } = useWorkspace();
|
||||
const stateKey = dashboardId ? `ws-${statePrefix}-${dashboardId}` : `ws-${statePrefix}-default`;
|
||||
const stateKey = terminalStateKey(statePrefix, dashboardId);
|
||||
const { value: terminals, setValue: setTerminals } = useDashboardState<Record<string, string>>(
|
||||
stateKey,
|
||||
EMPTY_TERMINALS,
|
||||
|
||||
@@ -4,12 +4,13 @@ import { useDashboardState } from 'state/useDashboardState';
|
||||
import { useGlobal } from 'hooks/useGlobal';
|
||||
import type { TerminalConnectionState } from './Terminal';
|
||||
import { TerminalView } from './Terminal';
|
||||
import { terminalStateKey } from './state-key';
|
||||
|
||||
const EMPTY_TERMINALS: Record<string, string> = {};
|
||||
|
||||
export const HostTerminalWrapper = ({ panelId }: { panelId: string }) => {
|
||||
const { dashboardId, cwd } = useWorkspace();
|
||||
const stateKey = dashboardId ? `ws-host-terminals-${dashboardId}` : 'ws-host-terminals-default';
|
||||
const stateKey = terminalStateKey('host-terminals', dashboardId);
|
||||
const { value: terminals, setValue: setTerminals } = useDashboardState<Record<string, string>>(
|
||||
stateKey,
|
||||
EMPTY_TERMINALS,
|
||||
|
||||
@@ -4,16 +4,13 @@ import { useDashboardState } from 'state/useDashboardState';
|
||||
import { useGlobal } from 'hooks/useGlobal';
|
||||
import type { TerminalConnectionState } from './Terminal';
|
||||
import { TerminalView } from './Terminal';
|
||||
import { terminalStateKey } from './state-key';
|
||||
|
||||
const EMPTY_TERMINALS: Record<string, string> = {};
|
||||
|
||||
export const TerminalWrapper = ({ panelId }: { panelId: string }) => {
|
||||
const { dashboardId, cwd } = useWorkspace();
|
||||
const stateKey = (() => {
|
||||
const wsMatch = dashboardId?.match(/^ws-layout-(.+)$/);
|
||||
if (wsMatch) return `ws-terminals-${wsMatch[1]}`;
|
||||
return 'ws-terminals-default';
|
||||
})();
|
||||
const stateKey = terminalStateKey('terminals', dashboardId);
|
||||
const { value: terminals, setValue: setTerminals } = useDashboardState<Record<string, string>>(
|
||||
stateKey,
|
||||
EMPTY_TERMINALS,
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* The dashboard-state key a terminal panel stores its `panelId → sessionId` map under.
|
||||
*
|
||||
* `useWorkspace().dashboardId` is the workspace *key*, not an id — `ws-layout-<id>` for a dashboard and
|
||||
* `screens/<name>` for a screen. Three wrappers derived a key from it by three different rules, and two of
|
||||
* them left the prefix on: `ws-host-terminals-ws-layout-my-dash` parses back out as a dashboard called
|
||||
* `ws-layout-my-dash`, which the server then created, and which showed up in the Dashboards list as a real
|
||||
* dashboard. One rule, in one place, is the fix for that class.
|
||||
*
|
||||
* Screens fall back to `-default`, which is the behaviour `TerminalWrapper` already had: a screen has no
|
||||
* dashboard row to hang the map on, and panel ids are unique across the app.
|
||||
*/
|
||||
export function terminalStateKey(prefix: string, dashboardId: string | null): string {
|
||||
const id = dashboardId?.match(/^ws-layout-(.+)$/)?.[1];
|
||||
return `ws-${prefix}-${id ?? 'default'}`;
|
||||
}
|
||||
Reference in New Issue
Block a user