Repository navigation
mux: non-blocking browser CDP I/O + real headful Chrome default - #7609
Conversation
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThis PR adds BrowserMode wiring across launch, config, and docs, moves browser surfaces to a bounded worker queue with timeout recovery, routes TUI browser actions through queued commands, and updates mux attachment behavior and protocol/documentation text. ChangesBrowser mode and worker-queue refactor
Estimated code review effort: 4 (Complex) | ~75 minutes Possibly related PRs
Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (1 error, 1 warning)
✅ Passed checks (23 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThis PR makes browser panes non-blocking and changes launched Chrome to a visible default. The main changes are:
Confidence Score: 5/5This looks safe to merge.
Important Files Changed
Reviews (9): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile |
| if self.is_dead() { | ||
| anyhow::bail!("browser surface is closed"); | ||
| } | ||
| *self.latest_reconfigure.lock().unwrap() = Some(command); | ||
| self.wake_worker() | ||
| } | ||
|
|
||
| fn enqueue_latest_nav(&self, command: BrowserCommand) -> anyhow::Result<()> { | ||
| if self.is_dead() { |
There was a problem hiding this comment.
Back, Forward, Reload, and Activate now use the same latest-wins slot as URL navigation. If a user double-clicks Back or sends Back then Forward while the worker is still blocked in a CDP timeout, the later command overwrites the earlier one while both callers receive success, so relative history actions silently execute fewer steps than requested.
There was a problem hiding this comment.
Fixed in this branch. back/forward/reload/activate no longer share the latest-wins nav slot; they enqueue on the per-surface bounded FIFO command channel, so a queued back can never be overwritten by a later forward. Only Navigate (URL) keeps the latest-wins slot, where just the final destination matters. Ordering between a Navigate and a channel command is preserved because Navigate’s WakeLatest marker holds its position in the FIFO. Added a deterministic regression test (queued_back_and_forward_do_not_collapse_while_worker_is_blocked): it wedges the worker inside a slow Page.navigate, queues back then forward, and asserts both Page.navigateToHistoryEntry calls fire (entry 10 then 12). Verified red→green on the AWS macOS builder: without the fix it fails at back must navigate to entry 10 (got 12); with the fix it passes.
| match tx.try_send(command) { | ||
| Ok(()) | Err(TrySendError::Full(_)) => Ok(()), | ||
| Err(TrySendError::Disconnected(_)) => anyhow::bail!("browser command worker is closed"), | ||
| } |
There was a problem hiding this comment.
enqueue_input treats a full worker queue as success for every browser input command. When the worker is blocked in a slow CDP call and the queue fills, key presses, inserted text, clicks, or wheel events are discarded even though the socket or TUI caller already received an accepted response.
There was a problem hiding this comment.
Intended bounded backpressure, not changing. The queue is a fixed 64-deep per-surface channel; a full queue only happens when the worker is wedged in a slow CDP call, at which point two timeouts mark the surface Failed("browser is not responding") and the loss is surfaced via browser-state/status. The two alternatives are both worse: an unbounded queue is exactly the unbounded-collection memory-growth the design avoids while a browser hangs, and a blocking send reintroduces the terminal freeze this PR fixes (the socket/TUI thread must never block on CDP I/O). Mouse-moves additionally coalesce. The more serious sibling case — discrete history commands overwriting each other even with one slot free — is the real bug and is fixed in this branch.
| mode: BrowserMode::default(), | ||
| user_data_dir: None, | ||
| ephemeral: true, | ||
| }) | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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".
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
mux/crates/mux-tui/src/app.rs (1)
1475-1522: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueMinor duplication across the three enqueue helpers.
enqueue_active_browser_command,enqueue_browser_command_for_pane, andenqueue_browser_commandrepeat the same "resolve surface → verifykind() == Browser→ enqueue → set/clearstatus_message" shape, differing only in how the target surface is resolved. Consider extracting a sharedenqueue_for(surface_id, surface, kind)helper that the three call into after resolving their surface differently.♻️ Sketch of a shared helper
+ fn enqueue_for(&mut self, surface_id: SurfaceId, surface: SurfaceHandle, kind: BrowserInputKind) { + if surface.kind() != SurfaceKind::Browser { + self.status_message = Some("active surface is not a browser".to_string()); + return; + } + self.browser_input.enqueue(BrowserInputEvent { surface_id, surface, kind }); + self.status_message = None; + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mux/crates/mux-tui/src/app.rs` around lines 1475 - 1522, The three browser enqueue helpers in app.rs duplicate the same resolve/validate/enqueue/status flow. Extract the shared “verify SurfaceKind::Browser, enqueue BrowserInputEvent, and update status_message” logic into a common helper, then have enqueue_active_browser_command, enqueue_browser_command_for_pane, and enqueue_browser_command call it after they each resolve the target surface in their own way. Preserve the existing error/status strings and keep browser_surface_for_pane as the pane-specific resolver.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@mux/crates/mux-core/src/browser.rs`:
- Around line 1186-1198: `enqueue_bounded` is silently discarding full-queue
failures for input that must not be lost, and `key_event`/`insert_text` are
using that path. Keep the bounded drop behavior only for disposable mouse-move
style commands, and route keyboard/text commands through a separate enqueue path
that returns an error when the queue is full. Update the call sites in
`BrowserCommand` handling for `key_event` and `insert_text` so they do not
report success when keystrokes or pasted text are dropped.
- Around line 690-720: The FIFO ordering in start_browser_worker is being broken
because take_latest_worker_commands is drained after every batch, letting
latest-slot navigation commands run ahead of older queued browser controls.
Update the worker loop in start_browser_worker so latest_reconfigure/latest_nav
are only flushed when a BrowserCommand::WakeLatest is processed in its FIFO
position, or otherwise preserve sequencing so Back/Forward/Reload cannot be
overtaken by later Navigate commands. Keep the fix localized to the
batching/draining logic around take_latest_worker_commands and
run_browser_worker_command.
- Around line 803-808: Recovery state is split between worker success handling
in the match on result, store_frame, and the timeout path, so not-responding
recovery can get out of sync. Centralize the recovery reset logic in a shared
helper or introduce a shared recovery epoch used by both the worker command flow
and store_frame, and make sure it clears both consecutive_timeouts and
not_responding_reported together. Update the success path in the worker handler,
the frame-recovery path in store_frame, and the timeout-triggering logic so they
all consult the same recovery state.
In `@mux/docs/configuration.md`:
- Around line 57-58: Update the documentation for browser.capture_scale to match
the runtime validation in the config docs: the accepted range is strictly
greater than 0 and up to 1, not 0.0 through 1.0. Adjust the description near
browser.max_capture_megapixels in configuration.md so it clearly states the
lower bound is exclusive and avoids implying 0.0 is valid.
In `@mux/docs/protocol.md`:
- Around line 113-114: The protocol wording in the browser-queue sentence is
conflating separate browser commands with the `resize-surface` flow. Reword the
sentence around the browser CDP queue behavior so it clearly refers only to
`resize-surface` enqueuing per-surface work and returning `ok:true`, while
keeping browser input, navigation, activation, and reconfigure as separate verbs
handled elsewhere; use the existing browser state/status event language to
describe completion and failure.
---
Outside diff comments:
In `@mux/crates/mux-tui/src/app.rs`:
- Around line 1475-1522: The three browser enqueue helpers in app.rs duplicate
the same resolve/validate/enqueue/status flow. Extract the shared “verify
SurfaceKind::Browser, enqueue BrowserInputEvent, and update status_message”
logic into a common helper, then have enqueue_active_browser_command,
enqueue_browser_command_for_pane, and enqueue_browser_command call it after they
each resolve the target surface in their own way. Preserve the existing
error/status strings and keep browser_surface_for_pane as the pane-specific
resolver.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 378b4707-5321-4bcc-9fda-97a72096d7cd
📒 Files selected for processing (15)
mux/README.mdmux/crates/mux-cdp/src/chrome.rsmux/crates/mux-cdp/src/client.rsmux/crates/mux-cdp/src/lib.rsmux/crates/mux-core/src/browser.rsmux/crates/mux-core/src/lib.rsmux/crates/mux-core/src/mux.rsmux/crates/mux-core/src/surface.rsmux/crates/mux-core/tests/browser_runtime.rsmux/crates/mux-tui/src/app.rsmux/crates/mux-tui/src/browser_input.rsmux/crates/mux-tui/src/config.rsmux/docs/browser-panes.mdmux/docs/configuration.mdmux/docs/protocol.md
| fn start_browser_worker( | ||
| surface: Arc<Surface>, | ||
| rx: Receiver<BrowserCommand>, | ||
| latest_reconfigure: Arc<Mutex<Option<BrowserCommand>>>, | ||
| latest_nav: Arc<Mutex<Option<BrowserCommand>>>, | ||
| mux: Weak<Mux>, | ||
| done_tx: Option<Sender<()>>, | ||
| ) { | ||
| let id = surface.id; | ||
| let _ = | ||
| std::thread::Builder::new().name(format!("browser-surface-{id}-worker")).spawn(move || { | ||
| let mut failures = BrowserWorkerErrorState::default(); | ||
| while let Ok(first) = rx.recv() { | ||
| let mut batch = vec![first]; | ||
| while let Ok(next) = rx.try_recv() { | ||
| batch.push(next); | ||
| } | ||
| coalesce_worker_mouse_moves(&mut batch); | ||
| for command in batch { | ||
| if matches!(command, BrowserCommand::WakeLatest) { | ||
| for command in take_latest_worker_commands(&latest_reconfigure, &latest_nav) | ||
| { | ||
| run_browser_worker_command(&surface, command, &mux, id, &mut failures); | ||
| } | ||
| } else { | ||
| run_browser_worker_command(&surface, command, &mux, id, &mut failures); | ||
| } | ||
| } | ||
| for command in take_latest_worker_commands(&latest_reconfigure, &latest_nav) { | ||
| run_browser_worker_command(&surface, command, &mux, id, &mut failures); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
Preserve FIFO ordering when draining latest-slot commands.
Line 718 drains latest_nav/latest_reconfigure after every batch, even before FIFO commands that arrived while the worker was executing the current batch. That lets a later Navigate leapfrog an earlier queued Back/Forward/Reload, breaking the ordering contract for browser actions. Drain latest slots only when a WakeLatest marker reaches its FIFO position, or add sequencing so latest-wins commands cannot bypass older queued controls.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@mux/crates/mux-core/src/browser.rs` around lines 690 - 720, The FIFO ordering
in start_browser_worker is being broken because take_latest_worker_commands is
drained after every batch, letting latest-slot navigation commands run ahead of
older queued browser controls. Update the worker loop in start_browser_worker so
latest_reconfigure/latest_nav are only flushed when a BrowserCommand::WakeLatest
is processed in its FIFO position, or otherwise preserve sequencing so
Back/Forward/Reload cannot be overtaken by later Navigate commands. Keep the fix
localized to the batching/draining logic around take_latest_worker_commands and
run_browser_worker_command.
| // Bounded, in-order delivery for disposable pointer/key input. Input events | ||
| // are high-frequency and individually expendable, so under backpressure the | ||
| // worker queue drops the newest event rather than blocking or replacing an | ||
| // unrelated queued one. Callers are intentionally told `ok` even on drop: | ||
| // losing one mouse-move or keystroke frame is not a reported failure. | ||
| fn enqueue_bounded(&self, command: BrowserCommand) -> anyhow::Result<()> { | ||
| if self.is_dead() { | ||
| anyhow::bail!("browser surface is closed"); | ||
| } | ||
| let tx = self.command_sender()?; | ||
| match tx.try_send(command) { | ||
| Ok(()) | Err(TrySendError::Full(_)) => Ok(()), | ||
| Err(TrySendError::Disconnected(_)) => anyhow::bail!("browser command worker is closed"), |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Do not silently drop keyboard/text input.
enqueue_bounded returns Ok(()) on a full queue, and key_event/insert_text use that path. Dropping mouse moves is acceptable, but dropping keystrokes or pasted text while reporting success loses user input. Route non-disposable text/key commands through an error-on-full enqueue path.
Proposed direction
+ fn enqueue_required_input(&self, command: BrowserCommand) -> anyhow::Result<()> {
+ if self.is_dead() {
+ anyhow::bail!("browser surface is closed");
+ }
+ let tx = self.command_sender()?;
+ match tx.try_send(command) {
+ Ok(()) => Ok(()),
+ Err(TrySendError::Full(_)) => {
+ anyhow::bail!("browser command queue is full; browser may be unresponsive")
+ }
+ Err(TrySendError::Disconnected(_)) => anyhow::bail!("browser command worker is closed"),
+ }
+ }
+
pub fn key_event(
&self,
event_type: &str,
key: &str,
@@
- self.enqueue_bounded(BrowserCommand::Key {
+ self.enqueue_required_input(BrowserCommand::Key {
event_type: event_type.to_string(),
key: key.to_string(),
code: code.to_string(),
@@
pub fn insert_text(&self, text: &str) -> anyhow::Result<()> {
- self.enqueue_bounded(BrowserCommand::InsertText(text.to_string()))
+ self.enqueue_required_input(BrowserCommand::InsertText(text.to_string()))
}Also applies to: 1336-1365
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@mux/crates/mux-core/src/browser.rs` around lines 1186 - 1198,
`enqueue_bounded` is silently discarding full-queue failures for input that must
not be lost, and `key_event`/`insert_text` are using that path. Keep the bounded
drop behavior only for disposable mouse-move style commands, and route
keyboard/text commands through a separate enqueue path that returns an error
when the queue is full. Update the call sites in `BrowserCommand` handling for
`key_event` and `insert_text` so they do not report success when keystrokes or
pasted text are dropped.
| | `browser.max_capture_megapixels` | number | `2.0` | Maximum browser capture size before downscaling | | ||
| | `browser.capture_scale` | number or null | `null` | Fixed capture scale from 0.0 through 1.0 | |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Correct the browser.capture_scale lower bound.
The runtime accepts only 0 < scale <= 1, so documenting 0.0 through 1.0 is inaccurate and will mislead users. A literal 0.0 config will be rejected at load time.
♻️ Proposed doc fix
-| `browser.capture_scale` | number or null | `null` | Fixed capture scale from 0.0 through 1.0 |
+| `browser.capture_scale` | number or null | `null` | Fixed capture scale from greater than 0.0 through 1.0 |📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| | `browser.max_capture_megapixels` | number | `2.0` | Maximum browser capture size before downscaling | | |
| | `browser.capture_scale` | number or null | `null` | Fixed capture scale from 0.0 through 1.0 | | |
| | `browser.max_capture_megapixels` | number | `2.0` | Maximum browser capture size before downscaling | | |
| | `browser.capture_scale` | number or null | `null` | Fixed capture scale from greater than 0.0 through 1.0 | |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@mux/docs/configuration.md` around lines 57 - 58, Update the documentation for
browser.capture_scale to match the runtime validation in the config docs: the
accepted range is strictly greater than 0 and up to 1, not 0.0 through 1.0.
Adjust the description near browser.max_capture_megapixels in configuration.md
so it clearly states the lower bound is exclusive and avoids implying 0.0 is
valid.
0ff130a to
c31b191
Compare
#7622) The change-area detector treats any path not explicitly macos-neutral as a macOS change, so mux-only PRs resolved macos=true. Combined with the new linux-preflight staging (#7583), that made the required app-host Swift tests skip while the routing guard required them, failing 'tests' and 'ci-status' on every mux-only PR (e.g. #7609). cmux-mux is a standalone Rust project gated by its own 'mux' workflow and never affects the macOS app build or app-host tests, so 'mux/' belongs in is_macos_neutral. Adds test_mux_only_skips_macos.
Two browser-pane improvements on top of the CDP base on main.
Anti-lag: browser CDP commands never block the socket or the TUI event
loop. Each browser surface owns a per-surface worker thread with a
bounded queue; server browser-* handlers and the resize CDP reconfigure
enqueue and ack immediately (accepted, not completed). Discrete history
commands (back/forward/reload/activate) and input go through the bounded
FIFO so a queued Back is never overwritten by a later Forward; only URL
navigation uses the latest-wins slot. Two consecutive CDP timeouts mark
only that surface Failed('browser is not responding'), once per stall
episode, re-armed by a fresh frame; other panes stay live. No mutex held
across a blocking call; worker reaped on kill and the pane-missing path;
wedged-pane close is fire-and-forget.
Real Chrome: browser.mode defaults to headful, launching the user's real
Google Chrome in a visible window with a persistent profile so signed-in
sites treat the pane as a human. --disable-blink-features=
AutomationControlled plus a launched-only best-effort UA de-headless
clear the two loudest bot signals. browser.mode: headless opts back into
a hidden window. Docs cover the Chrome 136 / SingletonLock real-profile
caveats and agent-browser ws:// attach.
Verified on the AWS M4 Pro builder (macOS 15.7.4): fmt, clippy, cargo
test --workspace all green; mux-core --test-threads=64 clean x3. This
Mac's macOS 26 SDK cannot link ghostty-vt-sys locally.
c31b191 to
8e2d4b9
Compare
…ovements # Conflicts: # cmux-tui/crates/cmux-tui-cdp/src/chrome.rs # cmux-tui/crates/cmux-tui-core/src/browser.rs # cmux-tui/crates/cmux-tui/src/app.rs # cmux-tui/crates/cmux-tui/src/browser_input.rs # cmux-tui/crates/cmux-tui/src/config.rs # cmux-tui/docs/browser-panes.md # cmux-tui/docs/configuration.md
|
Caution Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted. Error details |
The merge commit accidentally re-staged the worktree's stale ghostty checkout over main's submodule bump.
|
Caution Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted. Error details |
|
Caution Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted. Error details |
|
Caution Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted. Error details |
…ovements # Conflicts: # vendor/bonsplit
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 20c4fba. Configure here.
| self.dead.store(true, Ordering::Release); | ||
| self.close_taps(); | ||
| let _ = self.session.lock().unwrap().take(); | ||
| self.close_command_sender(); |
There was a problem hiding this comment.
Stale not-responding latch
Medium Severity
After a browser is not responding episode, not_responding_reported is cleared only when a fresh screencast frame arrives. Successful clear_error (back/forward/reload) or set_url_title (successful navigate) can return the pane to Live without resetting that latch, so later CDP timeouts may no longer promote the surface to failed or broadcast recovery to attach clients.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 20c4fba. Configure here.


Two browser-pane improvements on top of the CDP base already on main.
Anti-lag: browser CDP I/O can no longer freeze the terminal
Before: the server processed each connection's commands serially and browser-* handlers did their CDP call inline (up to the call timeout), so one wedged/slow Chrome dammed the socket that also carries terminal input for attach clients. Omnibar/nav actions blocked the TUI event loop the same way.
Now, three invariants:
Failed("browser is not responding"), once per stall episode, re-armed when a fresh frame clears it. Other panes stay live.Worker machinery: per-kind latest-wins slots (resize vs nav, no cross-kind clobber), no mutex held across a blocking call, worker reaped on kill and on the pane-missing adoption path, wedged-pane close is fire-and-forget (
close_target_detached). Regression test drives a Chrome wedged after bootstrap and asserts a second navigate + a resize + list-workspaces all ack under 500ms on the same socket, plus a non-blocking close.Real headful Chrome by default
browser.modedefaults toheadful, launching the user's real Google Chrome in a visible window with a persistent profile, so signed-in sites treat the pane as a human.--disable-blink-features=AutomationControlledplus a launched-only best-effort UA de-headless (Browser.getVersioncached once,HeadlessChrome→Chrome,setUserAgentOverridebeforePage.enable, never fails a surface, external browsers untouched) clear the two loudest bot signals.browser.mode: "headless"opts back into a hidden window. Docs cover the Chrome 136 / SingletonLock real-profile caveats and agent-browserws://attach (no wss overclaim).Verification
Built and tested on the AWS M4 Pro builder (macOS 15.7.4) because this dev Mac's macOS 26 SDK can't link
ghostty-vt-sys:cargo fmt --check,cargo clippy --workspace --all-targets -- -D warnings,cargo test --workspaceall green;cargo test -p mux-core --test-threads=64clean ×3 (does not reintroduce the PTY-exhaustion CI failure just fixed on main).smoke-tui.pypasses;smoke-attach.pyhits the known themed-prompt false positive that also fails on plain main (CI is its gate).16 files, all under
mux/. No new dependencies.🤖 Generated with Claude Code
Need help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.Note
Medium Risk
Large concurrency and lifecycle changes across browser, mux socket, and attach paths; behavior is heavily regression-tested but wedged-Chrome and queue semantics affect all browser users.
Overview
Browser panes no longer run blocking CDP on the TUI event loop or the mux control socket. Each surface gets a worker thread with a bounded queue: pointer/key input may drop under load, URL nav and resize use latest-wins slots, and back/forward/reload/activate stay FIFO with explicit
ok:falsewhen the queue is full. Handlers ack immediately (ok:truemeans accepted); failures show up via status events and per-surface “browser is not responding” after two CDP timeouts, with recovery when frames return (including attach client title/state broadcast).Headful Chrome is the default (
browser.mode: "headful"): visible window, session-scoped persistent profile,--disable-blink-features=AutomationControlled, and a one-time launched-runtime UA tweak (HeadlessChrome→Chrome).headlessopts back in. CDP gainsBrowser.getVersion,set_user_agent, and fire-and-forgetclose_target_detached.The TUI routes omnibar and browser shortcuts through the off-loop browser input dispatcher, surfacing “browser is busy” when the outer queue is full. Docs/config/protocol are updated for mode, queue semantics,
browser.discoverdefaultfalse, profile/Chrome 136 caveats, and browser attach streaming over protocol v6.Reviewed by Cursor Bugbot for commit 20c4fba. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by cubic
Moves all blocking browser CDP work off the UI/socket path and makes headful Chrome with a persistent profile the default, improving responsiveness and realism. Attach now streams browser panes, and protocol/browser commands return immediate acks with clearer backpressure and recovery.
New Features
ok:true= accepted, not completed). Discrete controls (back/forward/reload/activate) use FIFO and returnok:falsewhen the queue is full; high‑rate pointer/key input may drop to keep the UI non‑blocking.close_target_detached.browser.mode: "headful"with a visible window and persistent profile; launched runtimes add--disable-blink-features=AutomationControlledand a one‑time UA de‑headless (Browser.getVersion,HeadlessChrome→Chrome; external browsers untouched).browser.mode: "headless"opts back in.browser-*input and control commands that enqueue work and return immediate acks;attach-surfacestreams both PTY and browser panes. Docs cover session‑scoped profiles, Chrome 136 profile lock caveats,browser.discoverdefaulting tofalse, andws:///http://CDP endpoints (nowss://).Bug Fixes
Written for commit 20c4fba. Summary will update on new commits.
Summary by CodeRabbit
browser.mode(headful/headless) with session-scoped profiles and improved Chrome/CDP attachment guidance.