Skip to content

Add customizable Markdown preview keybindings - #4423

Closed
lawrencecchen wants to merge 16 commits into
mainfrom
issue-4140-markdown-preview-keybindings
Closed

lawrencecchen wants to merge 16 commits into
mainfrom
issue-4140-markdown-preview-keybindings

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented May 20, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Add Markdown Preview actions for Vim-style scroll, paging, and search shortcuts.
  • Make the Markdown shortcuts customizable through Settings and ~/.config/cmux/cmux.json, including bare panel-local keys.
  • Document the shortcuts in the README, docs site data, and schema.

Testing

  • xcodebuildmcp macos test --project-path cmux.xcodeproj --scheme cmux-unit --configuration Debug --derived-data-path /tmp/cmux-mdkeys-test --extra-args=-only-testing:cmuxTests/MarkdownPanelTests/testMarkdownPreviewKeyboardShortcutsUseVimDefaults
  • xcodebuildmcp macos test --project-path cmux.xcodeproj --scheme cmux-unit --configuration Debug --derived-data-path /tmp/cmux-mdkeys-test --extra-args=-only-testing:cmuxTests/MarkdownPanelTests/testMarkdownPreviewKeyboardShortcutsCanUseBareCmuxJSONBindings
  • xcodebuildmcp macos test --project-path cmux.xcodeproj --scheme cmux-unit --configuration Debug --derived-data-path /tmp/cmux-mdkeys-test --extra-args=-only-testing:cmuxTests/MarkdownPanelTests/testMarkdownPreviewKeyCommandHandlersScrollAndSearch
  • xcodebuildmcp macos test --project-path cmux.xcodeproj --scheme cmux-unit --configuration Debug --derived-data-path /tmp/cmux-mdkeys-test --extra-args=-only-testing:cmuxTests/MarkdownPanelTests/testMarkdownRenderKeepsVisibleHeadingPositionAfterContentUpdate
  • jq empty Resources/Localizable.xcstrings web/data/cmux.schema.json web/messages/en.json web/messages/ja.json
  • ./scripts/reload.sh --tag mdkeys

Local note: the full MarkdownPanelTests class still crashes in existing WebKit image tests on this machine, while the two image tests pass when run individually.

Closes #4140


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

  • Let Codesmith autofix CI failures and bot reviews

Note

Medium Risk
Medium risk: adds new shortcut actions and routing logic (including bare-key handling) plus new WebKit prompt handling, which could affect keyboard event dispatch and shortcut conflict resolution across panels/windows.

Overview
Adds a dedicated set of Markdown Preview keyboard shortcut actions (vim-style scroll/page and in-preview find) that are panel-local, customizable via Settings and cmux.json, and allowed to use bare first strokes without modifiers.

Updates shortcut parsing/recording and conflict resolution to support these bare-key Markdown actions, introduces a markdown-specific shortcut context, and routes the Find family (⌘F/⌘G etc.) to the focused Markdown preview when applicable without stealing input from text fields/auxiliary windows.

Extends the markdown WKWebView shell to execute smooth scrolling and window.find search (with localized prompts and dialog title), adds extensive unit tests, and updates README/docs/schema/translations to document the new bindings.

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


Summary by cubic

Adds configurable, panel‑local Markdown Preview keybindings for Vim‑style scroll, half‑page moves, and search. Shortcuts are surface‑local to the focused preview in main windows and now own the Find family without breaking global chords.

  • New Features

    • Defaults: H/J/K/L scroll, Ctrl‑U/D half‑page, / and Shift‑/ search, N/Shift‑N next/previous; aliases default to Cmd‑F, Cmd‑G, Opt‑Cmd‑G, Ctrl‑N/P; all configurable.
    • Bare first strokes allowed only for these actions; supports chords; configurable in Settings or ~/.config/cmux/cmux.json.
    • In‑viewer smooth scroll and find (window.find) with localized prompts and a “Markdown Preview” dialog title; README/docs/schema/translations updated.
  • Bug Fixes

    • Added a Markdown‑panel shortcut context and surface‑local routing; handles keys only in preview mode and in main windows; avoids collisions and does not swallow chord first strokes.
    • Routes Cmd‑F/G and menu Find/Next/Previous directly to the focused preview; ignores shortcuts inside text inputs; preserves terminal and global shortcuts.
    • Fixed scroll restore near the top; kept shortcut config case compatibility and implicit Shift parsing; recorder respects per‑action bare‑first‑stroke rules.

Written for commit e4c3439. Summary will update on new commits. Review in cubic

Summary by CodeRabbit

  • New Features

    • Added Markdown Preview keyboard shortcuts for scrolling, page navigation and in‑preview find (supports bare first keystrokes when the Markdown panel is focused). Viewer preserves persistent search and handles preview key‑commands. Recorder UI respects per-action bare‑first‑stroke settings.
  • Documentation

    • Updated keyboard‑shortcuts docs, examples and schema to include Markdown Preview guidance and config samples.
  • Localization

    • Added translations for Markdown Preview labels/descriptions across many locales.
  • Tests

    • Added tests for shortcut resolution, chord behavior, and in‑viewer scroll/find interactions.

Review Change Stack

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

@vercel

vercel Bot commented May 20, 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 23, 2026 5:15am
cmux-staging Building Building Preview, Comment May 23, 2026 5:15am

@coderabbitai

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

Adds focused-panel Markdown preview navigation and in-preview search: new shortcut actions allowing optional bare first strokes, recorder/settings wiring, chord-capable resolver, WKWebView interception forwarding commands to page JS, localized strings, docs/schema updates, and tests.

Changes

Markdown Preview Keyboard Shortcuts

