fix(tui): confirm a kill with y, and make the dialog say what it would kill

Killing demanded the session's NAME typed out in full. That is the right
ceremony for dropping a production database and the wrong one for closing a
pane you are looking at; the beta tester's verdict was "thats stupid, just make
me type Y to confirm". `x` then `y` is already two deliberate keystrokes on a
row the user selected, and the conversation lives in its transcript, which a
kill does not touch.

Everything that is not `y` CANCELS rather than being ignored, so a stray key
closes the dialog instead of leaving a destructive prompt armed and waiting for
whatever gets typed next. Enter cancels too: it is the key most likely to be
hit by reflex, and this is the one dialog that destroys something.

⚠️ Found while verifying the new dialog: it did not name the session. The label
was computed as `row.session.name ?? id.slice(0, 8)`, and `??` falls back only
on null or undefined, so every session the server left with an EMPTY name — all
of them, until the TUI started naming its own — sailed through and the box read
"Kill ?". A destructive prompt that cannot say what it will destroy is worse
than no prompt, and it is now a single keystroke. The caller passes the same
label the LIST shows, so the dialog names the row in front of the user.

The typed-name machinery goes with it: TuiConfirmState.typed, setConfirmInput(),
confirmAccepts() and the 'typing'/'reject' steps are all removed rather than
left as unreachable branches.
This commit is contained in:
Codeman maintainer
2026-08-20 01:49:59 +02:00
parent 5202eac1e9
commit ed4d0da2da
8 changed files with 75 additions and 84 deletions
+10 -4
View File
@@ -267,11 +267,17 @@ describe('the store', () => {
it('tracks the confirm-kill overlay, keyed to the name it showed', () => {
const model = createTuiModel();
model.replaceSessions([session({ sessionId: 'a', name: 'w4-api' })]);
model.beginConfirmKill(model.rows()[0]);
model.beginConfirmKill(model.rows()[0], 'w4-api');
expect(model.mode).toBe('confirm-kill');
expect(model.confirm).toEqual({ sessionId: 'a', name: 'w4-api', typed: '' });
model.setConfirmInput('w4-ap');
expect(model.confirm?.typed).toBe('w4-ap');
// The name is the whole payload: it is what the dialog shows so the user
// knows WHICH session a `y` is about to destroy.
expect(model.confirm).toEqual({ sessionId: 'a', name: 'w4-api' });
// Regression: the label is supplied by the caller. Deriving it here with
// `name ?? id` let an EMPTY name through, and the dialog read "Kill ?".
model.replaceSessions([session({ sessionId: 'b', name: '' })]);
model.beginConfirmKill(model.rows()[0], 'mirofish');
expect(model.confirm?.name).toBe('mirofish');
model.closeOverlay();
expect(model.mode).toBe('list');
expect(model.confirm).toBeNull();