Skip to content

Add hover-only pane tab bar controls#23

Open
lawrencecchen wants to merge 12 commits intomainfrom
task-hover-icons-hide-titlebar
Open

Add hover-only pane tab bar controls#23
lawrencecchen wants to merge 12 commits intomainfrom
task-hover-icons-hide-titlebar

Conversation

@lawrencecchen
Copy link

@lawrencecchen lawrencecchen commented Mar 13, 2026

Summary

  • add a hover-only visibility mode for pane tab bar split controls
  • turn hidden split-control space into a window drag region when the host app hides its workspace titlebar

Testing

  • Built via the parent cmux tagged reload using this submodule branch.

Summary by cubic

Adds hover-only pane tab bar controls and full-width tab drop capture. When the workspace titlebar is hidden, empty tab‑bar areas become a draggable region that honors double‑clicks and the macOS traffic‑light inset without breaking tab drags.

  • New Features

    • Controls visibility mode: always or on-hover (@AppStorage paneTabBarControlsVisibilityMode).
    • Controls appear on hover; fade in/out and ignore hits when hidden.
    • When hidden and workspaceTitlebarVisible is false, a TabBarWindowDragRegion sits behind the controls and trailing space; it is disabled during tab drags; honors system double‑click actions.
    • Added accessibility IDs: paneTabBar, paneTabBarControlsRegion, and each split button.
  • Bug Fixes

    • Captures tab drops across the full tab bar (including trailing space) with smarter target index resolution; fixes hidden‑titlebar drops and prevents lingering indicators.
    • Respects the macOS traffic‑light button inset when the titlebar is hidden, and skips it when the tab bar starts right of the buttons; added tests for edge cases.
    • Removed the pane titlebar safe-area inset to prevent layout shifts and accidental drag regions.
    • Disabled window dragging for pane hosts and wrapped the tab bar in a non‑movable host; kept tab‑bar hit‑testing inside the Bonsplit host to avoid pass-through and accidental window moves.

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

Summary by CodeRabbit

  • New Features

    • Tab bar controls can be set to always visible or show-on-hover.
    • Hover-sensitive split-button area with animated visibility and hit-testing.
    • Option to show the workspace title bar.
  • Improvements

    • Enhanced drag regions for tabs and window dragging for more reliable drag behavior.
    • Refined double-click handling for window minimize/zoom actions.
    • Accessibility identifiers added for terminal, browser, and split controls.

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@coderabbitai
Copy link

coderabbitai bot commented Mar 13, 2026

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 hover-driven visibility and AppStorage-backed settings to the tab bar, replaces the inline trailing drop zone with a conditional trailingInteractionView, introduces split-button hover/drag handling and accessibility IDs, adds TabBarWindowDragRegion (NSViewRepresentable) to handle mouseDown/double-click/drag window behavior, and wraps TabBarView with an AppKit hosting wrapper. (43 words)

Changes

Cohort / File(s) Summary
Tab Bar Visibility & Interaction
Sources/Bonsplit/Internal/Views/TabBarView.swift
Adds TabBarControlsVisibilityMode, AppStorage-backed settings, hover state (isHoveringTabBar), derived helpers (controlsVisibilityMode, shouldShowSplitButtonsNow, shouldUseTabBarDragRegion, isTabDragActive), replaces inline trailing drop-zone with trailingInteractionView(width:), implements splitButtonsArea with hover/animation/hit-test gating, adds onHover handlers, and sets accessibility identifiers.
Window Drag Region
Sources/Bonsplit/Internal/Views/TabBarWindowDragRegion.swift
New TabBarWindowDragRegion as NSViewRepresentable providing DraggableView that intercepts mouseDown, handles double-click via global prefs (AppleDoubleClick / AppleMiniaturizeOnDoubleClick) to minimize/zoom/ignore, conditionally enables window movement, calls window.performDrag(with:), and restores window movable state.
Pane Container Hosting & Drag Behavior
Sources/Bonsplit/Internal/Views/PaneContainerView.swift, Sources/Bonsplit/Internal/Views/SplitContainerView.swift, Sources/Bonsplit/Internal/Views/SplitNodeView.swift
Wraps TabBarView in TabBarHostingWrapper (NSView + NSHostingView) to control hosting, accessibility, and hit-testing; swaps arranged-subview containers to PaneDragContainerView; PaneDragContainerView.mouseDownCanMoveWindow now returns false to prevent pane clicks from moving the window.

