Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions cmux-tui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ The default session is `main`. Default sockets live at `$TMPDIR/cmux-tui-<uid>/<

Use `--term <value>` to set `TERM` for child PTYs. Without it, children get `xterm-256color`; `CMUX_TUI_TERM` can override the process default in the surface layer, with `CMUX_MUX_TERM` retained as a legacy fallback.

## Browser Realism

By default, browser panes launch your real Google Chrome or another Chrome-family binary in `browser.mode: "headful"` with a visible window and a persistent per-session profile. Log into Google or other sites once in that visible window; cookies and logins persist across sessions. Set `browser.mode: "headless"` to hide the launched Chrome window. Both modes keep the anti-throttle flags, `--disable-blink-features=AutomationControlled`, the persistent `--user-data-dir`, and `about:blank` startup.

Chrome 136 and newer reject CDP remote debugging on the OS-default profile directory, and a running normal Chrome owns its profile `SingletonLock`. Use the mux profile, set `browser.user_data_dir` to a copy or a dedicated directory after quitting normal Chrome, or attach to a Chrome you started with `--remote-debugging-port`.

To attach instead of launching, set `browser.cdp_url`, `CMUX_MUX_CDP_URL`, or enable discovery. Agent Browser works the same way: run `agent-browser get cdp-url` and use the returned `ws://` URL. This build supports `ws://` and `http://` CDP endpoints; `wss://` is not supported.

## Development

