Skip to content

fix: open links in MarkdownPanelView via explicit OpenURLAction - #3558

Merged
austinywang merged 2 commits into
manaflow-ai:mainfrom
psh4607:fix-markdown-panel-link-handler
May 5, 2026
Merged

austinywang merged 2 commits into
manaflow-ai:mainfrom
psh4607:fix-markdown-panel-link-handler

Conversation

@psh4607

@psh4607 psh4607 commented May 5, 2026 •

Copy link
Copy Markdown
Contributor

Fix: links in MarkdownPanelView are styled but never opened on click

Summary

  • What changed? Added an explicit OpenURLAction to the Markdown(panel.content) view in Sources/Panels/MarkdownPanelView.swift so link clicks inside the rendered .md panel route through NSWorkspace.shared.open(url).
  • Why? Without it, links are visually rendered (color-styled via .link { ForegroundColor(...) }) but clicking them does nothing. The combination of MarkdownPanelPointerObserver (left-click overlay used for first-click panel focus + event forwarding) and .textSelection(.enabled) interferes with SwiftUI Link's default activation path, so the \.openURL environment is never invoked. Setting it explicitly mirrors the existing NSWorkspace.shared.open(url) pattern used everywhere else in Sources/ (e.g. ContentView.swift, SessionIndexView.swift, GhosttyTerminalView.swift, AuthManager.swift, etc. — see git grep "NSWorkspace.shared.open").

Diff:

                 Markdown(panel.content)
                     .markdownTheme(cmuxMarkdownTheme)
                     .textSelection(.enabled)
+                    // Explicit handler: pointer-observer overlay + .textSelection
+                    // suppress SwiftUI Link's default activation, leaving links
+                    // styled but un-clickable without this.
+                    .environment(\.openURL, OpenURLAction { url in
+                        NSWorkspace.shared.open(url)
+                        return .handled
+                    })
                     .padding(.horizontal, 24)
                     .padding(.vertical, 16)

Net: 7 added lines, 1 file (Sources/Panels/MarkdownPanelView.swift). No new imports — AppKit and SwiftUI are already imported.

Scope (what this does NOT fix)

This PR is intentionally narrow. It fixes only the rendered .md file viewer panel (MarkdownPanel opened via Workspace.openOrFocusMarkdownSplit(...)).

It does not touch:

  • The Ghostty terminal panel's OSC 8 hyperlink / cmd-click handling (separate code path in Sources/GhosttyTerminalView.swift's GHOSTTY_ACTION_OPEN_URL case).
  • Any link-related routing settings (BrowserLinkOpenSettings, CmdClickMarkdownRouteSettings, BrowserAvailabilitySettings).
  • The cmd-click → markdown viewer routing for .md paths in terminals (Route .md file clicks to markdown panel instead of system default #1283).

Related issues that this PR does not claim to resolve but might tangentially help:

Testing

  • Local compile: could not run a full xcodebuild build locally — GhosttyKit.xcframework requires zig (per CONTRIBUTING.md's ./scripts/setup.sh) which isn't installed in my environment. SourceKit-LSP diagnostics on the changed file are clean.
  • What I verified manually: read every existing OpenURLAction and NSWorkspace.shared.open call site in Sources/ — the new code matches the project's existing pattern. The OpenURLAction API and NSWorkspace.shared.open(_:) are both stable AppKit/SwiftUI APIs (macOS 11+ / 10.x+ respectively), well below the project's deployment target.
  • CI is the source of truth here — please run the full build via the standard pipeline.
  • No new tests added intentionally. AGENTS.md Test quality policy says "Tests must verify observable runtime behavior through executable paths" and "Do not add tests that only verify source code text". A unit test that asserts OpenURLAction is wired would be exactly the AST-shape test that policy forbids. The behavioral test is "open a .md file with a link in cmux's markdown panel, click the link, expect default handler to open" — that requires the running app and is therefore appropriate for a manual / E2E check, not a unit test.

Demo Video

For UI or behavior changes, include a short demo video (GitHub upload, Loom, or other direct link).

  • Video URL or attachment: <to be added by submitter — open any .md file with an http/https/file link in cmux's markdown panel and click; before this PR, the click does nothing; after, the URL opens in the default handler>

Review Trigger (Copy/Paste as PR comment)

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

Checklist

  • I tested the change locally — blocked on zig / xcframework setup; CI build expected to validate
  • I added or updated tests for behavior changes — N/A; behavioral test would violate Test quality policy (would only assert AST shape). Verification path is manual / E2E in the running app.
  • I updated docs/changelog if needed — no user-facing docs changed; CHANGELOG.md update intentionally deferred since /release flow regenerates it
  • I requested bot reviews after my latest commit (copy/paste block above or equivalent)
  • All code review bot comments are resolved
  • All human review comments are resolved

Summary by CodeRabbit

  • Bug Fixes
    • Links in markdown panels now open using the system URL handler (e.g., the default browser), preserving their original visual styling. If opening externally fails, the app falls back to the system's default handling to ensure consistent behavior.

The rendered `.md` file panel styled hyperlinks via the markdown theme but
left them un-clickable: the combination of the `MarkdownPanelPointerObserver`
overlay and `.textSelection(.enabled)` suppresses SwiftUI `Link`'s default
activation, so the `\.openURL` environment was never invoked and clicks
fell through to selection. Wire it explicitly to `NSWorkspace.shared.open`,
matching the pattern already used elsewhere in `Sources/`.
Copilot AI review requested due to automatic review settings May 5, 2026 15:25
@vercel

vercel Bot commented May 5, 2026

Copy link
Copy Markdown

@psh4607 is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@psh4607

psh4607 commented May 5, 2026

Copy link
Copy Markdown
Contributor Author

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create an environment for this repo.

@coderabbitai

coderabbitai Bot commented May 5, 2026 •

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f659a340-67cc-4c41-adbe-64b94ef54052

📥 Commits

Reviewing files that changed from the base of the PR and between d49cd71 and 7c8a70a.

📒 Files selected for processing (1)
  • Sources/Panels/MarkdownPanelView.swift

📝 Walkthrough

Walkthrough

The MarkdownPanelView now sets an explicit .environment(\.openURL, OpenURLAction { ... }) on the rendered Markdown, which opens clicked URLs via NSWorkspace.shared.open(url) and returns .handled on success or .systemAction on failure.

Changes

Link Opening Behavior Override

Layer / File(s) Summary
Core Implementation
Sources/Panels/MarkdownPanelView.swift
Markdown(panel.content) now attaches .environment(\.openURL, OpenURLAction { url in NSWorkspace.shared.open(url); return .handled }), and returns .systemAction when NSWorkspace.shared.open(url) fails, overriding SwiftUI's default openURL handling for this Markdown view.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~5 minutes

Poem

🐰 I nudge the link, a gentle tap,
Through workspace doors I chart the map,
Default steps replaced today—
The rabbit opens paths our way. 🥕🔗

🚥 Pre-merge checks | ✅ 13
✅ Passed checks (13 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and specifically describes the main change: adding an explicit OpenURLAction to enable link opening in MarkdownPanelView.
Description check ✅ Passed The description is comprehensive and covers all major sections from the template: Summary (what/why), Testing, Demo Video, Review Trigger, and Checklist with detailed explanations.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 PR adds OpenURLAction closure to SwiftUI View. Closure doesn't capture mutable state, only calls thread-safe NSWorkspace.shared.open(). SwiftUI Views on MainActor are explicitly allowed per rules.
Cmux Swift Blocking Runtime ✅ Passed PR adds NSWorkspace.shared.open() call. No blocking primitives (semaphores, sleeps, delays, locks) introduced per blocking runtime rules. Standard AppKit API for main-thread use.
Cmux Swift Concurrency ✅ Passed PR adds a SwiftUI OpenURLAction handler for markdown links. No legacy async patterns are introduced or expanded per the modernization rules.
Cmux Swift @Concurrent ✅ Passed The PR adds a synchronous OpenURLAction closure for link handling. No async work, @concurrent violations, or actor isolation issues. Follows standard SwiftUI patterns for UI-bound operations.
Cmux Swift File And Package Boundaries ✅ Passed PR adds 12 lines to 411-line view file. Focused bug fix to URL handling. File has clear single UI responsibility. No violations of size, responsibility, or boundary rules.
Cmux Swift Logging ✅ Passed The PR adds a new file with OpenURLAction code (no logging) and NSLog statements properly guarded by #if DEBUG, which is explicitly allowed per swift-logging.md rules.
Cmux Swiftui State Layout ✅ Passed PR adds .environment(\.openURL, ...) modifier to Markdown view. This does not violate state/layout rules. DispatchQueue mutations occur in proper contexts (onChange, AppKit bridge).
Cmux Architecture Rethink ✅ Passed Small local fix with clear owner and invariant. Platform bridge override for SwiftUI's unreliable Link activation. No timing repairs, locks, observers, or split lifecycle.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

@coderabbitai

coderabbitai Bot commented May 5, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@psh4607 Sure! I'll start reviewing the changes in this PR right away.

[review]

@cubic-dev-ai

cubic-dev-ai Bot commented May 5, 2026

Copy link
Copy Markdown

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

@psh4607 I have started the AI code review. It will take a few minutes to complete.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Swish!

ℹ️ 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".

Copilot AI 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.

Pull request overview

This PR fixes link activation inside the app’s rendered Markdown panel by explicitly wiring a SwiftUI OpenURLAction for the Markdown(panel.content) view, ensuring link clicks are opened via NSWorkspace.shared.open(_:) even when the pointer observer overlay and .textSelection(.enabled) interfere with the default SwiftUI activation path.

Changes:

  • Add an explicit \.openURL environment override for the Markdown renderer in MarkdownPanelView.
  • Route link clicks to NSWorkspace.shared.open(url) and return .handled from the OpenURLAction.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

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

greptile-apps Bot commented May 5, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a bug where links rendered inside MarkdownPanelView were visually styled but silently non-functional on click, caused by .textSelection(.enabled) and the MarkdownPanelPointerObserverView overlay suppressing SwiftUI's default Link activation path.

  • Adds .environment(\\.openURL, OpenURLAction { url in NSWorkspace.shared.open(url) ? .handled : .systemAction }) scoped to the Markdown(panel.content) view — the correctly narrow placement matches the existing NSWorkspace.shared.open pattern used throughout Sources/.
  • The actual implementation is stronger than the PR description's snippet: it returns .systemAction (not unconditionally .handled) when NSWorkspace.shared.open returns false, so unhandled URL schemes fall through to the system rather than being silently swallowed.

Confidence Score: 5/5

Safe to merge — a narrow, well-scoped correctness fix that routes previously dead link clicks to the system URL handler without touching any other code path.

The change is seven lines in one file, correctly scoped to the Markdown view, and matches the established NSWorkspace.shared.open pattern already used across Sources/. The implementation handles the failure case properly (.systemAction fallback) and introduces no new concurrency, state, or lifecycle concerns.

No files require special attention.

Important Files Changed

Filename Overview
Sources/Panels/MarkdownPanelView.swift Adds an explicit .environment(\.openURL, OpenURLAction { ... }) on the MarkdownUI view so link clicks route through NSWorkspace.shared.open(url) rather than the unreliable SwiftUI default path; returns .systemAction on failure rather than always .handled.

Sequence Diagram

sequenceDiagram
    participant User
    participant MarkdownPanelView
    participant OpenURLAction
    participant NSWorkspace

    User->>MarkdownPanelView: Click link in rendered Markdown
    Note over MarkdownPanelView: Default SwiftUI Link path suppressed by<br/>.textSelection(.enabled) + PointerObserver
    MarkdownPanelView->>OpenURLAction: \.openURL environment invoked with url
    OpenURLAction->>NSWorkspace: NSWorkspace.shared.open(url)
    alt URL opened successfully
        NSWorkspace-->>OpenURLAction: true
        OpenURLAction-->>MarkdownPanelView: .handled
    else No registered handler
        NSWorkspace-->>OpenURLAction: false
        OpenURLAction-->>MarkdownPanelView: .systemAction (fallback)
    end
Loading

Reviews (2): Last reviewed commit: "review: surface NSWorkspace.shared.open ..." | Re-trigger Greptile

…x misleading comment

Addresses review feedback on manaflow-ai#3558:

- Copilot: returning .handled unconditionally masks failures from
  NSWorkspace.shared.open (Bool return). Capture the result and fall
  back to .systemAction on failure so a click never silently no-ops.
- Greptile (P2) + Copilot: previous comment blamed the
  MarkdownPanelPointerObserver overlay together with .textSelection
  for suppressing default Link activation, contradicting the existing
  observer comment which (correctly) states the overlay does not
  intercept clicks. Reword to describe the observed effect (default
  Link path does not fire reliably) without misattributing the cause.
@psh4607

psh4607 commented May 5, 2026

Copy link
Copy Markdown
Contributor Author

Pushed 7c8a70a3 addressing review feedback:

  • Copilot (line 69) — .handled masks NSWorkspace.shared.open failures. Now captures the Bool and returns .handled only on success; falls back to .systemAction so a click never silently no-ops if no handler is registered for the scheme.
  • Greptile P2 + Copilot (line 66) — comment contradicts the existing pointer-observer comment on line 37. The previous wording wrongly blamed the MarkdownPanelPointerObserverView overlay; the observer in fact forwards events without intercepting them (line 37 is correct). Reworded to describe the observed effect (default Link path does not fire reliably) without misattributing the cause, so line 37 and the new comment are no longer in conflict.

Re-requesting reviews for the new commit:

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

@cubic-dev-ai

cubic-dev-ai Bot commented May 5, 2026

Copy link
Copy Markdown

Pushed 7c8a70a3 addressing review feedback:

  • Copilot (line 69) — .handled masks NSWorkspace.shared.open failures. Now captures the Bool and returns .handled only on success; falls back to .systemAction so a click never silently no-ops if no handler is registered for the scheme.
  • Greptile P2 + Copilot (line 66) — comment contradicts the existing pointer-observer comment on line 37. The previous wording wrongly blamed the MarkdownPanelPointerObserverView overlay; the observer in fact forwards events without intercepting them (line 37 is correct). Reworded to describe the observed effect (default Link path does not fire reliably) without misattributing the cause, so line 37 and the new comment are no longer in conflict.

...

@psh4607 I have started the AI code review. It will take a few minutes to complete.

@coderabbitai

coderabbitai Bot commented May 5, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Fetch the latest content of MarkdownPanelView.swift from the PR branch
gh api repos/manaflow-ai/cmux/contents/Sources/Panels/MarkdownPanelView.swift \
  --jq '.content' | base64 --decode

Length of output: 14646


🏁 Script executed:

#!/bin/bash
# Fetch the file from the PR branch
gh api "repos/manaflow-ai/cmux/contents/Sources/Panels/MarkdownPanelView.swift?ref=fix-markdown-panel-link-handler" \
  --jq '.content' | base64 --decode

Length of output: 97


🏁 Script executed:

#!/bin/bash
# Fetch PR details to get correct head branch
gh pr view 3558 --repo manaflow-ai/cmux --json headRefName,headRefOid

# Fetch the file using the commit SHA directly
gh api "repos/manaflow-ai/cmux/contents/Sources/Panels/MarkdownPanelView.swift?ref=7c8a70a3" \
  --jq '.content' | base64 --decode

Length of output: 15404


@psh4607 Both fixes are verified in commit 7c8a70a3 — the code looks good:

Fix 1 — Failure surfacing:

.environment(\.openURL, OpenURLAction { url in
    NSWorkspace.shared.open(url) ? .handled : .systemAction
})

Correctly captures NSWorkspace.shared.open(url)'s Bool return and falls back to .systemAction on failure, so unregistered-scheme clicks propagate through SwiftUI's fallback chain rather than being silently swallowed. ✅

Fix 2 — Comment accuracy:
The revised comment describes the observed effect ("SwiftUI's default Link path does not fire reliably for the rendered Markdown in this panel") without misattributing the cause to MarkdownPanelPointerObserverView. This is now fully consistent with the observer's own comment at line 37 ("Observe left-clicks without intercepting them…"). ✅

No other concerns with the implementation. The change remains narrow in scope, idiomatic, and consistent with other NSWorkspace.shared.open usage in the project.


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

ℹ️ 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".

@psh4607

psh4607 commented May 5, 2026

Copy link
Copy Markdown
Contributor Author

Hey @lawrencecchen @austinywang — when you get a chance, could one of you authorize the Vercel deploy and take a look? All bot reviews (Codex, CodeRabbit, Greptile, cubic) came back clean. Thanks!

@vercel

vercel Bot commented May 5, 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 May 5, 2026 7:05pm

@austinywang
austinywang merged commit b26f624 into manaflow-ai:main May 5, 2026
18 checks passed
@austinywang austinywang mentioned this pull request May 5, 2026

This branch was successfully deployed

1 active deployment
Preview – cmux — 7c8a70a3 Deployed May 5, 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.

3 participants