Sequence Diagram

sequenceDiagram
    participant User
    participant TabBar as TabBarView
    participant DragRegion as TabBarWindowDragRegion
    participant Window
    participant System

    User->>TabBar: hover enter/leave
    TabBar->>TabBar: update hover state & evaluate visibility
    TabBar->>DragRegion: render drag region or drop target (trailingInteractionView)
    User->>DragRegion: mouseDown (click / double-click / drag)
    DragRegion->>DragRegion: inspect clickCount
    alt double-click
        DragRegion->>System: read double-click prefs
        System-->>DragRegion: prefs
        DragRegion->>Window: minimize or zoom per prefs
    else single-click / drag
        DragRegion->>Window: enable movableIfNeeded
        DragRegion->>Window: performDrag(with:)
        Window-->>DragRegion: drag complete
        DragRegion->>Window: restore movable state
    end
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

Poem

🐰
I nibble at the tab bar's edge and wake the buttons with a twitch,
A double-tap, a gentle drag — the window rolls, then finds its niche.
Split peeks, hover hums, accessibility sings; a rabbit cheered the tiny things.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 'Add hover-only pane tab bar controls' directly matches the PR's main objective: adding a hover-only visibility mode for pane tab bar split controls alongside always-visible mode.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch task-hover-icons-hide-titlebar
📝 Coding Plan
  • Generate coding plan for human review comments

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

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

No issues found across 2 files

Copy link

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

🧹 Nitpick comments (1)
Sources/Bonsplit/Internal/Views/TabBarView.swift (1)

78-81: Consider extracting AppStorage keys to constants.

The hardcoded string keys "paneTabBarControlsVisibilityMode" and "workspaceTitlebarVisible" could be extracted to static constants if they're referenced elsewhere in the codebase (e.g., settings UI). This helps prevent typos and makes refactoring easier.

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

