Skip to content

state: split dashboardLayoutSlice into reducer/model modules; saves carry their fetch baseline - #149

Draft
kai-openswarm wants to merge 6 commits into
openswarm-ai:devfrom
kai-openswarm:c11/layout-slice
Draft

state: split dashboardLayoutSlice into reducer/model modules; saves carry their fetch baseline#149
kai-openswarm wants to merge 6 commits into
openswarm-ai:devfrom
kai-openswarm:c11/layout-slice

Conversation

@kai-openswarm

@kai-openswarm kai-openswarm commented Aug 18, 2026

Copy link
Copy Markdown

Draft, stacked on #147 (its import-safe config.ts/safeMode.ts are what let the reducer tests run under node:test; the extra commits in this diff disappear once #147 merges). This is one of three god-file splits (this, #150 AgentChat, #151 electron/main.js). Because you commit to these files weekly, I'd like a short freeze on dashboardLayoutSlice.ts before this leaves draft: you stop touching that file, I rebase and mark ready within the hour, you review/merge, freeze lifts. Say which order suits you; my default is this one first, then AgentChat, then electron.

dashboardLayoutSlice.ts was 2,145 lines. This moves its reducers and helpers into single-purpose modules next to it and shrinks the slice to the store wiring, the fetch/save thunks and the extraReducers (568 lines). Every exported action name and the persisted layout shape are unchanged.

Module What moved there
dashboardLayoutModel.ts types, defaults, initial state
dashboardLayoutGeometry.ts grid/collision/placement helpers
dashboardLayoutCardState.ts one accessor registry for every CardType; z-order ledger rekey/remove/reconcile
dashboardLayout{Agent,View,Browser,Workflow,Window,Canvas,Closed,Glow,Reset}Reducers.ts the reducers, by card family

Behaviour changes, each with tests (node:test, 24 cases in dashboardLayout{Slice,SaveAuthority,FetchAuthority,CardState}.test.ts):

  • Save authority. saveLayout now takes the per-dashboard baseline object the last successful fetch created (saveAuthority) and refuses a payload whose baseline is not the current one, or while a refetch for that dashboard is pending. A delayed or unmount save captured before a rejected/superseded fetch can no longer erase the server's layout. useLayoutSave and the header's Share flush pass it from a selector.
  • Fetch authority. Only the latest request generation may fulfill for the active dashboard; a late fetch cannot replace the active dashboard or be saved under its id, and a late rejection cannot revoke a newer baseline.
  • Unknown persisted layout fields are read on fetch and written back on save instead of being dropped, so a newer build's fields survive a save from an older one.
  • z-order ledger is reconciled after a wholesale layout replace.

Also: the geometry helpers read the viewport through viewportSize() (same fallbacks the callers used, 1440×900, when there is no window), so the reducer tests run under node:test. AppShell/canvas behaviour is untouched.

Proof: tsc --noEmit clean; node scripts/run-tests.mjs 167/167; packaged macOS app: upstream smoke 5/5 + the characterization suite (#146) 19/19.

Not in this PR (separate decisions): a compositions/experts card family, the dashboard header a11y refactor.

… a window, so reducer tests run under node:test

Both modules touched window at import time (port/host derivation, the fetch
interceptor install, the debug handle), so any node:test file that imports a
reducer importing API_BASE died with 'window is not defined' before its first
assertion; fetchSessionsStrip.test.ts has been red that way since the
resilience work landed, unnoticed because nothing runs these tests in CI. In a
renderer (window present) nothing changes: same port/host, same interceptor,
same handle. Without one the module answers with the defaults and installs
nothing.
Nothing ran any of them in CI: the 235-file backend pytest suite, the 22
renderer node:test files and the edge suite were run by hand, one file at a
time, so a regression only surfaced when someone happened to run the right
one. Three small workflows, hosted ubuntu, path-filtered, read-only token:

- backend-tests: pytest on Python 3.13 from the locked requirements, plus a
  completion assertion (junit testcase count == collect-only count) so a test
  process that dies mid-run can never read as green
- frontend-tests: tsc --noEmit + node:test via tsx over src/**/*.test.ts(x),
  through frontend/scripts/run-tests.mjs (the runner the tests already name)
- edge-tests: pytest for openswarm-edge

All three are green on the current tree: 2951 backend tests, 143 frontend
tests across 22 files, 14 edge tests.
Two of five hosted runs of the backend suite stalled at 99% until the job cap
with no summary and no junit: one test blocked forever on a bare
ws.receive_json() (fixed on its own in a separate change). A CI lane should
never depend on every test being unable to hang, so add pytest-timeout to the
dev requirements and run the suite with --timeout=300. On Linux the default
signal method fails just the offending test and the run continues, so the
report and the "every collected test ran" assertion stay meaningful.
test_ws_endpoint_streams_a_full_turn_end_to_end read the socket with a bare
ws.receive_json() in a 40-iteration loop and broke only on the assistant reply.
When the loop ends early for any reason (fewer than 40 events, no reply), the
next receive blocks forever and the whole pytest run stalls at 99% until the
job cap. On hosted runners it does exactly that intermittently, on Linux and
Windows alike: the turn path's configure_provider_env decides whether 9Router
needs reviving from provider evidence earlier tests may leave behind, and that
revival spawns/installs the router behind a module-level asyncio.Lock; the
background turn-label aux call reaches the same machinery. Neither is part of
this test's contract ("SDK and WS auth mocked, everything else real").

Pin both out with monkeypatch, bound every receive at 5s (a regression now
fails this test instead of hanging the runner), and wait for the turn's
completed status before asserting on session.messages so the assertion cannot
race the loop's tail.
Same class as the config/backendConnection change: safeMode.ts read `window`
at import, and dashboardLayoutSlice imports it, so any reducer test that
imports the slice died under node:test before it ran. Guard the read; in a
renderer nothing changes.
…arry their fetch baseline

dashboardLayoutSlice.ts was 2,145 lines. This moves its reducers and helpers
into single-purpose modules next to it, keeps every exported action name and
the persisted layout shape, and shrinks the slice to the store wiring, the
fetch/save thunks and the extraReducers (568 lines):

  dashboardLayoutModel.ts        types, defaults, initial state
  dashboardLayoutGeometry.ts     grid/collision/placement helpers
  dashboardLayoutCardState.ts    one accessor registry for every CardType,
                                 z-order ledger rekey/remove/reconcile
  dashboardLayout{Agent,View,Browser,Workflow,Window,Canvas,Closed,Glow,
                  Reset}Reducers.ts

Behaviour changes, each with tests (node:test, 24 cases):

- Save authority. saveLayout now takes the per-dashboard baseline object the
  last successful fetch created (`saveAuthority`) and refuses a payload whose
  baseline is not the current one, or while a refetch for that dashboard is
  pending. A delayed or unmount save captured before a rejected/superseded
  fetch can no longer erase the server's layout. useLayoutSave and
  DashboardHeader's Share flush pass it from a selector.
- Fetch authority. Only the latest request generation may fulfill for the
  active dashboard; a late fetch cannot replace the active dashboard or be
  saved under its id, and a late rejection cannot revoke a newer baseline.
- Unknown persisted layout fields are read on fetch and written back on save
  instead of being dropped, so a newer build's fields survive an older one.
- z-order ledger is reconciled after a wholesale layout replace.

The geometry helpers read the viewport through viewportSize(), which falls
back exactly as the callers did (1440x900) when there is no window, so the
reducer tests run under node:test.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant