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.
`TuiModelStore.confirmSatisfied()` and `approvalFor()` had no caller
outside their own tests. The first one mattered: it answered "does the
typed text authorize this kill?" with an exact name match, while the rule
actually consulted (`confirmAccepts()` in tui-app) also accepts the
8-character id prefix a mux name carries. Two divergent answers to one
question, the stricter one unreachable and waiting to be picked up by
mistake. knip cannot see class members, so the dead-code sweep never
flagged either.
The tests they existed for now assert observable state instead, and the
approvals one got stronger on the way: it checks that a session id coming
back does not inherit the dead session's dialog, which is the invariant
`removeSession()` is actually keeping.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Measured against a live Claude pane: an Ink TUI paints by ROW and emits
almost no newlines, so dropping cursor-position sequences collapsed a whole
screen into one unreadable line, and a tail cut mid-sequence printed the
remains of it (";1H") as text. Now a jump to column 1 starts a display line,
a jump inside a row moves the write position (capped, since a stream may
address a column no terminal has), and a severed CSI head is dropped before
parsing.
The preview is readable against a real session as a result: tool calls, the
working line and the composer all land where they belong.
Also drop the repeated session name from a search row, whose snippet opens
with the name the row already shows in its first column.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The store gains the three overlays phase 2 needs, each taking the keyboard
when it is set and all of them cleared together by closeOverlay(), plus the
pure flattening of `GET /api/search`'s typed groups into rows a cursor can
move over: headers are chrome, and only a session that is on the list counts
as selectable, since a history hit has no row to move the cursor to.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rows are the ones GET /api/sessions/unified already returns and blocked states
are the items the approvals inbox already parsed, both imported as types only
so a CLI process pulls in neither the server nor node-pty. Classification
speaks the web UI's language (red blocked, yellow waiting, green working) so a
user with both surfaces open never has to translate between them.
Groups order by how long a session has been in its state, which is why WORKING
anchors on the pane's last Enter: a working pane repaints about once a second,
so its last-activity stamp always says "now".
Selection is tracked by session id, never by row index: rows re-sort under the
cursor whenever a session starts working or an approval lands, and an
index-tracked cursor would quietly move the selection to another session
between two keystrokes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>