agentmux_cef\commands/
palette.rs

1// Copyright 2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3//
4// `run_command` IPC handler — dispatches a command ID to the frontend registry.
5//
6// The Rust side does not own the command registry; it simply forwards the ID
7// to the frontend via a CEF `CustomEvent`. The frontend's `commandRegistry.run(id)`
8// handles validation and execution.
9
10use std::sync::Arc;
11
12use cef::{CefString, ImplBrowser, ImplFrame};
13
14use crate::state::AppState;
15
16/// Dispatch a command palette ID to the frontend of the target window.
17///
18/// Args:
19///   `id`          — stable command ID (e.g. `"open:terminal"`)
20///   `windowLabel` — (optional) which window to target; defaults to `"main"`
21pub fn run_command(state: &Arc<AppState>, args: &serde_json::Value) -> Result<serde_json::Value, String> {
22    let id = args["id"]
23        .as_str()
24        .ok_or_else(|| "run_command: missing 'id' field".to_string())?;
25
26    let window_label = args["windowLabel"].as_str().unwrap_or("main");
27
28    let js = format!(
29        "window.dispatchEvent(new CustomEvent('agentmux-run-command', {{ detail: {{ id: {:?} }} }}));",
30        id
31    );
32
33    // Phase H.2.b — reducer-aware lookup with fallback.
34    let browser = state
35        .get_browser(window_label)
36        .or_else(|| state.first_browser().map(|(_, b)| b));
37
38    if let Some(browser) = browser {
39        if let Some(frame) = browser.main_frame() {
40            let code = CefString::from(js.as_str());
41            let url = CefString::from("");
42            frame.execute_java_script(Some(&code), Some(&url), 0);
43            tracing::debug!(id = %id, window = %window_label, "[palette] dispatched run_command");
44        }
45    } else {
46        tracing::warn!(id = %id, "[palette] run_command: no browser available");
47    }
48
49    Ok(serde_json::Value::Null)
50}
51
52/// Open an agent pane with a specific Forge agent.
53///
54/// Dispatches a `CustomEvent('agentmux-open-agent')` to the frontend, which
55/// creates a block with `view: "agent"` + `agentId` and lets the AgentView
56/// handle the full launch flow (CLI resolve, auth, controller).
57///
58/// Args:
59///   `agent_id` — Forge agent ID or name (e.g. `"agentx"`)
60pub fn open_agent(state: &Arc<AppState>, args: &serde_json::Value) -> Result<serde_json::Value, String> {
61    let agent_id = args["agent_id"]
62        .as_str()
63        .ok_or_else(|| "open_agent: missing 'agent_id' field".to_string())?;
64
65    let js = format!(
66        "window.dispatchEvent(new CustomEvent('agentmux-open-agent', {{ detail: {{ agentId: {:?} }} }}));",
67        agent_id
68    );
69
70    // Phase H.2.b — reducer-aware "any browser" routing.
71    let browser = state.first_browser().map(|(_, b)| b);
72
73    if let Some(browser) = browser {
74        if let Some(frame) = browser.main_frame() {
75            let code = CefString::from(js.as_str());
76            let url = CefString::from("");
77            frame.execute_java_script(Some(&code), Some(&url), 0);
78            tracing::info!(agent_id = %agent_id, "[app-api] dispatched open_agent");
79        }
80    } else {
81        tracing::warn!(agent_id = %agent_id, "[app-api] open_agent: no browser available");
82    }
83
84    Ok(serde_json::json!({ "dispatched": true, "agent_id": agent_id }))
85}