fix(tui): preserve prior segment output on Ctrl+C interrupt

interruptTurn only flushed the in-flight streaming chunk (bufRef) to
the transcript before calling idle(), which wiped segmentMessages and
pendingSegmentTools. Every tool call and commentary line the agent had
already emitted in the current turn disappeared the moment the user
cancelled, even though that output is exactly what they want to keep
when they hit Ctrl+C (quote from the blitz feedback: "everything was
fine up until the point where you wanted to push to main").

Append each flushed segment message to the transcript first, then
render the in-flight partial with the `*[interrupted]*` marker and its
pendingSegmentTools. Sys-level "interrupted" note still fires when
there is nothing to preserve.
This commit is contained in:
Brooklyn Nicholson 2026-04-21 11:43:59 -05:00
parent ce98e1ef11
commit dd5ead1007
22 changed files with 228 additions and 76 deletions

View file

@ -95,14 +95,36 @@ class TurnController {
this.interrupted = true
gw.request<SessionInterruptResponse>('session.interrupt', { session_id: sid }).catch(() => {})
const segments = this.segmentMessages
const partial = this.bufRef.trimStart()
const tools = this.pendingSegmentTools
partial ? appendMessage({ role: 'assistant', text: `${partial}\n\n*[interrupted]*` }) : sys('interrupted')
// Drain streaming/segment state off the nanostore before writing the
// preserved snapshot to the transcript — otherwise each flushed segment
// appears in both `turn.streamSegments` and the transcript for one frame.
this.idle()
this.clearReasoning()
this.turnTools = []
patchTurnState({ activity: [], outcome: '' })
for (const msg of segments) {
appendMessage(msg)
}
// Always surface an interruption indicator — if there's an in-flight
// `partial` or pending tools, fold them into a single assistant message;
// otherwise emit a sys note so the transcript always records that the
// turn was cancelled, even when only prior `segments` were preserved.
if (partial || tools.length) {
appendMessage({
role: 'assistant',
text: partial ? `${partial}\n\n*[interrupted]*` : '*[interrupted]*',
...(tools.length && { tools })
})
} else {
sys('interrupted')
}
patchUiState({ status: 'interrupted' })
this.clearStatusTimer()