feat(picker): surface alt-x decline reasons in the header#3336
Conversation
When alt-x declines to remove a row, the row visibly stays put but the reason only drained to stderr after the picker exited — the *why* scrolled past unseen. A new generation-guarded HeaderFlash shows a one-line reason in the picker header (skim's only writable chrome slot — there is no footer) for a beat, then self-clears back to the column labels. Wired into all three "can't remove" surfaces, each styled to match the canonical representation it already stashes: - current worktree → info (○) — a by-design decline - unmerged branch-only → info (○) — a by-design retain - main / dirty / locked → error (✗) — a genuine rejection The full diagnostic still drains to stderr on exit; the flash is the additive in-picker echo. This closes the TODO(picker-feedback) the keep paths carried. Also aligns the --prs loading marker with the watchdog and the picker's other in-flight placeholders: `loading open PRs…` → `↳ Loading open PRs…` (hint symbol in the gutter, "Loading" capitalized, text still at column 2). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
worktrunk-bot
left a comment
There was a problem hiding this comment.
Clean, additive feedback layer — the HeaderFlash generation guard reads correctly (the bump-before-lock / load-under-lock ordering makes the stale-clear race unrepresentable), and the three keep/error surfaces each match the symbol of the diagnostic they already stash (○ for the by-design retains, ✗ for the genuine rejection). Coverage of the mechanism (set / guarded clear / display priority) and the self-clearing timer is thorough.
Holding the verdict as a COMMENT rather than approving: this edits src/commands/picker/mod.rs, which carries the alt-x removal dispatch (is_force() / do_removal / branch force-delete), and per the repo's data-loss-surface review policy a change to that file is a human's call to merge. For the record, the removal dispatch in apply and the background-removal paths are unchanged here — the new flash_header calls sit only in the declined-removal arms (keep current worktree, keep unmerged branch-only row, and the Err rejection arm), so this is feedback-only and doesn't alter what gets deleted. Flagging for @max-sixty's merge decision on the deletion-surface grounds, not on any correctness concern.
One inline doc nit below.
The Err-arm flash renders via error_message (✗), not warning_message (▲); the test's doc comment predated that symbol change. Addresses tend's inline review note on PR #3336. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Release v0.65.0. Version bump plus the 0.65.0 changelog section. **User-facing changes in this release:** - **Picker `alt-x` flashes why a worktree wasn't removed** (#3336, #3350) — the keep/failure reason now appears in the picker header instead of only draining to stderr on exit. - **`-vv` diagnostics consolidate on `diagnostic.md`, led by the performance profile** (#3329). This entry's bullet was previously misfiled under the already-released `0.63.0` section (the PR merged after `0.64.0` was tagged); this release moves it into `0.65.0`, where the change actually ships, and drops a misleading "removed `profile.txt`" clause for a file that never existed. - **`wt remove` preserves your subdirectory position** (#3344, closes #3343, thanks @caillou for reporting). **Also included:** a fix to the release skill's reporter-finding grep — `sort -un` on `#`-prefixed lines collapsed the whole issue-reference list to a single entry (every line sorts as numeric 0), which nearly dropped @caillou's credit. Now sorts on the numeric field after `#`. The nightly cross-platform suite (full-tests linux/macos/windows, feature-powerset, release-target, nix-flake, minimal-versions) passed green on the cut-from tip before this PR was opened. > _This was written by Claude Code on behalf of max_ --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When
alt-xin thewt switchpicker declines to remove a row, the row visibly stays put but the reason only drained to stderr after the picker exited — the user pressed the key, nothing seemed to happen, and the "why" scrolled past on quit. This adds an in-picker echo of that reason.A new generation-guarded
HeaderFlashshows a one-line message in the picker header (skim reserves exactly one non-selectable chrome line — the header — and has no footer slot) for a beat, then self-clears back to the column labels. It's wired into all three "can't remove" surfaces, each styled to match the canonical representation that path already stashes for the on-exit drain:○ Can't remove the current worktree(info — a by-design decline)○ Kept <branch> — branch is unmerged(info — a by-design retain)✗ The main worktree cannot be removed(error — a genuine rejection)The full diagnostic still drains to stderr on exit; the flash is additive. This closes the
TODO(picker-feedback)the keep paths carried.The same change also aligns the
--prsloading marker with the watchdog and the picker's other in-flight placeholders (↳ <dim>Fetching…</>):loading open PRs…→↳ Loading open PRs…. The hint symbol sits in the pointer gutter and "Loading" stays aligned at column 2 (↳is the same two columns the old indent used).Implementation notes
HeaderFlashis shared picker-lifetime between the header item (which renders it) andAltXRemover(which sets it).flash_headersets the message, repaints, and spawns a short-lived timer that clears it afterHEADER_FLASH_DURATIONand repaints again. The clear is generation-guarded — a newer flash arriving mid-beat replaces the old one and isn't wiped by the old timer — andclear_if_currentreads the generation under the message lock so the check and clear are atomic against a concurrentset.Testing
Unit tests cover the
HeaderFlashmechanism (set / generation-guarded clear / display priority of flash › loading › labels), the keep-path symbol, and the timer driven to completion (so the detached thread's clear+repaint is covered under nextest's process-per-test). A PTY integration test confirms the flash actually paints onalt-xof an unremovable row. The loading-marker assertions were updated in lockstep.