Skip to content

Fix browser tab drops into sidebar workspaces - #3430

Merged
lawrencecchen merged 8 commits into
mainfrom
task-browser-tab-drag-workspace-code-path
May 2, 2026
Merged

lawrencecchen merged 8 commits into
mainfrom
task-browser-tab-drag-workspace-code-path

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented May 2, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Route browser-tab sidebar drops that create a workspace through moveBonsplitTabToNewWorkspace and the shared moveSurfaceToNewWorkspace path.
  • Add an AppKit drop overlay over measured sidebar workspace rows so Bonsplit/browser tabs can drop before, after, or between workspace rows.
  • Cover the sidebar workspace drop planner with unit tests for center row moves, edge insertion, gaps, after-last fallthrough, and pinned workspace boundaries.

Issues

  • User-reported dogfood bug from chat. No GitHub issue was provided.

Verification

  • Dogfooded btabs3, user confirmed “seems good”.
  • git diff --check origin/main..HEAD
  • xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux-unit -destination 'platform=macOS' -derivedDataPath /tmp/cmux-btabpr-unit test -only-testing:cmuxTests/SidebarDropPlannerTests
  • ./scripts/reload.sh --tag btabpr

Summary by cubic

Fixes sidebar drag-and-drop of browser tabs so you can drop into an existing workspace or create a new one in the right spot (including below the last row), with clear before/after indicators. Also prevents stale terminal/browser portals from covering the active workspace during unmounts or handoffs.

  • Bug Fixes

    • Route browser-tab drops through the shared new-workspace move path with insertionIndexOverride clamped after pinned rows, and validate the planned drop action before accepting.
    • Keep portal views hidden for unmounted or retiring workspaces by disabling portal rendering and canceling layout follow-ups; allow immediate hide updates in GhosttyTerminalView.
    • Add a drop overlay over measured workspace rows to enable before/after, gap, and after-last-row drops, plus unit tests for drop planning and portal visibility.
  • Refactors

    • Extract sidebar drop logic into SidebarDropPlanner and SidebarBonsplitTabWorkspaceDropOverlay, and move Ghostty surface-config helpers into WorkspaceSurfaceConfig.swift to reduce ContentView/Workspace complexity.

Written for commit 78e98dc. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Enhanced sidebar drag-and-drop: per-row workspace targets, clearer drop indicators, autoscroll during drag, and selection sync after moves.
    • New option to specify insertion index when creating a workspace; explicit positions are clamped to respect pinned ordering.
    • Workspace-level control to enable/disable portal rendering, improving visibility behavior during handoff and teardown.
  • Tests

    • Added unit and UI tests covering workspace drop planning, insertion-override clamping, and terminal visibility policy.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@vercel

vercel Bot commented May 2, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment May 2, 2026 9:41am
cmux-staging Building Building Preview, Comment May 2, 2026 9:41am

@coderabbitai

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

Introduces sidebar drag-and-drop to move Bonsplit tabs into existing or newly inserted workspaces: adds drop-planning models, per-row geometry anchors, an AppKit overlay to capture workspace drops, threads placement+insertion-index overrides through AppDelegate → TabManager (with clamping for pinned boundaries), and adds unit/UI tests and workspace portal-rendering gating.

Changes

Sidebar Workspace Tab Drop Feature

