Implemented.
Browser-local layout storage is not enough for a control room that may run in several browser windows or on several monitors. Each window needs an independent layout when desired, and an intentional shared layout when the same URL is opened elsewhere.
Introduce server-backed workspace views.
A workspace view is a persisted layout document:
Each browser window opens one workspace view at a time. The view identity lives
in the URL as ?workspace=<id>.
That gives TermAl two important behaviors:
This is not collaborative layout editing. In Phase 1, a workspace view is single-writer in practice, and if two browsers open the same view at once, last write wins.
When the user opens TermAl without a workspace query parameter:
?workspace=<generated-id>That means a fresh browser window naturally gets its own layout instead of fighting over a shared browser-global key.
If the URL already contains ?workspace=review-monitor, the frontend loads and
persists the layout under that ID.
This supports:
?workspace=planner?workspace=reviewThe browser still keeps a per-workspace local cache as a warm-start fallback, but the server is the source of truth. Same-tab workspace switches should flush any pending debounced save before navigation so the backend copy stays current.
Workspace views live in the main persisted backend state alongside projects, sessions, and orchestration instances.
struct WorkspaceLayoutDocument {
id: String,
revision: u64,
updated_at: String,
control_panel_side: WorkspaceControlPanelSide,
workspace: serde_json::Value,
}
enum WorkspaceControlPanelSide {
Left,
Right,
}
And in the persisted state:
struct StateInner {
// existing fields...
workspace_layouts: BTreeMap<String, WorkspaceLayoutDocument>,
}
The backend treats the nested workspace payload as an opaque JSON document.
The frontend remains responsible for schema validation.
The shipped Phase 1 API includes a list route for the workspace switcher in addition to direct get/put by ID.
GET /api/workspaces
GET /api/workspaces/{id}
PUT /api/workspaces/{id}
DELETE /api/workspaces/{id}
/api/workspaces/{id}Returns:
{
"layout": {
"id": "planner-monitor",
"revision": 3,
"updatedAt": "2026-03-28 10:24:11",
"controlPanelSide": "left",
"lightThemeId": "warm-light",
"darkThemeId": "dark",
"themeMode": "auto",
"workspace": { "...": "workspace document" }
}
}
If the workspace view does not exist, return 404.
/api/workspacesReturns a summary list ordered by most recent update. The frontend uses this to populate the workspace switcher and to reopen saved browser layouts.
/api/workspaces/{id}Request:
{
"controlPanelSide": "left",
"lightThemeId": "warm-light",
"darkThemeId": "dark",
"themeMode": "auto",
"workspace": { "...": "workspace document" }
}
Behavior:
revisionrevision/api/state snapshot so other connected switchers see the updated summariesPhase 1 intentionally keeps the rule simple:
This is acceptable because the main use case is one workspace view per monitor.
Future improvements can add optimistic concurrency or live layout events, but that should not block the first useful version.
The frontend boot order becomes:
Workspace persistence is debounced slightly so split dragging and canvas
dragging do not write on every pointer move. Pending saves are flushed with
keepalive on pagehide/unload paths where possible.
The existing frontend workspace validation remains the gatekeeper:
Those can come later once the basic multi-browser workflow is solid.
workspace_layouts to the persisted backend state.GET /api/workspaces/{id}, PUT /api/workspaces/{id}, and DELETE /api/workspaces/{id}.?workspace=<id> in the frontend.localStorage key to:
?workspace=<id> URL in another browser restores the
same workspace view./api/state snapshots so other browser switchers
stay in sync.