agentmux_cef\commands/floating_pane.rs
1// Copyright 2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Phase 1 of the floating-pane tear-off feature (issue #810 + spec
5//! `docs/specs/SPEC_FLOATING_PANE_TEAROFF_2026_05_11.md`).
6//!
7//! This module hosts the IPC command and Win32-native primitive that
8//! creates a *subordinate* floating window — a free-positioned palette-
9//! style window OWNED by the source AgentMux main window. Unlike the
10//! existing tab tear-off path (which spawns a full new AgentMux
11//! instance), a floating pane:
12//!
13//! - has no taskbar entry (`WS_EX_TOOLWINDOW`),
14//! - has no Alt-Tab entry (also `WS_EX_TOOLWINDOW`),
15//! - minimizes / restores / destroys with its owner,
16//! - shares the source instance's sidecar, data dir, and reducer state.
17//!
18//! Phase 1 ships **only** the windowing primitive. The browser embedded
19//! in the floating window loads
20//! `<frontend>?floatingPaneId=<id>&windowLabel=floating-<n>` and the
21//! frontend renders a minimal placeholder shell that says "Floating
22//! pane: \<id\>". Wiring the *drag-out gesture* to this primitive is
23//! Phase 3. Wiring the full `<Block>` renderer is Phase 2.
24//!
25//! Non-Windows platforms are out of scope per spec §10. The macOS path
26//! is `NSWindow::addChildWindow`; Linux varies by compositor. Both will
27//! be addressed in a follow-up.
28
29use std::sync::Arc;
30
31use serde::Deserialize;
32
33use crate::state::AppState;
34
35#[derive(Debug, Deserialize)]
36pub struct OpenFloatingPaneArgs {
37 /// Reducer-side identifier for the pane being torn off. Threaded
38 /// through to the frontend via the query string so the floating-
39 /// pane shell knows what to render.
40 pub pane_id: String,
41 /// Screen-space top-left coordinates where the new floating window
42 /// should appear. Typically the cursor position at drop time.
43 pub x: i32,
44 pub y: i32,
45 /// Initial window size. Phase 6 will memo the source pane's last
46 /// docked size; Phase 1 accepts a caller-provided default.
47 pub width: i32,
48 pub height: i32,
49}
50
51#[derive(Debug, serde::Serialize)]
52pub struct OpenFloatingPaneResponse {
53 /// The window label assigned to the floating window. Stable for
54 /// the life of the floater; persists into `state.window_meta` like
55 /// any other top-level label.
56 pub window_label: String,
57}
58
59/// IPC handler — called when the frontend or an agent invokes
60/// `open_floating_pane_window` on the host. Validates input, allocates
61/// a stable label, and posts a UI-thread task to create the owned HWND
62/// and embed a CEF browser inside it.
63pub fn open_floating_pane_window(
64 state: &Arc<AppState>,
65 args: &serde_json::Value,
66) -> Result<serde_json::Value, String> {
67 let parsed: OpenFloatingPaneArgs = serde_json::from_value(args.clone())
68 .map_err(|e| format!("open_floating_pane_window: invalid args: {e}"))?;
69
70 if parsed.pane_id.is_empty() {
71 return Err("open_floating_pane_window: pane_id is required".to_string());
72 }
73 if parsed.width <= 0 || parsed.height <= 0 {
74 return Err(format!(
75 "open_floating_pane_window: width/height must be positive (got {}×{})",
76 parsed.width, parsed.height
77 ));
78 }
79
80 // The H.7 main-window-creation gate (any pane mid-close → wedged
81 // Chromium IPC) applies here too — same Chromium message loop. If
82 // a pane is closing, refuse the floating-window creation; the
83 // caller retries.
84 if state.any_browser_pane_closing() {
85 tracing::warn!(
86 target: "wfr:gate",
87 "[wfr:gate] open_floating_pane_window refused — pane is mid-close (H.7 invariant)"
88 );
89 return Err("a pane is currently closing; retry shortly".to_string());
90 }
91
92 let window_id = uuid::Uuid::new_v4();
93 let window_label = format!("floating-{}", window_id.simple());
94
95 tracing::info!(
96 pane_id = %parsed.pane_id,
97 label = %window_label,
98 x = parsed.x,
99 y = parsed.y,
100 w = parsed.width,
101 h = parsed.height,
102 "[floating-pane] open_floating_pane_window request",
103 );
104
105 #[cfg(target_os = "windows")]
106 {
107 crate::floating_pane::post_create_floating_window(state, &parsed, &window_label);
108 Ok(serde_json::to_value(OpenFloatingPaneResponse { window_label }).unwrap_or_default())
109 }
110
111 #[cfg(not(target_os = "windows"))]
112 {
113 // Phase 1 is Windows-only per spec §10. macOS uses
114 // `NSWindow::addChildWindow`; Linux varies. Both are explicit
115 // follow-ups; return a clear error so callers fail loudly.
116 let _ = parsed;
117 let _ = window_label;
118 Err(
119 "open_floating_pane_window: not yet implemented on this platform (Windows only in Phase 1)"
120 .to_string(),
121 )
122 }
123}