Keep interrupted prompts in conversation history (#33198)

## What changed

- Leave an output-free interrupted prompt in the transcript and open a blank
  composer for the user's next instruction.
- Use the normal interruption notice for both `Esc` and `Ctrl-C` interrupts.
- Track safety-buffering prompts separately so failed retries can still restore
  the prompt, including after switching between threads.

## Testing

- Cover output-free `Esc` and `Ctrl-C` interrupts, failed safety-buffer retries,
  and restoration of safety-buffer state from thread snapshots.

GitOrigin-RevId: 30b368d00dfdb981d06d4c95ce3f252389b7917c
This commit is contained in:
Felipe Coury authored and copyberry committed 2026-07-15 01:07:29 +00:00
1 parent 4df8027a97
commit 70a0b1eef8
29 files changed
+186 -269

No files matched your search

+2 -17
View File
@@ -664,7 +664,7 @@ pub(crate) struct ChatWidget {
// order.
suppress_initial_user_message_submit: bool,
input_queue: InputQueueState,
cancel_edit: CancelEditState,
safety_buffering_prompt: Option<UserMessage>,
/// Main chat-surface bindings resolved from `tui.keymap.chat`.
chat_keymap: ChatKeymap,
/// Keybinding to show for popping the most-recently queued message back
@@ -787,13 +787,6 @@ pub(crate) enum InterruptedTurnNoticeMode {
Suppress,
}
#[derive(Debug, Default)]
struct CancelEditState {
prompt: Option<UserMessage>,
eligible: bool,
armed: bool,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum ReplayKind {
ResumeInitialMessages,
@@ -1219,9 +1212,6 @@ impl ChatWidget {
}
fn add_boxed_history(&mut self, cell: Box<dyn HistoryCell>) {
if self.turn_lifecycle.agent_turn_running && !cell.display_lines(u16::MAX).is_empty() {
self.record_visible_turn_activity();
}
// Keep the placeholder session header as the active cell until real session info arrives,
// so we can merge headers instead of committing a duplicate box to history.
let keep_placeholder_header_active = !self.is_session_configured()
@@ -1840,12 +1830,7 @@ impl ChatWidget {
}
pub(crate) fn prepare_local_op_submission(&mut self, op: &AppCommand) {
if let AppCommand::Interrupt { behavior } = op
&& self.turn_lifecycle.agent_turn_running
{
if *behavior == crate::app_command::InterruptBehavior::RestorePromptIfNoOutput {
self.arm_cancel_edit();
}
if matches!(op, AppCommand::Interrupt) && self.turn_lifecycle.agent_turn_running {
if let Some(controller) = self.stream_controller.as_mut() {
controller.clear_queue();
}