Layer / File(s) Summary
Data Shape / Models
Sources/Sidebar/SidebarDropPlanner.swift
Adds SidebarDropEdge, SidebarDropIndicator, SidebarDropPlanner.WorkspaceDropTarget, and SidebarDropAction.
Drop Planning Logic
Sources/Sidebar/SidebarDropPlanner.swift
Implements SidebarDropPlanner APIs: indicator(...), targetIndex(...), workspaceAction(for:targets:), edge detection, legal insertion/clamping, and pinned-workspace/tab rules.
Sidebar Geometry & Preference Anchors
Sources/ContentView.swift
Publishes per-workspace row anchors via SidebarWorkspaceRowFramePreferenceKey, builds WorkspaceDropTarget list from anchors in VerticalTabsSidebar, and relaxes showsCenteredTopDropIndicator gating.
AppKit Overlay / Wiring
Sources/Sidebar/SidebarBonsplitTabWorkspaceDropOverlay.swift
Adds SidebarBonsplitTabWorkspaceDropOverlay: NSViewRepresentable and SidebarBonsplitTabWorkspaceDropView: NSView to register drag types, consult planner for workspace actions, manage autoscroll and SidebarDropIndicator, and invoke existing-workspace or new-workspace move handlers.
Move API & Insertion Plumbing
Sources/AppDelegate+MoveTabToNewWorkspace.swift, Sources/TabManager+DetachedWorkspace.swift
Extends moveBonsplitTabToNewWorkspace(...) and moveSurfaceToNewWorkspace(...) to accept/forward placementOverride and insertionIndexOverride; adds canMoveBonsplitTab(tabId:toWorkspace:); TabManager.addWorkspace(fromDetachedSurface:...) accepts insertionIndexOverride and computes/clamps final insertion index (never before pinned boundary).
Workspace Portal Rendering / Visibility
Sources/Workspace.swift, Sources/ContentView.swift, Sources/GhosttyTerminalView.swift
Adds Workspace.portalRenderingEnabled and setPortalRenderingEnabled(_:reason:), gates reconcile/follow-up paths on the flag, toggles portal rendering per mounted/handoff events, and updates Ghostty visibility policy to immediately apply false-visible state.
Workspace Surface Config
Sources/WorkspaceSurfaceConfig.swift
Adds CmuxSurfaceConfigTemplate and helpers to read/derive runtime font size and build inherited surface configs.
Tests & Build Integration
cmuxTests/SidebarWorkspaceDropPlannerTests.swift, cmuxTests/WorkspaceUnitTests.swift, cmuxTests/GhosttyTerminalViewVisibilityPolicyTests.swift, GhosttyTabs.xcodeproj/project.pbxproj
Adds planner unit tests and a detached-insert clamping regression, moves GhosttyTerminalView visibility tests to a separate file, registers new source and test files in the Xcode project.
sequenceDiagram
    actor User
    participant SwiftUI as SwiftUI (ContentView)
    participant AppKit as AppKit (SidebarBonsplitTabWorkspaceDropView)
    participant Planner as SidebarDropPlanner
    participant Delegate as AppDelegate
    participant TabMgr as TabManager

    User->>AppKit: Drag Bonsplit tab over sidebar row
    AppKit->>Planner: workspaceAction(for: point, targets)
    Planner-->>AppKit: WorkspaceDropAction (existing/new + indicator)
    AppKit->>SwiftUI: update dropIndicator binding / autoscroll
    User->>AppKit: Release drag
    AppKit->>Delegate: call moveBonsplitTabToNewWorkspace(..., placementOverride?, insertionIndexOverride?)
    Delegate->>TabMgr: addWorkspace(fromDetachedSurface:..., insertionIndexOverride: ...)
    TabMgr-->>SwiftUI: workspace list updated
    SwiftUI->>SwiftUI: sync selection bindings
    AppKit->>SwiftUI: clear dropIndicator
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly Related PRs

Poem

🐰✨ I hopped along the sidebar line,
Felt edges, gaps, and frames align.
I nudged a tab to worlds brand-new,
Clamped its place where pinned ones grew.
A little hop — the workspace flew!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.16% 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
Title check ✅ Passed The title 'Fix browser tab drops into sidebar workspaces' directly and clearly describes the main bug fix in the PR, which routes browser-tab drops to work properly with sidebar workspaces.
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.
Description check ✅ Passed The PR description covers the main changes (sidebar drag-drop, tab routing, drop overlay), testing approach (dogfood, unit tests), and includes verification steps.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch task-browser-tab-drag-workspace-code-path

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.

@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