```bash
Expand Down
89 changes: 75 additions & 14 deletions cmux-tui/crates/cmux-tui-cdp/src/chrome.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::ffi::OsString;
use std::io::{BufRead, BufReader};
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::sync::Mutex;
use std::sync::atomic::{AtomicU64, Ordering};
Expand All @@ -12,10 +12,18 @@ static PROFILE_SEQ: AtomicU64 = AtomicU64::new(1);
#[derive(Debug, Clone)]
pub struct ChromeLaunchOptions {
pub binary: PathBuf,
pub mode: BrowserMode,
pub user_data_dir: Option<PathBuf>,
pub ephemeral: bool,
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum BrowserMode {
#[default]
Headful,
Headless,
}

/// A launched Chrome/Chromium process plus its profile dir.
pub struct Chrome {
child: Mutex<Option<Child>>,
Expand All @@ -25,25 +33,24 @@ pub struct Chrome {
}

impl Chrome {
/// Launch Chrome in headless mode and wait for the browser CDP
/// endpoint printed on stderr.
/// Launch Chrome (headful by default, headless when the launch
/// options request it) and wait for the browser CDP endpoint printed
/// on stderr.
pub fn launch(binary: PathBuf) -> anyhow::Result<Self> {
Chrome::launch_with(&ChromeLaunchOptions { binary, user_data_dir: None, ephemeral: true })
Chrome::launch_with(&ChromeLaunchOptions {
binary,
mode: BrowserMode::default(),
user_data_dir: None,
ephemeral: true,
})
}
Comment on lines +42 to 46

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Headful Default Breaks Headless Hosts

Chrome::launch now uses BrowserMode::default(), which launches without --headless=new. Existing server or CI installs that create browser panes without browser.mode and without a display can no longer get a DevTools endpoint, so pane creation waits for the launch timeout and then fails instead of using the previous hidden Chrome behavior.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deliberate, documented product default, not changing. Real headful Chrome by default is the headline feature of this PR; mux is a macOS terminal app that runs on the user’s desktop (there is a display), and signed-in sites need a real visible window to treat the pane as human. The documented opt-out is browser.mode: "headless" (covered in docs/configuration.md and docs/browser-panes.md). There is no headless "server/CI install" path that auto-creates browser panes in mux, so the previous hidden-window default is not something we are silently regressing for existing headless hosts; anyone on such a host sets browser.mode: "headless".


pub fn launch_with(options: &ChromeLaunchOptions) -> anyhow::Result<Self> {
let (profile_dir, profile_ephemeral) = profile_dir_for(options)?;
std::fs::create_dir_all(&profile_dir)?;
let mut child = Command::new(&options.binary)
.arg("--headless=new")
.arg("--remote-debugging-port=0")
.arg("--no-first-run")
.arg("--no-default-browser-check")
.arg("--disable-background-timer-throttling")
.arg("--disable-backgrounding-occluded-windows")
.arg("--disable-renderer-backgrounding")
.arg(format!("--user-data-dir={}", profile_dir.display()))
.arg("about:blank")
let mut command = Command::new(&options.binary);
command.args(chrome_args_for(&profile_dir, options.mode));
let mut child = command
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::piped())
Expand Down Expand Up @@ -133,6 +140,28 @@ fn make_profile_dir() -> anyhow::Result<PathBuf> {
Ok(dir)
}

fn chrome_args_for(profile_dir: &Path, mode: BrowserMode) -> Vec<String> {
let mut args = Vec::new();
if mode == BrowserMode::Headless {
args.push("--headless=new".to_string());
}
args.extend([
"--remote-debugging-port=0".to_string(),
"--no-first-run".to_string(),
"--no-default-browser-check".to_string(),
"--disable-background-timer-throttling".to_string(),
"--disable-backgrounding-occluded-windows".to_string(),
"--disable-renderer-backgrounding".to_string(),
"--disable-blink-features=AutomationControlled".to_string(),
format!("--user-data-dir={}", profile_dir.display()),
]);
if mode == BrowserMode::Headful {
args.push("--window-size=1280,900".to_string());
}
args.push("about:blank".to_string());
args
}

fn profile_dir_for(options: &ChromeLaunchOptions) -> anyhow::Result<(PathBuf, bool)> {
if options.ephemeral {
return Ok((make_profile_dir()?, true));
Expand Down Expand Up @@ -173,6 +202,7 @@ mod tests {

let options = ChromeLaunchOptions {
binary: PathBuf::from("chrome"),
mode: BrowserMode::Headful,
user_data_dir: Some(explicit_dir.clone()),
ephemeral: true,
};
Expand All @@ -191,6 +221,7 @@ mod tests {
std::env::temp_dir().join(format!("cmux-tui-cdp-verbatim-{}", std::process::id()));
let options = ChromeLaunchOptions {
binary: PathBuf::from("chrome"),
mode: BrowserMode::Headful,
user_data_dir: Some(explicit_dir.clone()),
ephemeral: false,
};
Expand All @@ -199,4 +230,34 @@ mod tests {
assert!(!ephemeral);
assert_eq!(selected, explicit_dir);
}

#[test]
fn headful_args_omit_headless_and_keep_stealth_throttle_profile_window() {
let profile = PathBuf::from("/tmp/cmux profile");
let args = chrome_args_for(&profile, BrowserMode::Headful);

assert!(!args.iter().any(|arg| arg == "--headless=new"));
assert!(args.iter().any(|arg| arg == "--remote-debugging-port=0"));
assert!(args.iter().any(|arg| arg == "--no-first-run"));
assert!(args.iter().any(|arg| arg == "--no-default-browser-check"));
assert!(args.iter().any(|arg| arg == "--disable-background-timer-throttling"));
assert!(args.iter().any(|arg| arg == "--disable-backgrounding-occluded-windows"));
assert!(args.iter().any(|arg| arg == "--disable-renderer-backgrounding"));
assert!(args.iter().any(|arg| arg == "--disable-blink-features=AutomationControlled"));
assert!(args.iter().any(|arg| arg == "--user-data-dir=/tmp/cmux profile"));
assert!(args.iter().any(|arg| arg == "--window-size=1280,900"));
assert_eq!(args.last().map(String::as_str), Some("about:blank"));
}

#[test]
fn headless_args_add_headless_and_omit_window_size() {
let profile = PathBuf::from("/tmp/cmux-profile");
let args = chrome_args_for(&profile, BrowserMode::Headless);

assert!(args.iter().any(|arg| arg == "--headless=new"));
assert!(args.iter().any(|arg| arg == "--disable-blink-features=AutomationControlled"));
assert!(args.iter().any(|arg| arg == "--user-data-dir=/tmp/cmux-profile"));
assert!(!args.iter().any(|arg| arg == "--window-size=1280,900"));
assert_eq!(args.last().map(String::as_str), Some("about:blank"));
}
}
28 changes: 28 additions & 0 deletions cmux-tui/crates/cmux-tui-cdp/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,15 @@ impl CdpClient {
self.call("Target.setDiscoverTargets", json!({ "discover": discover }), None).map(|_| ())
}

pub fn browser_version(&self) -> anyhow::Result<String> {
let result = self.call("Browser.getVersion", json!({}), None)?;
result
.get("userAgent")
.and_then(|value| value.as_str())
.map(str::to_string)
.ok_or_else(|| anyhow::anyhow!("Browser.getVersion response missing userAgent"))
}

pub fn create_target(&self, url: &str) -> anyhow::Result<String> {
let result = self.call("Target.createTarget", json!({ "url": url }), None)?;
result
Expand All @@ -198,10 +207,29 @@ impl CdpClient {
self.call("Target.closeTarget", json!({ "targetId": target_id }), None).map(|_| ())
}

pub fn close_target_detached(&self, target_id: &str) -> anyhow::Result<()> {
let id = self.inner.next_id.fetch_add(1, Ordering::Relaxed);
let msg = json!({
"id": id,
"method": "Target.closeTarget",
"params": { "targetId": target_id },
});
self.send_value(&msg)
}

pub fn page_enable(&self, session_id: &str) -> anyhow::Result<()> {
self.call("Page.enable", json!({}), Some(session_id)).map(|_| ())
}

pub fn set_user_agent(&self, session_id: &str, user_agent: &str) -> anyhow::Result<()> {
self.call(
"Emulation.setUserAgentOverride",
json!({ "userAgent": user_agent }),
Some(session_id),
)
.map(|_| ())
}

pub fn start_screencast(
&self,
session_id: &str,
Expand Down
2 changes: 1 addition & 1 deletion cmux-tui/crates/cmux-tui-cdp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
mod chrome;
mod client;

pub use chrome::{Chrome, ChromeLaunchOptions};
pub use chrome::{BrowserMode, Chrome, ChromeLaunchOptions};
pub use client::{
CdpClient, CdpEvent, CdpKeyEvent, NavigationEntry, NavigationHistory, ScreencastFrame,
TargetCreated, TargetInfo, discover_browser_ws_url, resolve_browser_ws_url,
Expand Down
Loading
Loading