Skip to content

Make Separate All Panes and Separate Current Pane keybindable commands - #231

Closed
thdxg wants to merge 1 commit into
claude/pr-228-overlay-pills-a4a0cbfrom
claude/pr-228-pane-to-sidebar
Closed

Make Separate All Panes and Separate Current Pane keybindable commands#231
thdxg wants to merge 1 commit into
claude/pr-228-overlay-pills-a4a0cbfrom
claude/pr-228-pane-to-sidebar

Conversation

@thdxg

@thdxg thdxg commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Stacked on #230 (which stacks on #228) — retarget as the PRs beneath merge.

Separate Panes existed only in the tab context menu. This PR makes it and a new per-pane sibling first-class commands — palette rows plus rebindable hotkeys (unbound by default, matching the other tree-rewriting actions like Apply/Save Layout):

  • Separate All Panes — the context-menu action, invocable on the active tab.
  • Separate Current Pane — new: splits just the focused pane out of the active tab into its own tab, inserted right after it.

Why commands and not a sidebar drag

Dragging a pane's grab handle onto the sidebar was tried first (twice) and is not implementable with public SwiftUI: the sidebar column's hierarchy never receives the grab handle's AppKit NSDraggingSession. Tested against the live app with synthesized drags — row-, ForEach-, and header-level dropDestination; row-, header-, and List-level onDrop; and a target outside the List — none ever got a single delegate callback, while the identical delegate fires fine in the workspace column (the same drag session reshapes splits there). The finding is recorded on MovablePane's doc comment so nobody retries it blind.

How it works

  • AppState.separatePane does removePane's tree/zoom/focus repair minus the surface teardown, so the Pane object (surface and running shell) survives the trip; crossing projects rebinds routing identity the way moveTab does. The destination workspace resolves before detaching, so a failure can't leave the pane belonging to no tab.
  • Both commands follow the add-an-action recipe: AppCommand cases (palette picks them up via allCases), HotkeyAction cases, and routing through the AppCommand.action(in:) single source of truth in Responders.swift, so palette, menu, and binding can't drift. Enablement guards make them no-ops on single-pane tabs.
  • MovablePane stays as the shared drag-pasteboard decode for the workspace's LeafDropDelegate, which previously unpacked the UUID bytes inline.

Testing

  • New AppStateTests: pane lands in its own tab with source-tab zoom/focus repair, insertion at a slot, cross-project rebind + destination activation, only-pane / unknown-pane no-ops. Full suite passes; format/lint clean.
  • Live-verified through the command palette in a hermetic app instance: "Separate Current Pane" on a 2-pane tab produced the new tab right after the source with focus following; "Separate All Panes" exploded a 3-pane tab into single-pane tabs in place.

🤖 Generated with Claude Code

@github-actions github-actions Bot added area:ui Views, Settings UI area:state AppState, models, persistence area:tests Test changes labels Aug 7, 2026
@thdxg
thdxg force-pushed the claude/pr-228-pane-to-sidebar branch 2 times, most recently from 088d6e2 to 75f866e Compare August 7, 2026 09:40
@github-actions github-actions Bot added the area:hotkeys Key routing, hotkeys label Aug 7, 2026
@thdxg thdxg changed the title Drag a pane's grab handle to the sidebar to split it into its own tab Make Separate All Panes and Separate Current Pane keybindable commands Aug 7, 2026
@thdxg
thdxg force-pushed the claude/pr-228-pane-to-sidebar branch 3 times, most recently from eacf46e to a4da8fb Compare August 7, 2026 10:10
@thdxg
thdxg force-pushed the claude/pr-228-pane-to-sidebar branch 2 times, most recently from ecdc830 to 55826e0 Compare August 7, 2026 15:47
Separate Panes existed only in the tab context menu, and dragging a
pane's grab handle to the sidebar was tried as the per-pane counterpart
but is not implementable: the sidebar column's SwiftUI hierarchy never
receives the grab handle's AppKit NSDraggingSession. Every public drop
mechanism was tested against the live app — row-, ForEach-, and
header-level dropDestination, row/header/List-level onDrop, and a
target outside the List — and none ever got a callback, while the same
delegate fires fine in the workspace column. So both operations become
AppCommands instead: palette rows plus rebindable hotkeys, unbound by
default like the other tree-rewriting actions.

- Separate All Panes: the context-menu action, now invocable on the
  active tab from the palette or a binding.
- Separate Current Pane: new — splits just the focused pane out of the
  active tab into its own tab inserted right after it, via
  AppState.separatePane: removePane's tree/zoom/focus repair minus the
  surface teardown, so the pane object (surface and running shell)
  survives the trip; crossing projects rebinds routing identity the way
  moveTab does. Covered by new AppStateTests.

MovablePane stays as the shared drag-pasteboard decode for the
workspace's LeafDropDelegate, which previously unpacked the UUID bytes
inline.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:hotkeys Key routing, hotkeys area:state AppState, models, persistence area:tests Test changes area:ui Views, Settings UI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant