Skip to main content

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::store::Store;
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: &Store,
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: &Store,
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                // "Tab N" — same scheme as `wcore/tab.rs`'s
86                // `create_tab_with_opts` and the `handle_create_tab` service
87                // handler behind the user's "+" button. Earlier
88                // this used "T{N}" (a stylistic mismatch), then plain
89                // "tabN" (SPEC_TAB_GAPS_AND_NAMING_2026_04_25 + the
90                // first-principles spec SPEC_TAB_BAR_FIRST_PRINCIPLES_2026_04_25),
91                // revised to "Tab N" 2026-09-02.
92                name: format!("Tab {}", ws.tabids.len() + ws.pinnedtabids.len() + 1),
93                layoutstate: layout.oid.clone(),
94                blockids: vec![],
95                meta: MetaMapType::new(),
96                ..Default::default()
97            };
98            tx.insert(&mut tab)?;
99
100            // Link tab to workspace
101            ws.tabids.push(tab.oid.clone());
102            ws.activetabid = tab.oid.clone();
103            tx.update(&mut ws)?;
104
105            ws.oid
106        } else {
107            let _ws = tx.must_get::<Workspace>(workspace_id)?;
108            workspace_id.to_string()
109        };
110
111        // Create window
112        let mut window = Window {
113            oid: Uuid::new_v4().to_string(),
114            workspaceid: ws_id,
115            isnew: true,
116            pos: Point { x: 0, y: 0 },
117            winsize: WinSize {
118                width: 0,
119                height: 0,
120            },
121            meta: MetaMapType::new(),
122            ..Default::default()
123        };
124        tx.insert(&mut window)?;
125
126        // Update client with new window ID
127        let clients = tx.get_all::<Client>()?;
128        if let Some(client) = clients.into_iter().next() {
129            let mut client = client;
130            client.windowids.push(window.oid.clone());
131            tx.update(&mut client)?;
132        }
133
134        Ok(window)
135    })
136}
137
138/// Close a window and remove from client's window list.
139/// Cascades cleanup through workspace → tabs → blocks, killing all shell processes.
140pub fn close_window(store: &Store, window_id: &str) -> Result<(), StoreError> {
141    // Cascade: kill all shell processes in this window's workspace
142    if let Ok(window) = store.must_get::<Window>(window_id) {
143        if let Ok(workspace) = store.must_get::<Workspace>(&window.workspaceid) {
144            let all_tab_ids: Vec<String> = workspace
145                .tabids
146                .iter()
147                .chain(workspace.pinnedtabids.iter())
148                .cloned()
149                .collect();
150            for tab_id in &all_tab_ids {
151                let _ = super::tab::delete_tab_inner(store, tab_id);
152            }
153            let _ = store.delete::<Workspace>(&window.workspaceid);
154        }
155    }
156
157    let mut client = get_client(store)?;
158    client.windowids.retain(|id| id != window_id);
159    store.update(&mut client)?;
160    store.delete::<Window>(window_id)?;
161    Ok(())
162}
163
164/// Focus a window (move to front of client's window list).
165pub fn focus_window(store: &Store, window_id: &str) -> Result<(), StoreError> {
166    let mut client = get_client(store)?;
167    if let Some(pos) = client.windowids.iter().position(|id| id == window_id) {
168        let id = client.windowids.remove(pos);
169        client.windowids.insert(0, id);
170        store.update(&mut client)?;
171    }
172    Ok(())
173}
174
175/// Switch a window to a different workspace.
176pub fn switch_workspace(
177    store: &Store,
178    window_id: &str,
179    ws_id: &str,
180) -> Result<(), StoreError> {
181    // Verify workspace exists
182    let _ = store.must_get::<Workspace>(ws_id)?;
183
184    let mut window = store.must_get::<Window>(window_id)?;
185    window.workspaceid = ws_id.to_string();
186    store.update(&mut window)?;
187    Ok(())
188}
189
190/// Check and fix a window — ensure it has a valid workspace with tabs.
191pub(super) fn check_and_fix_window(store: &Store, window_id: &str) -> Result<(), StoreError> {
192    let window = match store.get::<Window>(window_id)? {
193        Some(w) => w,
194        None => return Ok(()), // window doesn't exist, nothing to fix
195    };
196
197    // Check workspace exists
198    let ws = match store.get::<Workspace>(&window.workspaceid)? {
199        Some(ws) => ws,
200        None => {
201            // Workspace missing — create a new one
202            let ws = create_workspace(store, "")?;
203            let mut window = window;
204            window.workspaceid = ws.oid.clone();
205            store.update(&mut window)?;
206            ws
207        }
208    };
209
210    // Ensure workspace has at least one tab (matches Go: checks both tabids and pinnedtabids)
211    if ws.tabids.is_empty() && ws.pinnedtabids.is_empty() {
212        create_tab(store, &ws.oid)?;
213    }
214
215    Ok(())
216}