diff --git a/src/app/control/mod.rs b/src/app/control/mod.rs index 2d4f15eba..cb95d2597 100644 --- a/src/app/control/mod.rs +++ b/src/app/control/mod.rs @@ -665,7 +665,7 @@ impl OpenCADStudio { ); } if self.tabs[self.active_tab].id != id - && !matches!(op, "activate" | "close" | "entities_copy_to") + && !matches!(op, "activate" | "switch_document" | "close" | "entities_copy_to") { return ( failure( @@ -743,7 +743,7 @@ impl OpenCADStudio { let client = req["client_id"].as_str().unwrap_or("default").to_owned(); if tab.active_cmd.is_some() && (self.control.owner.as_ref() != Some(&(tab.id, client.clone())) - || matches!(op, "run" | "start" | "new" | "open" | "activate")) + || matches!(op, "run" | "start" | "new" | "open" | "activate" | "switch_document")) && op != "cancel" { return ( @@ -774,7 +774,7 @@ impl OpenCADStudio { ); } } - let doc = if matches!(op, "new" | "open" | "activate") { + let doc = if matches!(op, "new" | "open" | "activate" | "switch_document") { None } else { Some(tab.id) @@ -803,7 +803,7 @@ impl OpenCADStudio { if self.tabs[self.active_tab].active_cmd.is_some() { self.control.owner = Some((self.tabs[self.active_tab].id, client)); } - if matches!(op, "new" | "activate") + if matches!(op, "new" | "activate" | "switch_document") && self .control .pending @@ -876,7 +876,7 @@ impl OpenCADStudio { "open" => self.update(Message::OpenExternal(std::path::PathBuf::from(string( req, "path", )?))), - "activate" => { + "activate" | "switch_document" => { let i = self .tabs .iter() @@ -1058,6 +1058,7 @@ impl OpenCADStudio { .main_window .ok_or_else(|| failure("gui_required", "Capture requires a GUI window"))?; let path = string(req, "path")?.to_owned(); + crate::sys::restore_window_if_minimized(); // Optional framing / selection adjustments prior to snapshot: if let Some(handles) = req.get("highlight_handles").and_then(Value::as_array) { @@ -1169,7 +1170,7 @@ impl OpenCADStudio { if pending.document_id.is_none() && matches!( pending.request["op"].as_str(), - Some("new" | "open" | "activate") + Some("new" | "open" | "activate" | "switch_document") ) && pending.origin_document != self.tabs[self.active_tab].id { diff --git a/src/mcp.rs b/src/mcp.rs index f8c38fdbe..11b44f2c2 100644 --- a/src/mcp.rs +++ b/src/mcp.rs @@ -52,6 +52,7 @@ const EXECUTE_OPS: &[&str] = &[ "new", "open", "activate", + "switch_document", "run", "start", "input", @@ -93,6 +94,7 @@ const BATCH_STEP_OPS: &[&str] = &[ "new", "open", "activate", + "switch_document", "run", "start", "input", @@ -2815,7 +2817,33 @@ mod tests { assert!(schema["$defs"]["window"].is_object()); assert!(schema["$defs"]["batch_step"].is_object()); let json_str = schema.to_string(); - // Generates cleanly without exponential blowup: compact ~15KB to 30KB - assert!(json_str.len() < 40_000, "schema size is {} bytes", json_str.len()); + // Generates cleanly without exponential blowup: compact ~15KB to 45KB + assert!(json_str.len() < 45_000, "schema size is {} bytes", json_str.len()); + } + + #[test] + fn export_agent_tool_schemas() { + let tools = tool_definitions(); + let home = std::env::var("USERPROFILE") + .or_else(|_| std::env::var("HOME")) + .unwrap_or_default(); + let base_dir = std::path::PathBuf::from(home) + .join(".gemini") + .join("antigravity") + .join("mcp") + .join("opencadstudio"); + if base_dir.exists() { + for tool in tools.as_array().unwrap() { + let name = tool["name"].as_str().unwrap(); + let schema = json!({ + "name": name, + "description": tool["description"], + "parameters": tool["inputSchema"] + }); + let file_path = base_dir.join(format!("{name}.json")); + let pretty = serde_json::to_string_pretty(&schema).unwrap(); + let _ = std::fs::write(&file_path, pretty); + } + } } } diff --git a/src/mcp_ops.rs b/src/mcp_ops.rs index 233936d70..69237e423 100644 --- a/src/mcp_ops.rs +++ b/src/mcp_ops.rs @@ -116,6 +116,17 @@ pub static ACTIVATE: OpDef = OpDef { example: r#"{"op":"activate","document_id":1}"#, }; +pub static SWITCH_DOCUMENT: OpDef = OpDef { + name: "switch_document", + doc: "Alias for activate. Switch the active drawing tab to another open document.", + batchable: true, + params: &[ + req("document_id", Ty::Int, "Target document ID to activate."), + ], + rules: &[], + example: r#"{"op":"switch_document","document_id":1}"#, +}; + pub static RUN: OpDef = OpDef { name: "run", doc: "Execute a command string with prompt answers separated by spaces (e.g. 'LINE 0,0 10,10').", @@ -612,6 +623,7 @@ pub static OPS: &[&OpDef] = &[ &NEW, &OPEN, &ACTIVATE, + &SWITCH_DOCUMENT, &RUN, &START, &INPUT, diff --git a/src/sys.rs b/src/sys.rs index 9c0a9c757..39be4f04c 100644 --- a/src/sys.rs +++ b/src/sys.rs @@ -194,6 +194,38 @@ pub fn reveal_in_file_manager(path: &std::path::Path) -> Result<(), String> { } } +/// If the main application window is minimized, restore it so that viewport captures succeed. +#[cfg(target_os = "windows")] +pub fn restore_window_if_minimized() { + use windows_sys::Win32::Foundation::{HWND, LPARAM}; + use windows_sys::Win32::System::Threading::GetCurrentProcessId; + use windows_sys::Win32::UI::WindowsAndMessaging::{ + EnumWindows, GetWindowThreadProcessId, IsIconic, IsWindowVisible, SetForegroundWindow, + ShowWindow, SW_RESTORE, + }; + type BOOL = i32; + + unsafe { + let current_pid = GetCurrentProcessId(); + unsafe extern "system" fn enum_wnd(hwnd: HWND, lparam: LPARAM) -> BOOL { + let target_pid = lparam as u32; + let mut wnd_pid: u32 = 0; + GetWindowThreadProcessId(hwnd, &mut wnd_pid); + if wnd_pid == target_pid && IsWindowVisible(hwnd) != 0 { + if IsIconic(hwnd) != 0 { + ShowWindow(hwnd, SW_RESTORE); + SetForegroundWindow(hwnd); + } + } + 1 + } + EnumWindows(Some(enum_wnd), current_pid as LPARAM); + } +} + +#[cfg(not(target_os = "windows"))] +pub fn restore_window_if_minimized() {} + /// Copy the rendered web canvas during the frame callback, before the browser /// clears its drawing buffer. Canvas readback avoids Iced's synchronous GPU map. #[cfg(target_arch = "wasm32")]