diff --git a/frontend/src/app/pages/Dashboard/controls/CardSearchPalette.tsx b/frontend/src/app/pages/Dashboard/controls/CardSearchPalette.tsx index 19dbfd97..4873b677 100644 --- a/frontend/src/app/pages/Dashboard/controls/CardSearchPalette.tsx +++ b/frontend/src/app/pages/Dashboard/controls/CardSearchPalette.tsx @@ -37,6 +37,8 @@ const CardSearchPalette: React.FC = ({ const items = useMemo((): CardSearchItem[] => { const result: CardSearchItem[] = []; for (const card of Object.values(cards)) { + // A layout entry without a session cannot be searched or focused; one such entry took the whole palette down on every render. + if (typeof card.session_id !== 'string') continue; const session = sessions[card.session_id]; result.push({ id: card.session_id, diff --git a/frontend/src/shared/state/dashboardLayoutSlice.ts b/frontend/src/shared/state/dashboardLayoutSlice.ts index bbc41acf..7addc518 100644 --- a/frontend/src/shared/state/dashboardLayoutSlice.ts +++ b/frontend/src/shared/state/dashboardLayoutSlice.ts @@ -806,6 +806,8 @@ const dashboardLayoutSlice = createSlice({ }> ) { const { sessionId, x, y, width, height, expandedSessionIds, exact } = action.payload; + // A card keyed "undefined" persists, survives every reload, and threw inside the search palette on each render (a probe once dispatched the wrong key); the bad state is unwritable here. + if (typeof sessionId !== 'string' || !sessionId) return; const pos = exact ? { x, y } : findOpenSpotNear(x, y, collectOccupiedRects(state, expandedSessionIds), width, height); state.cards[sessionId] = { session_id: sessionId, diff --git a/frontend/src/shared/state/placeCardRefusesNoSession.test.ts b/frontend/src/shared/state/placeCardRefusesNoSession.test.ts new file mode 100644 index 00000000..1636862d --- /dev/null +++ b/frontend/src/shared/state/placeCardRefusesNoSession.test.ts @@ -0,0 +1,17 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import dashboardLayoutReducer, { placeCard } from './dashboardLayoutSlice'; + +// A drill dispatched placeCard without a sessionId and the reducer wrote cards["undefined"], which persisted and made the +// search palette throw on every render until the file was edited by hand. The bad state is unrepresentable now. +test('placeCard without a session id writes nothing', () => { + const before = dashboardLayoutReducer(undefined, { type: 'init' }); + const after = dashboardLayoutReducer(before, placeCard({ x: 1, y: 2, width: 100, height: 100 } as unknown as Parameters[0])); + assert.deepEqual(Object.keys(after.cards), Object.keys(before.cards)); + assert.ok(!('undefined' in after.cards)); +}); + +test('placeCard with a session id still places', () => { + const after = dashboardLayoutReducer(undefined, placeCard({ sessionId: 's1', x: 1, y: 2, width: 100, height: 100, exact: true })); + assert.equal(after.cards.s1.x, 1); +});