Skip to content

fix: prevent recursive os_unfair_lock crash on cmd-clicked .md viewer route + drop fragment/query gate - #3559

Merged
austinywang merged 5 commits into
manaflow-ai:mainfrom
psh4607:fix-md-route-and-crash
May 5, 2026
Merged

austinywang merged 5 commits into
manaflow-ai:mainfrom
psh4607:fix-md-route-and-crash

Conversation

@psh4607

@psh4607 psh4607 commented May 5, 2026 •

Copy link
Copy Markdown
Contributor

Closes #3370.
Extends partial coverage of #1283 (fragmented .md URLs now route through the viewer).

Summary

  • What changed? In Sources/GhosttyTerminalView.swift's GHOSTTY_ACTION_OPEN_URL handler, the cmd-click .md viewer route (a) defers Workspace.openOrFocusMarkdownSplit to DispatchQueue.main.async instead of running synchronously inside performOnMain, and (b) no longer gates routing on fragment == nil / query == nil. An async NSWorkspace.shared.open fallback covers the rare case where deferred split creation fails.
  • Why?
    1. Crash fix (openMarkdownInCmuxViewer: stable ignores setting (opens in system editor), nightly crashes with recursive os_unfair_lock in Surface.encodeKey #3370). With openMarkdownInCmuxViewer: true, cmd-clicking a real .md link inside a terminal pane crashes cmux on stable 0.64.1 (reproduced) and on nightly per the original report. Surface.openUrl (Ghostty side) is holding an internal lock when it dispatches into the Swift handler; the synchronous performOnMain chain runs openOrFocusMarkdownSplit -> newMarkdownSplit -> focusPanel -> applyTabSelectionNow -> Surface.encodeKey, and Surface.encodeKey tries to re-acquire that same lock — `BUG IN CLIENT OF LIBPLATFORM: Trying to recursively lock an os_unfair_lock`, Abort Cause 259. Running the split creation on the next runloop tick lets `Surface.openUrl` return and release its lock first.
    2. Routing coverage (Route .md file clicks to markdown panel instead of system default #1283). Tools like Claude Code routinely emit markdown links with line-anchor fragments (`file:///abs/path/foo.md#L42`). Under the previous `fragment == nil` / `query == nil` gate, those URLs fell through to `NSWorkspace.shared.open` and opened in the system editor (Xcode / VS Code / Typora) even when `openMarkdownInCmuxViewer` was on. The viewer's contract only consumes `URL.path` (which excludes fragment/query), so stripping happens automatically and the panel-state behavior is unchanged.

Net diff: 1 file, +28/-11 lines.

Crash backtrace (cmux 0.64.1 stable, build 81, macOS 26.4.1, Apple Silicon)

```
0 _os_unfair_lock_recursive_abort
1 _os_unfair_lock_lock_slow
2 Surface.encodeKey
3 Workspace.applyTabSelectionNow
4 Workspace.focusPanel
5 Workspace.newMarkdownSplit
6 Workspace.openOrFocusMarkdownSplit
7 closure #33 in GhosttyApp.handleAction
... performOnMain (synchronous, MainActor.assumeIsolated)
13 GhosttyApp.handleAction
14 @objc closure #2 in GhosttyApp.initializeGhostty
15 Surface.openUrl ← holds the lock that frame 2 tries to re-acquire
16 @objc GhosttyNSView.mouseUp
... AppKit event delivery
```

`Surface.openUrl` (Ghostty submodule) acquires the surface lock for the duration of the URL action callback. The Swift handler runs synchronously inside that window via `performOnMain` (`@MainActor () -> T`), so any panel/focus mutation that ends up in `Surface.encodeKey` deadlocks on the same `os_unfair_lock`. Deferring the panel mutation by one runloop tick is the smallest correct fix; it does not require Ghostty submodule changes.

Why this also resolves the original #3370 stable report

The `addisonlynch` repro on 0.63.2 stable used a placeholder path (`/any/path/to/file.md`) that does not exist; `CmdClickMarkdownRouteSettings.shouldRoute(path:)` rejects unreadable paths, so on stable they hit the editor-fall-through branch and concluded "the setting is ignored." With a real `.md` file the same stable build hits the recursive-lock crash documented above (verified locally on 0.64.1 → identical commit as 0.63.2's mainline routing path). One bug, two reports.

Scope (what this does NOT do)

  • Does not touch `Sources/GhosttyTerminalView.swift`'s `handleCommandClickRelease` path (line 8526). That cmd-click-on-detected-path branch is not in the crash trace and is not entered through Ghostty's `Surface.openUrl` lock; touching it would risk regressions in a path that is currently working.
  • Does not modify the markdown panel itself (`MarkdownPanel.swift` / `MarkdownPanelView.swift`).
  • Does not address the broader feature request in Route .md file clicks to markdown panel instead of system default #1283 to detect bare filenames at the terminal level — the existing OSC 8 / `file://` URL routing already handles those cases once toggled on. After this PR a wider class of `file://` URLs (those with anchors/queries) will reach that path.
  • Does not change the default-off behavior. The gate still requires `openMarkdownInCmuxViewer` to be enabled.

Testing

  • Manual reproduction: with `openMarkdownInCmuxViewer: true` on stable 0.64.1, cmd-clicking a real `.md` link in a terminal pane reliably crashes the app with the trace above. See `~/Library/Logs/DiagnosticReports/cmux-2026-05-06-010520.ips` and `cmux-2026-05-06-010546.ips` (both produced during this investigation, both with identical recursive-lock signature).
  • 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. CI is the source of truth here.
  • No new tests added intentionally. AGENTS.md `Test quality policy` says "Tests must verify observable runtime behavior through executable paths". A unit test that asserts `DispatchQueue.main.async` is wired would be exactly the AST-shape test that policy forbids. The behavioral test is "open a `.md` link with `openMarkdownInCmuxViewer` on and observe no crash" — that requires the running app and is appropriate for a manual / E2E check.

Demo Video

For UI or behavior changes, include a short demo video.

  • Video URL or attachment: <to be added after a tagged-debug verification — the simplest demo is `cmd+click` on a `.md` link in a Claude Code chat pane: pre-PR the app crashes, post-PR the panel opens.>

Review Trigger (Copy/Paste as PR comment)

```text
@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. The crash itself was verified on the user's stable 0.64.1 install (crash report attached above).
  • I added or updated tests for behavior changes — N/A; behavioral test would violate `Test quality policy` (would only assert AST shape). The 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
    • Local file:// Markdown links now route even if they include fragments or queries, and routing is limited to local hosts (empty or "localhost").
    • In-app Markdown split creation is deferred and re-validated before opening; if creating a split fails or validation changes, the file reliably opens in the system viewer.

Fixes manaflow-ai#3370 (and reduces manaflow-ai#1283's gating):

- Defer Workspace.openOrFocusMarkdownSplit to the next runloop tick.
  Ghostty's Surface.openUrl holds an internal lock when it dispatches
  into the Swift open-URL handler; opening a new panel synchronously
  triggers Workspace.applyTabSelectionNow -> Surface.encodeKey, which
  tries to acquire the same lock and aborts with recursive_os_unfair_lock
  (BUG IN CLIENT OF LIBPLATFORM, Abort Cause 259). Running the split
  creation on DispatchQueue.main.async lets Surface.openUrl return and
  release the lock first.

- Drop the fragment == nil / query == nil gate. Tools like Claude Code
  emit markdown links with line-anchor fragments (foo.md#L42); under
  the previous gate those URLs were routed to NSWorkspace and opened
  in the system editor instead of the cmux markdown viewer. The viewer
  only consumes URL.path (which is already fragment/query-free), so
  stripping is automatic and the panel-state contract is preserved.

- Add an async fallback so a click is never silently lost: if the
  deferred split creation fails (e.g. the source pane was closed in
  between), the URL still surfaces through NSWorkspace.

Crash backtrace from a stable 0.64.1 reproduction:

  0  _os_unfair_lock_recursive_abort
  1  _os_unfair_lock_lock_slow
  2  Surface.encodeKey
  3  Workspace.applyTabSelectionNow
  4  Workspace.focusPanel
  5  Workspace.newMarkdownSplit
  6  Workspace.openOrFocusMarkdownSplit
  7  GhosttyApp.handleAction (closure manaflow-ai#33)
  ... performOnMain (sync, MainActor.assumeIsolated)
 13  GhosttyApp.handleAction
 14  Surface.openUrl  <-- holds the lock that frame 2 re-enters
 15  GhosttyNSView.mouseUp
Copilot AI review requested due to automatic review settings May 5, 2026 16:10
@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.

@chatgpt-codex-connector

Copy link
Copy Markdown

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

@psh4607

psh4607 commented May 5, 2026

Copy link
Copy Markdown
Contributor Author

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

@coderabbitai

coderabbitai Bot commented May 5, 2026 •

Copy link
Copy Markdown

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

Cmd-click handling for local file:// markdown URLs no longer requires the URL to lack fragment/query but restricts hosts to nil/empty/"localhost". The handler returns immediately for routed markdown URLs and defers split creation via DispatchQueue.main.async; the async block re-resolves the workspace, re-checks routing, tries openOrFocusMarkdownSplit(...), and falls back to NSWorkspace.shared.open(fileURL) if checks fail or split creation returns nil.

Changes

Markdown URL Routing and Execution

Layer / File(s) Summary
Routing Condition
Sources/GhosttyTerminalView.swift
Removed requirement that file:// URLs have no fragment/query; added allowlist that fileURL.host be nil, "", or "localhost, while keeping isFileURL and markdown-path check.
Immediate Handler Return
Sources/GhosttyTerminalView.swift
For routed markdown URLs the handler now returns true immediately instead of synchronously calling openOrFocusMarkdownSplit(...).
Deferred Split Creation
Sources/GhosttyTerminalView.swift
Split creation is deferred onto DispatchQueue.main.async; the async block re-resolves the current workspace, re-checks remote-surface gate and shouldRoute, then attempts workspace.openOrFocusMarkdownSplit(from:filePath:).
Fallback / External Open
Sources/GhosttyTerminalView.swift
If the re-check fails or openOrFocusMarkdownSplit returns nil in the async block, it calls NSWorkspace.shared.open(fileURL) as a fallback; non-routed links continue existing external/open flow.

Sequence Diagram(s)

sequenceDiagram
    participant TerminalView
    participant Resolver as WorkspaceResolver
    participant Workspace
    participant System as NSWorkspace

    TerminalView->>Resolver: cmd-click -> parse URL & shouldRoute?
    alt shouldRoute == true
        TerminalView-->>TerminalView: return true (immediate)
        TerminalView->>System: schedule DispatchQueue.main.async
        Note right of TerminalView: async block runs later
        TerminalView->>Resolver: re-resolve workspace & re-check shouldRoute
        alt re-check == true
            Resolver->>Workspace: openOrFocusMarkdownSplit(fileURL)
            alt split created/focused
                Workspace->>Workspace: focus/open split
            else split creation failed
                Workspace->>System: NSWorkspace.shared.open(fileURL)
            end
        else re-check == false
            TerminalView->>System: NSWorkspace.shared.open(fileURL)
        end
    else shouldRoute == false
        TerminalView->>System: NSWorkspace.shared.open(fileURL) (immediate)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • manaflow-ai/cmux#2904: Modifies the same cmd-click/URL markdown routing flow and related openOrFocusMarkdownSplit behavior.
  • manaflow-ai/cmux#912: Changes GhosttyTerminalView workspace resolution for routed opens and related URL-open routing logic.

Poem

🐰 I tap a file and hop away,
I queue a split for later play,
I check the workspace once again,
If gates forbid, the system then
Opens the file — a patient hop.


Important

Pre-merge checks failed

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

❌ Failed checks (1 warning, 2 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Cmux Swift Actor Isolation ❓ Inconclusive No result was produced after verification. Marking as INCONCLUSIVE. Re-run the check or adjust instructions to produce a final result.
Cmux Architecture Rethink ❓ Inconclusive No result was produced after verification. Marking as INCONCLUSIVE. Re-run the check or adjust instructions to produce a final result.
✅ Passed checks (10 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the two main changes: fixing a recursive os_unfair_lock crash and removing the fragment/query gate for markdown URL routing.
Description check ✅ Passed The description comprehensively covers what changed, why it was necessary, crash details, scope limitations, and testing approach, largely following the template structure.
Linked Issues check ✅ Passed The PR successfully addresses issue #3370 by deferring split creation to prevent recursive lock crashes and removing the fragment/query gate to allow markdown URLs with anchors to route through the viewer.
Out of Scope Changes check ✅ Passed All changes are narrowly scoped to fixing the crash and improving markdown URL routing; handleCommandClickRelease and markdown panel code are explicitly excluded.
Cmux Swift Blocking Runtime ✅ Passed DispatchQueue.main.async in passive callback (GHOSTTY_ACTION_OPEN_URL) fixes recursive os_unfair_lock crash. Allowed safety deferral: passive-callback, documents C lock constraint, prevents crash.
Cmux Swift Concurrency ✅ Passed DispatchQueue.main.async defers work at C callback boundary to fix recursive lock crash. This is allowed per modernization rules: minimal main-queue hops from legacy callbacks.
Cmux Swift @Concurrent ✅ Passed Only synchronous code changes. No async functions, @concurrent annotations, or nonisolated async declarations. MainActor state accessed correctly via DispatchQueue.main.async. Rule not applicable.
Cmux Swift File And Package Boundaries ✅ Passed Small focused bug fix (~50 lines added) to existing oversized file (13,432 lines). Qualifies as allowed "focused bug fix" and "UI/AppKit/Ghostty glue" under rules.
Cmux Swift Logging ✅ Passed The PR introduces no logging violations. Added code contains only comments, async dispatch, and system API calls—no print, debugPrint, dump, NSLog, file logging, or inappropriate Logger usage.
Cmux Swiftui State Layout ✅ Passed TerminalSurface is AppKit bridge backing model for NSView, not cmux SwiftUI state. @ObservableObject/@published for AppKit state management is allowed per rules.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@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.

@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 PR now.

[review]

@greptile-apps

greptile-apps Bot commented May 5, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a recursive os_unfair_lock crash (#3370) triggered when cmd-clicking a .md link while openMarkdownInCmuxViewer is enabled, and broadens routing to accept file:// URLs that carry fragment or query components (e.g. foo.md#L42 emitted by Claude Code).

  • Crash fix: openOrFocusMarkdownSplit is now deferred via DispatchQueue.main.async so that Surface.openUrl releases its internal lock before the split-creation focus path calls Surface.encodeKey. The deferral is clearly documented as a timing-based mitigation with a pointer to the structural fix tracked in Architectural: dispatch Ghostty OPEN_URL callback async to remove cmux's runloop-tick lock workaround #3560.
  • Routing coverage: The fragment == nil / query == nil guards are removed; the viewer receives only URL.path (which strips fragments automatically), so anchor links now route correctly.
  • Async fallback hygiene: The deferred closure re-resolves the workspace, re-applies the remote-surface guard, performs a TOCTOU readability re-check, and falls back to NSWorkspace.shared.open on any failure so no cmd-click is silently lost.

Confidence Score: 5/5

Safe to merge — well-scoped single-file deferral with documented fallback for every failure mode.

The diff is small (one file, +28/-11) and all substantive concerns raised in prior review rounds were resolved in follow-up commits. The async closure is written defensively with workspace re-resolution, remote-surface re-check, TOCTOU readability guard, and system-opener fallback on every failure path.

No files require special attention.

Important Files Changed

Filename Overview
Sources/GhosttyTerminalView.swift Markdown URL routing block refactored: fragment/query gates removed and split creation deferred via DispatchQueue.main.async with full async-guard hygiene. Previously flagged concerns (timing dependency, tuple/Workspace type mismatch) were addressed in prior commits.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["GHOSTTY_ACTION_OPEN_URL callback\nSurface lock held"] --> B{isEnabled?}
    B -- No --> Z["Fall through to BrowserLinkOpen / NSWorkspace"]
    B -- Yes --> C{isFileURL + local host + .md path?}
    C -- No --> Z
    C -- Yes --> D["performOnMain sync\nResolve workspace\nRemote-surface guard\nshouldRoute check"]
    D -- Guard fails --> E["return false"]
    E --> Z
    D -- Guards pass --> F["Capture workspaceId + surfaceId\nDispatchQueue.main.async\nreturn true"]
    F --> G["Surface lock released same runloop cycle"]
    G --> H["async block: re-resolve workspace\nremote-surface re-check"]
    H -- Remote surface --> I["NSWorkspace.shared.open fileURL"]
    H -- Local --> J["TOCTOU shouldRoute re-check"]
    J -- File gone --> I
    J -- File OK --> K["openOrFocusMarkdownSplit"]
    K -- non-nil success --> L["Panel opens - done"]
    K -- nil pane gone --> I
Loading

Reviews (4): Last reviewed commit: "review: re-apply remote-surface guard in..." | Re-trigger Greptile

Comment thread Sources/GhosttyTerminalView.swift

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 updates terminal URL handling in GhosttyTerminalView so cmd-clicked local markdown links open in cmux’s markdown viewer without triggering the recursive os_unfair_lock crash described in #3370, while also covering file:// markdown URLs that include fragments or queries as requested in #1283.

Changes:

  • Expands markdown viewer routing to local file:// markdown URLs even when they include #fragment or ?query.
  • Defers markdown split creation to the next main-queue turn so the Ghostty open_url callback can unwind before focus-changing UI work runs.
  • Adds an async NSWorkspace.shared.open fallback if the deferred markdown split cannot be created.

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

Comment thread Sources/GhosttyTerminalView.swift Outdated
Comment thread Sources/GhosttyTerminalView.swift
Comment thread Sources/GhosttyTerminalView.swift
psh4607 added 2 commits May 6, 2026 01:22
…low-ai#3560

Addresses Greptile P2 review feedback on manaflow-ai#3559: the
DispatchQueue.main.async deferral is a timing-based mitigation that
relies on the Ghostty C side releasing its surface lock within the
same runloop cycle that dispatches this Swift callback — an
undocumented invariant. Strengthens the inline comment to spell out:

- the dependency is on Ghostty implementation detail, not API contract
- future Ghostty changes that move the unlock can silently regress
  every Swift caller in this performOnMain window, not just this one
- the structural fix lives on the C side (async dispatch of the
  OPEN_URL callback) and is tracked in manaflow-ai#3560
- do NOT remove this DispatchQueue.main.async until manaflow-ai#3560 lands

No code-behavior change; comment-only update so future readers cannot
'clean up' the deferral without reading why it exists.
Addresses Copilot review feedback on manaflow-ai#3559:

- Re-resolve workspace at dispatch time via
  AppDelegate.shared?.workspaceContainingPanel(panelId:preferredWorkspaceId:),
  mirroring the deferred browser path. The user can move a tab to a
  different workspace between the synchronous gate and the runloop
  tick; using the captured workspace would route into the wrong one
  and incorrectly fall through to NSWorkspace even though the source
  pane still lives.

- Re-validate CmdClickMarkdownRouteSettings.shouldRoute inside the
  async closure. The file may have been deleted/renamed in the gap;
  if so, fall through to NSWorkspace instead of materializing a
  MarkdownPanel onto an unavailable file (the panel would render its
  'file unavailable' state, which is worse UX than letting the system
  opener report the missing path).
Comment thread Sources/GhosttyTerminalView.swift Outdated
…coalesce

AppDelegate.shared?.workspaceContainingPanel(panelId:preferredWorkspaceId:)
returns (workspace: Workspace, tabManager: TabManager)?, not Workspace?,
so the previous '?? workspace' fallback would not compile (Swift rejects
the operands as incompatible types). Extract .workspace from the tuple
before coalescing, matching the existing usage pattern at
Sources/AppDelegate.swift:13250.

Caught by Greptile P1 review on manaflow-ai#3559. Compile-only check is still
blocked locally on the GhosttyKit.xcframework / zig dependency, so
this slipped through SourceKit-LSP's partial type resolution; the
suggestion patch from Greptile is a direct match for the in-tree
caller convention.

@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

🤖 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/GhosttyTerminalView.swift`:
- Around line 3824-3841: After re-resolving the workspace into
resolvedWorkspace, re-apply the same remote-vs-local guard used earlier so we
don't open an in-app markdown split for a remote surface: call the same
remote-surface check (the one used in the synchronous gate) against the
panel/surface or resolvedWorkspace and if it indicates the surface is remote,
fall back to NSWorkspace.shared.open(fileURL) and return; otherwise continue to
use CmdClickMarkdownRouteSettings.shouldRoute(path:) and
resolvedWorkspace.openOrFocusMarkdownSplit(from:fileId:filePath:) as before.
🪄 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: a7a1c0bf-fbca-46a0-9f44-b4c036fe0d27

📥 Commits

Reviewing files that changed from the base of the PR and between 4a08558 and ce91c78.

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

Comment thread Sources/GhosttyTerminalView.swift
The synchronous gate at line 3796-3799 forbids routing markdown opens
for remote terminal surfaces. Inside the deferred async closure that
re-resolves the workspace (because the panel can migrate before the
next runloop tick), the same remote/local guard must be re-applied —
otherwise a panel that moved into a remote workspace during the gap
would still receive an in-app split, bypassing the sync rule.

Mirror the sync gate via resolvedWorkspace.isRemoteTerminalSurface(...)
and fall back to NSWorkspace.shared.open on remote.

Caught by CoderabbitAI (🟠 Major) on manaflow-ai#3559.

@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.

♻️ Duplicate comments (1)
Sources/GhosttyTerminalView.swift (1)

3828-3845: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Re-apply the remote-workspace guard after the async re-resolve.

Line 3828 re-resolves the workspace because the panel can move before the async block runs, but this path no longer re-checks whether the surface is remote before Line 3838 opens the in-app markdown split. If the surface moves into a remote workspace during that gap, this bypasses the earlier remote/local routing guard. Fall back to NSWorkspace.shared.open(fileURL) here before shouldRoute(path:) / openOrFocusMarkdownSplit(...).

Suggested fix
                         let resolvedWorkspace = AppDelegate.shared?.workspaceContainingPanel(
                             panelId: surfaceId,
                             preferredWorkspaceId: preferredWorkspaceId
                         )?.workspace ?? workspace
+                        guard !resolvedWorkspace.isRemoteTerminalSurface(surfaceId) else {
+                            NSWorkspace.shared.open(fileURL)
+                            return
+                        }
                         // TOCTOU re-check: file may have been removed/renamed
                         // since the synchronous gate. Fall through if so.
                         guard CmdClickMarkdownRouteSettings.shouldRoute(path: fileURL.path) else {
                             NSWorkspace.shared.open(fileURL)
                             return
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/GhosttyTerminalView.swift` around lines 3828 - 3845, After
re-resolving the workspace into resolvedWorkspace, re-apply the remote-workspace
guard before calling CmdClickMarkdownRouteSettings.shouldRoute(...) /
resolvedWorkspace.openOrFocusMarkdownSplit(...): if the resolvedWorkspace has
become remote (e.g., check resolvedWorkspace.isRemote or equivalent API on the
workspace object), call NSWorkspace.shared.open(fileURL) and return so we don’t
bypass remote routing; then proceed to shouldRoute(...) and
openOrFocusMarkdownSplit(...) as before.
🤖 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.

Duplicate comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 3828-3845: After re-resolving the workspace into
resolvedWorkspace, re-apply the remote-workspace guard before calling
CmdClickMarkdownRouteSettings.shouldRoute(...) /
resolvedWorkspace.openOrFocusMarkdownSplit(...): if the resolvedWorkspace has
become remote (e.g., check resolvedWorkspace.isRemote or equivalent API on the
workspace object), call NSWorkspace.shared.open(fileURL) and return so we don’t
bypass remote routing; then proceed to shouldRoute(...) and
openOrFocusMarkdownSplit(...) as before.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ec7f356f-92f0-4f83-adc1-b550a38975ab

📥 Commits

Reviewing files that changed from the base of the PR and between ce91c78 and 78a8a82.

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

@psh4607

psh4607 commented May 5, 2026

Copy link
Copy Markdown
Contributor Author

Hey @lawrencecchen @austinywang — when one of you has a moment, could you take a look at #3558 and #3559?

Both are external-fork PRs, so the GitHub Actions workflows (2 on #3558, 8 on #3559) and the Vercel deploys are stuck on "Approve and run". A one-click approval would unblock CI.

Review status:

Happy to revise anything once CI runs. 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:01pm

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

This branch was successfully deployed

1 active deployment
Preview – cmux — 0a374f34 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.

openMarkdownInCmuxViewer: stable ignores setting (opens in system editor), nightly crashes with recursive os_unfair_lock in Surface.encodeKey

3 participants