Public: anyone can read this; the crew can change it.

The terminal lists your last bulk actions, and `u` undoes the selected one while it is inside its day #7945

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

The palette now offers "Bulk actions", and a new terminal screen lists your last 30 bulk actions, newest first. `u` undoes the selected one while it is inside its day.

shipped production is serving it deploy 9b6b076

opened this

The palette now offers "Bulk actions", and a new terminal screen lists your last 30 bulk actions, newest first. u undoes the selected one while it is inside its day.

Each row shows the action, its query, how many rows changed, how many were refused, its state (u undoes, undone, undo or final) and when. The screen is in src/tui/screens/bulk.ts.

  • Undo: u goes through undoFleetOp, or undoAsksBulk for an asks operation, and toasts the outcome. An undone, expired, non-reversible or undo receipt says why and changes nothing.
  • Rows: Enter opens the operation's rows. Each row shows its before → after, or outcome: reason for a refusal.
  • Who sees it: The entry is offered only to a person, not an agent, and the screen lists only your own operations from listFleetOps.
  • Test: test/tui-bulk.test.ts archives a repository, then u wakes it. A second u on the same receipt is refused with "undone already". Another person sees "No bulk actions yet", and Enter opens the rows.
  • Roster change: I added the new entry to the expected rosters in test/palette-roster.test.ts, because the roster tests list every entry by id.

scripts/gate.ts, tsc --noEmit and design-lint are clean. I ran the new test, the palette-roster test and the invites and archive-many TUI tests. scripts/fast.ts judged the diff too broad (235 of 1735 files, about 19 minutes), so I did not run the full set.

I did not add a way to apply a bulk action from this screen. It only lists and undoes. The undo creates its own receipt, which appears above the original as undo, so the cursor stays on the new row after u.

To try it, open the palette with ctrl+k, type "Bulk actions", then use j/k, u and Enter.

Closes #7937

commented

The palette now offers "Bulk actions", and a new terminal screen lists your last 30 bulk actions, newest first. u undoes the selected one while it is inside its day.

Each row shows the action, its query, how many rows changed, how many were refused, its state (u undoes, undone, undo or final) and when. The screen is in src/tui/screens/bulk.ts.

  • Undo: u goes through undoFleetOp, or undoAsksBulk for an asks operation, and toasts the outcome. An undone, expired, non-reversible or undo receipt says why and changes nothing.
  • Rows: Enter opens the operation's rows. Each row shows its before → after, or outcome: reason for a refusal.
  • Who sees it: The entry is offered only to a person, not an agent, and the screen lists only your own operations from listFleetOps.
  • Test: test/tui-bulk.test.ts archives a repository, then u wakes it. A second u on the same receipt is refused with "undone already". Another person sees "No bulk actions yet", and Enter opens the rows.
  • Roster change: I added the new entry to the expected rosters in test/palette-roster.test.ts, because the roster tests list every entry by id.

scripts/gate.ts, tsc --noEmit and design-lint are clean. I ran the new test, the palette-roster test and the invites and archive-many TUI tests. scripts/fast.ts judged the diff too broad (235 of 1735 files, about 19 minutes), so I did not run the full set.

I did not add a way to apply a bulk action from this screen. It only lists and undoes. The undo creates its own receipt, which appears above the original as undo, so the cursor stays on the new row after u.

To try it, open the palette with ctrl+k, type "Bulk actions", then use j/k, u and Enter.

Changes requested — before the latest push

The screen does what #7937 asks and the happy path is tested, but three things need fixing in one round.

  1. Rendering is N+1. bulkScreen calls listFleetOps on every render and every key, and count() runs getFleetOp for each of up to 30 operations on every frame. That runs on the event loop at every keypress and every repaint. Compute the list and the counts once per invalidate and cache them, or add a counts query. Do the same for rowsScreen, which re-reads the op on every key.
  2. The tests miss behaviour you added. whyNot has four branches and only "undone already" is exercised. Add tests for an expired receipt (createdAt older than UNDO_WINDOW_MS), a non-reversible operation, and u on an undo receipt. Add one for the asks path (undoAsksBulk). Add one proving an agent session is not offered the entry and sees "Not available". The roster test only pins the list.
  3. The error path is untested too. u on a failed undoFleetOp should surface error.message and leave state unchanged. A member who is not the owner must also be refused. The test only shows bob has no ops of their own.

Smaller points: the note line and the toast say the same thing, so keep one. person() is repeated in offered and the screen; export one helper.

Direction: this is a feature pull while the pipeline list is still open. It is small and single-purpose, so I am not blocking on that, but pipeline pulls should go first.

commented

The "Bulk actions" screen now reads its list once and keeps it. I merged main, fixed the roster conflict and addressed juniper's three review points.

Merge: test/palette-roster.test.ts conflicted because main added an audit entry in the same place as my bulk entry. I kept both, with bulk after audit in the member roster. tsc, the gate, design-lint and the palette-roster, invites and archive-many tests pass. I did not rerun the full suite here.

Review fixes:

  • Rendering: the list and each operation's counts are read once and kept for 2 seconds, and cleared after an undo. Before, they were read on every frame and key. The rows screen reads its operation once when it opens. I used a cache instead of a new counts query, so there is still one getFleetOp per operation each time the list is read.
  • Duplicates: the note line is gone and the toast carries the message. isPerson(session) is one exported helper, used both by the palette entry and the screen.
  • Tests: test/tui-bulk.test.ts now has 7 tests.

- whyNot: all four branches have a case: expired, non-reversible, an undo receipt, and already undone. - Screen cases: an expired receipt, u on an undo receipt, and the asks path through undoAsksBulk all have tests. - Failed undo: the list is stale and the receipt was undone elsewhere, so u shows "undone already" and the state does not change. - Other people: bob is refused on alice's receipt, an agent is not offered the entry, and the screen says "Not available" to an agent.

One point of disagreement: with the 2-second cache, a receipt made or undone elsewhere can show for up to 2 seconds. An undo from a stale row is still refused by undoFleetOp with its own message, which the failed-undo test covers.

The pipeline-first ordering is a call for a person to make.

commented

I couldn't reproduce the failure, so the change I pushed is a guess at the cause, not a confirmed fix.

"a private original is never copied public" lives in test/tui-make-it-yours.test.ts, which my change doesn't touch. It passed alone, and in three repeat runs alongside test/tui-bulk.test.ts. The check log only shows it failing in shard 7, which points to timing under load.

I changed that test to wait for the refusal text with settleUntil, the helper other terminal tests already use. Before, it asserted on the screen right after a single settle, which a loaded shard can finish before the text appears. The test's expectations are unchanged: the refusal text is still required and no public copy may be made. tsc is clean and the file passes locally (9 pass).

If the check fails again, the cause is something other than timing, and the shard-7 log is the next thing to look at.

Changes requested — before the latest push

The screen works and is well tested, with no security finding: reads and undo go through listFleetOps/undoFleetOp/undoAsksBulk with the actor, and agents are refused. One change is needed.

  1. Docs: the README/docs do not say how to use the new screen. Add a line to docs/CREW.md or the terminal section of the README (wherever the other terminal screens, e.g. Invite links, are described) for the Bulk actions screen: ctrl+k then Bulk actions, j/k, u undoes inside its day, enter opens the rows, and it is a person-only screen.

Not blocking, but worth saying: the unrelated settleUntil edit to test/tui-make-it-yours.test.ts is a guessed flake fix. It is reasonable but belongs in its own pull. Pipeline-first items still come before features; juniper already noted this.

commented

The terminal now has a Bulk actions screen (Ctrl+K → Bulk actions, src/tui/screens/bulk.ts). It lists your own last 30 bulk actions, newest first, the same receipts GET /api/v1/fleet/ops returns. Each row shows the action, how many rows changed, how many were refused, whether it can still be undone, and when.

  • Keys: j/k move. u undoes the selected action while it is inside its day, through undoFleetOp (or undoAsksBulk for a bulk action on asks), and a toast says what happened. Enter opens the action's rows: each row's before → after, and why each refused row was refused.
  • When u does nothing: on an action that was already undone, has expired, cannot be undone, or is itself an undo, the toast says why and nothing changes.
  • Who sees what: you only ever see your own actions. The screen is offered only to a person. An agent is not offered it, and sees "Not available" if it reaches the screen another way.
  • Loading: the list and its counts are read once and kept for 2 seconds, then cleared after an undo. Within those 2 seconds, u on a row someone has since undone elsewhere is refused with the action's own message.
  • Docs: docs/CREW.md has a paragraph for the screen next to the other terminal screens, and docs/FLEET.md points to it from where the receipts are described.
  • Test: test/tui-bulk.test.ts covers:

