Skip to content

feat: drag-and-drop worktree reordering with persistence#270

Open
Guilhem-lm wants to merge 2 commits into
mainfrom
glm/allow-dd-worktree
Open

feat: drag-and-drop worktree reordering with persistence#270
Guilhem-lm wants to merge 2 commits into
mainfrom
glm/allow-dd-worktree

Conversation

@Guilhem-lm

@Guilhem-lm Guilhem-lm commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds drag-and-drop reordering of the worktree list with a persisted custom order, available across both the web dashboard and the CLI. Previously the list was always sorted alphabetically by branch.

The order is stored per-project in .webmux/order.json (a { schemaVersion, branches[] } global branch order), mirroring the existing archive-state pattern. The sidebar sorts by this order (ordered branches first, unknown/new branches appended alphabetically), so newly created worktrees slot in predictably and renamed/removed branches degrade gracefully.

Demo

Screen.Recording.2026-06-17.at.15.06.19.mov

Changes

Persistence (backend)

  • WorktreeOrderState type + WORKTREE_ORDER_STATE_VERSION in domain/model.ts
  • getProjectOrderStatePath / readWorktreeOrderState / writeWorktreeOrderState in adapters/fs.ts
  • Pure order-service.ts: createWorktreeOrderState (dedupe), moveBranchInOrder, pruneWorktreeOrder, buildWorktreeOrderComparator
  • OrderStateService (locked mutations, skip-write-if-unchanged), mirroring ArchiveStateService; wired into runtime.ts
  • buildWorktreeSnapshots sorts by the saved order instead of plain alphabetical
  • readProjectSnapshot prunes stale branches and passes the order through
  • PUT /api/worktrees/order endpoint (apiSetWorktreeOrder)

API contract

  • setWorktreeOrder path, request/response schemas, contract entry, exported types

Frontend

  • Native HTML5 drag-and-drop on each sidebar row with a drop-indicator bar
  • Dragging is constrained to siblings (same parent) so the git-derived hierarchy stays intact
  • Optimistic reorder with rollback + toast on API failure (App.svelte)
  • Pure helpers moveBranchInOrder (sibling-validated) and applyBranchOrder; WorktreeListRow gains parentBranch

CLI parity

  • webmux list honors the saved order
  • New webmux reorder <branch> --before|--after <branch> command (parsing, usage, handler, root-command wiring, shell completions)

Checks

  • svelte-check: 0 errors / 0 warnings
  • backend tsc --noEmit: clean
  • New tests pass: order-service, order-state-service, snapshot ordering (backend); moveBranchInOrder / applyBranchOrder / parentBranch (frontend); reorder parsing + end-to-end reorder→list (CLI)
  • Production bundles build for both webmux and the server

Manual testing

  • Launched the dashboard against a real project (/Users/guilhem/Devel/windmill, 31 worktrees): UI loads, GET /api/worktrees returns the list, PUT /api/worktrees/order returns 200
  • Drag-reordered rows in the sidebar and confirmed the order persisted across refresh
  • Note: pre-existing unrelated test failures (localStorage.clear env issue; git/lifecycle/instance-registry suites) are present on the clean base too

Test plan

  • Drag a worktree above/below a sibling; confirm the drop indicator and that order persists after reload
  • Confirm a child cannot be dropped outside its parent group
  • Create a new worktree; confirm it appears in a sensible position (alphabetical, after ordered ones)
  • webmux reorder <branch> --before <other> then webmux list reflects the new order
  • webmux reorder <unknown> --after <branch> errors cleanly

Generated with Claude Code

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Jun 17, 2026

Copy link
Copy Markdown

Claude finished @Guilhem-lm's task in 3m 3s —— View job


Code Review — drag-and-drop worktree reordering

Reviewed the full diff against origin/main. This is a clean, well-structured PR: types-first, pure logic split from I/O, the OrderStateService mirrors ArchiveStateService (locked mutations + skip-write-if-unchanged), parity across web/CLI, and solid test coverage. The hierarchy handling is also robust — the backend emits a flat order-sorted list and the frontend re-nests it via buildWorktreeListRows, so sibling order persists while the git-derived tree stays intact. 👍

Below are findings, none of which are merge-blockers.

🟡 Worth addressing

