Skip to content

feat(picker): surface alt-x decline reasons in the header#3336

Merged
max-sixty merged 2 commits into
mainfrom
picker-waiting-row
Jul 1, 2026
Merged

feat(picker): surface alt-x decline reasons in the header#3336
max-sixty merged 2 commits into
mainfrom
picker-waiting-row

Conversation

@max-sixty

Copy link
Copy Markdown
Owner

When alt-x in the wt switch picker 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 HeaderFlash shows 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:

  • current worktree → ○ Can't remove the current worktree (info — a by-design decline)
  • unmerged branch-only row → ○ Kept <branch> — branch is unmerged (info — a by-design retain)
  • main worktree / dirty / locked → ✗ 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 --prs loading 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

HeaderFlash is shared picker-lifetime between the header item (which renders it) and AltXRemover (which sets it). flash_header sets the message, repaints, and spawns a short-lived timer that clears it after HEADER_FLASH_DURATION and 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 — and clear_if_current reads the generation under the message lock so the check and clear are atomic against a concurrent set.

Testing

Unit tests cover the HeaderFlash mechanism (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 on alt-x of an unremovable row. The loading-marker assertions were updated in lockstep.

This was written by Claude Code on behalf of max

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 worktrunk-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

Comment thread tests/integration_tests/switch_picker.rs Outdated
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>
@max-sixty max-sixty merged commit 7f6f34d into main Jul 1, 2026
37 checks passed
@max-sixty max-sixty deleted the picker-waiting-row branch July 1, 2026 03:56
max-sixty added a commit that referenced this pull request Jul 2, 2026
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>
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