Tile panes in 2D and reposition them by dragging - #55
Merged
Conversation
The canvas could only stack panes vertically: layoutRects split purely on height, insertBelow always appended below, and no operation could move a pane once placed. Panes now tile in both directions and can be dragged onto another pane's edge to re-tile. Pane tree: - Splits carry an `orientation`. "column" is the default by omission, so layouts persisted before this change still load as the vertical stacks they were. - layoutRects is axis-generic, and minimumSize sums along a split's own axis while taking the max across it. The old minimumHeight only ever summed heights, so it could not describe side-by-side minimums. - insertRelative places a pane above/below/left/right; moveLeaf prunes the pane from its old slot before re-inserting, so the branch it leaves collapses rather than keeping an empty slot. Impossible moves return the original root by identity so callers can detect a no-op cheaply. Interaction: - Pointer events, not HTML5 drag-and-drop: panes host live xterm canvases, where a native drag image reads as a rendering glitch, and owning the gesture lets the drop target be a region of a pane rather than the whole element. - Nearest edge wins, giving four triangular drop zones meeting at the centre. Escape cancels; a lone pane never starts a drag. - Dividers are orientation-aware for pointer drags (clientX vs clientY) and keys (left/right on a row, up/down on a column). ARIA reports the separator's own orientation, the opposite of its drag axis. The drop highlight is position:fixed, since client rects are already viewport-space - no positioned ancestor required and no pane's overflow can clip it. It animates between targets so the gesture reads as continuous, and holds still under prefers-reduced-motion. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
Adds true 2D pane tiling (row/column splits) and enables drag-to-reposition panes by dropping onto a target pane edge, while keeping legacy (orientation-less) layouts loading as vertical stacks.
Changes:
- Extended the pane tree model with split
orientation, axis-generic layout/minimum-size calculations, and amoveLeafoperation for re-tiling. - Implemented pointer-based pane drag/reposition interactions and made dividers orientation-aware (cursor, drag axis, keyboard arrows, ARIA orientation).
- Added styling for row splits and a fixed-position animated drop indicator; updated tests to cover 2D behavior and no-op paths.
Reviewed changes
Copilot reviewed 5 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| native/macos/psyche-build-tauri/web/styles.css | Adds row-split styling, drag cursors, and drop-indicator styling. |
| native/macos/psyche-build-tauri/web/panes/pane-tree.mjs | Introduces orientation-aware pane tree operations, min-size logic, layout, and move semantics. |
| native/macos/psyche-build-tauri/web/panes/pane-entry.js | Re-exports new pane-tree APIs for consumers. |
| native/macos/psyche-build-tauri/web/panes.bundle.js | Regenerated bundled output reflecting pane-tree and interaction changes. |
| native/macos/psyche-build-tauri/web/main.js | Adds pointer-driven drag-to-reposition and updates divider behavior for row/column splits. |
| tests/tauriPaneTree.test.ts | Adds/updates unit tests for 2D tiling, layout geometry, and move/no-op behavior. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+1156
to
+1158
| .terminal-pane-header { cursor: grab; } | ||
| body.is-pane-dragging { cursor: grabbing; user-select: none; } | ||
| body.is-pane-dragging .terminal-pane-header { cursor: grabbing; } |
Comment on lines
+1403
to
+1416
| function paneElementAt(clientX, clientY) { | ||
| var ids = canvasThreadIds(); | ||
| for (var i = 0; i < ids.length; i++) { | ||
| var thread = findThread(ids[i]); | ||
| var pane = thread && thread.pane; | ||
| if (!pane) continue; | ||
| var rect = pane.getBoundingClientRect(); | ||
| if (clientX >= rect.left && clientX <= rect.right && | ||
| clientY >= rect.top && clientY <= rect.bottom) { | ||
| return { thread: thread, rect: rect }; | ||
| } | ||
| } | ||
| return null; | ||
| } |
The pane tree is imported dynamically, so its exports are untyped and `.map((leaf) => ...)` callbacks tripped TS7006 under tsconfig.test.json. Local `vitest run` never surfaced this - only `pnpm typecheck` does, which is why CI caught it and the local run did not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The pane canvas could only stack vertically.
layoutRectssplit purely on height,insertBelowalways appended below, and no operation could move a pane once placed — you could resize a divider or close a pane, and that was the whole vocabulary. Panes now tile in both directions and can be dragged onto another pane's edge to re-tile.Pane tree
orientation."column"is the default by omission, so every layout persisted before this change still loads as the vertical stack it was — no migration, no version field.layoutRectsis axis-generic, andminimumSizesums along a split's own axis while taking the max across it. The oldminimumHeightonly ever summed heights, so it had no way to express side-by-side minimums.insertRelativeplaces a pane above/below/left/right.moveLeafprunes the pane from its old slot before re-inserting, so the branch it vacated collapses instead of leaving an empty slot. Every impossible move — same leaf, unknown leaf, bad position, single-pane canvas — returns the original root by identity, so callers detect a no-op with===.Interaction
clientXvsclientY) and for keys (←/→on a row split,↑/↓on a column). ARIA reports the separator's own orientation, which is the opposite of its drag axis.position: fixed— client rects are already viewport-space, so it needs no positioned ancestor and no pane'soverflowcan clip it. It animates between targets so the gesture reads as continuous, and holds still underprefers-reduced-motion.Verification
npx vitest run— 1447 passed (up from 1440), 11 skipped, 3 failed.tauriPaneTree.test.tsis at 19 passing, covering: orientation-free legacy layouts still loading as columns, row-axis geometry, the minimum-size mirror (646×120 fits a row split, 645 does not), branch collapse on move, mixed row/column trees staying inside their rect, and every no-op path.script/build_and_run.sh --verify.The 3 failures are this machine's known environmental baseline, in release-signing tests untouched by this change:
appStoreConnect.test.tspins pnpm10.14.0against a local10.33.2, andreleaseWorkflow.test.ts(×2) callsstat -c %Lp, a BSD format spec that fails because GNU coreutilsstatprecedes it on PATH.Not verified
Screenshots and window enumeration are blocked on this machine, so the drag has not been watched, only reasoned about and unit-tested. Three specifics worth a reviewer's hands:
cursor: grabcovers the entire header, including the title text.canFitworking, but it will read as "the drop didn't take."Note for reviewers
web/panes.bundle.jsis checked-in generated output, regenerated withpnpm build:web.main.jsandstyles.cssare loaded directly and are not bundled. Files undernative/are tracked despite a.gitignorerule, so staging needs-f.🤖 Generated with Claude Code