1. Stale drop indicator on invalid hoverfrontend/src/lib/WorktreeList.svelte handleRowDragOver (~L214)
When the guard bails out early (hovering a non-sibling row or the dragged row itself), it doesn't clear dropTarget. So if you hover a valid sibling (paints the bar) then move onto a row in a different parent group, the blue drop bar lingers on the previous sibling. There's also no ondragleave, so the indicator persists when the cursor leaves the list area. Clearing dropTarget = null in the early-return branch would keep the indicator only where a drop is actually allowed.

2. Touch / mobile has no reorder pathWorktreeList.svelte (drag handlers)
Native HTML5 drag events (dragstart/dragover/drop) don't fire on touch devices, so sidebar reordering is desktop-only. Per frontend/CLAUDE.md the UI is mobile-first, and the webmux reorder CLI doesn't help a mobile web user. Not a blocker, but worth flagging as a known limitation or a pointer-events follow-up.

🟢 Minor / nits

3. webmux reorder with missing args exits 0bin/src/worktree-commands.ts (~L996)
parseReorderCommandArgs returns null both for --help and for missing required args, and the handler prints usage + return 0 in both cases. So webmux reorder somebranch (no --before/--after) is treated as success. The unknown-branch paths correctly return 1; consider distinguishing "explicit --help" from "incomplete invocation" so the latter exits non-zero (matches the errors cleanly test-plan intent).

4. CLI reorder bypasses the OrderStateService lockbin/src/worktree-commands.ts (~L1020)
The CLI reads/writes order.json directly via readWorktreeOrderState/writeWorktreeOrderState, while the server funnels mutations through OrderStateService's in-process mutation queue. With webmux serve running, a concurrent CLI reorder and a server-side prune (which runs on every readProjectSnapshot) could interleave as a last-writer-wins file race. The service lock is in-process so it wouldn't protect cross-process anyway, and this matches the existing archive-state CLI pattern — noting it for awareness, not asking for a fix here.

5. Logic duplicated across the package boundaryfrontend/src/lib/worktree-list.ts vs backend/src/services/order-service.ts
moveBranchInOrder and the order comparator exist in both. It's largely unavoidable (frontend validates siblings against WorktreeInfo; backend operates on plain branch arrays and the CLI already imports the backend copy), so this is acceptable — just flagging in case the comparator tie-break logic ever needs to stay in sync between the two.

✅ Confirmed correct

  • OrderStateService.mutate serializes via the promise queue and skips the write when orderStatesEqual, matching the archive service.
  • pruneWorktreeOrder keeps the persisted order in sync with live branches, so setOrder sending only currently-known branches won't silently lose ordering for branches that still exist.
  • apiSetWorktreeOrder not broadcasting a snapshot is consistent with apiSetWorktreeArchived / apiSetWorktreeLabel, which also rely on snapshot refresh rather than an explicit push.
  • Read-side validation in isWorktreeOrderState + graceful fallback to an empty state handles corrupt/missing order.json cleanly.

  • Read changed files & understand the feature
  • Review backend (persistence, services, snapshot, endpoint)
  • Review API contract
  • Review frontend (drag-and-drop, optimistic update)
  • Review CLI parity
  • Post review feedback

