Include completed commentary in the /copy picker (#43846)

## Why

During an active turn, `/copy` could still offer the previous final response even after a new commentary message had completed.

## What changed

Record every completed agent message with non-empty visible Markdown for copying, including commentary. The picker can then offer the latest completed commentary as a whole response and expose its code blocks.

## Testing

Add a regression test and picker snapshot covering completed commentary during an active turn after a previous final response. Verify that selecting “Whole response” copies the commentary and that the picker lists its shell code block.

GitOrigin-RevId: 0ea1d5e3952ccac21f2ee951d482619de82f28e0
This commit is contained in:
Benjamin Carlsson
2026-09-08 16:15:24 +00:00
committed by copyberry
parent b090e901f8
commit dd9512c000
4 changed files with 46 additions and 4 deletions
+1 -3
View File
@@ -355,9 +355,7 @@ impl ChatWidget {
} else {
self.finalize_completed_assistant_message(Some(parsed.visible_markdown.as_str()));
}
if matches!(item.phase, Some(MessagePhase::FinalAnswer) | None)
&& !parsed.visible_markdown.is_empty()
{
if !parsed.visible_markdown.is_empty() {
self.transcript
.record_agent_markdown(parsed.visible_markdown.clone(), message);
}
@@ -1840,6 +1840,40 @@ async fn slash_copy_state_tracks_turn_complete_final_reply() {
);
}
#[tokio::test]
async fn slash_copy_picker_uses_completed_commentary_during_active_turn() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
complete_turn_with_message(&mut chat, "turn-1", Some("Previous final response"));
handle_turn_started(&mut chat, "active");
let commentary = "Here is the command:\n\n```sh\necho current\n```";
let current_thread_id = chat.thread_id.map(|id| id.to_string()).unwrap_or_default();
chat.handle_server_notification(
ServerNotification::ItemCompleted(ItemCompletedNotification {
thread_id: current_thread_id,
turn_id: "active".to_string(),
completed_at_ms: 0,
item: AppServerThreadItem::AgentMessage {
id: "active-commentary".to_string(),
text: commentary.to_string(),
phase: Some(MessagePhase::Commentary),
memory_citation: None,
delivery: None,
questions: None,
},
}),
/*replay_kind*/ None,
);
while rx.try_recv().is_ok() {}
chat.dispatch_command(SlashCommand::Copy);
insta::assert_snapshot!(render_bottom_popup(&chat, /*width*/ 80));
chat.handle_key_event(KeyEvent::new(KeyCode::Char('1'), KeyModifiers::NONE));
assert_eq!(
next_copy_selection(&mut rx),
(commentary.to_string(), "Whole response".to_string(),)
);
}
#[tokio::test]
async fn slash_copy_state_tracks_plan_item_completion() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
@@ -0,0 +1,10 @@
---
source: tui/src/chatwidget/tests/slash_commands.rs
expression: "render_bottom_popup(&chat, 80)"
---
Copy to clipboard
› 1. Whole response Here is the command:
2. sh code echo current
Press enter to confirm or esc to go back
+1 -1
View File
@@ -38,7 +38,7 @@ pub(super) struct TranscriptState {
pub(super) active_cell_revision: u64,
/// One bounded entry shared by layout and paint across unchanged active-cell frames.
pub(super) active_cell_layout: Cell<Option<ActiveCellLayoutCache>>,
/// Markdown of the most recently completed agent response for whole-response copying.
/// Markdown of the most recently completed agent message for whole-response copying.
pub(super) last_agent_markdown: Option<String>,
/// Original source of that response, before display sanitization, for exact block copying.
pub(super) last_agent_source: Option<String>,