In `@Sources/Bonsplit/Internal/Views/TabBarView.swift` around lines 78 - 81,
Extract the hardcoded AppStorage keys into named constants and use them in the
property wrappers to avoid duplication/typos; e.g., add a static constant (or
centralized Settings/StorageKeys struct) such as
paneTabBarControlsVisibilityModeKey and workspaceTitlebarVisibleKey and replace
the string literals in the `@AppStorage` properties controlsVisibilityModeRawValue
and showWorkspaceTitlebar with those constants so other code (settings UI,
tests) can reuse them consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@Sources/Bonsplit/Internal/Views/TabBarView.swift`:
- Around line 78-81: Extract the hardcoded AppStorage keys into named constants
and use them in the property wrappers to avoid duplication/typos; e.g., add a
static constant (or centralized Settings/StorageKeys struct) such as
paneTabBarControlsVisibilityModeKey and workspaceTitlebarVisibleKey and replace
the string literals in the `@AppStorage` properties controlsVisibilityModeRawValue
and showWorkspaceTitlebar with those constants so other code (settings UI,
tests) can reuse them consistently.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e838ad55-c977-4ea9-9b08-2feed56df4c8

📥 Commits

Reviewing files that changed from the base of the PR and between 7b43403 and 43f6b15.

📒 Files selected for processing (1)
  • Sources/Bonsplit/Internal/Views/TabBarView.swift

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 43f6b158fb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

ZStack(alignment: .trailing) {
if !shouldShowSplitButtonsNow && shouldUseTabBarDragRegion && !isTabDragActive {
TabBarWindowDragRegion()
.frame(maxWidth: .infinity, maxHeight: .infinity)

Choose a reason for hiding this comment

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

P2 Badge Constrain hidden controls drag region width

In splitButtonsArea, the hidden-state drag region uses .frame(maxWidth: .infinity), which makes this HStack child flexible and able to absorb extra horizontal space instead of staying the split-buttons width. In the onHover + hidden-titlebar path, that can steal width from the scrollable tabs area and compress/hide tabs while controls are hidden. The drag region should be constrained to the controls slot width rather than allowed to expand infinitely.

Useful? React with 👍 / 👎.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fe83f44ee9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +467 to +469
if shouldUseTabBarDragRegion && !isTabDragActive {
TabBarWindowDragRegion()
.frame(width: width, height: TabBarMetrics.tabHeight)

Choose a reason for hiding this comment

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

P2 Badge Keep trailing tab-drop area active for external drags

When workspaceTitlebarVisible is false, this condition switches the trailing interaction area from an onDrop target to TabBarWindowDragRegion unless isTabDragActive is set. isTabDragActive only reflects local drag state (draggingTab/activeDragTab), so cross-controller drags (the same external-drag case already handled in validateDrop) leave this region non-droppable. In that scenario, dropping after the last tab in the wide trailing gap stops working and users are forced to hit only the tiny 30px end drop zone.

Useful? React with 👍 / 👎.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 79c00997f3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +490 to +492
if !shouldShowSplitButtonsNow && shouldUseTabBarDragRegion && !isTabDragActive {
TabBarWindowDragRegion()
.frame(maxWidth: .infinity, maxHeight: .infinity)

Choose a reason for hiding this comment

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

P2 Badge Keep a drag hotspot when titlebar is hidden

This condition only adds TabBarWindowDragRegion when the split controls are hidden, so with workspaceTitlebarVisible == false and the default controls mode (always), the controls slot is never draggable; if tabs also overflow (so there is no trailing gap), the tab bar has no window-drag target at all. Because this commit also switches pane hosts to BonsplitHostingView (mouseDownCanMoveWindow = false), users in that configuration can no longer drag the window from the pane tab bar.

Useful? React with 👍 / 👎.

Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

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


<file name="Sources/Bonsplit/Internal/Views/PaneContainerView.swift">

<violation number="1" location="Sources/Bonsplit/Internal/Views/PaneContainerView.swift:14">
P1: Returning `self` from `hitTest` swallows child hit targets, preventing `TabBarWindowDragRegion` from receiving mouse events for drag/double-click behavior.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3401bd9fc3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +60 to +63
case .leftMouseDown:
if bounds.contains(point), !hitTestRoutesToWindowDragRegion(at: point) {
suppressWindowDraggingIfNeeded(window: window)
}

Choose a reason for hiding this comment

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

P2 Badge Ignore hidden tab bars in drag-suppression monitor

The new local mouse monitor suppresses window.isMovable whenever the cursor is inside this view’s bounds, but it never checks whether the tab bar is currently hidden/inactive. In this codebase, inactive workspaces are hidden via isHidden (not detached), so their TabBarInteractionContainerView instances still receive monitor callbacks; if their cached frames overlap the active workspace, they can incorrectly disable window dragging for visible UI interactions. Add a visibility/hidden-ancestor guard before calling suppressWindowDraggingIfNeeded so only the active tab bar can affect window movability.

Useful? React with 👍 / 👎.

Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

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

Prompt for AI agents (unresolved issues)

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


<file name="Sources/Bonsplit/Internal/Views/PaneContainerView.swift">

<violation number="1" location="Sources/Bonsplit/Internal/Views/PaneContainerView.swift:22">
P1: `viewDidMoveToWindow` clears the tracked window before restoring `isMovable`, which can leave a window permanently non-movable; it also installs an event monitor when detached (`window == nil`).</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment on lines +22 to +30
if window !== monitoredWindow {
removeEventMonitor()
monitoredWindow = window
installEventMonitor()
}

if window == nil {
restoreWindowDraggingIfNeeded()
}
Copy link

@cubic-dev-ai cubic-dev-ai bot Mar 13, 2026

Choose a reason for hiding this comment

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

P1: viewDidMoveToWindow clears the tracked window before restoring isMovable, which can leave a window permanently non-movable; it also installs an event monitor when detached (window == nil).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/Bonsplit/Internal/Views/PaneContainerView.swift, line 22:

<comment>`viewDidMoveToWindow` clears the tracked window before restoring `isMovable`, which can leave a window permanently non-movable; it also installs an event monitor when detached (`window == nil`).</comment>

<file context>
@@ -3,15 +3,101 @@ import UniformTypeIdentifiers
+    override func viewDidMoveToWindow() {
+        super.viewDidMoveToWindow()
+
+        if window !== monitoredWindow {
+            removeEventMonitor()
+            monitoredWindow = window
</file context>
Suggested change
if window !== monitoredWindow {
removeEventMonitor()
monitoredWindow = window
installEventMonitor()
}
if window == nil {
restoreWindowDraggingIfNeeded()
}
if window !== monitoredWindow {
restoreWindowDraggingIfNeeded()
removeEventMonitor()
monitoredWindow = window
if window != nil {
installEventMonitor()
}
}
Fix with Cubic

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8ebf7c8bc1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +22 to +25
if window !== monitoredWindow {
removeEventMonitor()
monitoredWindow = window
installEventMonitor()

Choose a reason for hiding this comment

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

P2 Badge Restore movability before swapping monitored window

When viewDidMoveToWindow detects a window change, it assigns monitoredWindow = window (and installs a new monitor) before restoring any pending previousWindowMovableState. If this view has already suppressed dragging (window.isMovable = false) and is detached/reparented before the matching leftMouseUp arrives, restoreWindowDraggingIfNeeded() will restore against the new/nil window instead of the original one, leaving the original window stuck non-movable. Restore the old window state before overwriting monitoredWindow.

Useful? React with 👍 / 👎.

Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

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


<file name="Sources/Bonsplit/Internal/Views/TabBarView.swift">

<violation number="1" location="Sources/Bonsplit/Internal/Views/TabBarView.swift:1000">
P2: `dropExited` conditionally clears the indicator using a location-derived index; with dynamic target resolution this can leave a stale drop indicator visible after exiting the tab bar. Clear `dropTargetIndex` unconditionally on exit.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7f2c13f648

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +214 to +216
if !isEnabled {
clearDropState()
}

Choose a reason for hiding this comment

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

P2 Badge Stop clearing hover state during external tab drags

TabBarNativeDropBridge.updateNSView clears drop state every time isEnabled is false, but isEnabled is driven by local drag state (isTabDragActive). During cross-controller drags, SwiftUI onDrop delegates still handle hover/indicator updates while this bridge remains disabled, so each view update resets dropLifecycle back to .idle right after dropEntered, and dropUpdated then short-circuits without maintaining the indicator or target updates. This regresses external drag feedback and target tracking until drop.

Useful? React with 👍 / 👎.

Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

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


<file name="Sources/Bonsplit/Internal/Views/TabBarView.swift">

<violation number="1" location="Sources/Bonsplit/Internal/Views/TabBarView.swift:214">
P1: Wrap the `clearDropState()` call in `DispatchQueue.main.async` to avoid modifying state during view update.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment on lines +214 to +216
if !isEnabled {
clearDropState()
}
Copy link

@cubic-dev-ai cubic-dev-ai bot Mar 13, 2026

Choose a reason for hiding this comment

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

P1: Wrap the clearDropState() call in DispatchQueue.main.async to avoid modifying state during view update.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/Bonsplit/Internal/Views/TabBarView.swift, line 214:

<comment>Wrap the `clearDropState()` call in `DispatchQueue.main.async` to avoid modifying state during view update.</comment>

<file context>
@@ -25,6 +25,198 @@ private struct TabDropFramesPreferenceKey: PreferenceKey {
+        view.updateDropTarget = updateDropTarget
+        view.clearDropState = clearDropState
+        view.performDrop = performDrop
+        if !isEnabled {
+            clearDropState()
+        }
</file context>
Suggested change
if !isEnabled {
clearDropState()
}
if !isEnabled {
DispatchQueue.main.async {
clearDropState()
}
}
Fix with Cubic

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