Skip to content

Add configurable file extension openers - #5040

Closed
lawrencecchen wants to merge 11 commits into
mainfrom
task-custom-file-extension-openers
Closed

lawrencecchen wants to merge 11 commits into
mainfrom
task-custom-file-extension-openers

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented May 31, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Route Cmd-clicked .html and .htm paths to the cmux browser by default instead of the file preview editor.
  • Add per-extension opener settings in Settings and cmux.json, with browser, preview, Markdown, preferred editor, system default, and automatic behaviors.
  • Document the new app.fileExtensionOpeners config schema.

Testing

  • swift test --package-path Packages/CmuxSettings --filter SettingCodable
  • swift test --package-path Packages/CmuxSettingsUI
  • bun test in web/
  • bun run lint in web/ (passes with existing warnings)
  • ./scripts/reload.sh --tag fileopen --swift-frontend-workaround

Note: full swift test --package-path Packages/CmuxSettings still hits the existing SettingCatalogTests.userDefaultsStorageKeysAreUnique failure on main (115 keys vs 106 unique keys on main; 116 vs 107 with this branch).


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag @codesmith with what you need. Autofix is disabled.


Note

Medium Risk
Changes default Cmd-click behavior for HTML and centralizes file-open routing; scope is settings and UX rather than auth or data, with tests covering overrides and JSON loading.

Overview
Adds per-extension Cmd-click routing via app.fileExtensionOpeners, with a dedicated File Opening settings section and cmux.json support.

Defaults and behavior: .html / .htm now open in the cmux browser instead of the generic file preview. Each extension can be set to automatic (legacy Markdown/supported-file toggles), cmux preview, Markdown viewer, browser, preferred editor, or system default. Built-in defaults merge with user overrides; storing automatic on a built-in extension opts out of that default.

Routing: CommandClickFileOpenRouter resolves extension rules first, then existing settings. Terminal link handling uses deferred opens with fallbacks; browser routes keep URL fragments when opening local files.

UI / config: File drops, preferred editor, extension editor (add/remove, show-all cap), and related toggles move out of App into File Opening. Search anchors, localization, schema docs, and tests cover storage, JSON import, and HTML routing regressions.

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


Summary by cubic

Cmd-click on .html/.htm now opens in the cmux browser by default. File opening is configurable per extension via Settings and cmux.json, with a new “File Opening” section and a router that falls back to your preferred editor or the system default.

  • New Features

    • New setting: app.fileExtensionOpeners maps extensions to behaviors: automatic, cmuxPreview, markdownViewer, cmuxBrowser, preferredEditor, systemDefault. Defaults .html/.htm → cmuxBrowser; keys may include or omit a leading dot; values are normalized; built-in defaults are merged and can be overridden. Changes post a notification and the Settings UI uses observer tokens to stay in sync.
    • Routing: Centralized shouldHandleCommandClick resolves extension rules first, then legacy Markdown/supported-file toggles. “automatic” respects built‑in defaults (so HTML won’t route to preview). Opens/focuses a browser split for local HTML, and falls back to preferred editor or system default if in‑app open fails; terminal link handling uses this path.
    • Settings UI: New “File Opening” section with File Drops, Preferred Editor, and a File Extension Openers editor (add/remove, “Show All/Fewer”). Search, navigation, and localization updated. cmux.json schema/loader validate, normalize, and merge openers with defaults; the template seeds defaults; tests cover defaults, overrides, JSON parsing, routing, and an HTML preview regression. Configuration docs now include the File Opening section and an example for fileExtensionOpeners.
  • Migration

    • Behavior change: HTML opens in the cmux browser on Cmd-click.
    • To keep previous behavior, set in Settings → File Opening or in cmux.json, for example:
      • cmux preview: "fileExtensionOpeners": { "html": "cmuxPreview" }
      • system default: "fileExtensionOpeners": { "html": "systemDefault" }
      • preferred editor: "fileExtensionOpeners": { "html": "preferredEditor" }

Written for commit 4297516. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • File Opening settings section now available for configuring command-click behavior by file extension
    • Set extensions to open via: automatic, cmux preview, markdown viewer, cmux browser, preferred editor, or system default
  • Documentation

    • Configuration documentation and schema updated with file extension opener reference

@vercel

vercel Bot commented May 31, 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 31, 2026 9:37am
cmux-staging Building Building Preview, Comment May 31, 2026 9:37am

@coderabbitai

coderabbitai Bot commented May 31, 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

This PR adds per-file-extension command-click routing behavior configuration. It introduces a new FileExtensionOpenBehavior enum and persistence layer, refactors the CommandClickFileOpenRouter to use a unified Action model, updates the settings UI to include a dedicated "File Opening" section with an editor for configuring extension openers, integrates parsing and validation, and includes localization, schema, and comprehensive tests.

Changes

File Extension-Based Command-Click Open Behavior

Layer / File(s) Summary
FileExtensionOpenBehavior model and settings key
Packages/CmuxSettings/Sources/CmuxSettings/Values/FileExtensionOpenBehavior.swift, Packages/CmuxSettings/Sources/CmuxSettings/Values/FileExtensionOpenBehaviorSettings.swift, Packages/CmuxSettings/Sources/CmuxSettings/Keys/AppCatalogSection.swift, Sources/CmuxSettingsJSONPathSupport.swift, Packages/CmuxSettings/Tests/CmuxSettingsTests/SettingCodableTests.swift
Core enum with six routing cases (automatic, cmuxPreview, markdownViewer, cmuxBrowser, preferredEditor, systemDefault), normalization and decoding helpers, persisted UserDefaults key, change notifications, and round-trip serialization tests.
Command-click routing refactor
Sources/CommandClickFileOpenRouter.swift
Unified Action model and Fallback handler, shouldHandleCommandClick and shouldRouteInCmux now compute routing via action(for:defaults:) consulting FileExtensionOpenBehaviorSettings, refactored openInCmux and openCommandClickFile to switch on Action, new deferredOpenCommandClickFile defers routing with runtime workspace resolution.
Routing integration in terminal and app
Sources/GhosttyTerminalView.swift, Sources/cmuxApp.swift, Sources/Workspace.swift
GhosttyTerminalView routes resolved file paths through new command-click APIs with .systemDefault and .preferredEditor fallbacks; CmdClickSupportedFileRouteSettings branches on per-extension behavior; resetAllSettings resets file-extension openers; Workspace.openOrFocusBrowserSplit() matches or creates browser panels.
FileExtensionOpenersEditor view
Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Rows/FileExtensionOpenersEditor.swift
SwiftUI editor component with draft text field, add button (disabled until extension normalizes), cached normalized/sorted extension list, per-extension behavior picker, remove button, show-all toggle for large sets, and localized display names for behaviors.
FileOpeningSection and settings layout
Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/FileOpeningSection.swift, Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/AppSection.swift, Sources/SettingsNavigation.swift
New FileOpeningSection renders file-drop picker, preferred-editor text field, extension-openers editor, and routing toggles; removes legacy file-opening rows from AppSection; adds FileExtensionOpenersSettingsCard to SettingsNavigation with state persistence and change-notification wiring.
Navigation target and discovery
Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/SettingsSectionID.swift, Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+Default.swift, Sources/SettingsSearchAliases.swift, Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Scene/SettingsWindowScene.swift, Packages/CmuxSettingsUI/Tests/CmuxSettingsUITests/SettingsRowAnchorResolutionTests.swift
New .fileOpening navigation target with localized title and SF Symbol; relocates file-opening curated entries to .fileOpening and adds new file-extension-openers entry; updates search aliases and anchor mappings; inserts FileOpeningSection after AppSection in settings window.
Settings file parsing and template
Sources/KeyboardShortcutSettingsFileStore.swift, Sources/KeyboardShortcutSettingsFileStore+Template.swift
Parses app.fileExtensionOpeners from cmux.json with extension normalization and behavior validation; adds jsonStringDictionary helper; extends default template with fileExtensionOpeners entry populated from defaults.
Localization, schema, and documentation
Resources/Localizable.xcstrings, web/data/cmux.schema.json, web/app/[locale]/docs/configuration/page.tsx
Localizes File Opening section title/keywords and file-extension editor UI strings (add, remove, behavior labels, empty state, show-all toggle, subtitle); adds schema for app.fileExtensionOpeners with extension keys and allowed opener values; adds commented example in docs.
Unit and integration tests
cmuxTests/WindowAndDragTests.swift
CMD-click routing tests verify HTML default behavior, system-browser routing and command-click handleability, override routing for configured extensions, and non-Cmux routing when .txt opener is set to preferredEditor; JSON settings-file test validates app.fileExtensionOpeners parsing and supported-file routing behavior.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • manaflow-ai/cmux#5012: Extends the settings search/row anchoring surface for the new app.fileExtensionOpeners settings key, directly tied to the search-index + row-anchor infrastructure.
  • manaflow-ai/cmux#4864: Both PRs modify cmd-click/terminal file opening routing; main PR refactors CommandClickFileOpenRouter and switches GhosttyTerminalView to use its new command-click APIs, overlapping at the same routing code paths.
  • manaflow-ai/cmux#4975: Main PR's new file-extension settings and UI are direct extensions of the Settings SPM/packages architecture, with overlapping updates to SettingsSectionID and CuratedSettingEntry+Default.

