Skip to content

fix: goto_split:previous/next cycle through all panes with wrapping - #2639

Merged
austinywang merged 13 commits into
manaflow-ai:mainfrom
mykmelez:fix/goto-split-cycle-navigation
Jul 28, 2026
Merged

austinywang merged 13 commits into
manaflow-ai:mainfrom
mykmelez:fix/goto-split-cycle-navigation

Conversation

@mykmelez

@mykmelez mykmelez commented Apr 6, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • goto_split:previous and goto_split:next now cycle through all panes in tree order regardless of split direction (horizontal/vertical) and wrap at the ends
  • Previously these were mapped to directional left/right navigation via Bonsplit, which skipped vertically-split panes entirely (the comment in GhosttyTerminalView.swift acknowledged this: "Bonsplit doesn't have cycle-based navigation")
  • The fix uses Bonsplit's existing allPaneIds and focusPane APIs to implement proper cycle traversal without requiring Bonsplit changes

Test plan

  • UI tests: GotoSplitCycleUITests — creates 3-pane layout (horizontal + vertical splits), verifies Cmd+] and Cmd+[ visit all panes and wrap
  • Manual testing: verified both goto_split:next and goto_split:previous cycle through all panes in mixed split layouts
  • Regression: existing BrowserPaneNavigationKeybindUITests unaffected (directional navigation unchanged)

Note: per regression test commit policy, the test commit is first (expected to fail) and the fix commit is second.

🤖 Generated with Claude Code


Summary by cubic

Fixes goto_split:previous/next to cycle through all panes in tree order with wrap-around, and routes these shortcuts to the terminal so Ghostty keybinds work reliably. Directional up/down/left/right stays the same; multi-window routing targets the correct TabManager.

  • Bug Fixes
    • Implement cycle navigation via Bonsplit allPaneIds/focusPane; add Workspace.cycleFocus and TabManager.cycleSplitFocus/cyclePaneFocus; remove previous/next from focusDirection.
    • Resolve TabManager by tabId in the action handler so previous/next operate in the correct window; record cycle state from the routed workspace.
    • Route goto_split:previous/next key equivalents to the terminal view before the main menu; load shortcuts from Ghostty config.
    • Add GotoSplitCycleUITests with a 3‑pane terminal layout; wait for terminal focus and avoid duplicate setupComplete writes.

Written for commit 235ead8. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes

    • Split-pane cycling now deterministically traverses all panes in tree order and wraps at ends.
  • New Features

    • Added next/previous cycle navigation to move focus sequentially across panes (keyboard cycle support).
  • Tests

    • New UI tests exercise a three-pane terminal layout to verify per-keystroke focus changes, full-cycle coverage, and wrapping; test harness launches the app with a test layout and validates focus snapshots.

mykmelez and others added 2 commits April 6, 2026 11:44
Add tests verifying that goto_split:previous and goto_split:next cycle
through all panes regardless of split direction (horizontal and vertical)
and wrap at the ends. Uses Ghostty's default keybinds (Cmd+]/[).

Extends the goto_split test infrastructure with a three_pane_terminal
layout mode (CMUX_UI_TEST_GOTO_SPLIT_LAYOUT=three_pane_terminal) and
a cycle navigation recorder for test observability.

These tests are expected to FAIL without the accompanying fix, because
goto_split:previous/next currently map to directional left/right
navigation which skips vertically-split panes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previously, goto_split:previous and goto_split:next were mapped to
directional left/right navigation in Bonsplit, which only found spatially
adjacent panes and skipped vertically-split panes entirely.

This adds cycle-based navigation that traverses all panes in tree order
(using Bonsplit's allPaneIds) and wraps around at the ends, matching
Ghostty's intended behavior for these actions.

Changes:
- Workspace.cycleFocus(forward:) traverses allPaneIds with wrapping
- TabManager.cycleSplitFocus delegates to Workspace.cycleFocus
- GhosttyTerminalView.handleAction routes PREVIOUS/NEXT through cycle
  navigation instead of mapping to directional .left/.right
- focusDirection() no longer handles PREVIOUS/NEXT cases

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Apr 6, 2026

Copy link
Copy Markdown

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

A member of the Team first needs to authorize it.

@cubic-dev-ai

cubic-dev-ai Bot commented Apr 6, 2026

Copy link
Copy Markdown

This review could not be run because your cubic account has exceeded the monthly review limit. If you need help restoring access, please contact contact@cubic.dev.

@coderabbitai

coderabbitai Bot commented Apr 6, 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 deterministic cycle-based pane-focus navigation routed via tab-specific managers, implements Workspace/TabManager cycle APIs and test-handshake recording, and introduces a three-pane UI test suite that validates next/previous cycling and wraparound.

Changes

Cohort / File(s) Summary
Project
GhosttyTabs.xcodeproj/project.pbxproj
Added GotoSplitCycleUITests.swift to the cmuxUITests target (PBXFileReference & PBXBuildFile).
UI Tests
cmuxUITests/GotoSplitCycleUITests.swift
New UI test suite that launches a three-pane terminal layout, uses Ghostty keybinds to exercise goto_split:next/previous, verifies each cycle visits all pane IDs and wraps, and implements JSON handshake, setup/teardown helpers, and polling utilities.
App Startup / Test Helpers
Sources/AppDelegate.swift
Added setupThreePaneTerminalLayout(tabManager:) to compose a right-then-down three-pane terminal layout, polling/handshake writes (setupComplete/setupError), and recordGotoSplitCycleMoveIfNeeded(tabId:forward:) to record cycle moves for tests.
Action Handling
Sources/GhosttyTerminalView.swift
Refined GOTO_SPLIT handling: cache goto_split direction, handle NEXT/PREVIOUS by resolving a tab-specific TabManager and calling cycleSplitFocus(tabId:forward:) (and record in DEBUG); other directions route via spatial focusDirection(from:).
Focus Logic
Sources/TabManager.swift, Sources/Workspace.swift
Added TabManager.cycleSplitFocus(tabId:forward:) and Workspace.cycleFocus(forward:) to move focus across bonsplit panes in tree order with wraparound, unfocus previous pane, focus target pane, and reconcile tab selection.

Sequence Diagram

sequenceDiagram
    actor User
    participant GhosttyTerminalView as "GhosttyTerminalView"
    participant TabManager as "TabManager"
    participant Workspace as "Workspace"
    participant Bonsplit as "bonsplitController"
    participant AppDelegate as "AppDelegate"

    User->>GhosttyTerminalView: GOTO_SPLIT_NEXT / PREVIOUS
    GhosttyTerminalView->>TabManager: cycleSplitFocus(tabId, forward)
    TabManager->>Workspace: cycleFocus(forward)
    Workspace->>Bonsplit: unfocus(currentPane)
    Workspace->>Bonsplit: focusPane(targetPane)
    Workspace->>Workspace: applyTabSelection(tabId:inPane:)
    Workspace-->>TabManager: return success
    TabManager-->>GhosttyTerminalView: return Bool
    GhosttyTerminalView->>AppDelegate: recordGotoSplitCycleMoveIfNeeded(tabId, forward) (DEBUG)
    AppDelegate->>AppDelegate: write snapshot JSON for UI tests
    GhosttyTerminalView-->>User: focus updated
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped across three panes, left then right,
keystrokes twirled beneath the terminal light,
tree-order turns, focus skips and wraps,
JSON hums our tiny test-time maps,
🥕 A cheerful cycle — goodnight, good byte.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 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.
Title check ✅ Passed The title clearly states the key behavior change: goto_split previous/next now cycles through panes with wrapping.
Description check ✅ Passed The description covers the summary and testing well, with only non-critical template sections like demo video, review trigger, and checklist missing.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (5)
Sources/Workspace.swift (1)

10098-10114: Use tree traversal instead of allPaneIds for the cycle order.

This method promises tree-order traversal, but it currently builds the sequence from bonsplitController.allPaneIds. The same file already uses SidebarBranchOrdering.orderedPaneIds(tree:) when pane order matters. If allPaneIds ever diverges from tree order after split/move churn, goto_split:previous/next will walk panes in the wrong sequence. Please either derive the list from treeSnapshot() here or verify that Bonsplit guarantees allPaneIds is tree-ordered.

♻️ Suggested change
-        let allPaneIds = bonsplitController.allPaneIds
+        let orderedPaneIdStrings = SidebarBranchOrdering.orderedPaneIds(
+            tree: bonsplitController.treeSnapshot()
+        )
+        let panesById = Dictionary(
+            uniqueKeysWithValues: bonsplitController.allPaneIds.map { ($0.id.uuidString, $0) }
+        )
+        let allPaneIds = orderedPaneIdStrings.compactMap { panesById[$0] }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Workspace.swift` around lines 10098 - 10114, The current navigation
uses bonsplitController.allPaneIds which may not reflect tree-order; change the
sequence source to a tree-ordered list by calling treeSnapshot() and passing it
to SidebarBranchOrdering.orderedPaneIds(tree:) (or otherwise derive ordered pane
ids from the tree) instead of allPaneIds; keep the rest of the logic (unfocusing
via focusedPanelId/panels and computing targetIndex) identical but replace
references to allPaneIds with the ordered list so goto_split previous/next walks
panes in true tree order.
cmuxUITests/GotoSplitCycleUITests.swift (4)

12-17: Add tearDown to remove the test data file.

The test creates a JSON file at dataPath but never cleans it up. Adding a tearDown method ensures test artifacts don't accumulate on disk, especially during repeated local test runs.

♻️ Proposed fix
     override func setUp() {
         super.setUp()
         continueAfterFailure = false
         dataPath = "/tmp/cmux-ui-test-goto-split-cycle-\(UUID().uuidString).json"
         try? FileManager.default.removeItem(atPath: dataPath)
     }
+
+    override func tearDown() {
+        try? FileManager.default.removeItem(atPath: dataPath)
+        super.tearDown()
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmuxUITests/GotoSplitCycleUITests.swift` around lines 12 - 17, Add a tearDown
method to remove the temporary JSON created in setUp: implement override func
tearDown() { try? FileManager.default.removeItem(atPath: dataPath);
super.tearDown() } so the file referenced by dataPath is deleted after each
test; ensure you call super.tearDown() and use the same FileManager removal
logic as in setUp to avoid leftover artifacts.

37-41: Guard against missing dictionary keys to avoid test crashes.

The waitForData call confirms keys exist, but loadData() is called again afterward. If the file changes between calls or parsing differs, these force unwraps will crash without a useful diagnostic. Using guard let provides clearer failure messages.

♻️ Proposed fix
-        let allPaneIds = Set(setup["allPaneIds"]!.split(separator: ",").map(String.init))
+        guard let allPaneIdsRaw = setup["allPaneIds"] else {
+            XCTFail("Missing allPaneIds in setup data")
+            return
+        }
+        let allPaneIds = Set(allPaneIdsRaw.split(separator: ",").map(String.init))
         XCTAssertEqual(allPaneIds.count, 3, "Expected 3 distinct pane IDs")

-        let startPane = setup["focusedPaneId"]!
+        guard let startPane = setup["focusedPaneId"] else {
+            XCTFail("Missing focusedPaneId in setup data")
+            return
+        }
         XCTAssertTrue(allPaneIds.contains(startPane), "Start pane should be in allPaneIds")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmuxUITests/GotoSplitCycleUITests.swift` around lines 37 - 41, Replace the
force-unwrapped dictionary accesses after loadData() with safe guards: instead
of using setup["allPaneIds"]! and setup["focusedPaneId"]! directly (used when
computing allPaneIds and startPane), add guard let statements to unwrap
setup["allPaneIds"] and setup["focusedPaneId"] (and ensure the split/map result
exists) and call XCTFail with a clear diagnostic and return if missing; this
change should be made in the test function that calls loadData()/waitForData()
so the failure is descriptive rather than crashing (reference symbols:
loadData(), waitForData(), allPaneIds, startPane).

85-88: Same force unwrap issue as the next test.

Apply the same guard-let pattern here for consistency and better failure diagnostics.

♻️ Proposed fix
-        let allPaneIds = Set(setup["allPaneIds"]!.split(separator: ",").map(String.init))
+        guard let allPaneIdsRaw = setup["allPaneIds"] else {
+            XCTFail("Missing allPaneIds in setup data")
+            return
+        }
+        let allPaneIds = Set(allPaneIdsRaw.split(separator: ",").map(String.init))
         XCTAssertEqual(allPaneIds.count, 3, "Expected 3 distinct pane IDs")

-        let startPane = setup["focusedPaneId"]!
+        guard let startPane = setup["focusedPaneId"] else {
+            XCTFail("Missing focusedPaneId in setup data")
+            return
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmuxUITests/GotoSplitCycleUITests.swift` around lines 85 - 88, The test
currently force-unwraps values from setup (allPaneIds and focusedPaneId); change
to a guard-let pattern to safely unwrap setup["allPaneIds"] and
setup["focusedPaneId"], call XCTFail with a clear message and return if either
is missing, then proceed to build allPaneIds (split/map) and assert count and
use startPane; update references to use the unwrapped variables (e.g., use the
local allPaneIdsString and startPane variables instead of force-unwrapped setup
accesses).

69-112: Consider extracting shared test logic into a parameterized helper.

Both tests share ~90% identical code, differing only in the key ("]" vs "[") and direction name in assertions. A helper like verifyCycleNavigation(key:directionName:) would reduce duplication and make adding edge-case tests easier.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmuxUITests/GotoSplitCycleUITests.swift` around lines 69 - 112, The two
nearly identical tests should be consolidated by extracting the shared logic
into a parameterized helper (e.g., verifyCycleNavigation(key: String,
directionName: String)); move the setup/teardown, launchWithThreePaneLayout()
call, the waitForData checks, loadData usage, the loop that types
app.typeKey(...), waitForDataMatch, and the final assertions into that helper,
and then replace each testGotoSplitPreviousCyclesAllPanes and its counterpart
with a single one-line call to verifyCycleNavigation(key: "[", directionName:
"previous") (and the other test calling with key: "]" and directionName:
"next"); ensure helper references functions/locals like
launchWithThreePaneLayout(), waitForData(...), waitForDataMatch(...),
loadData(), and asserted variables such as allPaneIds, focusedPaneId and visited
so the behavior and assertion text remain the same.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmuxUITests/GotoSplitCycleUITests.swift`:
- Around line 193-203: The helper launchAndEnsureForeground currently returns
success when app.state == .runningBackground, which contradicts its name and can
cause flaky input failures; update it to either (A) rename the function to
launchAndEnsureRunning (or similar) if background is an acceptable end state, or
(B) ensure the app is actually foreground by calling app.activate() (or another
activation attempt) when app.state == .runningBackground and re-checking
app.state before returning, keeping the existing XCTExpectFailure wrapper around
app.launch() and preserving the failure path that calls XCTFail with state info.

In `@Sources/GhosttyTerminalView.swift`:
- Around line 2839-2841: The code uses AppDelegate.shared?.tabManager which
biases to the active window; instead resolve the TabManager that actually owns
the given tabId before calling cycleSplitFocus(tabId:forward:). Replace the
AppDelegate.shared?.tabManager lookup with the cross-window resolver used
elsewhere (e.g. the same lookup pattern as in Sources/TerminalController.swift —
a TabManager lookup by tabId like tabManagerForTab(tabId:) or similar), then
call the found TabManager.cycleSplitFocus(tabId: forward:); if no owning manager
is found, fall back gracefully (no-op or the existing behavior).

---

Nitpick comments:
In `@cmuxUITests/GotoSplitCycleUITests.swift`:
- Around line 12-17: Add a tearDown method to remove the temporary JSON created
in setUp: implement override func tearDown() { try?
FileManager.default.removeItem(atPath: dataPath); super.tearDown() } so the file
referenced by dataPath is deleted after each test; ensure you call
super.tearDown() and use the same FileManager removal logic as in setUp to avoid
leftover artifacts.
- Around line 37-41: Replace the force-unwrapped dictionary accesses after
loadData() with safe guards: instead of using setup["allPaneIds"]! and
setup["focusedPaneId"]! directly (used when computing allPaneIds and startPane),
add guard let statements to unwrap setup["allPaneIds"] and
setup["focusedPaneId"] (and ensure the split/map result exists) and call XCTFail
with a clear diagnostic and return if missing; this change should be made in the
test function that calls loadData()/waitForData() so the failure is descriptive
rather than crashing (reference symbols: loadData(), waitForData(), allPaneIds,
startPane).
- Around line 85-88: The test currently force-unwraps values from setup
(allPaneIds and focusedPaneId); change to a guard-let pattern to safely unwrap
setup["allPaneIds"] and setup["focusedPaneId"], call XCTFail with a clear
message and return if either is missing, then proceed to build allPaneIds
(split/map) and assert count and use startPane; update references to use the
unwrapped variables (e.g., use the local allPaneIdsString and startPane
variables instead of force-unwrapped setup accesses).
- Around line 69-112: The two nearly identical tests should be consolidated by
extracting the shared logic into a parameterized helper (e.g.,
verifyCycleNavigation(key: String, directionName: String)); move the
setup/teardown, launchWithThreePaneLayout() call, the waitForData checks,
loadData usage, the loop that types app.typeKey(...), waitForDataMatch, and the
final assertions into that helper, and then replace each
testGotoSplitPreviousCyclesAllPanes and its counterpart with a single one-line
call to verifyCycleNavigation(key: "[", directionName: "previous") (and the
other test calling with key: "]" and directionName: "next"); ensure helper
references functions/locals like launchWithThreePaneLayout(), waitForData(...),
waitForDataMatch(...), loadData(), and asserted variables such as allPaneIds,
focusedPaneId and visited so the behavior and assertion text remain the same.

In `@Sources/Workspace.swift`:
- Around line 10098-10114: The current navigation uses
bonsplitController.allPaneIds which may not reflect tree-order; change the
sequence source to a tree-ordered list by calling treeSnapshot() and passing it
to SidebarBranchOrdering.orderedPaneIds(tree:) (or otherwise derive ordered pane
ids from the tree) instead of allPaneIds; keep the rest of the logic (unfocusing
via focusedPanelId/panels and computing targetIndex) identical but replace
references to allPaneIds with the ordered list so goto_split previous/next walks
panes in true tree order.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: c787c334-ac1d-44d4-92b4-fa1ea9a4dd44

📥 Commits

Reviewing files that changed from the base of the PR and between 179b16c and cdaa58b.

📒 Files selected for processing (6)
  • GhosttyTabs.xcodeproj/project.pbxproj
  • Sources/AppDelegate.swift
  • Sources/GhosttyTerminalView.swift
  • Sources/TabManager.swift
  • Sources/Workspace.swift
  • cmuxUITests/GotoSplitCycleUITests.swift

Comment thread cmuxUITests/GotoSplitCycleUITests.swift
Comment thread Sources/AppDelegate.swift
Comment thread Sources/GhosttyTerminalView.swift Outdated
@greptile-apps

greptile-apps Bot commented Apr 6, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Replaces the stub goto_split:previous/next → left/right fallback with proper cycle navigation that traverses all panes in Bonsplit tree order and wraps at the ends. A new Workspace.cycleFocus method drives the traversal via allPaneIds/focusPane; AppDelegate gains shortcut loading and NSWindow-level routing to ensure cycle key-equivalents reach the terminal view before the main menu.

  • Core fix (Workspace, TabManager, GhosttyTerminalView): cycleFocus computes the target index with wrapping arithmetic, calls unfocus() on the outgoing panel, then reconciles AppKit first-responder and Bonsplit state via applyTabSelection. The Ghostty action handler dispatches previous/next to cycleSplitFocus(tabId:) with correct multi-window tabManagerFor resolution.
  • Key routing (AppDelegate, NSWindow extension): shouldRouteGhosttyGotoSplitCycleShortcutToTerminal intercepts cycle key-equivalents at the window level before the menu bar, so Cmd+]/Cmd+[ reach GhosttyNSView.performKeyEquivalentAfterMenuMiss when a terminal is focused. The AppDelegate shortcut handler covers the non-terminal path.
  • Test scaffolding (AppDelegate, GotoSplitCycleUITests): setupThreePaneTerminalLayout creates the 3-pane layout required by the new UI tests; an existing review comment covers its asyncAfter polling loop.

Confidence Score: 4/5

The cycle navigation logic is correct and multi-window tab manager resolution is properly handled; the one area needing attention is the asyncAfter polling loop in setupThreePaneTerminalLayout, which ships in the production binary gated only by an env-var check and is captured in the existing review comment.

The production Workspace.cycleFocus, TabManager forwarders, and GhosttyTerminalView action dispatch are all clean. The setupThreePaneTerminalLayout function introduced in AppDelegate.swift contains an asyncAfter polling loop (up to 60 iterations over 6 s) compiled into the production binary; the existing review comment covers this. No other blocking issues were found.

Sources/AppDelegate.swift — specifically the setupThreePaneTerminalLayout function and its DispatchQueue.main.asyncAfter retry loop.

Important Files Changed

Filename Overview
Sources/Workspace.swift Adds cycleFocus(forward:) using bonsplitController.allPaneIds/focusPane with correct wrapping arithmetic; unfocus() + applyTabSelection reconcile AppKit first-responder with bonsplit state.
Sources/TabManager.swift Adds two cycle-focus forwarders: cyclePaneFocus (uses selectedTabId, called from AppDelegate shortcut path) and cycleSplitFocus(tabId:forward:) (explicit workspace lookup, called from terminal action path); both delegate to Workspace.cycleFocus.
Sources/GhosttyTerminalView.swift Removes the incorrect previous/next → left/right fallback from focusDirection; adds a pre-check in the GHOSTTY_ACTION_GOTO_SPLIT handler that routes previous/next to cycleSplitFocus via tabManagerFor(tabId:) for correct multi-window targeting.
Sources/AppDelegate.swift Loads goto_split:previous/next shortcuts from Ghostty config; adds NSWindow-level routing to deliver cycle key equivalents to the terminal before the menu; adds setupThreePaneTerminalLayout for UI-test scaffolding — this function contains an asyncAfter polling loop (flagged in existing review comment) that ships in the production binary gated only by an env-var check.
cmuxUITests/GotoSplitCycleUITests.swift New 268-line UI test creates a 3-pane terminal layout, reads shortcuts from app-written JSON, and verifies per-keystroke focus changes, full-cycle coverage, and wrap-around for both next and previous directions.
cmux.xcodeproj/project.pbxproj Adds GotoSplitCycleUITests.swift to the test target; file references and build-phase entries look correct.

Sequence Diagram

sequenceDiagram
    participant User
    participant NSWindow
    participant AppDelegate
    participant GhosttyNSView
    participant GhosttyTerminalView
    participant TabManager
    participant Workspace

    User->>NSWindow: keyDown (Cmd+] / Cmd+[)
    NSWindow->>AppDelegate: shouldRouteGhosttyGotoSplitCycleShortcutToTerminal(event)
    AppDelegate-->>NSWindow: true (shortcut matches)
    NSWindow->>GhosttyNSView: performKeyEquivalentAfterMenuMiss(event)
    GhosttyNSView->>GhosttyTerminalView: handleAction(GHOSTTY_ACTION_GOTO_SPLIT, previous/next)
    GhosttyTerminalView->>AppDelegate: tabManagerFor(tabId)
    AppDelegate-->>GhosttyTerminalView: TabManager (multi-window resolved)
    GhosttyTerminalView->>TabManager: cycleSplitFocus(tabId:, forward:)
    TabManager->>Workspace: cycleFocus(forward:)
    Workspace->>Workspace: unfocus() current panel
    Workspace->>Workspace: compute targetIndex (wrapping)
    Workspace->>Workspace: bonsplitController.focusPane(targetPaneId)
    Workspace->>Workspace: applyTabSelection(tabId:, inPane:)
    GhosttyNSView-->>NSWindow: true (consumed)

    Note over AppDelegate,TabManager: Non-terminal path (browser pane focused)
    User->>AppDelegate: performShortcut(event)
    AppDelegate->>AppDelegate: matchesGhosttyGotoSplitNext/PreviousShortcut
    AppDelegate->>AppDelegate: cmuxRememberFindSelectionBeforePanelFocusMove
    AppDelegate->>TabManager: cyclePaneFocus(forward:)
    TabManager->>Workspace: cycleFocus(forward:)
Loading

Reviews (3): Last reviewed commit: "Merge remote-tracking branch 'manaflow/m..." | Re-trigger Greptile

mykmelez and others added 2 commits April 6, 2026 19:01
The setupThreePaneTerminalLayout helper was writing setupComplete
immediately after creating splits, before a terminal surface became
first responder. Ghostty keybinds only fire when GhosttyNSView has
focus, so early keystrokes could miss.

Now waits for .ghosttyDidFocusSurface and verifies a terminal panel
is focused before signaling readiness, matching the pattern used by
the existing browser split setup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use tabManagerFor(tabId:) instead of AppDelegate.shared?.tabManager
so that goto_split:previous/next routes to the correct window's
TabManager in multi-window scenarios, rather than biasing toward
the active window.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@mykmelez

mykmelez commented Apr 7, 2026

Copy link
Copy Markdown
Contributor Author

Addressed two of the CodeRabbit review comments in follow-up commits:

  • Wait for terminal focus before signaling setup complete (5a93244): setupThreePaneTerminalLayout now observes .ghosttyDidFocusSurface and verifies a terminal panel is focused before writing setupComplete, matching the existing browser setup pattern.
  • Resolve TabManager by tabId (a3cd333): Uses tabManagerFor(tabId:) with fallback so cycle navigation routes to the correct window's TabManager in multi-window scenarios.

The launchAndEnsureForeground naming comment was not addressed — it's an established pattern copied from BrowserPaneNavigationKeybindUITests.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

8588-8625: ⚠️ Potential issue | 🟠 Major

Still gate readiness on the terminal’s actual keyboard target.

window.firstResponder is NSView is too weak here. The poll can still write setupComplete while some unrelated view owns focus, and a stale scheduled poll can keep running after a successful notification path. That reintroduces the same flake this helper is meant to remove. Resolve the window from the focused terminal panel and use its hosted view’s preferred-focus check, with a resolved guard so pending polls stop once setup succeeds.

Suggested fix
-        var observer: NSObjectProtocol?
+        var observer: NSObjectProtocol?
+        var resolved = false
         let deadline = Date().addingTimeInterval(6.0)
 
         func checkAndSignal() {
+            guard !resolved else { return }
             guard Date() < deadline else {
                 if let observer { NotificationCenter.default.removeObserver(observer) }
+                resolved = true
                 self.writeGotoSplitTestData(["setupError": "Timed out waiting for terminal focus"])
                 return
             }
             guard let focusedPanelId = tab.focusedPanelId,
-                  tab.terminalPanel(for: focusedPanelId) != nil,
-                  let window = NSApp.mainWindow ?? NSApp.keyWindow,
-                  window.firstResponder is NSView else {
+                  let terminalPanel = tab.terminalPanel(for: focusedPanelId),
+                  let window = terminalPanel.hostedView.window,
+                  terminalPanel.hostedView.responderMatchesPreferredKeyboardFocus(window.firstResponder) else {
                 DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { checkAndSignal() }
                 return
             }
 
             if let observer { NotificationCenter.default.removeObserver(observer) }
+            resolved = true
 
             let allPaneIds = tab.bonsplitController.allPaneIds.map(\.description)

Based on learnings: In Sources/AppDelegate.swift, determine whether to repair focus by asking the focused terminal’s hosted view if the current first responder already matches that panel’s preferred keyboard target.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/AppDelegate.swift` around lines 8588 - 8625, The poll currently uses
a weak check (window.firstResponder is NSView) and can report setupComplete
while an unrelated view is focused and leave scheduled polls running; update
checkAndSignal to resolve the focused terminal panel via tab.focusedPanelId and
tab.terminalPanel(for:), obtain that panel’s hosted view / preferred keyboard
target (e.g. the hosted view’s preferredFocus or preferredKeyboardTarget API),
and only consider readiness true when window.firstResponder matches that
specific preferred target; when readiness succeeds remove the observer and set a
local resolved flag so any pending DispatchQueue.asyncAfter callbacks return
early and do not re-send setupComplete or keep polling. Ensure observer removal
happens in both success and timeout paths and reference checkAndSignal,
observer, tab.focusedPanelId, tab.terminalPanel(for:), and
bonsplitController.focusedPaneId in your changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@Sources/AppDelegate.swift`:
- Around line 8588-8625: The poll currently uses a weak check
(window.firstResponder is NSView) and can report setupComplete while an
unrelated view is focused and leave scheduled polls running; update
checkAndSignal to resolve the focused terminal panel via tab.focusedPanelId and
tab.terminalPanel(for:), obtain that panel’s hosted view / preferred keyboard
target (e.g. the hosted view’s preferredFocus or preferredKeyboardTarget API),
and only consider readiness true when window.firstResponder matches that
specific preferred target; when readiness succeeds remove the observer and set a
local resolved flag so any pending DispatchQueue.asyncAfter callbacks return
early and do not re-send setupComplete or keep polling. Ensure observer removal
happens in both success and timeout paths and reference checkAndSignal,
observer, tab.focusedPanelId, tab.terminalPanel(for:), and
bonsplitController.focusedPaneId in your changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 08d7ce14-6eb5-4093-b58d-d662d203bac4

📥 Commits

Reviewing files that changed from the base of the PR and between cdaa58b and a3cd333.

📒 Files selected for processing (2)
  • Sources/AppDelegate.swift
  • Sources/GhosttyTerminalView.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • Sources/GhosttyTerminalView.swift

mykmelez and others added 2 commits April 12, 2026 19:21
The checkAndSignal poll and .ghosttyDidFocusSurface observer could
both fire and write setupComplete twice. Add a resolved flag so the
first successful path short-circuits subsequent invocations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@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

♻️ Duplicate comments (2)
Sources/AppDelegate.swift (1)

8655-8668: ⚠️ Potential issue | 🟠 Major

Still gate setupComplete on the target terminal actually owning first responder.

window.firstResponder is NSView is too weak here. Any view in the key/main window satisfies it, so this can still write setupComplete before the selected TerminalPanel owns keyboard focus, which puts Cmd+] / Cmd+[ back into the same flaky race with AppKit.

Proposed fix
         func checkAndSignal() {
             guard !resolved else { return }
             guard Date() < deadline else {
                 if let observer { NotificationCenter.default.removeObserver(observer) }
                 resolved = true
                 self.writeGotoSplitTestData(["setupError": "Timed out waiting for terminal focus"])
                 return
             }
-            guard let focusedPanelId = tab.focusedPanelId,
-                  tab.terminalPanel(for: focusedPanelId) != nil,
-                  let window = NSApp.mainWindow ?? NSApp.keyWindow,
-                  window.firstResponder is NSView else {
+            guard let focusedPanelId = tab.focusedPanelId,
+                  let terminalPanel = tab.terminalPanel(for: focusedPanelId),
+                  let window = terminalPanel.hostedView.window,
+                  let firstResponder = window.firstResponder,
+                  terminalPanel.hostedView.responderMatchesPreferredKeyboardFocus(firstResponder) else {
                 DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { checkAndSignal() }
                 return
             }

Based on learnings: In AppDelegate.repairFocusedTerminalKeyboardRoutingIfNeeded(window:event:), determine whether to repair focus by asking the focused terminal’s hosted view if the current first responder already matches that panel’s preferred keyboard target.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/AppDelegate.swift` around lines 8655 - 8668, The current
first-responder check in checkAndSignal() is too permissive; replace the
window.firstResponder is NSView guard with a check that the focused
TerminalPanel actually owns the keyboard focus — e.g. get the panel via
tab.terminalPanel(for: focusedPanelId) and verify window.firstResponder ===
terminalPanel.preferredKeyboardTarget (or that
terminalPanel.hostedView.isFirstResponder / the panel’s hosted view reports
itself as the preferred keyboard responder) before proceeding to write
setupComplete; if it isn’t, continue the async retry path. Also follow the same
decision logic used in
AppDelegate.repairFocusedTerminalKeyboardRoutingIfNeeded(window:event:) when
determining the panel’s preferred keyboard target.
Sources/GhosttyTerminalView.swift (1)

2982-2990: ⚠️ Potential issue | 🟠 Major

Resolve directional split focus through the owning TabManager too.

Lines 2988-2990 still route up/down/left/right through the active window’s tabManager, so directional split navigation can still hit the wrong window in multi-window sessions.

Suggested fix
             return performOnMain {
-                guard let tabManager = AppDelegate.shared?.tabManager else { return false }
+                guard let app = AppDelegate.shared,
+                      let tabManager = app.tabManagerFor(tabId: tabId) ?? app.tabManager else {
+                    return false
+                }
                 return tabManager.moveSplitFocus(tabId: tabId, surfaceId: surfaceId, direction: direction)
             }

Based on learnings: sendPickedElementToTerminal(workspaceId:summary:) resolves targets across all mainWindowContexts instead of using the active window manager, because the active tabManager is window-biased.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/GhosttyTerminalView.swift` around lines 2982 - 2990, The code
currently always uses AppDelegate.shared?.tabManager (window-biased) which can
target the wrong window; instead resolve the owning TabManager for the surface
before calling moveSplitFocus. In the performOnMain block, obtain the TabManager
from the surfaceView/terminalSurface owner (e.g. the surface's owning window or
terminalSurface.owningTabManager / surfaceView.window/windowController that
exposes a tabManager) and call tabManager.moveSplitFocus(tabId: tabId,
surfaceId: surfaceId, direction: direction) on that resolved manager rather than
AppDelegate.shared?.tabManager so directional split focus is routed to the
correct window.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/AppDelegate.swift`:
- Around line 9620-9627: recordGotoSplitCycleMoveIfNeeded currently uses
AppDelegate.tabManager.selectedWorkspace which can snapshot the wrong workspace
after multi-window routing; change the function to accept (or resolve) the
triggering tabId and use that tabId to find the correct TabManager and workspace
before calling gotoSplitFindStateSnapshot and writeGotoSplitTestData (preserve
the isGotoSplitUITestRecordingEnabled guard). Also update the
GhosttyTerminalView.swift call site to pass the triggering tabId into
recordGotoSplitCycleMoveIfNeeded so diagnostics reflect the actual
cycleSplitFocus target.

---

Duplicate comments:
In `@Sources/AppDelegate.swift`:
- Around line 8655-8668: The current first-responder check in checkAndSignal()
is too permissive; replace the window.firstResponder is NSView guard with a
check that the focused TerminalPanel actually owns the keyboard focus — e.g. get
the panel via tab.terminalPanel(for: focusedPanelId) and verify
window.firstResponder === terminalPanel.preferredKeyboardTarget (or that
terminalPanel.hostedView.isFirstResponder / the panel’s hosted view reports
itself as the preferred keyboard responder) before proceeding to write
setupComplete; if it isn’t, continue the async retry path. Also follow the same
decision logic used in
AppDelegate.repairFocusedTerminalKeyboardRoutingIfNeeded(window:event:) when
determining the panel’s preferred keyboard target.

In `@Sources/GhosttyTerminalView.swift`:
- Around line 2982-2990: The code currently always uses
AppDelegate.shared?.tabManager (window-biased) which can target the wrong
window; instead resolve the owning TabManager for the surface before calling
moveSplitFocus. In the performOnMain block, obtain the TabManager from the
surfaceView/terminalSurface owner (e.g. the surface's owning window or
terminalSurface.owningTabManager / surfaceView.window/windowController that
exposes a tabManager) and call tabManager.moveSplitFocus(tabId: tabId,
surfaceId: surfaceId, direction: direction) on that resolved manager rather than
AppDelegate.shared?.tabManager so directional split focus is routed to the
correct window.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: d4f8205d-f192-4d54-96aa-301ea3e30be1

📥 Commits

Reviewing files that changed from the base of the PR and between a3cd333 and 49dc450.

📒 Files selected for processing (4)
  • Sources/AppDelegate.swift
  • Sources/GhosttyTerminalView.swift
  • Sources/TabManager.swift
  • Sources/Workspace.swift
🚧 Files skipped from review as they are similar to previous changes (2)
  • Sources/TabManager.swift
  • Sources/Workspace.swift

Comment thread Sources/AppDelegate.swift Outdated
recordGotoSplitCycleMoveIfNeeded now accepts tabId and resolves the
workspace via tabManagerFor(tabId:), consistent with how cycleSplitFocus
itself is routed. Previously it used the active window's tabManager,
which could snapshot the wrong workspace in multi-window scenarios.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

8649-8689: ⚠️ Potential issue | 🟠 Major

Tighten setup-complete gating to the target terminal responder.

checkAndSignal() still treats any .ghosttyDidFocusSurface plus window.firstResponder is NSView as success. During XCTest startup/fallback-window churn, an unrelated Ghostty focus event can satisfy that and mark setupComplete before this three-pane tab actually owns keyboard focus, which makes the first Cmd+] / Cmd+[ flaky again.

Proposed fix
-        func checkAndSignal() {
+        func checkAndSignal(notification: Notification? = nil) {
             guard !resolved else { return }
             guard Date() < deadline else {
                 if let observer { NotificationCenter.default.removeObserver(observer) }
                 resolved = true
                 self.writeGotoSplitTestData(["setupError": "Timed out waiting for terminal focus"])
                 return
             }
-            guard let focusedPanelId = tab.focusedPanelId,
-                  tab.terminalPanel(for: focusedPanelId) != nil,
-                  let window = NSApp.mainWindow ?? NSApp.keyWindow,
-                  window.firstResponder is NSView else {
-                DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { checkAndSignal() }
+            if let notifiedTabId = notification?.userInfo?[GhosttyNotificationKey.tabId] as? UUID,
+               notifiedTabId != tab.id {
+                return
+            }
+            guard let focusedPanelId = tab.focusedPanelId,
+                  let terminalPanel = tab.terminalPanel(for: focusedPanelId),
+                  let window = tabManager.window ?? self.windowId(for: tabManager).flatMap(self.mainWindow(for:)),
+                  terminalPanel.hostedView.window === window,
+                  terminalPanel.hostedView.responderMatchesPreferredKeyboardFocus(window.firstResponder) else {
+                DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { checkAndSignal(notification: nil) }
                 return
             }

             if let observer { NotificationCenter.default.removeObserver(observer) }
             resolved = true
@@
         observer = NotificationCenter.default.addObserver(
             forName: .ghosttyDidFocusSurface,
             object: nil,
             queue: .main
-        ) { _ in checkAndSignal() }
+        ) { note in checkAndSignal(notification: note) }

         // Also poll in case the notification already fired before we observed.
-        DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { checkAndSignal() }
+        DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { checkAndSignal(notification: nil) }

Based on learnings: In Sources/AppDelegate.swift, keyboard focus checks should use hostedView.responderMatchesPreferredKeyboardFocus(responder) so the responder is verified against the intended terminal surface.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/AppDelegate.swift` around lines 8649 - 8689, The gating in
checkAndSignal is too permissive: replace the generic "window.firstResponder is
NSView" check with a verification that the terminal surface actually owns
keyboard focus by locating the terminal's hostedView (from
tab.terminalPanel(for: focusedPanelId) or similar) and calling
hostedView.responderMatchesPreferredKeyboardFocus(responder) against
window.firstResponder; keep the existing timeout/observer removal logic and only
treat the check as successful when responderMatchesPreferredKeyboardFocus
returns true so the writeGotoSplitTestData("setupComplete": "true") is emitted
only for the target terminal responder.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@Sources/AppDelegate.swift`:
- Around line 8649-8689: The gating in checkAndSignal is too permissive: replace
the generic "window.firstResponder is NSView" check with a verification that the
terminal surface actually owns keyboard focus by locating the terminal's
hostedView (from tab.terminalPanel(for: focusedPanelId) or similar) and calling
hostedView.responderMatchesPreferredKeyboardFocus(responder) against
window.firstResponder; keep the existing timeout/observer removal logic and only
treat the check as successful when responderMatchesPreferredKeyboardFocus
returns true so the writeGotoSplitTestData("setupComplete": "true") is emitted
only for the target terminal responder.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9ad817bb-0737-431e-93ae-76ab0d77c40b

📥 Commits

Reviewing files that changed from the base of the PR and between 49dc450 and e8553d7.

📒 Files selected for processing (2)
  • Sources/AppDelegate.swift
  • Sources/GhosttyTerminalView.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • Sources/GhosttyTerminalView.swift

@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

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

7079-7083: ⚠️ Potential issue | 🟡 Minor

Require the actual terminal surface to be first responder before signaling ready.

This still passes when any NSView is first responder, so setupComplete can be written before Ghostty receives Cmd+] / Cmd+[ keybinds. Gate on the focused terminal hosted view instead.

Proposed fix
-            guard let focusedPanelId = tab.focusedPanelId,
-                  tab.terminalPanel(for: focusedPanelId) != nil,
-                  let window = NSApp.mainWindow ?? NSApp.keyWindow,
-                  window.firstResponder is NSView else {
+            guard let focusedPanelId = tab.focusedPanelId,
+                  let terminalPanel = tab.terminalPanel(for: focusedPanelId),
+                  terminalPanel.hostedView.isSurfaceViewFirstResponder() else {
                 DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { checkAndSignal() }
                 return
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/AppDelegate.swift` around lines 7079 - 7083, The current guard only
checks window.firstResponder is any NSView; instead fetch the terminal panel via
tab.terminalPanel(for: focusedPanelId) and verify the actual hosted terminal
view is the first responder (e.g. compare window.firstResponder ===
terminalPanel.hostedView or test that the first responder is a descendant of
terminalPanel.hostedView) before calling checkAndSignal(), so setupComplete is
only signaled when the focused terminal surface truly has focus.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 3121-3124: The DEBUG call to app.recordGotoSplitCycleMoveIfNeeded
runs even when tabManager.cycleSplitFocus(...) returns false, causing tests to
record a move that didn't happen; fix by capturing the Bool result from
cycleSplitFocus(tabId:forward:) and only call
recordGotoSplitCycleMoveIfNeeded(tabId:forward:) when that result is true (i.e.,
wrap the DEBUG call in an if result { ... } using the local result variable).

In `@Sources/TabManager.swift`:
- Around line 5391-5396: Add a DEBUG-only unified debug log call in
cycleSplitFocus to mirror existing focus/split instrumentation: after locating
the Tab and before/after calling tab.cycleFocus(forward:), emit a dlog(...)
wrapped in `#if` DEBUG / `#endif` that records the tab id (tab.id or tabId) and the
direction (forward/backward) and a short event name like "split-focus-cycle" so
this shortcut is traceable in the unified debug log; reference the
cycleSplitFocus function and the tab.cycleFocus(forward:) call when making the
change.

---

Duplicate comments:
In `@Sources/AppDelegate.swift`:
- Around line 7079-7083: The current guard only checks window.firstResponder is
any NSView; instead fetch the terminal panel via tab.terminalPanel(for:
focusedPanelId) and verify the actual hosted terminal view is the first
responder (e.g. compare window.firstResponder === terminalPanel.hostedView or
test that the first responder is a descendant of terminalPanel.hostedView)
before calling checkAndSignal(), so setupComplete is only signaled when the
focused terminal surface truly has focus.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 80ea86c1-2e70-4507-862a-38e690a1a0b0

📥 Commits

Reviewing files that changed from the base of the PR and between e8553d7 and 5459d03.

📒 Files selected for processing (5)
  • GhosttyTabs.xcodeproj/project.pbxproj
  • Sources/AppDelegate.swift
  • Sources/GhosttyTerminalView.swift
  • Sources/TabManager.swift
  • Sources/Workspace.swift
✅ Files skipped from review due to trivial changes (1)
  • GhosttyTabs.xcodeproj/project.pbxproj
🚧 Files skipped from review as they are similar to previous changes (1)
  • Sources/Workspace.swift

Comment thread Sources/GhosttyTerminalView.swift Outdated
Comment thread Sources/TabManager.swift
@basnijholt

Copy link
Copy Markdown

Coming from iTerm2 I really missed this feature. Hope it can get merged 🤞

…e-navigation

# Conflicts:
#	Sources/TabManager.swift
#	cmux.xcodeproj/project.pbxproj
@petzel

petzel commented Jun 16, 2026

Copy link
Copy Markdown

I'm excited about this! Also coming from iTerm, and missing this functionality.

@1vecera

1vecera commented Jul 27, 2026

Copy link
Copy Markdown

Another data point for merging this: I run cmux as a multi-agent workspace — currently 7 panes in one workspace, each holding a different coding agent.

Directional ⌥⌘+arrow navigation degrades badly past ~4 panes: you have to hold a mental map of the split tree to know which arrow lands where. The only non-directional fallback today is focus-history ⌘[ / ⌘], which is MRU rather than positional and collides with browser back/forward in browser surfaces.

This PR's wrapping cycle is exactly the missing primitive. Happy to test it on macOS if that helps unstick review.

mykmelez added 2 commits July 27, 2026 14:44
…e-navigation

# Conflicts:
#	Sources/AppDelegate.swift
#	Sources/Workspace.swift
#	cmux.xcodeproj/project.pbxproj
…e-navigation

# Conflicts:
#	cmux.xcodeproj/project.pbxproj
@cursor

cursor Bot commented Jul 27, 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.

@mykmelez

Copy link
Copy Markdown
Contributor Author

This PR's wrapping cycle is exactly the missing primitive. Happy to test it on macOS if that helps unstick review.

Hi @1vecera , thanks for the offer! I'm not sure if it'll help, as I'm unsure what is sticking review at this point. I've asked @austinywang to review it several times, including in a Discord thread where he asked people to recommend PRs to review (https://discord.com/channels/1324643092963266570/1324643772272742473/threads/1515100799355850832); but he still hasn't done so.

@austinywang , any chance you can prioritize this PR the next time you spend some time focusing on reviews? Multiple commenters eagerly await it!

@austinywang

Copy link
Copy Markdown
Contributor

I opened follow-up PR #9046 to complete/supersede this fork PR, with credit to @mykmelez.

I built and launched cmux DEV pr-2639-dev, created a 3-terminal-pane mixed horizontal+vertical split layout, and verified the real Cmd+] / Cmd+[ keyboard shortcuts end-to-end:

  • Cmd+]: pane:2 -> pane:4 -> pane:3 -> pane:2
  • Cmd+[: pane:2 -> pane:3 -> pane:4 -> pane:2

That visits every pane in stable order and wraps at both ends. Full verification details are in #9046: #9046 (comment)

I also replied to and resolved the four previously unresolved review threads on this PR.

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.

5 participants