Layer / File(s) Summary
Markdown preview actions and flags
Sources/KeyboardShortcutSettings.swift
Ten new action cases, localized labels and defaults; allowsBareFirstStroke and isSurfaceLocalShortcutAction flags; recorded normalization rejects bare first-stroke proposals unless permitted.
Context, recorder wiring, and config parsing
Sources/KeyboardShortcutContext.swift, Sources/KeyboardShortcutRecorder.swift, Sources/AppDelegate.swift, Sources/KeyboardShortcutSettingsControls.swift, Sources/KeyboardShortcutSettings.swift
Adds .markdownPanel context; threads requireFirstStrokeModifier through recorder UI and native button; generalizes StoredShortcut.parseConfig to accept bare-first-stroke parsing and updates settings-file parsing; AppDelegate excludes surface-local actions from chord arming.
Shortcut resolver and command types
Sources/Panels/MarkdownPreviewKeyboardShortcuts.swift
MarkdownPreviewKeyCommand enum and MarkdownPreviewKeyboardShortcutResolver map single-stroke and chorded NSEvents to semantic preview commands and detect chord prefixes.
WebView interception and coordinator handling
Sources/Panels/MarkdownWebSupport.swift, Sources/Panels/MarkdownWebRenderer.swift
MarkdownWebView.onKeyboardShortcut intercepts events; renderer/coordinator buffers chords, resolves commands via resolver, forwards commands to page JS, and clears handlers on teardown; includes non-consumption fallback and DEBUG observer hook.
JavaScript search and key command handlers
Resources/markdown-viewer/shell.html
Adds persistent preview search state, scroll helpers, prompt/search flow using window.find, early top-scroll restore, and exposes window.__cmuxMarkdownPreviewSearch and window.__cmuxMarkdownPreviewHandleKeyCommand.
Assets and Xcode wiring
Sources/Panels/MarkdownViewerAssets.swift, Resources/Localizable.xcstrings, cmux.xcodeproj/project.pbxproj
Adds localized viewer string keys (search prompts), updates localized strings JSON, and registers the new Swift source in the Xcode project build configuration.
Docs, schema, examples, and i18n
README.md, web/data/cmux.schema.json, web/app/[locale]/docs/*, web/data/cmux-shortcuts.ts, web/messages/*
README adds Markdown Preview table; schema and docs include new action IDs and bare-first-stroke guidance; new shortcut category and many localization entries added across locales.
Comprehensive test coverage
cmuxTests/MarkdownPanelTests.swift, cmuxTests/AppDelegateShortcutRoutingTests.swift
Unit tests for default Vim mappings, settings-file bindings and chords, chord-miss behavior, routing tests, and integration WKWebView tests exercising JS handlers; includes test helpers and teardown cleanup.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant WebView as MarkdownWebView
  participant Coordinator as MarkdownWebRenderer.Coordinator
  participant Resolver as MarkdownPreviewKeyboardShortcutResolver
  participant JS as window.__cmuxMarkdownPreviewHandleKeyCommand
  User->>WebView: Press key
  WebView->>Coordinator: onKeyboardShortcut(event)
  alt Chord first stroke detected
    Coordinator->>Coordinator: Buffer pendingShortcutChord
    Coordinator-->>WebView: Return handled (true)
  else Single stroke or chord second stroke
    Coordinator->>Resolver: Resolve command(for:event, pendingFirstStroke)
    Resolver-->>Coordinator: MarkdownPreviewKeyCommand
    Coordinator->>JS: Forward command via JS invocation
    JS->>JS: Perform scroll/find action in page
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I hopped through docs with vim-born cheer,

j and k and / drew matches near,
Chords remembered, searches bound tight,
Scrolls in sync, the preview feels light,
A rabbit’s hop through markdown bright.


Caution

Pre-merge checks failed

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

  • Ignore

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
Cmux Architecture Rethink ❌ Error shell.html has hardcoded keyboard mappings (markdownPreviewCommandForKeyboardEvent) that duplicate Swift's resolver logic. User customizations are ignored when Swift's resolver returns nil. Remove hardcoded JS key→command mappings. Let only Swift's MarkdownPreviewKeyboardShortcutResolver determine actions so user overrides in cmux.json are honored consistently.
Docstring Coverage ⚠️ Warning Docstring coverage is 3.39% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (15 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly describes the main feature: adding customizable keybindings for Markdown previews, which directly aligns with the substantial changes across keyboard shortcut handling, markdown preview routing, and UI localization.
Linked Issues check ✅ Passed The PR fully implements the requirements from issue #4140: Vim-style navigation (h/j/k/l scrolling), paging (Ctrl+U/D), search (/ and ? with n/N cycling), user configuration via Settings and cmux.json, bare panel-local keys, chord support, and proper routing to avoid interfering with global/terminal shortcuts.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing customizable Markdown preview keybindings: keyboard shortcut infrastructure, markdown panel routing, web viewer handlers, localization, documentation, and related tests. No unrelated functionality has been introduced.
Cmux Swift Actor Isolation ✅ Passed All UI classes properly marked @MainActor. Static closure in @MainActor Coordinator is isolated. New value types and helpers don't introduce MainActor violations.
Cmux Swift Blocking Runtime ✅ Passed PR introduces no blocking/timing synchronization patterns (semaphores, sleeps, locks, or delayed dispatch) in 12 modified production Swift files.
Cmux No Hacky Sleeps ✅ Passed PR introduces no hacky sleeps in JavaScript/TypeScript/shell code. New markdown preview handlers use proper event handlers and APIs; existing UI feedback timeouts predate this PR.
Cmux Swift Concurrency ✅ Passed PR introduces no new legacy async patterns. Only instance is WKWebView.evaluateJavaScript(completionHandler: nil), which is an allowed third-party API boundary exception per concurrency rules.
Cmux Swift @Concurrent ✅ Passed All async functions in this PR are on @MainActor classes (MarkdownRendererSession, Coordinator) performing UI-bound WebKit operations. No concurrent annotation rules are violated.
Cmux Swift File And Package Boundaries ✅ Passed 103-line new file with single responsibility passes. All oversized additions <250 lines. No mixed responsibilities or inappropriate package boundaries detected.
Cmux Swift Logging ✅ Passed PR has no unguarded print/debugPrint/dump/NSLog in production code. DEBUG-only observer properly guarded with #if DEBUG. No sensitive data in logging.
Cmux User-Facing Error Privacy ✅ Passed User-facing strings added (search prompts, shortcut labels) are generic product-level descriptions with no vendor names, credentials, or sensitive implementation details exposed.
Cmux Full Internationalization ✅ Passed Swift uses String(localized:) with 19 locales; web UI updates all 20 message files; HTML prompts localized via MarkdownViewerAssets function.
Cmux Swiftui State Layout ✅ Passed PR passes check. New state (pendingShortcutChord) in NSViewRepresentable Coordinator mutated in event handlers. No new @Published/@observable, GeometryReader, or List subtree violations detected.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR adds keyboard shortcuts to existing Markdown preview panel without creating any new NSWindow, NSPanel, NSWindowController, Window, or WindowGroup instances.
Description check ✅ Passed PR description provides clear summary of changes, comprehensive testing commands, and specific test case names verifying Vim defaults, bare key parsing, shortcut routing, and viewer behavior.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-4140-markdown-preview-keybindings

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/AppDelegate.swift
@greptile-apps

greptile-apps Bot commented May 20, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds panel-local Markdown Preview keyboard shortcuts (Vim-style H/J/K/L scroll, ⌃U/D half-page, //?/N/⇧N find) that are configurable via Settings and cmux.json, including support for bare first-stroke keys scoped exclusively to the focused Markdown panel. It implements the full shortcut dispatch pipeline — from AppDelegate event interception through a new WKUIDelegate window.prompt handler for in-preview search — plus localization strings, docs, schema, and comprehensive unit tests.

  • New shortcut surface: MarkdownPreviewKeyboardShortcuts.swift resolves key events to MarkdownPreviewKeyCommand values; MarkdownWebView intercepts via performKeyEquivalent/keyDown/doCommand; AppDelegate.handleFocusedMarkdownPreviewShortcut provides the primary dispatch path with guards for command palette, terminal focus, text fields, and non-main windows.
  • Bare-key parsing: allowsBareFirstStroke and isSurfaceLocalShortcutAction on Action share identical switch bodies, creating a future drift hazard; the recorder UI correctly passes the flag through.
  • Localization gap: All 13 new Localizable.xcstrings keys and the inline JS fallback strings ship en/ja only, while the existing catalog already supports 19 locales (noted in prior review threads).

Confidence Score: 4/5

Safe to merge with the understanding that open localization gaps (already tracked in prior review threads) will be addressed as follow-up; the shortcut dispatch guards are solid and the default keybindings will not affect terminal or browser panels.

The new dispatch pipeline is well-guarded and the test coverage is thorough. The open items from prior review threads — missing xcstrings translations across 17 locales, the chord cancellation dual-dispatch edge case for custom chord users, and the find-shortcut being silently consumed before the viewer is ready — remain unresolved in this diff and affect production user experience.

Resources/Localizable.xcstrings (13 new keys, only en/ja translations) and Sources/Panels/MarkdownWebSupport.swift (doCommand override and hardcoded keyCode interception for Ctrl+P/N).

Important Files Changed

Filename Overview
Sources/Panels/MarkdownWebRenderer.swift Adds Coordinator.handleKeyboardShortcut (chord-aware dispatcher), performKeyboardCommand (JS bridge), and the WKUIDelegate prompt handler for window.prompt-based search dialogs. pendingShortcutChord is correctly reset on coordinator close.
Sources/Panels/MarkdownWebSupport.swift Adds onKeyboardShortcut hook to MarkdownWebView and overrides keyDown, performKeyEquivalent, and doCommand(by:). The doCommand override redirects all commands to nextResponder (bypassing super) and contains hardcoded keyCodes 35/45 for Ctrl+P/N interception.
Sources/Panels/MarkdownPreviewKeyboardShortcuts.swift New resolver mapping MarkdownPreviewKeyCommand to Action, supporting single-stroke and chord resolution. Stateless and well-structured.
Sources/KeyboardShortcutSettings.swift Adds 12 markdown shortcut actions with defaults, allowsBareFirstStroke/isSurfaceLocalShortcutAction (identical switch bodies — duplication risk), conflict-exclusion logic for command-palette navigation, and inferShiftFromBareKeyToken config parsing for bare uppercase keys and ?.
Sources/AppDelegate.swift Adds handleFocusedMarkdownPreviewShortcut with proper guards (main window, no pending chord, no command palette/terminal/text-input/sidebar focus) and wires Cmd-F/G family through the markdown path before the global find handler.
Sources/TabManager.swift Adds focusedMarkdownPanel computed property and routes startFind/findNext/findPrevious through the focused markdown panel before falling back to focusedBrowserPanel.
Resources/markdown-viewer/shell.html Adds JS scroll helpers, window.find-based search, markdownPreviewPromptSearch (with proper empty-string guard), and the __cmuxMarkdownPreviewHandleKeyCommand dispatch switch. Fixes nearTop scroll-restore to run immediately before the deferred apply path.
Sources/KeyboardShortcutContext.swift Adds .markdownPanel context (always returns false from isAvailable, overlaps with .nonBrowserPanel) to prevent conflict-check false positives for panel-local shortcuts.
Resources/Localizable.xcstrings Adds 13 new string keys; all carry only en and ja translations. Existing catalog entries for adjacent keys ship all 19 supported locales — the 17 missing locales will show English fallback text in shortcut settings and search dialogs.
cmuxTests/MarkdownPanelTests.swift Comprehensive unit tests covering shortcut resolution, chord behavior, bare-key config parsing, and in-viewer scroll/find routing via keyboardCommandObserver. Test-only #if DEBUG hook is well-isolated.

Sequence Diagram

sequenceDiagram
    participant User
    participant AppKit
    participant AppDelegate
    participant MarkdownWebView
    participant Coordinator
    participant JS as shell.html JS

    User->>AppKit: Key event
    AppKit->>AppDelegate: sendEvent(_:)
    AppDelegate->>AppDelegate: handleFocusedMarkdownPreviewShortcut
    Note over AppDelegate: Guards: main window, no chord prefix,<br/>no command palette/terminal/text input

    alt "Markdown panel focused & preview mode"
        AppDelegate->>Coordinator: handleKeyboardShortcut(event)
        alt Pending chord
            Coordinator->>Coordinator: clear pendingShortcutChord
            Coordinator->>Coordinator: resolve second stroke
        else Single-stroke or chord prefix
            Coordinator->>Coordinator: resolve command / set pendingShortcutChord
        end
        Coordinator->>Coordinator: performKeyboardCommand(command)
        Coordinator->>JS: evaluateJavaScript(__cmuxMarkdownPreviewHandleKeyCommand)
        JS->>JS: scroll / window.find / window.prompt
        JS-->>AppKit: WKUIDelegate: runJavaScriptTextInputPanel
        AppKit-->>User: NSAlert search dialog
        AppDelegate-->>AppKit: return true (event consumed)
    else Fallback: MarkdownWebView is first responder
        AppKit->>MarkdownWebView: performKeyEquivalent / keyDown / doCommand
        MarkdownWebView->>Coordinator: onKeyboardShortcut(event)
        Coordinator->>JS: evaluateJavaScript(...)
        MarkdownWebView-->>AppKit: return true
    else Non-markdown / browser
        AppDelegate->>AppDelegate: matchConfiguredShortcut(.find/.findNext/...)
        AppDelegate->>AppDelegate: tabManager.findNext() → focusedBrowserPanel
    end
Loading

Reviews (15): Last reviewed commit: "Tighten markdown preview shortcut dispat..." | Re-trigger Greptile

Comment thread web/messages/en.json
Comment on lines 551 to +554
"surfaces": "Surfaces",
"surfacesBlurb": "Surfaces are tabs inside a pane.",
"markdownPreview": "Markdown Preview",
"markdownPreviewBlurb": "Markdown preview shortcuts are local to the focused Markdown panel, so bare Vim-style keys do not affect terminals.",

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 Missing translations for 18 of 20 supported locales

The new markdownPreview and markdownPreviewBlurb message keys are only present in en.json and ja.json. web/i18n/routing.ts defines 20 supported locales (en, ja, zh-CN, zh-TW, ko, de, es, fr, it, da, pl, ru, bs, ar, no, pt-BR, th, tr, km, uk), so 18 locale files — ar.json, zh-CN.json, ko.json, de.json, and 14 others — are not updated. Users in those locales will silently receive the English fallback for the Markdown Preview section heading and blurb on the keyboard-shortcuts page.

Rule Used: Flag production user-facing text that is not fully... (source)

Comment thread web/data/cmux.schema.json
Comment on lines 935 to 938
"shortcutFirstStroke": {
"allOf": [
{
"$ref": "#/$defs/shortcutStroke"
},
{
"anyOf": [
{
"pattern": "^(?:(?:[cC][mM][dD]|[cC][oO][mM][mM][aA][nN][dD]|[sS][hH][iI][fF][tT]|[oO][pP][tT]|[oO][pP][tT][iI][oO][nN]|[aA][lL][tT]|[cC][tT][rR][lL]|[cC][oO][nN][tT][rR][oO][lL]|[cC][tT][lL]|⌘|⇧|⌥|⌃)\\+)+"
},
{
"pattern": "^(?: |[sS][pP][aA][cC][eE]|[sS][pP][aA][cC][eE][bB][aA][rR]|<[sS][pP][aA][cC][eE]>)$"
}
]
}
],
"description": "First or only shortcut stroke. Must include a modifier unless the key is Space."
"$ref": "#/$defs/shortcutStroke",
"description": "First or only shortcut stroke. Most actions require a modifier unless the key is Space. Markdown preview actions also accept bare first strokes such as h, j, k, l, /, and n."
},

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 Modifier-requirement validation removed from schema for all actions

The previous shortcutFirstStroke definition enforced that a modifier key (or Space) must be present via an anyOf pattern constraint. Removing it means JSON-schema-aware editors and validators will accept bare-key first strokes for any action — including actions like toggleSidebar or openBrowser where the Swift runtime will still reject bare keys at load time. Users misconfiguring non-markdown actions with bare keys will get no schema feedback, only a silent runtime rejection.

Suggested change
"shortcutFirstStroke": {
"allOf": [
{
"$ref": "#/$defs/shortcutStroke"
},
{
"anyOf": [
{
"pattern": "^(?:(?:[cC][mM][dD]|[cC][oO][mM][mM][aA][nN][dD]|[sS][hH][iI][fF][tT]|[oO][pP][tT]|[oO][pP][tT][iI][oO][nN]|[aA][lL][tT]|[cC][tT][rR][lL]|[cC][oO][nN][tT][rR][oO][lL]|[cC][tT][lL]|⌘|⇧|⌥|⌃)\\+)+"
},
{
"pattern": "^(?: |[sS][pP][aA][cC][eE]|[sS][pP][aA][cC][eE][bB][aA][rR]|<[sS][pP][aA][cC][eE]>)$"
}
]
}
],
"description": "First or only shortcut stroke. Must include a modifier unless the key is Space."
"$ref": "#/$defs/shortcutStroke",
"description": "First or only shortcut stroke. Most actions require a modifier unless the key is Space. Markdown preview actions also accept bare first strokes such as h, j, k, l, /, and n."
},
"shortcutFirstStroke": {
"oneOf": [
{
"allOf": [
{ "$ref": "#/$defs/shortcutStroke" },
{
"anyOf": [
{ "pattern": "^(?:(?:[cC][mM][dD]|[cC][oO][mM][mM][aA][nN][dD]|[sS][hH][iI][fF][tT]|[oO][pP][tT]|[oO][pP][tT][iI][oO][nN]|[aA][lL][tT]|[cC][tT][rR][lL]|[cC][oO][nN][tT][rR][oO][lL]|[cC][tT][lL]|⌘|⇧|⌥|⌃)\\+)+" },
{ "pattern": "^(?: |[sS][pP][aA][cC][eE]|[sS][pP][aA][cC][eE][bB][aA][rR]|<[sS][pP][aA][cC][eE]>)$" }
]
}
],
"description": "First stroke with a required modifier (or Space). Used by most actions."
},
{
"$ref": "#/$defs/shortcutStroke",
"description": "Bare first stroke accepted only for Markdown preview actions (e.g. h, j, k, l, /, n)."
}
],
"description": "First or only shortcut stroke. Most actions require a modifier unless the key is Space. Markdown preview actions also accept bare first strokes such as h, j, k, l, /, and n."
},

Comment on lines 30 to +45
onPointerDown?()
super.mouseDown(with: event)
}

override func keyDown(with event: NSEvent) {
if onKeyboardShortcut?(event) == true {
return
}
super.keyDown(with: event)
}

override func performKeyEquivalent(with event: NSEvent) -> Bool {
if onKeyboardShortcut?(event) == true {
return true
}
return super.performKeyEquivalent(with: event)

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 Chord cancellation can accidentally trigger a single-stroke action via dual dispatch

Both performKeyEquivalent and keyDown invoke onKeyboardShortcut. When a chord prefix (e.g., a user-configured ctrl+b) has been captured and the user then presses a key with modifier flags (e.g., ctrl+d), performKeyEquivalent is called first: handleKeyboardShortcut enters the pending-chord branch, the second stroke fails to match, the defer clears pendingShortcutChord, and the method returns false. AppKit then calls keyDown with the same event. With pendingShortcutChord now nil, handleKeyboardShortcut re-evaluates the event as a fresh single-stroke and may match markdownPageDown (⌃D), firing an unintended command. This only affects users who configure chord-based markdown shortcuts; the shipping defaults are all single-stroke.

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

Caution

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

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

2331-2348: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Support literal ? and uppercase single-key tokens in cmux.json.

Line 2331 routes Markdown preview overrides through parseConfig, but ShortcutStroke.parseConfig(_:) still treats "N" as plain n and "?" as a literal ? key with no shift. That makes the obvious Vim-style config values either wrong or unmappable, so the new settings-file customization path cannot faithfully express ?/N.

💡 Proposed fix
 extension ShortcutStroke {
     static func parseConfig(_ rawValue: String) -> ShortcutStroke? {
         guard !rawValue.isEmpty else { return nil }

         let rawParts = rawValue.split(separator: "+", omittingEmptySubsequences: false)
             .map(String.init)
         let parts = rawParts.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
         guard !parts.isEmpty, let lastRawPart = rawParts.last, !lastRawPart.isEmpty else {
             return nil
         }

         var command = false
         var shift = false
         var option = false
         var control = false

         for modifier in parts.dropLast() {
             switch modifier.lowercased() {
             case "cmd", "command", "⌘":
                 command = true
             case "shift", "⇧":
                 shift = true
             case "opt", "option", "alt", "⌥":
                 option = true
             case "ctrl", "control", "ctl", "⌃":
                 control = true
             default:
                 return nil
             }
         }

-        guard let key = parseConfigKeyToken(lastRawPart) else { return nil }
+        let trimmedKeyToken = lastRawPart.trimmingCharacters(in: .whitespacesAndNewlines)
+        guard let parsedKey = parseConfigKeyToken(lastRawPart) else { return nil }
+
+        let inferredShiftFromUppercase =
+            trimmedKeyToken.count == 1 &&
+            trimmedKeyToken.lowercased() != trimmedKeyToken &&
+            trimmedKeyToken.uppercased() == trimmedKeyToken
+
+        let inferredShiftFromSymbol = trimmedKeyToken == "?"
+        let key = trimmedKeyToken == "?" ? "/" : parsedKey
+
         return ShortcutStroke(
             key: key,
             command: command,
-            shift: shift,
+            shift: shift || inferredShiftFromUppercase || inferredShiftFromSymbol,
             option: option,
             control: control
         )
     }
🤖 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/KeyboardShortcutSettings.swift` around lines 2331 - 2348, The parser
currently normalizes tokens so uppercase letters and shift-required symbols
(like "?") lose their shift semantics; update ShortcutStroke.parseConfig(_:) to
detect single-character tokens: if the token is an uppercase ASCII letter, set
the key to the lowercase character and add the Shift modifier to modifierFlags;
if the token is a symbol that normally requires Shift (e.g., "?"), set the key
to the unshifted equivalent ("/") or represent the symbol as the key while
adding Shift to modifierFlags so the literal "?" is preserved as a shifted
stroke; ensure StoredShortcut.parseConfig(strokes:allowBareFirstStroke:)
continues to call ShortcutStroke.parseConfig(_:) and relies on the updated
modifierFlags behavior.
🤖 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 `@README.md`:
- Line 208: The table row showing the search shortcuts is ambiguous because it
repeats "/" as both the shortcut and the column separator; update the cell
content string currently showing "| / / ⇧ / | Search forward/backward |" to use
the Vim convention for backward search by replacing the middle "/" with "?" so
it reads something like "| / ? ⇧ / | Search forward/backward |", ensuring the
forward search symbol "/" and backward search symbol "?" (Shift+/) are clearly
indicated.

In `@Resources/Localizable.xcstrings`:
- Around line 113626-113709: The new localization keys (e.g.,
"shortcut.markdownScrollLeft.label", "shortcut.markdownScrollDown.label",
"shortcut.markdownScrollUp.label", "shortcut.markdownScrollRight.label",
"shortcut.markdownPageUp.label", "shortcut.markdownPageDown.label",
"shortcut.markdownFindForward.label", "shortcut.markdownFindBackward.label",
"shortcut.markdownFindNext.label", "shortcut.markdownFindPrevious.label",
"markdown.web.searchPromptForward", "markdown.web.searchPromptBackward") were
added only for en and ja; update each of these entries to include translated
values for every locale already present in Resources/Localizable.xcstrings
(following the repository’s fallback convention where applicable) so the catalog
has full locale coverage and no partial localizations for production-facing
strings.

In `@web/app/`[locale]/docs/configuration/page.tsx:
- Around line 371-372: The new English copy in
web/app/[locale]/docs/configuration/page.tsx (the inline sentence starting with
"Bare first strokes...") must be moved into next-intl messages: replace the
hard-coded text with a t('docs.configuration.bareStrokes') (or similar unique
key) call in the page component, add that key to web/i18n/routing.ts for this
route, and add corresponding entries in each web/messages/* locale file
(including the English file) so all locales are covered; ensure the key name
matches across the page, routing, and every messages file.

In `@web/data/cmux.schema.json`:
- Around line 936-937: The new English description added to the schema under
"$ref": "`#/`$defs/shortcutStroke" is user-facing and must be localized; remove or
replace the hard-coded description text in the schema and instead reference a
localization key (e.g., shortcutStroke.description) that pulls from the i18n
message bundle, then add the corresponding entries to every locale in
web/messages/ and update web/i18n/routing.ts if needed to include the key —
locate the description in cmux.schema.json (the $defs/shortcutStroke entry) and
ensure the schema only references the i18n key rather than embedding English
text.

---

Outside diff comments:
In `@Sources/KeyboardShortcutSettings.swift`:
- Around line 2331-2348: The parser currently normalizes tokens so uppercase
letters and shift-required symbols (like "?") lose their shift semantics; update
ShortcutStroke.parseConfig(_:) to detect single-character tokens: if the token
is an uppercase ASCII letter, set the key to the lowercase character and add the
Shift modifier to modifierFlags; if the token is a symbol that normally requires
Shift (e.g., "?"), set the key to the unshifted equivalent ("/") or represent
the symbol as the key while adding Shift to modifierFlags so the literal "?" is
preserved as a shifted stroke; ensure
StoredShortcut.parseConfig(strokes:allowBareFirstStroke:) continues to call
ShortcutStroke.parseConfig(_:) and relies on the updated modifierFlags behavior.
🪄 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: 2c3c940e-b13f-44da-9dc3-136c036b97cc

📥 Commits

Reviewing files that changed from the base of the PR and between d6ec6b2 and b42be41.

📒 Files selected for processing (21)
  • README.md
  • Resources/Localizable.xcstrings
  • Resources/markdown-viewer/shell.html
  • Sources/AppDelegate.swift
  • Sources/KeyboardShortcutContext.swift
  • Sources/KeyboardShortcutRecorder.swift
  • Sources/KeyboardShortcutSettings.swift
  • Sources/KeyboardShortcutSettingsControls.swift
  • Sources/KeyboardShortcutSettingsFileStore.swift
  • Sources/Panels/MarkdownPreviewKeyboardShortcuts.swift
  • Sources/Panels/MarkdownViewerAssets.swift
  • Sources/Panels/MarkdownWebRenderer.swift
  • Sources/Panels/MarkdownWebSupport.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/MarkdownPanelTests.swift
  • web/app/[locale]/docs/configuration/page.tsx
  • web/app/[locale]/docs/keyboard-shortcuts/page.tsx
  • web/data/cmux-shortcuts.ts
  • web/data/cmux.schema.json
  • web/messages/en.json
  • web/messages/ja.json

Comment thread README.md Outdated
Comment thread Resources/Localizable.xcstrings
Comment on lines +371 to +372
<code>9</code>. Bare first strokes such as <code>j</code> and <code>/</code> are accepted
for Markdown preview actions because they are only active inside the focused Markdown panel.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Move new Markdown bare-stroke explanation to next-intl messages.

Lines 371-372 introduce new user-facing English copy in a [locale] docs page; this should be translated message content (e.g., t(...)) instead of inline text.

As per coding guidelines: “Web UI/copy… must be consumed from next-intl… and represented across all locales in web/i18n/routing.ts and every matching file in web/messages/.”

🤖 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 `@web/app/`[locale]/docs/configuration/page.tsx around lines 371 - 372, The new
English copy in web/app/[locale]/docs/configuration/page.tsx (the inline
sentence starting with "Bare first strokes...") must be moved into next-intl
messages: replace the hard-coded text with a t('docs.configuration.bareStrokes')
(or similar unique key) call in the page component, add that key to
web/i18n/routing.ts for this route, and add corresponding entries in each
web/messages/* locale file (including the English file) so all locales are
covered; ensure the key name matches across the page, routing, and every
messages file.

Comment thread web/data/cmux.schema.json Outdated
Comment on lines +936 to +937
"$ref": "#/$defs/shortcutStroke",
"description": "First or only shortcut stroke. Most actions require a modifier unless the key is Space. Markdown preview actions also accept bare first strokes such as h, j, k, l, /, and n."

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Localize or relocate new schema description copy before exposing it in localized docs.

Line 937 adds new user-facing English text in schema metadata, and this description is rendered directly in locale docs; that bypasses per-locale translations.

As per coding guidelines: “Flag any user-facing text that isn’t fully localized across all supported locales… and update every locale listed in web/i18n/routing.ts with matching web/messages/ entries.”

🤖 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 `@web/data/cmux.schema.json` around lines 936 - 937, The new English
description added to the schema under "$ref": "`#/`$defs/shortcutStroke" is
user-facing and must be localized; remove or replace the hard-coded description
text in the schema and instead reference a localization key (e.g.,
shortcutStroke.description) that pulls from the i18n message bundle, then add
the corresponding entries to every locale in web/messages/ and update
web/i18n/routing.ts if needed to include the key — locate the description in
cmux.schema.json (the $defs/shortcutStroke entry) and ensure the schema only
references the i18n key rather than embedding English text.

@cubic-dev-ai cubic-dev-ai 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.

3 issues found across 21 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="web/messages/en.json">

<violation number="1" location="web/messages/en.json:553">
P1: New i18n keys `markdownPreview` and `markdownPreviewBlurb` are missing from 18 of 20 supported locale files.</violation>
</file>

<file name="Resources/markdown-viewer/shell.html">

<violation number="1" location="Resources/markdown-viewer/shell.html:1459">
P1: The new markdown find-forward/find-backward commands call `window.prompt`, but the markdown WKWebView delegate does not implement JavaScript text-input handling, so prompt behaves like Cancel and the search command never starts.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread web/messages/en.json
"workspacesBlurb": "Workspaces live in the sidebar. Each workspace has its own set of panes and surfaces.",
"surfaces": "Surfaces",
"surfacesBlurb": "Surfaces are tabs inside a pane.",
"markdownPreview": "Markdown Preview",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: New i18n keys markdownPreview and markdownPreviewBlurb are missing from 18 of 20 supported locale files.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At web/messages/en.json, line 553:

<comment>New i18n keys `markdownPreview` and `markdownPreviewBlurb` are missing from 18 of 20 supported locale files.</comment>

<file context>
@@ -550,6 +550,8 @@
         "workspacesBlurb": "Workspaces live in the sidebar. Each workspace has its own set of panes and surfaces.",
         "surfaces": "Surfaces",
         "surfacesBlurb": "Surfaces are tabs inside a pane.",
+        "markdownPreview": "Markdown Preview",
+        "markdownPreviewBlurb": "Markdown preview shortcuts are local to the focused Markdown panel, so bare Vim-style keys do not affect terminals.",
         "splitPanes": "Split Panes",
</file context>

Comment thread Resources/markdown-viewer/shell.html
Comment thread Sources/Panels/MarkdownWebRenderer.swift Outdated
Comment thread Sources/Panels/MarkdownWebRenderer.swift
Comment thread Sources/Panels/MarkdownWebRenderer.swift Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

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

293-301: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Consume Escape when cancelling a pending preview chord.

After a first chord stroke is armed, Line 300 returns false for ShortcutStroke.isEscapeCancelEvent(event). That clears pendingShortcutChord, but still lets Escape fall through to app-level handlers, so a local chord cancel can trigger unrelated global Escape behavior.

Suggested fix
             guard let command = MarkdownPreviewKeyboardShortcutResolver.command(
                 for: event,
                 pendingFirstStroke: pendingShortcutChord
             ) else {
-                return !ShortcutStroke.isEscapeCancelEvent(event)
+                return true
             }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Panels/MarkdownWebRenderer.swift` around lines 293 - 301, In
handleKeyboardShortcut, when a pendingShortcutChord exists you currently clear
pendingShortcutChord and, if
MarkdownPreviewKeyboardShortcutResolver.command(...) returns nil, return
!ShortcutStroke.isEscapeCancelEvent(event) which lets Escape propagate; change
this so that after clearing pendingShortcutChord you still detect a cancel
Escape via ShortcutStroke.isEscapeCancelEvent(event) and return true to consume
it (otherwise return false for non-Escape events). Keep the call to
MarkdownPreviewKeyboardShortcutResolver.command(...) and the
pendingShortcutChord clearing logic (function: handleKeyboardShortcut, symbol:
pendingShortcutChord, helper: ShortcutStroke.isEscapeCancelEvent, resolver:
MarkdownPreviewKeyboardShortcutResolver.command).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/Panels/MarkdownPreviewKeyboardShortcuts.swift`:
- Around line 43-45: configuredCommandActions currently mixes markdown-local
actions with globalFindCommandActions causing global find shortcuts to start
local chord prefixes; change the chord-matching to use only the markdown-local
list (commandActions) instead of configuredCommandActions: keep
configuredCommandActions as the combined list for single-stroke resolution but
update chordPrefix(...) and chordCommand(...) to iterate over commandActions (or
a new local variable like localChordActions = commandActions) so
globalFindCommandActions are excluded from prefix/chord matching; apply the same
fix wherever chordPrefix/chordCommand currently iterate the combined list.

---

Outside diff comments:
In `@Sources/Panels/MarkdownWebRenderer.swift`:
- Around line 293-301: In handleKeyboardShortcut, when a pendingShortcutChord
exists you currently clear pendingShortcutChord and, if
MarkdownPreviewKeyboardShortcutResolver.command(...) returns nil, return
!ShortcutStroke.isEscapeCancelEvent(event) which lets Escape propagate; change
this so that after clearing pendingShortcutChord you still detect a cancel
Escape via ShortcutStroke.isEscapeCancelEvent(event) and return true to consume
it (otherwise return false for non-Escape events). Keep the call to
MarkdownPreviewKeyboardShortcutResolver.command(...) and the
pendingShortcutChord clearing logic (function: handleKeyboardShortcut, symbol:
pendingShortcutChord, helper: ShortcutStroke.isEscapeCancelEvent, resolver:
MarkdownPreviewKeyboardShortcutResolver.command).
🪄 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: 63b8aea9-e9ac-436e-a3c3-a04e004d9474

📥 Commits

Reviewing files that changed from the base of the PR and between c80a1d0 and c875fd8.

📒 Files selected for processing (9)
  • README.md
  • Sources/AppDelegate.swift
  • Sources/Panels/MarkdownPanel.swift
  • Sources/Panels/MarkdownPreviewKeyboardShortcuts.swift
  • Sources/Panels/MarkdownWebRenderer.swift
  • Sources/Panels/MarkdownWebSupport.swift
  • cmuxTests/AppDelegateShortcutRoutingTests.swift
  • cmuxTests/MarkdownPanelTests.swift
  • web/data/cmux-shortcuts.ts

Comment thread Sources/Panels/MarkdownPreviewKeyboardShortcuts.swift Outdated

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 9 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Sources/AppDelegate.swift">

<violation number="1" location="Sources/AppDelegate.swift:14180">
P2: Markdown preview shortcuts are routed without verifying the first responder belongs to the markdown preview, so keys can be consumed while another input is focused.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread Sources/AppDelegate.swift Outdated
Comment thread Resources/markdown-viewer/shell.html Outdated

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

Caution

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

⚠️ Outside diff range comments (1)
cmuxTests/MarkdownPanelTests.swift (1)

413-417: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

This test doesn't actually cover the chord-miss fallback.

Line 417 uses j, but j is already a valid standalone markdown-preview shortcut. That means this still returns true even if the pending ctrl+b chord is not cleared correctly, so the test can pass while the "miss should fall through" contract is broken. Use an unbound second key and assert false, then optionally send j afterward to prove the miss cleared the pending chord.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmuxTests/MarkdownPanelTests.swift` around lines 413 - 417, The test
currently uses a standalone shortcut ("j") which masks whether a pending chord
from MarkdownWebRenderer.Coordinator.handleKeyboardShortcut(try
keyDownEvent("b", ...)) is cleared; change the second keyDownEvent call to an
unbound key (e.g., use a character not mapped to any shortcut via
keyDownEvent("x", ...)) and assert that coordinator.handleKeyboardShortcut(...)
returns false to verify the chord miss falls through, then (optionally) send the
original bound keyDownEvent("j", ...) and assert true to prove the pending chord
was cleared.
🤖 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 `@Resources/markdown-viewer/shell.html`:
- Around line 1513-1564: The code currently hardcodes default key mappings
inside markdownPreviewCommandForKeyboardEvent (used by document.addEventListener
and invoking window.__cmuxMarkdownPreviewHandleKeyCommand), which bypasses user
remaps; change it to delegate shortcut resolution to the configurable resolver
instead of in-page logic: remove the hardcoded mapping switch/if blocks in
markdownPreviewCommandForKeyboardEvent and instead call the existing resolver
API (e.g., a function exposed on window or the settings-backed resolver) with
the event/key/modifier info and use its returned command (so
markdownPreviewShouldHandleKeyboardEvent still guards events but all mapping
logic lives in the resolver); ensure document.addEventListener uses that
resolver result and no longer contains built-in defaults.

In `@Sources/Panels/MarkdownWebSupport.swift`:
- Around line 48-55: The override of doCommand(by:) must not call
super.doCommand(by:) due to the prohibited_super_call rule; instead, when the
keyboard shortcut check falls through, forward the command along the responder
chain (e.g., call nextResponder?.doCommand(by: selector) or the project-approved
fallback helper) rather than super.doCommand(by:). Update the method containing
Self.isControlNavigationCommand(...), onKeyboardShortcut?(event) and the current
super.doCommand(by: selector) call to use nextResponder?.doCommand(by: selector)
or the repository's documented fallback function per the cmux lint rules in
.github/review-bot-rules/.

---

Outside diff comments:
In `@cmuxTests/MarkdownPanelTests.swift`:
- Around line 413-417: The test currently uses a standalone shortcut ("j") which
masks whether a pending chord from
MarkdownWebRenderer.Coordinator.handleKeyboardShortcut(try keyDownEvent("b",
...)) is cleared; change the second keyDownEvent call to an unbound key (e.g.,
use a character not mapped to any shortcut via keyDownEvent("x", ...)) and
assert that coordinator.handleKeyboardShortcut(...) returns false to verify the
chord miss falls through, then (optionally) send the original bound
keyDownEvent("j", ...) and assert true to prove the pending chord was cleared.
🪄 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: e8004286-3195-4e6b-8e48-589b5ec05e82

📥 Commits

Reviewing files that changed from the base of the PR and between c875fd8 and 9b9881f.

📒 Files selected for processing (8)
  • Resources/markdown-viewer/shell.html
  • Sources/AppDelegate.swift
  • Sources/Panels/MarkdownPanel.swift
  • Sources/Panels/MarkdownWebRenderer.swift
  • Sources/Panels/MarkdownWebSupport.swift
  • Sources/TabManager.swift
  • cmuxTests/AppDelegateShortcutRoutingTests.swift
  • cmuxTests/MarkdownPanelTests.swift

Comment thread Resources/markdown-viewer/shell.html Outdated
Comment on lines +1513 to +1564
function markdownPreviewShouldHandleKeyboardEvent(ev) {
if (!ev || ev.defaultPrevented || ev.isComposing) { return false; }
var target = ev.target;
if (target && target.closest && target.closest('input, textarea, select, [contenteditable=""], [contenteditable="true"]')) {
return false;
}
return true;
}

function markdownPreviewCommandForKeyboardEvent(ev) {
if (!markdownPreviewShouldHandleKeyboardEvent(ev)) { return null; }
var key = String(ev.key || '').toLowerCase();
var hasCommand = !!ev.metaKey;
var hasControl = !!ev.ctrlKey;
var hasOption = !!ev.altKey;
var hasShift = !!ev.shiftKey;

if (hasCommand && !hasControl && !hasOption && !hasShift && key === 'f') { return 'findForward'; }
if (hasCommand && !hasControl && !hasOption && !hasShift && key === 'g') { return 'findNext'; }
if (hasCommand && !hasControl && hasOption && !hasShift && key === 'g') { return 'findPrevious'; }
if (!hasCommand && hasControl && !hasOption && !hasShift && key === 'n') { return 'findNext'; }
if (!hasCommand && hasControl && !hasOption && !hasShift && key === 'p') { return 'findPrevious'; }
if (!hasCommand && hasControl && !hasOption && !hasShift && key === 'u') { return 'pageUp'; }
if (!hasCommand && hasControl && !hasOption && !hasShift && key === 'd') { return 'pageDown'; }
if (!hasCommand && !hasControl && !hasOption && !hasShift) {
switch (key) {
case 'h': return 'scrollLeft';
case 'j': return 'scrollDown';
case 'k': return 'scrollUp';
case 'l': return 'scrollRight';
case '/': return 'findForward';
case 'n': return 'findNext';
default: break;
}
}
if (!hasCommand && !hasControl && !hasOption && hasShift) {
switch (key) {
case '?': return 'findBackward';
case 'n': return 'findPrevious';
default: break;
}
}
return null;
}

document.addEventListener('keydown', function(ev) {
var command = markdownPreviewCommandForKeyboardEvent(ev);
if (!command) { return; }
ev.preventDefault();
ev.stopPropagation();
window.__cmuxMarkdownPreviewHandleKeyCommand(command);
}, true);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep shortcut resolution in the configurable resolver, not in the page.

This re-encodes the default preview bindings a second time. If a user remaps or removes h/j/k/l, /, n/N, Cmd-F, Cmd-G, Ctrl-N/P, or Ctrl-U/D, Line 1558 will still consume the old defaults whenever the event reaches the DOM, so the new Settings/cmux.json customization does not actually take effect in the preview.

Suggested direction
-  function markdownPreviewShouldHandleKeyboardEvent(ev) {
-    if (!ev || ev.defaultPrevented || ev.isComposing) { return false; }
-    var target = ev.target;
-    if (target && target.closest && target.closest('input, textarea, select, [contenteditable=""], [contenteditable="true"]')) {
-      return false;
-    }
-    return true;
-  }
-
-  function markdownPreviewCommandForKeyboardEvent(ev) {
-    if (!markdownPreviewShouldHandleKeyboardEvent(ev)) { return null; }
-    ...
-  }
-
-  document.addEventListener('keydown', function(ev) {
-    var command = markdownPreviewCommandForKeyboardEvent(ev);
-    if (!command) { return; }
-    ev.preventDefault();
-    ev.stopPropagation();
-    window.__cmuxMarkdownPreviewHandleKeyCommand(command);
-  }, true);
+  // Keep only the semantic command handler here.
+  // Let the native markdown-preview shortcut resolver decide which
+  // keys/chords are active so user overrides and disabled bindings
+  // are honored consistently.
🤖 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 `@Resources/markdown-viewer/shell.html` around lines 1513 - 1564, The code
currently hardcodes default key mappings inside
markdownPreviewCommandForKeyboardEvent (used by document.addEventListener and
invoking window.__cmuxMarkdownPreviewHandleKeyCommand), which bypasses user
remaps; change it to delegate shortcut resolution to the configurable resolver
instead of in-page logic: remove the hardcoded mapping switch/if blocks in
markdownPreviewCommandForKeyboardEvent and instead call the existing resolver
API (e.g., a function exposed on window or the settings-backed resolver) with
the event/key/modifier info and use its returned command (so
markdownPreviewShouldHandleKeyboardEvent still guards events but all mapping
logic lives in the resolver); ensure document.addEventListener uses that
resolver result and no longer contains built-in defaults.

Comment thread Sources/Panels/MarkdownWebSupport.swift

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 8 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Resources/markdown-viewer/shell.html">

<violation number="1" location="Resources/markdown-viewer/shell.html:1433">
P2: Per-keystroke scrolling uses smooth animation, which can drop/flatten repeated scroll steps when keys are pressed quickly.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

var nextLeft = Math.max(0, (window.scrollX || scroller.scrollLeft || 0) + deltaX);
var nextTop = clampScrollY((window.scrollY || scroller.scrollTop || 0) + deltaY);
try {
window.scrollTo({ left: nextLeft, top: nextTop, behavior: 'smooth' });

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: Per-keystroke scrolling uses smooth animation, which can drop/flatten repeated scroll steps when keys are pressed quickly.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Resources/markdown-viewer/shell.html, line 1433:

<comment>Per-keystroke scrolling uses smooth animation, which can drop/flatten repeated scroll steps when keys are pressed quickly.</comment>

<file context>
@@ -1429,16 +1429,11 @@
-    var previousBehavior = root.style.scrollBehavior;
-    root.style.scrollBehavior = 'auto';
     try {
+      window.scrollTo({ left: nextLeft, top: nextTop, behavior: 'smooth' });
+    } catch (e) {
       window.scrollTo(nextLeft, nextTop);
</file context>
Suggested change
window.scrollTo({ left: nextLeft, top: nextTop, behavior: 'smooth' });
window.scrollTo({ left: nextLeft, top: nextTop, behavior: 'auto' });

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 3 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="web/messages/en.json">

<violation number="1" location="web/messages/en.json:553">
P1: New i18n keys `markdownPreview` and `markdownPreviewBlurb` are missing from 18 of 20 supported locale files.</violation>
</file>

<file name="Resources/markdown-viewer/shell.html">

<violation number="1" location="Resources/markdown-viewer/shell.html:1433">
P2: Per-keystroke scrolling uses smooth animation, which can drop/flatten repeated scroll steps when keys are pressed quickly.</violation>
</file>

<file name="Sources/Panels/MarkdownWebRenderer.swift">

<violation number="1" location="Sources/Panels/MarkdownWebRenderer.swift:423">
P2: Add full locale coverage for `markdown.preview.dialog.title` in `Localizable.xcstrings`; it currently has only `en` and `ja` translations.

(Based on your team's feedback about Localizable.xcstrings locale coverage.) [FEEDBACK_USED]</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

if !title.isEmpty {
return title
}
return String(localized: "markdown.preview.dialog.title", defaultValue: "Markdown Preview")

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: Add full locale coverage for markdown.preview.dialog.title in Localizable.xcstrings; it currently has only en and ja translations.

(Based on your team's feedback about Localizable.xcstrings locale coverage.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/Panels/MarkdownWebRenderer.swift, line 423:

<comment>Add full locale coverage for `markdown.preview.dialog.title` in `Localizable.xcstrings`; it currently has only `en` and `ja` translations.

(Based on your team's feedback about Localizable.xcstrings locale coverage.) </comment>

<file context>
@@ -384,6 +384,59 @@ struct MarkdownWebRenderer: NSViewRepresentable {
+            if !title.isEmpty {
+                return title
+            }
+            return String(localized: "markdown.preview.dialog.title", defaultValue: "Markdown Preview")
+        }
+
</file context>

Comment on lines 102672 to 102692
}
}
},
"markdown.preview.dialog.title": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Markdown Preview"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "Markdown プレビュー"
}
}
}
},
"markdown.mode.showPreview": {

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 markdown.preview.dialog.title missing translations for 17 locales

The new "markdown.preview.dialog.title" entry, used as the messageText of the NSAlert search dialog in MarkdownWebRenderer.Coordinator.markdownDialogTitle(), only ships English and Japanese translations. The existing string catalog already supports ar, bs, da, de, es, fr, it, ko, nb, pl, pt-BR, ru, th, tr, uk, zh-Hans, and zh-Hant — confirmed by adjacent entries like shortcut.markdownScrollLeft.label and markdown.web.searchPromptForward, which both carry all 19 locales. Users in those 17 locales will see "Markdown Preview" (English) as the alert window title whenever they invoke a find command in the Markdown preview.

Rule Used: Flag production user-facing text that is not fully... (source)

…review-keybindings

# Conflicts:
#	Sources/TabManager.swift
default:
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.

Identical computed properties duplicate the same switch logic

Low Severity

allowsBareFirstStroke and isSurfaceLocalShortcutAction have byte-for-byte identical switch implementations covering the same 10 markdown cases. One could delegate to the other (or both to a shared isMarkdownPreviewAction property) to avoid maintaining two identical switch blocks that must be kept in sync when new panel-local actions are added.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4b58f65. Configure here.

Comment on lines +1462 to +1465
var query = window.prompt(cmuxString(promptKey, fallback), markdownPreviewSearchQuery);
if (query == null) { return false; }
markdownPreviewSearchQuery = String(query);
return markdownPreviewSearch(markdownPreviewSearchQuery, backwards);

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 Search query lost on empty-string submission — markdownPreviewSearchQuery is overwritten with the empty string before markdownPreviewSearch validates it. If the user opens the prompt (pressing / or ?) and then submits without typing anything, the module-level variable is set to "" and the previous query is gone. Any subsequent n / N keystroke will call markdownPreviewSearch("", …), hit the if (!value) guard immediately, and do nothing until a fresh search is initiated. The fix is to only commit the new query when it is non-empty.

Suggested change
var query = window.prompt(cmuxString(promptKey, fallback), markdownPreviewSearchQuery);
if (query == null) { return false; }
markdownPreviewSearchQuery = String(query);
return markdownPreviewSearch(markdownPreviewSearchQuery, backwards);
var query = window.prompt(cmuxString(promptKey, fallback), markdownPreviewSearchQuery);
if (query == null) { return false; }
var trimmedQuery = String(query);
if (trimmedQuery) { markdownPreviewSearchQuery = trimmedQuery; }
return markdownPreviewSearch(markdownPreviewSearchQuery, backwards);

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 08d9328. Configure here.


static var actions: [KeyboardShortcutSettings.Action] {
commandActions.map(\.1)
}

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 actions property is dead code

Low Severity

The MarkdownPreviewKeyboardShortcutResolver.actions computed property is defined but never referenced anywhere in the codebase. A grep for this property across all source and test files returns zero matches. This is unused dead code introduced by the PR.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 08d9328. Configure here.

…review-keybindings

# Conflicts:
#	web/messages/ar.json
#	web/messages/bs.json
#	web/messages/da.json
#	web/messages/de.json
#	web/messages/en.json
#	web/messages/es.json
#	web/messages/fr.json
#	web/messages/it.json
#	web/messages/ja.json
#	web/messages/km.json
#	web/messages/ko.json
#	web/messages/no.json
#	web/messages/pl.json
#	web/messages/pt-BR.json
#	web/messages/ru.json
#	web/messages/th.json
#	web/messages/tr.json
#	web/messages/uk.json
#	web/messages/zh-CN.json
#	web/messages/zh-TW.json
Comment on lines +57 to +64
func handlePreviewKeyboardShortcut(_ event: NSEvent) -> Bool {
guard displayMode == .preview else { return false }
return rendererSession.handleKeyboardShortcut(event)
}

@discardableResult
func performPreviewKeyboardCommand(_ command: MarkdownPreviewKeyCommand) -> Bool {
guard displayMode == .preview else { return false }

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 Find shortcut silently consumed before the viewer is ready

performPreviewKeyboardCommand returns true whenever displayMode == .preview, but rendererSession.performKeyboardCommand calls evaluateJavaScript without guarding on whether the shell has loaded. When the WKWebView is nil (coordinator closed) or the page is still loading (isLoaded == false), the JS guard window.__cmuxMarkdownPreviewHandleKeyCommand && short-circuits to false and nothing executes — yet the function still returns true, consuming the shortcut. A user who presses / or Cmd-F the moment a Markdown panel opens will get no search dialog and no feedback, and the fallback to focusedBrowserPanel?.startFind() in performFindShortcutInActiveMainWindow is never reached. The same applies to the AppDelegate path through handlePreviewKeyboardShortcut. Returning false when !coordinator.isLoaded (or surfacing the loaded state through MarkdownRendererSession) would let the event fall through to other handlers.

@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 — e4c3439a Deployed May 23, 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.

[Feature Request]: Enhance Markdown Preview with Customizable and Vim-style Keybindings

2 participants