Avoid rendering sub-agent activity twice in the TUI (#39049)

## Why

The app server sends sub-agent activity in both item-started and item-completed
notifications, causing the TUI to add the same activity to history twice.

## What changed

Render live sub-agent activity when the item completes instead of also rendering
it when the item starts. Preserve the existing replay behavior.

## Testing

Add a regression test that sends matching started and completed notifications
and verifies that the activity produces one history cell.

GitOrigin-RevId: 76a66f3657fe297f37067ed672ab9642997b4499
This commit is contained in:
Benjamin Carlsson
2026-08-17 18:56:00 +00:00
committed by copyberry
parent 0c901fd141
commit 1a8bac9405
3 changed files with 40 additions and 1 deletions

View File

@@ -361,7 +361,6 @@ impl ChatWidget {
reasoning_effort,
agents_states,
}),
item @ ThreadItem::SubAgentActivity { .. } => self.on_sub_agent_activity(item),
ThreadItem::EnteredReviewMode { review, .. } if !from_replay => {
self.enter_review_mode_with_hint(review, /*from_replay*/ false);
}

View File

@@ -0,0 +1,5 @@
---
source: tui/src/chatwidget/tests.rs
expression: rendered
---
• Interacted with `/root/researcher`

View File

@@ -928,6 +928,41 @@ async fn live_app_server_command_output_delta_transcript_snapshot() {
);
}
#[tokio::test]
async fn live_app_server_sub_agent_activity_renders_once() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
let activity = AppServerThreadItem::SubAgentActivity {
id: "activity-1".to_string(),
kind: codex_app_server_protocol::SubAgentActivityKind::Interacted,
agent_thread_id: ThreadId::new().to_string(),
agent_path: "/root/researcher".to_string(),
};
chat.handle_server_notification(
ServerNotification::ItemStarted(ItemStartedNotification {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
started_at_ms: 0,
item: activity.clone(),
}),
/*replay_kind*/ None,
);
chat.handle_server_notification(
ServerNotification::ItemCompleted(ItemCompletedNotification {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: 0,
item: activity,
}),
/*replay_kind*/ None,
);
let cells = drain_insert_history(&mut rx);
assert_eq!(cells.len(), 1);
let rendered = lines_to_single_string(&cells[0]);
assert_chatwidget_snapshot!("app_server_sub_agent_activity_renders_once", rendered);
}
#[tokio::test]
async fn live_app_server_collab_wait_items_render_history() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;