🤖 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/ContentView.swift`:
- Around line 14916-14926: The code currently returns nil when beforeTarget is
nil (cursor below every WorkspaceDropTarget), preventing "drop after last
workspace"; change the insertion index calculation to treat a nil beforeTarget
as "after the last target" by using orderedTargets.count instead of 0: compute
let beforeIndex = orderedTargets.firstIndex(of: beforeTarget) ??
orderedTargets.count and pass that to legalNewWorkspaceInsertionIndex (and then
call workspaceIndicator(forInsertionIndex:orderedTargets:) with the resulting
insertionIndex) so drops below the last measured row are accepted.
- Around line 12487-12559: SidebarBonsplitTabWorkspaceDropOverlay currently
captures a live TabManager (tabManager) which violates the rule about holding
ObservableObject stores under LazyVStack/ForEach; replace that live store with
immutable value snapshots and action closures: remove the tabManager property
and instead accept a snapshot of tabs (e.g. [Tab] or a lightweight
TabsSnapshot), a snapshot selectedTabId (UUID?), and closures for move
operations and selection helpers (e.g.
moveBonsplitTab(tabId:toWorkspace:focus:focusWindow:)->Bool,
moveBonsplitTabToNewWorkspace(...)->ResultSnapshot, and a closure to compute
index or set lastSidebarSelectionIndex). Update uses inside updateNSView and
syncSidebarSelection to call those closures / read from the snapshots (replace
tabManager.selectedTabId, tabManager.tabs.firstIndex { ... }, and any move
calls) and adjust performExistingWorkspaceMove/performNewWorkspaceMove to invoke
the provided move closures and return results; ensure
selectedTabIds/lastSidebarSelectionIndex are updated via the provided selection
helper closure instead of accessing TabManager directly.
🪄 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: 6e25f054-00d9-4157-bbf6-f878cc643f3e

📥 Commits

Reviewing files that changed from the base of the PR and between 1f1d187 and 0e22699.

📒 Files selected for processing (4)
  • Sources/AppDelegate+MoveTabToNewWorkspace.swift
  • Sources/ContentView.swift
  • Sources/TabManager+DetachedWorkspace.swift
  • cmuxTests/SidebarOrderingTests.swift

Comment thread Sources/ContentView.swift Outdated
Comment thread Sources/ContentView.swift Outdated
@greptile-apps

greptile-apps Bot commented May 2, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes browser-tab sidebar drops by routing them through moveBonsplitTabToNewWorkspace with a new insertionIndexOverride parameter threaded through moveSurfaceToNewWorkspace and TabManager.addWorkspace. It adds an AppKit-backed SidebarBonsplitTabWorkspaceDropOverlay that uses anchor preferences to measure live workspace row frames and a new SidebarDropPlanner.workspaceAction planner supporting center (merge), top-edge, bottom-edge, and between-row insertion zones while respecting pinned workspace boundaries.

Confidence Score: 4/5

Safe to merge; only P2 findings present

No P0/P1 issues found. The planner logic is correct and well-tested for four of five zones. The insertionIndexOverride bypass in addWorkspace is protected by an existing bounds guard but doesn't re-enforce the pinned-workspace ordering invariant. All other changes — anchor preference plumbing, closure-based NSViewRepresentable callbacks, and the draggedTabId guard removal — are correct and consistent with existing codebase patterns.

Sources/TabManager+DetachedWorkspace.swift line 55 — insertionIndexOverride is used without re-validating the pinned-workspace ordering constraint

Important Files Changed

Filename Overview
Sources/ContentView.swift Adds SidebarBonsplitTabWorkspaceDropOverlay NSViewRepresentable using anchor preferences to measure row frames, new SidebarDropPlanner.workspaceAction planner, and removes the draggedTabId != nil guard from showsCenteredTopDropIndicator so the indicator also renders for browser-tab drags
Sources/AppDelegate+MoveTabToNewWorkspace.swift Threads insertionIndexOverride: Int? through moveBonsplitTabToNewWorkspace and moveSurfaceToNewWorkspace so callers can specify an exact insertion position
Sources/TabManager+DetachedWorkspace.swift Adds insertionIndexOverride to addWorkspace(fromDetachedSurface:) and uses it to bypass the default newTabInsertIndex calculation; existing bounds guard protects against out-of-range values but does not re-enforce pinned-workspace ordering
cmuxTests/SidebarOrderingTests.swift Adds five unit tests covering center-row existing-workspace, top-edge new-workspace, gap new-workspace, after-last-row nil, and pinned-boundary cases; bottom-edge (bottom-quarter) drop case not covered

Sequence Diagram

sequenceDiagram
    participant BTab as Browser Tab Drag
    participant Overlay as SidebarBonsplitTabWorkspaceDropView
    participant Planner as SidebarDropPlanner
    participant App as AppDelegate
    participant TM as TabManager

    BTab->>Overlay: draggingEntered / draggingUpdated
    Overlay->>Planner: workspaceAction(for: point, targets:)
    alt center zone
        Planner-->>Overlay: .existingWorkspace(workspaceId)
        Overlay->>App: moveBonsplitTab(tabId:toWorkspace:)
    else top/bottom edge or gap
        Planner-->>Overlay: .newWorkspace(insertionIndex:, indicator:)
        Overlay->>App: moveBonsplitTabToNewWorkspace(tabId:insertionIndexOverride:)
        App->>TM: addWorkspace(fromDetachedSurface:insertionIndexOverride:)
        TM-->>TM: insert at override index (or append if out-of-bounds)
    else below all rows
        Planner-->>Overlay: nil → NSDragOperationNone
        Note over Overlay: falls through to SidebarBonsplitTabNewWorkspaceDropOverlay
    end
    Overlay-->>BTab: NSDragOperation (.move or [])
Loading

Reviews (1): Last reviewed commit: "fix: use shared workspace move path for ..." | Re-trigger Greptile

Comment thread cmuxTests/SidebarOrderingTests.swift Outdated
Comment thread Sources/TabManager+DetachedWorkspace.swift Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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/Sidebar/SidebarDropPlanner.swift`:
- Around line 107-118: The code in workspaceAction(for:targets:) only handles
drops before a target and returns nil when the pointer is past
orderedTargets.last; change it to detect the "after-last" case and produce a
.newWorkspace insertion at the end: compute insertionIndex =
legalNewWorkspaceInsertionIndex(orderedTargets.count, orderedTargets:
orderedTargets) when the point.y is below the last target (or when the guard
fails because no beforeTarget), and return .newWorkspace with
workspaceIndicator(forInsertionIndex:insertionIndex,
orderedTargets:orderedTargets); update the existing guard/else path so
orderedTargets.count is used for end-of-list drops and prepareForDragOperation
can accept inserts at the end.
🪄 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: 6d888841-28fb-45e5-963c-c9b09ffe267e

