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}