Poem

🐰 Hops with glee, the files now know their way—
Each extension finds its opener bright,
HTML hops to browser by the day,
While .txt prefers the editor's light!
Configure, preview, or browse with care—
Per-file routing magic in the air!


Caution

Pre-merge checks failed

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

  • Ignore

❌ Failed checks (3 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Swift Concurrency ❌ Error PR adds fire-and-forget Task in new CommandClickFileOpenRouter.deferredOpenCommandClickFile() with meaningful lifecycle (opens files, calls fallbacks) that is not stored or tracked. Store or return the Task; use structured concurrency instead of fire-and-forget for meaningful work beyond isolation boundary crossing.
Cmux Swiftui State Layout ❌ Error FileExtensionOpenersSettingsCard mutates UserDefaults via setOpeners() call in Binding setter during render—violating render-time state mutation rule. Move setOpeners() to .onChange or .task callback, or use DefaultsValueModel.set() pattern that wraps writes in background Tasks.
Cmux Architecture Rethink ❌ Error FileExtensionOpenersSettingsCard adds @State mutable owner with dual .onReceive observers alongside FileOpeningSection, creating split UI lifecycle ownership violating architectural rules. Consolidate to single owner: use FileOpeningSection with DefaultsValueModel or remove duplicate notification-based state in SettingsNavigation.
Docstring Coverage ⚠️ Warning Docstring coverage is 14.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (14 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding configurable file extension openers as a new feature.
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 All new production types have correct actor isolation: value enums are Sendable, services are stateless, MainActor methods handle UI, nonisolated methods do pure computation on thread-safe APIs.
Cmux Swift Blocking Runtime ✅ Passed PR introduces no blocking synchronization patterns: 0 semaphores, locks, NSCondition, .main.sync, sleep calls, or Task.sleep. Uses non-blocking async patterns.
Cmux No Hacky Sleeps ✅ Passed Only documentation files modified (web config example, schema JSON). No sleep(), setTimeout, polling, or timing-based race fixes in TypeScript/JavaScript/shell runtime code.
Cmux Algorithmic Complexity ✅ Passed New openOrFocusBrowserSplit mirrors existing openOrFocusMarkdownSplit pattern; FileExtensionOpenersEditor bounded to 32 rows; no nested scans or batch rescans detected.
Cmux Swift @Concurrent ✅ Passed PR follows @concurrent rules: nonisolated functions do thread-safe operations; @MainActor methods properly isolate Workspace access; no missing @concurrent on nonisolated async work.
Cmux Swift File And Package Boundaries ✅ Passed New logic properly placed in SwiftPM packages (54/60/213/116 lines) with single responsibilities. App modifications under thresholds. No mixed responsibilities or boundary violations.
Cmux Swift Logging ✅ Passed PR adds no print/debugPrint/dump/NSLog in app code; reuses existing nonisolated Logger with privacy redaction for fileExtensionOpeners validation; new classes have no logging.
Cmux User-Facing Error Privacy ✅ Passed User-facing text complies with privacy rules: no vendor names, credentials, env vars, or raw upstream messages; proper localization; invalid settings use privacy-aware hashed logging.
Cmux Full Internationalization ✅ Passed All user-facing Swift text uses String(localized:defaultValue:) with translations for en/ja. 18 new localization keys added with complete translations. Web and test changes follow existing patterns.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR adds file extension openers via SwiftUI Views embedded in existing settings window. No new standalone NSWindow, NSPanel, or SwiftUI Window/WindowGroup declarations were added.
Description check ✅ Passed PR description covers all key areas: summary (what changed and why), testing details (specific commands and results), and addresses the main behavior change with documentation.
✨ 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-custom-file-extension-openers

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Comment thread Sources/CommandClickFileOpenRouter.swift Outdated
Comment thread Sources/KeyboardShortcutSettingsFileStore.swift Outdated

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

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

@@ -0,0 +1,33 @@
import Foundation

public enum FileExtensionOpenBehavior: String, CaseIterable, Identifiable, Sendable, SettingCodable {

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 Add DocC comments to the public package API

The repository's AGENTS.md requires every public symbol added under Packages/ to have Swift-DocC /// documentation, including types, enum cases, properties, and methods. This new public enum and its public members are currently undocumented, so the package API violates the documented review contract and will leave generated package docs incomplete; please add DocC comments for the enum, cases, id, defaultOpeners, and normalizedExtension.

Useful? React with 👍 / 👎.

@greptile-apps

greptile-apps Bot commented May 31, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR routes Cmd-clicked .html/.htm paths to the cmux browser by default and adds a fully configurable per-extension opener map (app.fileExtensionOpeners) backed by UserDefaults, cmux.json, and a new "File Opening" Settings section.

  • Routing engine: CommandClickFileOpenRouter is refactored around a private Action enum; DispatchQueue.main.async is replaced with Task { @MainActor in … } (fixing the previously-flagged concurrency issue), and the terminal-link and mouseDown call sites are updated consistently with a typed Fallback enum.
  • Settings storage: FileExtensionOpenBehaviorSettings merges built-in defaults on read and prunes redundant entries on write; the cmux.json parser uses continue for invalid entries so one bad extension value no longer silently drops the rest of the app block.
  • UI: A new FileOpeningSection (CmuxSettingsUI) and a legacy FileExtensionOpenersSettingsCard (in-app) both surface the extension editor; all new strings use String(localized:defaultValue:) but only carry en and ja translations in the string catalog.

Confidence Score: 5/5

Safe to merge; routing logic is well-tested, the concurrency migration is correct, and fallback chains are consistent across all call sites.

The routing refactor is straightforward: a well-covered Action enum replaces ad-hoc boolean checks, the concurrency migration (DispatchQueue → Task @mainactor) is correct, and the cmux.json parser now uses continue so a single bad entry no longer silently drops the rest of the app block. The only open items are documentation clarity for the automatic semantic on built-in extensions and missing catalog translations for 18 non-en/ja locales, both of which are non-blocking.

Resources/Localizable.xcstrings — all new user-facing strings need translations for the 18 locales beyond en and ja that the catalog already supports.

Important Files Changed

Filename Overview
Sources/CommandClickFileOpenRouter.swift Centralises Cmd-click routing with a clean Action enum; correctly migrates DispatchQueue.main.async to Task @mainactor and passes a typed Fallback enum instead of an optional closure. The action/automatic/built-in-extension semantics are logically consistent and well-tested.
Packages/CmuxSettings/Sources/CmuxSettings/Values/FileExtensionOpenBehaviorSettings.swift Solid helper: guards against missing/corrupt UserDefaults entries, prunes entries matching built-in defaults on write, and merges defaults on read so effective openers are always correct.
Packages/CmuxSettings/Sources/CmuxSettings/Values/FileExtensionOpenBehavior.swift Well-defined enum; normalizer is clear and consistent. Doc comment for automatic is misleading for built-in extensions (flagged).
Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Rows/FileExtensionOpenersEditor.swift Presents Restore for built-in extensions and Remove for user-added ones. Caches are kept in sync via onChange. The add-duplicate no-op is acceptable.
Sources/KeyboardShortcutSettingsFileStore.swift Uses continue (not return) for invalid per-extension entries, so a bad value skips only that entry and the rest of the app block is parsed correctly. Non-dict value for the whole key is caught by the else-if branch.
Sources/Workspace.swift Adds openOrFocusBrowserSplit with correct fallback chain (preferredRightSideTargetPane → split → new surface). BrowserAvailabilitySettings guard prevents opening when browser is disabled.
Resources/Localizable.xcstrings All ~20 new user-facing strings (section header, behavior picker labels, editor subtitle/placeholder, search aliases) only carry en and ja translations; the catalog already supports 20 locales.
Sources/cmuxApp.swift Moves file-opening settings into a new File Opening section; reset path correctly calls setOpeners with defaultValue. Legacy SettingsView wired via FileExtensionOpenersSettingsCard with onReceive guards.
Sources/GhosttyTerminalView.swift Switches from shouldRouteInCmux to shouldHandleCommandClick and passes typed Fallback enum; both terminal-link and mouseDown paths updated consistently.
Sources/SettingsNavigation.swift Adds fileOpening navigation target and FileExtensionOpenersSettingsCard with dual onReceive observers. Search index and JSON key map updated to point file-opening settings at the new section.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Cmd-click file path] --> B{shouldHandleCommandClick}
    B -- false --> Z[System/Ghostty handles link]
    B -- true --> C[deferredOpenCommandClickFile]
    C --> D[action for path]
    D --> E{fileExtensionOpeners behavior?}
    E -- cmuxBrowser --> F[openOrFocusBrowserSplit]
    E -- cmuxPreview --> G[openOrFocusFilePreviewSplit]
    E -- markdownViewer --> H[openOrFocusMarkdownSplit]
    E -- preferredEditor --> I[PreferredEditorSettings.open]
    E -- systemDefault --> J[NSWorkspace.shared.open]
    E -- "automatic + built-in ext (html/htm)" --> K[.unhandled → fallback]
    E -- "automatic + other ext or no entry" --> L[Legacy routing]
    L --> M{CmdClickMarkdownRouteSettings}
    M -- yes --> H
    M -- no --> N{CmdClickSupportedFileRouteSettings}
    N -- yes --> G
    N -- no --> K
    F -- nil/fail --> O[fallback.open]
    G -- nil/fail --> O
    H -- nil/fail --> O
    K --> O
Loading

Reviews (8): Last reviewed commit: "Document file opening settings section" | Re-trigger Greptile

Comment on lines +438 to +445
for (rawExtension, rawBehavior) in values {
guard let normalizedExtension = FileExtensionOpenBehavior.normalizedExtension(rawExtension),
let behavior = FileExtensionOpenBehavior(rawValue: rawBehavior) else {
logInvalid("app.fileExtensionOpeners", sourcePath: sourcePath)
return
}
normalized[normalizedExtension] = behavior.rawValue
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 A single unrecognized extension behavior in cmux.json causes the return to exit the entire section-parsing function, so every setting that follows — openSupportedFilesInCmux, openMarkdownInCmuxViewer, iMessageMode, confirmQuit, and everything else in the app block — is silently dropped from the managed snapshot. The runtime reader (FileExtensionOpenBehaviorSettings.openers()) correctly uses continue to skip invalid entries; the file-store parser should do the same.

Suggested change
for (rawExtension, rawBehavior) in values {
guard let normalizedExtension = FileExtensionOpenBehavior.normalizedExtension(rawExtension),
let behavior = FileExtensionOpenBehavior(rawValue: rawBehavior) else {
logInvalid("app.fileExtensionOpeners", sourcePath: sourcePath)
return
}
normalized[normalizedExtension] = behavior.rawValue
}
for (rawExtension, rawBehavior) in values {
guard let normalizedExtension = FileExtensionOpenBehavior.normalizedExtension(rawExtension),
let behavior = FileExtensionOpenBehavior(rawValue: rawBehavior) else {
logInvalid("app.fileExtensionOpeners", sourcePath: sourcePath)
continue
}
normalized[normalizedExtension] = behavior.rawValue
}

Comment on lines +246 to +273
@MainActor
static func deferredOpenCommandClickFile(
workspace: Workspace,
preferredWorkspaceId: UUID,
surfaceId: UUID,
filePath: String,
fallback: Fallback
) {
DispatchQueue.main.async {
let resolvedWorkspace = AppDelegate.shared?.workspaceContainingPanel(
panelId: surfaceId,
preferredWorkspaceId: preferredWorkspaceId
)?.workspace ?? workspace
guard !resolvedWorkspace.isRemoteTerminalSurface(surfaceId) else {
fallback.open(URL(fileURLWithPath: filePath))
return
}
guard openCommandClickFile(
workspace: resolvedWorkspace,
sourcePanelId: surfaceId,
filePath: filePath,
fallback: fallback
) else {
fallback.open(URL(fileURLWithPath: filePath))
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 deferredOpenCommandClickFile is already isolated to @MainActor, so DispatchQueue.main.async is a legacy dispatch that escapes Swift's actor model — the closure runs outside any actor isolation, dropping the @MainActor guarantee on the calls inside. Per the cmux-swift-concurrency-modernization rule, use Task { @MainActor in … } instead to keep the deferred work properly isolated.

Suggested change
@MainActor
static func deferredOpenCommandClickFile(
workspace: Workspace,
preferredWorkspaceId: UUID,
surfaceId: UUID,
filePath: String,
fallback: Fallback
) {
DispatchQueue.main.async {
let resolvedWorkspace = AppDelegate.shared?.workspaceContainingPanel(
panelId: surfaceId,
preferredWorkspaceId: preferredWorkspaceId
)?.workspace ?? workspace
guard !resolvedWorkspace.isRemoteTerminalSurface(surfaceId) else {
fallback.open(URL(fileURLWithPath: filePath))
return
}
guard openCommandClickFile(
workspace: resolvedWorkspace,
sourcePanelId: surfaceId,
filePath: filePath,
fallback: fallback
) else {
fallback.open(URL(fileURLWithPath: filePath))
return
}
}
}
@MainActor
static func deferredOpenCommandClickFile(
workspace: Workspace,
preferredWorkspaceId: UUID,
surfaceId: UUID,
filePath: String,
fallback: Fallback
) {
Task { @MainActor in
let resolvedWorkspace = AppDelegate.shared?.workspaceContainingPanel(
panelId: surfaceId,
preferredWorkspaceId: preferredWorkspaceId
)?.workspace ?? workspace
guard !resolvedWorkspace.isRemoteTerminalSurface(surfaceId) else {
fallback.open(URL(fileURLWithPath: filePath))
return
}
guard openCommandClickFile(
workspace: resolvedWorkspace,
sourcePanelId: surfaceId,
filePath: filePath,
fallback: fallback
) else {
fallback.open(URL(fileURLWithPath: filePath))
return
}
}
}

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!

coderabbitai[bot]
coderabbitai Bot previously requested changes May 31, 2026

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

🤖 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
`@Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Rows/FileExtensionOpenersEditor.swift`:
- Around line 108-117: The setBehavior(_:, for:) and remove(_:) methods
currently mutate the normalizedOpeners map using the raw fileExtension, causing
duplicate keys like ".html" vs "html" and failed removals; before writing or
removing, transform the incoming fileExtension to the same normalized form used
by normalizedOpeners (i.e., compute a normalizedKey from fileExtension using the
same normalization logic) and use that normalizedKey when setting
next[normalizedKey] = behavior and when calling next.removeValue(forKey:
normalizedKey) so openers stays consistent.

In `@Resources/Localizable.xcstrings`:
- Around line 143092-143107: The ja localization for the string key
settings.search.alias.setting.app.file-extension-openers currently duplicates
the English token list; replace that copied-English value with a proper Japanese
localization (update the "value" inside the "ja" -> "stringUnit" for
settings.search.alias.setting.app.file-extension-openers) using appropriate
Japanese terms for the tokens (e.g., localize phrases like "file extension
openers", "cmd click", "browser preview", "system default editor", etc.), keep
extractionState as "manual" and set the stringUnit state to "translated".

In `@Sources/CommandClickFileOpenRouter.swift`:
- Around line 246-253: Add a brief doc comment above the `@MainActor` static func
deferredOpenCommandClickFile(...) explaining that the function dispatches
asynchronously to avoid the Ghostty deadlock (same rationale as
deferredOpenFileInCmux), summarizing the deadlock/workaround and referencing
that it mirrors deferredOpenFileInCmux’s behavior so maintainers know why the
async dispatch is necessary.
🪄 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: 4906919c-6175-467a-8fe3-6863f09d8bae

📥 Commits

Reviewing files that changed from the base of the PR and between 6154ad3 and 7d9b706.

📒 Files selected for processing (21)
  • Packages/CmuxSettings/Sources/CmuxSettings/Keys/AppCatalogSection.swift
  • Packages/CmuxSettings/Sources/CmuxSettings/Values/FileExtensionOpenBehavior.swift
  • Packages/CmuxSettings/Tests/CmuxSettingsTests/SettingCodableTests.swift
  • Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+Default.swift
  • Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/SettingsSectionID.swift
  • Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Rows/FileExtensionOpenersEditor.swift
  • Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/AppSection.swift
  • Packages/CmuxSettingsUI/Tests/CmuxSettingsUITests/SettingsRowAnchorResolutionTests.swift
  • Resources/Localizable.xcstrings
  • Sources/CmuxSettingsJSONPathSupport.swift
  • Sources/CommandClickFileOpenRouter.swift
  • Sources/GhosttyTerminalView.swift
  • Sources/KeyboardShortcutSettingsFileStore+Template.swift
  • Sources/KeyboardShortcutSettingsFileStore.swift
  • Sources/SettingsNavigation.swift
  • Sources/SettingsSearchAliases.swift
  • Sources/Workspace.swift
  • Sources/cmuxApp.swift
  • cmuxTests/WindowAndDragTests.swift
  • web/app/[locale]/docs/configuration/page.tsx
  • web/data/cmux.schema.json

Comment thread Resources/Localizable.xcstrings
Comment thread Sources/CommandClickFileOpenRouter.swift
coderabbitai[bot]
coderabbitai Bot previously requested changes May 31, 2026

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

🤖 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
`@Packages/CmuxSettings/Sources/CmuxSettings/Values/FileExtensionOpenBehaviorSettings.swift`:
- Around line 8-46: The methods openers, behavior(forPath:), setOpeners, and
notifyDidChange currently default to UserDefaults.standard and
NotificationCenter.default; remove those default parameter values so callers
must inject UserDefaults and NotificationCenter. Specifically, change
openers(defaults: UserDefaults) -> [String: FileExtensionOpenBehavior] and
behavior(forPath: String, defaults: UserDefaults) -> FileExtensionOpenBehavior?
so they no longer use .standard, and change setOpeners(_ openers: [String:
FileExtensionOpenBehavior], defaults: UserDefaults, notificationCenter:
NotificationCenter) and notifyDidChange(notificationCenter: NotificationCenter)
to require explicit instances; update internal calls (e.g., behavior calling
openers) to forward the passed defaults/notificationCenter and update all call
sites to pass the appropriate injected instances. Ensure normalized logic and
return types remain unchanged.
- Around line 8-21: The openers(defaults:) function currently builds an empty
result so any persisted override removes all built-in defaults; change the logic
in openers to seed result from FileExtensionOpenBehavior.defaultValue (rather
than starting empty) and then iterate stored to normalize keys (using
FileExtensionOpenBehavior.normalizedExtension(_:)) and override matching entries
with parsed behaviors (FileExtensionOpenBehavior(rawValue:)), so defaults remain
unless explicitly replaced; update references to result, normalizedExtension,
and behavior accordingly.
- Around line 3-49: Add Swift-DocC triple-slash (///) comments for the new
public API introduced in the FileExtensionOpenBehaviorSettings enum: document
the enum itself and each public member including key, didChangeNotification,
defaultValue, and the methods openers(defaults:), behavior(forPath:defaults:),
setOpeners(_:defaults:notificationCenter:), and
notifyDidChange(notificationCenter:). For each symbol include a concise one-line
summary of purpose and any important parameter/return semantics (e.g., that
openers reads normalized mappings from UserDefaults and setOpeners writes
normalized rawValue strings and posts didChangeNotification). Ensure comments
are placed immediately above each declaration using /// so the package surface
is documented.

In
`@Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry`+Default.swift:
- Around line 59-64: Replace the hard-coded English titles in the
CuratedSettingEntry initializers (the .init calls with section: .fileOpening and
ids "file-drops", "preferred-editor", "file-extension-openers",
"supported-file-previews", "markdown-viewer") with localized strings using the
Swift localization API (e.g., String(localized: , defaultValue: ) or your
project's equivalent) and reference matching keys in the string catalog; ensure
each title uses a unique localization key and add corresponding translations to
the .strings or .stringsdict entries so the settings-search UI shows localized
text.

In `@Sources/cmuxApp.swift`:
- Around line 6918-6925: Update the subtitle for the SettingsCardRow that uses
configurationReview .json("app.preferredEditor") by replacing the localized
string key "settings.app.preferredEditor.subtitle" with copy that explains the
new opener flow (it should mention both the fallback and the explicit routing of
extensions to the preferred editor); update the String(localized: ...) call in
the SettingsCardRow initializer (the one adjacent to TextField and
preferredEditorCommand) to reference the new copy and then add corresponding
translated entries for that same key in all Resources/*.xcstrings files so every
supported locale is updated.
🪄 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: 5ea31bdf-a05d-4f22-a015-d50fbc4a5348

📥 Commits

Reviewing files that changed from the base of the PR and between 7d9b706 and df2e59f.

📒 Files selected for processing (12)
  • Packages/CmuxSettings/Sources/CmuxSettings/Values/FileExtensionOpenBehaviorSettings.swift
  • Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+Default.swift
  • Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/SettingsSectionID.swift
  • Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Scene/SettingsWindowScene.swift
  • Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/AppSection.swift
  • Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/FileOpeningSection.swift
  • Packages/CmuxSettingsUI/Tests/CmuxSettingsUITests/SettingsRowAnchorResolutionTests.swift
  • Resources/Localizable.xcstrings
  • Sources/CommandClickFileOpenRouter.swift
  • Sources/SettingsNavigation.swift
  • Sources/SettingsSearchAliases.swift
  • Sources/cmuxApp.swift
💤 Files with no reviewable changes (1)
  • Sources/CommandClickFileOpenRouter.swift

Comment thread Sources/cmuxApp.swift
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

coderabbitai[bot]
coderabbitai Bot previously requested changes May 31, 2026

@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
`@Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Rows/FileExtensionOpenersEditor.swift`:
- Around line 17-18: The current FileExtensionOpenersEditor recomputes let
extensions = sortedExtensions by calling openers.keys.sorted(...) on every
render which still sorts the entire collection; change this to cache a sorted
snapshot whenever the source map (app.fileExtensionOpeners / openers) changes
and then use a capped slice of that cached array in body rendering. Concretely,
add a stored `@State` or `@StateObject` (e.g., cachedSortedExtensions) and update it
only from the mutating path or in an onChange(of: openers) handler or a computed
property that compares a version/identity token; replace direct calls to
openers.keys.sorted(...) and usages of sortedExtensions with the
cachedSortedExtensions and then take prefix(32) for UI rows so the heavy sort
runs only when openers actually changes. Ensure the change covers the other
spots you flagged (the uses around lines 44-56 and 71-76) by reading from the
same cachedSortedExtensions.
🪄 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: eebcc2a6-0fe2-4105-ab45-8ead71d26ec6

📥 Commits

Reviewing files that changed from the base of the PR and between df2e59f and 01ba7ad.

📒 Files selected for processing (4)
  • Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Rows/FileExtensionOpenersEditor.swift
  • Resources/Localizable.xcstrings
  • Sources/SettingsNavigation.swift
  • cmuxTests/WorkspaceUnitTests.swift

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

}
result[normalizedExtension] = behavior
}
return result

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Built-in defaults block removal

Medium Severity

FileExtensionOpenBehaviorSettings.openers always re-injects built-in html/htm browser entries on read, but setOpeners and DefaultsValueModel persist only explicit overrides. Removing those rows writes a map without them, yet Cmd-click routing still uses the browser. The legacy card reloads via openers() after didChangeNotification, so removed built-ins can reappear immediately while behavior never matches the edited list.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 559c9f2. Configure here.

fallback?()
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unused deferred file open helpers

Low Severity

This commit switches terminal link handling to deferredOpenCommandClickFile, but deferredOpenFileInCmux and its openInCmux call remain with no remaining callers. That dead path duplicates extension-aware opening logic and can mislead future changes back to the old deferral behavior.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 559c9f2. Configure here.

Comment on lines +159 to +165
var next = normalizedOpenersCache
next[normalized] = behavior
applyOpeners(next)
}

private func remove(_ fileExtension: String) {
guard let normalized = FileExtensionOpenBehavior.normalizedExtension(fileExtension) else { 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.

P1 Remove button silently no-ops for built-in default extensions

remove("html") writes a map without "html" to UserDefaults, but FileExtensionOpenBehaviorSettings.openers() always starts from defaultValue and merges stored entries on top — so "html" is immediately re-inserted with .cmuxBrowser the moment UserDefaults.didChangeNotification fires in FileExtensionOpenersSettingsCard. The user sees the row flash away and reappear. Worse, if the user had previously overridden html to .preferredEditor and then presses minus expecting to reset to the global toggle, their stored override is erased and the built-in default .cmuxBrowser is silently restored instead.

The fix is to either: (a) hide or replace the minus button with a "Reset" button for entries whose normalized key is in FileExtensionOpenBehavior.defaultOpeners, making the intent explicit; or (b) make "remove" write .automatic for default-keyed entries so the stored entry wins over the built-in default in the merge.

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

Caution

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

⚠️ Outside diff range comments (7)
Packages/CmuxSettings/Sources/CmuxSettings/Values/FileExtensionOpenBehaviorSettings.swift (1)

21-24: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use the tolerant enum decoder here.

Unknown persisted values are currently skipped outright, so a stale/corrupt entry for "html" will silently re-enable the built-in .cmuxBrowser default instead of degrading to .automatic. That diverges from the new FileExtensionOpenBehavior.decodeFromUserDefaults(_:) behavior.

Suggested fix
         for (rawExtension, rawBehavior) in stored {
             guard let normalizedExtension = FileExtensionOpenBehavior.normalizedExtension(rawExtension),
-                  let rawBehavior = rawBehavior as? String,
-                  let behavior = FileExtensionOpenBehavior(rawValue: rawBehavior) else {
+                  let behavior = FileExtensionOpenBehavior.decodeFromUserDefaults(rawBehavior) else {
                 continue
             }
             result[normalizedExtension] = 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
`@Packages/CmuxSettings/Sources/CmuxSettings/Values/FileExtensionOpenBehaviorSettings.swift`
around lines 21 - 24, The loop currently uses
FileExtensionOpenBehavior.normalizedExtension(...) and
FileExtensionOpenBehavior(rawValue: rawBehavior) which silently drops unknown
persisted values; replace the direct enum initializer with the tolerant decoder
by calling FileExtensionOpenBehavior.decodeFromUserDefaults(rawBehavior) (or the
equivalent tolerant decode method) so unknown/stale values degrade to .automatic
instead of being skipped—keep the normalizedExtension check and use the
decoder's result to produce the behavior used in the mapping.
Sources/CommandClickFileOpenRouter.swift (1)

211-229: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep Ghostty deadlock deferral on DispatchQueue.main.async in deferredOpenCommandClickFile

deferredOpenCommandClickFile says it mirrors deferredOpenFileInCmux (which documents deferring via DispatchQueue.main.async to avoid the Surface.openUrl re-entry/deadlock), but the implementation schedules via Task { @mainactor in ... } instead. Task { @mainactor in } guarantees MainActor isolation, not an explicit “next run-loop tick” deferral contract, so this can reintroduce the risk the workaround is meant to prevent.

Suggested fix
-        Task { `@MainActor` in
+        DispatchQueue.main.async {
             let resolvedWorkspace = AppDelegate.shared?.workspaceContainingPanel(
                 panelId: surfaceId,
                 preferredWorkspaceId: preferredWorkspaceId
             )?.workspace ?? workspace
             guard !resolvedWorkspace.isRemoteTerminalSurface(surfaceId) else {
                 fallback.open(URL(fileURLWithPath: filePath))
                 return
             }
             guard openCommandClickFile(
                 workspace: resolvedWorkspace,
                 sourcePanelId: surfaceId,
                 filePath: filePath,
                 fallback: fallback
             ) else {
                 fallback.open(URL(fileURLWithPath: filePath))
                 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/CommandClickFileOpenRouter.swift` around lines 211 - 229, The current
deferredOpenCommandClickFile implementation uses Task { `@MainActor` in ... }
which only enforces MainActor isolation but does not guarantee a run-loop tick
deferral; change the scheduling to DispatchQueue.main.async { ... } to mirror
deferredOpenFileInCmux and avoid Surface.openUrl re-entrancy/deadlock. Locate
the block inside deferredOpenCommandClickFile that resolves resolvedWorkspace
(using AppDelegate.shared?.workspaceContainingPanel and isRemoteTerminalSurface)
and that calls openCommandClickFile/fallback.open, and replace the Task {
`@MainActor` in ... } scheduling with DispatchQueue.main.async { ... } so the body
runs on the next main run-loop tick while still performing the same workspace
checks and fallback logic.
Sources/KeyboardShortcutSettingsFileStore.swift (1)

436-446: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Normalize opener values when loading app.fileExtensionOpeners.

This path normalizes extensions but still parses behaviors with FileExtensionOpenBehavior(rawValue:), so it only accepts exact enum spellings. That bypasses the normalization/decoding behavior introduced for this setting and makes cmux.json stricter than the rest of the feature contract. Please parse rawBehavior through the shared FileExtensionOpenBehavior normalization helper before persisting the dictionary.

🤖 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/KeyboardShortcutSettingsFileStore.swift` around lines 436 - 446, The
code normalizes file extensions but still constructs behaviors with
FileExtensionOpenBehavior(rawValue:), rejecting non-exact spellings; change the
parsing to use the shared FileExtensionOpenBehavior normalization helper (the
helper on the FileExtensionOpenBehavior type) to convert rawBehavior into a
normalized enum case before using behavior.rawValue when populating normalized;
keep the existing guard/logInvalid/continue flow so failures still log via
logInvalid("app.fileExtensionOpeners", sourcePath: sourcePath) and then assign
snapshot.managedUserDefaults[FileExtensionOpenBehaviorSettings.key] =
.stringDictionary(normalized).
Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/FileOpeningSection.swift (2)

5-25: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Document the new public package API.

FileOpeningSection, its public initializer, and its public body are new public package symbols, but none of them have DocC comments yet.

As per coding guidelines, Packages/**/*.swift: Every public symbol in any new Swift package under Packages/ is documented with a Swift-DocC triple-slash comment (///) at the time of writing.

🤖 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
`@Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/FileOpeningSection.swift`
around lines 5 - 25, Add Swift-DocC triple-slash comments for the new public
API: document the FileOpeningSection type, its public
init(defaultsStore:catalog:) and the public body property. For
FileOpeningSection add a top-level /// comment describing the purpose of the
view; for the init add /// lines describing the parameters (defaultsStore and
catalog) and what the initializer configures; and for the public var body add a
brief /// describing the view’s content/behavior. Place the comments immediately
above the struct declaration, the init signature, and the body property
respectively so the public symbols are fully documented.

53-64: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Wire row anchors for the new file-opening settings.

SettingsNavigation.swift registers row-level anchors for app.preferredEditor, app.openSupportedFilesInCmux, and app.openMarkdownInCmuxViewer, but these rows never publish matching searchAnchorIDs. Search results for those settings will not jump to or highlight the exact control.

♻️ Suggested change
                 SettingsCardRow(
                     configurationReview: .json("app.preferredEditor"),
+                    searchAnchorID: "setting:fileOpening:preferred-editor",
                     String(localized: "settings.app.preferredEditor", defaultValue: "Open Files With"),
                     subtitle: String(localized: "settings.app.preferredEditor.subtitle", defaultValue: "Command used when an extension is set to Preferred Editor, or when Cmd-click file previews are disabled or a file is unsupported. Leave empty for system default.")
                 ) {
@@
                 SettingsCardRow(
                     configurationReview: .json("app.openSupportedFilesInCmux"),
+                    searchAnchorID: "setting:fileOpening:supported-file-previews",
                     String(localized: "settings.app.openSupportedFilesInCmux", defaultValue: "Open Supported Files in cmux"),
                     subtitle: String(localized: "settings.app.openSupportedFilesInCmux.subtitle", defaultValue: "Cmd-clicking readable files opens text, code, PDFs, images, audio, video, and Quick Look previews in cmux.")
                 ) {
@@
                 SettingsCardRow(
                     configurationReview: .json("app.openMarkdownInCmuxViewer"),
+                    searchAnchorID: "setting:fileOpening:markdown-viewer",
                     String(localized: "settings.app.openMarkdownInCmuxViewer", defaultValue: "Open Markdown in cmux Viewer"),
                     subtitle: String(localized: "settings.app.openMarkdownInCmuxViewer.subtitle", defaultValue: "When supported file routing is on, Cmd-clicking Markdown files opens the rendered cmux markdown viewer instead of the generic file preview.")
                 ) {

Based on learnings, when adding a new Settings section in SwiftUI, navigation/search wiring needs matching entries and anchors so jump-to behavior does not break.

Also applies to: 77-97

🤖 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
`@Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/FileOpeningSection.swift`
around lines 53 - 64, The SettingsCardRow entries for the new file-opening
controls (the SettingsCardRow with configurationReview
.json("app.preferredEditor") and the other two rows for
"app.openSupportedFilesInCmux" and "app.openMarkdownInCmuxViewer") need to
publish matching search anchor IDs so navigation/search can jump to the exact
control; add a search anchor/anchor preference to each row (or the control
inside it, e.g., the TextField or toggle) that emits the same identifier used in
SettingsNavigation.swift (app.preferredEditor, app.openSupportedFilesInCmux,
app.openMarkdownInCmuxViewer) so the existing SearchAnchorKey/lookup can find
and highlight the element. Ensure the anchor is attached to the interactive
control (TextField or Toggle) and uses the exact string keys registered in
SettingsNavigation.swift.
Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Rows/FileExtensionOpenersEditor.swift (2)

5-21: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Document the new public package API.

FileExtensionOpenersEditor, its public initializer, and its public body were added without DocC comments. Please add /// summaries so the package surface complies with the package API documentation rule.

As per coding guidelines, Packages/**/*.swift: Every public symbol in any new Swift package under Packages/ is documented with a Swift-DocC triple-slash comment (///) at the time of writing.

🤖 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
`@Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Rows/FileExtensionOpenersEditor.swift`
around lines 5 - 21, Add Swift-DocC triple-slash comments for the new public
API: document the public struct FileExtensionOpenersEditor with a one-line
summary of its purpose, document the public init(openers:) with a brief
description of the parameter and initialization behavior, and document the
public body property with a short description of the view it produces; place ///
comments immediately above the declarations of FileExtensionOpenersEditor,
init(openers: Binding<[String: FileExtensionOpenBehavior]>), and the body
property so the package meets the public-symbol documentation rule.

25-29: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add the missing anchor for the “File Extension Openers” row.

SettingsSearchIndex now maps app.fileExtensionOpeners to setting:fileOpening:file-extension-openers, but this row never exposes that anchor. Search/jump-to will find the entry, then fall back to the section instead of scrolling to and highlighting the editor.

♻️ Suggested change
             SettingsCardRow(
                 configurationReview: .json("app.fileExtensionOpeners"),
+                searchAnchorID: "setting:fileOpening:file-extension-openers",
                 String(localized: "settings.app.fileExtensionOpeners", defaultValue: "File Extension Openers"),
                 subtitle: String(localized: "settings.app.fileExtensionOpeners.subtitle", defaultValue: "Add any file extension and choose whether Cmd-click opens it in preview, browser, Markdown viewer, preferred editor, or system default. HTML opens in the cmux browser by default.")
             ) {

Based on learnings, when adding a new Settings section in SwiftUI, navigation/search wiring needs matching entries and anchors so jump-to behavior does not break.

🤖 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
`@Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Rows/FileExtensionOpenersEditor.swift`
around lines 25 - 29, The SettingsCardRow for the "File Extension Openers" entry
is missing the anchor that SettingsSearchIndex maps to (app.fileExtensionOpeners
-> setting:fileOpening:file-extension-openers), so searches jump to the section
instead of the row; fix this by exposing the matching anchor on the
SettingsCardRow (the same view that uses configurationReview:
.json("app.fileExtensionOpeners"))—e.g., add the anchor/id/accessibility
identifier "setting:fileOpening:file-extension-openers" to that SettingsCardRow
so jump-to highlights and scrolls to the editor.
🤖 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.

Outside diff comments:
In
`@Packages/CmuxSettings/Sources/CmuxSettings/Values/FileExtensionOpenBehaviorSettings.swift`:
- Around line 21-24: The loop currently uses
FileExtensionOpenBehavior.normalizedExtension(...) and
FileExtensionOpenBehavior(rawValue: rawBehavior) which silently drops unknown
persisted values; replace the direct enum initializer with the tolerant decoder
by calling FileExtensionOpenBehavior.decodeFromUserDefaults(rawBehavior) (or the
equivalent tolerant decode method) so unknown/stale values degrade to .automatic
instead of being skipped—keep the normalizedExtension check and use the
decoder's result to produce the behavior used in the mapping.

In
`@Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Rows/FileExtensionOpenersEditor.swift`:
- Around line 5-21: Add Swift-DocC triple-slash comments for the new public API:
document the public struct FileExtensionOpenersEditor with a one-line summary of
its purpose, document the public init(openers:) with a brief description of the
parameter and initialization behavior, and document the public body property
with a short description of the view it produces; place /// comments immediately
above the declarations of FileExtensionOpenersEditor, init(openers:
Binding<[String: FileExtensionOpenBehavior]>), and the body property so the
package meets the public-symbol documentation rule.
- Around line 25-29: The SettingsCardRow for the "File Extension Openers" entry
is missing the anchor that SettingsSearchIndex maps to (app.fileExtensionOpeners
-> setting:fileOpening:file-extension-openers), so searches jump to the section
instead of the row; fix this by exposing the matching anchor on the
SettingsCardRow (the same view that uses configurationReview:
.json("app.fileExtensionOpeners"))—e.g., add the anchor/id/accessibility
identifier "setting:fileOpening:file-extension-openers" to that SettingsCardRow
so jump-to highlights and scrolls to the editor.

In
`@Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/FileOpeningSection.swift`:
- Around line 5-25: Add Swift-DocC triple-slash comments for the new public API:
document the FileOpeningSection type, its public init(defaultsStore:catalog:)
and the public body property. For FileOpeningSection add a top-level /// comment
describing the purpose of the view; for the init add /// lines describing the
parameters (defaultsStore and catalog) and what the initializer configures; and
for the public var body add a brief /// describing the view’s content/behavior.
Place the comments immediately above the struct declaration, the init signature,
and the body property respectively so the public symbols are fully documented.
- Around line 53-64: The SettingsCardRow entries for the new file-opening
controls (the SettingsCardRow with configurationReview
.json("app.preferredEditor") and the other two rows for
"app.openSupportedFilesInCmux" and "app.openMarkdownInCmuxViewer") need to
publish matching search anchor IDs so navigation/search can jump to the exact
control; add a search anchor/anchor preference to each row (or the control
inside it, e.g., the TextField or toggle) that emits the same identifier used in
SettingsNavigation.swift (app.preferredEditor, app.openSupportedFilesInCmux,
app.openMarkdownInCmuxViewer) so the existing SearchAnchorKey/lookup can find
and highlight the element. Ensure the anchor is attached to the interactive
control (TextField or Toggle) and uses the exact string keys registered in
SettingsNavigation.swift.

In `@Sources/CommandClickFileOpenRouter.swift`:
- Around line 211-229: The current deferredOpenCommandClickFile implementation
uses Task { `@MainActor` in ... } which only enforces MainActor isolation but does
not guarantee a run-loop tick deferral; change the scheduling to
DispatchQueue.main.async { ... } to mirror deferredOpenFileInCmux and avoid
Surface.openUrl re-entrancy/deadlock. Locate the block inside
deferredOpenCommandClickFile that resolves resolvedWorkspace (using
AppDelegate.shared?.workspaceContainingPanel and isRemoteTerminalSurface) and
that calls openCommandClickFile/fallback.open, and replace the Task { `@MainActor`
in ... } scheduling with DispatchQueue.main.async { ... } so the body runs on
the next main run-loop tick while still performing the same workspace checks and
fallback logic.

In `@Sources/KeyboardShortcutSettingsFileStore.swift`:
- Around line 436-446: The code normalizes file extensions but still constructs
behaviors with FileExtensionOpenBehavior(rawValue:), rejecting non-exact
spellings; change the parsing to use the shared FileExtensionOpenBehavior
normalization helper (the helper on the FileExtensionOpenBehavior type) to
convert rawBehavior into a normalized enum case before using behavior.rawValue
when populating normalized; keep the existing guard/logInvalid/continue flow so
failures still log via logInvalid("app.fileExtensionOpeners", sourcePath:
sourcePath) and then assign
snapshot.managedUserDefaults[FileExtensionOpenBehaviorSettings.key] =
.stringDictionary(normalized).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 131a32e7-0ff0-4b4e-93a7-4cfdc23da85c

📥 Commits

Reviewing files that changed from the base of the PR and between 01ba7ad and 559c9f2.

📒 Files selected for processing (13)
  • Packages/CmuxSettings/Sources/CmuxSettings/Values/FileExtensionOpenBehavior.swift
  • Packages/CmuxSettings/Sources/CmuxSettings/Values/FileExtensionOpenBehaviorSettings.swift
  • Packages/CmuxSettings/Tests/CmuxSettingsTests/SettingCodableTests.swift
  • Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+Default.swift
  • Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Rows/FileExtensionOpenersEditor.swift
  • Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/FileOpeningSection.swift
  • Resources/Localizable.xcstrings
  • Sources/CommandClickFileOpenRouter.swift
  • Sources/KeyboardShortcutSettingsFileStore.swift
  • Sources/SettingsNavigation.swift
  • Sources/cmuxApp.swift
  • cmuxTests/WindowAndDragTests.swift
  • cmuxTests/WorkspaceUnitTests.swift

guard let normalizedExtension = FileExtensionOpenBehavior.normalizedExtension(ext) else {
return nil
}
return openers(defaults: defaults)[normalizedExtension]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Settings and routing disagree

Medium Severity

Cmd-click routing reads fileExtensionOpeners through FileExtensionOpenBehaviorSettings.openers, which always merges built-in entries such as html/htm → cmuxBrowser. The File Opening settings UI persists and reloads the raw UserDefaults map via DefaultsValueModel, so partial saves, an empty {}, or removing rows can show “no overrides” while HTML still opens in the cmux browser. The legacy card reload path re-merges defaults and can make removed built-in rows reappear.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f864d02. Configure here.

var next = normalizedOpenersCache
if next[normalized] == nil {
next[normalized] = .cmuxPreview
}

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 extensions default preview

Low Severity

When adding an extension in File Extension Openers, a new key is always initialized to cmuxPreview. Re-adding html after it is absent from the stored map therefore opens Cmd-click preview instead of the product default cmuxBrowser used by routing until the user changes the picker.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f864d02. Configure here.

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

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

private func remove(_ fileExtension: String) {
guard let normalized = FileExtensionOpenBehavior.normalizedExtension(fileExtension) else { return }
var next = normalizedOpenersCache
next.removeValue(forKey: normalized)

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 Preserve built-in overrides when removing rows

When the user removes one of the built-in rows such as .html or .htm, this only deletes the key from the stored map. FileExtensionOpenBehaviorSettings.openers(defaults:) then starts from defaultValue and overlays the stored entries, so the deleted built-in key is silently reintroduced for routing while the Settings UI no longer shows it. In that scenario Cmd-click still opens HTML in the cmux browser even though the user just removed the opener; removal needs to write an explicit override (or make built-in rows reset/non-removable) instead of simply omitting the key.

Useful? React with 👍 / 👎.

if CmdClickSupportedFileRouteSettings.shouldRoute(path: path, defaults: defaults) {
return .cmuxPreview
}
return .unhandled

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Automatic HTML still routes cmux

Medium Severity

Choosing automatic for built-in extensions such as html is meant to opt out of the built-in opener, but routing still treats the file like a normal supported preview. Cmd-click on .html can open in cmux preview instead of staying external, which conflicts with the documented opt-out and the new routing tests.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f0e9b09. Configure here.

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

ℹ️ 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 on lines +54 to +55
case .automatic:
break

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 Keep automatic overrides from falling back to preview

When a user sets a built-in opener such as html to automatic to opt out of the new cmux-browser default, this break falls through to the legacy Markdown/supported-file checks. For a readable .html file with the default openSupportedFilesInCmux setting still enabled, CmdClickSupportedFileRouteSettings.shouldRoute also falls through on .automatic, so shouldHandleCommandClick returns true and Cmd-click opens a cmux preview instead of no longer being handled by cmux. This is the exact scenario documented for disabling built-in defaults, so the automatic override needs to suppress the built-in route rather than re-enter generic supported-file routing.

Useful? React with 👍 / 👎.

@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 6 total unresolved issues (including 5 from previous reviews).

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 1f1ab68. Configure here.

Comment thread Sources/cmuxApp.swift
case .markdownViewer, .cmuxBrowser, .preferredEditor, .systemDefault:
return 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.

Automatic HTML still previews

Medium Severity

Setting html or htm to automatic is meant to opt out of the built-in browser opener, but CmdClickSupportedFileRouteSettings.shouldRoute still treats those paths like any other extension after the automatic branch and can route readable HTML into cmux preview when openSupportedFilesInCmux is on. That conflicts with the new tests and with keeping HTML off the generic supported-file path.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1f1ab68. Configure here.

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

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

) {
NSWorkspace.shared.open(fileURL)
}
filePath: fileURL.path,

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 Preserve file URL fragments for browser routes

When Cmd-clicking a local file URL that includes a fragment or query and the extension is handled by the new .cmuxBrowser route (HTML by default), this passes only fileURL.path into the router. openCommandClickFile then reconstructs URL(fileURLWithPath:), so links such as file:///tmp/report.html#section open file:///tmp/report.html in the cmux browser and lose the anchor/query that the terminal link originally contained. This only affects routed file URLs; pass the original target.url through the browser/system-default path or preserve its components before stripping for preview-only routes.

Useful? React with 👍 / 👎.

Comment on lines +1629 to +1638
private func jsonStringDictionary(_ rawValue: Any?) -> [String: String]? {
guard let values = rawValue as? [String: Any] else { return nil }
var strings: [String: String] = [:]
strings.reserveCapacity(values.count)
for (key, value) in values {
guard let string = value as? String else { return nil }
strings[key] = string
}
return strings
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 A single non-string value (e.g. "pdf": 123) causes jsonStringDictionary to return nil for the entire dict, so the caller's outer if let values = ... branch never runs and all valid extension openers are silently discarded — only logInvalid is called. The runtime FileExtensionOpenBehaviorSettings.openers() uses continue to skip bad entries; the file-store parser should match that behaviour by skipping non-string values rather than bailing out of the whole dictionary.

Suggested change
private func jsonStringDictionary(_ rawValue: Any?) -> [String: String]? {
guard let values = rawValue as? [String: Any] else { return nil }
var strings: [String: String] = [:]
strings.reserveCapacity(values.count)
for (key, value) in values {
guard let string = value as? String else { return nil }
strings[key] = string
}
return strings
}
private func jsonStringDictionary(_ rawValue: Any?) -> [String: String]? {
guard let values = rawValue as? [String: Any] else { return nil }
var strings: [String: String] = [:]
strings.reserveCapacity(values.count)
for (key, value) in values {
guard let string = value as? String else { continue }
strings[key] = string
}
return strings
}

@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: 5c433259ab

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

import SwiftUI

@MainActor
public struct FileOpeningSection: View {

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 Document the new public settings view

The repo's AGENTS.md requires every public symbol added under Packages/ to have Swift-DocC /// documentation. This new public view API (and its public initializer/body below) is currently undocumented, so the package docs contract is still violated even though the earlier FileExtensionOpenBehavior docs have been added; please add DocC comments for the public symbols introduced here.

Useful? React with 👍 / 👎.

@lawrencecchen
lawrencecchen dismissed stale reviews from coderabbitai[bot], coderabbitai[bot], and coderabbitai[bot] May 31, 2026 10:33

Addressed in later commits on this PR; the latest CodeRabbit check on head 4297516 passed/skipped.

@lawrencecchen lawrencecchen added the stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening. label Sep 23, 2026
@github-project-automation github-project-automation Bot moved this from Todo to Done in cmux backlog Sep 23, 2026

This branch was successfully deployed

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

Labels

stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants