Skip to content

Cmd+[ / Cmd+] traverse the global workspace focus history like the titlebar arrows - #9299

Closed
azooz2003-bit wants to merge 2 commits into
mainfrom
feat-cmd-bracket-workspace-history
Closed

azooz2003-bit wants to merge 2 commits into
mainfrom
feat-cmd-bracket-workspace-history

Conversation

@azooz2003-bit

@azooz2003-bit azooz2003-bit commented Jul 31, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes the default ⌘[ / ⌘] bindings never reaching focus history. Ghostty's macOS defaults bind goto_split:previous/next to the same keys, and the shortcut dispatch checked that mirror before the focus-history branch, consuming the key to cycle pane focus inside the current workspace (a no-op in single-pane workspaces). The titlebar arrows call TabManager.navigateBack()/navigateForward() directly, so the buttons crossed workspaces while the keys never did.

The goto_split mirror now yields any event that matches a bound Focus Back/Forward shortcut (full matchConfiguredShortcut, including shortcuts.when overrides), in both the app-level monitor dispatch and the window key-equivalent fallback. ⌘[ / ⌘] therefore run the exact arrow-button path: same per-window history model, same closed-workspace pruning, same availability conditions.

Default change: ⌘[ / ⌘] previously cycled panes through the Ghostty mirror; they now navigate global focus history, matching what the keyboard-shortcuts docs already claimed. The old behavior is preserved two ways: unbind Focus Back/Forward and the mirror gets the keys back, or bind the new rebindable actions focusPreviousPane / focusNextPane (default unbound, same cyclePaneFocus body). The new actions follow the full shortcut policy: KeyboardShortcutSettings + ShortcutAction (defaults, display names, panes group), Settings recorder rows, shortcuts.bindings.* in ~/.config/cmux/cmux.json, cmux.schema.json, the web keyboard-shortcuts page (en+ja), and skills/cmux-settings/references/shortcut-actions.md. Labels are localized in Resources/Localizable.xcstrings for every catalog language.

