feat(worktrees): merge & clean up, health badges, archive-before-remove snapshots#40
Merged
Conversation
…ve snapshots The second half of the agent-worktree loop: retiring a worktree safely once its work lands (improvements.md W1-W3). Engine (worktree.rs): - worktree_health: detected base, ahead-of-base, can-fast-forward, upstream/unpushed, and merged-detection via a containment scan across all local branches (detect_base_branch alone names a sibling when several worktrees sit at one fork point and would hide "merged"). - integrate_worktree_branch: squash / merge-commit / ff into the base, run in whichever worktree holds it (tracked-changes-only dirty guard, conflict auto-abort) or as a pure ref fast-forward when unheld. - Archive snapshots: archive_worktree_state captures HEAD + staged + unstaged + untracked into refs/strand/archive/<slug>/<secs> via a throwaway GIT_INDEX_FILE; restore puts the original directory and branch back when free (subject records the branch, body the path), else falls back detached; list/delete counterparts. prunable reason parsed alongside locked. IPC: six new repo_worktree_* commands; add/remove/prune now route through run_blocking (they wait on subprocesses). UI (Worktrees overview): merged/unpushed/unmerged badges with lock/prune reasons as tooltips, merged hero metric, Clean up (N) for clean+merged worktrees, Prune stale button, WorktreeMergeDialog with exact-command preview + editable base picker, removeWorktree archives first on every path, and a restorable "Archived snapshots" strip. +5 engine tests (health w/ siblings, ff/merge/squash, archive round-trip incl. identity restore and untracked bystanders).
…e busy state Every dialog guards post-await setState with a mountedRef, but armed it only via the useRef initializer with a cleanup-only effect. StrictMode's dev remount reuses the same ref object, so the simulated unmount left it permanently false on a mounted dialog: the first submit error skipped both setError and the finally setBusy(false), freezing the dialog on its busy label (found as "Merging..." stuck in WorktreeMergeDialog; the same latent bug sat in eleven other dialogs). Re-arm the ref in the effect body. Documented in docs/learnings.md; dev-only, production builds do not double-mount.
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.
Summary
The second half of the agent-worktree loop — retiring a worktree safely once its work lands (improvements.md W1–W3, from the 2026-07-07 worktree research pass):
Repo::integrate_worktree_branchlands a worktree branch on its base as squash (default) / merge-commit / fast-forward, running in whichever worktree holds the base (tracked-changes-only dirty guard, conflict auto-abort) or as a pure ref fast-forward when the base isn't checked out anywhere.WorktreeMergeDialogpreviews the exact git commands, has an editable base picker (the detection heuristic can name a sibling when several worktrees share a fork point), and optionally removes the worktree + deletes the branch in the same motion.Repo::worktree_healthclassifies each worktree (merged via a containment scan across all local branches, ahead-of-base, can-fast-forward, unpushed). The overview badges merged / unpushed / unmerged (lock + prune reasons as tooltips), and grows Clean up (N) — confirm-listed removal of clean, fully-merged worktrees incl. branch deletion — and Prune stale.refs/strand/archive/<slug>/<secs>(throwawayGIT_INDEX_FILE, real index untouched). Restore puts the original directory and branch back when they're free, else falls back detached; a collapsible "Archived snapshots" strip offers Restore / Delete.mountedRefin the effect body — StrictMode's dev remount left the ref permanently false, freezing any dialog on its busy label the first time a submit errored.Also rides along:
repo_worktree_add/remove/prunenow route throughrun_blocking.Test plan
cargo test -p strand-core— 102 passed (+5 worktree: health with sibling branches at the fork, ff/merge/squash integration incl. dirty-base refusal and untracked bystanders, archive round-trip incl. identity restore and second-restore fallback)cargo clippy -p strand-core -p strand-tauri -- -D warnings— cleanpnpm --filter ./ui exec tsc --noEmit— cleanpnpm --filter ./ui exec vitest run— 200 passedtauri devagainst a playground repo with merged / dirty / unmerged sibling worktrees: badges, Clean up, merge dialog (incl. frozen-busy repro → fixed, dirty-base inline error), force-remove → restore round-trip with branch + directory identity back.🤖 Generated with Claude Code