agentmux_cef/
events.rs

1// Copyright 2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3//
4// Rust -> JS event emission via CEF's execute_javascript.
5//
6// Events are dispatched as CustomEvents on `window`, matching the pattern
7// used by the frontend's `listenEvent()` in platform/ipc.ts:
8//
9//   window.dispatchEvent(new CustomEvent('agentmux-event', {
10//     detail: { event: 'event-name', payload: ... }
11//   }))
12
13use cef::{Browser, CefString, ImplBrowser, ImplFrame};
14
15/// Emit an event to the frontend via CEF's execute_javascript.
16///
17/// The event will be dispatched as a `CustomEvent` named `agentmux-event`
18/// with `detail.event` set to the event name and `detail.payload` set to
19/// the serialized payload.
20pub fn emit_event(browser: &Browser, event: &str, payload: &serde_json::Value) {
21    if let Some(frame) = browser.main_frame() {
22        let payload_str = serde_json::to_string(payload).unwrap_or_else(|_| "null".to_string());
23        let js = format!(
24            "window.dispatchEvent(new CustomEvent('agentmux-event', {{ detail: {{ event: '{}', payload: {} }} }}));",
25            event, payload_str
26        );
27        let code = CefString::from(js.as_str());
28        let url = CefString::from("");
29        frame.execute_java_script(Some(&code), Some(&url), 0);
30    }
31}
32
33/// Emit an event to the "main" browser stored in AppState.
34/// This is a convenience wrapper for use from command handlers and background tasks.
35pub fn emit_event_from_state(state: &crate::state::AppState, event: &str, payload: &serde_json::Value) {
36    // Phase H.2.b — reducer-aware lookup with fallback.
37    if let Some(browser) = state.get_browser("main") {
38        emit_event(&browser, event, payload);
39    } else if let Some((_label, browser)) = state.first_browser() {
40        // Fallback: emit to any available browser
41        emit_event(&browser, event, payload);
42    } else {
43        tracing::warn!("Cannot emit event '{}': no browser handle in state", event);
44    }
45}
46
47/// Emit an event to ALL browser windows (for cross-window drag broadcasts).
48pub fn emit_event_all_windows(state: &crate::state::AppState, event: &str, payload: &serde_json::Value) {
49    // Phase H.2.b — reducer-aware iteration with fallback.
50    let all = state.list_browsers();
51    if all.is_empty() {
52        tracing::warn!("Cannot broadcast event '{}': no browsers", event);
53        return;
54    }
55    for (_label, browser) in all {
56        emit_event(&browser, event, payload);
57    }
58}
59
60/// Emit an event to every TOP-LEVEL window — i.e. the host frontend
61/// renderers, excluding pane child browsers. Use this instead of
62/// `emit_event_all_windows` when the payload carries metadata that
63/// shouldn't be visible to untrusted remote content loaded inside a
64/// browser pane. The host frontend's `listenEvent` lives in the
65/// top-level renderer; pane main frames load arbitrary URLs.
66pub fn emit_event_to_top_level_windows(
67    state: &crate::state::AppState,
68    event: &str,
69    payload: &serde_json::Value,
70) {
71    let all = state.list_top_level_browsers();
72    if all.is_empty() {
73        tracing::warn!("Cannot broadcast event '{}': no top-level browsers", event);
74        return;
75    }
76    for (_label, browser) in all {
77        emit_event(&browser, event, payload);
78    }
79}
80
81/// Emit an event to a specific browser window by label.
82/// Used by the tear-off Phase 4 hook to push merge-candidate
83/// changes to the destination renderer.
84pub fn emit_event_to_window(
85    state: &crate::state::AppState,
86    label: &str,
87    event: &str,
88    payload: &serde_json::Value,
89) -> bool {
90    // Phase H.2.b — reducer-aware lookup with fallback.
91    match state.get_browser(label) {
92        Some(browser) => {
93            emit_event(&browser, event, payload);
94            true
95        }
96        None => {
97            tracing::warn!(
98                "Cannot emit event '{}' to label '{}': no such browser",
99                event,
100                label
101            );
102            false
103        }
104    }
105}