Skip to content

browser inputs: dispatch pointerenter non-bubbling to match mouseenter and spec - #5958

Merged
lawrencecchen merged 1 commit into
mainfrom
feat-cli-browser-actions
Jun 12, 2026
Merged

lawrencecchen merged 1 commit into
mainfrom
feat-cli-browser-actions

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jun 12, 2026 •

Copy link
Copy Markdown
Contributor

The browser view CLI actions this branch was named for (react-grab, devtools, console, focus-mode, zoom, history) already landed on main via #5766, with follow-up refinements in #5870 and #5778. This PR is now reduced to the one fix from that work that was not yet on main.

PointerEvent pointerenter (like mouseenter) must not bubble. __cmuxMouse already honored a bubbles flag and dispatched mouseenter non-bubbling, but __cmuxPointer hardcoded bubbles:true, so pointerenter bubbled. That fires spurious enter signals on ancestor elements and can corrupt hover state (hover menus, pointer tracking) in nested components when __cmuxClick/__cmuxHover run. Fix adds a bubbles parameter to __cmuxPointer mirroring __cmuxMouse and passes false at the two pointerenter call sites. pointerover/move/down/up keep the default bubbles:true, which is correct per spec.

🤖 Generated with Claude Code


Note

Low Risk
Small change to injected JS input simulation in TerminalController; no auth, data, or routing impact.

Overview
Fixes synthetic click and hover gestures in injected browserInputHelpers so pointerenter matches real DOM behavior and existing mouseenter handling.

__cmuxPointer now accepts an optional bubbles flag (default on). pointerenter is dispatched with bubbles: false in __cmuxClick and __cmuxHover, aligning the pointer + mouse sequence frameworks expect for enter events.

Reviewed by Cursor Bugbot for commit 4ecc5a7. Bugbot is set up for automated code reviews on this repo. Configure here.

@vercel

vercel Bot commented Jun 12, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jun 12, 2026 7:10am
cmux-staging Ready Ready Preview, Comment Jun 12, 2026 7:10am

@coderabbitai

coderabbitai Bot commented Jun 12, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR expands browser automation: adds CLI routing and helpers for new browser verbs (react-grab, devtools, focus-mode, zoom, history), extends TabManager with react-grab surface override API, centralizes injected JS input helpers and keyboard/text event handling, implements browser-action utilities and handlers, and updates docs.

Changes

Browser Command Automation Expansion

Layer / File(s) Summary
CLI browser command routing and parameters
CLI/cmux.swift
Extends verb allowlist with new browser subcommands and adds helpers to resolve caller-provided --workspace/--window (CMUX_WORKSPACE_ID fallback). Implements handlers for react-grab (toggle + optional --return-to validation), devtools (toggle/console), focus-mode (enter/exit/toggle/on/off), zoom (in/out/reset), and history (clear requiring --force/--yes). Updates CLI help text.
TabManager React Grab API expansion
Sources/TabManager.swift
Adds public toggleReactGrab(in:browserSurfaceId:returnTerminalSurfaceId:) -> UUID? accepting explicit surface overrides and validating types; updates toggleReactGrabFromCurrentFocus() to delegate to the new API and uses resolved returnTerminalPanelId in async handling.
TerminalController browser action dispatch wiring
Sources/TerminalController.swift
Adds CLI/action dispatcher cases for browser.react_grab.toggle, browser.devtools.toggle, browser.console.show, browser.focus_mode.set, browser.zoom.set, and browser.history.clear. Extends the allowed action-name list.
Browser input synthesis via JavaScript helpers
Sources/TerminalController.swift
Introduces shared browserInputHelpers with __cmuxClick, __cmuxHover, __cmuxSetChecked, and __cmuxKey. Refactors selector actions (click/dblclick/hover) and checkbox/radio handling to use these helpers and verify resulting states.
Text input and keyboard improvements
Sources/TerminalController.swift
Dispatches cancelable beforeinput (insertText/insertReplacementText) and returns input_rejected when canceled; dispatches input/change with inputType and fallback. Reworks keyboard injection to use __cmuxKey, tightens Enter implicit-submit conditions, and updates keydown/keyup sub-actions.
Browser action implementations and utilities
Sources/TerminalController.swift
Adds helpers to resolve target browser surface (surface_id precedence), build standardized payloads, and reject unresolved explicit handles. Implements handlers for new browser actions; history.clear requires force=true and clears only default profile history.
Browser automation documentation
web/app/[locale]/docs/browser-automation/page.tsx
Updates command index lists to include react-grab, devtools, focus-mode, zoom, and history. Extends navigation example to demonstrate new commands.

Sequence Diagram(s)

sequenceDiagram
  participant CLI as CLI Parser
  participant RouteHelpers as Route Helpers
  participant TerminalController as TerminalController
  participant TabManager as TabManager
  participant BrowserJS as Browser JS
  CLI->>RouteHelpers: extract verb, workspace, surface from args
  RouteHelpers->>RouteHelpers: resolve workspace/window via fallback
  RouteHelpers->>TerminalController: dispatch browser action
  TerminalController->>TabManager: toggleReactGrab(surface overrides)
  TabManager->>BrowserJS: trigger React Grab pasteback
  BrowserJS->>BrowserJS: return terminal panel ID
  TerminalController->>BrowserJS: dispatch keyboard/mouse via __cmuxKey/__cmuxClick
  BrowserJS->>BrowserJS: synthesize event sequence and honor beforeinput cancellation
