Skip to content

Fix LinkedIn external-link redirect handoff in browser pane (#2928)#2930

Open
austinywang wants to merge 2 commits intomainfrom
issue-2928-linkedin-redirect
Open

Fix LinkedIn external-link redirect handoff in browser pane (#2928)#2930
austinywang wants to merge 2 commits intomainfrom
issue-2928-linkedin-redirect

Conversation

@austinywang
Copy link
Copy Markdown
Contributor

@austinywang austinywang commented Apr 16, 2026

Summary

  • preserve the original URLRequest when browser popup and new-tab retargeting hands off a navigation
  • make popup WebKit session inheritance unconditional so popups always reuse the opener websiteDataStore and processPool
  • add regression coverage for request-preserving redirect handoff and popup context inheritance

Testing

  • not run locally, per repo policy
  • attempted ./scripts/reload.sh --tag issue-2928-linkedin-redirect and ./scripts/reload.sh --tag issue-2928-linkedin-redirect --launch, but the local Xcode build on this machine stalled before compile tasks began, so the dev app could not be launched for manual verification

Closes #2928


Note

Medium Risk
Touches browser navigation routing and popup/new-tab creation paths; bugs could alter how links open or which headers/cookies are sent during redirects.

Overview
Fixes new-tab and popup retargeting to carry the full original URLRequest (including Referer, custom headers, method, and body) so redirect/interstitial flows like LinkedIn’s don’t lose context.

This threads an initialRequest through Workspace.newBrowserSurface/BrowserPanel creation, updates BrowserNavigationDelegate + openLinkInNewTab/popup opener handoff to prefer request-based navigation, and introduces an explicit BrowserPopupBrowserContext to ensure popups always reuse the opener’s websiteDataStore and processPool.

Adds regression tests for request-preserving new-tab seeding and popup context inheritance.

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


Summary by cubic

Fixes LinkedIn external-link redirect handoff by preserving the original request when opening in a new tab or popup, and by making popup WebKit session inheritance unconditional. Fixes #2928.

  • Bug Fixes
    • Preserve the full URLRequest (method, body, headers like Referer) when cmd/middle-clicking or retargeting links to a new tab; plumbs initialRequest through Workspace.newBrowserSurface.
    • Popups always reuse the opener’s websiteDataStore and processPool so cookies and OAuth flows work consistently.
    • Added tests for request-preserving new-tab handoff and popup context inheritance.

Written for commit 8f5114f. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Enhanced new tab navigation to preserve request headers, HTTP method, and body data during navigation.
    • Improved popup window handling with consistent context inheritance from the opener window.
  • Tests

    • Added test coverage for request preservation during new tab navigation.

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 16, 2026

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

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Apr 16, 2026 1:59am

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 16, 2026

📝 Walkthrough

Walkthrough

This PR implements full URLRequest propagation through browser navigation paths, enabling preservation of HTTP headers, method, and body when opening links in new tabs and popups, while establishing a shared browser context between opener and popup windows.

Changes

Cohort / File(s) Summary
New request-based navigation architecture
Sources/Panels/BrowserPanel.swift
Added BrowserNewTabNavigationSeed struct and browserNewTabNavigationSeed(from:...) builder; extended BrowserPanel initializer to accept initialRequest: URLRequest? for request-based navigation; refactored openLinkInNewTab to accept full URLRequest via new overload while preserving URL-based compatibility; added requestNavigation callbacks to BrowserNavigationDelegate and BrowserUIDelegate to pass complete requests through navigation decision points.
Popup browser context sharing
Sources/Panels/BrowserPanel.swift, Sources/Panels/BrowserPopupWindowController.swift
Added BrowserPopupBrowserContext type and BrowserPanel.popupBrowserContext computed property to expose opener's websiteDataStore and processPool; updated BrowserPopupWindowController to require and store browserContext, unconditionally configuring WKWebViewConfiguration from it; modified popup-to-opener navigation to route via openInOpenerTab(_ request: URLRequest) instead of URL-based method.
Workspace integration
Sources/Workspace.swift
Added initialRequest: URLRequest? = nil parameter to newBrowserSurface method; threaded parameter through to BrowserPanel initialization for request-aware surface creation.
Request preservation validation
cmuxTests/GhosttyConfigTests.swift
Added BrowserNewTabNavigationSeedTests test class validating that browserNewTabNavigationSeed preserves URL, HTTP method, body, headers (Referer, custom headers), and bypass-host value while setting appropriate cache policy.

Sequence Diagram(s)

sequenceDiagram
    participant WV as WebView
    participant ND as NavigationDelegate
    participant BP as BrowserPanel
    participant WS as Workspace
    participant NBS as NewBrowserSurface

    WV->>ND: decidePolicyFor navigationAction<br/>(target="_blank" or window.open)
    ND->>ND: Extract request from navigationAction
    ND->>BP: requestNavigation(request, .newTab)
    BP->>BP: Build BrowserNewTabNavigationSeed<br/>from URLRequest
    BP->>WS: newBrowserSurface(initialRequest: seed.initialRequest)
    WS->>NBS: Create BrowserPanel<br/>with initialRequest
    NBS->>NBS: Configure WebView<br/>Navigate via URLRequest<br/>(preserves headers, method, body)
    NBS-->>BP: New surface created
    BP-->>ND: Navigation handled
    ND-->>WV: Cancel original navigation
Loading
sequenceDiagram
    participant Parent as Parent WebView
    participant PND as PopupNavigationDelegate
    participant PWC as BrowserPopupWindowController
    participant BPC as BrowserPopupBrowserContext
    participant Popup as Popup WebView

    Parent->>PND: Popup navigation request
    PND->>PWC: openInOpenerTab(request)
    PWC->>PWC: Check openerPanel exists
    PWC->>BPC: Access shared browserContext<br/>(websiteDataStore, processPool)
    BPC-->>PWC: Return context
    PWC->>Parent: openerPanel.openLinkInNewTab(request)
    Parent->>Parent: New tab created with<br/>shared context & full request
    Parent-->>PWC: Acknowledged
    PWC-->>Popup: Navigation complete
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • manaflow-ai/cmux#1600: Introduces shared browser context between opener and popups via BrowserPopupBrowserContext, which this PR builds upon and refines for full URLRequest propagation.
  • manaflow-ai/cmux#1296: Modifies BrowserPanel initializer signatures and navigation/open-in-new-tab flows, directly intersecting with this PR's refactoring of those same code paths.
  • manaflow-ai/cmux#1493: Updates popup retargeting and new-window navigation logic in BrowserPanel, overlapping with this PR's changes to navigation delegate decision paths and request handling.

Poem

🐰✨ Requests hop through tabs with flair,
Headers, bodies, cookies shared with care,
Popups now inherit opener's gleam,
LinkedIn redirects fulfill their dream! 🌐

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the primary change: fixing LinkedIn external-link redirect handoff in the browser pane, directly addressing issue #2928.
Description check ✅ Passed The description covers the main changes (preserving URLRequest, unconditional popup WebKit session inheritance, regression tests) and mentions testing approach, but lacks a demo video and incomplete checklist.
Linked Issues check ✅ Passed The changes comprehensively address all objectives from #2928: unconditional popup session inheritance (BrowserPopupBrowserContext), full URLRequest preservation through new-tab/popup retargeting, and regression test coverage for request-preserving handoff.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the stated objectives: URLRequest preservation in navigation routing, popup context inheritance, and regression testing for the LinkedIn redirect issue.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 issue-2928-linkedin-redirect

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.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 16, 2026

Greptile Summary

Fixes LinkedIn external-link redirect handoff by propagating the full URLRequest (with Referer and other headers) through the new-tab pipeline instead of extracting only the URL, and makes popup WKWebsiteDataStore/WKProcessPool inheritance unconditional. All changed code paths are internally consistent and the existing popup-context tests are preserved alongside the new seed tests.

Confidence Score: 5/5

Safe to merge; all remaining findings are P2 style/documentation suggestions with no impact on correctness.

The core logic — preserving the full URLRequest through the new-tab handoff and unconditionally inheriting the opener's WebKit browsing context — is correct and consistent with how BrowserUIDelegate already handled this. The initialRequest/initialURL priority in BrowserPanel.init is well-ordered (initialRequest wins), avoiding any double navigation. The only findings are a cosmetic dead-branch in two debug log strings and a test scope note. No P0/P1 issues found.

No files require special attention.

Important Files Changed

Filename Overview
Sources/Panels/BrowserPanel.swift Adds BrowserNewTabNavigationSeed/BrowserPopupBrowserContext structs and openLinkInNewTab(request:) overload; routes new-tab navigation through full URLRequest rather than bare URL; adds requestNavigation callback to BrowserNavigationDelegate; wires popupBrowserContext into createFloatingPopup.
Sources/Panels/BrowserPopupWindowController.swift Stores browserContext explicitly; removes conditional WebKit-context inheritance so all popups (including nested) unconditionally inherit the opener panel's websiteDataStore and processPool; updates openInOpenerTab to accept URLRequest.
Sources/Workspace.swift Threads initialRequest parameter through newBrowserSurface → BrowserPanel.init; minimal plumbing change, no logic altered.
cmuxTests/GhosttyConfigTests.swift Adds BrowserNewTabNavigationSeedTests validating that browserNewTabNavigationSeed preserves method, body, custom headers, bypass host, and resets cachePolicy; test is a pure-function unit test of a new helper, though it cannot cover the full end-to-end WebKit request replay.

Sequence Diagram

sequenceDiagram
    participant User
    participant NavDelegate as BrowserNavigationDelegate
    participant Panel as BrowserPanel
    participant Workspace
    participant NewPanel as New BrowserPanel

    User->>NavDelegate: cmd+click / target=_blank
    NavDelegate->>NavDelegate: openRequestInNewTab(navigationAction.request)
    NavDelegate->>Panel: requestNavigation(request, .newTab)
    Panel->>Panel: openLinkInNewTab(request:)
    Panel->>Panel: browserNewTabNavigationSeed(from: request)
    Note over Panel: Preserves method/headers/body<br/>Resets cachePolicy to .useProtocolCachePolicy
    Panel->>Workspace: newBrowserSurface(url: seed.url, initialRequest: seed.initialRequest)
    Workspace->>NewPanel: init(initialURL: url, initialRequest: request)
    NewPanel->>NewPanel: navigateWithoutInsecureHTTPPrompt(request: initialRequest)

    Note over User,NewPanel: Popup path
    User->>Panel: window.open() scripted popup
    Panel->>Panel: createFloatingPopup(configuration, windowFeatures)
    Panel->>Panel: popupBrowserContext (websiteDataStore + processPool)
    Panel->>NavDelegate: BrowserPopupWindowController(browserContext: popupBrowserContext)
    Note over NavDelegate: configureWebViewConfiguration unconditional<br/>Nested popups inherit same opener context
Loading

Reviews (1): Last reviewed commit: "Preserve browser redirect request contex..." | Re-trigger Greptile

Comment on lines +1186 to +1196
XCTAssertEqual(seed.initialRequest.httpMethod, "POST")
XCTAssertEqual(seed.initialRequest.httpBody, body)
XCTAssertEqual(
seed.initialRequest.value(forHTTPHeaderField: "Referer"),
"https://www.linkedin.com/feed/"
)
XCTAssertEqual(
seed.initialRequest.value(forHTTPHeaderField: "X-Cmux-Test"),
"keep-me"
)
XCTAssertEqual(seed.initialRequest.cachePolicy, .useProtocolCachePolicy)
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 POST body / cache-policy assertions may not reflect real-world trigger path

The test constructs a POST request with httpBody and verifies the seed preserves it — but LinkedIn's redirect interstitials are driven by server-side 302/HTML-meta redirects on GET requests, not POSTs. The POST + body path through navigateWithoutInsecureHTTPPrompt and then into a WKWebView.load(_:) call is untested at the WebKit level, and WebKit commonly drops or ignores non-standard headers (including Referer) on programmatic loads. The test proves the struct properties are propagated correctly, but doesn't confirm WebKit actually replays those values — consider adding a note in the test that this verifies the seed struct only, not the end-to-end navigation fidelity.

Comment on lines 6265 to 6275
if shouldOpenInNewTab,
let url = navigationAction.request.url {
navigationAction.request.url != nil {
#if DEBUG
dlog("browser.nav.decidePolicy.action kind=openInNewTab url=\(url.absoluteString)")
dlog(
"browser.nav.decidePolicy.action kind=openInNewTab url=\(navigationAction.request.url?.absoluteString ?? "nil")"
)
#endif
openInNewTab?(url)
openRequestInNewTab(navigationAction.request)
decisionHandler(.cancel)
return
}
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 Debug log uses ?? "nil" after nil-check guard

Both the openInNewTab and openInNewTabFromNilTarget branches now guard with navigationAction.request.url != nil but the debug log inside each branch still uses optional-chain + ?? "nil". Since the nil case can never be reached at that point, the fallback string is dead. A forced unwrap (or a local let url = ... rebinding) would make the intent clearer.

Suggested change
if shouldOpenInNewTab,
let url = navigationAction.request.url {
navigationAction.request.url != nil {
#if DEBUG
dlog("browser.nav.decidePolicy.action kind=openInNewTab url=\(url.absoluteString)")
dlog(
"browser.nav.decidePolicy.action kind=openInNewTab url=\(navigationAction.request.url?.absoluteString ?? "nil")"
)
#endif
openInNewTab?(url)
openRequestInNewTab(navigationAction.request)
decisionHandler(.cancel)
return
}
dlog(
"browser.nav.decidePolicy.action kind=openInNewTab url=\(navigationAction.request.url!.absoluteString)"
)

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/Panels/BrowserPanel.swift`:
- Around line 2792-2795: initialRequest is currently sent directly to
navigateWithoutInsecureHTTPPrompt(...) and bypasses the insecure-HTTP
warning/allowlist flow that initialURL goes through; change the initialRequest
branch to run through the same insecure-HTTP gate used for initialURL (the same
check/prompt/allowlist path) before setting shouldRenderWebView and navigating,
so that callers passing a plain-HTTP request cannot skip the warning. Locate the
branch handling initialRequest/initialURL and replace the direct call to
navigateWithoutInsecureHTTPPrompt(request: initialRequest, ...) with the same
insecure-HTTP handling logic used for initialURL (invoke that
gate/prompt/allowlist path and only call navigateWithoutInsecureHTTPPrompt if
the gate permits).
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: e1da5434-a96c-4a21-96b8-2f749d44eb6f

📥 Commits

Reviewing files that changed from the base of the PR and between 35167aa and 8f5114f.

📒 Files selected for processing (4)
  • Sources/Panels/BrowserPanel.swift
  • Sources/Panels/BrowserPopupWindowController.swift
  • Sources/Workspace.swift
  • cmuxTests/GhosttyConfigTests.swift

Comment on lines +2792 to +2795
if let initialRequest {
shouldRenderWebView = true
navigateWithoutInsecureHTTPPrompt(request: initialRequest, recordTypedNavigation: false)
} else if let url = initialURL {
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

initialRequest bypasses the insecure-HTTP warning path.

Line 2792 now loads initialRequest via navigateWithoutInsecureHTTPPrompt(...), so any caller that passes a blocked plain-HTTP request skips the warning/allowlist flow that initialURL still gets. Route this branch through the same insecure-HTTP gate first so the new API doesn't silently weaken the protection.

⚠️ Proposed fix
-        if let initialRequest {
-            shouldRenderWebView = true
-            navigateWithoutInsecureHTTPPrompt(request: initialRequest, recordTypedNavigation: false)
+        if let initialRequest {
+            shouldRenderWebView = true
+            if let url = initialRequest.url,
+               shouldBlockInsecureHTTPNavigation(to: url) {
+                presentInsecureHTTPAlert(
+                    for: initialRequest,
+                    intent: .currentTab,
+                    recordTypedNavigation: false
+                )
+            } else {
+                navigateWithoutInsecureHTTPPrompt(
+                    request: initialRequest,
+                    recordTypedNavigation: false
+                )
+            }
         } else if let url = initialURL {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Panels/BrowserPanel.swift` around lines 2792 - 2795, initialRequest
is currently sent directly to navigateWithoutInsecureHTTPPrompt(...) and
bypasses the insecure-HTTP warning/allowlist flow that initialURL goes through;
change the initialRequest branch to run through the same insecure-HTTP gate used
for initialURL (the same check/prompt/allowlist path) before setting
shouldRenderWebView and navigating, so that callers passing a plain-HTTP request
cannot skip the warning. Locate the branch handling initialRequest/initialURL
and replace the direct call to navigateWithoutInsecureHTTPPrompt(request:
initialRequest, ...) with the same insecure-HTTP handling logic used for
initialURL (invoke that gate/prompt/allowlist path and only call
navigateWithoutInsecureHTTPPrompt if the gate permits).

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

No issues found across 4 files

Copy link
Copy Markdown

@cursor cursor bot left a comment

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.

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 8f5114f. Configure here.

if let url = initialURL {
if let initialRequest {
shouldRenderWebView = true
navigateWithoutInsecureHTTPPrompt(request: initialRequest, recordTypedNavigation: false)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

New tab init bypasses insecure HTTP check

Medium Severity

The openLinkInNewTab(url:) overload now delegates to openLinkInNewTab(request:), which always creates a seed containing a non-nil initialRequest. When the new BrowserPanel is initialized with initialRequest, it calls navigateWithoutInsecureHTTPPrompt instead of navigate(to:). The old navigate(to:) path called shouldBlockInsecureHTTPNavigation and showed the insecure HTTP alert; the new path skips that check entirely. This regresses callers that relied on the new panel's init to perform the check — specifically context menu "Open Link in New Tab" (onContextMenuOpenLinkInNewTab) and popup-to-new-tab via openInOpenerTab — since those paths call openLinkInNewTab directly without a prior insecure HTTP guard. A secondary consequence is that insecureHTTPBypassHostOnce is stored but never consumed, leaving a stale one-time bypass for a future navigation.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8f5114f. Configure here.

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.

LinkedIn external links open to 'not found' page in browser pane

1 participant