(Note: I tried to leave items 1 & 2 as inline comments but the GitHub inline-comment API returned an error, so they're consolidated here.)

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 17, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
webmux beee042 Commit Preview URL

Branch Preview URL
Jun 17 2026, 01:13 PM

…order args

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Guilhem-lm

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review! Addressed in beee042:

🟡 #1 — Stale drop indicator ✅ Fixed. handleRowDragOver now clears dropTarget in the early-return branch (hovering the dragged row or a non-sibling), and added an ondragleave on the <ul> that clears the indicator when the cursor actually leaves the list (guarded with a relatedTarget instanceof Node containment check to avoid flicker between rows).

🟢 #3webmux reorder exit code ✅ Fixed. parseReorderCommandArgs now returns null only for --help (handler prints usage, exits 0); a missing <branch> or missing --before/--after throws CommandUsageError, so incomplete invocations print to stderr and exit 1. Added a test for both throw cases.

Acknowledged, not changing:

  • Add test.txt #2 Touch/mobile — Correct, native HTML5 DnD is desktop-only. Leaving as a known limitation; a pointer-events-based touch reorder is a worthwhile follow-up but out of scope here.
  • Test: break paneCount type to fail CI #4 CLI bypasses the in-process lock — Agreed it's last-writer-wins across processes, but the lock is in-process only and this matches the existing archive-state CLI pattern, so keeping it consistent.
  • Fix agent status not showing from backend #5 Cross-package duplication — The frontend copy validates siblings against WorktreeInfo while the backend operates on plain branch arrays (and the CLI imports the backend copy), so the duplication is the comparator tie-break only. Noted for future sync.

svelte-check clean, CLI + frontend reorder tests green.

@Guilhem-lm
Guilhem-lm marked this pull request as ready for review June 17, 2026 13:14
@claude

claude Bot commented Jun 17, 2026

Copy link
Copy Markdown

Claude finished @Guilhem-lm's task in 1m 46s —— View job


Code Review — drag-and-drop worktree reordering (re-review of beee042)

Re-reviewed the full diff against origin/main, focusing on the fixes pushed in beee042. Both addressed items check out, and I didn't find any new blockers. Nice, clean work. ✅

Verified fixes

#1 — Stale drop indicator ✅ Confirmed fixed.

  • handleRowDragOver now sets dropTarget = null in the early-return branch (WorktreeList.svelte:212), so hovering the dragged row or a non-sibling no longer leaves a lingering bar.
  • The <ul> ondragleave handler clears dropTarget only when the cursor actually leaves the list, guarded by event.relatedTarget instanceof Node && event.currentTarget.contains(...) — correctly avoids the row-to-row flicker that a naive ondragleave would cause.

#3webmux reorder exit code ✅ Confirmed fixed.

  • parseReorderCommandArgs now returns null only for --help/-h; a missing <branch> or missing --before/--after throws CommandUsageError (worktree-commands.ts:633,636), so incomplete invocations print to stderr and exit non-zero.
  • Test coverage matches: returns null only for --help, throws when required args are missing, and rejects using both --before and --after.

Acknowledged items (no action needed)

Agree with the dispositions in your reply — #2 (touch/mobile is desktop-only HTML5 DnD), #4 (CLI bypasses the in-process lock, consistent with the archive-state pattern), and #5 (comparator duplication) are all reasonable to defer.

🟢 Minor observations (non-blocking, optional)

a. No-op drag triggers a network round-trip. Dropping --after the row immediately above the dragged item (or --before the one immediately below) produces an identical order, but moveBranchInOrder still returns a non-null array, so reorderWorktree issues a PUT /api/worktrees/order. The server side is safe — OrderStateService.mutate skips the disk write via orderStatesEqual — so the only cost is one redundant request and an optimistic re-sort to the same order. Could short-circuit client-side by comparing the new order to the current branch order before calling setWorktreeOrder, but it's cosmetic. Fix this →

b. CLI reorder is not sibling-constrained, unlike the web UI. The backend moveBranchInOrder operates on a flat branch array with no parent check, so webmux reorder child --before some-root is accepted even though the web UI forbids cross-parent drops. In practice this is harmless because the snapshot emits a flat order-sorted list and the frontend re-nests via buildWorktreeListRows, so hierarchy is reconstructed regardless of the stored order. Worth a one-line note in the reorder help or a follow-up if you want strict parity, but not a correctness issue.

✅ Still confirmed correct

  • OrderStateService mirrors ArchiveStateService: serialized mutationQueue, skip-write-when-unchanged, injectable readState/writeState for tests.
  • pruneWorktreeOrder runs on every readProjectSnapshot, keeping persisted order in sync with live branches.
  • isWorktreeOrderState validation + emptyWorktreeOrderState fallback handle corrupt/missing order.json gracefully.
  • buildWorktreeOrderComparator (ordered-first, then alphabetical) is shared by the snapshot builder and the CLI list, so new/unknown branches slot in predictably on both surfaces.
  • API contract additions (SetWorktreeOrderRequest/ResponseSchema, setWorktreeOrder path + entry) are consistent with the existing label/archive endpoints.

Verdict: LGTM. The two flagged items are addressed correctly; remaining notes are optional polish. (Note: Bash execution is sandboxed in this environment, so I verified the fixes by reading the diff and the added tests rather than running the suite — you reported svelte-check + the reorder tests green.)


  • Read changed files & understand the feature
  • Verify fixes in beee042 (drop indicator, CLI exit code)
  • Review backend / API / frontend / CLI
  • Post review feedback

@centdix centdix left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Better to just store the ordering in local storage

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