Loading
sequenceDiagram
  participant Action as Browser Action
  participant Util as Resolution Util
  participant Surface as Browser Surface
  participant Store as History Store
  Action->>Util: resolve browser surface (surface_id precedence)
  alt surface_id provided
    Util->>Surface: use explicit surface
  else no explicit surface
    Util->>Surface: use focused or sole browser
  end
  alt history.clear action
    Action->>Store: clear default profile history (force=true required)
  end
  Util->>Action: return standardized payload
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • manaflow-ai/cmux#5766: Overlapping CLI and TerminalController changes for browser view actions and TabManager react-grab surface override logic.
  • manaflow-ai/cmux#5870: Overlaps on react-grab/--return-to routing scoping and cancelable beforeinput handling.
  • manaflow-ai/cmux#4573: Introduces browser focus-mode state/toggling APIs that this PR's focus-mode automation dispatches to.

Poem

🐰 A browser now dances to CLI command,
With JavaScript helpers that understand,
React Grab, DevTools, zoom, and focus mode,
Events flow true on every code!
History clears, keyboards take flight—input feels right!


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (4 errors, 3 warnings)

Check name Status Explanation Resolution
Cmux Algorithmic Complexity ❌ Error FAIL: CLI/cmux.swift validateSurfaceHandleInWindow (lines ~5925-5958) lists workspaces in a window and for each rescans surfaces via surface.list; O(workspaces*surfaces) with no bound/benchmark. Refactor to avoid per-workspace surface rescans: call a single surface.list scoped by window_id (or build an indexed cache/mapping) and match the handle in one pass; add benchmark if kept.
Cmux Swift Concurrency ❌ Error PR introduces fire-and-forget Task { @mainactor ... } in Sources/TabManager.swift (performReactGrabToggle), violating swift-concurrency-modernization. Refactor to avoid fire-and-forget: make the caller async and await, or store the Task and cancel/track its lifecycle from the owning operation.
Cmux Swift File And Package Boundaries ❌ Error TerminalController.swift (~22.4k LOC) mixes socket/browser-protocol + JS/input parsing + AppKit/WebKit; browserInputHelpers exists only in app Sources (no SwiftPM boundary). Extract browser-action/JS-input/domain dispatch into a small SwiftPM package target (no AppKit/WebKit), keep TerminalController/CLI as routing glue, and reduce responsibility mixing in the oversized files per the boundary rule.
Cmux Full Internationalization ❌ Error CLI/cmux.swift throws CLIError with raw user-facing English literals (e.g., “Invalid surface handle”, “browser history clear permanently deletes…”) and they’re not in Resources/Localizable.xcstring... Localize each new CLI-visible string using String(localized:defaultValue:) with stable keys, then add translated entries in Resources/Localizable.xcstrings (and InfoPlist if affected) for every locale in web/i18n/routing.ts.
Docstring Coverage ⚠️ Warning Docstring coverage is 22.58% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ⚠️ Warning The PR title focuses narrowly on a single fix (pointerenter non-bubbling), but the changeset implements major new CLI browser actions (react-grab, devtools, focus-mode, zoom, history) with substantial routing, validation, and input-event corrections across multiple files. Update title to reflect the primary change: browser view CLI actions (react-grab, devtools, focus-mode, zoom, history) and input-event fixes, or narrow the scope to match the actual title.
Description check ⚠️ Warning PR description is largely incomplete and fails to align with the substantial changes in the raw summary (141+ lines in CLI, 59+ in TabManager, 346+ in TerminalController). Expand the description to cover all major changes: new CLI verbs (react-grab, devtools, focus-mode, zoom, history), TabManager method additions, TerminalController v2 action dispatch, and input-event correctness fixes. Include testing evidence and demo video as required by template.
✅ Passed checks (14 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed Inspected updated Swift: TabManager and TerminalController are @MainActor; new browser action handlers call main-actor work via v2MainSync and don’t add nonisolated UI/state or new shared mutable S...
Cmux Swift Blocking Runtime ✅ Passed In PR #5958 Swift diffs, no DispatchSemaphore/wait(), sleep/Task.sleep, DispatchQueue.asyncAfter/timers, DispatchQueue.main.sync, NSLock/pthread, or polling were found; added code uses Task{ @MainA...
Cmux Expensive Synchronous Load ✅ Passed TabManager.swift’s only RestorableAgentSessionIndex.load fallback is guarded by SharedLiveAgentIndex.shared (currentIndexSchedulingRefresh() ?? …) in closeWorkspace; CLI/cmux.swift and TerminalCont...
Cmux Cache Substitution Correctness ✅ Passed browser.history.clear calls BrowserHistoryStore.shared.clearHistory(), which loads persisted history (loadIfNeeded) and invalidates suggestion cache; panel_snapshot cache is in-memory only (no pers...
Cmux No Hacky Sleeps ✅ Passed PR changes only Swift + .tsv + one TSX docs page; searched PR diff for sleep/setTimeout/setInterval/timers/polling—none found, so no hacky sleeps in non-Swift runtime code.
Cmux Swift @Concurrent ✅ Passed In CLI/cmux.swift, Sources/TabManager.swift, and Sources/TerminalController.swift there are 0 @concurrent and 0 “nonisolated … async” signatures; async UI work is done inside Task { @MainActor } cl...
Cmux Swift Logging ✅ Passed Checked PR diff snippets for CLI/cmux.swift + TabManager.swift: no new print/debugPrint/dump/NSLog or stdout/stderr logging; TabManager logging added only via #if DEBUG cmuxDebugLog.
Cmux User-Facing Error Privacy ✅ Passed Scans of browser-related CLIError strings and TerminalController v2Browser* error responses show no forbidden env var/provider/token/header/secret/vendor details in user-facing text.
Cmux Swiftui State Layout ✅ Passed PR touches only CLI/cmux.swift, Sources/TabManager.swift, Sources/TerminalController.swift, and docs; SwiftUI markers (@Observable/@Published/GeometryReader/List/import SwiftUI) were not found in t...
Cmux Architecture Rethink ✅ Passed Inspected new Swift routing + v2 browser handlers (CLI/cmux.swift, Sources/TabManager.swift, Sources/TerminalController.swift) for the rethink-rule patterns (sleep/asyncAfter/polling/locks/observer...
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR #5958 only updates CLI routing and v2 browser actions (CLI/cmux.swift, Sources/TabManager.swift, Sources/TerminalController.swift, docs); diff search shows no NSWindow/NSPanel/WindowGroup or cmu...
Cmux Source Artifacts ✅ Passed PR #5958 changed only .github/swift-file-length-budget.tsv, CLI/cmux.swift, Sources/TabManager.swift, Sources/TerminalController.swift, and web/app/[locale]/docs/browser-automation/page.tsx—none ar...
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-cli-browser-actions

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread CLI/cmux.swift
Comment thread CLI/cmux.swift
Comment thread Sources/TerminalController.swift Outdated
for key in keys where v2HasNonNullParam(params, key) && v2UUID(params, key) == nil {
return .err(code: "invalid_params", message: "Unresolved \(key)", data: nil)
}
return nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale workspace UUID not rejected

Medium Severity

New v2RejectUnresolvedHandles treats workspace_id as resolved whenever v2UUID parses a UUID string, without checking that workspace exists in the target window. A stale but syntactically valid workspace_id yields a generic not-found instead of an explicit unresolved-handle error.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d66f0fc. Configure here.

@greptile-apps

greptile-apps Bot commented Jun 12, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This commit is a single-focus correctness fix: __cmuxPointer gains a bubbles parameter (defaulting to true via bubbles===false?false:true), and the two pointerenter call sites in __cmuxClick and __cmuxHover now pass false, matching spec behavior and the already-correct mouseenter treatment.

  • pointerenter is now dispatched with bubbles: false in both __cmuxClick and __cmuxHover, eliminating spurious hover-enter signals propagating to ancestor listeners (matching the W3C Pointer Events spec).
  • All other pointer event types (pointerover, pointermove, pointerdown, pointerup) correctly retain bubbles: true since the fifth argument is omitted and the ternary defaults to true.

Confidence Score: 5/5

This is a targeted one-line correctness fix; no regressions are expected and existing callers correctly receive the old default behavior.

The change adds a bubbles parameter with a safe default (true) to __cmuxPointer and passes false only for pointerenter, which does not bubble per the W3C spec. All other pointer event dispatches are unaffected. The fix mirrors the already-correct mouseenter path and eliminates the spurious ancestor notifications that the old code produced.

No files require special attention; the change is confined to a single JavaScript helper string inside Sources/TerminalController.swift.

Important Files Changed

Filename Overview
Sources/TerminalController.swift Six-line change: adds a bubbles parameter to __cmuxPointer and passes false for the two pointerenter call sites, correctly matching the non-bubbling spec behavior that was already applied to the mouseenter counterpart.

Sequence Diagram

sequenceDiagram
    participant Caller as cmuxClick/cmuxHover
    participant P as __cmuxPointer
    participant DOM as DOM Element

    Caller->>P: "pointerover (bubbles default=true)"
    P->>DOM: dispatchEvent(pointerover, bubbles:true)
    Caller->>P: "pointerenter (bubbles=false) FIXED"
    P->>DOM: dispatchEvent(pointerenter, bubbles:false)
    Note over DOM: Only target receives event, ancestors skipped
    Caller->>P: "pointermove (bubbles default=true)"
    P->>DOM: dispatchEvent(pointermove, bubbles:true)
Loading

Reviews (2): Last reviewed commit: "browser inputs: dispatch pointerenter no..." | Re-trigger Greptile

Comment thread Sources/TerminalController.swift Outdated
Comment on lines 12486 to 12496
v2BrowserSelectorAction(params: params, actionName: "click") { selectorLiteral in
"""
(() => {
\(Self.browserInputHelpers)
const el = document.querySelector(\(selectorLiteral));
if (!el) return { ok: false, error: 'not_found' };
if (el.disabled) return { ok: false, error: 'disabled' };
el.scrollIntoView({ block: 'nearest', inline: 'nearest' });
if (typeof el.click === 'function') {
el.click();
} else {
el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window, detail: 1 }));
}
__cmuxClick(el);
return { ok: true };
})()

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 pointerenter incorrectly bubbles

__cmuxPointer hardcodes bubbles: true for all pointer events, including pointerenter. Unlike pointermove/pointerdown, pointerenter does not bubble in the spec — and this is already handled correctly for its mouse counterpart (__cmuxMouse(el, 'mouseenter', c, 0, 0, false)). When pointerenter bubbles, every ancestor that has a pointerenter listener receives a spurious hover-enter signal, which can open parent menus, trigger parent tooltip logic, or corrupt hover state in React/Vue pointer-tracking components. The fix is to pass bubbles: false when dispatching pointerenter (and pointerleave, if added later).

Comment thread Sources/TerminalController.swift Outdated
/// vanilla handlers all fire. Define them once at the top of an injected snippet, then call
/// `__cmuxClick(el)`, `__cmuxHover(el)`, `__cmuxSetChecked(el, desired)`, and `__cmuxKey(t,type,key)`.
private static let browserInputHelpers = """
function __cmuxCenter(el){const r=el.getBoundingClientRect();return {x:Math.floor(r.left+Math.min(r.width,r.width/2)),y:Math.floor(r.top+Math.min(r.height,r.height/2))};}

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.

P2 Math.min(r.width, r.width/2) is always r.width/2

getBoundingClientRect() always returns non-negative width/height, so r.width/2 ≤ r.width is always true and Math.min adds no semantic value. The simpler form is clearer and avoids a head-scratch moment for any reader trying to understand why there's a min here.

Suggested change
function __cmuxCenter(el){const r=el.getBoundingClientRect();return {x:Math.floor(r.left+Math.min(r.width,r.width/2)),y:Math.floor(r.top+Math.min(r.height,r.height/2))};}
function __cmuxCenter(el){const r=el.getBoundingClientRect();return {x:Math.floor(r.left+r.width/2),y:Math.floor(r.top+r.height/2)};}

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit c86dba6. Configure here.

Comment thread Sources/TerminalController.swift Outdated
__cmuxPointer(el,'pointerdown',c,1);__cmuxMouse(el,'mousedown',c,1,1);
if(typeof el.focus==='function'){try{el.focus({preventScroll:true});}catch(e){try{el.focus();}catch(e2){}}}
__cmuxPointer(el,'pointerup',c,0);__cmuxMouse(el,'mouseup',c,0,1);
if(typeof el.click==='function'){el.click();}else{__cmuxMouse(el,'click',c,0,1);}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Click helper fires events twice

High Severity

The new __cmuxClick helper dispatches a full synthetic pointer and mouse down/up sequence and then calls el.click(). In WebKit that typically runs another activation, so browser click (and dblclick, which calls it twice) can invoke listeners twice and leave toggle controls back in their original state.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c86dba6. Configure here.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 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 `@CLI/cmux.swift`:
- Line 32773: Update the usage/help string that currently reads "browser
focus-mode enter|exit|toggle [--surface <id>]" to also advertise the accepted
aliases "on" and "off" (e.g., "browser focus-mode enter|exit|toggle|on|off
[--surface <id>]") so users see all supported modes; locate the command/usage
definition in CLI/cmux.swift (the focus-mode usage string) and modify that
literal so the help output includes the on/off aliases.
- Around line 11827-11835: The history-clear code path currently ignores routing
flags; before calling client.sendV2("browser.history.clear", ...), detect any
supplied routing flags (e.g., --surface, --workspace, --window via hasFlag and
browserActionVerbArgs()) and either (preferred) pass them through the existing
handle-validation/resolution helper (e.g., resolve/validate routing handles) and
throw a CLIError if any supplied handle cannot be resolved, or (fallback) reject
the command immediately when any of those flags are present by throwing a
CLIError explaining they are unsupported for this destructive action; ensure the
check happens before the hasFlag(--force) guard and before client.sendV2 is
invoked.

In `@Sources/TabManager.swift`:
- Around line 6280-6292: The logic that computes returnTerminalPanelId
incorrectly clears a route-derived return when browserSurfaceId is provided;
change the branching in the block that sets returnTerminalPanelId so that: if
returnTerminalSurfaceId is non-nil use it (as now); else if
route?.returnTerminalPanelId is non-nil and workspace.panels[thatId]?.panelType
== .terminal then use the route-derived ID (independent of browserSurfaceId);
otherwise if browserSurfaceId is nil fall back to nil; ensure you reference
returnTerminalSurfaceId, browserSurfaceId, route?.returnTerminalPanelId and
workspace.panels when making the checks so the route-derived terminal is
preserved unless explicitly overridden by --return-to or is invalid.

In `@Sources/TerminalController.swift`:
- Around line 13281-13290: v2RejectUnresolvedHandles currently only checks that
a param parses to a UUID via v2UUID; update it to also verify the UUID actually
resolves to the correct resource and context (surface, return_to type,
workspace, window) so supplied-but-wrong-workspace/type handles are rejected.
For keys like "surface_id", "return_to", "workspace_id", "window_id" call the
appropriate resolver (e.g.
v2ResolveSurface/v2ResolveReturnTo/v2ResolveWorkspace/v2ResolveWindow or the
central handle-resolver used elsewhere) instead of just v2UUID, and if
resolution fails or the resolved object is of the wrong type/owner return
.err(code: "invalid_params", message: "Unresolved <key>", data: nil); keep the
existing presence check using v2HasNonNullParam and reuse v2UUID only to obtain
the candidate id before resolution.
- Around line 12646-12660: The textContent/contenteditable branch currently
skips dispatching a cancelable beforeinput and thus ignores cancellation; before
mutating el.textContent in the else branch, dispatch a cancelable
InputEvent('beforeinput', { bubbles: true, cancelable: true, inputType:
'insertText', data: chunk }) (wrap in try/catch), check its boolean return (as
done in the 'value' branch) and if it returns false return { ok: false, error:
'input_rejected' }; only then perform el.textContent = ... and dispatch the
input/change events as already implemented so contenteditable targets honor
beforeinput cancellation (mirror the behavior around the 'value' in el path and
Self.reactCompatibleSetValue).
- Around line 12713-12718: kdNotPrevented records whether the synthetic keydown
was canceled but code still always dispatches keypress for printable keys;
change the logic so __cmuxKey(target, 'keypress', k) is only invoked when
kdNotPrevented is true (i.e. when the keydown was not prevented) — update the
kpNotPrevented assignment to check kdNotPrevented before calling __cmuxKey (keep
the existing printable-key/Enter condition), leaving __cmuxKey(target, 'keyup',
k) unchanged.
🪄 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: befb4ffc-4266-43a0-b469-b49db5f1bd64

📥 Commits

Reviewing files that changed from the base of the PR and between cb631e0 and d66f0fc.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (4)
  • CLI/cmux.swift
  • Sources/TabManager.swift
  • Sources/TerminalController.swift
  • web/app/[locale]/docs/browser-automation/page.tsx

Comment thread CLI/cmux.swift
Comment on lines +11827 to +11835
if subcommand == "history" {
let verb = browserActionVerbArgs().first?.lowercased() ?? "clear"
guard verb == "clear" else {
throw CLIError(message: "Unsupported browser history subcommand: \(verb) (expected: clear)")
}
guard hasFlag(subArgs, name: "--force") || hasFlag(subArgs, name: "--yes") else {
throw CLIError(message: "browser history clear permanently deletes the default browser profile's history (same as the View menu's Clear Browser History); pass --force to confirm")
}
let payload = try client.sendV2(method: "browser.history.clear", params: ["force": true])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Reject or validate routing flags before clearing history.

browser history clear bypasses the new handle-validation path entirely. If a caller supplies --surface, --workspace, or --window, those values are silently ignored here, so malformed explicit handles do not fail fast and the command still clears the default profile history. That breaks the new “supplied-but-unresolvable handles are hard errors” contract and is risky on a destructive action.

Suggested fix
         if subcommand == "history" {
             let verb = browserActionVerbArgs().first?.lowercased() ?? "clear"
             guard verb == "clear" else {
                 throw CLIError(message: "Unsupported browser history subcommand: \(verb) (expected: clear)")
             }
             guard hasFlag(subArgs, name: "--force") || hasFlag(subArgs, name: "--yes") else {
                 throw CLIError(message: "browser history clear permanently deletes the default browser profile's history (same as the View menu's Clear Browser History); pass --force to confirm")
             }
+            if surfaceRaw != nil || parseOption(subArgs, name: "--workspace").0 != nil || parseOption(subArgs, name: "--window").0 != nil {
+                throw CLIError(message: "browser history clear does not accept --surface, --workspace, or --window")
+            }
             let payload = try client.sendV2(method: "browser.history.clear", params: ["force": true])
             output(payload, fallback: "OK")
             return
         }
🤖 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 `@CLI/cmux.swift` around lines 11827 - 11835, The history-clear code path
currently ignores routing flags; before calling
client.sendV2("browser.history.clear", ...), detect any supplied routing flags
(e.g., --surface, --workspace, --window via hasFlag and browserActionVerbArgs())
and either (preferred) pass them through the existing
handle-validation/resolution helper (e.g., resolve/validate routing handles) and
throw a CLIError if any supplied handle cannot be resolved, or (fallback) reject
the command immediately when any of those flags are present by throwing a
CLIError explaining they are unsupported for this destructive action; ensure the
check happens before the hasFlag(--force) guard and before client.sendV2 is
invoked.

Comment thread CLI/cmux.swift
browser back|forward|reload [--snapshot-after]
browser react-grab toggle [--surface <id>] [--return-to <terminal-surface>]
browser devtools toggle|console [--surface <id>]
browser focus-mode enter|exit|toggle [--surface <id>]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Document the on / off focus-mode aliases here.

The parser accepts enter, exit, toggle, on, and off, but this usage text only advertises the first three. That makes two supported modes effectively undiscoverable from cmux browser --help.

🤖 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 `@CLI/cmux.swift` at line 32773, Update the usage/help string that currently
reads "browser focus-mode enter|exit|toggle [--surface <id>]" to also advertise
the accepted aliases "on" and "off" (e.g., "browser focus-mode
enter|exit|toggle|on|off [--surface <id>]") so users see all supported modes;
locate the command/usage definition in CLI/cmux.swift (the focus-mode usage
string) and modify that literal so the help output includes the on/off aliases.

Comment thread Sources/TabManager.swift
Comment on lines +6280 to 6292
// Return terminal: an explicit return surface is authoritative (must be a terminal in
// this workspace, no fallback) so pasteback never silently goes to the wrong terminal.
// With no explicit return, adopt the route's terminal only when the browser also came
// from the route (matching shortcut semantics).
let returnTerminalPanelId: UUID?
if let explicit = returnTerminalSurfaceId {
guard workspace.panels[explicit]?.panelType == .terminal else { return nil }
returnTerminalPanelId = explicit
} else if browserSurfaceId == nil {
returnTerminalPanelId = route?.returnTerminalPanelId
} else {
returnTerminalPanelId = nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep the route-derived return terminal when only the browser target is overridden.

This branch makes an explicit browser target clear the implicit returnTerminalPanelId, so browser react-grab toggle --surface <browser> loses pasteback to the caller’s terminal unless --return-to is also passed. The PR contract describes --surface and --return-to as independent overrides, so overriding the browser target should not suppress an otherwise valid route-derived return target.

Proposed fix
-        } else if browserSurfaceId == nil {
-            returnTerminalPanelId = route?.returnTerminalPanelId
-        } else {
-            returnTerminalPanelId = nil
+        } else {
+            returnTerminalPanelId = route?.returnTerminalPanelId
         }
🤖 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 `@Sources/TabManager.swift` around lines 6280 - 6292, The logic that computes
returnTerminalPanelId incorrectly clears a route-derived return when
browserSurfaceId is provided; change the branching in the block that sets
returnTerminalPanelId so that: if returnTerminalSurfaceId is non-nil use it (as
now); else if route?.returnTerminalPanelId is non-nil and
workspace.panels[thatId]?.panelType == .terminal then use the route-derived ID
(independent of browserSurfaceId); otherwise if browserSurfaceId is nil fall
back to nil; ensure you reference returnTerminalSurfaceId, browserSurfaceId,
route?.returnTerminalPanelId and workspace.panels when making the checks so the
route-derived terminal is preserved unless explicitly overridden by --return-to
or is invalid.

Comment thread Sources/TerminalController.swift Outdated
Comment on lines 12646 to 12660
if ('value' in el) {
const newValue = (el.value || '') + chunk;
// beforeinput is cancelable; honor a page that rejects the edit (input masks,
// controlled editors) instead of forcing the value and drifting from app state.
let proceed = true;
try { proceed = el.dispatchEvent(new InputEvent('beforeinput', { bubbles: true, cancelable: true, inputType: 'insertText', data: chunk })); } catch (e) {}
if (!proceed) return { ok: false, error: 'input_rejected' };
\(Self.reactCompatibleSetValue)
el.dispatchEvent(new Event('input', { bubbles: true }));
try { el.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: chunk })); }
catch (e) { el.dispatchEvent(new Event('input', { bubbles: true })); }
el.dispatchEvent(new Event('change', { bubbles: true }));
} else {
el.textContent = (el.textContent || '') + chunk;
try { el.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: chunk })); } catch (e) {}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the exact code around the referenced lines.
sed -n '12620,12710p' Sources/TerminalController.swift | nl -ba | sed -n '1,120p'

# Also extract a bit wider to catch surrounding context where input_rejected is handled.
sed -n '12580,12780p' Sources/TerminalController.swift | nl -ba | sed -n '1,220p'

# Search within the file for the rejection sentinel and the two branches.
rg -n "input_rejected|beforeinput|reactCompatibleSetValue|value' in el|textContent" Sources/TerminalController.swift

Repository: manaflow-ai/cmux

Length of output: 103


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Helper: print with 1-based absolute line numbers
print_range () {
  local start="$1" end="$2"
  awk -v s="$start" -v e="$end" 'NR>=s && NR<=e {printf "%6d %s\n", NR, $0}' "Sources/TerminalController.swift"
}

print_range 12620 12710
print_range 12580 12780

rg -n "input_rejected|beforeinput|reactCompatibleSetValue|value' in el|textContent" Sources/TerminalController.swift

Repository: manaflow-ai/cmux

Length of output: 22241


Honor canceled beforeinput for textContent/contenteditable targets (type + fill)

v2BrowserType/v2BrowserFill only return { ok: false, error: 'input_rejected' } when 'value' in el; the else path (el.textContent = ...) never dispatches a cancelable beforeinput nor respects its cancellation, so editors that land in the textContent branch can still be force-mutated and drift from controlled state (e.g., around Sources/TerminalController.swift lines 12646-12660 and 12680-12692).

🤖 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 `@Sources/TerminalController.swift` around lines 12646 - 12660, The
textContent/contenteditable branch currently skips dispatching a cancelable
beforeinput and thus ignores cancellation; before mutating el.textContent in the
else branch, dispatch a cancelable InputEvent('beforeinput', { bubbles: true,
cancelable: true, inputType: 'insertText', data: chunk }) (wrap in try/catch),
check its boolean return (as done in the 'value' branch) and if it returns false
return { ok: false, error: 'input_rejected' }; only then perform el.textContent
= ... and dispatch the input/change events as already implemented so
contenteditable targets honor beforeinput cancellation (mirror the behavior
around the 'value' in el path and Self.reactCompatibleSetValue).

Comment thread Sources/TerminalController.swift Outdated
Comment on lines +12713 to +12718
const kdNotPrevented = __cmuxKey(target, 'keydown', k);
// keypress historically fires for character-producing keys, which includes Enter and
// Space; many pages still bind submit/search to keypress for Enter.
let kpNotPrevented = true;
if (k.length === 1 || k === 'Enter') { kpNotPrevented = __cmuxKey(target, 'keypress', k); }
__cmuxKey(target, 'keyup', k);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the exact snippet in TerminalController.swift
rg -n "kdNotPrevented|kpNotPrevented|__cmuxKey\\(target, 'keypress'" Sources/TerminalController.swift

# Print the surrounding block (around the reported lines)
sed -n '12700,12760p' Sources/TerminalController.swift

# Also search for related keydown/keypress flow elsewhere in the file (cheap sanity check)
rg -n "__cmuxKey\\(target, 'keydown'|__cmuxKey\\(target, 'keypress'|__cmuxKey\\(target, 'keyup'" Sources/TerminalController.swift | head -n 50

Repository: manaflow-ai/cmux

Length of output: 4615


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "browserInputHelpers|__cmuxKey\\b|function __cmuxKey|const __cmuxKey|__cmuxKey\\s*=|kpNotPrevented|kdNotPrevented" Sources/TerminalController.swift

# Show the definition of browserInputHelpers
rg -n "browserInputHelpers" Sources/TerminalController.swift -n
python3 - <<'PY'
import re,sys,subprocess,os, textwrap, json
PY

# Print a wider chunk around browserInputHelpers definition (based on line numbers from search output)
# We'll fetch around the first occurrence for visibility.
first=$(rg -n "browserInputHelpers" Sources/TerminalController.swift | head -n1 | cut -d: -f1)
start=$((first-60))
end=$((first+220))
sed -n "${start},${end}p" Sources/TerminalController.swift

# Also inspect other nearby key event mappings (full coverage around v2BrowserPress + v2BrowserKeyDown)
rg -n "func v2BrowserPress|func v2BrowserKeyDown" Sources/TerminalController.swift

Repository: manaflow-ai/cmux

Length of output: 16930


🌐 Web query:

In browser, does calling preventDefault() on a keydown event prevent the subsequent keypress event from firing for printable keys/Enter?

💡 Result:

Yes, calling preventDefault on a keydown event typically prevents the subsequent keypress event from firing in modern web browsers [1][2][3]. When you call preventDefault on a keydown event, you are preventing the default action of that event [1]. Because the dispatch of a keypress event is often considered a default action of the preceding keydown event for printable characters, stopping the keydown event prevents the browser from firing the keypress event [1][2]. Important considerations: 1. Deprecation: The keypress event itself is deprecated and is no longer recommended for use [4][5][6]. Modern web development practices suggest using the beforeinput or keydown events instead [4][6]. 2. Browser Behavior: While this behavior is standard in modern browsers, it was not always consistent in older browser versions [2][7]. Some older implementations might have dispatched the keypress event regardless of whether the keydown event was prevented [2][8]. 3. Event Flow: Because keypress is a legacy event, relying on the specific sequence of keydown and keypress—or attempting to manipulate the browser's default behavior through these events—is discouraged in favor of more modern input handling methods [4][5].

Citations:


Suppress keypress when keydown was canceled.

kdNotPrevented captures whether the synthetic keydown event was canceled (__cmuxKey returns dispatchEvent’s cancel status), but keypress is still dispatched for printable keys / Enter regardless. In normal browser behavior, canceling keydown (preventDefault) prevents the follow-on keypress, so pages that block keydown can still receive a synthetic keypress here and run duplicate/forbidden handlers.

Suggested direction
-              let kpNotPrevented = true;
-              if (k.length === 1 || k === 'Enter') { kpNotPrevented = __cmuxKey(target, 'keypress', k); }
+              let kpNotPrevented = true;
+              if (kdNotPrevented && (k.length === 1 || k === 'Enter')) {
+                kpNotPrevented = __cmuxKey(target, 'keypress', k);
+              }
🤖 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 `@Sources/TerminalController.swift` around lines 12713 - 12718, kdNotPrevented
records whether the synthetic keydown was canceled but code still always
dispatches keypress for printable keys; change the logic so __cmuxKey(target,
'keypress', k) is only invoked when kdNotPrevented is true (i.e. when the
keydown was not prevented) — update the kpNotPrevented assignment to check
kdNotPrevented before calling __cmuxKey (keep the existing printable-key/Enter
condition), leaving __cmuxKey(target, 'keyup', k) unchanged.

Comment thread Sources/TerminalController.swift Outdated
Comment on lines +13281 to +13290
/// Returns an error if any of the given handle params is SUPPLIED but does not resolve.
/// v2UUID returns nil for both an absent param and a present-but-unresolvable handle (e.g. a
/// stale `surface:2`/`workspace:99` ref), so a supplied target must not be treated as omitted
/// and silently fall back to the focused/selected context. Returns nil when all are valid.
private func v2RejectUnresolvedHandles(_ params: [String: Any], _ keys: [String]) -> V2CallResult? {
// Use v2HasNonNullParam (not v2String) for presence: v2String trims empties to nil, so an
// empty/whitespace explicit handle would otherwise look absent and silently fall back.
for key in keys where v2HasNonNullParam(params, key) && v2UUID(params, key) == nil {
return .err(code: "invalid_params", message: "Unresolved \(key)", data: nil)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Explicit handle validation is still too weak for the new authoritative-routing contract.

v2RejectUnresolvedHandles only verifies that a supplied value can be turned into a UUID. A valid handle from the wrong workspace/window — or a return_to surface of the wrong type — still passes here and then degrades into the generic .not_found path in the new browser handlers. That breaks the PR’s new “supplied-but-unresolvable handles are hard errors” behavior for explicit surface_id / return_to / workspace_id / window_id.

Also applies to: 13294-13405

🤖 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 `@Sources/TerminalController.swift` around lines 13281 - 13290,
v2RejectUnresolvedHandles currently only checks that a param parses to a UUID
via v2UUID; update it to also verify the UUID actually resolves to the correct
resource and context (surface, return_to type, workspace, window) so
supplied-but-wrong-workspace/type handles are rejected. For keys like
"surface_id", "return_to", "workspace_id", "window_id" call the appropriate
resolver (e.g.
v2ResolveSurface/v2ResolveReturnTo/v2ResolveWorkspace/v2ResolveWindow or the
central handle-resolver used elsewhere) instead of just v2UUID, and if
resolution fails or the resolved object is of the wrong type/owner return
.err(code: "invalid_params", message: "Unresolved <key>", data: nil); keep the
existing presence check using v2HasNonNullParam and reuse v2UUID only to obtain
the candidate id before resolution.

…r and spec

PointerEvent 'pointerenter' (like 'mouseenter') must not bubble, but
__cmuxPointer hardcoded bubbles:true while __cmuxMouse already honored a
bubbles flag and dispatched mouseenter non-bubbling. A bubbling
pointerenter fires spurious enter signals on ancestor elements and can
corrupt hover state (hover menus, pointer-tracking) in nested components
when __cmuxClick/__cmuxHover run. Add a bubbles parameter to __cmuxPointer
mirroring __cmuxMouse and pass false at the pointerenter call sites.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lawrencecchen
lawrencecchen force-pushed the feat-cli-browser-actions branch from c86dba6 to 4ecc5a7 Compare June 12, 2026 07:02
@lawrencecchen lawrencecchen changed the title CLI browser view actions (react-grab, devtools, focus-mode, zoom, history) + input-event correctness fixes browser inputs: dispatch pointerenter non-bubbling to match mouseenter and spec Jun 12, 2026
@lawrencecchen
lawrencecchen merged commit a61a5e5 into main Jun 12, 2026
20 checks passed
@lawrencecchen
lawrencecchen deleted the feat-cli-browser-actions branch June 12, 2026 07:25
hhsw2015 pushed a commit to hhsw2015/cmux that referenced this pull request Jun 12, 2026
…r and spec (manaflow-ai#5958)

PointerEvent 'pointerenter' (like 'mouseenter') must not bubble, but
__cmuxPointer hardcoded bubbles:true while __cmuxMouse already honored a
bubbles flag and dispatched mouseenter non-bubbling. A bubbling
pointerenter fires spurious enter signals on ancestor elements and can
corrupt hover state (hover menus, pointer-tracking) in nested components
when __cmuxClick/__cmuxHover run. Add a bubbles parameter to __cmuxPointer
mirroring __cmuxMouse and pass false at the pointerenter call sites.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lawrencecchen
lawrencecchen restored the feat-cli-browser-actions branch June 12, 2026 19:55

This branch was successfully deployed

1 active deployment
Preview – cmux — 4ecc5a76 Deployed Jun 12, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant