agentmux_srv/backend/wcore/
dnd.rs1use uuid::Uuid;
7
8use crate::backend::storage::store::Store;
9use crate::backend::storage::StoreError;
10use crate::backend::obj::*;
11
12use super::tab::{create_tab, delete_tab, set_active_tab};
13
14pub fn move_block_to_tab(
20 store: &Store,
21 block_id: &str,
22 source_tab_id: &str,
23 dest_tab_id: &str,
24 ws_id: &str,
25 auto_close_source: bool,
26) -> Result<(), StoreError> {
27 tracing::info!(
28 block_id = %block_id,
29 source_tab = %source_tab_id,
30 dest_tab = %dest_tab_id,
31 ws_id = %ws_id,
32 auto_close = %auto_close_source,
33 "[dnd] move_block_to_tab"
34 );
35 if source_tab_id == dest_tab_id {
36 tracing::debug!("[dnd] move_block_to_tab: same tab, no-op");
37 return Ok(()); }
39
40 let mut block = store.must_get::<Block>(block_id)?;
42
43 let mut source_tab = store.must_get::<Tab>(source_tab_id)?;
45 source_tab.blockids.retain(|id| id != block_id);
46 store.update(&mut source_tab)?;
47
48 let mut dest_tab = store.must_get::<Tab>(dest_tab_id)?;
50 dest_tab.blockids.push(block_id.to_string());
51 store.update(&mut dest_tab)?;
52
53 block.parentoref = format!("tab:{}", dest_tab_id);
55 store.update(&mut block)?;
56
57 if auto_close_source && source_tab.blockids.is_empty() {
59 let ws = store.must_get::<Workspace>(ws_id)?;
60 let total_tabs = ws.tabids.len() + ws.pinnedtabids.len();
61 if total_tabs > 1 {
62 tracing::info!(source_tab = %source_tab_id, "[dnd] auto-closing empty source tab");
63 delete_tab(store, ws_id, source_tab_id)?;
64 } else {
65 tracing::debug!(source_tab = %source_tab_id, "[dnd] source tab empty but is last tab — keeping");
66 }
67 }
68
69 tracing::info!(block_id = %block_id, dest_tab = %dest_tab_id, "[dnd] move_block_to_tab complete");
70 Ok(())
71}
72
73pub fn promote_block_to_tab(
78 store: &Store,
79 block_id: &str,
80 source_tab_id: &str,
81 ws_id: &str,
82 auto_close_source: bool,
83) -> Result<Tab, StoreError> {
84 tracing::info!(
85 block_id = %block_id,
86 source_tab = %source_tab_id,
87 ws_id = %ws_id,
88 auto_close = %auto_close_source,
89 "[dnd] promote_block_to_tab"
90 );
91 let mut block = store.must_get::<Block>(block_id)?;
93
94 let mut source_tab = store.must_get::<Tab>(source_tab_id)?;
96 source_tab.blockids.retain(|id| id != block_id);
97 store.update(&mut source_tab)?;
98
99 let new_tab = create_tab(store, ws_id)?;
101
102 let mut new_tab = store.must_get::<Tab>(&new_tab.oid)?;
104 new_tab.blockids.push(block_id.to_string());
105 store.update(&mut new_tab)?;
106
107 block.parentoref = format!("tab:{}", new_tab.oid);
109 store.update(&mut block)?;
110
111 set_active_tab(store, ws_id, &new_tab.oid)?;
113
114 if auto_close_source && source_tab.blockids.is_empty() {
116 let ws = store.must_get::<Workspace>(ws_id)?;
117 let total_tabs = ws.tabids.len() + ws.pinnedtabids.len();
118 if total_tabs > 1 {
119 tracing::info!(source_tab = %source_tab_id, "[dnd] auto-closing empty source tab after promote");
120 delete_tab(store, ws_id, source_tab_id)?;
121 }
122 }
123
124 tracing::info!(block_id = %block_id, new_tab = %new_tab.oid, "[dnd] promote_block_to_tab complete");
125 Ok(new_tab)
126}
127
128pub fn move_tab_to_workspace(
134 store: &Store,
135 tab_id: &str,
136 source_ws_id: &str,
137 dest_ws_id: &str,
138 insert_index: Option<usize>,
139) -> Result<(), StoreError> {
140 tracing::info!(
141 tab_id = %tab_id,
142 source_ws = %source_ws_id,
143 dest_ws = %dest_ws_id,
144 insert_index = ?insert_index,
145 "[dnd] move_tab_to_workspace"
146 );
147 if source_ws_id == dest_ws_id {
148 tracing::debug!("[dnd] move_tab_to_workspace: same workspace, no-op");
149 return Ok(()); }
151
152 let _tab = store.must_get::<Tab>(tab_id)?;
154
155 let mut source_ws = store.must_get::<Workspace>(source_ws_id)?;
157 let total_tabs = source_ws.tabids.len() + source_ws.pinnedtabids.len();
158 if total_tabs <= 1 {
159 tracing::warn!(tab_id = %tab_id, total_tabs = %total_tabs, "[dnd] move_tab_to_workspace blocked: last tab");
160 return Err(StoreError::Other(
161 "cannot move last tab out of workspace".to_string(),
162 ));
163 }
164 source_ws.tabids.retain(|id| id != tab_id);
165 source_ws.pinnedtabids.retain(|id| id != tab_id);
166 if source_ws.activetabid == tab_id {
167 let new_active = source_ws
168 .tabids
169 .first()
170 .or(source_ws.pinnedtabids.first())
171 .cloned()
172 .unwrap_or_default();
173 tracing::info!(old_active = %tab_id, new_active = %new_active, "[dnd] switching active tab in source workspace");
174 source_ws.activetabid = new_active;
175 }
176 store.update(&mut source_ws)?;
177
178 let mut dest_ws = store.must_get::<Workspace>(dest_ws_id)?;
180 let idx = insert_index.unwrap_or(dest_ws.tabids.len());
181 let insert_at = idx.min(dest_ws.tabids.len());
182 dest_ws.tabids.insert(insert_at, tab_id.to_string());
183 dest_ws.activetabid = tab_id.to_string();
184 store.update(&mut dest_ws)?;
185
186 tracing::info!(tab_id = %tab_id, dest_ws = %dest_ws_id, insert_at = %insert_at, "[dnd] move_tab_to_workspace complete");
187 Ok(())
188}
189
190pub fn restore_torn_off_tab(
207 store: &Store,
208 tab_id: &str,
209 source_ws_id: &str,
210 dest_ws_id: &str,
211 insert_index: Option<usize>,
212 was_pinned: bool,
213) -> Result<(), StoreError> {
214 tracing::info!(
215 tab_id = %tab_id,
216 source_ws = %source_ws_id,
217 dest_ws = %dest_ws_id,
218 insert_index = ?insert_index,
219 was_pinned = %was_pinned,
220 "[dnd] restore_torn_off_tab"
221 );
222 if source_ws_id == dest_ws_id {
223 tracing::debug!("[dnd] restore_torn_off_tab: same workspace, no-op");
224 return Ok(());
225 }
226
227 store.with_tx(|tx| {
228 let _tab = tx.must_get::<Tab>(tab_id)?;
235 let mut source_ws = tx.must_get::<Workspace>(source_ws_id)?;
236 let mut dest_ws = tx.must_get::<Workspace>(dest_ws_id)?;
237
238 source_ws.tabids.retain(|id| id != tab_id);
242 source_ws.pinnedtabids.retain(|id| id != tab_id);
243 let source_now_empty =
244 source_ws.tabids.is_empty() && source_ws.pinnedtabids.is_empty();
245 if source_ws.activetabid == tab_id {
246 if let Some(new_active) = source_ws
252 .tabids
253 .first()
254 .or(source_ws.pinnedtabids.first())
255 .cloned()
256 {
257 source_ws.activetabid = new_active;
258 }
259 }
260
261 dest_ws.tabids.retain(|id| id != tab_id);
265 dest_ws.pinnedtabids.retain(|id| id != tab_id);
266
267 let insert_at = if was_pinned {
274 let idx = insert_index.unwrap_or(dest_ws.pinnedtabids.len());
275 let insert_at = idx.min(dest_ws.pinnedtabids.len());
276 dest_ws.pinnedtabids.insert(insert_at, tab_id.to_string());
277 insert_at
278 } else {
279 let idx = insert_index.unwrap_or(dest_ws.tabids.len());
280 let insert_at = idx.min(dest_ws.tabids.len());
281 dest_ws.tabids.insert(insert_at, tab_id.to_string());
282 insert_at
283 };
284 dest_ws.activetabid = tab_id.to_string();
285
286 tx.update(&mut dest_ws)?;
292 if source_now_empty {
293 tracing::info!(source_ws = %source_ws_id, "[dnd] deleting empty tear-off workspace");
294 tx.delete::<Workspace>(source_ws_id)?;
295 } else {
296 tx.update(&mut source_ws)?;
297 }
298
299 tracing::info!(
300 tab_id = %tab_id,
301 dest_ws = %dest_ws_id,
302 insert_at = %insert_at,
303 was_pinned = %was_pinned,
304 "[dnd] restore_torn_off_tab complete"
305 );
306 Ok(())
307 })
308}
309
310pub fn tear_off_tab(
315 store: &Store,
316 tab_id: &str,
317 source_ws_id: &str,
318) -> Result<Workspace, StoreError> {
319 tracing::info!(tab_id = %tab_id, source_ws = %source_ws_id, "[dnd] tear_off_tab");
320 let mut source_ws = store.must_get::<Workspace>(source_ws_id)?;
322 let total_tabs = source_ws.tabids.len() + source_ws.pinnedtabids.len();
323 if total_tabs <= 1 {
324 tracing::warn!(tab_id = %tab_id, total_tabs = %total_tabs, "[dnd] tear_off_tab blocked: last tab");
325 return Err(StoreError::Other(
326 "cannot tear off last tab from workspace".to_string(),
327 ));
328 }
329 source_ws.tabids.retain(|id| id != tab_id);
330 source_ws.pinnedtabids.retain(|id| id != tab_id);
331 if source_ws.activetabid == tab_id {
332 let new_active = source_ws
333 .tabids
334 .first()
335 .or(source_ws.pinnedtabids.first())
336 .cloned()
337 .unwrap_or_default();
338 tracing::info!(old_active = %tab_id, new_active = %new_active, "[dnd] switching active tab after tear-off");
339 source_ws.activetabid = new_active;
340 }
341 store.update(&mut source_ws)?;
342
343 let mut new_ws = Workspace {
345 oid: Uuid::new_v4().to_string(),
346 name: String::new(),
347 tabids: vec![tab_id.to_string()],
348 pinnedtabids: vec![],
349 activetabid: tab_id.to_string(),
350 meta: MetaMapType::new(),
351 ..Default::default()
352 };
353 store.insert(&mut new_ws)?;
354
355 tracing::info!(tab_id = %tab_id, new_ws = %new_ws.oid, "[dnd] tear_off_tab complete");
356 Ok(new_ws)
357}