diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index dec9d8c089..be61d1772f 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -89,6 +89,7 @@ pub(crate) async fn apply_bespoke_event_handling( let Event { id: event_turn_id, msg, + .. } = event; match msg { EventMsg::TaskComplete(_ev) => { diff --git a/codex-rs/core/src/codex_delegate.rs b/codex-rs/core/src/codex_delegate.rs index c7aebbaf92..18e0a69424 100644 --- a/codex-rs/core/src/codex_delegate.rs +++ b/codex-rs/core/src/codex_delegate.rs @@ -182,14 +182,17 @@ async fn forward_events( // ignore all legacy delta events Event { id: _, + agent_id: _, msg: EventMsg::AgentMessageDelta(_) | EventMsg::AgentReasoningDelta(_), } => {} Event { id: _, + agent_id: _, msg: EventMsg::SessionConfigured(_), } => {} Event { id, + agent_id: _, msg: EventMsg::ExecApprovalRequest(event), } => { // Initiate approval via parent session; do not surface to consumer. @@ -205,6 +208,7 @@ async fn forward_events( } Event { id, + agent_id: _, msg: EventMsg::ApplyPatchApprovalRequest(event), } => { handle_patch_approval( @@ -372,6 +376,7 @@ mod tests { tx_out .send(Event { id: "full".to_string(), + agent_id: None, msg: EventMsg::TurnAborted(TurnAbortedEvent { reason: TurnAbortReason::Interrupted, }), @@ -391,6 +396,7 @@ mod tests { tx_events .send(Event { id: "evt".to_string(), + agent_id: None, msg: EventMsg::RawResponseItem(RawResponseItemEvent { item: ResponseItem::CustomToolCall { id: None, diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs index 1a90b7b223..1886710361 100644 --- a/codex-rs/core/src/compact.rs +++ b/codex-rs/core/src/compact.rs @@ -68,7 +68,7 @@ async fn run_compact_task_inner( ) { let initial_input_for_turn: ResponseInputItem = ResponseInputItem::from(input); - let mut history = sess.clone_history().await; + let mut history = sess.clone_history(&turn_context.agent_id).await; history.record_items( &[initial_input_for_turn.into()], turn_context.truncation_policy, @@ -87,7 +87,8 @@ async fn run_compact_task_inner( effort: turn_context.client.get_reasoning_effort(), summary: turn_context.client.get_reasoning_summary(), }); - sess.persist_rollout_items(&[rollout_item]).await; + sess.persist_rollout_items(&turn_context.agent_id, &[rollout_item]) + .await; loop { let turn_input = history.get_history_for_prompt(); @@ -150,7 +151,10 @@ async fn run_compact_task_inner( } } - let history_snapshot = sess.clone_history().await.get_history(); + let history_snapshot = sess + .clone_history(&turn_context.agent_id) + .await + .get_history(); let summary_suffix = get_last_assistant_message_from_turn(&history_snapshot).unwrap_or_default(); let summary_text = format!("{SUMMARY_PREFIX}\n{summary_suffix}"); @@ -164,14 +168,16 @@ async fn run_compact_task_inner( .cloned() .collect(); new_history.extend(ghost_snapshots); - sess.replace_history(new_history).await; + sess.replace_history(&turn_context.agent_id, new_history) + .await; sess.recompute_token_usage(&turn_context).await; let rollout_item = RolloutItem::Compacted(CompactedItem { message: summary_text.clone(), replacement_history: None, }); - sess.persist_rollout_items(&[rollout_item]).await; + sess.persist_rollout_items(&turn_context.agent_id, &[rollout_item]) + .await; let event = EventMsg::ContextCompacted(ContextCompactedEvent {}); sess.send_event(&turn_context, event).await; diff --git a/codex-rs/core/src/compact_remote.rs b/codex-rs/core/src/compact_remote.rs index b855f28d39..bf0eb111d1 100644 --- a/codex-rs/core/src/compact_remote.rs +++ b/codex-rs/core/src/compact_remote.rs @@ -40,7 +40,7 @@ async fn run_remote_compact_task_inner_impl( sess: &Arc, turn_context: &Arc, ) -> CodexResult<()> { - let mut history = sess.clone_history().await; + let mut history = sess.clone_history(&turn_context.agent_id).await; let prompt = Prompt { input: history.get_history_for_prompt(), tools: vec![], @@ -64,15 +64,19 @@ async fn run_remote_compact_task_inner_impl( if !ghost_snapshots.is_empty() { new_history.extend(ghost_snapshots); } - sess.replace_history(new_history.clone()).await; + sess.replace_history(&turn_context.agent_id, new_history.clone()) + .await; sess.recompute_token_usage(turn_context).await; let compacted_item = CompactedItem { message: String::new(), replacement_history: Some(new_history), }; - sess.persist_rollout_items(&[RolloutItem::Compacted(compacted_item)]) - .await; + sess.persist_rollout_items( + &turn_context.agent_id, + &[RolloutItem::Compacted(compacted_item)], + ) + .await; let event = EventMsg::ContextCompacted(ContextCompactedEvent {}); sess.send_event(turn_context, event).await; diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index da113ae42d..415a54a13e 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -20,6 +20,7 @@ use crate::error::CodexErr; use crate::error::Result; use crate::error::SandboxErr; use crate::get_platform_sandbox; +use crate::protocol::AgentId; use crate::protocol::Event; use crate::protocol::EventMsg; use crate::protocol::ExecCommandOutputDeltaEvent; @@ -123,6 +124,7 @@ pub enum SandboxType { #[derive(Clone)] pub struct StdoutStream { pub sub_id: String, + pub agent_id: AgentId, pub call_id: String, pub tx_event: Sender, } @@ -710,6 +712,7 @@ async fn read_capped( }); let event = Event { id: stream.sub_id.clone(), + agent_id: Some(stream.agent_id.clone()), msg, }; #[allow(clippy::let_unit_value)] diff --git a/codex-rs/core/src/mcp_connection_manager.rs b/codex-rs/core/src/mcp_connection_manager.rs index 3213b22b71..6a4bb81f27 100644 --- a/codex-rs/core/src/mcp_connection_manager.rs +++ b/codex-rs/core/src/mcp_connection_manager.rs @@ -159,6 +159,7 @@ impl ElicitationRequestManager { let _ = tx_event .send(Event { id: "mcp_elicitation_request".to_string(), + agent_id: None, msg: EventMsg::ElicitationRequest(ElicitationRequestEvent { server_name, id, @@ -370,6 +371,7 @@ impl McpConnectionManager { let _ = tx_event .send(Event { id: INITIAL_SUBMIT_ID.to_owned(), + agent_id: None, msg: EventMsg::McpStartupComplete(summary), }) .await; @@ -661,6 +663,7 @@ async fn emit_update( tx_event .send(Event { id: INITIAL_SUBMIT_ID.to_owned(), + agent_id: None, msg: EventMsg::McpStartupUpdate(update), }) .await diff --git a/codex-rs/core/src/message_history.rs b/codex-rs/core/src/message_history.rs index ecc6851336..5aac0bab8b 100644 --- a/codex-rs/core/src/message_history.rs +++ b/codex-rs/core/src/message_history.rs @@ -5,7 +5,7 @@ //! JSON-Lines tooling. Each record has the following schema: //! //! ````text -//! {"conversation_id":"","ts":,"text":""} +//! {"conversation_id":"","agent_id":"","ts":,"text":""} //! ```` //! //! To minimise the chance of interleaved writes when multiple processes are @@ -54,6 +54,8 @@ const RETRY_SLEEP: Duration = Duration::from_millis(100); #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct HistoryEntry { pub session_id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub agent_id: Option, pub ts: u64, pub text: String, } @@ -69,6 +71,7 @@ fn history_filepath(config: &Config) -> PathBuf { /// which entails a small amount of blocking I/O internally. pub(crate) async fn append_entry( text: &str, + agent_id: Option<&str>, conversation_id: &ConversationId, config: &Config, ) -> Result<()> { @@ -99,6 +102,7 @@ pub(crate) async fn append_entry( // Construct the JSON line first so we can write it in a single syscall. let entry = HistoryEntry { session_id: conversation_id.to_string(), + agent_id: agent_id.map(str::to_string), ts, text: text.to_string(), }; @@ -418,11 +422,13 @@ mod tests { let entries = vec![ HistoryEntry { session_id: "first-session".to_string(), + agent_id: None, ts: 1, text: "first".to_string(), }, HistoryEntry { session_id: "second-session".to_string(), + agent_id: None, ts: 2, text: "second".to_string(), }, @@ -453,11 +459,13 @@ mod tests { let initial = HistoryEntry { session_id: "first-session".to_string(), + agent_id: None, ts: 1, text: "first".to_string(), }; let appended = HistoryEntry { session_id: "second-session".to_string(), + agent_id: None, ts: 2, text: "second".to_string(), }; @@ -507,7 +515,7 @@ mod tests { let history_path = codex_home.path().join("history.jsonl"); - append_entry(&entry_one, &conversation_id, &config) + append_entry(&entry_one, None, &conversation_id, &config) .await .expect("write first entry"); @@ -517,7 +525,7 @@ mod tests { config.history.max_bytes = Some(usize::try_from(limit_bytes).expect("limit should fit into usize")); - append_entry(&entry_two, &conversation_id, &config) + append_entry(&entry_two, None, &conversation_id, &config) .await .expect("write second entry"); @@ -555,13 +563,13 @@ mod tests { let history_path = codex_home.path().join("history.jsonl"); - append_entry(&short_entry, &conversation_id, &config) + append_entry(&short_entry, None, &conversation_id, &config) .await .expect("write first entry"); let short_entry_len = std::fs::metadata(&history_path).expect("metadata").len(); - append_entry(&long_entry, &conversation_id, &config) + append_entry(&long_entry, None, &conversation_id, &config) .await .expect("write second entry"); @@ -576,7 +584,7 @@ mod tests { .expect("max bytes should fit into usize"), ); - append_entry(&long_entry, &conversation_id, &config) + append_entry(&long_entry, None, &conversation_id, &config) .await .expect("write third entry"); diff --git a/codex-rs/core/src/rollout/tests.rs b/codex-rs/core/src/rollout/tests.rs index 1df3659ba0..ce8d409841 100644 --- a/codex-rs/core/src/rollout/tests.rs +++ b/codex-rs/core/src/rollout/tests.rs @@ -582,6 +582,7 @@ async fn test_updated_at_uses_file_mtime() -> Result<()> { let conversation_id = ConversationId::from_string(&uuid.to_string())?; let meta_line = RolloutLine { timestamp: ts.to_string(), + agent_id: None, item: RolloutItem::SessionMeta(SessionMetaLine { meta: SessionMeta { id: conversation_id, @@ -600,6 +601,7 @@ async fn test_updated_at_uses_file_mtime() -> Result<()> { let user_event_line = RolloutLine { timestamp: ts.to_string(), + agent_id: None, item: RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent { message: "hello".into(), images: None, @@ -611,6 +613,7 @@ async fn test_updated_at_uses_file_mtime() -> Result<()> { for idx in 0..total_messages { let response_line = RolloutLine { timestamp: format!("{ts}-{idx:02}"), + agent_id: None, item: RolloutItem::ResponseItem(ResponseItem::Message { id: None, role: "assistant".into(), diff --git a/codex-rs/core/src/tasks/undo.rs b/codex-rs/core/src/tasks/undo.rs index 5da7edd16f..83e24e24a7 100644 --- a/codex-rs/core/src/tasks/undo.rs +++ b/codex-rs/core/src/tasks/undo.rs @@ -59,7 +59,7 @@ impl SessionTask for UndoTask { return None; } - let mut history = sess.clone_history().await; + let mut history = sess.clone_history(&ctx.agent_id).await; let mut items = history.get_history(); let mut completed = UndoCompletedEvent { success: false, @@ -96,7 +96,7 @@ impl SessionTask for UndoTask { match restore_result { Ok(Ok(())) => { items.remove(idx); - sess.replace_history(items).await; + sess.replace_history(&ctx.agent_id, items).await; let short_id: String = commit_id.chars().take(7).collect(); info!(commit_id = commit_id, "Undo restored ghost snapshot"); completed.success = true; diff --git a/codex-rs/core/src/tasks/user_shell.rs b/codex-rs/core/src/tasks/user_shell.rs index aec09514ca..ed51120a70 100644 --- a/codex-rs/core/src/tasks/user_shell.rs +++ b/codex-rs/core/src/tasks/user_shell.rs @@ -108,6 +108,7 @@ impl SessionTask for UserShellCommandTask { let stdout_stream = Some(StdoutStream { sub_id: turn_context.sub_id.clone(), + agent_id: turn_context.agent_id.clone(), call_id: call_id.clone(), tx_event: session.get_tx_event(), }); diff --git a/codex-rs/core/src/tools/handlers/view_image.rs b/codex-rs/core/src/tools/handlers/view_image.rs index 6b308c0944..0f0a7c32d0 100644 --- a/codex-rs/core/src/tools/handlers/view_image.rs +++ b/codex-rs/core/src/tools/handlers/view_image.rs @@ -65,7 +65,10 @@ impl ToolHandler for ViewImageHandler { let event_path = abs_path.clone(); session - .inject_input(vec![UserInput::LocalImage { path: abs_path }]) + .inject_input( + &turn.agent_id, + vec![UserInput::LocalImage { path: abs_path }], + ) .await .map_err(|_| { FunctionCallError::RespondToModel( diff --git a/codex-rs/core/src/tools/runtimes/apply_patch.rs b/codex-rs/core/src/tools/runtimes/apply_patch.rs index 26d04f578c..62f2a08f49 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch.rs @@ -71,6 +71,7 @@ impl ApplyPatchRuntime { fn stdout_stream(ctx: &ToolCtx<'_>) -> Option { Some(crate::exec::StdoutStream { sub_id: ctx.turn.sub_id.clone(), + agent_id: ctx.turn.agent_id.clone(), call_id: ctx.call_id.clone(), tx_event: ctx.session.get_tx_event(), }) diff --git a/codex-rs/core/src/tools/runtimes/shell.rs b/codex-rs/core/src/tools/runtimes/shell.rs index 2c6c6cd29c..9d9cf3789c 100644 --- a/codex-rs/core/src/tools/runtimes/shell.rs +++ b/codex-rs/core/src/tools/runtimes/shell.rs @@ -53,6 +53,7 @@ impl ShellRuntime { fn stdout_stream(ctx: &ToolCtx<'_>) -> Option { Some(crate::exec::StdoutStream { sub_id: ctx.turn.sub_id.clone(), + agent_id: ctx.turn.agent_id.clone(), call_id: ctx.call_id.clone(), tx_event: ctx.session.get_tx_event(), }) diff --git a/codex-rs/core/tests/suite/resume_warning.rs b/codex-rs/core/tests/suite/resume_warning.rs index 4b6a133150..9751364760 100644 --- a/codex-rs/core/tests/suite/resume_warning.rs +++ b/codex-rs/core/tests/suite/resume_warning.rs @@ -9,6 +9,7 @@ use codex_core::protocol::EventMsg; use codex_core::protocol::InitialHistory; use codex_core::protocol::ResumedHistory; use codex_core::protocol::RolloutItem; +use codex_core::protocol::RolloutLine; use codex_core::protocol::TurnContextItem; use codex_core::protocol::WarningEvent; use codex_protocol::ConversationId; @@ -33,7 +34,11 @@ fn resume_history( InitialHistory::Resumed(ResumedHistory { conversation_id: ConversationId::default(), - history: vec![RolloutItem::TurnContext(turn_ctx)], + history: vec![RolloutLine { + timestamp: String::new(), + agent_id: None, + item: RolloutItem::TurnContext(turn_ctx), + }], rollout_path: rollout_path.to_path_buf(), }) } diff --git a/codex-rs/exec/src/event_processor_with_human_output.rs b/codex-rs/exec/src/event_processor_with_human_output.rs index a43718d569..69999e6837 100644 --- a/codex-rs/exec/src/event_processor_with_human_output.rs +++ b/codex-rs/exec/src/event_processor_with_human_output.rs @@ -160,7 +160,7 @@ impl EventProcessor for EventProcessorWithHumanOutput { } fn process_event(&mut self, event: Event) -> CodexStatus { - let Event { id: _, msg } = event; + let Event { id: _, msg, .. } = event; match msg { EventMsg::Error(ErrorEvent { message, .. }) => { let prefix = "ERROR:".style(self.red); diff --git a/codex-rs/exec/src/event_processor_with_jsonl_output.rs b/codex-rs/exec/src/event_processor_with_jsonl_output.rs index 03c51662b1..5b59e8dce6 100644 --- a/codex-rs/exec/src/event_processor_with_jsonl_output.rs +++ b/codex-rs/exec/src/event_processor_with_jsonl_output.rs @@ -510,6 +510,7 @@ impl EventProcessor for EventProcessorWithJsonOutput { fn print_config_summary(&mut self, _: &Config, _: &str, ev: &SessionConfiguredEvent) { self.process_event(Event { id: "".to_string(), + agent_id: None, msg: EventMsg::SessionConfigured(ev.clone()), }); } diff --git a/codex-rs/exec/tests/event_processor_with_json_output.rs b/codex-rs/exec/tests/event_processor_with_json_output.rs index 2b3673f5a6..24c666019e 100644 --- a/codex-rs/exec/tests/event_processor_with_json_output.rs +++ b/codex-rs/exec/tests/event_processor_with_json_output.rs @@ -61,6 +61,7 @@ use std::time::Duration; fn event(id: &str, msg: EventMsg) -> Event { Event { id: id.to_string(), + agent_id: None, msg, } } diff --git a/codex-rs/mcp-server/src/codex_tool_runner.rs b/codex-rs/mcp-server/src/codex_tool_runner.rs index 39ae7486e5..82bffe1dfd 100644 --- a/codex-rs/mcp-server/src/codex_tool_runner.rs +++ b/codex-rs/mcp-server/src/codex_tool_runner.rs @@ -68,6 +68,7 @@ pub async fn run_codex_tool_session( let session_configured_event = Event { // Use a fake id value for now. id: "".to_string(), + agent_id: None, msg: EventMsg::SessionConfigured(session_configured.clone()), }; outgoing diff --git a/codex-rs/mcp-server/src/outgoing_message.rs b/codex-rs/mcp-server/src/outgoing_message.rs index 83ac25fdfd..95adb2bb24 100644 --- a/codex-rs/mcp-server/src/outgoing_message.rs +++ b/codex-rs/mcp-server/src/outgoing_message.rs @@ -255,6 +255,7 @@ mod tests { let rollout_file = NamedTempFile::new()?; let event = Event { id: "1".to_string(), + agent_id: None, msg: EventMsg::SessionConfigured(SessionConfiguredEvent { session_id: conversation_id, model: "gpt-4o".to_string(), @@ -309,6 +310,7 @@ mod tests { }; let event = Event { id: "1".to_string(), + agent_id: None, msg: EventMsg::SessionConfigured(session_configured_event.clone()), }; let meta = OutgoingNotificationMeta { diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 66e12f086f..ec0da52473 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -1519,6 +1519,7 @@ mod tests { app.chat_widget.handle_codex_event(Event { id: String::new(), + agent_id: None, msg: EventMsg::SessionConfigured(event), }); diff --git a/codex-rs/tui/src/chatwidget/agent.rs b/codex-rs/tui/src/chatwidget/agent.rs index 240972347f..f6e729b43d 100644 --- a/codex-rs/tui/src/chatwidget/agent.rs +++ b/codex-rs/tui/src/chatwidget/agent.rs @@ -36,6 +36,7 @@ pub(crate) fn spawn_agent( eprintln!("{message}"); app_event_tx_clone.send(AppEvent::CodexEvent(Event { id: "".to_string(), + agent_id: None, msg: EventMsg::Error(err.to_error_event(None)), })); app_event_tx_clone.send(AppEvent::ExitRequest); @@ -48,6 +49,7 @@ pub(crate) fn spawn_agent( let ev = codex_core::protocol::Event { // The `id` does not matter for rendering, so we can use a fake value. id: "".to_string(), + agent_id: None, msg: codex_core::protocol::EventMsg::SessionConfigured(session_configured), }; app_event_tx_clone.send(AppEvent::CodexEvent(ev)); @@ -85,6 +87,7 @@ pub(crate) fn spawn_agent_from_existing( // Forward the captured `SessionConfigured` event so it can be rendered in the UI. let ev = codex_core::protocol::Event { id: "".to_string(), + agent_id: None, msg: codex_core::protocol::EventMsg::SessionConfigured(session_configured), }; app_event_tx_clone.send(AppEvent::CodexEvent(ev)); diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 3d055c9f7e..a2b853ce64 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -128,6 +128,7 @@ fn resumed_initial_messages_render_history() { chat.handle_codex_event(Event { id: "initial".into(), + agent_id: None, msg: EventMsg::SessionConfigured(configured), }); @@ -160,6 +161,7 @@ fn entered_review_mode_uses_request_hint() { chat.handle_codex_event(Event { id: "review-start".into(), + agent_id: None, msg: EventMsg::EnteredReviewMode(ReviewRequest { target: ReviewTarget::BaseBranch { branch: "feature".to_string(), @@ -181,6 +183,7 @@ fn entered_review_mode_defaults_to_current_changes_banner() { chat.handle_codex_event(Event { id: "review-start".into(), + agent_id: None, msg: EventMsg::EnteredReviewMode(ReviewRequest { target: ReviewTarget::UncommittedChanges, user_facing_hint: None, @@ -204,6 +207,7 @@ fn review_restores_context_window_indicator() { chat.handle_codex_event(Event { id: "token-before".into(), + agent_id: None, msg: EventMsg::TokenCount(TokenCountEvent { info: Some(make_token_info(pre_review_tokens, context_window)), rate_limits: None, @@ -213,6 +217,7 @@ fn review_restores_context_window_indicator() { chat.handle_codex_event(Event { id: "review-start".into(), + agent_id: None, msg: EventMsg::EnteredReviewMode(ReviewRequest { target: ReviewTarget::BaseBranch { branch: "feature".to_string(), @@ -223,6 +228,7 @@ fn review_restores_context_window_indicator() { chat.handle_codex_event(Event { id: "token-review".into(), + agent_id: None, msg: EventMsg::TokenCount(TokenCountEvent { info: Some(make_token_info(review_tokens, context_window)), rate_limits: None, @@ -232,6 +238,7 @@ fn review_restores_context_window_indicator() { chat.handle_codex_event(Event { id: "review-end".into(), + agent_id: None, msg: EventMsg::ExitedReviewMode(ExitedReviewModeEvent { review_output: None, }), @@ -252,6 +259,7 @@ fn token_count_none_resets_context_indicator() { chat.handle_codex_event(Event { id: "token-before".into(), + agent_id: None, msg: EventMsg::TokenCount(TokenCountEvent { info: Some(make_token_info(pre_compact_tokens, context_window)), rate_limits: None, @@ -261,6 +269,7 @@ fn token_count_none_resets_context_indicator() { chat.handle_codex_event(Event { id: "token-cleared".into(), + agent_id: None, msg: EventMsg::TokenCount(TokenCountEvent { info: None, rate_limits: None, @@ -291,6 +300,7 @@ fn context_indicator_shows_used_tokens_when_window_unknown() { chat.handle_codex_event(Event { id: "token-usage".into(), + agent_id: None, msg: EventMsg::TokenCount(TokenCountEvent { info: Some(token_info), rate_limits: None, @@ -727,6 +737,7 @@ fn exec_approval_emits_proposed_command_and_decision_history() { }; chat.handle_codex_event(Event { id: "sub-short".into(), + agent_id: None, msg: EventMsg::ExecApprovalRequest(ev), }); @@ -771,6 +782,7 @@ fn exec_approval_decision_truncates_multiline_and_long_commands() { }; chat.handle_codex_event(Event { id: "sub-multi".into(), + agent_id: None, msg: EventMsg::ExecApprovalRequest(ev_multi), }); let proposed_multi = drain_insert_history(&mut rx); @@ -821,6 +833,7 @@ fn exec_approval_decision_truncates_multiline_and_long_commands() { }; chat.handle_codex_event(Event { id: "sub-long".into(), + agent_id: None, msg: EventMsg::ExecApprovalRequest(ev_long), }); let proposed_long = drain_insert_history(&mut rx); @@ -863,6 +876,7 @@ fn begin_exec_with_source( }; chat.handle_codex_event(Event { id: call_id.to_string(), + agent_id: None, msg: EventMsg::ExecCommandBegin(event.clone()), }); event @@ -896,6 +910,7 @@ fn end_exec( } = begin_event; chat.handle_codex_event(Event { id: call_id.clone(), + agent_id: None, msg: EventMsg::ExecCommandEnd(ExecCommandEndEvent { call_id, process_id, @@ -1151,6 +1166,7 @@ fn exec_end_without_begin_uses_event_command() { let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); chat.handle_codex_event(Event { id: "call-orphan".to_string(), + agent_id: None, msg: EventMsg::ExecCommandEnd(ExecCommandEndEvent { call_id: "call-orphan".to_string(), process_id: None, @@ -1389,6 +1405,7 @@ fn undo_success_events_render_info_messages() { chat.handle_codex_event(Event { id: "turn-1".to_string(), + agent_id: None, msg: EventMsg::UndoStarted(UndoStartedEvent { message: Some("Undo requested for the last turn...".to_string()), }), @@ -1400,6 +1417,7 @@ fn undo_success_events_render_info_messages() { chat.handle_codex_event(Event { id: "turn-1".to_string(), + agent_id: None, msg: EventMsg::UndoCompleted(UndoCompletedEvent { success: true, message: None, @@ -1426,6 +1444,7 @@ fn undo_failure_events_render_error_message() { chat.handle_codex_event(Event { id: "turn-2".to_string(), + agent_id: None, msg: EventMsg::UndoStarted(UndoStartedEvent { message: None }), }); assert!( @@ -1435,6 +1454,7 @@ fn undo_failure_events_render_error_message() { chat.handle_codex_event(Event { id: "turn-2".to_string(), + agent_id: None, msg: EventMsg::UndoCompleted(UndoCompletedEvent { success: false, message: Some("Failed to restore workspace state.".to_string()), @@ -1461,6 +1481,7 @@ fn undo_started_hides_interrupt_hint() { chat.handle_codex_event(Event { id: "turn-hint".to_string(), + agent_id: None, msg: EventMsg::UndoStarted(UndoStartedEvent { message: None }), }); @@ -1584,6 +1605,7 @@ fn view_image_tool_call_adds_history_cell() { chat.handle_codex_event(Event { id: "sub-image".into(), + agent_id: None, msg: EventMsg::ViewImageToolCall(ViewImageToolCallEvent { call_id: "call-image".into(), path: image_path, @@ -1609,6 +1631,7 @@ fn interrupt_exec_marks_failed_snapshot() { // cause the active exec cell to be finalized as failed and flushed. chat.handle_codex_event(Event { id: "call-int".into(), + agent_id: None, msg: EventMsg::TurnAborted(codex_core::protocol::TurnAbortedEvent { reason: TurnAbortReason::Interrupted, }), @@ -1634,6 +1657,7 @@ fn interrupted_turn_error_message_snapshot() { // Simulate an in-progress task so the widget is in a running state. chat.handle_codex_event(Event { id: "task-1".into(), + agent_id: None, msg: EventMsg::TaskStarted(TaskStartedEvent { model_context_window: None, }), @@ -1642,6 +1666,7 @@ fn interrupted_turn_error_message_snapshot() { // Abort the turn (like pressing Esc) and drain inserted history. chat.handle_codex_event(Event { id: "task-1".into(), + agent_id: None, msg: EventMsg::TurnAborted(codex_core::protocol::TurnAbortedEvent { reason: TurnAbortReason::Interrupted, }), @@ -2286,6 +2311,7 @@ fn approval_modal_exec_snapshot() -> anyhow::Result<()> { }; chat.handle_codex_event(Event { id: "sub-approve".into(), + agent_id: None, msg: EventMsg::ExecApprovalRequest(ev), }); // Render to a fixed-size test terminal and snapshot. @@ -2339,6 +2365,7 @@ fn approval_modal_exec_without_reason_snapshot() -> anyhow::Result<()> { }; chat.handle_codex_event(Event { id: "sub-approve-noreason".into(), + agent_id: None, msg: EventMsg::ExecApprovalRequest(ev), }); @@ -2381,6 +2408,7 @@ fn approval_modal_patch_snapshot() -> anyhow::Result<()> { }; chat.handle_codex_event(Event { id: "sub-approve-patch".into(), + agent_id: None, msg: EventMsg::ApplyPatchApprovalRequest(ev), }); @@ -2417,6 +2445,7 @@ fn interrupt_restores_queued_messages_into_composer() { // Deliver a TurnAborted event with Interrupted reason (as if Esc was pressed). chat.handle_codex_event(Event { id: "turn-1".into(), + agent_id: None, msg: EventMsg::TurnAborted(codex_core::protocol::TurnAbortedEvent { reason: TurnAbortReason::Interrupted, }), @@ -2455,6 +2484,7 @@ fn interrupt_prepends_queued_messages_before_existing_composer_text() { chat.handle_codex_event(Event { id: "turn-1".into(), + agent_id: None, msg: EventMsg::TurnAborted(codex_core::protocol::TurnAbortedEvent { reason: TurnAbortReason::Interrupted, }), @@ -2500,12 +2530,14 @@ fn ui_snapshots_small_heights_task_running() { // Activate status line chat.handle_codex_event(Event { id: "task-1".into(), + agent_id: None, msg: EventMsg::TaskStarted(TaskStartedEvent { model_context_window: None, }), }); chat.handle_codex_event(Event { id: "task-1".into(), + agent_id: None, msg: EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent { delta: "**Thinking**".into(), }), @@ -2531,6 +2563,7 @@ fn status_widget_and_approval_modal_snapshot() { // Begin a running task so the status indicator would be active. chat.handle_codex_event(Event { id: "task-1".into(), + agent_id: None, msg: EventMsg::TaskStarted(TaskStartedEvent { model_context_window: None, }), @@ -2538,6 +2571,7 @@ fn status_widget_and_approval_modal_snapshot() { // Provide a deterministic header for the status line. chat.handle_codex_event(Event { id: "task-1".into(), + agent_id: None, msg: EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent { delta: "**Analyzing**".into(), }), @@ -2560,6 +2594,7 @@ fn status_widget_and_approval_modal_snapshot() { }; chat.handle_codex_event(Event { id: "sub-approve-exec".into(), + agent_id: None, msg: EventMsg::ExecApprovalRequest(ev), }); @@ -2583,6 +2618,7 @@ fn status_widget_active_snapshot() { // Activate the status indicator by simulating a task start. chat.handle_codex_event(Event { id: "task-1".into(), + agent_id: None, msg: EventMsg::TaskStarted(TaskStartedEvent { model_context_window: None, }), @@ -2590,6 +2626,7 @@ fn status_widget_active_snapshot() { // Provide a deterministic header via a bold reasoning chunk. chat.handle_codex_event(Event { id: "task-1".into(), + agent_id: None, msg: EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent { delta: "**Analyzing**".into(), }), @@ -2611,6 +2648,7 @@ fn mcp_startup_header_booting_snapshot() { chat.handle_codex_event(Event { id: "mcp-1".into(), + agent_id: None, msg: EventMsg::McpStartupUpdate(McpStartupUpdateEvent { server: "alpha".into(), status: McpStartupStatus::Starting, @@ -2632,6 +2670,7 @@ fn background_event_updates_status_header() { chat.handle_codex_event(Event { id: "bg-1".into(), + agent_id: None, msg: EventMsg::BackgroundEvent(BackgroundEventEvent { message: "Waiting for `vim`".to_string(), }), @@ -2663,6 +2702,7 @@ fn apply_patch_events_emit_history_cells() { }; chat.handle_codex_event(Event { id: "s1".into(), + agent_id: None, msg: EventMsg::ApplyPatchApprovalRequest(ev), }); let cells = drain_insert_history(&mut rx); @@ -2703,6 +2743,7 @@ fn apply_patch_events_emit_history_cells() { }; chat.handle_codex_event(Event { id: "s1".into(), + agent_id: None, msg: EventMsg::PatchApplyBegin(begin), }); let cells = drain_insert_history(&mut rx); @@ -2731,6 +2772,7 @@ fn apply_patch_events_emit_history_cells() { }; chat.handle_codex_event(Event { id: "s1".into(), + agent_id: None, msg: EventMsg::PatchApplyEnd(end), }); let cells = drain_insert_history(&mut rx); @@ -2753,6 +2795,7 @@ fn apply_patch_manual_approval_adjusts_header() { ); chat.handle_codex_event(Event { id: "s1".into(), + agent_id: None, msg: EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent { call_id: "c1".into(), turn_id: "turn-c1".into(), @@ -2772,6 +2815,7 @@ fn apply_patch_manual_approval_adjusts_header() { ); chat.handle_codex_event(Event { id: "s1".into(), + agent_id: None, msg: EventMsg::PatchApplyBegin(PatchApplyBeginEvent { call_id: "c1".into(), turn_id: "turn-c1".into(), @@ -2802,6 +2846,7 @@ fn apply_patch_manual_flow_snapshot() { ); chat.handle_codex_event(Event { id: "s1".into(), + agent_id: None, msg: EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent { call_id: "c1".into(), turn_id: "turn-c1".into(), @@ -2825,6 +2870,7 @@ fn apply_patch_manual_flow_snapshot() { ); chat.handle_codex_event(Event { id: "s1".into(), + agent_id: None, msg: EventMsg::PatchApplyBegin(PatchApplyBeginEvent { call_id: "c1".into(), turn_id: "turn-c1".into(), @@ -2862,6 +2908,7 @@ fn apply_patch_approval_sends_op_with_submission_id() { }; chat.handle_codex_event(Event { id: "sub-123".into(), + agent_id: None, msg: EventMsg::ApplyPatchApprovalRequest(ev), }); @@ -2893,6 +2940,7 @@ fn apply_patch_full_flow_integration_like() { ); chat.handle_codex_event(Event { id: "sub-xyz".into(), + agent_id: None, msg: EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent { call_id: "call-1".into(), turn_id: "turn-call-1".into(), @@ -2934,6 +2982,7 @@ fn apply_patch_full_flow_integration_like() { ); chat.handle_codex_event(Event { id: "sub-xyz".into(), + agent_id: None, msg: EventMsg::PatchApplyBegin(PatchApplyBeginEvent { call_id: "call-1".into(), turn_id: "turn-call-1".into(), @@ -2948,6 +2997,7 @@ fn apply_patch_full_flow_integration_like() { ); chat.handle_codex_event(Event { id: "sub-xyz".into(), + agent_id: None, msg: EventMsg::PatchApplyEnd(PatchApplyEndEvent { call_id: "call-1".into(), turn_id: "turn-call-1".into(), @@ -2973,6 +3023,7 @@ fn apply_patch_untrusted_shows_approval_modal() -> anyhow::Result<()> { ); chat.handle_codex_event(Event { id: "sub-1".into(), + agent_id: None, msg: EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent { call_id: "call-1".into(), turn_id: "turn-call-1".into(), @@ -3024,6 +3075,7 @@ fn apply_patch_request_shows_diff_summary() -> anyhow::Result<()> { ); chat.handle_codex_event(Event { id: "sub-apply".into(), + agent_id: None, msg: EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent { call_id: "call-apply".into(), turn_id: "turn-apply".into(), @@ -3096,6 +3148,7 @@ fn plan_update_renders_history_cell() { }; chat.handle_codex_event(Event { id: "sub-1".into(), + agent_id: None, msg: EventMsg::PlanUpdate(update), }); let cells = drain_insert_history(&mut rx); @@ -3117,6 +3170,7 @@ fn stream_error_updates_status_indicator() { let msg = "Reconnecting... 2/5"; chat.handle_codex_event(Event { id: "sub-1".into(), + agent_id: None, msg: EventMsg::StreamError(StreamErrorEvent { message: msg.to_string(), codex_error_info: Some(CodexErrorInfo::Other), @@ -3140,6 +3194,7 @@ fn warning_event_adds_warning_history_cell() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None); chat.handle_codex_event(Event { id: "sub-1".into(), + agent_id: None, msg: EventMsg::Warning(WarningEvent { message: "test warning message".to_string(), }), @@ -3159,6 +3214,7 @@ fn stream_recovery_restores_previous_status_header() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None); chat.handle_codex_event(Event { id: "task".into(), + agent_id: None, msg: EventMsg::TaskStarted(TaskStartedEvent { model_context_window: None, }), @@ -3166,6 +3222,7 @@ fn stream_recovery_restores_previous_status_header() { drain_insert_history(&mut rx); chat.handle_codex_event(Event { id: "retry".into(), + agent_id: None, msg: EventMsg::StreamError(StreamErrorEvent { message: "Reconnecting... 1/5".to_string(), codex_error_info: Some(CodexErrorInfo::Other), @@ -3174,6 +3231,7 @@ fn stream_recovery_restores_previous_status_header() { drain_insert_history(&mut rx); chat.handle_codex_event(Event { id: "delta".into(), + agent_id: None, msg: EventMsg::AgentMessageDelta(AgentMessageDeltaEvent { delta: "hello".to_string(), }), @@ -3194,6 +3252,7 @@ fn multiple_agent_messages_in_single_turn_emit_multiple_headers() { // Begin turn chat.handle_codex_event(Event { id: "s1".into(), + agent_id: None, msg: EventMsg::TaskStarted(TaskStartedEvent { model_context_window: None, }), @@ -3202,6 +3261,7 @@ fn multiple_agent_messages_in_single_turn_emit_multiple_headers() { // First finalized assistant message chat.handle_codex_event(Event { id: "s1".into(), + agent_id: None, msg: EventMsg::AgentMessage(AgentMessageEvent { message: "First message".into(), }), @@ -3210,6 +3270,7 @@ fn multiple_agent_messages_in_single_turn_emit_multiple_headers() { // Second finalized assistant message in the same turn chat.handle_codex_event(Event { id: "s1".into(), + agent_id: None, msg: EventMsg::AgentMessage(AgentMessageEvent { message: "Second message".into(), }), @@ -3218,6 +3279,7 @@ fn multiple_agent_messages_in_single_turn_emit_multiple_headers() { // End turn chat.handle_codex_event(Event { id: "s1".into(), + agent_id: None, msg: EventMsg::TaskComplete(TaskCompleteEvent { last_agent_message: None, }), @@ -3248,12 +3310,14 @@ fn final_reasoning_then_message_without_deltas_are_rendered() { // No deltas; only final reasoning followed by final message. chat.handle_codex_event(Event { id: "s1".into(), + agent_id: None, msg: EventMsg::AgentReasoning(AgentReasoningEvent { text: "I will first analyze the request.".into(), }), }); chat.handle_codex_event(Event { id: "s1".into(), + agent_id: None, msg: EventMsg::AgentMessage(AgentMessageEvent { message: "Here is the result.".into(), }), @@ -3275,24 +3339,28 @@ fn deltas_then_same_final_message_are_rendered_snapshot() { // Stream some reasoning deltas first. chat.handle_codex_event(Event { id: "s1".into(), + agent_id: None, msg: EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent { delta: "I will ".into(), }), }); chat.handle_codex_event(Event { id: "s1".into(), + agent_id: None, msg: EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent { delta: "first analyze the ".into(), }), }); chat.handle_codex_event(Event { id: "s1".into(), + agent_id: None, msg: EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent { delta: "request.".into(), }), }); chat.handle_codex_event(Event { id: "s1".into(), + agent_id: None, msg: EventMsg::AgentReasoning(AgentReasoningEvent { text: "request.".into(), }), @@ -3301,12 +3369,14 @@ fn deltas_then_same_final_message_are_rendered_snapshot() { // Then stream answer deltas, followed by the exact same final message. chat.handle_codex_event(Event { id: "s1".into(), + agent_id: None, msg: EventMsg::AgentMessageDelta(AgentMessageDeltaEvent { delta: "Here is the ".into(), }), }); chat.handle_codex_event(Event { id: "s1".into(), + agent_id: None, msg: EventMsg::AgentMessageDelta(AgentMessageDeltaEvent { delta: "result.".into(), }), @@ -3314,6 +3384,7 @@ fn deltas_then_same_final_message_are_rendered_snapshot() { chat.handle_codex_event(Event { id: "s1".into(), + agent_id: None, msg: EventMsg::AgentMessage(AgentMessageEvent { message: "Here is the result.".into(), }), @@ -3337,6 +3408,7 @@ fn chatwidget_exec_and_status_layout_vt100_snapshot() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None); chat.handle_codex_event(Event { id: "t1".into(), + agent_id: None, msg: EventMsg::AgentMessage(AgentMessageEvent { message: "I’m going to search the repo for where “Change Approved” is rendered to update that view.".into() }), }); @@ -3356,6 +3428,7 @@ fn chatwidget_exec_and_status_layout_vt100_snapshot() { let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); chat.handle_codex_event(Event { id: "c1".into(), + agent_id: None, msg: EventMsg::ExecCommandBegin(ExecCommandBeginEvent { call_id: "c1".into(), process_id: None, @@ -3369,6 +3442,7 @@ fn chatwidget_exec_and_status_layout_vt100_snapshot() { }); chat.handle_codex_event(Event { id: "c1".into(), + agent_id: None, msg: EventMsg::ExecCommandEnd(ExecCommandEndEvent { call_id: "c1".into(), process_id: None, @@ -3388,12 +3462,14 @@ fn chatwidget_exec_and_status_layout_vt100_snapshot() { }); chat.handle_codex_event(Event { id: "t1".into(), + agent_id: None, msg: EventMsg::TaskStarted(TaskStartedEvent { model_context_window: None, }), }); chat.handle_codex_event(Event { id: "t1".into(), + agent_id: None, msg: EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent { delta: "**Investigating rendering code**".into(), }), @@ -3432,6 +3508,7 @@ fn chatwidget_markdown_code_blocks_vt100_snapshot() { chat.handle_codex_event(Event { id: "t1".into(), + agent_id: None, msg: EventMsg::TaskStarted(TaskStartedEvent { model_context_window: None, }), @@ -3480,6 +3557,7 @@ printf 'fenced within fenced\n' chat.handle_codex_event(Event { id: "t1".into(), + agent_id: None, msg: EventMsg::AgentMessageDelta(AgentMessageDeltaEvent { delta }), }); // Drive commit ticks and drain emitted history lines into the vt100 buffer. @@ -3503,6 +3581,7 @@ printf 'fenced within fenced\n' // Finalize the stream without sending a final AgentMessage, to flush any tail. chat.handle_codex_event(Event { id: "t1".into(), + agent_id: None, msg: EventMsg::TaskComplete(TaskCompleteEvent { last_agent_message: None, }), @@ -3520,6 +3599,7 @@ fn chatwidget_tall() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None); chat.handle_codex_event(Event { id: "t1".into(), + agent_id: None, msg: EventMsg::TaskStarted(TaskStartedEvent { model_context_window: None, }), diff --git a/codex-rs/tui2/src/app.rs b/codex-rs/tui2/src/app.rs index 1275c1a339..d39543020c 100644 --- a/codex-rs/tui2/src/app.rs +++ b/codex-rs/tui2/src/app.rs @@ -2446,6 +2446,7 @@ mod tests { app.chat_widget.handle_codex_event(Event { id: String::new(), + agent_id: None, msg: EventMsg::SessionConfigured(event), }); diff --git a/codex-rs/tui2/src/chatwidget.rs b/codex-rs/tui2/src/chatwidget.rs index 62d026d61c..b2272482cd 100644 --- a/codex-rs/tui2/src/chatwidget.rs +++ b/codex-rs/tui2/src/chatwidget.rs @@ -23,6 +23,7 @@ use codex_core::protocol::AgentReasoningRawContentEvent; use codex_core::protocol::ApplyPatchApprovalRequestEvent; use codex_core::protocol::BackgroundEventEvent; use codex_core::protocol::CreditsSnapshot; +use codex_core::protocol::DEFAULT_AGENT_ID; use codex_core::protocol::DeprecationNoticeEvent; use codex_core::protocol::ErrorEvent; use codex_core::protocol::Event; @@ -1634,6 +1635,7 @@ impl ChatWidget { self.app_event_tx.send(AppEvent::CodexEvent(Event { id: "1".to_string(), + agent_id: None, // msg: EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent { // call_id: "1".to_string(), // command: vec!["git".into(), "apply".into()], @@ -1799,10 +1801,25 @@ impl ChatWidget { } pub(crate) fn handle_codex_event(&mut self, event: Event) { - let Event { id, msg } = event; + let Event { id, msg, agent_id } = event; + if !self.should_handle_agent_event(agent_id.as_deref(), &msg) { + return; + } self.dispatch_event_msg(Some(id), msg, false); } + fn should_handle_agent_event(&self, agent_id: Option<&str>, msg: &EventMsg) -> bool { + let agent_id = agent_id.unwrap_or(DEFAULT_AGENT_ID); + if agent_id == DEFAULT_AGENT_ID { + return true; + } + + matches!( + msg, + EventMsg::ExecApprovalRequest(_) | EventMsg::ApplyPatchApprovalRequest(_) + ) + } + /// Dispatch a protocol `EventMsg` to the appropriate handler. /// /// `id` is `Some` for live events and `None` for replayed events from diff --git a/codex-rs/tui2/src/chatwidget/agent.rs b/codex-rs/tui2/src/chatwidget/agent.rs index 240972347f..f6e729b43d 100644 --- a/codex-rs/tui2/src/chatwidget/agent.rs +++ b/codex-rs/tui2/src/chatwidget/agent.rs @@ -36,6 +36,7 @@ pub(crate) fn spawn_agent( eprintln!("{message}"); app_event_tx_clone.send(AppEvent::CodexEvent(Event { id: "".to_string(), + agent_id: None, msg: EventMsg::Error(err.to_error_event(None)), })); app_event_tx_clone.send(AppEvent::ExitRequest); @@ -48,6 +49,7 @@ pub(crate) fn spawn_agent( let ev = codex_core::protocol::Event { // The `id` does not matter for rendering, so we can use a fake value. id: "".to_string(), + agent_id: None, msg: codex_core::protocol::EventMsg::SessionConfigured(session_configured), }; app_event_tx_clone.send(AppEvent::CodexEvent(ev)); @@ -85,6 +87,7 @@ pub(crate) fn spawn_agent_from_existing( // Forward the captured `SessionConfigured` event so it can be rendered in the UI. let ev = codex_core::protocol::Event { id: "".to_string(), + agent_id: None, msg: codex_core::protocol::EventMsg::SessionConfigured(session_configured), }; app_event_tx_clone.send(AppEvent::CodexEvent(ev)); diff --git a/codex-rs/tui2/src/chatwidget/tests.rs b/codex-rs/tui2/src/chatwidget/tests.rs index bff3ac5a05..e322f65ccf 100644 --- a/codex-rs/tui2/src/chatwidget/tests.rs +++ b/codex-rs/tui2/src/chatwidget/tests.rs @@ -127,6 +127,7 @@ fn resumed_initial_messages_render_history() { chat.handle_codex_event(Event { id: "initial".into(), + agent_id: None, msg: EventMsg::SessionConfigured(configured), }); @@ -159,6 +160,7 @@ fn entered_review_mode_uses_request_hint() { chat.handle_codex_event(Event { id: "review-start".into(), + agent_id: None, msg: EventMsg::EnteredReviewMode(ReviewRequest { target: ReviewTarget::BaseBranch { branch: "feature".to_string(), @@ -180,6 +182,7 @@ fn entered_review_mode_defaults_to_current_changes_banner() { chat.handle_codex_event(Event { id: "review-start".into(), + agent_id: None, msg: EventMsg::EnteredReviewMode(ReviewRequest { target: ReviewTarget::UncommittedChanges, user_facing_hint: None, @@ -203,6 +206,7 @@ fn review_restores_context_window_indicator() { chat.handle_codex_event(Event { id: "token-before".into(), + agent_id: None, msg: EventMsg::TokenCount(TokenCountEvent { info: Some(make_token_info(pre_review_tokens, context_window)), rate_limits: None, @@ -212,6 +216,7 @@ fn review_restores_context_window_indicator() { chat.handle_codex_event(Event { id: "review-start".into(), + agent_id: None, msg: EventMsg::EnteredReviewMode(ReviewRequest { target: ReviewTarget::BaseBranch { branch: "feature".to_string(), @@ -222,6 +227,7 @@ fn review_restores_context_window_indicator() { chat.handle_codex_event(Event { id: "token-review".into(), + agent_id: None, msg: EventMsg::TokenCount(TokenCountEvent { info: Some(make_token_info(review_tokens, context_window)), rate_limits: None, @@ -231,6 +237,7 @@ fn review_restores_context_window_indicator() { chat.handle_codex_event(Event { id: "review-end".into(), + agent_id: None, msg: EventMsg::ExitedReviewMode(ExitedReviewModeEvent { review_output: None, }), @@ -251,6 +258,7 @@ fn token_count_none_resets_context_indicator() { chat.handle_codex_event(Event { id: "token-before".into(), + agent_id: None, msg: EventMsg::TokenCount(TokenCountEvent { info: Some(make_token_info(pre_compact_tokens, context_window)), rate_limits: None, @@ -260,6 +268,7 @@ fn token_count_none_resets_context_indicator() { chat.handle_codex_event(Event { id: "token-cleared".into(), + agent_id: None, msg: EventMsg::TokenCount(TokenCountEvent { info: None, rate_limits: None, @@ -290,6 +299,7 @@ fn context_indicator_shows_used_tokens_when_window_unknown() { chat.handle_codex_event(Event { id: "token-usage".into(), + agent_id: None, msg: EventMsg::TokenCount(TokenCountEvent { info: Some(token_info), rate_limits: None, @@ -725,6 +735,7 @@ fn exec_approval_emits_proposed_command_and_decision_history() { }; chat.handle_codex_event(Event { id: "sub-short".into(), + agent_id: None, msg: EventMsg::ExecApprovalRequest(ev), }); @@ -769,6 +780,7 @@ fn exec_approval_decision_truncates_multiline_and_long_commands() { }; chat.handle_codex_event(Event { id: "sub-multi".into(), + agent_id: None, msg: EventMsg::ExecApprovalRequest(ev_multi), }); let proposed_multi = drain_insert_history(&mut rx); @@ -819,6 +831,7 @@ fn exec_approval_decision_truncates_multiline_and_long_commands() { }; chat.handle_codex_event(Event { id: "sub-long".into(), + agent_id: None, msg: EventMsg::ExecApprovalRequest(ev_long), }); let proposed_long = drain_insert_history(&mut rx); @@ -861,6 +874,7 @@ fn begin_exec_with_source( }; chat.handle_codex_event(Event { id: call_id.to_string(), + agent_id: None, msg: EventMsg::ExecCommandBegin(event.clone()), }); event @@ -894,6 +908,7 @@ fn end_exec( } = begin_event; chat.handle_codex_event(Event { id: call_id.clone(), + agent_id: None, msg: EventMsg::ExecCommandEnd(ExecCommandEndEvent { call_id, process_id, @@ -1149,6 +1164,7 @@ fn exec_end_without_begin_uses_event_command() { let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); chat.handle_codex_event(Event { id: "call-orphan".to_string(), + agent_id: None, msg: EventMsg::ExecCommandEnd(ExecCommandEndEvent { call_id: "call-orphan".to_string(), process_id: None, @@ -1346,6 +1362,7 @@ fn undo_success_events_render_info_messages() { chat.handle_codex_event(Event { id: "turn-1".to_string(), + agent_id: None, msg: EventMsg::UndoStarted(UndoStartedEvent { message: Some("Undo requested for the last turn...".to_string()), }), @@ -1357,6 +1374,7 @@ fn undo_success_events_render_info_messages() { chat.handle_codex_event(Event { id: "turn-1".to_string(), + agent_id: None, msg: EventMsg::UndoCompleted(UndoCompletedEvent { success: true, message: None, @@ -1383,6 +1401,7 @@ fn undo_failure_events_render_error_message() { chat.handle_codex_event(Event { id: "turn-2".to_string(), + agent_id: None, msg: EventMsg::UndoStarted(UndoStartedEvent { message: None }), }); assert!( @@ -1392,6 +1411,7 @@ fn undo_failure_events_render_error_message() { chat.handle_codex_event(Event { id: "turn-2".to_string(), + agent_id: None, msg: EventMsg::UndoCompleted(UndoCompletedEvent { success: false, message: Some("Failed to restore workspace state.".to_string()), @@ -1418,6 +1438,7 @@ fn undo_started_hides_interrupt_hint() { chat.handle_codex_event(Event { id: "turn-hint".to_string(), + agent_id: None, msg: EventMsg::UndoStarted(UndoStartedEvent { message: None }), }); @@ -1541,6 +1562,7 @@ fn view_image_tool_call_adds_history_cell() { chat.handle_codex_event(Event { id: "sub-image".into(), + agent_id: None, msg: EventMsg::ViewImageToolCall(ViewImageToolCallEvent { call_id: "call-image".into(), path: image_path, @@ -1566,6 +1588,7 @@ fn interrupt_exec_marks_failed_snapshot() { // cause the active exec cell to be finalized as failed and flushed. chat.handle_codex_event(Event { id: "call-int".into(), + agent_id: None, msg: EventMsg::TurnAborted(codex_core::protocol::TurnAbortedEvent { reason: TurnAbortReason::Interrupted, }), @@ -1591,6 +1614,7 @@ fn interrupted_turn_error_message_snapshot() { // Simulate an in-progress task so the widget is in a running state. chat.handle_codex_event(Event { id: "task-1".into(), + agent_id: None, msg: EventMsg::TaskStarted(TaskStartedEvent { model_context_window: None, }), @@ -1599,6 +1623,7 @@ fn interrupted_turn_error_message_snapshot() { // Abort the turn (like pressing Esc) and drain inserted history. chat.handle_codex_event(Event { id: "task-1".into(), + agent_id: None, msg: EventMsg::TurnAborted(codex_core::protocol::TurnAbortedEvent { reason: TurnAbortReason::Interrupted, }), @@ -2070,6 +2095,7 @@ fn approval_modal_exec_snapshot() { }; chat.handle_codex_event(Event { id: "sub-approve".into(), + agent_id: None, msg: EventMsg::ExecApprovalRequest(ev), }); // Render to a fixed-size test terminal and snapshot. @@ -2121,6 +2147,7 @@ fn approval_modal_exec_without_reason_snapshot() { }; chat.handle_codex_event(Event { id: "sub-approve-noreason".into(), + agent_id: None, msg: EventMsg::ExecApprovalRequest(ev), }); @@ -2161,6 +2188,7 @@ fn approval_modal_patch_snapshot() { }; chat.handle_codex_event(Event { id: "sub-approve-patch".into(), + agent_id: None, msg: EventMsg::ApplyPatchApprovalRequest(ev), }); @@ -2195,6 +2223,7 @@ fn interrupt_restores_queued_messages_into_composer() { // Deliver a TurnAborted event with Interrupted reason (as if Esc was pressed). chat.handle_codex_event(Event { id: "turn-1".into(), + agent_id: None, msg: EventMsg::TurnAborted(codex_core::protocol::TurnAbortedEvent { reason: TurnAbortReason::Interrupted, }), @@ -2233,6 +2262,7 @@ fn interrupt_prepends_queued_messages_before_existing_composer_text() { chat.handle_codex_event(Event { id: "turn-1".into(), + agent_id: None, msg: EventMsg::TurnAborted(codex_core::protocol::TurnAbortedEvent { reason: TurnAbortReason::Interrupted, }), @@ -2278,12 +2308,14 @@ fn ui_snapshots_small_heights_task_running() { // Activate status line chat.handle_codex_event(Event { id: "task-1".into(), + agent_id: None, msg: EventMsg::TaskStarted(TaskStartedEvent { model_context_window: None, }), }); chat.handle_codex_event(Event { id: "task-1".into(), + agent_id: None, msg: EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent { delta: "**Thinking**".into(), }), @@ -2309,6 +2341,7 @@ fn status_widget_and_approval_modal_snapshot() { // Begin a running task so the status indicator would be active. chat.handle_codex_event(Event { id: "task-1".into(), + agent_id: None, msg: EventMsg::TaskStarted(TaskStartedEvent { model_context_window: None, }), @@ -2316,6 +2349,7 @@ fn status_widget_and_approval_modal_snapshot() { // Provide a deterministic header for the status line. chat.handle_codex_event(Event { id: "task-1".into(), + agent_id: None, msg: EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent { delta: "**Analyzing**".into(), }), @@ -2338,6 +2372,7 @@ fn status_widget_and_approval_modal_snapshot() { }; chat.handle_codex_event(Event { id: "sub-approve-exec".into(), + agent_id: None, msg: EventMsg::ExecApprovalRequest(ev), }); @@ -2361,6 +2396,7 @@ fn status_widget_active_snapshot() { // Activate the status indicator by simulating a task start. chat.handle_codex_event(Event { id: "task-1".into(), + agent_id: None, msg: EventMsg::TaskStarted(TaskStartedEvent { model_context_window: None, }), @@ -2368,6 +2404,7 @@ fn status_widget_active_snapshot() { // Provide a deterministic header via a bold reasoning chunk. chat.handle_codex_event(Event { id: "task-1".into(), + agent_id: None, msg: EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent { delta: "**Analyzing**".into(), }), @@ -2389,6 +2426,7 @@ fn mcp_startup_header_booting_snapshot() { chat.handle_codex_event(Event { id: "mcp-1".into(), + agent_id: None, msg: EventMsg::McpStartupUpdate(McpStartupUpdateEvent { server: "alpha".into(), status: McpStartupStatus::Starting, @@ -2410,6 +2448,7 @@ fn background_event_updates_status_header() { chat.handle_codex_event(Event { id: "bg-1".into(), + agent_id: None, msg: EventMsg::BackgroundEvent(BackgroundEventEvent { message: "Waiting for `vim`".to_string(), }), @@ -2441,6 +2480,7 @@ fn apply_patch_events_emit_history_cells() { }; chat.handle_codex_event(Event { id: "s1".into(), + agent_id: None, msg: EventMsg::ApplyPatchApprovalRequest(ev), }); let cells = drain_insert_history(&mut rx); @@ -2481,6 +2521,7 @@ fn apply_patch_events_emit_history_cells() { }; chat.handle_codex_event(Event { id: "s1".into(), + agent_id: None, msg: EventMsg::PatchApplyBegin(begin), }); let cells = drain_insert_history(&mut rx); @@ -2509,6 +2550,7 @@ fn apply_patch_events_emit_history_cells() { }; chat.handle_codex_event(Event { id: "s1".into(), + agent_id: None, msg: EventMsg::PatchApplyEnd(end), }); let cells = drain_insert_history(&mut rx); @@ -2531,6 +2573,7 @@ fn apply_patch_manual_approval_adjusts_header() { ); chat.handle_codex_event(Event { id: "s1".into(), + agent_id: None, msg: EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent { call_id: "c1".into(), turn_id: "turn-c1".into(), @@ -2550,6 +2593,7 @@ fn apply_patch_manual_approval_adjusts_header() { ); chat.handle_codex_event(Event { id: "s1".into(), + agent_id: None, msg: EventMsg::PatchApplyBegin(PatchApplyBeginEvent { call_id: "c1".into(), turn_id: "turn-c1".into(), @@ -2580,6 +2624,7 @@ fn apply_patch_manual_flow_snapshot() { ); chat.handle_codex_event(Event { id: "s1".into(), + agent_id: None, msg: EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent { call_id: "c1".into(), turn_id: "turn-c1".into(), @@ -2603,6 +2648,7 @@ fn apply_patch_manual_flow_snapshot() { ); chat.handle_codex_event(Event { id: "s1".into(), + agent_id: None, msg: EventMsg::PatchApplyBegin(PatchApplyBeginEvent { call_id: "c1".into(), turn_id: "turn-c1".into(), @@ -2640,6 +2686,7 @@ fn apply_patch_approval_sends_op_with_submission_id() { }; chat.handle_codex_event(Event { id: "sub-123".into(), + agent_id: None, msg: EventMsg::ApplyPatchApprovalRequest(ev), }); @@ -2671,6 +2718,7 @@ fn apply_patch_full_flow_integration_like() { ); chat.handle_codex_event(Event { id: "sub-xyz".into(), + agent_id: None, msg: EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent { call_id: "call-1".into(), turn_id: "turn-call-1".into(), @@ -2712,6 +2760,7 @@ fn apply_patch_full_flow_integration_like() { ); chat.handle_codex_event(Event { id: "sub-xyz".into(), + agent_id: None, msg: EventMsg::PatchApplyBegin(PatchApplyBeginEvent { call_id: "call-1".into(), turn_id: "turn-call-1".into(), @@ -2726,6 +2775,7 @@ fn apply_patch_full_flow_integration_like() { ); chat.handle_codex_event(Event { id: "sub-xyz".into(), + agent_id: None, msg: EventMsg::PatchApplyEnd(PatchApplyEndEvent { call_id: "call-1".into(), turn_id: "turn-call-1".into(), @@ -2751,6 +2801,7 @@ fn apply_patch_untrusted_shows_approval_modal() { ); chat.handle_codex_event(Event { id: "sub-1".into(), + agent_id: None, msg: EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent { call_id: "call-1".into(), turn_id: "turn-call-1".into(), @@ -2800,6 +2851,7 @@ fn apply_patch_request_shows_diff_summary() { ); chat.handle_codex_event(Event { id: "sub-apply".into(), + agent_id: None, msg: EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent { call_id: "call-apply".into(), turn_id: "turn-apply".into(), @@ -2870,6 +2922,7 @@ fn plan_update_renders_history_cell() { }; chat.handle_codex_event(Event { id: "sub-1".into(), + agent_id: None, msg: EventMsg::PlanUpdate(update), }); let cells = drain_insert_history(&mut rx); @@ -2891,6 +2944,7 @@ fn stream_error_updates_status_indicator() { let msg = "Reconnecting... 2/5"; chat.handle_codex_event(Event { id: "sub-1".into(), + agent_id: None, msg: EventMsg::StreamError(StreamErrorEvent { message: msg.to_string(), codex_error_info: Some(CodexErrorInfo::Other), @@ -2914,6 +2968,7 @@ fn warning_event_adds_warning_history_cell() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None); chat.handle_codex_event(Event { id: "sub-1".into(), + agent_id: None, msg: EventMsg::Warning(WarningEvent { message: "test warning message".to_string(), }), @@ -2933,6 +2988,7 @@ fn stream_recovery_restores_previous_status_header() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None); chat.handle_codex_event(Event { id: "task".into(), + agent_id: None, msg: EventMsg::TaskStarted(TaskStartedEvent { model_context_window: None, }), @@ -2940,6 +2996,7 @@ fn stream_recovery_restores_previous_status_header() { drain_insert_history(&mut rx); chat.handle_codex_event(Event { id: "retry".into(), + agent_id: None, msg: EventMsg::StreamError(StreamErrorEvent { message: "Reconnecting... 1/5".to_string(), codex_error_info: Some(CodexErrorInfo::Other), @@ -2948,6 +3005,7 @@ fn stream_recovery_restores_previous_status_header() { drain_insert_history(&mut rx); chat.handle_codex_event(Event { id: "delta".into(), + agent_id: None, msg: EventMsg::AgentMessageDelta(AgentMessageDeltaEvent { delta: "hello".to_string(), }), @@ -2968,6 +3026,7 @@ fn multiple_agent_messages_in_single_turn_emit_multiple_headers() { // Begin turn chat.handle_codex_event(Event { id: "s1".into(), + agent_id: None, msg: EventMsg::TaskStarted(TaskStartedEvent { model_context_window: None, }), @@ -2976,6 +3035,7 @@ fn multiple_agent_messages_in_single_turn_emit_multiple_headers() { // First finalized assistant message chat.handle_codex_event(Event { id: "s1".into(), + agent_id: None, msg: EventMsg::AgentMessage(AgentMessageEvent { message: "First message".into(), }), @@ -2984,6 +3044,7 @@ fn multiple_agent_messages_in_single_turn_emit_multiple_headers() { // Second finalized assistant message in the same turn chat.handle_codex_event(Event { id: "s1".into(), + agent_id: None, msg: EventMsg::AgentMessage(AgentMessageEvent { message: "Second message".into(), }), @@ -2992,6 +3053,7 @@ fn multiple_agent_messages_in_single_turn_emit_multiple_headers() { // End turn chat.handle_codex_event(Event { id: "s1".into(), + agent_id: None, msg: EventMsg::TaskComplete(TaskCompleteEvent { last_agent_message: None, }), @@ -3022,12 +3084,14 @@ fn final_reasoning_then_message_without_deltas_are_rendered() { // No deltas; only final reasoning followed by final message. chat.handle_codex_event(Event { id: "s1".into(), + agent_id: None, msg: EventMsg::AgentReasoning(AgentReasoningEvent { text: "I will first analyze the request.".into(), }), }); chat.handle_codex_event(Event { id: "s1".into(), + agent_id: None, msg: EventMsg::AgentMessage(AgentMessageEvent { message: "Here is the result.".into(), }), @@ -3049,24 +3113,28 @@ fn deltas_then_same_final_message_are_rendered_snapshot() { // Stream some reasoning deltas first. chat.handle_codex_event(Event { id: "s1".into(), + agent_id: None, msg: EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent { delta: "I will ".into(), }), }); chat.handle_codex_event(Event { id: "s1".into(), + agent_id: None, msg: EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent { delta: "first analyze the ".into(), }), }); chat.handle_codex_event(Event { id: "s1".into(), + agent_id: None, msg: EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent { delta: "request.".into(), }), }); chat.handle_codex_event(Event { id: "s1".into(), + agent_id: None, msg: EventMsg::AgentReasoning(AgentReasoningEvent { text: "request.".into(), }), @@ -3075,12 +3143,14 @@ fn deltas_then_same_final_message_are_rendered_snapshot() { // Then stream answer deltas, followed by the exact same final message. chat.handle_codex_event(Event { id: "s1".into(), + agent_id: None, msg: EventMsg::AgentMessageDelta(AgentMessageDeltaEvent { delta: "Here is the ".into(), }), }); chat.handle_codex_event(Event { id: "s1".into(), + agent_id: None, msg: EventMsg::AgentMessageDelta(AgentMessageDeltaEvent { delta: "result.".into(), }), @@ -3088,6 +3158,7 @@ fn deltas_then_same_final_message_are_rendered_snapshot() { chat.handle_codex_event(Event { id: "s1".into(), + agent_id: None, msg: EventMsg::AgentMessage(AgentMessageEvent { message: "Here is the result.".into(), }), @@ -3111,6 +3182,7 @@ fn chatwidget_exec_and_status_layout_vt100_snapshot() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None); chat.handle_codex_event(Event { id: "t1".into(), + agent_id: None, msg: EventMsg::AgentMessage(AgentMessageEvent { message: "I’m going to search the repo for where “Change Approved” is rendered to update that view.".into() }), }); @@ -3130,6 +3202,7 @@ fn chatwidget_exec_and_status_layout_vt100_snapshot() { let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); chat.handle_codex_event(Event { id: "c1".into(), + agent_id: None, msg: EventMsg::ExecCommandBegin(ExecCommandBeginEvent { call_id: "c1".into(), process_id: None, @@ -3143,6 +3216,7 @@ fn chatwidget_exec_and_status_layout_vt100_snapshot() { }); chat.handle_codex_event(Event { id: "c1".into(), + agent_id: None, msg: EventMsg::ExecCommandEnd(ExecCommandEndEvent { call_id: "c1".into(), process_id: None, @@ -3162,12 +3236,14 @@ fn chatwidget_exec_and_status_layout_vt100_snapshot() { }); chat.handle_codex_event(Event { id: "t1".into(), + agent_id: None, msg: EventMsg::TaskStarted(TaskStartedEvent { model_context_window: None, }), }); chat.handle_codex_event(Event { id: "t1".into(), + agent_id: None, msg: EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent { delta: "**Investigating rendering code**".into(), }), @@ -3206,6 +3282,7 @@ fn chatwidget_markdown_code_blocks_vt100_snapshot() { chat.handle_codex_event(Event { id: "t1".into(), + agent_id: None, msg: EventMsg::TaskStarted(TaskStartedEvent { model_context_window: None, }), @@ -3254,6 +3331,7 @@ printf 'fenced within fenced\n' chat.handle_codex_event(Event { id: "t1".into(), + agent_id: None, msg: EventMsg::AgentMessageDelta(AgentMessageDeltaEvent { delta }), }); // Drive commit ticks and drain emitted history lines into the vt100 buffer. @@ -3277,6 +3355,7 @@ printf 'fenced within fenced\n' // Finalize the stream without sending a final AgentMessage, to flush any tail. chat.handle_codex_event(Event { id: "t1".into(), + agent_id: None, msg: EventMsg::TaskComplete(TaskCompleteEvent { last_agent_message: None, }), @@ -3294,6 +3373,7 @@ fn chatwidget_tall() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None); chat.handle_codex_event(Event { id: "t1".into(), + agent_id: None, msg: EventMsg::TaskStarted(TaskStartedEvent { model_context_window: None, }),