agentmux_cef\client/
helpers.rs

1// Copyright 2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Small helper functions extracted from client/mod.rs in
5//! task #182 PR-G for navigability.
6
7/// Quote a string as a JavaScript string literal — escape backslashes,
8/// quotes, and newlines so it's safe to embed inside `<script>` via
9/// `format!`. Used by the recovery page to inject the app URL for the
10/// Reload button's navigation target.
11use super::dlog;
12
13pub(super) fn js_string_literal(s: &str) -> String {
14    let mut out = String::with_capacity(s.len() + 2);
15    out.push('"');
16    for c in s.chars() {
17        match c {
18            '\\' => out.push_str("\\\\"),
19            '"' => out.push_str("\\\""),
20            '\n' => out.push_str("\\n"),
21            '\r' => out.push_str("\\r"),
22            '\t' => out.push_str("\\t"),
23            '<' => out.push_str("\\u003c"), // defense against </script> injection
24            '>' => out.push_str("\\u003e"),
25            '&' => out.push_str("\\u0026"),
26            c if (c as u32) < 0x20 => {
27                out.push_str(&format!("\\u{:04x}", c as u32));
28            }
29            c => out.push(c),
30        }
31    }
32    out.push('"');
33    out
34}
35
36/// Minimal HTML escape for the recovery page. Only the characters that
37/// would break the `format!`-templated string need attention; the input
38/// (CEF status enum + cef-provided error string) is trusted but may
39/// contain `&` / `<` / `>` in some failure modes.
40pub(super) fn html_escape(s: &str) -> String {
41    s.replace('&', "&amp;")
42        .replace('<', "&lt;")
43        .replace('>', "&gt;")
44        .replace('"', "&quot;")
45        .replace('\'', "&#39;")
46}
47
48/// Synchronously tell the backend to close a window's workspace/tabs/shells.
49///
50/// Uses a raw TCP connection so no async runtime or extra crate is needed.
51/// Called from a background thread in `on_before_close` so the CEF UI thread
52/// is not blocked. Fire-and-forget: we write the request and don't read the response.
53pub(super) fn backend_close_window(web_endpoint: &str, auth_key: &str, window_id: &str) {
54    use std::io::Write;
55
56    // Parse host:port from "http://127.0.0.1:PORT"
57    let addr_str = web_endpoint
58        .trim_start_matches("http://")
59        .trim_start_matches("https://");
60    let addr: std::net::SocketAddr = match addr_str.parse() {
61        Ok(a) => a,
62        Err(e) => {
63            tracing::warn!("[backend_close_window] cannot parse endpoint '{}': {}", web_endpoint, e);
64            return;
65        }
66    };
67
68    let body = serde_json::json!({
69        "service": "window",
70        "method": "CloseWindow",
71        "args": [window_id],
72        "uicontext": null,
73    }).to_string();
74    let request = format!(
75        "POST /agentmux/service?service=window&method=CloseWindow&authkey={} HTTP/1.1\r\n\
76         Host: 127.0.0.1\r\n\
77         Content-Type: application/json\r\n\
78         Content-Length: {}\r\n\
79         Connection: close\r\n\
80         \r\n\
81         {}",
82        auth_key, body.len(), body
83    );
84
85    dlog(&format!("backend_close_window: connecting to {} for window_id={}", addr, window_id));
86    let timeout = std::time::Duration::from_millis(2000);
87    match std::net::TcpStream::connect_timeout(&addr, timeout) {
88        Ok(mut stream) => {
89            stream.set_write_timeout(Some(timeout)).ok();
90            stream.set_read_timeout(Some(timeout)).ok();
91            match stream.write_all(request.as_bytes()) {
92                Ok(_) => {
93                    dlog(&format!("backend_close_window: sent request for window_id={}", window_id));
94                    // Read response to confirm the backend received it
95                    use std::io::Read;
96                    let mut resp = String::new();
97                    let _ = stream.read_to_string(&mut resp);
98                    let first_line = resp.lines().next().unwrap_or("(empty)").to_string();
99                    dlog(&format!("backend_close_window: response first line: {}", first_line));
100                }
101                Err(e) => dlog(&format!("backend_close_window: write failed: {}", e)),
102            }
103        }
104        Err(e) => dlog(&format!("backend_close_window: connect failed to {}: {}", addr, e)),
105    }
106}