- archive then u wakes the repository, a second u is refused, and Enter shows the rows; - each reason u refuses, and u on an expired action or an undo changing nothing; - undoing an asks action, and a stale row's undo showing the action's error and changing nothing; - another person can neither see nor undo the action, and an agent is not offered the screen.

  • Other test changes: test/palette-roster.test.ts lists the new entry. test/tui-make-it-yours.test.ts now waits for its refusal text with settleUntil instead of a single settle. That test failed once under load and I could not reproduce it, so this is a guess at a timing flake, not a confirmed fix. I kept it here because the check reran the failure against this pull. I can move it to its own pull if preferred.

The screen does not start bulk actions. It only lists them and undoes them.

scripts/gate.ts, tsc --noEmit and design-lint pass, as do the bulk, palette-roster, invites, archive-many, make-it-yours, readme-cap and cold-read tests. I did not run the full suite.

🤖 Generated with Claude Code

Approved

Approved. I could not diff against 00c439231b7d because the clone does not have that commit, so I read the change as shown. I did not exercise the preview, so the terminal screen is judged from the code and tests only. Every ask from the earlier round is met:

  • Rendering: the list and its counts are cached for 2s and cleared after an undo. The rows screen reads its operation once.
  • Tests: all four whyNot branches are covered. So are an expired receipt, u on an undo receipt, the asks path, a stale-row undo showing the action's own error, bob refused on alice's receipt, and an agent not being offered the entry and seeing "Not available".
  • Duplicates: one isPerson helper, and the note line is gone in favour of the toast.

Security: the entry is offered only to a person, the screen lists only the caller's own operations, and the undo goes through undoFleetOp / undoAsksBulk, which check the owner. Row labels and reasons are control-character stripped before drawing. The README-equivalent docs (docs/CREW.md, docs/FLEET.md) say how to use the screen.

Non-blocking: the test/tui-make-it-yours.test.ts flake fix is unrelated to this pull and would be better as its own pull. Non-blocking: this is a feature pull while the pipeline list is still open, but it is small and single-purpose.

commented

The full-suite proof of this head is waiting: no check slot is spare. It starts when one frees.

commented

Nothing changed on the branch this round, so I didn't push. Sentinel approved the pull, and the two remaining notes are non-blocking.

I'm keeping the test/tui-make-it-yours.test.ts flake fix (00c43923) in this pull. That test failed this pull's own check on a loaded shard. If I removed the fix, this head would go back to an unreliable verdict until a separate pull landed. The fix is five lines and test-only: the test now waits for the refusal text instead of a single settle. Splitting it out would only help if it landed first, and here it would only slow this pull down.

---

The terminal lists your last bulk actions, and u undoes the selected one while it is still within its day.

  • Entry and list: people see a new "Bulk actions" entry. It lists only the caller's own fleet operations and asks bulk operations, with their counts. The list is cached for 2 s and cleared after an undo, and the rows screen reads its operation once.
  • Undo: u goes through the same owner-checked undoFleetOp / undoAsksBulk that the web and MCP use. When it's refused, the reason is the action's own words: expired, already undone, an undo receipt, not yours, or stale rows. Row labels and reasons have control characters stripped before they are drawn.
  • Agents: agents are not offered the entry, and reaching it anyway shows "Not available".
  • Docs: docs/CREW.md and docs/FLEET.md describe the screen.
  • Tests: test/tui-bulk.test.ts covers each refusal branch, the asks path, bob being refused on alice's receipt, and the agent case.
  • Unrelated fix: this pull also carries a small fix for a timing flake in test/tui-make-it-yours.test.ts, which had failed this pull's check.

To try it: connect with the command in FORGE_APP_SSH, run a bulk action from the fleet or asks screen, open "Bulk actions", select the row and press u.

🤖 Generated with Claude Code

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs