agentmux_srv/server/service/workspace.rs
1// Copyright 2025-2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4//! `workspace` service handler (workspace + tab lifecycle, drag-and-drop moves).
5//!
6//! The dispatcher below just routes each method to its handler; the
7//! handler bodies live in sibling modules:
8//! * [`super::workspace_lifecycle`] — `CreateWorkspace` / `GetWorkspace` /
9//! `DeleteWorkspace` / `UpdateWorkspace`.
10//! * [`super::tab_lifecycle`] — `CreateTab` / `SetActiveTab` / `CloseTab` /
11//! `UpdateTabIds` / `ReorderTab`.
12//! * [`super::tab_move`] — `MoveBlockToTab` / `PromoteBlockToTab` /
13//! `MoveTabToWorkspace` / `RestoreTornOffTab`.
14//! * [`super::tear_off`] — `TearOffBlock` / `RedockFloatingPane` /
15//! `TearOffTab`.
16
17use crate::backend::service::{WebCallType, WebReturnType};
18use crate::backend::wcore;
19
20use super::super::AppState;
21use super::tab_lifecycle::{
22 handle_close_tab, handle_create_tab, handle_reorder_tab, handle_set_active_tab,
23 handle_update_tab_ids,
24};
25use super::tab_move::{
26 handle_move_block_to_tab, handle_move_tab_to_workspace, handle_promote_block_to_tab,
27 handle_restore_torn_off_tab,
28};
29use super::tear_off::{handle_redock_floating_pane, handle_tear_off_block, handle_tear_off_tab};
30use super::workspace_lifecycle::{
31 handle_create_workspace, handle_delete_workspace, handle_get_workspace,
32 handle_update_workspace,
33};
34
35pub(super) async fn handle_workspace_service(state: &AppState, call: &WebCallType) -> WebReturnType {
36 let store = &state.mstore;
37 // Phase E.2c.2 — workspace lifecycle dispatches through the
38 // srv reducer for event emission (sagas / renderer / persist
39 // subscriber consume them) AND synchronously applies the
40 // emitted events to SQLite via the subscriber's apply path.
41 // Synchronous SQLite writes are required during the migration
42 // window because tab/block RPC still hits wcore directly and
43 // expects workspaces to be present in SQLite by the time the
44 // RPC reply returns (e.g., a CreateTab call right after
45 // CreateWorkspace would 404 on the workspace lookup if we
46 // only relied on the async subscriber). The subscriber later
47 // receives the same event on the broadcast bus and re-applies
48 // idempotently — safe because each apply arm checks SQLite
49 // state before writing. (Both reagent + codex flagged this
50 // race as P1 #615.)
51 //
52 // Reads (`GetWorkspace` / `ListWorkspaces`) stay on mstore
53 // until the tab + block RPC layers also migrate (E.2c.3 +
54 // E.2c.4). The reducer's `WorkspaceRecord` doesn't track
55 // `pinnedtabids` and its `tabids` / `activetabid` go stale
56 // immediately after any wcore-direct tab op — reading from
57 // it before tabs are migrated returns wrong data.
58 match call.method.as_str() {
59 "CreateWorkspace" => handle_create_workspace(state, call).await,
60 "GetWorkspace" => handle_get_workspace(state, call).await,
61 "DeleteWorkspace" => handle_delete_workspace(state, call).await,
62 "ListWorkspaces" => match wcore::list_workspaces(store) {
63 Ok(list) => WebReturnType::success(serde_json::to_value(&list).unwrap_or_default()),
64 Err(e) => WebReturnType::error(e.to_string()),
65 },
66 "CreateTab" => handle_create_tab(state, call).await,
67 "SetActiveTab" => handle_set_active_tab(state, call).await,
68 "CloseTab" => handle_close_tab(state, call).await,
69 "UpdateWorkspace" => handle_update_workspace(state, call).await,
70 "UpdateTabIds" => handle_update_tab_ids(state, call).await,
71 "MoveBlockToTab" => handle_move_block_to_tab(state, call).await,
72 "PromoteBlockToTab" => handle_promote_block_to_tab(state, call).await,
73 "ReorderTab" => handle_reorder_tab(state, call).await,
74 "MoveTabToWorkspace" => handle_move_tab_to_workspace(state, call).await,
75 "RestoreTornOffTab" => handle_restore_torn_off_tab(state, call).await,
76 "TearOffBlock" => handle_tear_off_block(state, call).await,
77 "RedockFloatingPane" => handle_redock_floating_pane(state, call).await,
78 "TearOffTab" => handle_tear_off_tab(state, call).await,
79 _ => WebReturnType::error(format!("unknown workspace method: {}", call.method)),
80 }
81}