📥 Commits

Reviewing files that changed from the base of the PR and between 0e22699 and f97fb7b.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (5)
  • GhosttyTabs.xcodeproj/project.pbxproj
  • Sources/ContentView.swift
  • Sources/Sidebar/SidebarBonsplitTabWorkspaceDropOverlay.swift
  • Sources/Sidebar/SidebarDropPlanner.swift
  • cmuxTests/SidebarWorkspaceDropPlannerTests.swift
✅ Files skipped from review due to trivial changes (1)
  • cmuxTests/SidebarWorkspaceDropPlannerTests.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • Sources/ContentView.swift

Comment thread Sources/Sidebar/SidebarDropPlanner.swift Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 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/Sidebar/SidebarBonsplitTabWorkspaceDropOverlay.swift`:
- Around line 79-115: Add DEBUG-only logging in the overlay drag lifecycle: in
draggingEntered(_:) and draggingUpdated(_:) call
SidebarDropPlanner.workspaceAction(for: localPoint(sender), targets: targets)
(safely) and under `#if` DEBUG use dlog(...) to emit a sanitized summary of the
planner result and the computed drop indicator; in draggingExited(_:) and
concludeDragOperation(_:) call dlog(...) under `#if` DEBUG to note exit/conclude
events and that setDropIndicator(nil) was called; in prepareForDragOperation(_:)
log the boolean outcome of acceptsDrag(sender) and whether
SidebarDropPlanner.workspaceAction(...) returned non-nil; in
performDragOperation(_:) wrap debug logging in `#if` DEBUG to log the chosen
action (case .existingWorkspace(workspaceId) vs .newWorkspace(insertionIndex,
indicator)), the results of performExistingWorkspaceMove(...) or
performNewWorkspaceMove(...), and the final moved boolean before returning;
ensure all logs use the Bonsplit canonical dlog("...") and only include
sanitized IDs/indices (no sensitive data).
- Around line 21-24: The isValidTransfer closure currently always calls
AppDelegate.shared?.canMoveBonsplitTabToNewWorkspace(tabId:) which wrongly
rejects transfers planned for .existingWorkspace; change the logic to inspect
the transfer's planned action (from BonsplitTabDragPayload.currentTransfer())
and validate according to that action: if the planned action is .newWorkspace
call canMoveBonsplitTabToNewWorkspace(tabId:), if it is .existingWorkspace
validate mergeability using the same check the planner or AppDelegate provides
for merges (e.g. the canMerge/validateMerge API used elsewhere), and return
false for other/unknown planned actions; apply the same fix to the other
closures mentioned around lines 91-99 and 136-139.

