1use crate::backend::obj::*;
10use crate::backend::service::{self, WebCallType, WebReturnType};
11
12use super::super::AppState;
13use super::layout_helpers::{queue_source_layout_delete, setup_torn_off_block_layout};
14use super::reducer_helpers::{dispatch_to_reducer, publish_events};
15
16pub(crate) async fn handle_move_block_to_tab(state: &AppState, call: &WebCallType) -> WebReturnType {
22 let store = &state.mstore;
23 let args = &call.args;
24 let ws_id: String = match service::get_arg(args, 0) {
25 Ok(v) => v,
26 Err(e) => return WebReturnType::error(e),
27 };
28 let block_id: String = match service::get_arg(args, 1) {
29 Ok(v) => v,
30 Err(e) => return WebReturnType::error(e),
31 };
32 let source_tab_id: String = match service::get_arg(args, 2) {
33 Ok(v) => v,
34 Err(e) => return WebReturnType::error(e),
35 };
36 let dest_tab_id: String = match service::get_arg(args, 3) {
37 Ok(v) => v,
38 Err(e) => return WebReturnType::error(e),
39 };
40 let auto_close: bool = service::get_arg(args, 4).unwrap_or(true);
41 tracing::info!(ws_id = %ws_id, block_id = %block_id, source_tab = %source_tab_id, dest_tab = %dest_tab_id, "[dnd:svc] MoveBlockToTab via reducer");
42 if source_tab_id == dest_tab_id {
51 return WebReturnType::success_empty();
52 }
53 let events = dispatch_to_reducer(
59 state,
60 agentmux_common::ipc::Command::MoveBlock {
61 block_id: block_id.clone(),
62 src_tab_id: source_tab_id.clone(),
63 dst_tab_id: dest_tab_id.clone(),
64 dst_index: u32::MAX,
65 },
66 )
67 .await;
68 if let Some(err_msg) = events.iter().find_map(|e| match e {
69 agentmux_common::ipc::Event::Error { message, .. } => Some(message.clone()),
70 _ => None,
71 }) {
72 return WebReturnType::error(err_msg);
73 }
74 for ev in &events {
75 if let Err(e) = crate::persist_subscriber::apply_event_to_mstore(ev, store) {
76 return WebReturnType::error(format!(
77 "MoveBlockToTab: SQLite write failed: {}",
78 e
79 ));
80 }
81 }
82 publish_events(state, &events);
83 if auto_close {
85 let should_close = match store.must_get::<Tab>(&source_tab_id) {
86 Ok(t) => t.blockids.is_empty(),
87 Err(_) => false,
88 };
89 if should_close {
90 let total_tabs = match store.must_get::<Workspace>(&ws_id) {
91 Ok(ws) => ws.tabids.len() + ws.pinnedtabids.len(),
92 Err(_) => 0,
93 };
94 if total_tabs > 1 {
95 let close_events = dispatch_to_reducer(
96 state,
97 agentmux_common::ipc::Command::DeleteTab {
98 workspace_id: ws_id.clone(),
99 tab_id: source_tab_id.clone(),
100 force: false,
105 },
106 )
107 .await;
108 for ev in &close_events {
109 let _ = crate::persist_subscriber::apply_event_to_mstore(ev, store);
110 }
111 publish_events(state, &close_events);
112 }
113 }
114 }
115 let mut updates = vec![];
116 if let Ok(src) = store.must_get::<Tab>(&source_tab_id) {
117 updates.push(MuxObjUpdate {
118 updatetype: "update".into(),
119 otype: OTYPE_TAB.to_string(),
120 oid: source_tab_id.clone(),
121 obj: Some(mux_obj_to_value(&src)),
122 });
123 }
124 if let Ok(dst) = store.must_get::<Tab>(&dest_tab_id) {
125 updates.push(MuxObjUpdate {
126 updatetype: "update".into(),
127 otype: OTYPE_TAB.to_string(),
128 oid: dest_tab_id.clone(),
129 obj: Some(mux_obj_to_value(&dst)),
130 });
131 }
132 if let Ok(ws) = store.must_get::<Workspace>(&ws_id) {
133 updates.push(MuxObjUpdate {
134 updatetype: "update".into(),
135 otype: OTYPE_WORKSPACE.to_string(),
136 oid: ws_id.clone(),
137 obj: Some(mux_obj_to_value(&ws)),
138 });
139 }
140 WebReturnType::success_with_updates(updates)
141}
142
143pub(crate) async fn handle_promote_block_to_tab(state: &AppState, call: &WebCallType) -> WebReturnType {
148 let store = &state.mstore;
149 let args = &call.args;
150 let ws_id: String = match service::get_arg(args, 0) {
151 Ok(v) => v,
152 Err(e) => return WebReturnType::error(e),
153 };
154 let block_id: String = match service::get_arg(args, 1) {
155 Ok(v) => v,
156 Err(e) => return WebReturnType::error(e),
157 };
158 let source_tab_id: String = match service::get_arg(args, 2) {
159 Ok(v) => v,
160 Err(e) => return WebReturnType::error(e),
161 };
162 let auto_close: bool = service::get_arg(args, 3).unwrap_or(true);
163 tracing::info!(ws_id = %ws_id, block_id = %block_id, source_tab = %source_tab_id, "[dnd:svc] PromoteBlockToTab via saga");
164 let saga_result = crate::sagas::promote_block_to_tab::run(
165 state,
166 block_id.clone(),
167 source_tab_id.clone(),
168 ws_id.clone(),
169 )
170 .await;
171 let new_tab_oid = match saga_result {
172 Ok(v) => v
173 .get("new_tab_id")
174 .and_then(|v| v.as_str())
175 .unwrap_or_default()
176 .to_string(),
177 Err(reason) => return WebReturnType::error(reason),
178 };
179
180 if let Err(e) = setup_torn_off_block_layout(state, &new_tab_oid, &block_id).await {
184 tracing::warn!(new_tab = %new_tab_oid, "PromoteBlockToTab: layout setup failed: {}", e);
185 }
186 if let Err(e) = queue_source_layout_delete(state, &source_tab_id, &block_id).await {
188 tracing::warn!(source_tab = %source_tab_id, "PromoteBlockToTab: source layout delete-action enqueue failed: {}", e);
189 }
190 let active_events = dispatch_to_reducer(
192 state,
193 agentmux_common::ipc::Command::SetActiveTab {
194 workspace_id: ws_id.clone(),
195 tab_id: new_tab_oid.clone(),
196 },
197 )
198 .await;
199 for ev in &active_events {
200 if let Err(e) = crate::persist_subscriber::apply_event_to_mstore(ev, store) {
201 tracing::warn!("PromoteBlockToTab: SetActiveTab apply failed: {}", e);
202 }
203 }
204 publish_events(state, &active_events);
205
206 if auto_close {
208 let should_close = match store.must_get::<Tab>(&source_tab_id) {
209 Ok(t) => t.blockids.is_empty(),
210 Err(_) => false,
211 };
212 if should_close {
213 let total_tabs = match store.must_get::<Workspace>(&ws_id) {
214 Ok(ws) => ws.tabids.len() + ws.pinnedtabids.len(),
215 Err(_) => 0,
216 };
217 if total_tabs > 1 {
218 let close_events = dispatch_to_reducer(
219 state,
220 agentmux_common::ipc::Command::DeleteTab {
221 workspace_id: ws_id.clone(),
222 tab_id: source_tab_id.clone(),
223 force: false,
228 },
229 )
230 .await;
231 for ev in &close_events {
232 let _ = crate::persist_subscriber::apply_event_to_mstore(ev, store);
233 }
234 publish_events(state, &close_events);
235 }
236 }
237 }
238
239 let mut updates = vec![];
240 if let Ok(new_tab) = store.must_get::<Tab>(&new_tab_oid) {
241 updates.push(MuxObjUpdate {
242 updatetype: "update".into(),
243 otype: OTYPE_TAB.to_string(),
244 oid: new_tab_oid.clone(),
245 obj: Some(mux_obj_to_value(&new_tab)),
246 });
247 }
248 if let Ok(src) = store.must_get::<Tab>(&source_tab_id) {
249 updates.push(MuxObjUpdate {
250 updatetype: "update".into(),
251 otype: OTYPE_TAB.to_string(),
252 oid: source_tab_id.clone(),
253 obj: Some(mux_obj_to_value(&src)),
254 });
255 }
256 if let Ok(ws) = store.must_get::<Workspace>(&ws_id) {
257 updates.push(MuxObjUpdate {
258 updatetype: "update".into(),
259 otype: OTYPE_WORKSPACE.to_string(),
260 oid: ws_id.clone(),
261 obj: Some(mux_obj_to_value(&ws)),
262 });
263 }
264 WebReturnType::success_data_updates(
265 serde_json::to_value(&new_tab_oid).unwrap_or_default(),
266 updates,
267 )
268}
269
270pub(crate) async fn handle_move_tab_to_workspace(state: &AppState, call: &WebCallType) -> WebReturnType {
277 let store = &state.mstore;
278 let args = &call.args;
279 let tab_id: String = match service::get_arg(args, 0) {
280 Ok(v) => v,
281 Err(e) => return WebReturnType::error(e),
282 };
283 let source_ws_id: String = match service::get_arg(args, 1) {
284 Ok(v) => v,
285 Err(e) => return WebReturnType::error(e),
286 };
287 let dest_ws_id: String = match service::get_arg(args, 2) {
288 Ok(v) => v,
289 Err(e) => return WebReturnType::error(e),
290 };
291 let insert_index: Option<u32> = service::get_arg::<usize>(args, 3)
292 .ok()
293 .map(|v| v.try_into().unwrap_or(u32::MAX));
294 tracing::info!(tab_id = %tab_id, source_ws = %source_ws_id, dest_ws = %dest_ws_id, insert_index = ?insert_index, "[dnd:svc] MoveTabToWorkspace via reducer");
295 if source_ws_id == dest_ws_id {
301 return WebReturnType::success_empty();
302 }
303 match store.get::<Workspace>(&source_ws_id) {
314 Ok(Some(src_ws)) => {
315 let total_tabs = src_ws.tabids.len() + src_ws.pinnedtabids.len();
316 if total_tabs <= 1 {
317 return WebReturnType::error(
318 "cannot move last tab out of workspace".to_string(),
319 );
320 }
321 }
322 Ok(None) => {
323 return WebReturnType::error(format!(
324 "MoveTabToWorkspace: source workspace not found: {}",
325 source_ws_id
326 ));
327 }
328 Err(e) => {
329 return WebReturnType::error(format!(
330 "MoveTabToWorkspace: workspace read failed: {}",
331 e
332 ));
333 }
334 }
335 let dst_index = insert_index.unwrap_or(u32::MAX);
336 let events = dispatch_to_reducer(
337 state,
338 agentmux_common::ipc::Command::MoveTab {
339 tab_id: tab_id.clone(),
340 src_workspace_id: source_ws_id.clone(),
341 dst_workspace_id: dest_ws_id.clone(),
342 dst_index,
343 },
344 )
345 .await;
346 if let Some(err_msg) = events.iter().find_map(|e| match e {
347 agentmux_common::ipc::Event::Error { message, .. } => Some(message.clone()),
348 _ => None,
349 }) {
350 return WebReturnType::error(err_msg);
351 }
352 for ev in &events {
353 if let Err(e) = crate::persist_subscriber::apply_event_to_mstore(ev, store) {
354 return WebReturnType::error(format!(
355 "MoveTabToWorkspace: SQLite write failed: {}",
356 e
357 ));
358 }
359 }
360 publish_events(state, &events);
361 let mut updates = Vec::new();
362 if let Ok(src_ws) = store.must_get::<Workspace>(&source_ws_id) {
363 updates.push(MuxObjUpdate {
364 updatetype: "update".into(),
365 otype: OTYPE_WORKSPACE.to_string(),
366 oid: source_ws_id.clone(),
367 obj: Some(mux_obj_to_value(&src_ws)),
368 });
369 }
370 if let Ok(dst_ws) = store.must_get::<Workspace>(&dest_ws_id) {
371 updates.push(MuxObjUpdate {
372 updatetype: "update".into(),
373 otype: OTYPE_WORKSPACE.to_string(),
374 oid: dest_ws_id.clone(),
375 obj: Some(mux_obj_to_value(&dst_ws)),
376 });
377 }
378 WebReturnType::success_with_updates(updates)
379}
380
381pub(crate) async fn handle_restore_torn_off_tab(state: &AppState, call: &WebCallType) -> WebReturnType {
387 let store = &state.mstore;
388 let args = &call.args;
389 let tab_id: String = match service::get_arg(args, 0) {
390 Ok(v) => v,
391 Err(e) => return WebReturnType::error(e),
392 };
393 let source_ws_id: String = match service::get_arg(args, 1) {
394 Ok(v) => v,
395 Err(e) => return WebReturnType::error(e),
396 };
397 let dest_ws_id: String = match service::get_arg(args, 2) {
398 Ok(v) => v,
399 Err(e) => return WebReturnType::error(e),
400 };
401 let insert_index: Option<u32> = service::get_arg::<usize>(args, 3)
402 .ok()
403 .map(|v| v.try_into().unwrap_or(u32::MAX));
404 tracing::info!(tab_id = %tab_id, source_ws = %source_ws_id, dest_ws = %dest_ws_id, insert_index = ?insert_index, "[dnd:svc] RestoreTornOffTab via saga");
405 let saga_result = crate::sagas::restore_torn_off_tab::run(
406 state,
407 tab_id,
408 source_ws_id.clone(),
409 dest_ws_id.clone(),
410 insert_index,
411 )
412 .await;
413 match saga_result {
414 Ok(_) => {
415 let mut updates = Vec::new();
416 match store.get::<Workspace>(&source_ws_id) {
417 Ok(Some(src_ws)) => {
418 updates.push(MuxObjUpdate {
419 updatetype: "update".into(),
420 otype: OTYPE_WORKSPACE.to_string(),
421 oid: source_ws_id.clone(),
422 obj: Some(mux_obj_to_value(&src_ws)),
423 });
424 }
425 Ok(None) => {
426 updates.push(MuxObjUpdate {
427 updatetype: "delete".into(),
428 otype: OTYPE_WORKSPACE.to_string(),
429 oid: source_ws_id.clone(),
430 obj: None,
431 });
432 }
433 Err(_) => {}
434 }
435 if let Ok(dst_ws) = store.must_get::<Workspace>(&dest_ws_id) {
436 updates.push(MuxObjUpdate {
437 updatetype: "update".into(),
438 otype: OTYPE_WORKSPACE.to_string(),
439 oid: dest_ws_id.clone(),
440 obj: Some(mux_obj_to_value(&dst_ws)),
441 });
442 }
443 WebReturnType::success_with_updates(updates)
444 }
445 Err(reason) => WebReturnType::error(reason),
446 }
447}