Avoid cloning deferred TUI lifecycle payloads (#34347)

## What changed

- Move lifecycle and approval payloads into either the interrupt queue or the
  immediate handler instead of cloning them for both branches.
- Borrow MCP elicitation request metadata while constructing the corresponding
  form, and consume apply-patch changes after collecting notification paths.

## Testing

- Add a history replay test confirming deferred MCP start and completion events
  remain FIFO after streaming finishes.

GitOrigin-RevId: 84ed34f6ff173463a1da016dc140405392c0ba45
This commit is contained in:
Charlie Marsh authored and copyberry committed 2026-07-20 13:54:11 +00:00
1 parent 2244d11a1d
commit ada5a79ddf
7 files changed
+111 -52

No files matched your search

@@ -1135,6 +1135,64 @@ async fn replayed_in_progress_mcp_tool_call_stays_active() {
assert!(!active.contains("MCP tool call completed without a result"));
}
#[tokio::test]
async fn deferred_mcp_lifecycle_events_keep_fifo_after_stream_finishes() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
let cwd = chat.config.cwd.to_path_buf();
chat.stream_controller = Some(crate::streaming::controller::StreamController::new(
/*width*/ Some(80),
cwd.as_path(),
chat.history_render_mode(),
));
chat.on_mcp_tool_call_started(AppServerThreadItem::McpToolCall {
id: "mcp-deferred".to_string(),
server: "copilot-bridge".to_string(),
tool: "copilot".to_string(),
status: codex_app_server_protocol::McpToolCallStatus::InProgress,
arguments: json!({"action": "wait"}),
app_context: None,
mcp_app_resource_uri: None,
plugin_id: None,
result: None,
error: None,
duration_ms: None,
});
assert!(!chat.interrupts.is_empty());
chat.stream_controller = None;
chat.on_mcp_tool_call_completed(AppServerThreadItem::McpToolCall {
id: "mcp-deferred".to_string(),
server: "copilot-bridge".to_string(),
tool: "copilot".to_string(),
status: codex_app_server_protocol::McpToolCallStatus::Completed,
arguments: json!({"action": "wait"}),
app_context: None,
mcp_app_resource_uri: None,
plugin_id: None,
result: Some(Box::new(codex_app_server_protocol::McpToolCallResult {
content: vec![json!({"type": "text", "text": "deferred result"})],
structured_content: None,
meta: None,
})),
error: None,
duration_ms: Some(5),
});
assert!(!chat.interrupts.is_empty());
assert!(drain_insert_history(&mut rx).is_empty());
chat.flush_interrupt_queue();
assert!(chat.interrupts.is_empty());
assert!(chat.transcript.active_cell.is_none());
let rendered = drain_insert_history(&mut rx)
.into_iter()
.map(|lines| lines_to_single_string(&lines))
.collect::<String>();
assert!(rendered.contains("deferred result"), "{rendered}");
}
#[tokio::test]
async fn live_reasoning_summary_is_not_rendered_twice_when_item_completes() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;