In `@Sources/TabManager`+DetachedWorkspace.swift:
- Around line 55-59: The override path currently computes insertIndex via
detachedWorkspaceInsertIndex(...) using only snapshot.tabs, which can allow
inserting into a pinned segment that changed between snapshot and the live
updatedTabs; fix by re-clamping the computed insertion index against the live
pinned bounds before performing updatedTabs.insert(...): call
clampedDetachedWorkspaceInsertIndex(...) (or equivalent logic) with the live
updatedTabs (and the same insertionIndexOverride and placementOverride) to
obtain a final safe index, then use that index for updatedTabs.insert; apply the
same re-clamping change to the other override sites that mirror this logic (the
other detached-workspace insert locations referenced by the review).
🪄 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: cd3fc842-6e77-4071-b9cc-65fece8f5cf2

📥 Commits

Reviewing files that changed from the base of the PR and between f97fb7b and 98ea55e.

📒 Files selected for processing (5)
  • Sources/ContentView.swift
  • Sources/Sidebar/SidebarBonsplitTabWorkspaceDropOverlay.swift
  • Sources/TabManager+DetachedWorkspace.swift
  • cmuxTests/SidebarWorkspaceDropPlannerTests.swift
  • cmuxTests/WorkspaceUnitTests.swift
🚧 Files skipped from review as they are similar to previous changes (2)
  • cmuxTests/SidebarWorkspaceDropPlannerTests.swift
  • Sources/ContentView.swift

Comment thread Sources/Sidebar/SidebarBonsplitTabWorkspaceDropOverlay.swift Outdated
Comment thread Sources/Sidebar/SidebarBonsplitTabWorkspaceDropOverlay.swift
Comment thread Sources/TabManager+DetachedWorkspace.swift Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
Sources/TabManager+DetachedWorkspace.swift (1)

117-139: 💤 Low value

pinnedCount is computed inconsistently between the two overloads.

The tabs: [WorkspaceCreationTabSnapshot] overload (line 121) counts all pinned entries via reduce (consistent with newTabInsertIndex), while the workspaces: [Workspace] overload (line 133) counts only the contiguous leading pinned workspaces via prefix { }.count. Under the pinned-first ordering invariant these produce identical results, but the asymmetry is a maintenance hazard if the invariant is ever relaxed.

