Skip to content

Fix browser context menu opening the wrong link - #5780

Merged
lawrencecchen merged 11 commits into
mainfrom
task-browser-context-menu-wrong-link
Jun 12, 2026
Merged

lawrencecchen merged 11 commits into
mainfrom
task-browser-context-menu-wrong-link

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jun 10, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Right-click → "Open Link in Default Browser" (and "Open Link in New Tab") could open a different link than the one under the cursor. The menu actions re-resolved the link with a main-frame document.elementFromPoint hit test at the raw AppKit event coordinates, which disagrees with the real click target whenever CSS coordinates diverge from view points (page zoom != 100%) or the link lives inside an iframe the main frame cannot hit-test.
  • Fix: capture the link at contextmenu time. A document-start user script, injected into every frame in an isolated content world (same fingerprinting-safety pattern as the media-playback hook in BrowserPanel+MediaPlayback.swift), reports the event target's closest anchor to a private message handler. The menu actions prefer that captured link when it belongs to the same right-click as the open menu (paired by uptime, 2s window) and fall back to the old hit test otherwise.
  • Also scale the remaining elementFromPoint fallbacks (link, image, debug inspect) by pageZoom so coordinate-based resolution is correct on zoomed pages.
  • The capture code and link-resolution helpers live in a new Sources/Panels/CmuxWebView+ContextMenuLinkCapture.swift so CmuxWebView.swift and BrowserConfigTests.swift stay inside the Swift file length budget.

Testing

  • Two-commit red/green structure, verified with -only-testing on the AWS M4 Pro fleet Mac (aws-m4pro-2) because the CI tests job's app-host suite currently times out mid-suite and does not reliably reach this class:
    • At the test-only commit (no fix): testOpenLinkInDefaultBrowserOpensTheLinkUnderTheRightClick fails with XCTAssertEqual failed: ("https://example.test/decoy") is not equal to ("https://example.test/clicked"), the exact wrong-link symptom.
    • At head: CmuxWebViewContextMenuLinkCaptureTests and all 4 pre-existing CmuxWebViewContextMenuTests pass.
  • The test loads a real page with two links, dispatches a DOM contextmenu event on one link while the AppKit menu event points at the other, and asserts the right-clicked link is what opens.
  • Tagged build ctxlink (cloud builder) launched locally; browser pane preflighted via the debug socket: page loads, contextmenu dispatch runs with no JS errors, and the private capture handler is not visible to page JavaScript (isolated content world confirmed).

Issues


Note

Medium Risk
Touches injected WebKit scripts and context-menu navigation paths across all browser panes; pairing and spoofing guards reduce but do not eliminate edge cases on non-mouse menu opens.

Overview
Fixes browser context menu actions (Open Link in Default Browser, Open Link in New Tab, linked-file download) opening the wrong URL when link resolution disagreed with the actual right-click target (page zoom, iframes, and a vertical mirror bug in coordinate fallbacks).

A new CmuxWebView+ContextMenuLinkCapture extension injects a document-start script in an isolated WKContentWorld that records the contextmenu event’s anchor and pairs it with the open menu via uptime/timestamp guards. resolveContextMenuLinkURL prefers that capture and only then falls back to elementFromPoint. cssViewportPoint respects flipped WKWebView coordinates and pageZoom for image/link/debug hit tests. Captures are cleared on right- and ctrl-click; untrusted synthetic contextmenu events are ignored (with a test-only opt-in). Unit tests cover capture vs skew, stale pairing, spoofing, and coordinate math.

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

Summary by CodeRabbit

  • New Features
    • "Open Link in Default Browser" now reliably targets the link under the right‑click across zoomed and flipped views; link-capture is initialized on view creation.
  • Bug Fixes
    • Clears stale captures on modifier/right‑click and ignores synthetic/untrusted contextmenu events to avoid wrong URLs.
    • Improved hit‑testing using CSS‑viewport coordinates for accurate element resolution.
  • Tests
    • Added automated tests for right‑click link detection, synthetic‑event protection, and viewport coordinate handling.

Round 2: live probe evidence and three more fixes

