agentmux_srv/server/service/
tab_lifecycle.rs1use crate::backend::obj::*;
9use crate::backend::service::{self, CloseTabRtnType, WebCallType, WebReturnType};
10
11use super::super::AppState;
12use super::reducer_helpers::{compensate_via_reducer, dispatch_to_reducer, publish_events};
13
14pub(crate) async fn handle_create_tab(state: &AppState, call: &WebCallType) -> WebReturnType {
21 let store = &state.mstore;
22 let args = &call.args;
23 let ws_id: String = match service::get_arg(args, 0) {
24 Ok(v) => v,
25 Err(e) => return WebReturnType::error(e),
26 };
27 let tab_name: String = service::get_arg(args, 1).unwrap_or_default();
28 let activate: bool = service::get_arg(args, 2).unwrap_or(true);
29 let resolved_name = if tab_name.is_empty() {
37 match store.get::<Workspace>(&ws_id) {
38 Ok(Some(ws)) => {
39 format!("Tab {}", ws.tabids.len() + ws.pinnedtabids.len() + 1)
40 }
41 _ => "Tab 1".to_string(),
42 }
43 } else {
44 tab_name.clone()
45 };
46 let events = dispatch_to_reducer(
47 state,
48 agentmux_common::ipc::Command::CreateTab {
49 workspace_id: ws_id.clone(),
50 name: resolved_name,
51 },
52 )
53 .await;
54 if let Some(err_msg) = events.iter().find_map(|e| match e {
61 agentmux_common::ipc::Event::Error { message, .. } => Some(message.clone()),
62 _ => None,
63 }) {
64 return WebReturnType::error(err_msg);
65 }
66 let tab_id = events.iter().find_map(|e| match e {
67 agentmux_common::ipc::Event::TabCreated { tab_id, .. } => Some(tab_id.clone()),
68 _ => None,
69 });
70 let mut apply_err: Option<String> = None;
73 for ev in &events {
74 if let Err(e) = crate::persist_subscriber::apply_event_to_mstore(ev, store) {
75 apply_err = Some(e.to_string());
76 break;
77 }
78 }
79 if let Some(err) = apply_err {
80 if let Some(tid) = tab_id.as_ref() {
81 compensate_via_reducer(
82 state,
83 agentmux_common::ipc::Command::DeleteTab {
84 workspace_id: ws_id.clone(),
85 tab_id: tid.clone(),
86 force: true,
91 },
92 store,
93 )
94 .await;
95 }
96 return WebReturnType::error(format!("CreateTab: SQLite write failed: {}", err));
97 }
98 publish_events(state, &events);
99 let auto_activated = events
102 .iter()
103 .any(|e| matches!(e, agentmux_common::ipc::Event::ActiveTabChanged { .. }));
104 if activate && !auto_activated {
105 if let Some(tid) = tab_id.as_ref() {
106 let active_events = dispatch_to_reducer(
107 state,
108 agentmux_common::ipc::Command::SetActiveTab {
109 workspace_id: ws_id.clone(),
110 tab_id: tid.clone(),
111 },
112 )
113 .await;
114 let mut active_err: Option<String> = None;
115 for ev in &active_events {
116 if let Err(e) =
117 crate::persist_subscriber::apply_event_to_mstore(ev, store)
118 {
119 active_err = Some(e.to_string());
120 break;
121 }
122 }
123 if active_err.is_none() {
124 publish_events(state, &active_events);
125 }
126 if let Some(err) = active_err {
130 tracing::warn!(
131 "CreateTab: post-create activate failed: {}",
132 err
133 );
134 }
135 }
136 }
137 match tab_id {
138 Some(id) => {
139 let mut updates = vec![];
140 if let Ok(tab) = store.must_get::<Tab>(&id) {
141 updates.push(MuxObjUpdate {
142 updatetype: "update".into(),
143 otype: OTYPE_TAB.to_string(),
144 oid: id.clone(),
145 obj: Some(mux_obj_to_value(&tab)),
146 });
147 }
148 if let Ok(ws) = store.must_get::<Workspace>(&ws_id) {
149 updates.push(MuxObjUpdate {
150 updatetype: "update".into(),
151 otype: OTYPE_WORKSPACE.to_string(),
152 oid: ws_id.clone(),
153 obj: Some(mux_obj_to_value(&ws)),
154 });
155 }
156 WebReturnType::success_data_updates(
157 serde_json::to_value(&id).unwrap_or_default(),
158 updates,
159 )
160 }
161 None => WebReturnType::error(
162 "CreateTab: reducer did not emit TabCreated".to_string(),
163 ),
164 }
165}
166
167pub(crate) async fn handle_set_active_tab(state: &AppState, call: &WebCallType) -> WebReturnType {
172 let store = &state.mstore;
173 let args = &call.args;
174 let ws_id: String = match service::get_arg(args, 0) {
175 Ok(v) => v,
176 Err(e) => return WebReturnType::error(e),
177 };
178 let tab_id: String = match service::get_arg(args, 1) {
179 Ok(v) => v,
180 Err(e) => return WebReturnType::error(e),
181 };
182 let events = dispatch_to_reducer(
188 state,
189 agentmux_common::ipc::Command::SetActiveTab {
190 workspace_id: ws_id.clone(),
191 tab_id: tab_id.clone(),
192 },
193 )
194 .await;
195 if let Some(err_msg) = events.iter().find_map(|e| match e {
198 agentmux_common::ipc::Event::Error { message, .. } => Some(message.clone()),
199 _ => None,
200 }) {
201 return WebReturnType::error(err_msg);
202 }
203 let mut apply_err: Option<String> = None;
210 for ev in &events {
211 if let Err(e) = crate::persist_subscriber::apply_event_to_mstore(ev, store) {
212 apply_err = Some(e.to_string());
213 break;
214 }
215 }
216 if let Some(err) = apply_err {
217 return WebReturnType::error(format!("SetActiveTab: SQLite write failed: {}", err));
218 }
219 publish_events(state, &events);
220 if let Ok(ws) = store.must_get::<Workspace>(&ws_id) {
221 let update = MuxObjUpdate {
222 updatetype: "update".into(),
223 otype: OTYPE_WORKSPACE.to_string(),
224 oid: ws_id.clone(),
225 obj: Some(mux_obj_to_value(&ws)),
226 };
227 WebReturnType::success_with_updates(vec![update])
228 } else {
229 WebReturnType::success_empty()
230 }
231}
232
233pub(crate) async fn handle_close_tab(state: &AppState, call: &WebCallType) -> WebReturnType {
243 let store = &state.mstore;
244 let args = &call.args;
245 let ws_id: String = match service::get_arg(args, 0) {
246 Ok(v) => v,
247 Err(e) => return WebReturnType::error(e),
248 };
249 let tab_id: String = match service::get_arg(args, 1) {
250 Ok(v) => v,
251 Err(e) => return WebReturnType::error(e),
252 };
253 if let Err(reason) =
254 crate::sagas::delete_tab::run(state, ws_id.clone(), tab_id.clone()).await
255 {
256 return WebReturnType::error(reason);
257 }
258 let rtn = CloseTabRtnType {
259 closewindow: false,
260 newactivetabid: String::new(),
261 };
262 let mut updates = vec![MuxObjUpdate {
263 updatetype: "delete".into(),
264 otype: OTYPE_TAB.to_string(),
265 oid: tab_id.clone(),
266 obj: None,
267 }];
268 if let Ok(ws) = store.must_get::<Workspace>(&ws_id) {
269 updates.push(MuxObjUpdate {
270 updatetype: "update".into(),
271 otype: OTYPE_WORKSPACE.to_string(),
272 oid: ws_id.clone(),
273 obj: Some(mux_obj_to_value(&ws)),
274 });
275 }
276 WebReturnType::success_data_updates(
277 serde_json::to_value(&rtn).unwrap_or_default(),
278 updates,
279 )
280}
281
282pub(crate) async fn handle_update_tab_ids(state: &AppState, call: &WebCallType) -> WebReturnType {
293 let store = &state.mstore;
294 let args = &call.args;
295 let ws_id: String = match service::get_arg(args, 0) {
296 Ok(v) => v,
297 Err(e) => return WebReturnType::error(e),
298 };
299 let tab_ids: Vec<String> = match service::get_arg(args, 1) {
300 Ok(v) => v,
301 Err(e) => return WebReturnType::error(e),
302 };
303 let events = dispatch_to_reducer(
305 state,
306 agentmux_common::ipc::Command::ReorderTabsBulk {
307 workspace_id: ws_id.clone(),
308 tab_ids,
309 },
310 )
311 .await;
312 if let Some(err_msg) = events.iter().find_map(|e| match e {
313 agentmux_common::ipc::Event::Error { message, .. } => Some(message.clone()),
314 _ => None,
315 }) {
316 return WebReturnType::error(err_msg);
317 }
318 for ev in &events {
319 if let Err(e) = crate::persist_subscriber::apply_event_to_mstore(ev, store) {
320 return WebReturnType::error(format!(
321 "UpdateTabIds: SQLite write failed: {}",
322 e
323 ));
324 }
325 }
326 publish_events(state, &events);
327 if let Ok(updated_ws) = store.must_get::<Workspace>(&ws_id) {
328 let update = MuxObjUpdate {
329 updatetype: "update".into(),
330 otype: OTYPE_WORKSPACE.to_string(),
331 oid: ws_id.clone(),
332 obj: Some(mux_obj_to_value(&updated_ws)),
333 };
334 return WebReturnType::success_with_updates(vec![update]);
335 }
336 WebReturnType::success_empty()
337}
338
339pub(crate) async fn handle_reorder_tab(state: &AppState, call: &WebCallType) -> WebReturnType {
346 let store = &state.mstore;
347 let args = &call.args;
348 let ws_id: String = match service::get_arg(args, 0) {
349 Ok(v) => v,
350 Err(e) => return WebReturnType::error(e),
351 };
352 let tab_id: String = match service::get_arg(args, 1) {
353 Ok(v) => v,
354 Err(e) => return WebReturnType::error(e),
355 };
356 let new_index: usize = match service::get_arg(args, 2) {
357 Ok(v) => v,
358 Err(e) => return WebReturnType::error(e),
359 };
360 tracing::info!(ws_id = %ws_id, tab_id = %tab_id, new_index = %new_index, "[dnd:svc] ReorderTab");
361 let new_index_u32 = u32::try_from(new_index).unwrap_or(u32::MAX);
367 let events = dispatch_to_reducer(
368 state,
369 agentmux_common::ipc::Command::ReorderTab {
370 workspace_id: ws_id.clone(),
371 tab_id: tab_id.clone(),
372 new_index: new_index_u32,
373 },
374 )
375 .await;
376 if let Some(err_msg) = events.iter().find_map(|e| match e {
378 agentmux_common::ipc::Event::Error { message, .. } => Some(message.clone()),
379 _ => None,
380 }) {
381 return WebReturnType::error(err_msg);
382 }
383 let mut apply_err: Option<String> = None;
384 for ev in &events {
385 if let Err(e) = crate::persist_subscriber::apply_event_to_mstore(ev, store) {
386 apply_err = Some(e.to_string());
387 break;
388 }
389 }
390 if let Some(err) = apply_err {
391 return WebReturnType::error(format!("ReorderTab: SQLite write failed: {}", err));
392 }
393 publish_events(state, &events);
394 if let Ok(ws) = store.must_get::<Workspace>(&ws_id) {
395 let update = MuxObjUpdate {
396 updatetype: "update".into(),
397 otype: OTYPE_WORKSPACE.to_string(),
398 oid: ws_id.clone(),
399 obj: Some(mux_obj_to_value(&ws)),
400 };
401 WebReturnType::success_with_updates(vec![update])
402 } else {
403 WebReturnType::success_empty()
404 }
405}