Skip to content

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

Open
mykmelez wants to merge 4 commits intomanaflow-ai:mainfrom
mykmelez:fix/goto-split-cycle-navigation
Open

fix: goto_split:previous/next cycle through all panes with wrapping#2639
mykmelez wants to merge 4 commits intomanaflow-ai:mainfrom
mykmelez:fix/goto-split-cycle-navigation

Conversation

@mykmelez
Copy link
Copy Markdown

@mykmelez mykmelez commented Apr 6, 2026

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 and goto_split:next to cycle through all panes in tree order with wrap-around, regardless of split direction. Directional up/down/left/right is unchanged, and multi-window routing now targets the correct TabManager.

  • Bug Fixes
    • Implement cycle-based navigation using Bonsplit’s allPaneIds/focusPane; add Workspace.cycleFocus and TabManager.cycleSplitFocus, and remove previous/next mapping from focusDirection.
    • Resolve TabManager by tabId in the action handler so previous/next navigates in the correct window.
    • Add GotoSplitCycleUITests and a test-only 3-pane terminal layout; wait for terminal focus before signaling setup complete to ensure keystrokes are received.

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

Summary by CodeRabbit

  • Bug Fixes

    • Split-pane cycling now moves through all panes in a deterministic tree order with automatic wrapping (replacing previous directional fallback behavior).
  • New Features

    • Cycle navigation (next/previous) is supported to traverse panes sequentially.
  • Tests

    • Added comprehensive UI tests that verify next/previous pane cycling, wrapping behavior, and a three-pane test layout.

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
Copy link
Copy Markdown

vercel bot commented Apr 6, 2026

@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
Copy link
Copy Markdown

cubic-dev-ai bot commented Apr 6, 2026

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
Copy link
Copy Markdown

coderabbitai bot commented Apr 6, 2026

📝 Walkthrough

Walkthrough

Adds cycle-based pane navigation and UI tests: introduces workspace/tab manager cycle focus APIs, routes goto-split previous/next actions to cycle logic, records test setup/moves via AppDelegate, and adds a three-pane UI test suite that validates cycling and wrapping.

Changes

Cohort / File(s) Summary
Project Setup
GhosttyTabs.xcodeproj/project.pbxproj
Added GotoSplitCycleUITests.swift to the cmuxUITests target (build file and file reference).
Core Cycle Navigation
Sources/TabManager.swift, Sources/Workspace.swift
Added TabManager.cycleSplitFocus(tabId:forward:) and Workspace.cycleFocus(forward:) to cycle keyboard focus across panes in tree order with wrapping and to reconcile selection/state.
Action Routing
Sources/GhosttyTerminalView.swift
Updated GHOSTTY_ACTION_GOTO_SPLIT handling: PREVIOUS/NEXT now call tabManager.cycleSplitFocus(...) (and record moves in DEBUG); other directions use spatial moveSplitFocus(...).
Test Infrastructure
Sources/AppDelegate.swift
Added setupThreePaneTerminalLayout(tabManager:) to create a terminal-only 3-pane layout for UI tests and recordGotoSplitCycleMoveIfNeeded(forward:) to snapshot/record goto-split moves to test output.
UI Tests
cmuxUITests/GotoSplitCycleUITests.swift
New UI test class GotoSplitCycleUITests with tests for next/previous cycling, setup/teardown, JSON-state polling helpers, and assertions that cycling visits all panes and wraps.

Sequence Diagram

sequenceDiagram
    actor User
    participant GhosttyTerminalView
    participant TabManager
    participant Workspace
    participant bonsplitController
    participant AppDelegate

    User->>GhosttyTerminalView: Trigger GOTO_SPLIT_NEXT (Cmd+])
    GhosttyTerminalView->>TabManager: cycleSplitFocus(tabId, forward: true)
    TabManager->>Workspace: cycleFocus(forward: true)
    Workspace->>Workspace: Determine next pane in tree order
    Workspace->>bonsplitController: unfocus(currentPane)
    Workspace->>bonsplitController: focusPane(targetPane)
    Workspace->>Workspace: applyTabSelection(in targetPane)
    Workspace-->>TabManager: return success
    TabManager-->>GhosttyTerminalView: return true
    GhosttyTerminalView->>AppDelegate: recordGotoSplitCycleMoveIfNeeded(forward: true)
    AppDelegate->>AppDelegate: write snapshot JSON (test output)
    GhosttyTerminalView-->>User: Focus updated to target pane
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰✨ I hopped through panes with nimble feet,
Next, previous—round they meet,
Tree-order turns and wraps with cheer,
A rabbit's cycle—loud and clear! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 34.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: fixing goto_split:previous/next to cycle through all panes with wrapping behavior, which is the core improvement in this PR.
Description check ✅ Passed The description covers the Summary, Test plan sections and includes implementation details. However, it lacks explicit sections for 'Testing' (how tested, manual verification) and the requested 'Demo Video' section from the template, though manual testing is mentioned.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


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