A probe build (tag ctxlk2) instrumented capture arrival, pairing, and both resolution paths during a live dogfood session. Six real right-clicks showed the capture stored 10-24ms before willOpenMenu and always matched the clicked link, while the coordinate fallback resolved the wrong link on every single click, including a flat page at 100% zoom (top link resolved as the bottom link; an HN title resolved as the row's upvote URL).

Root cause of the fallback wrongness: WKWebView is a flipped NSView on macOS, so view-local points are already top-left-origin; bounds.height - point.y double-flipped and mirrored every hit test vertically. This predates this PR (four copies of the subtraction on main) and is the original root cause of the wrong-link reports. It also affected the image download/copy fallbacks, which share cssViewportPoint.

Fixes added (red/green: tests at the first commit fail, pass after the second):

  • cssViewportPoint respects isFlipped instead of always re-flipping.
  • The capture handler ignores synthetic contextmenu events (isTrusted == false), closing the decoy-link spoof the review bots flagged. Unit tests opt back in through contextMenuLinkCaptureAcceptsUntrustedEventsForTesting.
  • rightMouseDown / ctrl-mouseDown clear the previous capture so a menu can only pair with the link captured by the click that opened it; the 2s window now only bounds non-mouse menu paths.

Verified on aws-m4pro-2: at the test-only commit testCssViewportPointDoesNotReflipFlippedViewCoordinates and testSyntheticContextMenuEventCannotPlantDecoyLink fail and the original capture test passes; at head all 3 pass.

lawrencecchen and others added 2 commits June 10, 2026 02:34
Right-click on a link, then "Open Link in Default Browser": the link that
opens must be the contextmenu event target, not whatever a main-frame
elementFromPoint hit test finds at the raw AppKit event coordinates. The
two diverge under page zoom and inside iframes, opening the wrong link.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
"Open Link in Default Browser" / "Open Link in New Tab" resolved the
link by re-running a main-frame document.elementFromPoint hit test at the
raw AppKit event coordinates when the menu item was clicked. That
re-resolution disagrees with the link the user right-clicked whenever CSS
coordinates diverge from view points (pageZoom != 1) or the link lives in
an iframe the main frame cannot hit-test, so the action opened the wrong
link.

Capture the link at contextmenu time instead: a document-start user
script, injected into every frame in an isolated content world (same
fingerprinting-safety pattern as the media-playback hook), reports the
contextmenu event target's closest anchor to a private message handler.
The menu actions prefer that captured link when it belongs to the same
right-click as the open menu, falling back to the hit test otherwise.

Also scale the remaining elementFromPoint fallbacks (link, image, debug
inspect) by pageZoom so coordinate-based resolution is correct on zoomed
pages.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 10, 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 12:50am
cmux-staging Building Building Preview, Comment Jun 12, 2026 12:50am

@coderabbitai

coderabbitai Bot commented Jun 10, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds an isolated-world document-start script to capture right-clicked link hrefs, timestamps captures and menu-open uptime, prefers recent captures when resolving menu actions, normalizes hit-testing to CSS viewport coordinates, provides robust JS fallbacks, integrates into CmuxWebView initializers, and adds tests and project wiring.

Changes

Context-menu link capture and resolution

Layer / File(s) Summary
Bootstrap script and installation
Sources/Panels/CmuxWebView+ContextMenuLinkCapture.swift, Sources/Panels/CmuxWebView.swift
Defines a WKContentWorld and injects a document-start script that listens for capture-phase contextmenu and posts the nearest a[href]/area[href] href. Adds installContextMenuLinkCapture() and installs it from both init(frame:configuration:) and init?(coder:) with a dedupe guard.
Capture state and message handling
Sources/Panels/CmuxWebView+ContextMenuLinkCapture.swift, Sources/Panels/CmuxWebView.swift
Introduces ContextMenuCapturedLink storing URL? plus capture uptime. Implements ContextMenuLinkCaptureMessageHandler to parse posted messages, filter untrusted events (opt-in for tests), and call noteContextMenuCapturedLink(_:) on main actor.
CSS viewport coordinate system
Sources/Panels/CmuxWebView+ContextMenuLinkCapture.swift, Sources/Panels/CmuxWebView.swift
Adds cssViewportPoint(for:) converting AppKit points into CSS-viewport coordinates using pageZoom and flip handling. Updates findImageURLAtPoint and debugInspectElementsAtPoint to use CSS x/y for JS queries.
Link resolution with capture pairing
Sources/Panels/CmuxWebView+ContextMenuLinkCapture.swift, Sources/Panels/CmuxWebView.swift
Records lastContextMenuOpenUptime in willOpenMenu(_:with:). Implements resolveContextMenuLinkURL(at:completion:) to prefer the most-recent captured link when within a short max-age of the menu-open uptime, otherwise fall back to coordinate-based detection.
Fallback link detection strategies
Sources/Panels/CmuxWebView+ContextMenuLinkCapture.swift
Provides findLinkAtPoint (elementFromPoint + ancestor walk) and findLinkURLAtPoint (elementsFromPoint, shadowRoot probing, ancestor chains, attribute heuristics) as coordinate-based fallbacks for robust URL extraction.
CmuxWebView lifecycle & input handling
Sources/Panels/CmuxWebView.swift
Clears contextMenuCapturedLink on Ctrl-click and at each rightMouseDown to avoid stale pairings; adds capture storage fields and integrates installation into initializers.
Project configuration
cmux.xcodeproj/project.pbxproj
Adds the new source and test files to PBXFileReference, PBXBuildFile, Sources groups, and the app/test target source lists.
Regression tests and helpers
cmuxTests/CmuxWebViewContextMenuLinkCaptureTests.swift
Adds tests that synthesize DOM contextmenu events and right-mouse NSEvents to assert the native "Open Link in Default Browser" action receives the correct href; includes a decoy-synthetic-event test, cssViewportPoint coordinate test, and test helpers.

Sequence Diagram

sequenceDiagram
  participant Page as WebPage (document)
  participant WK as CmuxWebView (WKContentWorld)
  participant Handler as ContextMenuLinkCaptureMessageHandler
  participant View as CmuxWebView (main actor)
  participant AppKit as AppKit Menu Flow
  participant Finder as DefaultBrowserOpener

  Page->>WK: contextmenu (capture) with target element
  WK->>Handler: postMessage({href, trusted})
  Handler->>View: noteContextMenuCapturedLink(URL, uptime)
  AppKit->>View: rightMouseDown -> willOpenMenu (record uptime)
  View->>View: resolveContextMenuLinkURL(at:point)
  alt recent captured link within max-age
    View->>Finder: openContextMenuLinkInDefaultBrowser(captured URL)
  else fallback
    View->>WK: evaluate findLinkURLAtPoint(cssPoint)
    View->>Finder: openContextMenuLinkInDefaultBrowser(found URL)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • manaflow-ai/cmux#4573: Changes CmuxWebView context-menu lifecycle; may interact with timing or menu augmentation.
  • manaflow-ai/cmux#4479: Modifies CmuxWebView context-menu items and interacts with menu construction and target-resolution paths.

"I nibble code and hop with cheer,
I stamp the uptime when clicks are near,
I map the page to CSS space,
So right-clicks find the proper place,
A rabbit's hop to open links, hooray!"


Important

Pre-merge checks failed

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

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
Cmux Architecture Rethink ❌ Error Capture handler records via Task { @mainactor ... } (delayed dispatch), and new tests rely on Task.sleep/polling to wait for capture, violating swift-architectural-rethink timing rules. Update the script-message callback to apply noteContextMenuCapturedLink synchronously on the main actor (no Task hop; use MainActor.assumeIsolated/assert), and adjust tests to await deterministic completion instead of sleep/poll loops.
Docstring Coverage ⚠️ Warning Docstring coverage is 31.58% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (19 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the primary fix: resolving the issue where browser context menu actions could open the wrong link due to coordinate resolution failures.
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 Actor isolation looks consistent: new context-menu message handler calls noteContextMenuCapturedLink inside MainActor.assumeIsolated (same as BrowserMediaPlaybackMessageHandler); no new unsafe Send...
Cmux Swift Blocking Runtime ✅ Passed Scanned new/updated production Swift files (CmuxWebView.swift, CmuxWebView+ContextMenuLinkCapture.swift) for rule-blocking primitives (semaphores, sleep, main.sync, locks); none present.
Cmux Expensive Synchronous Load ✅ Passed In CmuxWebView.swift and CmuxWebView+ContextMenuLinkCapture.swift (and the new tests), there’s no RestorableAgentSessionIndex.load/SharedLiveAgentIndex usage and no .load() calls indicative of the...
Cmux Cache Substitution Correctness ✅ Passed PASS: PR adds ephemeral in-memory context-menu link capture with 2s uptime freshness + cold fallback, and clears on rightMouseDown/ctrl-click; no persistence/history/undo/snapshot cached-value subs...
Cmux No Hacky Sleeps ✅ Passed Scanned Sources/Panels/CmuxWebView*.swift for setTimeout/setInterval/sleep/Date.now/polling; none found. Only cmuxTests/CmuxWebViewContextMenuLinkCaptureTests.swift contains Task.sleep.
Cmux Algorithmic Complexity ✅ Passed New production code (CmuxWebView context-menu capture) uses point-based DOM hit tests (elementFromPoint/elementsFromPoint) and bounded ancestor/shadow-chain walks; no nested full-collection scans,...
Cmux Swift Concurrency ✅ Passed No diff additions of DispatchQueue/Combine/Task. The only new @escaping completion APIs are moved from CmuxWebView.swift (3 removed, 3 added) into the new extension file.
Cmux Swift @Concurrent ✅ Passed Scanned CmuxWebView.swift, CmuxWebView+ContextMenuLinkCapture.swift, and CmuxWebViewContextMenuLinkCaptureTests.swift for '@concurrent' and 'nonisolated async' declarations; none found, so no swift...
Cmux Swift File And Package Boundaries ✅ Passed New production file is 299 lines (<400) and CmuxWebView.swift adds ~35 lines to an already large file; changes stay focused on context-menu link capture/resolution (no persistence/network/protocol...
Cmux Swift Logging ✅ Passed Scanned Sources/Panels/CmuxWebView.swift, Sources/Panels/CmuxWebView+ContextMenuLinkCapture.swift, and CmuxWebViewContextMenuLinkCaptureTests.swift: no print/debugPrint/dump/NSLog and no file-scope...
Cmux User-Facing Error Privacy ✅ Passed No user-facing alerts/recovery copy found in the new/modified context-menu link capture code; debugContextDownload (which includes raw error info) is gated behind #if DEBUG.
Cmux Full Internationalization ✅ Passed PR #5780 only changes CmuxWebView.swift, new CmuxWebView+ContextMenuLinkCapture.swift, pbxproj, and tests. The new capture file adds no user-facing Swift strings; context-menu titles remain wrapped...
Cmux Swiftui State Layout ✅ Passed PR changes only WebKit/AppKit files (CmuxWebView + context-menu capture + XCTest) and contain no SwiftUI/ObservableObject/@Published/GeometryReader/LazyVStack/List state/layout patterns per keyword...
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed Changed Swift files only add context-menu link capture helpers and tests; git diff for the PR contains no NSWindow/NSPanel/WindowGroup or cmuxAuxiliaryWindowIdentifiers close-shortcut ownership cha...
Cmux Source Artifacts ✅ Passed Rules forbid generated logs/caches/DerivedData/temp artifacts. The PR’s changed paths are only Swift source/tests and the pbxproj; no local-generated artifact directories/files were found in those...
Description check ✅ Passed The PR description comprehensively covers the problem statement, the proposed solution with technical depth, testing methodology with red/green verification, and manual validation evidence. It includes issue references and risk assessment.
✨ 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 task-browser-context-menu-wrong-link

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.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b796c19dd8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Sources/Panels/CmuxWebView.swift Outdated
Comment on lines +205 to +206
window.addEventListener("contextmenu", (event) => {
post(linkForEvent(event));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Ignore synthetic contextmenu events

Because this capture listener posts for every contextmenu event without checking event.isTrusted, page JavaScript can still forge link reports by dispatching synthetic MouseEvent('contextmenu') events on an arbitrary anchor; the new native menu action then prefers that captured URL for up to 2 seconds. On pages that dispatch such events shortly before or while the user opens the context menu, “Open Link in Default Browser” / “Open Link in New Tab” can open a page-chosen URL instead of the actual right-click target, despite the isolated message handler. Please only accept trusted user contextmenu events (or otherwise correlate the report to the native event).

Useful? React with 👍 / 👎.

Comment thread Sources/Panels/CmuxWebView.swift Outdated
@greptile-apps

greptile-apps Bot commented Jun 10, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes "Open Link in Default Browser" (and related menu actions) opening the wrong URL by capturing the contextmenu event target in an isolated WKContentWorld user script and preferring that capture over the old document.elementFromPoint coordinate hit test. It also corrects a pre-existing vertical-mirror bug in cssViewportPoint — WKWebView is a flipped NSView, so view-local points are already top-left-origin and must not be re-flipped.

  • Capture-first link resolution: a document-start script in an isolated content world captures the closest anchor at contextmenu time; willOpenMenu records pairing timestamps; resolveContextMenuLinkURL prefers the capture and falls back to the coordinate hit test when the capture is absent or stale.
  • Coordinate fallback hardening: cssViewportPoint respects isFlipped (avoiding the double-flip that mirrored every hit test vertically) and divides by pageZoom; applied to findImageURLAtPoint and debugInspectElementsAtPoint.
  • Security gate: the injected script drops isTrusted == false contextmenu events so page JavaScript cannot plant a decoy link; rightMouseDown and ctrl-mouseDown clear the previous capture so a menu can only pair with the link captured by its own click.

Confidence Score: 5/5

Safe to merge; the capture-first approach correctly fixes the wrong-link bug and the cssViewportPoint double-flip is properly addressed.

The core fix captures the contextmenu event target in an isolated content world and pairs it with the AppKit menu via timestamp guards; four regression tests demonstrate it end-to-end. The rightMouseDown clearing and isTrusted gate close the mispairing and decoy-link vectors. The one new observation — internal var mutation surface on the three stored properties — is a style trade-off of the file-split, not a defect in the live code path.

Sources/Panels/CmuxWebView.swift — the three capture-state stored properties are internal vars; direct assignment in mouseDown/rightMouseDown bypasses the method-level lifecycle defined in CmuxWebView+ContextMenuLinkCapture.swift.

Important Files Changed

Filename Overview
Sources/Panels/CmuxWebView+ContextMenuLinkCapture.swift New file that captures the contextmenu event target in an isolated WKContentWorld and uses it to correctly resolve the right-clicked link; also contains cssViewportPoint fix for double-flip bug, findLinkURLAtPoint and findLinkAtPoint fallbacks with correct zoom scaling.
Sources/Panels/CmuxWebView.swift Adds three internal stored properties for capture state; hooks installContextMenuLinkCapture in both inits; rightMouseDown override clears capture; ctrl-click mouseDown also clears capture; willOpenMenu records menu-open uptime and event timestamp for pairing.
cmuxTests/CmuxWebViewContextMenuLinkCaptureTests.swift Four regression tests covering correct-link capture, stale-capture rejection, synthetic-event decoy-link prevention, and cssViewportPoint flip correctness.
cmux.xcodeproj/project.pbxproj Adds CmuxWebView+ContextMenuLinkCapture.swift to the app target and CmuxWebViewContextMenuLinkCaptureTests.swift to the test target; no unintended artifact additions.

Reviews (6): Last reviewed commit: "Move ContextMenuCapturedLink into the ca..." | Re-trigger Greptile

Comment thread Sources/Panels/CmuxWebView.swift Outdated
/// be while still describing the same right-click.
private static let contextMenuLinkCaptureMaxAge: TimeInterval = 2.0

func noteContextMenuCapturedLink(_ url: URL?) {

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 noteContextMenuCapturedLink access level should be private

The method is only called from the Task { @MainActor } closure inside ContextMenuLinkCaptureMessageHandler, which is a private nested type declared inside CmuxWebView. In Swift, nested types share their enclosing type's lexical scope, so private members of CmuxWebView are fully accessible from within the nested class. Leaving the method internal (implicit) widens the access surface unnecessarily and suggests a future call site outside the class hierarchy could invoke it.

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!

Comment thread Sources/Panels/CmuxWebView.swift Outdated
Comment on lines +1080 to +1086
private func capturedContextMenuLinkURLForCurrentMenu() -> URL? {
guard let captured = contextMenuCapturedLink,
let menuOpenUptime = lastContextMenuOpenUptime,
abs(captured.uptime - menuOpenUptime) <= Self.contextMenuLinkCaptureMaxAge
else { return nil }
return captured.url
}

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 capturedContextMenuLinkURLForCurrentMenu() loses the distinction between "no valid capture" and "DOM confirmed no link"

The function returns nil for two meaningfully different states: (1) no capture arrived or the uptime delta exceeded the 2 s window; and (2) the capture arrived but href was empty (the contextmenu target was not a link). The call site treats both as identical and falls through to findLinkURLAtPoint. In theory, a capture that says "no link here" is authoritative evidence that the hit-test fallback should also produce nil — but because the fallback still runs, a zoomed-page or cross-frame scenario where the DOM event fired on a non-link element yet the AppKit hit-test lands on a nearby link could still resolve the wrong target. Representing this as URL?? (or a small enum) would make the three-state contract explicit and let the caller skip the fallback authoritatively when the capture confirmed no link.

Comment on lines +1059 to 1087
/// Link reported by the contextmenu capture hook for the most recent
/// right-click (`url` is nil when the click was not on a link).
private struct ContextMenuCapturedLink {
let url: URL?
let uptime: TimeInterval
}
private var contextMenuCapturedLink: ContextMenuCapturedLink?
/// Uptime at which the current context menu opened, used to pair the menu
/// with the contextmenu capture report from the same right-click.
private var lastContextMenuOpenUptime: TimeInterval?
/// How far apart the DOM contextmenu capture and the AppKit menu open may
/// be while still describing the same right-click.
private static let contextMenuLinkCaptureMaxAge: TimeInterval = 2.0

func noteContextMenuCapturedLink(_ url: URL?) {
contextMenuCapturedLink = ContextMenuCapturedLink(
url: url,
uptime: ProcessInfo.processInfo.systemUptime
)
}

private func capturedContextMenuLinkURLForCurrentMenu() -> URL? {
guard let captured = contextMenuCapturedLink,
let menuOpenUptime = lastContextMenuOpenUptime,
abs(captured.uptime - menuOpenUptime) <= Self.contextMenuLinkCaptureMaxAge
else { return nil }
return captured.url
}
/// Saved native WebKit action for "Download Image".

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 Timing-based pairing via wall-clock uptime leaves the wrong-link state representable

The two new instance variables contextMenuCapturedLink and lastContextMenuOpenUptime correlate events from two independent pipelines (DOM → WKScriptMessage → MainActor, and AppKit willOpenMenu) using a 2-second wall-clock window. Per the architectural-rethink rule, timing-based side channels should be flagged when bad state remains representable: if the message-delivery hop from the isolated content world to the main actor takes longer than 2 s (e.g. under abnormal system load or debugger attachment), the capture silently expires and resolveContextMenuLinkURL falls back to the hit-test path that this PR is trying to retire. The two mutable side-channel variables are also never cleared after the menu action fires, so stale state lingers until the next right-click. A per-event identifier (e.g. a monotonic counter incremented in willOpenMenu and echoed back through the message handler) would make the pairing unambiguous and remove the time-window heuristic entirely. There is no public WKWebView API to do this today, so this is a pragmatic limitation, but the current design should be documented as such.

Rule Used: Flag Swift fixes that patch symptoms while leaving... (source)

@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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Sources/Panels/CmuxWebView.swift (1)

1759-1771: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the captured-link resolver for every link-targeted context-menu action.

resolveContextMenuLinkURL fixes iframe/zoom skew for the open-link actions, but contextMenuDownloadLinkedFile still starts from findLinkURLAtPoint. Right-clicking a link inside an iframe will still re-hit-test the main frame and can download the wrong target or fall back unnecessarily.

🐛 Proposed fix
-        findLinkURLAtPoint(point) { [weak self] url in
+        resolveContextMenuLinkURL(at: point) { [weak self] url in
             guard let self else { return }
             self.debugContextDownload(
                 "browser.ctxdl.resolve trace=\(traceID) kind=linked linkURL=\(url?.absoluteString ?? "nil")"
             )

Based on learnings, "When a behavior is exposed through multiple entrypoints ... implement one shared action/model path and verify every entrypoint that should invoke it."

🤖 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/Panels/CmuxWebView.swift` around lines 1759 - 1771,
contextMenuDownloadLinkedFile currently re-tests the page with
findLinkURLAtPoint and can hit the wrong element in iframes/zoom; change it to
use the shared resolver by calling resolveContextMenuLinkURL(at:completion:)
(which already prefers capturedContextMenuLinkURLForCurrentMenu()) instead of
directly invoking findLinkURLAtPoint so all link-targeted context-menu actions
use the same captured-link logic and return the correct URL.

Source: Coding guidelines

🤖 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 `@Sources/Panels/CmuxWebView.swift`:
- Around line 179-210: The injected context-menu listener
(contextMenuLinkCaptureBootstrapScriptSource) posts hrefs for every contextmenu
event and is vulnerable to synthetic events; update the injected script to only
post when event.isTrusted === true and the discovered href is non-empty to
prevent page JS steering URLs, and adjust the native pairing logic in
capturedContextMenuLinkURLForCurrentMenu() to require recent trusted captures
(e.g., ignore captures older than the existing 2.0s window or mark captures as
trusted) so only trusted, timely captures are resolved for “Open Link …”
actions.

---

Outside diff comments:
In `@Sources/Panels/CmuxWebView.swift`:
- Around line 1759-1771: contextMenuDownloadLinkedFile currently re-tests the
page with findLinkURLAtPoint and can hit the wrong element in iframes/zoom;
change it to use the shared resolver by calling
resolveContextMenuLinkURL(at:completion:) (which already prefers
capturedContextMenuLinkURLForCurrentMenu()) instead of directly invoking
findLinkURLAtPoint so all link-targeted context-menu actions use the same
captured-link logic and return the correct URL.
🪄 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: 0146ca00-09ba-4357-ad4e-44af6d4e4598

📥 Commits

Reviewing files that changed from the base of the PR and between 3dada25 and b796c19.

📒 Files selected for processing (2)
  • Sources/Panels/CmuxWebView.swift
  • cmuxTests/BrowserConfigTests.swift

Comment thread Sources/Panels/CmuxWebView.swift Outdated
Comment on lines +179 to +210
private static let contextMenuLinkCaptureBootstrapScriptSource = """
(() => {
try {
const post = (href) => {
try {
window.webkit.messageHandlers["\(contextMenuLinkCaptureMessageHandlerName)"].postMessage({
href: typeof href === "string" ? href : ""
});
} catch (_) {}
};
const linkForEvent = (event) => {
try {
const path = typeof event.composedPath === "function" ? event.composedPath() : [];
for (const node of path) {
if (!node || node.nodeType !== 1) continue;
const tag = node.tagName;
if ((tag === "A" || tag === "AREA") && node.href) return String(node.href);
}
const target = event.target;
if (target && target.closest) {
const link = target.closest("a[href],area[href]");
if (link && link.href) return String(link.href);
}
} catch (_) {}
return "";
};
window.addEventListener("contextmenu", (event) => {
post(linkForEvent(event));
}, true);
} catch (_) {}
})();
"""

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

file="Sources/Panels/CmuxWebView.swift"
test -f "$file" || { echo "Missing $file"; exit 1; }

echo "== Locate injected contextmenu bootstrap + handler name =="
rg -n "contextMenuLinkCaptureBootstrapScriptSource|addEventListener\\(\"contextmenu\"|postMessage\\(\\{\\s*href:|contextMenuLinkCaptureMessageHandlerName" "$file"

echo "== Show injected JS around listener =="
python3 - <<'PY'
import re
path="Sources/Panels/CmuxWebView.swift"
with open(path,'r',encoding='utf-8') as f:
    s=f.read()

m=re.search(r'contextMenuLinkCaptureBootstrapScriptSource\s*=\s*"""([\s\S]*?)"""', s)
if not m:
    print("No bootstrap script found")
    raise SystemExit(0)
js=m.group(1)
start=js.find('window.addEventListener("contextmenu"')
print("JS snippet starting at contextmenu listener:\n")
print(js[start:start+700])
PY

echo "== Check for isTrusted anywhere in file (esp injected JS + native pairing) =="
rg -n "isTrusted" "$file" || true

echo "== Inspect native pairing / uptime matching for captured contextmenu link =="
rg -n "2\\.0 seconds|uptime|mach_absolute_time|Date\\(|timeInterval|contextmenu|menu open|resolveContextMenuLinkURL|captured" "$file"

echo "== Find resolveContextMenuLinkURL and its callers =="
rg -n "resolveContextMenuLinkURL" "$file"
rg -n "findLinkURLAtPoint" "$file"
rg -n "contextMenuDownloadLinkedFile" "$file"

echo "== Dump relevant surrounding regions for the specific functions =="
# Use sed to print around matches but keep output small
for pat in "resolveContextMenuLinkURL" "contextMenuDownloadLinkedFile" "findLinkURLAtPoint" "contextMenuLinkCapture" "capturedLink"; do
  echo "--- Around: $pat ---"
  line=$(rg -n "$pat" "$file" | head -n1 | cut -d: -f1 || true)
  if [ -n "${line:-}" ]; then
    start=$((line-80))
    end=$((line+120))
    sed -n "${start},${end}p" "$file" | nl -ba | sed 's/^/ /'
  fi
done

Repository: manaflow-ai/cmux

Length of output: 3793


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="Sources/Panels/CmuxWebView.swift"
test -f "$file" || { echo "Missing $file"; exit 1; }

# Helper to print with line numbers without `nl`
pwl() { # args: start end
  start="$1"; end="$2"
  awk -v s="$start" -v e="$end" 'NR>=s && NR<=e {printf "%6d: %s\n", NR, $0}' "$file"
}

echo "== Check injected contextmenu listener for any isTrusted guard =="
pwl 170 235

echo "== Check capturedContextMenuLinkURLForCurrentMenu() pairing (around 1059-1086) =="
pwl 1035 1095

echo "== Check resolveContextMenuLinkURL() (around 1759-1775) =="
pwl 1748 1795

echo "== Find who uses resolveContextMenuLinkURL vs findLinkURLAtPoint =="
echo "-- findLinkURLAtPoint callers (top matches) --"
rg -n "findLinkURLAtPoint\\(" "$file" | head -n 30

echo "-- resolveContextMenuLinkURL callers (top matches) --"
rg -n "resolveContextMenuLinkURL\\(" "$file" | head -n 30

echo "== Inspect contextMenuDownloadLinkedFile() to see if it uses captured link or point hit-testing =="
line=$(rg -n "contextMenuDownloadLinkedFile" "$file" | head -n1 | cut -d: -f1)
echo "contextMenuDownloadLinkedFile line: $line"
if [ -n "${line:-}" ]; then
  pwl $((line-120)) $((line+160))
fi

Repository: manaflow-ai/cmux

Length of output: 23186


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="Sources/Panels/CmuxWebView.swift"

pwl(){ s="$1"; e="$2"; awk -v s="$s" -v e="$e" 'NR>=s && NR<=e {printf "%6d: %s\n", NR, $0}' "$file"; }

echo "== Find where lastContextMenuOpenUptime is set =="
rg -n "lastContextMenuOpenUptime" "$file"
line=$(rg -n "lastContextMenuOpenUptime\\s*=" "$file" | head -n1 | cut -d: -f1 || true)
if [ -n "${line:-}" ]; then
  pwl $((line-80)) $((line+80))
fi

echo "== Inspect resolveContextMenuLinkURL call sites (~2452, ~2460) =="
rg -n "resolveContextMenuLinkURL\\(at: point\\)" "$file"
for l in $(rg -n "resolveContextMenuLinkURL\\(at: point\\)" "$file" | cut -d: -f1 | head -n 5); do
  pwl $((l-60)) $((l+80))
done

echo "== Inspect contextMenuDownloadLinkedFile(_:) body (~2679) =="
line=$(rg -n "`@objc` private func contextMenuDownloadLinkedFile" "$file" | head -n1 | cut -d: -f1)
pwl $((line-80)) $((line+220))

Repository: manaflow-ai/cmux

Length of output: 36585


Harden context-menu link capture against synthetic contextmenu events

The injected contextmenu listener posts a captured href for every event (no event.isTrusted check), and the native code pairs it within a 2.0 second window when resolving “Open Link …” actions—allowing page JS to steer the wrong URL. Also consider the pairing logic at capturedContextMenuLinkURLForCurrentMenu() (~1080–1086).

🐛 Proposed hardening
         window.addEventListener("contextmenu", (event) => {
+          if (!event || event.isTrusted !== true) return;
           post(linkForEvent(event));
         }, true);
🤖 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/Panels/CmuxWebView.swift` around lines 179 - 210, The injected
context-menu listener (contextMenuLinkCaptureBootstrapScriptSource) posts hrefs
for every contextmenu event and is vulnerable to synthetic events; update the
injected script to only post when event.isTrusted === true and the discovered
href is non-empty to prevent page JS steering URLs, and adjust the native
pairing logic in capturedContextMenuLinkURLForCurrentMenu() to require recent
trusted captures (e.g., ignore captures older than the existing 2.0s window or
mark captures as trusted) so only trusted, timely captures are resolved for
“Open Link …” actions.

…e.swift

Keeps CmuxWebView.swift and BrowserConfigTests.swift inside the Swift
file length budget: the capture hook, link resolution helpers, and the
zoom-aware hit-test coordinate conversion move to a dedicated extension
file, and the regression test gets its own wired test file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread Sources/Panels/CmuxWebView+ContextMenuLinkCapture.swift
Comment thread Sources/Panels/CmuxWebView+ContextMenuLinkCapture.swift

@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: 1

♻️ Duplicate comments (1)
Sources/Panels/CmuxWebView+ContextMenuLinkCapture.swift (1)

60-62: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Ignore synthetic contextmenu events in the capture hook.

This listener records page-scripted contextmenu events too. Because the native side later accepts any capture within the 2s pairing window, page JS can overwrite the stored href and steer the “Open Link …” actions to a different URL.

🔒 Minimal hardening
         window.addEventListener("contextmenu", (event) => {
+          if (!event || event.isTrusted !== true) return;
           post(linkForEvent(event));
         }, true);
🤖 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/Panels/CmuxWebView`+ContextMenuLinkCapture.swift around lines 60 -
62, The contextmenu listener currently captures synthetic page‑scripted events;
update the handler registered in window.addEventListener("contextmenu", ...) to
ignore non-user events by checking event.isTrusted (only call
post(linkForEvent(event)) when event.isTrusted is true) so that
synthetic/contextmenu events from page JS cannot overwrite the stored href; keep
the existing linkForEvent and post usages but gate them behind this trust check.
🤖 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 `@Sources/Panels/CmuxWebView`+ContextMenuLinkCapture.swift:
- Around line 218-221: The current subtree-wide fallback in collectChain(start)
uses el.querySelector('a[href],area[href]') which can pick up unrelated
descendant links when walking up to wrapper ancestors; remove that querySelector
fallback and instead only accept links that belong to the current element itself
(e.g. test el.matches('a[href],area[href]') or check immediate children only)
before calling normalize, so collectChain only captures links that are directly
on the node being inspected rather than anywhere in its subtree.

---

Duplicate comments:
In `@Sources/Panels/CmuxWebView`+ContextMenuLinkCapture.swift:
- Around line 60-62: The contextmenu listener currently captures synthetic
page‑scripted events; update the handler registered in
window.addEventListener("contextmenu", ...) to ignore non-user events by
checking event.isTrusted (only call post(linkForEvent(event)) when
event.isTrusted is true) so that synthetic/contextmenu events from page JS
cannot overwrite the stored href; keep the existing linkForEvent and post usages
but gate them behind this trust check.
🪄 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: 7c90bb08-fe81-43ca-b9de-55e931fd245a

📥 Commits

Reviewing files that changed from the base of the PR and between b796c19 and 04db05e.

📒 Files selected for processing (4)
  • Sources/Panels/CmuxWebView+ContextMenuLinkCapture.swift
  • Sources/Panels/CmuxWebView.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/CmuxWebViewContextMenuLinkCaptureTests.swift

Comment on lines +218 to +221
if (el.querySelector) {
const nestedLink = el.querySelector('a[href],area[href]');
if (nestedLink && nestedLink.href) return normalize(nestedLink.href);
}

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

Drop the subtree-wide fallback here.

collectChain(start) eventually reaches wrapper ancestors like cards or body, and el.querySelector('a[href],area[href]') then returns the first descendant link anywhere in that subtree. When capture is unavailable, that can still resolve the wrong link.

💡 Safer fallback
-                if (el.querySelector) {
-                    const nestedLink = el.querySelector('a[href],area[href]');
-                    if (nestedLink && nestedLink.href) return normalize(nestedLink.href);
-                }
📝 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.

Suggested change
if (el.querySelector) {
const nestedLink = el.querySelector('a[href],area[href]');
if (nestedLink && nestedLink.href) return normalize(nestedLink.href);
}
🤖 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/Panels/CmuxWebView`+ContextMenuLinkCapture.swift around lines 218 -
221, The current subtree-wide fallback in collectChain(start) uses
el.querySelector('a[href],area[href]') which can pick up unrelated descendant
links when walking up to wrapper ancestors; remove that querySelector fallback
and instead only accept links that belong to the current element itself (e.g.
test el.matches('a[href],area[href]') or check immediate children only) before
calling normalize, so collectChain only captures links that are directly on the
node being inspected rather than anywhere in its subtree.

…contextmenu decoy

The dogfood probe log (tag ctxlk2) showed the coordinate fallback resolving
the mirrored link on every right-click: WKWebView is a flipped view on macOS,
so subtracting from bounds.height double-flips Y. It also showed synthetic
contextmenu events are recorded just like real ones, so page JavaScript can
plant a decoy link (flagged by review bots on the PR).

Tests only; the untrusted-events test seam is declared but not yet enforced,
so both new tests fail at this commit.
…scope capture to one click

Three fixes from the ctxlk2 dogfood probe log:

- cssViewportPoint no longer flips Y: WKWebView is a flipped view on macOS,
  so view-local points are already top-left-origin. The old subtraction
  mirrored every fallback hit test vertically (probe log: clicking the top
  link resolved the bottom link, an HN title resolved the row's upvote URL).
  This was the pre-capture root cause of 'open in default browser opens the
  wrong link sometimes' and also affected image download/copy resolution,
  which share the helper.

- The capture message handler ignores synthetic contextmenu events
  (isTrusted == false), so page JavaScript cannot plant a decoy link
  (review-bot finding). Unit tests opt back in via the test seam.

- rightMouseDown / ctrl-mouseDown clear the previous capture, so a context
  menu can only pair with the link captured by the click that opened it; the
  2s pairing window now only bounds menu paths that never saw a mouse event.

Probe evidence (6/6 right-clicks): capture stored 10-24ms before willOpenMenu
and always matched the clicked link, while the coordinate fallback disagreed
every time until the Y-flip fix.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3d2f359056

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

let trusted = body?["trusted"] as? Bool ?? false
let href = body?["href"] as? String ?? ""
let url = href.isEmpty ? nil : URL(string: href)
Task { @MainActor [weak webView] in

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Record captured link synchronously

When WebKit delivers the contextmenu script message during the same right-click that opens the native menu, wrapping the state update in a MainActor task lets super.rightMouseDown continue into menu tracking before contextMenuCapturedLink is set. If the user then chooses “Open Link in Default Browser” / “Open Link in New Tab” on a zoomed page or iframe, resolveContextMenuLinkURL can still see no captured link and fall back to the coordinate hit test, reproducing the wrong-link behavior this change is meant to fix. Record the trusted report synchronously in the script-message callback instead of enqueueing it.

Useful? React with 👍 / 👎.

@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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Sources/Panels/CmuxWebView.swift (1)

2431-2456: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the captured-link resolver for "Download Linked File".

This action still begins with findLinkURLAtPoint(point), so right-clicks on zoomed pages or iframe content can still download the wrong target even though resolveContextMenuLinkURL(at:completion:) now has the captured href for this exact menu open.

💡 Minimal fix
-        findLinkURLAtPoint(point) { [weak self] url in
+        resolveContextMenuLinkURL(at: point) { [weak self] url in
             guard let self else { return }
             self.debugContextDownload(
                 "browser.ctxdl.resolve trace=\(traceID) kind=linked linkURL=\(url?.absoluteString ?? "nil")"
             )
🤖 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/Panels/CmuxWebView.swift` around lines 2431 - 2456, In
contextMenuDownloadLinkedFile, stop using findLinkURLAtPoint(point) and instead
call the captured-link resolver resolveContextMenuLinkURL(at:completion:) to
obtain the exact href captured when the context menu opened; then proceed with
the same normalization (normalizedLinkedDownloadURL) and
isDownloadSupportedScheme checks and call startContextMenuDownload as before.
Update the async completion closure references (currently using
findLinkURLAtPoint's callback) to use resolveContextMenuLinkURL(at:completion:)
and keep the existing trace/logging and fallback handling unchanged.
🤖 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 `@cmuxTests/CmuxWebViewContextMenuLinkCaptureTests.swift`:
- Line 155: Replace the locale-dependent title match on menu.items with a
selector-based match: instead of searching menu.items.first { $0.title == "Open
Link in Default Browser" }, find the item by comparing $0.action to the selector
that performs the "open link in default browser" behavior (e.g.,
`#selector`(yourTarget.openLinkInDefaultBrowser:)) so the test uses menu.items and
the resulting item variable but is independent of localized UI text.

---

Outside diff comments:
In `@Sources/Panels/CmuxWebView.swift`:
- Around line 2431-2456: In contextMenuDownloadLinkedFile, stop using
findLinkURLAtPoint(point) and instead call the captured-link resolver
resolveContextMenuLinkURL(at:completion:) to obtain the exact href captured when
the context menu opened; then proceed with the same normalization
(normalizedLinkedDownloadURL) and isDownloadSupportedScheme checks and call
startContextMenuDownload as before. Update the async completion closure
references (currently using findLinkURLAtPoint's callback) to use
resolveContextMenuLinkURL(at:completion:) and keep the existing trace/logging
and fallback handling 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: 0ea80cad-ccf9-4c50-83b3-fc14d72932c5

📥 Commits

Reviewing files that changed from the base of the PR and between 04db05e and 3d2f359.

📒 Files selected for processing (3)
  • Sources/Panels/CmuxWebView+ContextMenuLinkCapture.swift
  • Sources/Panels/CmuxWebView.swift
  • cmuxTests/CmuxWebViewContextMenuLinkCaptureTests.swift

)
webView.willOpenMenu(menu, with: rightMouseDown)

let item = try XCTUnwrap(menu.items.first { $0.title == "Open Link in Default Browser" })

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

Avoid English-title matching for a localized menu item in tests.

Line 155 finds the action item by "Open Link in Default Browser", which makes this regression test locale-dependent. Match by selector instead so the test validates behavior independent of UI language.

Suggested fix
-        let item = try XCTUnwrap(menu.items.first { $0.title == "Open Link in Default Browser" })
+        let openInDefaultBrowser = Selector(("contextMenuOpenLinkInDefaultBrowser:"))
+        let item = try XCTUnwrap(menu.items.first { $0.action == openInDefaultBrowser })

As per coding guidelines, user-facing text is localized across supported locales, so tests should avoid hard-coding English UI labels when validating behavior.

🤖 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 `@cmuxTests/CmuxWebViewContextMenuLinkCaptureTests.swift` at line 155, Replace
the locale-dependent title match on menu.items with a selector-based match:
instead of searching menu.items.first { $0.title == "Open Link in Default
Browser" }, find the item by comparing $0.action to the selector that performs
the "open link in default browser" behavior (e.g.,
`#selector`(yourTarget.openLinkInDefaultBrowser:)) so the test uses menu.items and
the resulting item variable but is independent of localized UI text.

Source: Coding guidelines

…andler

Autoreview finding: the Task { @mainactor } hop unordered the capture store
relative to rightMouseDown clearing and willOpenMenu, so a deferred report
from the previous click could repopulate the capture after the clear and pair
with the new menu. WebKit delivers script messages on the main thread; apply
synchronously via MainActor.assumeIsolated, same pattern and reasoning as
BrowserMediaPlaybackMessageHandler.

@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 d0e8d36. Configure here.

Comment thread Sources/Panels/CmuxWebView+ContextMenuLinkCapture.swift
…e IPC hop

Drops isTrusted == false events inside the capture hook itself instead of in
the native handler: the page cannot plant a decoy link and a synthetic
dispatch loop cannot flood the script-message bridge. The test seam is baked
into the script at install time; the production bridge path never consults
it.
Repo test policy: new non-UI tests use Swift Testing. Same three regression
tests, now a @mainactor @suite(.serialized) struct following the
BrowserWebContentProcessTests pattern.
…tale captures by menu-event time

Two autoreview findings:

- contextMenuDownloadLinkedFile resolved with the raw coordinate hit test;
  it now goes through resolveContextMenuLinkURL like the Open Link actions,
  so the captured contextmenu target wins and coordinates are only a
  fallback.

- Pairing now requires the capture to be newer than the NSEvent that opened
  the menu (same uptime clock, and the DOM contextmenu event always follows
  the AppKit event for the same interaction). A keyboard-opened menu can no
  longer reuse a capture left over from an earlier right-click; mouse-down
  clearing remains as defense in depth.
Aziz file-organization finding: the type, its writer, and its pairing logic
now live together in CmuxWebView+ContextMenuLinkCapture.swift; only the
stored property stays in the class body (extensions cannot add stored
properties).
@lawrencecchen
lawrencecchen merged commit f2627b9 into main Jun 12, 2026
21 checks passed
@lawrencecchen
lawrencecchen deleted the task-browser-context-menu-wrong-link branch June 12, 2026 01:00
@coderabbitai coderabbitai Bot mentioned this pull request Jun 26, 2026
5 tasks done
@lawrencecchen
lawrencecchen restored the task-browser-context-menu-wrong-link branch July 18, 2026 10:25

This branch was successfully deployed

1 active deployment
Preview – cmux — c1bbdb34 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