1use agentmux_common::ipc::{ErrorCode, Event};
5
6use crate::state::State;
7
8
9use crate::state::TabRecord;
10
11pub(super) fn handle_create_tab(state: &mut State, workspace_id: String, name: String) -> Vec<Event> {
21 let Some(workspace_record) = state.workspaces.get(&workspace_id) else {
22 let v = state.bump_version();
23 return vec![Event::Error {
24 code: ErrorCode::InvalidCommand,
25 message: format!("CreateTab: workspace not found: {}", workspace_id),
26 fatal: false,
27 version: v,
28 }];
29 };
30 let resolved_name = if name.is_empty() {
37 format!("tab{}", workspace_record.tab_ids.len() + 1)
38 } else {
39 name
40 };
41 let tab_id = uuid::Uuid::new_v4().to_string();
42 state.tabs.insert(
43 tab_id.clone(),
44 TabRecord {
45 tab_id: tab_id.clone(),
46 workspace_id: workspace_id.clone(),
47 name: resolved_name.clone(),
48 block_ids: Vec::new(),
49 focused_node_id: String::new(),
50 magnified_node_id: String::new(),
51 rootnode: None,
52 },
53 );
54 let workspace = state.workspaces.get_mut(&workspace_id).expect("checked");
55 workspace.tab_ids.push(tab_id.clone());
56 let activated = if workspace.active_tab_id.is_none() {
57 workspace.active_tab_id = Some(tab_id.clone());
58 true
59 } else {
60 false
61 };
62 let mut events = Vec::with_capacity(2);
63 let v = state.bump_version();
64 events.push(Event::TabCreated {
65 workspace_id: workspace_id.clone(),
66 tab_id: tab_id.clone(),
67 name: resolved_name,
68 version: v,
69 });
70 if activated {
71 let v2 = state.bump_version();
72 events.push(Event::ActiveTabChanged {
73 workspace_id,
74 tab_id: Some(tab_id),
75 version: v2,
76 });
77 }
78 events
79}
80
81pub(super) fn handle_delete_tab(
88 state: &mut State,
89 workspace_id: String,
90 tab_id: String,
91 force: bool,
92) -> Vec<Event> {
93 let Some(workspace) = state.workspaces.get_mut(&workspace_id) else {
94 return Vec::new();
95 };
96 let Some(pos) = workspace.tab_ids.iter().position(|t| t == &tab_id) else {
97 return Vec::new();
98 };
99 if !force && workspace.tab_ids.len() <= 1 {
115 let v = state.bump_version();
116 return vec![Event::Error {
117 code: ErrorCode::InvalidCommand,
118 message: format!(
119 "DeleteTab: refusing to delete the last tab in workspace {} (would leave empty workspace; pass force=true for compensation paths)",
120 workspace_id,
121 ),
122 fatal: false,
123 version: v,
124 }];
125 }
126 workspace.tab_ids.remove(pos);
127 let active_changed = if workspace.active_tab_id.as_deref() == Some(tab_id.as_str()) {
128 let new_active = workspace
129 .tab_ids
130 .get(pos)
131 .or_else(|| pos.checked_sub(1).and_then(|i| workspace.tab_ids.get(i)))
132 .cloned();
133 workspace.active_tab_id = new_active.clone();
134 Some(new_active)
135 } else {
136 None
137 };
138 let removed_tab = state.tabs.remove(&tab_id);
139 if let Some(tab) = &removed_tab {
144 for block_id in &tab.block_ids {
145 state.blocks.remove(block_id);
146 }
147 }
148 let mut events = Vec::with_capacity(2);
149 let v = state.bump_version();
150 events.push(Event::TabDeleted {
151 workspace_id: workspace_id.clone(),
152 tab_id,
153 version: v,
154 });
155 if let Some(new_active) = active_changed {
156 let v2 = state.bump_version();
157 events.push(Event::ActiveTabChanged {
158 workspace_id,
159 tab_id: new_active,
160 version: v2,
161 });
162 }
163 events
164}
165
166pub(super) fn handle_set_active_tab(
170 state: &mut State,
171 workspace_id: String,
172 tab_id: String,
173) -> Vec<Event> {
174 let Some(workspace) = state.workspaces.get_mut(&workspace_id) else {
175 let v = state.bump_version();
176 return vec![Event::Error {
177 code: ErrorCode::InvalidCommand,
178 message: format!("SetActiveTab: workspace not found: {}", workspace_id),
179 fatal: false,
180 version: v,
181 }];
182 };
183 if !workspace.tab_ids.iter().any(|t| t == &tab_id) {
184 let v = state.bump_version();
185 return vec![Event::Error {
186 code: ErrorCode::InvalidCommand,
187 message: format!(
188 "SetActiveTab: tab {} not in workspace {}",
189 tab_id, workspace_id
190 ),
191 fatal: false,
192 version: v,
193 }];
194 }
195 if workspace.active_tab_id.as_deref() == Some(tab_id.as_str()) {
196 return Vec::new();
197 }
198 workspace.active_tab_id = Some(tab_id.clone());
199 let v = state.bump_version();
200 vec![Event::ActiveTabChanged {
201 workspace_id,
202 tab_id: Some(tab_id),
203 version: v,
204 }]
205}
206
207pub(super) fn handle_reorder_tab(
212 state: &mut State,
213 workspace_id: String,
214 tab_id: String,
215 new_index: u32,
216) -> Vec<Event> {
217 let Some(workspace) = state.workspaces.get_mut(&workspace_id) else {
218 let v = state.bump_version();
219 return vec![Event::Error {
220 code: ErrorCode::InvalidCommand,
221 message: format!("ReorderTab: workspace not found: {}", workspace_id),
222 fatal: false,
223 version: v,
224 }];
225 };
226 let Some(current_pos) = workspace.tab_ids.iter().position(|t| t == &tab_id) else {
227 let v = state.bump_version();
228 return vec![Event::Error {
229 code: ErrorCode::InvalidCommand,
230 message: format!(
231 "ReorderTab: tab {} not in workspace {}",
232 tab_id, workspace_id
233 ),
234 fatal: false,
235 version: v,
236 }];
237 };
238 let len = workspace.tab_ids.len();
239 let target = (new_index as usize).min(len.saturating_sub(1));
240 if current_pos == target {
241 return Vec::new();
242 }
243 let id = workspace.tab_ids.remove(current_pos);
244 workspace.tab_ids.insert(target, id);
245 let v = state.bump_version();
246 vec![Event::TabReordered {
247 workspace_id,
248 tab_id,
249 new_index: target as u32,
250 version: v,
251 }]
252}
253
254pub(super) fn handle_reorder_tabs_bulk(
259 state: &mut State,
260 workspace_id: String,
261 tab_ids: Vec<String>,
262) -> Vec<Event> {
263 if !state.workspaces.contains_key(&workspace_id) {
281 let v = state.bump_version();
282 return vec![Event::Error {
283 code: ErrorCode::InvalidCommand,
284 message: format!("ReorderTabsBulk: workspace not found: {}", workspace_id),
285 fatal: false,
286 version: v,
287 }];
288 }
289 {
290 let mut seen: std::collections::HashSet<&String> =
291 std::collections::HashSet::with_capacity(tab_ids.len());
292 for id in &tab_ids {
293 if !seen.insert(id) {
294 let v = state.bump_version();
295 return vec![Event::Error {
296 code: ErrorCode::InvalidCommand,
297 message: format!(
298 "ReorderTabsBulk: tab_ids contains duplicate entry: {}",
299 id
300 ),
301 fatal: false,
302 version: v,
303 }];
304 }
305 }
306 }
307 if state.workspaces.get(&workspace_id).expect("checked").tab_ids == tab_ids {
308 return Vec::new();
309 }
310 state.workspaces.get_mut(&workspace_id).expect("checked").tab_ids = tab_ids.clone();
311 let v = state.bump_version();
312 vec![Event::TabsReorderedBulk {
313 workspace_id,
314 tab_ids,
315 version: v,
316 }]
317}
318
319pub(super) fn handle_move_tab(
334 state: &mut State,
335 tab_id: String,
336 src_workspace_id: String,
337 dst_workspace_id: String,
338 dst_index: u32,
339) -> Vec<Event> {
340 if src_workspace_id == dst_workspace_id {
341 let v = state.bump_version();
342 return vec![Event::Error {
343 code: ErrorCode::InvalidCommand,
344 message: "MoveTab: src and dst workspaces are identical; use ReorderTab".into(),
345 fatal: false,
346 version: v,
347 }];
348 }
349 if !state.workspaces.contains_key(&src_workspace_id) {
357 let v = state.bump_version();
358 return vec![Event::Error {
359 code: ErrorCode::InvalidCommand,
360 message: format!("MoveTab: src workspace not found: {}", src_workspace_id),
361 fatal: false,
362 version: v,
363 }];
364 }
365 if !state.workspaces.contains_key(&dst_workspace_id) {
366 let v = state.bump_version();
367 return vec![Event::Error {
368 code: ErrorCode::InvalidCommand,
369 message: format!("MoveTab: dst workspace not found: {}", dst_workspace_id),
370 fatal: false,
371 version: v,
372 }];
373 }
374 let tab_workspace_id: Option<String> =
375 state.tabs.get(&tab_id).map(|t| t.workspace_id.clone());
376 match tab_workspace_id {
377 None => {
378 let v = state.bump_version();
379 return vec![Event::Error {
380 code: ErrorCode::InvalidCommand,
381 message: format!("MoveTab: tab not found in state: {}", tab_id),
382 fatal: false,
383 version: v,
384 }];
385 }
386 Some(actual_ws) if actual_ws != src_workspace_id => {
387 let v = state.bump_version();
388 return vec![Event::Error {
389 code: ErrorCode::InvalidCommand,
390 message: format!(
391 "MoveTab: workspace_id mismatch — tab {} belongs to {}, not {}",
392 tab_id, actual_ws, src_workspace_id
393 ),
394 fatal: false,
395 version: v,
396 }];
397 }
398 Some(_) => {}
399 }
400
401 let new_src_active_tab_id: Option<String> = {
403 let src = state.workspaces.get_mut(&src_workspace_id).expect("checked");
404 src.tab_ids.retain(|id| id != &tab_id);
405 if src.active_tab_id.as_deref() == Some(tab_id.as_str()) {
406 src.active_tab_id = src.tab_ids.first().cloned();
407 }
408 src.active_tab_id.clone()
409 };
410
411 let final_dst_index: u32 = {
417 let dst = state.workspaces.get_mut(&dst_workspace_id).expect("checked");
418 let clamped = (dst_index as usize).min(dst.tab_ids.len());
419 dst.tab_ids.insert(clamped, tab_id.clone());
420 dst.active_tab_id = Some(tab_id.clone());
421 clamped as u32
422 };
423
424 state
426 .tabs
427 .get_mut(&tab_id)
428 .expect("checked")
429 .workspace_id = dst_workspace_id.clone();
430
431 let v = state.bump_version();
432 vec![Event::TabMoved {
433 tab_id: tab_id.clone(),
434 src_workspace_id,
435 dst_workspace_id,
436 dst_index: final_dst_index,
437 new_src_active_tab_id,
438 new_dst_active_tab_id: Some(tab_id),
439 version: v,
440 }]
441}
442
443pub(super) fn handle_rename_tab(state: &mut State, tab_id: String, name: String) -> Vec<Event> {
446 let Some(tab) = state.tabs.get_mut(&tab_id) else {
447 let v = state.bump_version();
448 return vec![Event::Error {
449 code: ErrorCode::InvalidCommand,
450 message: format!("RenameTab: tab not found: {}", tab_id),
451 fatal: false,
452 version: v,
453 }];
454 };
455 if tab.name == name {
456 return Vec::new();
457 }
458 tab.name = name.clone();
459 let v = state.bump_version();
460 vec![Event::TabRenamed {
461 tab_id,
462 name,
463 version: v,
464 }]
465}
466
467pub(super) fn handle_update_tab_meta(
470 state: &mut State,
471 tab_id: String,
472 meta_patch: serde_json::Value,
473) -> Vec<Event> {
474 if !state.tabs.contains_key(&tab_id) {
475 let v = state.bump_version();
476 return vec![Event::Error {
477 code: ErrorCode::InvalidCommand,
478 message: format!("UpdateTabMeta: tab not found: {}", tab_id),
479 fatal: false,
480 version: v,
481 }];
482 }
483 let v = state.bump_version();
484 vec![Event::TabMetaUpdated {
485 tab_id,
486 meta_patch,
487 version: v,
488 }]
489}