Skip to content

feat(app): drag a pane to move it within a session's splits - #173

Merged
TooonyChen merged 1 commit into
mainfrom
feature/drag-panes
Nov 10, 2026
Merged

TooonyChen merged 1 commit into
mainfrom
feature/drag-panes

Conversation

@TooonyChen

Copy link
Copy Markdown
Owner

Summary

Before this, the only way to rearrange a split session was to close panes and split again, which ends their shells. Now a pane can be dragged onto another one.

  • Grip: when a session has two or more panes, hovering over a 160×24 DIP strip at the middle of a pane's top edge shows a 40×10 pill with dots. It doesn't show with one pane, while a pane is zoomed, while the pane's find bar is open (the bar sits at the top and would cover it), or while another pane is being dragged.
  • Drag: the drag uses MyGo's in-window drag and drop (Element.Drag(paneDrag) on the grip, ui.DragOver/ui.Drop on each pane's box). A card with the pane's number, its title (or the session name) and the last line on its screen follows the pointer. The pane being dragged gets a darker veil. The pane under the pointer mutes its terminal and shows four zones: left and right strips, top and bottom bars. The zone is the side nearest the pointer (the pane split by its diagonals). It is filled in the accent color and reads "Drop on the right" / "放到右边". The other zones have dashed outlines and short names. Labels move out of the way of the card. Esc cancels.
  • Drop: paneTree.move removes the pane (its sibling takes the room) and splits the target on the chosen side, 50/50. The dropped pane gets the focus. Two kinds of drop do nothing, and that zone does not light up: a drop on the pane itself, and a drop on the side of its sibling where it already is. The divider's share is kept in that case.
  • Shell pick: remove drops the pane from paneTree.order, so move saves the order and puts it back. pick (monitor.ShellPick) goes by the order the shells opened in, so a moved pane's follower still reads its own shell, not the shell of whatever pane now sits where it was. The terminal objects are untouched.
  • Not stealing clicks: the pane box reads the pointer with PointerPosition, which takes no input (the terminal inside is still the press target). The grip sits in a PassThrough row and has KeepFocus. Only the 40×10 pill itself takes presses. Selecting text and mouse reporting in a full-screen program work everywhere else, including the rest of the strip, and a test covers both. The grip is only built while the pointer is in the strip, so most of the time nothing extra is there at all.
  • Keyboard alternative: it was cheap, so I added it. paneTree.swap trades the focused pane with its neighbor in a direction. The session row's menu (paneMenu, the one function the menu uses) gets a "Move pane" submenu with "Swap with the pane on the left/right/above/below". The Shortcuts page gets four actions (movePaneLeft/Right/Up/Down) with no default keys. The free-looking candidates are all taken by something else: Alt+Shift+arrows (Windows Terminal resize), Cmd+Ctrl+arrows (Ghostty resize), Ctrl+Shift+arrows (select by word in editors).
  • Workaround for MyGo: the grip stays in the tree while it is pressed (App.moving), Esc included. Without that, cancelling with Esc took the grip out of the next frame. That ended the press without a release, so the cancelled drag was never cleared and no later drag could start.
  • Docs: features.md (Splits, and the key names on the Shortcuts page) and development.md (Panes of a session), in both languages.

Compared with the mockup

Matches: the grip pill at the top middle of the pane, the dashed left/right/top/bottom zones with 左/上/下, the highlighted right zone reading "放到右边", the card following the pointer, the dimmed source pane, and the ① | ② | ③ layout after the drop. Differences:

  • Card content: the mockup's panes have names ("① 部署", "② 日志") on screen. Panes have no names, so the panes show no label and the card shows "② web-1" (or the program's title, when it sets one) plus the last line of its screen instead of the command.
  • Pane sizes after the drop: the mockup shows three equal columns. Here the target is split 50/50, so ① and ② share half and ③ keeps its half. "Equalize panes" in the row menu evens them out. I didn't want every drop to reset the dividers the user has set.
  • Zone label position: a highlighted label is placed out of the card's way (near the top or bottom of the strip, or at a quarter of a bar), so it isn't always in the middle.
  • Grip size: the grip is 40×10 rather than the mockup's wider pill, to cover less of the first terminal row.

Testing

  • go fmt ./... (no changes), go build ./..., go vet ./..., GOOS=windows go vet ./..., GOOS=darwin go vet ., go tool mygo vet, go test -race ./...: all pass.
  • TestPaneMove: moves onto each side of a pane check the tree, the focus, the unchanged order and picks, and that a shell opened afterwards still counts after them. It also covers drops that change nothing (onto itself, onto the sibling's side, keeping the divider's share) and swapping, including at the edge.
  • TestDropSide: the side nearest the pointer, and lastLine.
  • TestDragPane (real tester and SSH test server): no grip with one pane, off the strip, or zoomed. Esc cancels a drag. A drop on itself does nothing. Drops on each side of another pane give the expected tree. Terminals stay the same objects, picks are unchanged, and the server has three shells. Input still reaches the right shell. A drag inside a pane selects and copies text. A click in the strip beside the grip reaches a program with mouse reporting as ESC [M.
  • TestMovedPaneFollowsItsShell: after a move, each pane still follows its own shell's folder through the stand-in follower.
  • TestSwapPaneKeys: a bound Move pane key and the row menu's submenu swap panes. TestShortcutDefaults is updated for the four actions without keys.
  • TestDragPaneScreenshots: the grip on hover, a drag over the right side of pane ①, and the layout after the drop, in English and Simplified Chinese. Shots were checked against the mockup and are not committed.
  • Not tested: a real window with a real mouse (only the headless tester), on macOS, Windows or Linux. Also not tested: how the extra frames from PointerPosition on mouse moves over a split pane cost in practice.

Generated by Claude Code

A pane's grip, shown over a strip at the middle of its top edge while
the session has two panes or more, drags it with MyGo's in-window drag
and drop onto another pane, which shows four drop zones; dropping splits
that pane on the side nearest the pointer. The move keeps the pane's
terminal and its place in the order the shells opened in, so its
follower still reads its own shell. Esc cancels; a drop that would
change nothing does nothing. Move pane items in the row's menu and four
shortcuts without default keys swap the focused pane with a neighbor.

Closes #169

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015nMqJHsqtndDSqJDPp1qYv
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 10, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-10T04:04:33.947907Z 99f9f48 PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@TooonyChen
TooonyChen merged commit 487cf65 into main Nov 10, 2026
1 check passed
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.

2 participants