Skip to content

d on an empty worktree band with a pull request deletes the worktree, and its right-click opens the worktree's menu - #105

Merged
webdevcody merged 1 commit into
mainfrom
fix-104-empty-band-delete
Sep 25, 2026
Merged

webdevcody merged 1 commit into
mainfrom
fix-104-empty-band-delete

Conversation

@webdevcody

Copy link
Copy Markdown
Contributor

Closes #104. d on an empty worktree band whose branch has a detected pull request refused with "the pull request link can't be deleted", and the right-click opened the pull request's menu. Now both reach the worktree's Delete worktree confirm, as the band's hint says.

Contents: 🐛 Symptom · 🔍 Cause · ✅ Fix · 📸 Before / After · 🔁 Flow · ⚠️ Risk · 🔧 Technical overview · 🧪 Proof · 📝 Notes

🐛 Symptom

With Show all worktrees on, a checkout with nothing running is an EMPTY BAND whose hint reads d: delete worktree. When that checkout's branch has a pull request, d only flashed the pull request link can't be deleted — it comes from git, and no confirm opened. The worktree could not be deleted from its band while the pull request was detected. Reported on v0.40.0.

🔍 Cause

A band's cards are its sessions and terminals only, so the band counts as empty and draws the delete hint. The checkout's row list also carries its detected pull request as a link row, though, and the cursor rests on it. The d handler and the right-click menu matched the row under the cursor first: the link row took the "delete this link" path, and the empty-band branch after it never ran. The empty-band test fixture had no pull request, so the test never saw a link row under the cursor.

✅ Fix

  • d deletes the worktree
    • an empty band opens the worktree's own confirm: Delete worktree '<branch>' from disk?
    • whether or not a pull request is detected on its branch
  • The right-click opens the worktree's menu
    • on the band, and on the ↗ #N on its rule
    • the same Delete worktree row the key opens, so the key and the pointer stay in step
  • Unchanged
    • on a band with cards, d and the right-click still act on the card under the cursor
    • the pull request stays one step away: a left-click on its ↗ #N, or ⇧V
    • an empty root band says cannot delete the main checkout, as it did without a pull request

📸 Before / After

Captured with the SCREENSHOT HARNESS: a click on feature-x's empty band (draft PR #39 on its branch), then d.

Before (#104) After
Before: the cursor on feature-x's empty band showing "d: delete worktree", and after pressing d the footer flashes "the pull request link can't be deleted — it comes from git" with no confirm After: the same band and key open the "Delete worktree" confirm: "Delete worktree 'feature-x' from disk? 0 session(s) will be killed."

🔁 Flow

flowchart TD
  K["d, or a right-click, on the grid"] --> E{"empty_band: is the band under the cursor card-less?"}
  E -- "yes, even with the PR's link row selected" --> W["Delete worktree confirm / worktree menu"]
  E -- "no" --> R{"row under the cursor"}
  R -- "session" --> A["Delete agent"]
  R -- "terminal" --> T["Close terminal"]
  R -- "saved link" --> L["Delete link"]
  R -- "detected PR" --> F["flash: can't be deleted"]
  B["before: the row was matched first"] -. "PR link row on an empty band" .-> F
  classDef fixed fill:#1f6f3f,color:#fff;
  classDef bug fill:#7a1f1f,color:#fff;
  class W fixed;
  class B bug;
Loading

⚠️ Risk

Verdict: 🟢 Low risk. The change reorders two existing branches in the TUI's key and menu handlers, and a confirm still stands before any delete.

Level Why
🔒 Security & production Low No new surface: no request, route or file is added. The delete still goes through the existing Delete worktree confirm and its root-checkout guard
⚡ Performance Low Runs on a d press or a right-click only. On a card the check stops at the selected row, as before. Only a link row or no row builds the band list
🧩 Fit with the codebase Low Reuses the existing empty_band helper for both the key and the menu (INPUT PARITY), and adds a regression test beside the empty-band tests already there

Rollback: git revert of the merge restores the old order. There's no protocol or store change to undo.

🔧 Technical overview

  • The helper. crates/nebula-tui/src/event_loop/launcher.rs — empty_band returned None whenever any row was selected. It now returns None only for a row with a card behind it (SessionRow::sref() is Some: a session or a terminal), so a link row no longer hides an empty band.
  • The callers. crates/nebula-tui/src/event_loop.rs — open_delete_confirm and context_menu_items ask empty_band first, under the SESSIONS focus, and fall back to matching the selected row. The link arm stays for a band that has cards.
  • Why only link rows can be there. On the live grid, every non-archived session and every terminal in a checkout has a card, and the ARCHIVED VIEW draws no empty bands. So on an empty band the only row the cursor can rest on is a link: the detected pull request, or a saved link.
  • Why not drop link rows from the grid's row list. visible_session_rows is shared with the panel-era code paths, and about 100 tests pin it. Deciding at the two entry points that act on the band is the smaller change.

🧪 Proof

  • Regression test. an_empty_band_on_a_pull_request_still_deletes_the_worktree in crates/nebula-tui/src/event_loop/launcher.rs puts a detected pull request on the empty band's checkout and asserts that the link row is under the cursor. It then checks that d, a right-click on the band, and a right-click on the ↗ #7 each lead to the worktree's confirm. It fails on origin/main (no overlay; the flash instead) and passes here.
  • Live check. In the SCREENSHOT HARNESS, a build of origin/main shows the flash, and this branch shows the confirm (the screenshots above).
  • Gate. make ci: cargo fmt --check and clippy are clean. cargo test --workspace --no-fail-fast gives 1657 passed, 2 failed. The two failures are e2e_tui's nebula_open_from_inside_a_session_raises_the_file_tabs and tui_drag_past_the_pane_top_autoscrolls_and_copies_the_run, which already fail on main without this change. This diff touches neither.

📝 Notes

  • One commit, on top of v0.40.1 (0aa19b2). It merges cleanly into main.
  • Screenshot harness tip: the band's ↗ #N comes from the project's open-PR list before the checkout's own lookup adds its link row. A scene that depends on that row needs a pause (SHOT_KEY_SECS=3) before the key, or it tests the old no-row path.

🤖 Generated with Claude Code

…worktree, and its right-click opens the worktree's menu

- An empty band's only row can be its checkout's detected pull request,
  a link row the grid draws on the band's rule rather than as a card.
  The cursor rested on it, so d flashed "the pull request link can't be
  deleted — it comes from git" and the right-click opened the link's
  menu instead of the worktree's.
- empty_band now looks past link rows, and d and the row menu ask it
  before matching the row under the cursor.
- Regression test covers d, a right-click on the band and a right-click
  on the #N on its rule.

Closes #104

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@webdevcody
webdevcody merged commit 0993990 into main Sep 25, 2026
1 check passed
@claude

claude Bot commented Sep 25, 2026

Copy link
Copy Markdown

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

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.

d on an empty worktree band with a detected pull request says the PR link can't be deleted, instead of deleting the worktree

1 participant