feat: Board Pane Reorder UI#329
Conversation
There was a problem hiding this comment.
Pull request overview
This PR wires the already-shipped board reorder backend surface into the /board/:name desktop UI, adding drag-and-drop pane reordering and command-palette parity, plus backend support for cross-server neighbour resolution and comprehensive test coverage.
Changes:
- Add desktop board pane drag-and-drop reorder (header-only drag source, pane-root drop target) with optimistic preview and rollback/cancel handling.
- Add command palette actions to move the focused pane left/right with boundary gating and focus tracking by
server:windowIdkey. - Add backend cross-server neighbour lookup for reorder and add unit/e2e test coverage across frontend and backend.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| fab/changes/260708-rmiq-board-pane-reorder/plan.md | Implementation plan and acceptance criteria for board pane reorder UI. |
| fab/changes/260708-rmiq-board-pane-reorder/intake.md | Intake document describing scope and approach. |
| fab/changes/260708-rmiq-board-pane-reorder/.status.yaml | Change status and metrics for the fab pipeline. |
| fab/changes/260708-rmiq-board-pane-reorder/.history.jsonl | Fab pipeline stage history for the change. |
| docs/memory/run-kit/ui-patterns.md | Documented the board pane reorder UI pattern and palette parity. |
| docs/memory/run-kit/index.md | Updated memory index entry description to include board pane reorder pattern. |
| docs/memory/run-kit/architecture.md | Documented backend cross-server neighbour resolution behavior. |
| app/frontend/tests/e2e/board-reorder.spec.ts | Playwright e2e validating reorder endpoint + GET ordering + SSE broadcast. |
| app/frontend/tests/e2e/board-reorder.spec.md | Companion documentation describing what the e2e spec proves and why. |
| app/frontend/src/lib/board-reorder.ts | Pure helper functions for neighbour computation and focus reconciliation gates. |
| app/frontend/src/lib/board-reorder.test.ts | Vitest coverage for neighbour computation, focus-by-key reconcile, focus gating. |
| app/frontend/src/hooks/use-pin-actions.ts | Changed reorder action to rethrow after toast so callers can rollback optimistic UI. |
| app/frontend/src/hooks/use-board-pane-reorder.ts | New DnD hook for pane reorder with optimistic override + cancel/reject rollback. |
| app/frontend/src/hooks/use-board-pane-reorder.test.ts | Vitest coverage for DnD MIME guard, preview splice, cancel/reject rollback, reconcile. |
| app/frontend/src/components/board/board-pane.tsx | Wired drag/drop props into pane root/header and added drag-source dimming. |
| app/frontend/src/components/board/board-page.tsx | Integrated hook, added palette actions, and implemented focus-by-key tracking + gate. |
| app/frontend/src/components/board/board-header.tsx | Made header draggable (optional) and protected unpin button interactions. |
| app/backend/api/router.go | Updated reorder neighbour lookup to aggregate across servers + added warning on skip. |
| app/backend/api/router_test.go | Added tests for cross-server neighbour resolution and negative/boundary cases. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const authoritative = entries.map((en) => paneKey(en.server, en.windowId)); | ||
| const fromIdx = authoritative.indexOf(dragKey); | ||
| const landedIdx = override.indexOf(dragKey); | ||
| if (fromIdx === -1 || landedIdx === -1) return; | ||
| // Derive neighbours directly from the final override order (correct by | ||
| // construction — the override already reflects the drop position). | ||
| const before = landedIdx > 0 ? override[landedIdx - 1] : null; | ||
| const after = landedIdx < override.length - 1 ? override[landedIdx + 1] : null; | ||
| // No-op guard: if the pane did not actually move (neighbours unchanged | ||
| // from its authoritative slot), skip the POST. | ||
| const authBefore = fromIdx > 0 ? authoritative[fromIdx - 1] : null; | ||
| const authAfter = | ||
| fromIdx < authoritative.length - 1 ? authoritative[fromIdx + 1] : null; | ||
| if (before === authBefore && after === authAfter) return; // no-op: no POST |
There was a problem hiding this comment.
Fixed — onDrop now reconciles the raw override against the current authoritative keys (drop absent keys, append newly-present entries — the same reconciliation the render does) before deriving before/after, so a mid-drag pin/unpin or remote reorder can no longer POST a stale neighbour windowId. Added a regression test covering a neighbour removed mid-drag. (ee53be8)
| /** | ||
| * Pure order-computation helpers for board pane reorder — shared by the | ||
| * drag-and-drop hook (`use-board-pane-reorder.ts`) and the command-palette | ||
| * Move Focused Pane Left/Right actions (`board-page.tsx`). | ||
| * | ||
| * The board reorder endpoint (`POST /api/boards/{name}/reorder`) uses fractional | ||
| * indexing: it mints an orderKey strictly between the moved pane's new | ||
| * `before` and `after` neighbours (`ComputeOrderKey` in | ||
| * `app/backend/internal/tmux/board.go`). So a move is expressed as ONE POST | ||
| * carrying the new neighbours' windowIds — `before` is the windowId that ends | ||
| * up immediately BEFORE the moved pane (smaller orderKey), `after` is the one | ||
| * immediately AFTER (larger orderKey); each is `null` at the respective edge. | ||
| * | ||
| * Extracting the neighbour arithmetic here (mirroring `palette-move.ts`) keeps | ||
| * it unit-testable without mounting the board, and lets DnD and the palette | ||
| * share one implementation. | ||
| */ |
There was a problem hiding this comment.
Fixed — updated the module docstring to state the DnD hook deliberately does NOT import this module (it derives neighbours inline from its final optimistic override), matching computeMoveNeighbors' existing note. (ee53be8)
| // These helpers back both the board pane drag-and-drop hook | ||
| // (use-board-pane-reorder) and the command-palette Move Focused Pane | ||
| // Left/Right actions. Covering the insert-before neighbour arithmetic + | ||
| // boundary gating here proves the reorder POST's before/after are correct | ||
| // without mounting the board (mirrors palette-move.test.ts). |
There was a problem hiding this comment.
Fixed — corrected the test-file comment to say these helpers back only the command-palette Move Left/Right actions, noting the DnD hook derives neighbours inline and does not import this module. (ee53be8)
Wires the already-shipped backend reorder endpoint (orderKey sort, reorderPin, board-changed SSE) to the desktop board row: header-only drag handle with a derive-over-store optimistic preview, and Cmd+K Move Focused Pane Left/Right with boundary gating and focus-follow.
4fa4969 to
77a50de
Compare
Meta
rmiqexcludes
fab/,docs/· generated by fab-kit v2.13.5Pipeline: intake ✓ → apply ✓ → review ✓ → hydrate ✓ → ship → review-pr
Summary
Panes on
/board/$namerendered in serverorderKeyorder with no UI to change it, even though the backend reorder endpoint, sorted fetch, and SSE broadcast were already shipped and unused. This wires that dark plumbing to the desktop board row: drag-and-drop via the pane header plus Cmd+KMove Focused Pane Left/Right.Changes
application/x-board-pane-reorderMIME, derive-over-store optimistic preview (ref-based, no watcher effect, no drag-end snap-back), singlereorderPinPOST per drop with before/after neighbor windowIds, cross-server board support.setFocusedIndex..spec.md.