agentmux_srv\backend\wcore/
window.rs

1// Copyright 2025-2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Window CRUD, focus, workspace switching, and repair operations.
5
6use uuid::Uuid;
7
8use crate::backend::storage::wstore::WaveStore;
9use crate::backend::storage::StoreError;
10use crate::backend::obj::*;
11
12use super::get_client;
13use super::tab::create_tab;
14use super::workspace::create_workspace;
15
16/// Create a new window pointing to a workspace.
17/// If workspace_id is empty, auto-creates a new workspace + default tab (matches Go behavior).
18pub fn create_window(
19    store: &WaveStore,
20    workspace_id: &str,
21) -> Result<Window, StoreError> {
22    let ws_id = if workspace_id.is_empty() {
23        let ws = create_workspace(store, "")?;
24        let _tab = create_tab(store, &ws.oid)?;
25        ws.oid
26    } else {
27        let _ws = store.must_get::<Workspace>(workspace_id)?;
28        workspace_id.to_string()
29    };
30    let mut window = Window {
31        oid: Uuid::new_v4().to_string(),
32        workspaceid: ws_id,
33        isnew: true,
34        pos: Point { x: 0, y: 0 },
35        winsize: WinSize {
36            width: 0,
37            height: 0,
38        },
39        meta: MetaMapType::new(),
40        ..Default::default()
41    };
42    store.insert(&mut window)?;
43    Ok(window)
44}
45
46/// Create a new window with all required objects in a single transaction.
47/// If workspace_id is empty, creates workspace + tab + window + updates client
48/// all in one BEGIN/COMMIT — reducing 8+ lock acquisitions and fsyncs to 1.
49///
50/// Returns the created Window.
51pub fn create_window_full(
52    store: &WaveStore,
53    workspace_id: &str,
54) -> Result<Window, StoreError> {
55    store.with_tx(|tx| {
56        let ws_id = if workspace_id.is_empty() {
57            // Create workspace
58            let mut ws = Workspace {
59                oid: Uuid::new_v4().to_string(),
60                name: String::new(),
61                tabids: vec![],
62                pinnedtabids: vec![],
63                activetabid: String::new(),
64                meta: MetaMapType::new(),
65                ..Default::default()
66            };
67            tx.insert(&mut ws)?;
68
69            // Create layout state for tab
70            let mut layout = LayoutState {
71                oid: Uuid::new_v4().to_string(),
72                rootnode: None,
73                magnifiednodeid: String::new(),
74                focusednodeid: String::new(),
75                leaforder: None,
76                pendingbackendactions: None,
77                meta: None,
78                ..Default::default()
79            };
80            tx.insert(&mut layout)?;
81
82            // Create tab
83            let mut tab = Tab {
84                oid: Uuid::new_v4().to_string(),
85                // Plain "tabN" — same scheme used by `wcore/tab.rs:30`
86                // for tabs created via the user's "+" button. Earlier
87                // this used "T{N}" which was a stylistic mismatch.
88                // See SPEC_TAB_GAPS_AND_NAMING_2026_04_25 + the
89                // first-principles spec
90                // SPEC_TAB_BAR_FIRST_PRINCIPLES_2026_04_25.
91                name: format!("tab{}", ws.tabids.len() + ws.pinnedtabids.len() + 1),
92                layoutstate: layout.oid.clone(),
93                blockids: vec![],
94                meta: MetaMapType::new(),
95                ..Default::default()
96            };
97            tx.insert(&mut tab)?;
98
99            // Link tab to workspace
100            ws.tabids.push(tab.oid.clone());
101            ws.activetabid = tab.oid.clone();
102            tx.update(&mut ws)?;
103
104            ws.oid
105        } else {
106            let _ws = tx.must_get::<Workspace>(workspace_id)?;
107            workspace_id.to_string()
108        };
109
110        // Create window
111        let mut window = Window {
112            oid: Uuid::new_v4().to_string(),
113            workspaceid: ws_id,
114            isnew: true,
115            pos: Point { x: 0, y: 0 },
116            winsize: WinSize {
117                width: 0,
118                height: 0,
119            },
120            meta: MetaMapType::new(),
121            ..Default::default()
122        };
123        tx.insert(&mut window)?;
124
125        // Update client with new window ID
126        let clients = tx.get_all::<Client>()?;
127        if let Some(client) = clients.into_iter().next() {
128            let mut client = client;
129            client.windowids.push(window.oid.clone());
130            tx.update(&mut client)?;
131        }
132
133        Ok(window)
134    })
135}
136
137/// Close a window and remove from client's window list.
138/// Cascades cleanup through workspace → tabs → blocks, killing all shell processes.
139pub fn close_window(store: &WaveStore, window_id: &str) -> Result<(), StoreError> {
140    // Cascade: kill all shell processes in this window's workspace
141    if let Ok(window) = store.must_get::<Window>(window_id) {
142        if let Ok(workspace) = store.must_get::<Workspace>(&window.workspaceid) {
143            let all_tab_ids: Vec<String> = workspace
144                .tabids
145                .iter()
146                .chain(workspace.pinnedtabids.iter())
147                .cloned()
148                .collect();
149            for tab_id in &all_tab_ids {
150                let _ = super::tab::delete_tab_inner(store, tab_id);
151            }
152            let _ = store.delete::<Workspace>(&window.workspaceid);
153        }
154    }
155
156    let mut client = get_client(store)?;
157    client.windowids.retain(|id| id != window_id);
158    store.update(&mut client)?;
159    store.delete::<Window>(window_id)?;
160    Ok(())
161}
162
163/// Focus a window (move to front of client's window list).
164pub fn focus_window(store: &WaveStore, window_id: &str) -> Result<(), StoreError> {
165    let mut client = get_client(store)?;
166    if let Some(pos) = client.windowids.iter().position(|id| id == window_id) {
167        let id = client.windowids.remove(pos);
168        client.windowids.insert(0, id);
169        store.update(&mut client)?;
170    }
171    Ok(())
172}
173
174/// Switch a window to a different workspace.
175pub fn switch_workspace(
176    store: &WaveStore,
177    window_id: &str,
178    ws_id: &str,
179) -> Result<(), StoreError> {
180    // Verify workspace exists
181    let _ = store.must_get::<Workspace>(ws_id)?;
182
183    let mut window = store.must_get::<Window>(window_id)?;
184    window.workspaceid = ws_id.to_string();
185    store.update(&mut window)?;
186    Ok(())
187}
188
189/// Check and fix a window — ensure it has a valid workspace with tabs.
190pub(super) fn check_and_fix_window(store: &WaveStore, window_id: &str) -> Result<(), StoreError> {
191    let window = match store.get::<Window>(window_id)? {
192        Some(w) => w,
193        None => return Ok(()), // window doesn't exist, nothing to fix
194    };
195
196    // Check workspace exists
197    let ws = match store.get::<Workspace>(&window.workspaceid)? {
198        Some(ws) => ws,
199        None => {
200            // Workspace missing — create a new one
201            let ws = create_workspace(store, "")?;
202            let mut window = window;
203            window.workspaceid = ws.oid.clone();
204            store.update(&mut window)?;
205            ws
206        }
207    };
208
209    // Ensure workspace has at least one tab (matches Go: checks both tabids and pinnedtabids)
210    if ws.tabids.is_empty() && ws.pinnedtabids.is_empty() {
211        create_tab(store, &ws.oid)?;
212    }
213
214    Ok(())
215}