❤️ Share

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

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

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 on lines +193 to +203
private func launchAndEnsureForeground(_ app: XCUIApplication, timeout: TimeInterval = 12.0) {
let options = XCTExpectedFailure.Options()
options.isStrict = false
XCTExpectFailure("App activation may fail on headless CI runners", options: options) {
app.launch()
}

if app.state == .runningForeground { return }
if app.state == .runningBackground { return }
XCTFail("App failed to start. state=\(app.state.rawValue)")
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Method accepts runningBackground state despite name suggesting foreground.

The method returns successfully when app.state == .runningBackground, which may not actually ensure the app receives keyboard input reliably. Consider either renaming to launchAndEnsureRunning or attempting to activate the app when in background state. This could cause flaky test failures if key events don't reach a background app.

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

In `@cmuxUITests/GotoSplitCycleUITests.swift` around lines 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.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 6, 2026

Greptile Summary

This PR replaces the broken goto_split:previous/next behavior (which was mapped to directional left/right navigation and skipped vertical splits) with proper cycle traversal using Bonsplit's allPaneIds and focusPane APIs. The implementation in Workspace.cycleFocus(forward:) visits all panes in tree order with end-wrapping, and the UI tests correctly verify the full cycle and wrap behavior across a 3-pane mixed layout.

Confidence Score: 5/5

This PR is safe to merge — the fix is targeted, follows existing patterns, and is covered by new UI tests.

No P0 or P1 issues found. cycleFocus correctly handles edge cases (count > 1 guard, currentIndex not found, modular wrapping) and follows the same focus-reconciliation pattern as moveFocus. Tests exercise actual runtime behavior.

No files require special attention.

Important Files Changed

Filename Overview
Sources/Workspace.swift Adds cycleFocus(forward:) for tree-order pane cycling with wrapping; consistent with the existing moveFocus(direction:) pattern
Sources/TabManager.swift Adds cycleSplitFocus(tabId:forward:) delegating to Workspace.cycleFocus; straightforward
Sources/GhosttyTerminalView.swift Routes PREVIOUS/NEXT goto_split actions to new cycle navigation; directional cases unchanged
Sources/AppDelegate.swift Adds three-pane terminal layout setup and cycle-move recording helpers for the UI test harness (DEBUG-guarded)
cmuxUITests/GotoSplitCycleUITests.swift Well-structured UI tests verifying all-pane cycle navigation and end-wrapping via actual Cmd+]/[ key events

Sequence Diagram

sequenceDiagram
    participant U as User (Cmd+]/[)
    participant GTV as GhosttyTerminalView
    participant TM as TabManager
    participant WS as Workspace
    participant BC as BonsplitController

    U->>GTV: handleAction(GOTO_SPLIT_NEXT/PREV)
    GTV->>GTV: performOnMain { }
    GTV->>TM: cycleSplitFocus(tabId:, forward:)
    TM->>WS: cycleFocus(forward:)
    WS->>BC: allPaneIds
    BC-->>WS: [pane0, pane1, pane2]
    WS->>BC: focusedPaneId → currentIndex
    WS->>WS: targetIndex = (currentIndex ± 1) % count
    WS->>BC: focusPane(allPaneIds[targetIndex])
    WS->>WS: applyTabSelection(tabId:inPane:)
    TM-->>GTV: true
    GTV->>GTV: recordGotoSplitCycleMoveIfNeeded (DEBUG)
Loading

Reviews (1): Last reviewed commit: "fix: goto_split:previous/next now cycle ..." | 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
Copy link
Copy Markdown
Author

mykmelez commented Apr 7, 2026

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.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

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

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