Regression coverage lands red then green across two commits: FocusHistoryBracketShortcutRoutingTests (unit: real ⌘[ / ⌘] events through debugHandleCustomShortcut with the mirror installed via a DEBUG seam), FocusHistoryShortcutUITests (e2e over the control socket; simulate_shortcut shares the monitor's matcher and dispatch order), and tests_v2/test_focus_history_shortcut_cross_workspace.py for tagged-build socket verification.

Dictionary: goto_split is Ghostty's action for moving focus between terminal splits; the mirror is cmux's app-level re-implementation of those Ghostty keybinds so they work across cmux panes; focus history is the per-window stack behind the titlebar back/forward arrows; a when clause is the cmux.json predicate that scopes a shortcut to a focus context.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Summary by cubic

Cmd+[ / Cmd+] now navigate the global workspace focus history, matching the titlebar arrows. The Ghostty goto_split:previous/next mirror yields to Focus Back/Forward so these keys reach focus history; pane cycling is now rebindable.

  • Bug Fixes

    • Keys now call TabManager.navigateBack() / navigateForward() across workspaces and skip closed ones.
    • Ghostty mirror and window key-equivalent routes yield when Focus Back/Forward is bound (honors shortcuts.when).
    • Added unit, UI, and socket tests; introduced a DEBUG seam to install the Ghostty mirror and simulate shortcuts.
  • Migration

    • To keep pane cycling on Cmd+[ / Cmd+], unbind Focus Back/Forward, or bind focusPreviousPane / focusNextPane (default unbound).
    • Rebind in Settings or ~/.config/cmux/cmux.json via shortcuts.bindings.focusPreviousPane / shortcuts.bindings.focusNextPane.

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

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added configurable shortcuts for navigating to the previous or next pane.
    • Added localized labels and settings documentation for the new actions.
    • New pane-navigation shortcuts are unbound by default, preserving existing key bindings.
  • Bug Fixes

    • Improved shortcut routing so configured focus-history shortcuts take priority over conflicting pane-cycle shortcuts.
    • Focus history now navigates across workspaces while skipping closed workspaces.
  • Tests

    • Added coverage for workspace focus history and conflicting bracket-key shortcuts.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds unbound previous- and next-focus shortcut actions, localized metadata, and schema entries. AppDelegate gives configured focus-history shortcuts precedence over mirrored Ghostty pane-cycle shortcuts. Regression tests cover traversal, rebinding, and fallback routing.

Changes

Focus history shortcuts

Layer / File(s) Summary
Shortcut actions and metadata
Packages/macOS/CmuxSettings/..., Sources/KeyboardShortcutSettings.swift, Resources/Localizable.xcstrings, web/data/*, skills/cmux-settings/references/*
Adds focusPreviousPane and focusNextPane actions. Both actions are grouped under panes and unbound by default. Localized labels, schema entries, descriptions, and reference metadata are included.
Shortcut routing precedence
Sources/AppDelegate.swift
Configured focus-history shortcuts take precedence over mirrored Ghostty pane-cycle shortcuts. A DEBUG-only seam configures mirrored Ghostty shortcuts for tests.
Routing regression coverage
cmuxTests/*, cmuxUITests/*, tests_v2/*, cmux.xcodeproj/project.pbxproj
Adds unit, UI, and Python tests for focus-history traversal, closed-workspace skipping, shortcut rebinding, and Ghostty fallback routing. Registers the new tests in the Xcode project.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant KeyboardEvent
  participant AppDelegate
  participant GhosttyGotoSplit
  KeyboardEvent->>AppDelegate: Dispatch Cmd+[ or Cmd+]
  AppDelegate->>AppDelegate: Check configured focus-history shortcut
  AppDelegate->>GhosttyGotoSplit: Route to pane cycling when no focus-history match
Loading

Possibly related issues

Possibly related PRs

  • manaflow-ai/cmux#9296 — Modifies the same AppDelegate arbitration between focus-history shortcuts and Ghostty goto_split fallbacks.
  • manaflow-ai/cmux#8853 — Adds related focus-history shortcut routing and Cmd-bracket regression coverage.
  • manaflow-ai/cmux#9046 — Coordinates Ghostty pane-focus cycling with cmux focus-history shortcuts.

Suggested reviewers: lawrencecchen


Important

Pre-merge checks failed

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

❌ Failed checks (2 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Full Internationalization ❌ Error New web shortcut descriptions and notes only provide en/ja, and both new Swift catalog keys omit the existing km locale. Add locale-specific web entries for every locale in web/i18n/routing.ts, and add translated km values to both new Localizable.xcstrings keys.
Cmux No Test Or Debug Seam In Production Source ❌ Error Sources/AppDelegate.swift adds DEBUG-only debugSetGhosttyGotoSplitCycleShortcuts, which writes private Ghostty shortcut state and is called only by cmuxTests. Remove the production seam; expose needed state as internal and read it from tests via @testable import, following #6452.
Docstring Coverage ⚠️ Warning Docstring coverage is 15.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (22 passed)
Check name Status Explanation
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 Production changes add cases to an existing Sendable value enum and a helper inside @MainActor AppDelegate; no new shared Sendable references, protocols, or background UI access were introduced.
Cmux Swift Blocking Runtime ✅ Passed The production Swift diff adds shortcut matching and a DEBUG test seam only; it adds no semaphore, sleep, delayed dispatch, polling, main-queue sync, or manual lock. Timing waits are test-only.
Cmux Browser Automation Off-Main ✅ Passed The PR changes shortcut routing and tests only; neither scoped browser automation file changes, and the diff adds no browser socket or WebKit wait commands.
Cmux Expensive Synchronous Load ✅ Passed The production diff adds only shortcut matching and settings cases; added Swift lines contain no agent-history loader, transcript/file parse, directory scan, or other prohibited synchronous load.
Cmux Cache Substitution Correctness ✅ Passed The full PR diff adds shortcut routing and metadata only; it does not replace an authoritative persistence/history/undo/snapshot read with a cache or opportunistic value.
Cmux No Hacky Sleeps ✅ Passed The only non-Swift timing code is in the new Python regression test, where sleeps support test setup and polling; the TypeScript changes are shortcut metadata with no timers or waits.
Cmux Algorithmic Complexity ✅ Passed Production changes add fixed-count shortcut checks and enum/data entries only; no new loops, filters, sorts, nested scans, or per-workspace work. Settings lookups use dictionaries/sets.
Cmux Swift Concurrency ✅ Passed The PR adds no new Dispatch, Combine, completion-handler, or fire-and-forget Task patterns; its only async additions are XCTest methods using async/await, which the rule allows.
Cmux Swift @Concurrent ✅ Passed The Swift diff adds only synchronous shortcut cases and matcher logic; it introduces no async, nonisolated, @concurrent, or heavy async call sites.
Cmux Swift Package Boundaries ✅ Passed The diff keeps reusable ShortcutAction/default metadata in CmuxSettings; app changes are AppKit shortcut tables and AppDelegate Ghostty/event-routing glue, which the rule explicitly allows.
Cmux Swiftpm Lockfiles ✅ Passed The PR adds only Xcode test file references; no Package.swift, Package.resolved, .gitignore, or SwiftPM package-reference lines changed, so no lockfile update is required.
Cmux Swift Logging ✅ Passed The Swift diff adds shortcut routing and settings only; no added or materially changed print, debugPrint, dump, NSLog, file logging, Logger declaration, or sensitive-data log.
Cmux User-Facing Error Privacy ✅ Passed Production additions contain shortcut labels and advanced help, not user-facing errors or recovery copy; Ghostty references are in help/comments/tests, with no sensitive diagnostics or secrets.
Cmux Swiftui State Layout ✅ Passed The PR adds no SwiftUI views or state/layout patterns. Changed Swift code is AppKit shortcut routing, settings enums, and XCTest/Testing regression coverage.
Cmux Architecture Rethink ✅ Passed The PR is a local routing fix: production code adds no timing, locks, observers, caches, or new state owner; focus history remains in TabManager, and the DEBUG seam plus waits are test-only.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed The Swift diff changes shortcut routing and adds no standalone NSWindow, NSPanel, Window, WindowGroup, identifier assignment, or Cmd+W workaround.
Cmux Source Artifacts ✅ Passed All 14 changed paths are intentional source, tests, project/config files, docs, web data, or localization; no artifact directories, logs, binaries, caches, or build output were added.
Cmux No Ambient Global State ✅ Passed The production Swift diff adds enum cases, extension switch branches, and instance methods on AppDelegate; it adds no file-scope functions, mutable globals, static-only namespace, singleton, or new...
Title check ✅ Passed The title clearly and concisely states that Cmd+[ / Cmd+] now traverse global workspace focus history, matching the main change.
Description check ✅ Passed The description clearly explains the change, rationale, compatibility behavior, and regression testing, but omits the template headings, demo video, and checklist.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch feat-cmd-bracket-workspace-history
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-cmd-bracket-workspace-history

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.

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

🤖 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 `@cmuxUITests/FocusHistoryShortcutUITests.swift`:
- Around line 251-267: Remove the count < buffer.count early-break condition
from the response-reading loop in the visible read logic. Continue accumulating
chunks until a newline is found, the deadline expires, or Darwin.read returns a
non-positive count, preserving the existing timeout and accumulator return
behavior.

In `@Sources/AppDelegate.swift`:
- Around line 786-796: Remove debugSetGhosttyGotoSplitCycleShortcuts from
Sources/AppDelegate.swift and relocate its test override behavior to a dedicated
debug support file or test-harness injection outside Sources/**/*.swift.
Preserve the ability of FocusHistoryBracketShortcutRoutingTests to configure the
Ghostty previous/next shortcuts without adding a test-only member to AppDelegate
production source.

In `@tests_v2/test_focus_history_shortcut_cross_workspace.py`:
- Around line 55-86: Replace fixed sleeps in
tests_v2/test_focus_history_shortcut_cross_workspace.py lines 55-86 after
activate_app, workspace creation/selection, and close_workspace with
deadline-bounded polls of _selected_workspace(c) or list_workspaces(), following
_press_and_wait; update cmuxUITests/FocusHistoryShortcutUITests.swift lines
115-123 to use waitForCurrentWorkspace(_:timeout:) instead of
RunLoop.current.run. Ensure all synchronization waits on real completion
predicates.
- Around line 55-56: Replace the fixed time.sleep calls in the focus-history
test with deadline-bounded polling of real state, following _press_and_wait.
After activate_app, poll c.ping(); after each select_workspace, poll until
_selected_workspace(c) equals the target workspace; and after close_workspace,
poll list_workspaces() until the workspace is absent. Preserve the existing test
sequence and assertions.
- Around line 32-48: Update _press_and_wait to tolerate transient cmuxError
exceptions from _selected_workspace during shortcut polling: catch the error
inside the deadline-bounded loop, retain the previous/current workspace value
when no workspace is selected, and continue retrying until expected_ws is
observed or the timeout expires. Preserve the existing final return behavior.

In `@web/data/cmux-shortcuts.ts`:
- Around line 315-332: Add complete locale coverage to the focusPreviousPane and
focusNextPane entries in the shortcut definitions, adding description and note
translations for zh-CN, zh-TW, ko, de, es, fr, it, da, pl, ru, bs, ar, no,
pt-BR, th, tr, km, and uk alongside en and ja. Keep each locale’s existing
translation structure and ensure both shortcuts receive matching fields.
🪄 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 Plus

Run ID: b1e70f4e-cdc3-4cf9-86e5-ee0c78b66937

📥 Commits

Reviewing files that changed from the base of the PR and between 9e3e324 and da9a0fe.

📒 Files selected for processing (14)
  • Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction+Defaults.swift
  • Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction+DisplayName.swift
  • Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction+Group.swift
  • Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction.swift
  • Resources/Localizable.xcstrings
  • Sources/AppDelegate.swift
  • Sources/KeyboardShortcutSettings.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/FocusHistoryBracketShortcutRoutingTests.swift
  • cmuxUITests/FocusHistoryShortcutUITests.swift
  • skills/cmux-settings/references/shortcut-actions.md
  • tests_v2/test_focus_history_shortcut_cross_workspace.py
  • web/data/cmux-shortcuts.ts
  • web/data/cmux.schema.json

Comment on lines +251 to +267
var buffer = [UInt8](repeating: 0, count: 4096)
var accumulator = ""
let deadline = Date().addingTimeInterval(responseTimeout)
while Date() < deadline {
let count = Darwin.read(fd, &buffer, buffer.count)
guard count > 0 else { break }
if let chunk = String(bytes: buffer[0..<count], encoding: .utf8) {
accumulator.append(chunk)
if let newline = accumulator.firstIndex(of: "\n") {
return String(accumulator[..<newline])
}
if count < buffer.count {
break
}
}
}
return accumulator.isEmpty ? nil : accumulator.trimmingCharacters(in: .whitespacesAndNewlines)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

The early break on a partial read can truncate a slow multi-chunk response.

At line 262-264, the loop breaks as soon as count < buffer.count, even if no newline has been found yet. Darwin.read on a stream socket can legitimately return fewer bytes than requested while more of the line is still arriving, in which case this heuristic ends the loop prematurely and returns an incomplete response. SO_RCVTIMEO is already set on fd (lines 212-219), so the loop's Date() < deadline condition together with a count <= 0 timeout return is sufficient to bound the read; the partial-read heuristic is not needed and adds a truncation risk.

🔧 Proposed fix
                 if let chunk = String(bytes: buffer[0..<count], encoding: .utf8) {
                     accumulator.append(chunk)
                     if let newline = accumulator.firstIndex(of: "\n") {
                         return String(accumulator[..<newline])
                     }
-                    if count < buffer.count {
-                        break
-                    }
                 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var buffer = [UInt8](repeating: 0, count: 4096)
var accumulator = ""
let deadline = Date().addingTimeInterval(responseTimeout)
while Date() < deadline {
let count = Darwin.read(fd, &buffer, buffer.count)
guard count > 0 else { break }
if let chunk = String(bytes: buffer[0..<count], encoding: .utf8) {
accumulator.append(chunk)
if let newline = accumulator.firstIndex(of: "\n") {
return String(accumulator[..<newline])
}
if count < buffer.count {
break
}
}
}
return accumulator.isEmpty ? nil : accumulator.trimmingCharacters(in: .whitespacesAndNewlines)
var buffer = [UInt8](repeating: 0, count: 4096)
var accumulator = ""
let deadline = Date().addingTimeInterval(responseTimeout)
while Date() < deadline {
let count = Darwin.read(fd, &buffer, buffer.count)
guard count > 0 else { break }
if let chunk = String(bytes: buffer[0..<count], encoding: .utf8) {
accumulator.append(chunk)
if let newline = accumulator.firstIndex(of: "\n") {
return String(accumulator[..<newline])
}
}
}
return accumulator.isEmpty ? nil : accumulator.trimmingCharacters(in: .whitespacesAndNewlines)
🤖 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 `@cmuxUITests/FocusHistoryShortcutUITests.swift` around lines 251 - 267, Remove
the count < buffer.count early-break condition from the response-reading loop in
the visible read logic. Continue accumulating chunks until a newline is found,
the deadline expires, or Darwin.read returns a non-positive count, preserving
the existing timeout and accumulator return behavior.

Comment thread Sources/AppDelegate.swift
Comment on lines +786 to +796

#if DEBUG
/// Test seam: unit tests can install the mirrored Ghostty
/// goto_split:previous/next triggers without loading a Ghostty config
/// (Ghostty's macOS defaults put them on ⌘[ / ⌘], colliding with the
/// focus-history defaults this dispatch must win).
func debugSetGhosttyGotoSplitCycleShortcuts(previous: StoredShortcut?, next: StoredShortcut?) {
ghosttyGotoSplitPreviousShortcut = previous
ghosttyGotoSplitNextShortcut = next
}
#endif

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Remove the test-only seam from the production source.

cmuxTests/FocusHistoryBracketShortcutRoutingTests.swift calls debugSetGhosttyGotoSplitCycleShortcuts, and the method exists only under #if DEBUG. This adds a test-only debug... member to Sources/AppDelegate.swift. Move the override to a dedicated debug support file or inject the Ghostty shortcut source through the test harness.

As per path instructions, **/Sources/**/*.swift must not add test-only or debug-only seams in production Swift source; isolate an unavoidable debug facility in a dedicated debug file or folder.

🤖 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/AppDelegate.swift` around lines 786 - 796, Remove
debugSetGhosttyGotoSplitCycleShortcuts from Sources/AppDelegate.swift and
relocate its test override behavior to a dedicated debug support file or
test-harness injection outside Sources/**/*.swift. Preserve the ability of
FocusHistoryBracketShortcutRoutingTests to configure the Ghostty previous/next
shortcuts without adding a test-only member to AppDelegate production source.

Source: Path instructions

Comment on lines +32 to +48
def _selected_workspace(c: cmux) -> str:
for _idx, wsid, _title, selected in c.list_workspaces():
if selected:
return wsid
raise cmuxError("no selected workspace")


def _press_and_wait(c: cmux, combo: str, expected_ws: str, timeout: float = 5.0) -> str:
c.simulate_shortcut(combo)
deadline = time.time() + timeout
current = _selected_workspace(c)
while time.time() < deadline:
current = _selected_workspace(c)
if current == expected_ws:
return current
time.sleep(0.1)
return current

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

A transient "no selection" moment during _press_and_wait crashes the test instead of retrying.

_selected_workspace raises cmuxError when no workspace is currently marked selected. _press_and_wait's poll loop calls it every iteration (line 44) while a shortcut-driven workspace switch is in flight. If the app reports no selection for even one poll (a plausible transient state during a switch), the raised exception propagates out of the loop and fails the test immediately, instead of letting the deadline-bounded retry handle it.

🔧 Proposed fix
 def _press_and_wait(c: cmux, combo: str, expected_ws: str, timeout: float = 5.0) -> str:
     c.simulate_shortcut(combo)
     deadline = time.time() + timeout
-    current = _selected_workspace(c)
+    current = ""
     while time.time() < deadline:
-        current = _selected_workspace(c)
+        try:
+            current = _selected_workspace(c)
+        except cmuxError:
+            time.sleep(0.05)
+            continue
         if current == expected_ws:
             return current
         time.sleep(0.1)
     return current
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _selected_workspace(c: cmux) -> str:
for _idx, wsid, _title, selected in c.list_workspaces():
if selected:
return wsid
raise cmuxError("no selected workspace")
def _press_and_wait(c: cmux, combo: str, expected_ws: str, timeout: float = 5.0) -> str:
c.simulate_shortcut(combo)
deadline = time.time() + timeout
current = _selected_workspace(c)
while time.time() < deadline:
current = _selected_workspace(c)
if current == expected_ws:
return current
time.sleep(0.1)
return current
def _selected_workspace(c: cmux) -> str:
for _idx, wsid, _title, selected in c.list_workspaces():
if selected:
return wsid
raise cmuxError("no selected workspace")
def _press_and_wait(c: cmux, combo: str, expected_ws: str, timeout: float = 5.0) -> str:
c.simulate_shortcut(combo)
deadline = time.time() + timeout
current = ""
while time.time() < deadline:
try:
current = _selected_workspace(c)
except cmuxError:
time.sleep(0.05)
continue
if current == expected_ws:
return current
time.sleep(0.1)
return current
🧰 Tools
🪛 Ruff (0.16.0)

[warning] 36-36: Avoid specifying long messages outside the exception class

(TRY003)

🤖 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 `@tests_v2/test_focus_history_shortcut_cross_workspace.py` around lines 32 -
48, Update _press_and_wait to tolerate transient cmuxError exceptions from
_selected_workspace during shortcut polling: catch the error inside the
deadline-bounded loop, retain the previous/current workspace value when no
workspace is selected, and continue retrying until expected_ws is observed or
the timeout expires. Preserve the existing final return behavior.

Comment on lines +55 to +56
c.activate_app()
time.sleep(0.3)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Replace fixed sleeps with polling for readiness, matching _press_and_wait's pattern.

Lines 56, 64, 69, and 86 use fixed time.sleep() calls to let activate_app, select_workspace, and close_workspace take effect, instead of polling for the real state change. _press_and_wait (lines 39-48) already shows the correct pattern: poll a real predicate on a deadline. Apply the same approach here: poll c.ping() or workspace state after activate_app, poll _selected_workspace(c) == wsid after each select_workspace, and poll list_workspaces() for the workspace's removal after close_workspace.

Based on path instructions: "Do not use fixed sleeps, measured wall-clock assertions, or hard absolute latency ceilings in correctness tests" and "await real completion signals or deadline-bounded polls of real predicates rather than fixed-duration waits."

Also applies to: 59-70, 85-86

🤖 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 `@tests_v2/test_focus_history_shortcut_cross_workspace.py` around lines 55 -
56, Replace the fixed time.sleep calls in the focus-history test with
deadline-bounded polling of real state, following _press_and_wait. After
activate_app, poll c.ping(); after each select_workspace, poll until
_selected_workspace(c) equals the target workspace; and after close_workspace,
poll list_workspaces() until the workspace is absent. Preserve the existing test
sequence and assertions.

Source: Path instructions

Comment on lines +55 to +86
c.activate_app()
time.sleep(0.3)

created = []
for index in range(3):
wsid = c.new_workspace()
c.select_workspace(wsid)
c.rename_workspace(f"fhist-ws{index + 1}", wsid)
created.append(wsid)
time.sleep(0.15)

# Visit ws1 -> ws2 -> ws3 so the focus-history stack is deterministic.
for wsid in created:
c.select_workspace(wsid)
time.sleep(0.15)
_must(_selected_workspace(c) == created[2], "expected focus on ws3 before navigating")

# Back across workspaces: ws3 -> ws2 -> ws1.
got = _press_and_wait(c, "cmd+[", created[1])
_must(got == created[1], f"cmd+[ should land on ws2, got {got}")
got = _press_and_wait(c, "cmd+[", created[0])
_must(got == created[0], f"second cmd+[ should land on ws1, got {got}")

# Forward again: ws1 -> ws2 -> ws3.
got = _press_and_wait(c, "cmd+]", created[1])
_must(got == created[1], f"cmd+] should land on ws2, got {got}")
got = _press_and_wait(c, "cmd+]", created[2])
_must(got == created[2], f"second cmd+] should land on ws3, got {got}")

# Closed workspaces are skipped, matching the arrow buttons' pruning.
c.close_workspace(created[1])
time.sleep(0.3)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Fixed wall-clock sleeps used as synchronization in both regression suites. Both files use a fixed sleep to wait for a workspace-selection or app-state change to land, instead of polling the real state. The shared root cause is a "settle" delay standing in for a real completion signal, which the path instructions for these test directories explicitly forbid.

  • tests_v2/test_focus_history_shortcut_cross_workspace.py#L55-L86: replace time.sleep(0.3) after activate_app (line 56), time.sleep(0.15) in the creation and visit loops (lines 64, 69), and time.sleep(0.3) after close_workspace (line 86) with deadline-bounded polls of _selected_workspace(c) or list_workspaces(), reusing the pattern already in _press_and_wait.
  • cmuxUITests/FocusHistoryShortcutUITests.swift#L115-L123: replace RunLoop.current.run(until: Date().addingTimeInterval(0.15)) (line 122) with a call to the file's own waitForCurrentWorkspace(_:timeout:).

Based on path instructions: "Tests must await real completion signals or deadline-bounded polls of real predicates rather than fixed-duration waits before assertions" and "Do not use fixed sleeps, measured wall-clock assertions, or hard absolute latency ceilings in correctness tests."

📍 Affects 2 files
  • tests_v2/test_focus_history_shortcut_cross_workspace.py#L55-L86 (this comment)
  • cmuxUITests/FocusHistoryShortcutUITests.swift#L115-L123
🤖 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 `@tests_v2/test_focus_history_shortcut_cross_workspace.py` around lines 55 -
86, Replace fixed sleeps in
tests_v2/test_focus_history_shortcut_cross_workspace.py lines 55-86 after
activate_app, workspace creation/selection, and close_workspace with
deadline-bounded polls of _selected_workspace(c) or list_workspaces(), following
_press_and_wait; update cmuxUITests/FocusHistoryShortcutUITests.swift lines
115-123 to use waitForCurrentWorkspace(_:timeout:) instead of
RunLoop.current.run. Ensure all synchronization waits on real completion
predicates.

Source: Path instructions

Comment on lines +315 to +332
{
id: "focusPreviousPane",
combos: [],
description: { en: "Focus previous pane (cycle)", ja: "前のペインにフォーカス(循環)" },
note: {
en: "unbound by default; a Ghostty goto_split:previous keybind also cycles panes while Focus Back does not claim the same keys",
ja: "デフォルトでは未割り当て。Ghostty の goto_split:previous のキーバインドでも循環できます(Focus Back が同じキーを使っていない場合)",
},
},
{
id: "focusNextPane",
combos: [],
description: { en: "Focus next pane (cycle)", ja: "次のペインにフォーカス(循環)" },
note: {
en: "unbound by default; a Ghostty goto_split:next keybind also cycles panes while Focus Forward does not claim the same keys",
ja: "デフォルトでは未割り当て。Ghostty の goto_split:next のキーバインドでも循環できます(Focus Forward が同じキーを使っていない場合)",
},
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' "Configured web locales:"
rg -n -C 3 'locales|defaultLocale' web/i18n/routing.ts || true

printf '%s\n' "New shortcut metadata:"
rg -n -C 5 'focusPreviousPane|focusNextPane' web/data/cmux-shortcuts.ts web/messages || true

Repository: manaflow-ai/cmux

Length of output: 2732


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' "web/i18n/routing.ts:"
sed -n '1,120p' web/i18n/routing.ts

printf '%s\n' "Shortcut locale source/type validation:"
sed -n '1,80p' web/data/cmux-shortcuts.ts
printf '%s\n' rg -n 'description|note|supportedLocales|locales|type .*cmux|Shortcut|record' web -g '*.ts' -g '*.tsx' | head -n 220

printf '%s\n' "Message files:"
git ls-files web/messages | sed -n '1,120p'

Repository: manaflow-ai/cmux

Length of output: 4705


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' "Shortcut metadata object shapes in web/data/cmux-shortcuts.ts (last 120 lines):"
sed -n '240,350p' web/data/cmux-shortcuts.ts

printf '%s\n' "All non-en/ja keys present in LocalizedText fields near new shortcuts:"
python3 - <<'PY'
import json, re
from pathlib import Path
src = Path("web/data/cmux-shortcuts.ts").read_text()
for name in ("focusPreviousPane","focusNextPane"):
    idx = src.index(f"id: \"{name}\"")
    slice_ = src[idx:src.index("      },", idx) + 5]
    hits = re.findall(r'\b(?:description|note):\s*\{([^}]+)\}', slice_, re.S)
    print(name)
    for key in hits:
        keys = sorted(re.findall(r'\b([A-z][A-z0-9-]*)\s*:', key))
        print("  ", " ".join(keys))
PY

Repository: manaflow-ai/cmux

Length of output: 8919


Add full locale coverage for the new shortcut text.

web/i18n/routing.ts supports additional locales beyond en and ja, including zh-CN, zh-TW, ko, de, es, fr, it, da, pl, ru, bs, ar, no, pt-BR, th, tr, km, and uk. Add description and note entries for these supported locales for focusPreviousPane and focusNextPane.

🤖 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-shortcuts.ts` around lines 315 - 332, Add complete locale
coverage to the focusPreviousPane and focusNextPane entries in the shortcut
definitions, adding description and note translations for zh-CN, zh-TW, ko, de,
es, fr, it, da, pl, ru, bs, ar, no, pt-BR, th, tr, km, and uk alongside en and
ja. Keep each locale’s existing translation structure and ensure both shortcuts
receive matching fields.

Source: Path instructions

azooz2003-bit and others added 2 commits July 31, 2026 03:54
…cus history

Ghostty's macOS defaults bind goto_split:previous/next to cmd+[ / cmd+], the
same keys as Focus Back/Forward. The shortcut dispatch mirrors those triggers
to cycle pane focus and checks the mirror before the focus-history branch, so
the keys cycle panes inside the current workspace (or do nothing) while the
titlebar arrow buttons navigate across workspaces.

Coverage added ahead of the fix so CI shows red then green:
- cmuxTests/FocusHistoryBracketShortcutRoutingTests: dispatches real ⌘[ / ⌘]
  events through debugHandleCustomShortcut with the Ghostty mirror installed
  via a new DEBUG seam; expects workspace focus-history navigation.
- cmuxUITests/FocusHistoryShortcutUITests: end-to-end over the control socket
  (simulate_shortcut uses the same matcher as the app-level monitor); walks
  back/forward across three workspaces and checks closed-workspace skipping.
- tests_v2/test_focus_history_shortcut_cross_workspace.py: local socket
  verification against a tagged build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ecomes rebindable

The Ghostty goto_split:previous/next mirror in the shortcut dispatch now
yields to a bound Focus Back/Forward shortcut (matchConfiguredShortcut,
including shortcuts.when gating), so ⌘[ / ⌘] reach the focus-history branch
and drive the exact same TabManager.navigateBack()/navigateForward() path as
the titlebar arrow buttons: same history model, same closed-workspace
pruning, same enable conditions. Unbinding Focus Back/Forward hands the keys
back to the mirror, as the keyboard-shortcuts docs already promised.

Pane cycling stays available two ways: the Ghostty goto_split trigger on any
non-colliding key, and new cmux-owned rebindable actions focusPreviousPane /
focusNextPane (default unbound) that share the same cyclePaneFocus body, per
the shared-entrypoint policy. The window key-equivalent fallback route gets
the same yield so both dispatch layers agree.

The new actions follow the full shortcut policy: KeyboardShortcutSettings +
CmuxSettings ShortcutAction (defaults, display names, panes group), Settings
recorder rows, cmux.json shortcuts.bindings support, schema enum, web
keyboard-shortcuts page (en+ja), and the shortcut-actions reference. Labels
localized in Localizable.xcstrings for all catalog languages.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@azooz2003-bit
azooz2003-bit force-pushed the feat-cmd-bracket-workspace-history branch from da9a0fe to e73937d Compare July 31, 2026 10:58
@cursor

cursor Bot commented Jul 31, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@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 `@Resources/Localizable.xcstrings`:
- Around line 202980-203098: Add reviewed Khmer (km) stringUnit translations to
both shortcut.focusNextPane.label and shortcut.focusPreviousPane.label entries
in Resources/Localizable.xcstrings at 202980-203098 and 203575-203693,
respectively, ensuring both new shortcut labels cover every supported locale.
🪄 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 Plus

Run ID: e72e84d0-0b12-40ad-a1c0-ec3a69add6f5

📥 Commits

Reviewing files that changed from the base of the PR and between da9a0fe and e73937d.

📒 Files selected for processing (14)
  • Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction+Defaults.swift
  • Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction+DisplayName.swift
  • Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction+Group.swift
  • Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction.swift
  • Resources/Localizable.xcstrings
  • Sources/AppDelegate.swift
  • Sources/KeyboardShortcutSettings.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/FocusHistoryBracketShortcutRoutingTests.swift
  • cmuxUITests/FocusHistoryShortcutUITests.swift
  • skills/cmux-settings/references/shortcut-actions.md
  • tests_v2/test_focus_history_shortcut_cross_workspace.py
  • web/data/cmux-shortcuts.ts
  • web/data/cmux.schema.json

Comment on lines +202980 to +203098
"shortcut.focusNextPane.label": {
"extractionState": "manual",
"localizations": {
"ar": {
"stringUnit": {
"state": "translated",
"value": "التركيز على اللوحة التالية"
}
},
"bs": {
"stringUnit": {
"state": "translated",
"value": "Fokusiraj sljedeći panel"
}
},
"da": {
"stringUnit": {
"state": "translated",
"value": "Fokuser næste panel"
}
},
"de": {
"stringUnit": {
"state": "translated",
"value": "Nächsten Bereich fokussieren"
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Focus Next Pane"
}
},
"es": {
"stringUnit": {
"state": "translated",
"value": "Enfocar panel siguiente"
}
},
"fr": {
"stringUnit": {
"state": "translated",
"value": "Activer le panneau suivant"
}
},
"it": {
"stringUnit": {
"state": "translated",
"value": "Sposta focus pannello successivo"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "次のペインにフォーカス"
}
},
"ko": {
"stringUnit": {
"state": "translated",
"value": "다음 패널로 포커스"
}
},
"nb": {
"stringUnit": {
"state": "translated",
"value": "Fokuser neste panel"
}
},
"pl": {
"stringUnit": {
"state": "translated",
"value": "Fokus na następny panel"
}
},
"pt-BR": {
"stringUnit": {
"state": "translated",
"value": "Focar Próximo Painel"
}
},
"ru": {
"stringUnit": {
"state": "translated",
"value": "Фокус на следующую панель"
}
},
"th": {
"stringUnit": {
"state": "translated",
"value": "โฟกัสบานหน้าต่างถัดไป"
}
},
"tr": {
"stringUnit": {
"state": "translated",
"value": "Sonraki Bölmeye Odaklan"
}
},
"uk": {
"stringUnit": {
"state": "translated",
"value": "Фокус на наступну панель"
}
},
"zh-Hans": {
"stringUnit": {
"state": "translated",
"value": "聚焦下一个面板"
}
},
"zh-Hant": {
"stringUnit": {
"state": "translated",
"value": "聚焦下一個面板"
}
}
}
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add Khmer translations for both new shortcut labels.

Both entries omit the supported km locale. Add a reviewed Khmer translation to each entry.

  • Resources/Localizable.xcstrings#L202980-L203098: Add localizations.km.stringUnit for shortcut.focusNextPane.label.
  • Resources/Localizable.xcstrings#L203575-L203693: Add localizations.km.stringUnit for shortcut.focusPreviousPane.label.

Based on learnings and path instructions, new catalog keys must cover every locale supported by Resources/Localizable.xcstrings, including km.

📍 Affects 1 file
  • Resources/Localizable.xcstrings#L202980-L203098 (this comment)
  • Resources/Localizable.xcstrings#L203575-L203693
🤖 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/Localizable.xcstrings` around lines 202980 - 203098, Add reviewed
Khmer (km) stringUnit translations to both shortcut.focusNextPane.label and
shortcut.focusPreviousPane.label entries in Resources/Localizable.xcstrings at
202980-203098 and 203575-203693, respectively, ensuring both new shortcut labels
cover every supported locale.

Sources: Path instructions, Learnings

@azooz2003-bit

Copy link
Copy Markdown
Collaborator Author

Superseded by #9329 (same change rebased onto post-#9296 main, merged as 03b33ab). This branch's history predates the rebase.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant