diff --git a/crates/app-tauri/src/commands.rs b/crates/app-tauri/src/commands.rs index 6875ca0..7944d75 100644 --- a/crates/app-tauri/src/commands.rs +++ b/crates/app-tauri/src/commands.rs @@ -2336,6 +2336,18 @@ pub struct CloseViewWindowResponseDto { pub closed: bool, } +/// Snapshot returned by `list_open_view_windows`. +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ViewWindowSnapshot { + /// Panel id rendered by the detached window. + pub panel: String, + /// Stable Tauri window label. + pub label: String, + /// Whether Tauri currently reports the OS window as visible. + pub visible: bool, +} + /// Payload emitted on `view-window://lifecycle`. #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] @@ -2429,6 +2441,19 @@ pub(crate) fn view_window_url(panel: ViewPanel) -> String { format!("index.html?panel={}", panel.as_str()) } +fn view_panel_from_window_label(label: &str) -> Option { + let rest = label.strip_prefix("view-")?; + if let Ok(panel) = ViewPanel::parse(rest) { + return Some(panel); + } + + let (panel, project_id) = rest.rsplit_once('-')?; + if project_id.len() != 32 || uuid::Uuid::parse_str(project_id).is_err() { + return None; + } + ViewPanel::parse(panel).ok() +} + pub(crate) fn emit_view_window_lifecycle( app: &AppHandle, kind: &'static str, @@ -2468,6 +2493,30 @@ pub async fn get_focused_project( Ok(state.get_focused_project()) } +/// `list_open_view_windows` — return the detached panel windows currently +/// registered by Tauri. +#[tauri::command] +pub fn list_open_view_windows(app: AppHandle) -> Vec { + let mut windows = app + .webview_windows() + .into_iter() + .filter_map(|(label, window)| { + let panel = view_panel_from_window_label(&label)?; + Some(ViewWindowSnapshot { + panel: panel.as_str().to_owned(), + label, + visible: window.is_visible().unwrap_or(true), + }) + }) + .collect::>(); + windows.sort_by(|left, right| { + left.panel + .cmp(&right.panel) + .then(left.label.cmp(&right.label)) + }); + windows +} + /// `open_view_window` — open or focus a detached OS window for one panel. /// /// The window is a normal decorated, resizable system window. Its webview loads @@ -2578,6 +2627,42 @@ mod view_window_tests { assert!(err.message.contains("invalid view panel: terminal")); } + #[test] + fn view_window_label_parser_accepts_stable_and_legacy_labels() { + assert_eq!( + view_panel_from_window_label("view-tickets") + .unwrap() + .as_str(), + "tickets" + ); + assert_eq!( + view_panel_from_window_label("view-tickets-0000000000000000000000000000002a") + .unwrap() + .as_str(), + "tickets" + ); + assert!(view_panel_from_window_label("main").is_none()); + assert!(view_panel_from_window_label("view-unknown").is_none()); + assert!(view_panel_from_window_label("view-tickets-not-a-project").is_none()); + } + + #[test] + fn view_window_snapshot_payload_is_camel_case() { + let payload = ViewWindowSnapshot { + panel: "tickets".to_owned(), + label: "view-tickets".to_owned(), + visible: true, + }; + + let json = serde_json::to_string(&payload).unwrap(); + assert!(json.contains("\"panel\":\"tickets\""), "json was {json}"); + assert!( + json.contains("\"label\":\"view-tickets\""), + "json was {json}" + ); + assert!(json.contains("\"visible\":true"), "json was {json}"); + } + #[test] fn view_window_lifecycle_payload_is_camel_case() { let payload = ViewWindowLifecycleEventDto { diff --git a/crates/app-tauri/src/lib.rs b/crates/app-tauri/src/lib.rs index 03dcd11..d349d82 100644 --- a/crates/app-tauri/src/lib.rs +++ b/crates/app-tauri/src/lib.rs @@ -264,6 +264,7 @@ pub fn run() { commands::resolve_memory_links, commands::set_focused_project, commands::get_focused_project, + commands::list_open_view_windows, commands::open_view_window, commands::close_view_window, commands::move_tab_to_new_window,