/history counted raw message rows, so /history 1 printed whatever row happened
to be last (usually a tool result in a tool-heavy session) and "one row means
full body" was a proxy for "show me that one thing".
A turn is what a human reviews: /history now prints the last turn's two ends
(the user message and the model's answer, markdown for the latter), -v prints
every row of the selected turn(s), -vv prints full bodies plus the local system
row, and --preview collapses every row to one line. N addresses turn N,
last [N] the last N turns, --message N a single row.
Numbering is a display ordinal over conversation rows (local-only rows print as
local) and deliberately never a database row number - seq is an implementation
detail that shifts with /clear. Grouping and rendering live in the new
cli/transcript.py.
Ctrl+C now cancels the turn (or the auto-retry countdown), cancels a poll-based
prompt read such as wait's, and is ignored with a hint for off-loop readline
prompts; it never exits the REPL and never reaches MCP servers, which spawn in
their own session. Also drop ask_into_pty's stale "Ctrl+C cancels" claim: that
prompt is readline-based and empty input is its cancel.
The wait line rendered every non-error/non-slept outcome as "(disturbed)", so a
Ctrl+C cancellation of the wait prompt looked like the human disturbed it. Print
"(cancelled)" for the tool's "cancelled by user" result.
The wait tool's prompt runs off-loop on a worker thread, but its read polls
select/msvcrt, so it can act on a cancel: the SIGINT target now sets a flag the
reader checks between polls and raises PromptCancelledError with, which the tool
reports as "cancelled by user" while the turn continues. run_prompt_async /
run_cancellable_prompt_async hand the matching hook to the CLI guard; a stale
cancel never leaks into the next prompt read.
A SIGINT delivered while the event loop is parked in its selector is raised in
the loop's own frame: it escapes run_until_complete, the Runner cancels the main
task, and the REPL is left half-shut-down (no prompt, and the next line the
human types is eaten by an abandoned prompt reader) until the following Ctrl+C
kills the process. Off-loop prompts (ask_user_* / wait / tool confirms run in a
worker thread via asyncio.to_thread) and the auto-retry countdown are the usual
windows.
Keep one asyncio-level SIGINT handler installed for the whole session instead:
it cancels the innermost registered target (run_cancellable registers the turn
task, sleep_cancellable the retry countdown, off_loop_prompt the waiting prompt)
and never raises. Reads that block on the main thread inside a coroutine (the
REPL entry prompt, synchronous click prompts) suspend the router and take SIGINT
as a KeyboardInterrupt. Off-loop readline prompts cannot be interrupted without
abandoning their reader thread on stdin, so they ignore SIGINT with a one-line
hint.
A terminal Ctrl+C is delivered to every process in the foreground process
group, so a server spawned in our group died on any Ctrl+C during a turn -
killing in-flight tool calls and leaving the connection dead until
/mcp reconnect. start_new_session (POSIX) / CREATE_NEW_PROCESS_GROUP
(Windows) puts each server in its own session/group instead.
Tests assert the spawned server's process group and session differ from the
CLI's (they used to be the same group).