🔧 Suggested unification
-    private static func clampedDetachedWorkspaceInsertIndex(
-        _ proposedInsertion: Int,
-        workspaces: [Workspace]
-    ) -> Int {
-        let pinnedCount = workspaces.prefix { $0.isPinned }.count
-        return clampedDetachedWorkspaceInsertIndex(
-            proposedInsertion,
-            totalCount: workspaces.count,
-            pinnedCount: pinnedCount
-        )
-    }
+    private static func clampedDetachedWorkspaceInsertIndex(
+        _ proposedInsertion: Int,
+        workspaces: [Workspace]
+    ) -> Int {
+        let pinnedCount = workspaces.reduce(into: 0) { count, ws in
+            if ws.isPinned { count += 1 }
+        }
+        return clampedDetachedWorkspaceInsertIndex(
+            proposedInsertion,
+            totalCount: workspaces.count,
+            pinnedCount: pinnedCount
+        )
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/TabManager`+DetachedWorkspace.swift around lines 117 - 139, The two
overloads clampedDetachedWorkspaceInsertIndex compute pinnedCount differently
(WorkspaceCreationTabSnapshot uses reduce to count all pinned entries while
Workspace uses prefix to count only leading pinned entries); make them
consistent by counting all pinned workspaces in the workspaces overload (e.g.,
compute pinnedCount via filter or reduce over workspaces to count $0.isPinned)
so both clampedDetachedWorkspaceInsertIndex overloads use the same
pinned-counting logic.
🤖 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/TabManager`+DetachedWorkspace.swift:
- Around line 117-139: The two overloads clampedDetachedWorkspaceInsertIndex
compute pinnedCount differently (WorkspaceCreationTabSnapshot uses reduce to
count all pinned entries while Workspace uses prefix to count only leading
pinned entries); make them consistent by counting all pinned workspaces in the
workspaces overload (e.g., compute pinnedCount via filter or reduce over
workspaces to count $0.isPinned) so both clampedDetachedWorkspaceInsertIndex
overloads use the same pinned-counting logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d5424fa6-2be2-4b0d-9421-07e7851648a8

📥 Commits

Reviewing files that changed from the base of the PR and between 98ea55e and e394547.

📒 Files selected for processing (3)
  • Sources/AppDelegate+MoveTabToNewWorkspace.swift
  • Sources/Sidebar/SidebarBonsplitTabWorkspaceDropOverlay.swift
  • Sources/TabManager+DetachedWorkspace.swift
🚧 Files skipped from review as they are similar to previous changes (2)
  • Sources/AppDelegate+MoveTabToNewWorkspace.swift
  • Sources/Sidebar/SidebarBonsplitTabWorkspaceDropOverlay.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.

🧹 Nitpick comments (3)
Sources/Workspace.swift (2)

11527-11542: ⚡ Quick win

Emit a DEBUG portal-render toggle event here.

This method is now the central switch for whether a workspace may render portals at all, so it should log the transition with the workspace id, enabled flag, and reason.

🪵 Proposed logging addition
     func setPortalRenderingEnabled(_ enabled: Bool, reason: String) {
         let changed = portalRenderingEnabled != enabled
+#if DEBUG
+        if changed {
+            cmuxDebugLog(
+                "workspace.portalRendering workspace=\(id.uuidString.prefix(5)) " +
+                "enabled=\(enabled ? 1 : 0) reason=\(reason)"
+            )
+        }
+#endif
         portalRenderingEnabled = enabled
         if enabled {
             if changed {
                 beginEventDrivenLayoutFollowUp(

As per coding guidelines "Implement debug events in a unified log in DEBUG builds at /tmp/cmux-debug.log ... Use cmuxDebugLog("message") to log from code. All call sites must be wrapped in #if DEBUG / #endif."

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

In `@Sources/Workspace.swift` around lines 11527 - 11542, Add a DEBUG-only debug
event at the top of setPortalRenderingEnabled to log the workspace id, the new
enabled value, and the reason: wrap a call to cmuxDebugLog("...") in `#if` DEBUG /
`#endif` and format the message to include self.id (or the workspace identifier
property used in this class), the enabled Bool, and the reason string; keep the
rest of the method (calls to beginEventDrivenLayoutFollowUp,
clearLayoutFollowUp, hideAllTerminalPortalViews, hideAllBrowserPortalViews)
unchanged.

10504-10511: ⚡ Quick win

Route teardown through setPortalRenderingEnabled(...).

teardownAllPanels() is manually reproducing the new disable path instead of calling the helper you just introduced. That makes the teardown path easy to drift the next time portal-disable behavior changes.

♻️ Proposed refactor
     func teardownAllPanels() {
         // Hide portal-hosted content up front so a workspace being torn down
         // cannot keep drawing above the next selected/restored workspace while
         // panel close work is still unwinding.
-        portalRenderingEnabled = false
-        clearLayoutFollowUp()
-        hideAllTerminalPortalViews()
-        hideAllBrowserPortalViews()
+        setPortalRenderingEnabled(false, reason: "workspaceTeardown")
         let panelEntries = Array(panels)
         for (panelId, panel) in panelEntries {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Workspace.swift` around lines 10504 - 10511, teardownAllPanels() is
directly mutating portalRenderingEnabled instead of using the new helper; change
the first line to call setPortalRenderingEnabled(false) so teardown follows the
centralized disable path you added, then keep the subsequent calls
(clearLayoutFollowUp(), hideAllTerminalPortalViews(),
hideAllBrowserPortalViews()) as-is to preserve behavior; ensure any parameters
or side‑effects expected by setPortalRenderingEnabled(...) (e.g. completion
handlers or logging) are correctly propagated.
Sources/WorkspaceSurfaceConfig.swift (1)

82-94: ⚡ Quick win

Add a liveness check before the first ghostty_surface_inherited_config call

ghostty_surface_inherited_config(sourceSurface, context) at line 86 is called without first verifying the pointer via cmuxSurfacePointerAppearsLive, while the downstream call to cmuxCurrentSurfaceFontSizePoints (line 91) does perform that check. If sourceSurface is stale at the point of the first C call, this is a crash rather than a graceful fallback.

The caller in Workspace.inheritedTerminalConfig uses withExtendedLifetime to keep Swift wrappers alive, which mitigates the risk in practice, but the asymmetry between the two C call sites is fragile.

🛡️ Proposed defensive fix
 func cmuxInheritedSurfaceConfig(
     sourceSurface: ghostty_surface_t,
     context: ghostty_surface_context_e
 ) -> CmuxSurfaceConfigTemplate {
+    guard cmuxSurfacePointerAppearsLive(sourceSurface) else {
+        return CmuxSurfaceConfigTemplate()
+    }
     let inherited = ghostty_surface_inherited_config(sourceSurface, context)
     var config = CmuxSurfaceConfigTemplate(cConfig: inherited)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/WorkspaceSurfaceConfig.swift` around lines 82 - 94, In
cmuxInheritedSurfaceConfig, add a liveness check using
cmuxSurfacePointerAppearsLive(sourceSurface) before calling
ghostty_surface_inherited_config so you don't call into C with a stale pointer;
if the pointer is not live, return a safe default CmuxSurfaceConfigTemplate (or
construct one without calling ghostty_surface_inherited_config) and preserve the
existing behavior of applying runtime font-size from
cmuxCurrentSurfaceFontSizePoints when available. This change touches
cmuxInheritedSurfaceConfig, ghostty_surface_inherited_config,
cmuxSurfacePointerAppearsLive, cmuxCurrentSurfaceFontSizePoints, and the
CmuxSurfaceConfigTemplate initializer—ensure the defensive early-return happens
prior to the current call at the top of the function.
🤖 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/Workspace.swift`:
- Around line 11527-11542: Add a DEBUG-only debug event at the top of
setPortalRenderingEnabled to log the workspace id, the new enabled value, and
the reason: wrap a call to cmuxDebugLog("...") in `#if` DEBUG / `#endif` and format
the message to include self.id (or the workspace identifier property used in
this class), the enabled Bool, and the reason string; keep the rest of the
method (calls to beginEventDrivenLayoutFollowUp, clearLayoutFollowUp,
hideAllTerminalPortalViews, hideAllBrowserPortalViews) unchanged.
- Around line 10504-10511: teardownAllPanels() is directly mutating
portalRenderingEnabled instead of using the new helper; change the first line to
call setPortalRenderingEnabled(false) so teardown follows the centralized
disable path you added, then keep the subsequent calls (clearLayoutFollowUp(),
hideAllTerminalPortalViews(), hideAllBrowserPortalViews()) as-is to preserve
behavior; ensure any parameters or side‑effects expected by
setPortalRenderingEnabled(...) (e.g. completion handlers or logging) are
correctly propagated.

In `@Sources/WorkspaceSurfaceConfig.swift`:
- Around line 82-94: In cmuxInheritedSurfaceConfig, add a liveness check using
cmuxSurfacePointerAppearsLive(sourceSurface) before calling
ghostty_surface_inherited_config so you don't call into C with a stale pointer;
if the pointer is not live, return a safe default CmuxSurfaceConfigTemplate (or
construct one without calling ghostty_surface_inherited_config) and preserve the
existing behavior of applying runtime font-size from
cmuxCurrentSurfaceFontSizePoints when available. This change touches
cmuxInheritedSurfaceConfig, ghostty_surface_inherited_config,
cmuxSurfacePointerAppearsLive, cmuxCurrentSurfaceFontSizePoints, and the
CmuxSurfaceConfigTemplate initializer—ensure the defensive early-return happens
prior to the current call at the top of the function.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c2d43078-5c34-417a-861c-f0731e50b8c2

📥 Commits

Reviewing files that changed from the base of the PR and between e394547 and 6338a6f.

📒 Files selected for processing (8)
  • GhosttyTabs.xcodeproj/project.pbxproj
  • Sources/ContentView.swift
  • Sources/GhosttyTerminalView.swift
  • Sources/Workspace.swift
  • Sources/WorkspaceSurfaceConfig.swift
  • cmuxTests/GhosttyTerminalViewVisibilityPolicyTests.swift
  • cmuxTests/TerminalAndGhosttyTests.swift
  • cmuxTests/WorkspaceUnitTests.swift
💤 Files with no reviewable changes (1)
  • cmuxTests/TerminalAndGhosttyTests.swift
✅ Files skipped from review due to trivial changes (1)
  • cmuxTests/GhosttyTerminalViewVisibilityPolicyTests.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • cmuxTests/WorkspaceUnitTests.swift

@lawrencecchen
lawrencecchen merged commit 3f3964d into main May 2, 2026
40 checks passed
@lawrencecchen
lawrencecchen deleted the task-browser-tab-drag-workspace-code-path branch May 2, 2026 10:02

This branch was successfully deployed

1 active deployment
Preview – cmux — 78e98dc9 Deployed May 2, 2026 by vercel[bot]
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