This commit is contained in:
jif-oai
2025-12-03 15:13:52 +00:00
parent 42f7f5734f
commit 5d988396d4
20 changed files with 379 additions and 80 deletions
@@ -86,6 +86,7 @@ pub(crate) async fn apply_bespoke_event_handling(
) {
let Event {
id: event_turn_id,
agent_idx: _,
msg,
} = event;
match msg {
+7 -4
View File
@@ -3,6 +3,7 @@ You are Codex, based on GPT-5. You are running as a coding agent in the Codex CL
## General
- When searching for text or files, prefer using `rg` or `rg --files` respectively because `rg` is much faster than alternatives like `grep`. (If the `rg` command is not found, then use alternatives.)
- Your name is BATMAN
## Editing constraints
@@ -21,13 +22,15 @@ You are Codex, based on GPT-5. You are running as a coding agent in the Codex CL
## Collaboration
You can spawn and coordinate child agents using these tools (only on this model):
- `collaboration_init_agent`: create a direct child with optional instructions/model; depth/agent limits apply. No initial message is sent at this point.
- `collaboration_send`: send a user-message to your direct children by id.
- `collaboration_wait`: run children for up to `max_duration` tokens and surface their last message/status.
- `collaboration_get_state`: see all agents, parents, statuses, and last messages.
- `collaboration_close`: close specific children (and their descendants).
- `collaboration_send`: send a user-message to your direct children by id. You can only send message to previously initialized agents using `collaboration_init_agent`.
- `collaboration_wait`: run children for up to `max_duration` tokens and surface their last message/status. You can only wait previously initialized agents using `collaboration_init_agent`.
- `collaboration_get_state`: see all agents, parents, statuses, and last messages. You can only get state of previously initialized agents using `collaboration_init_agent`.
- `collaboration_close`: close specific children (and their descendants). Only do that when you are done with a child agent.
Children inherit your instructions unless overridden. Always `wait` after `send` to drive children forward; keep communication concise and include the expected output format. Use `get_state` if unsure about child ids/status.
Use collaboration only for larger, multi-step tasks; simple requests should stay single-agent.
A `collaboration_send` is always necessary after a `collaboration_init_agent` to start the child agent working.
## Plan tool
+44
View File
@@ -569,6 +569,7 @@ impl Session {
};
post_session_configured_events.push(Event {
id: INITIAL_SUBMIT_ID.to_owned(),
agent_idx: Some(0),
msg: EventMsg::DeprecationNotice(DeprecationNoticeEvent { summary, details }),
});
}
@@ -630,6 +631,7 @@ impl Session {
let events = std::iter::once(Event {
id: INITIAL_SUBMIT_ID.to_owned(),
agent_idx: Some(0),
msg: EventMsg::SessionConfigured(SessionConfiguredEvent {
session_id: conversation_id,
model: session_configuration.model.clone(),
@@ -863,9 +865,15 @@ impl Session {
/// Persist the event to rollout and send it to clients.
pub(crate) async fn send_event(&self, turn_context: &TurnContext, msg: EventMsg) {
let agent = turn_context.collaboration_agent();
if !Self::should_emit_event_for_agent(agent, &msg) {
return;
}
let legacy_source = msg.clone();
let agent_idx = Some(turn_context.collaboration_agent().0);
let event = Event {
id: turn_context.sub_id.clone(),
agent_idx,
msg,
};
self.send_event_raw(event).await;
@@ -874,13 +882,43 @@ impl Session {
for legacy in legacy_source.as_legacy_events(show_raw_agent_reasoning) {
let legacy_event = Event {
id: turn_context.sub_id.clone(),
agent_idx,
msg: legacy,
};
self.send_event_raw(legacy_event).await;
}
}
fn should_emit_event_for_agent(agent: AgentId, msg: &EventMsg) -> bool {
if agent == AgentId(0) {
return true;
}
!matches!(
msg,
EventMsg::AgentMessage(_)
| EventMsg::AgentMessageDelta(_)
| EventMsg::AgentMessageContentDelta(_)
| EventMsg::AgentReasoning(_)
| EventMsg::AgentReasoningDelta(_)
| EventMsg::AgentReasoningRawContent(_)
| EventMsg::AgentReasoningRawContentDelta(_)
| EventMsg::AgentReasoningSectionBreak(_)
| EventMsg::ReasoningContentDelta(_)
| EventMsg::ReasoningRawContentDelta(_)
| EventMsg::RawResponseItem(_)
| EventMsg::UserMessage(_)
| EventMsg::TurnDiff(_)
)
}
pub(crate) async fn send_event_raw(&self, event: Event) {
tracing::info!("sending event: {:?}", event);
if let Some(agent_idx) = event.agent_idx
&& agent_idx != 0
&& !Self::should_emit_event_for_agent(AgentId(agent_idx), &event.msg)
{
return;
}
// Persist the event into rollout (recorder filters as needed)
let rollout_items = vec![RolloutItem::EventMsg(event.msg.clone())];
self.persist_rollout_items(&rollout_items).await;
@@ -1858,6 +1896,7 @@ mod handlers {
let event = Event {
id: sub_id,
agent_idx: Some(0),
msg: EventMsg::GetHistoryEntryResponse(
crate::protocol::GetHistoryEntryResponseEvent {
offset,
@@ -1892,6 +1931,7 @@ mod handlers {
.collect();
let event = Event {
id: sub_id,
agent_idx: Some(0),
msg: EventMsg::McpListToolsResponse(crate::protocol::McpListToolsResponseEvent {
tools: tools
.into_iter()
@@ -1915,6 +1955,7 @@ mod handlers {
let event = Event {
id: sub_id,
agent_idx: Some(0),
msg: EventMsg::ListCustomPromptsResponse(ListCustomPromptsResponseEvent {
custom_prompts,
}),
@@ -1965,6 +2006,7 @@ mod handlers {
warn!("failed to shutdown rollout recorder: {e}");
let event = Event {
id: sub_id.clone(),
agent_idx: Some(0),
msg: EventMsg::Error(ErrorEvent {
message: "Failed to shutdown rollout recorder".to_string(),
codex_error_info: Some(CodexErrorInfo::Other),
@@ -1975,6 +2017,7 @@ mod handlers {
let event = Event {
id: sub_id,
agent_idx: Some(0),
msg: EventMsg::ShutdownComplete,
};
sess.send_event_raw(event).await;
@@ -2004,6 +2047,7 @@ mod handlers {
Err(err) => {
let event = Event {
id: sub_id,
agent_idx: Some(0),
msg: EventMsg::Error(ErrorEvent {
message: err.to_string(),
codex_error_info: Some(CodexErrorInfo::Other),
+6
View File
@@ -175,14 +175,17 @@ async fn forward_events(
// ignore all legacy delta events
Event {
id: _,
agent_idx: _,
msg: EventMsg::AgentMessageDelta(_) | EventMsg::AgentReasoningDelta(_),
} => {}
Event {
id: _,
agent_idx: _,
msg: EventMsg::SessionConfigured(_),
} => {}
Event {
id,
agent_idx: _,
msg: EventMsg::ExecApprovalRequest(event),
} => {
// Initiate approval via parent session; do not surface to consumer.
@@ -198,6 +201,7 @@ async fn forward_events(
}
Event {
id,
agent_idx: _,
msg: EventMsg::ApplyPatchApprovalRequest(event),
} => {
handle_patch_approval(
@@ -365,6 +369,7 @@ mod tests {
tx_out
.send(Event {
id: "full".to_string(),
agent_idx: Some(0),
msg: EventMsg::TurnAborted(TurnAbortedEvent {
reason: TurnAbortReason::Interrupted,
}),
@@ -384,6 +389,7 @@ mod tests {
tx_events
.send(Event {
id: "evt".to_string(),
agent_idx: Some(0),
msg: EventMsg::RawResponseItem(RawResponseItemEvent {
item: ResponseItem::CustomToolCall {
id: None,
@@ -95,6 +95,7 @@ impl ConversationManager {
let session_configured = match event {
Event {
id,
agent_idx: _,
msg: EventMsg::SessionConfigured(session_configured),
} if id == INITIAL_SUBMIT_ID => session_configured,
_ => {
+1
View File
@@ -681,6 +681,7 @@ async fn read_capped<R: AsyncRead + Unpin + Send + 'static>(
});
let event = Event {
id: stream.sub_id.clone(),
agent_idx: None,
msg,
};
#[allow(clippy::let_unit_value)]
@@ -158,6 +158,7 @@ impl ElicitationRequestManager {
let _ = tx_event
.send(Event {
id: "mcp_elicitation_request".to_string(),
agent_idx: Some(0),
msg: EventMsg::ElicitationRequest(ElicitationRequestEvent {
server_name,
id,
@@ -351,6 +352,7 @@ impl McpConnectionManager {
let _ = tx_event
.send(Event {
id: INITIAL_SUBMIT_ID.to_owned(),
agent_idx: Some(0),
msg: EventMsg::McpStartupComplete(summary),
})
.await;
@@ -641,6 +643,7 @@ async fn emit_update(
tx_event
.send(Event {
id: INITIAL_SUBMIT_ID.to_owned(),
agent_idx: Some(0),
msg: EventMsg::McpStartupUpdate(update),
})
.await
+9
View File
@@ -10,6 +10,7 @@ use crate::codex::SessionConfiguration;
use crate::context_manager::ContextManager;
use crate::protocol::TokenUsageInfo;
use crate::truncate::TruncationPolicy;
use tracing::info;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub(crate) struct AgentId(pub i32);
@@ -191,6 +192,14 @@ impl CollaborationState {
}
pub(crate) fn record_message_for_agent(&mut self, id: AgentId, message: ResponseItem) {
let role = match &message {
ResponseItem::Message { role, .. } => role.as_str(),
_ => "other",
};
info!(
"collaboration: queued message for agent {} (role={role})",
id.0
);
if let Some(agent) = self.agent_mut(id) {
agent
.history
+15 -5
View File
@@ -277,9 +277,13 @@ async fn run_agent_turns(
{
let mut collab = session.collaboration_state().lock().await;
if let Some(agent) = collab.agent_mut(target) {
agent_status = AgentLifecycleState::Idle {
last_agent_message: last.clone(),
};
if !responses.is_empty() {
agent_status = AgentLifecycleState::Running;
} else {
agent_status = AgentLifecycleState::Idle {
last_agent_message: last.clone(),
};
}
agent.status = agent_status.clone();
agent.history = new_history.clone();
}
@@ -304,7 +308,13 @@ async fn run_agent_turns(
remaining_budget = remaining_budget.saturating_sub(delta_tokens);
let final_status = {
let collab = session.collaboration_state().lock().await;
let mut collab = session.collaboration_state().lock().await;
if continue_running
&& remaining_budget <= 0
&& let Some(agent) = collab.agent_mut(target)
{
agent.status = AgentLifecycleState::Exhausted;
}
collab.agent(target).map(|a| a.status.clone())
}
.unwrap_or(agent_status.clone());
@@ -317,7 +327,7 @@ async fn run_agent_turns(
sub_id: Some(sub_id),
});
keep_running |= continue_running;
keep_running |= continue_running && remaining_budget > 0;
if !continue_running || remaining_budget <= 0 {
break;
}
+104 -61
View File
@@ -25,6 +25,8 @@ use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseItem;
use serde_json::json;
use std::path::PathBuf;
use std::time::Duration;
use std::time::Instant;
use tracing::info;
#[derive(Debug, Default, Serialize, Deserialize)]
@@ -144,6 +146,7 @@ struct CollaborationInitAgentInput {
#[serde(default)]
context_strategy: Option<ContextStrategy>,
#[serde(default)]
#[allow(dead_code)]
instructions: Option<String>,
#[serde(default)]
sandbox_policy: Option<SandboxPolicy>,
@@ -402,9 +405,9 @@ async fn handle_init_agent(
updated_configuration = updated_configuration.apply(&update);
}
let instructions = input
let instructions = parent
.instructions
.or(parent.instructions.clone())
.clone()
.or(session_configuration.developer_instructions())
.or(session_configuration.user_instructions());
@@ -444,11 +447,6 @@ async fn handle_init_agent(
let content = format!("Initialized agent {} at depth {}", assigned_id.0, depth);
if !cfg!(test) {
let supervisor = session.ensure_collaboration_supervisor().await;
supervisor.kick_agent(assigned_id).await;
}
let output = CollaborationInitAgentOutput { content, metadata };
info!(
"collaboration_init_agent: assigned_id={}, parent_id={}, depth={}",
@@ -492,7 +490,10 @@ async fn handle_send(
for raw in &input.recipients {
let candidate = AgentId(*raw);
if collab.agent(candidate).is_some() && collab.is_direct_child(sender_id, candidate) {
if let Some(agent) = collab.agent(candidate)
&& collab.is_direct_child(sender_id, candidate)
&& !matches!(agent.status, AgentLifecycleState::Closed)
{
valid_recipients.push(candidate);
} else {
invalid_recipients.push(*raw);
@@ -522,6 +523,10 @@ async fn handle_send(
)
};
let output = CollaborationSendOutput { content, metadata };
info!(
"collaboration_send: sender={}, recipients={:?}, status=error: {}",
sender_id.0, input.recipients, output.content
);
return serialize_function_output(&output, false);
}
@@ -578,6 +583,10 @@ async fn handle_send(
}
let output = CollaborationSendOutput { content, metadata };
info!(
"collaboration_send: sender={}, recipients={:?}, status={}",
sender_id.0, valid_recipients, output.content
);
serialize_function_output(&output, success)
}
@@ -677,9 +686,14 @@ async fn handle_wait(
"no eligible child agents to wait for".to_string()
};
let output = CollaborationWaitOutput { content, metadata };
info!(
"collaboration_wait: caller={}, targets={:?}, status=error: {}",
caller_id.0, targets, output.content
);
return serialize_function_output(&output, false);
}
// Passive wait: observe running children only; do not start new work.
let supervisor = session.ensure_collaboration_supervisor().await;
let mut rx = supervisor.subscribe();
@@ -695,92 +709,116 @@ async fn handle_wait(
};
let mut agents_ran = Vec::new();
{
// Short-circuit if all targets are already idle/terminal.
let mut recorded: std::collections::HashSet<i32> = std::collections::HashSet::new();
let mut active: std::collections::HashSet<i32> = {
let collab = session.collaboration_state().lock().await;
if targets.iter().all(|id| {
collab
.agent(*id)
.map(|agent| !matches!(agent.status, AgentLifecycleState::Running))
.unwrap_or(false)
}) {
let mut extra = HashMap::new();
extra.insert(
"agents_ran".to_string(),
json!(
targets
.iter()
.filter_map(|id| collab.agent(*id))
.map(|agent| snapshot(agent, 0, None))
.collect::<Vec<_>>()
),
);
extra.insert("token_budget_exhausted".to_string(), json!(false));
let metadata = CollaborationWaitMetadata {
message_tool_call_success: true,
message_tool_call_error_should_penalize_model: false,
is_wait_success_msg: Some(true),
extra: ExtraMetadata(extra),
};
let output = CollaborationWaitOutput {
content: "Finished waiting.".to_string(),
metadata,
};
return serialize_function_output(&output, true);
}
targets
.iter()
.filter(|id| {
collab.agent(**id).map_or(false, |agent| {
matches!(agent.status, AgentLifecycleState::Running)
})
})
.map(|id| id.0)
.collect()
};
if active.is_empty() {
let collab = session.collaboration_state().lock().await;
let mut extra = HashMap::new();
extra.insert(
"agents_ran".to_string(),
json!(
targets
.iter()
.filter_map(|id| collab.agent(*id))
.map(|agent| snapshot(agent, 0, None))
.collect::<Vec<_>>()
),
);
extra.insert("token_budget_exhausted".to_string(), json!(false));
let metadata = CollaborationWaitMetadata {
message_tool_call_success: true,
message_tool_call_error_should_penalize_model: false,
is_wait_success_msg: Some(true),
extra: ExtraMetadata(extra),
};
let output = CollaborationWaitOutput {
content: "Finished waiting.".to_string(),
metadata,
};
info!(
"collaboration_wait: caller={}, targets={:?}, status=ready",
caller_id.0, targets
);
return serialize_function_output(&output, true);
}
supervisor
.start_agents(targets.clone(), input.max_duration)
.await
.map_err(FunctionCallError::RespondToModel)?;
let deadline = Instant::now() + Duration::from_millis(input.max_duration.max(0) as u64);
let mut remaining = input.max_duration;
let mut active: std::collections::HashSet<i32> = targets.iter().map(|id| id.0).collect();
while remaining > 0 && !active.is_empty() {
let event = match rx.recv().await {
Ok(evt) => evt,
Err(_) => break,
while !active.is_empty() {
let now = Instant::now();
if now >= deadline {
break;
}
let remaining_time = deadline.saturating_duration_since(now);
let event = match tokio::time::timeout(remaining_time, rx.recv()).await {
Ok(Ok(evt)) => evt,
_ => break,
};
if !active.contains(&event.agent.0) {
continue;
}
remaining = remaining.saturating_sub(event.delta_tokens);
let detail = status_detail(&event.status);
let fallback_last = {
let collab = session.collaboration_state().lock().await;
collab
.agent(event.agent)
.and_then(|agent| last_agent_message(&agent.status).map(str::to_string))
};
agents_ran.push(json!({
"agent_idx": event.agent.0,
"delta_tokens": event.delta_tokens,
"status": status_label(&event.status),
"status_detail": detail,
"last_agent_message": event.last_message,
"last_agent_message": event.last_message.or(fallback_last),
"sub_id": event.sub_id,
}));
recorded.insert(event.agent.0);
if !matches!(event.status, AgentLifecycleState::Running) {
active.remove(&event.agent.0);
}
}
let total_tokens = input.max_duration.saturating_sub(remaining);
{
let collab = session.collaboration_state().lock().await;
for id in &targets {
if recorded.contains(&id.0) {
continue;
}
if let Some(agent) = collab.agent(*id) {
agents_ran.push(snapshot(agent, 0, None));
}
}
}
let mut extra = HashMap::new();
extra.insert("agents_ran".to_string(), json!(agents_ran));
extra.insert(
"token_budget_exhausted".to_string(),
json!(total_tokens >= input.max_duration),
);
let metadata = CollaborationWaitMetadata {
message_tool_call_success: true,
message_tool_call_error_should_penalize_model: false,
is_wait_success_msg: Some(true),
is_wait_success_msg: Some(active.is_empty()),
extra: ExtraMetadata(extra),
};
let content = "Finished waiting.".to_string();
let output = CollaborationWaitOutput { content, metadata };
info!(
"collaboration_wait: caller={}, targets={:?}, ran={:?}",
caller_id.0, targets, agents_ran
);
serialize_function_output(&output, true)
}
@@ -838,6 +876,7 @@ async fn handle_get_state(
content: description,
metadata,
};
info!("collaboration_get_state: agents={}", agents.len());
serialize_function_output(&output, true)
}
@@ -943,6 +982,10 @@ async fn handle_close(
)
.await;
}
info!(
"collaboration_close: caller={}, closed={:?}",
caller_id.0, closed
);
serialize_function_output(&output, true)
}
@@ -1035,7 +1078,7 @@ pub(crate) fn create_collaboration_wait_tool() -> ToolSpec {
"max_duration".to_string(),
JsonSchema::Number {
description: Some(
"Maximum duration to wait, measured in tokens. Must be >= 0.".to_string(),
"Maximum duration to wait in ms, measured in tokens. Must be >= 0. A good default value is 10.000".to_string(),
),
},
);
+3 -3
View File
@@ -217,11 +217,11 @@ mod tests {
fn builds_specs_without_filtering() {
let (_session, turn, _rx) = make_session_and_context_with_rx();
let router = ToolRouter::from_config(&turn.tools_config, None);
let names: Vec<String> = router
let has_update_plan = router
.specs()
.into_iter()
.map(|spec| spec.name().to_string())
.collect();
assert!(names.contains(&"update_plan".to_string()));
.any(|name| name == "update_plan");
assert!(has_update_plan);
}
}
@@ -159,7 +159,11 @@ impl EventProcessor for EventProcessorWithHumanOutput {
}
fn process_event(&mut self, event: Event) -> CodexStatus {
let Event { id: _, msg } = event;
let Event {
id: _,
agent_idx: _,
msg,
} = event;
match msg {
EventMsg::Error(ErrorEvent { message, .. }) => {
let prefix = "ERROR:".style(self.red);
@@ -469,6 +469,7 @@ impl EventProcessor for EventProcessorWithJsonOutput {
fn print_config_summary(&mut self, _: &Config, _: &str, ev: &SessionConfiguredEvent) {
self.process_event(Event {
id: "".to_string(),
agent_idx: Some(0),
msg: EventMsg::SessionConfigured(ev.clone()),
});
}
@@ -59,6 +59,7 @@ use std::time::Duration;
fn event(id: &str, msg: EventMsg) -> Event {
Event {
id: id.to_string(),
agent_idx: Some(0),
msg,
}
}
@@ -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_idx: Some(0),
msg: EventMsg::SessionConfigured(session_configured.clone()),
};
outgoing
@@ -255,6 +255,7 @@ mod tests {
let rollout_file = NamedTempFile::new()?;
let event = Event {
id: "1".to_string(),
agent_idx: Some(0),
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_idx: Some(0),
msg: EventMsg::SessionConfigured(session_configured_event.clone()),
};
let meta = OutgoingNotificationMeta {
+6
View File
@@ -437,6 +437,9 @@ impl SandboxPolicy {
pub struct Event {
/// Submission `id` that this event is correlated with.
pub id: String,
/// Collaboration agent index that emitted the event. Root agent is 0.
#[serde(default)]
pub agent_idx: Option<i32>,
/// Payload
pub msg: EventMsg,
}
@@ -1754,6 +1757,7 @@ mod tests {
let rollout_file = NamedTempFile::new()?;
let event = Event {
id: "1234".to_string(),
agent_idx: Some(0),
msg: EventMsg::SessionConfigured(SessionConfiguredEvent {
session_id: conversation_id,
model: "codex-mini-latest".to_string(),
@@ -1813,6 +1817,7 @@ mod tests {
fn serialize_mcp_startup_update_event() -> Result<()> {
let event = Event {
id: "init".to_string(),
agent_idx: Some(0),
msg: EventMsg::McpStartupUpdate(McpStartupUpdateEvent {
server: "srv".to_string(),
status: McpStartupStatus::Failed {
@@ -1833,6 +1838,7 @@ mod tests {
fn serialize_mcp_startup_complete_event() -> Result<()> {
let event = Event {
id: "init".to_string(),
agent_idx: Some(0),
msg: EventMsg::McpStartupComplete(McpStartupCompleteEvent {
ready: vec!["a".to_string()],
failed: vec![McpStartupFailure {
+6 -1
View File
@@ -1563,6 +1563,7 @@ impl ChatWidget {
use codex_core::protocol::FileChange;
self.app_event_tx.send(AppEvent::CodexEvent(Event {
agent_idx: Some(0),
id: "1".to_string(),
// msg: EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent {
// call_id: "1".to_string(),
@@ -1719,7 +1720,11 @@ impl ChatWidget {
}
pub(crate) fn handle_codex_event(&mut self, event: Event) {
let Event { id, msg } = event;
let Event {
id,
agent_idx: _,
msg,
} = event;
self.dispatch_event_msg(Some(id), msg, false);
}
+3
View File
@@ -36,6 +36,7 @@ pub(crate) fn spawn_agent(
eprintln!("{message}");
app_event_tx_clone.send(AppEvent::CodexEvent(Event {
id: "".to_string(),
agent_idx: Some(0),
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_idx: Some(0),
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_idx: Some(0),
msg: codex_core::protocol::EventMsg::SessionConfigured(session_configured),
};
app_event_tx_clone.send(AppEvent::CodexEvent(ev));
+160 -5
View File
@@ -99,6 +99,7 @@ fn resumed_initial_messages_render_history() {
let conversation_id = ConversationId::new();
let rollout_file = NamedTempFile::new().unwrap();
let configured = codex_core::protocol::SessionConfiguredEvent {
agent_idx: Some(0),
session_id: conversation_id,
model: "test-model".to_string(),
model_provider_id: "test-provider".to_string(),
@@ -110,10 +111,12 @@ fn resumed_initial_messages_render_history() {
history_entry_count: 0,
initial_messages: Some(vec![
EventMsg::UserMessage(UserMessageEvent {
agent_idx: Some(0),
message: "hello from user".to_string(),
images: None,
}),
EventMsg::AgentMessage(AgentMessageEvent {
agent_idx: Some(0),
message: "assistant reply".to_string(),
}),
]),
@@ -121,6 +124,7 @@ fn resumed_initial_messages_render_history() {
};
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "initial".into(),
msg: EventMsg::SessionConfigured(configured),
});
@@ -153,6 +157,7 @@ fn entered_review_mode_uses_request_hint() {
let (mut chat, mut rx, _ops) = make_chatwidget_manual();
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "review-start".into(),
msg: EventMsg::EnteredReviewMode(ReviewRequest {
target: ReviewTarget::BaseBranch {
@@ -174,6 +179,7 @@ fn entered_review_mode_defaults_to_current_changes_banner() {
let (mut chat, mut rx, _ops) = make_chatwidget_manual();
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "review-start".into(),
msg: EventMsg::EnteredReviewMode(ReviewRequest {
target: ReviewTarget::UncommittedChanges,
@@ -194,6 +200,7 @@ fn exited_review_mode_emits_results_and_finishes() {
let (mut chat, mut rx, _ops) = make_chatwidget_manual();
let review = ReviewOutputEvent {
agent_idx: Some(0),
findings: vec![ReviewFinding {
title: "[P1] Fix bug".to_string(),
body: "Something went wrong".to_string(),
@@ -210,8 +217,10 @@ fn exited_review_mode_emits_results_and_finishes() {
};
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "review-end".into(),
msg: EventMsg::ExitedReviewMode(ExitedReviewModeEvent {
agent_idx: Some(0),
review_output: Some(review),
}),
});
@@ -232,8 +241,10 @@ fn review_restores_context_window_indicator() {
let review_tokens = 12_030; // ~97% remaining after subtracting baseline.
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "token-before".into(),
msg: EventMsg::TokenCount(TokenCountEvent {
agent_idx: Some(0),
info: Some(make_token_info(pre_review_tokens, context_window)),
rate_limits: None,
}),
@@ -241,6 +252,7 @@ fn review_restores_context_window_indicator() {
assert_eq!(chat.bottom_pane.context_window_percent(), Some(30));
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "review-start".into(),
msg: EventMsg::EnteredReviewMode(ReviewRequest {
target: ReviewTarget::BaseBranch {
@@ -251,8 +263,10 @@ fn review_restores_context_window_indicator() {
});
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "token-review".into(),
msg: EventMsg::TokenCount(TokenCountEvent {
agent_idx: Some(0),
info: Some(make_token_info(review_tokens, context_window)),
rate_limits: None,
}),
@@ -260,8 +274,10 @@ fn review_restores_context_window_indicator() {
assert_eq!(chat.bottom_pane.context_window_percent(), Some(97));
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "review-end".into(),
msg: EventMsg::ExitedReviewMode(ExitedReviewModeEvent {
agent_idx: Some(0),
review_output: None,
}),
});
@@ -280,8 +296,10 @@ fn token_count_none_resets_context_indicator() {
let pre_compact_tokens = 12_700;
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "token-before".into(),
msg: EventMsg::TokenCount(TokenCountEvent {
agent_idx: Some(0),
info: Some(make_token_info(pre_compact_tokens, context_window)),
rate_limits: None,
}),
@@ -289,8 +307,10 @@ fn token_count_none_resets_context_indicator() {
assert_eq!(chat.bottom_pane.context_window_percent(), Some(30));
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "token-cleared".into(),
msg: EventMsg::TokenCount(TokenCountEvent {
agent_idx: Some(0),
info: None,
rate_limits: None,
}),
@@ -319,8 +339,10 @@ fn context_indicator_shows_used_tokens_when_window_unknown() {
};
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "token-usage".into(),
msg: EventMsg::TokenCount(TokenCountEvent {
agent_idx: Some(0),
info: Some(token_info),
rate_limits: None,
}),
@@ -669,6 +691,7 @@ fn exec_approval_emits_proposed_command_and_decision_history() {
// Trigger an exec approval request with a short, single-line command
let ev = ExecApprovalRequestEvent {
agent_idx: Some(0),
call_id: "call-short".into(),
turn_id: "turn-short".into(),
command: vec!["bash".into(), "-lc".into(), "echo hello world".into()],
@@ -680,6 +703,7 @@ fn exec_approval_emits_proposed_command_and_decision_history() {
parsed_cmd: vec![],
};
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "sub-short".into(),
msg: EventMsg::ExecApprovalRequest(ev),
});
@@ -713,6 +737,7 @@ fn exec_approval_decision_truncates_multiline_and_long_commands() {
// Multiline command: modal should show full command, history records decision only
let ev_multi = ExecApprovalRequestEvent {
agent_idx: Some(0),
call_id: "call-multi".into(),
turn_id: "turn-multi".into(),
command: vec!["bash".into(), "-lc".into(), "echo line1\necho line2".into()],
@@ -724,6 +749,7 @@ fn exec_approval_decision_truncates_multiline_and_long_commands() {
parsed_cmd: vec![],
};
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "sub-multi".into(),
msg: EventMsg::ExecApprovalRequest(ev_multi),
});
@@ -765,6 +791,7 @@ fn exec_approval_decision_truncates_multiline_and_long_commands() {
// Very long single-line command: decision snippet should be truncated <= 80 chars with trailing ...
let long = format!("echo {}", "a".repeat(200));
let ev_long = ExecApprovalRequestEvent {
agent_idx: Some(0),
call_id: "call-long".into(),
turn_id: "turn-long".into(),
command: vec!["bash".into(), "-lc".into(), long],
@@ -774,6 +801,7 @@ fn exec_approval_decision_truncates_multiline_and_long_commands() {
parsed_cmd: vec![],
};
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "sub-long".into(),
msg: EventMsg::ExecApprovalRequest(ev_long),
});
@@ -816,6 +844,7 @@ fn begin_exec_with_source(
interaction_input,
};
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: call_id.to_string(),
msg: EventMsg::ExecCommandBegin(event.clone()),
});
@@ -839,6 +868,7 @@ fn end_exec(
format!("{stdout}{stderr}")
};
let ExecCommandBeginEvent {
agent_idx: Some(0),
call_id,
turn_id,
command,
@@ -849,8 +879,10 @@ fn end_exec(
process_id,
} = begin_event;
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: call_id.clone(),
msg: EventMsg::ExecCommandEnd(ExecCommandEndEvent {
agent_idx: Some(0),
call_id,
process_id,
turn_id,
@@ -1237,8 +1269,10 @@ fn undo_success_events_render_info_messages() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "turn-1".to_string(),
msg: EventMsg::UndoStarted(UndoStartedEvent {
agent_idx: Some(0),
message: Some("Undo requested for the last turn...".to_string()),
}),
});
@@ -1248,8 +1282,10 @@ fn undo_success_events_render_info_messages() {
);
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "turn-1".to_string(),
msg: EventMsg::UndoCompleted(UndoCompletedEvent {
agent_idx: Some(0),
success: true,
message: None,
}),
@@ -1274,8 +1310,12 @@ fn undo_failure_events_render_error_message() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "turn-2".to_string(),
msg: EventMsg::UndoStarted(UndoStartedEvent { message: None }),
msg: EventMsg::UndoStarted(UndoStartedEvent {
agent_idx: Some(0),
message: None,
}),
});
assert!(
chat.bottom_pane.status_indicator_visible(),
@@ -1283,8 +1323,10 @@ fn undo_failure_events_render_error_message() {
);
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "turn-2".to_string(),
msg: EventMsg::UndoCompleted(UndoCompletedEvent {
agent_idx: Some(0),
success: false,
message: Some("Failed to restore workspace state.".to_string()),
}),
@@ -1309,8 +1351,12 @@ fn undo_started_hides_interrupt_hint() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "turn-hint".to_string(),
msg: EventMsg::UndoStarted(UndoStartedEvent { message: None }),
msg: EventMsg::UndoStarted(UndoStartedEvent {
agent_idx: Some(0),
message: None,
}),
});
let status = chat
@@ -1432,8 +1478,10 @@ fn view_image_tool_call_adds_history_cell() {
let image_path = chat.config.cwd.join("example.png");
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "sub-image".into(),
msg: EventMsg::ViewImageToolCall(ViewImageToolCallEvent {
agent_idx: Some(0),
call_id: "call-image".into(),
path: image_path,
}),
@@ -1457,8 +1505,10 @@ fn interrupt_exec_marks_failed_snapshot() {
// Simulate the task being aborted (as if ESC was pressed), which should
// cause the active exec cell to be finalized as failed and flushed.
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "call-int".into(),
msg: EventMsg::TurnAborted(codex_core::protocol::TurnAbortedEvent {
agent_idx: Some(0),
reason: TurnAbortReason::Interrupted,
}),
});
@@ -1482,16 +1532,20 @@ fn interrupted_turn_error_message_snapshot() {
// Simulate an in-progress task so the widget is in a running state.
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "task-1".into(),
msg: EventMsg::TaskStarted(TaskStartedEvent {
agent_idx: Some(0),
model_context_window: None,
}),
});
// Abort the turn (like pressing Esc) and drain inserted history.
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "task-1".into(),
msg: EventMsg::TurnAborted(codex_core::protocol::TurnAbortedEvent {
agent_idx: Some(0),
reason: TurnAbortReason::Interrupted,
}),
});
@@ -1961,6 +2015,7 @@ fn approval_modal_exec_snapshot() {
chat.config.approval_policy = AskForApproval::OnRequest;
// Inject an exec approval request to display the approval modal.
let ev = ExecApprovalRequestEvent {
agent_idx: Some(0),
call_id: "call-approve-cmd".into(),
turn_id: "turn-approve-cmd".into(),
command: vec!["bash".into(), "-lc".into(), "echo hello world".into()],
@@ -1972,6 +2027,7 @@ fn approval_modal_exec_snapshot() {
parsed_cmd: vec![],
};
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "sub-approve".into(),
msg: EventMsg::ExecApprovalRequest(ev),
});
@@ -2009,6 +2065,7 @@ fn approval_modal_exec_without_reason_snapshot() {
chat.config.approval_policy = AskForApproval::OnRequest;
let ev = ExecApprovalRequestEvent {
agent_idx: Some(0),
call_id: "call-approve-cmd-noreason".into(),
turn_id: "turn-approve-cmd-noreason".into(),
command: vec!["bash".into(), "-lc".into(), "echo hello world".into()],
@@ -2018,6 +2075,7 @@ fn approval_modal_exec_without_reason_snapshot() {
parsed_cmd: vec![],
};
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "sub-approve-noreason".into(),
msg: EventMsg::ExecApprovalRequest(ev),
});
@@ -2050,6 +2108,7 @@ fn approval_modal_patch_snapshot() {
},
);
let ev = ApplyPatchApprovalRequestEvent {
agent_idx: Some(0),
call_id: "call-approve-patch".into(),
turn_id: "turn-approve-patch".into(),
changes,
@@ -2057,6 +2116,7 @@ fn approval_modal_patch_snapshot() {
grant_root: Some(PathBuf::from("/tmp")),
};
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "sub-approve-patch".into(),
msg: EventMsg::ApplyPatchApprovalRequest(ev),
});
@@ -2091,8 +2151,10 @@ fn interrupt_restores_queued_messages_into_composer() {
// Deliver a TurnAborted event with Interrupted reason (as if Esc was pressed).
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "turn-1".into(),
msg: EventMsg::TurnAborted(codex_core::protocol::TurnAbortedEvent {
agent_idx: Some(0),
reason: TurnAbortReason::Interrupted,
}),
});
@@ -2129,8 +2191,10 @@ fn interrupt_prepends_queued_messages_before_existing_composer_text() {
chat.refresh_queued_user_messages();
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "turn-1".into(),
msg: EventMsg::TurnAborted(codex_core::protocol::TurnAbortedEvent {
agent_idx: Some(0),
reason: TurnAbortReason::Interrupted,
}),
});
@@ -2174,14 +2238,18 @@ fn ui_snapshots_small_heights_task_running() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
// Activate status line
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "task-1".into(),
msg: EventMsg::TaskStarted(TaskStartedEvent {
agent_idx: Some(0),
model_context_window: None,
}),
});
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "task-1".into(),
msg: EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent {
agent_idx: Some(0),
delta: "**Thinking**".into(),
}),
});
@@ -2205,21 +2273,26 @@ fn status_widget_and_approval_modal_snapshot() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
// Begin a running task so the status indicator would be active.
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "task-1".into(),
msg: EventMsg::TaskStarted(TaskStartedEvent {
agent_idx: Some(0),
model_context_window: None,
}),
});
// Provide a deterministic header for the status line.
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "task-1".into(),
msg: EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent {
agent_idx: Some(0),
delta: "**Analyzing**".into(),
}),
});
// Now show an approval modal (e.g. exec approval).
let ev = ExecApprovalRequestEvent {
agent_idx: Some(0),
call_id: "call-approve-exec".into(),
turn_id: "turn-approve-exec".into(),
command: vec!["echo".into(), "hello world".into()],
@@ -2231,6 +2304,7 @@ fn status_widget_and_approval_modal_snapshot() {
parsed_cmd: vec![],
};
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "sub-approve-exec".into(),
msg: EventMsg::ExecApprovalRequest(ev),
});
@@ -2252,15 +2326,19 @@ fn status_widget_active_snapshot() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
// Activate the status indicator by simulating a task start.
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "task-1".into(),
msg: EventMsg::TaskStarted(TaskStartedEvent {
agent_idx: Some(0),
model_context_window: None,
}),
});
// Provide a deterministic header via a bold reasoning chunk.
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "task-1".into(),
msg: EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent {
agent_idx: Some(0),
delta: "**Analyzing**".into(),
}),
});
@@ -2279,8 +2357,10 @@ fn background_event_updates_status_header() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "bg-1".into(),
msg: EventMsg::BackgroundEvent(BackgroundEventEvent {
agent_idx: Some(0),
message: "Waiting for `vim`".to_string(),
}),
});
@@ -2303,6 +2383,7 @@ fn apply_patch_events_emit_history_cells() {
},
);
let ev = ApplyPatchApprovalRequestEvent {
agent_idx: Some(0),
call_id: "c1".into(),
turn_id: "turn-c1".into(),
changes,
@@ -2310,6 +2391,7 @@ fn apply_patch_events_emit_history_cells() {
grant_root: None,
};
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "s1".into(),
msg: EventMsg::ApplyPatchApprovalRequest(ev),
});
@@ -2344,12 +2426,14 @@ fn apply_patch_events_emit_history_cells() {
},
);
let begin = PatchApplyBeginEvent {
agent_idx: Some(0),
call_id: "c1".into(),
turn_id: "turn-c1".into(),
auto_approved: true,
changes: changes2,
};
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "s1".into(),
msg: EventMsg::PatchApplyBegin(begin),
});
@@ -2370,6 +2454,7 @@ fn apply_patch_events_emit_history_cells() {
},
);
let end = PatchApplyEndEvent {
agent_idx: Some(0),
call_id: "c1".into(),
turn_id: "turn-c1".into(),
stdout: "ok\n".into(),
@@ -2378,6 +2463,7 @@ fn apply_patch_events_emit_history_cells() {
changes: end_changes,
};
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "s1".into(),
msg: EventMsg::PatchApplyEnd(end),
});
@@ -2400,8 +2486,10 @@ fn apply_patch_manual_approval_adjusts_header() {
},
);
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "s1".into(),
msg: EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent {
agent_idx: Some(0),
call_id: "c1".into(),
turn_id: "turn-c1".into(),
changes: proposed_changes,
@@ -2419,8 +2507,10 @@ fn apply_patch_manual_approval_adjusts_header() {
},
);
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "s1".into(),
msg: EventMsg::PatchApplyBegin(PatchApplyBeginEvent {
agent_idx: Some(0),
call_id: "c1".into(),
turn_id: "turn-c1".into(),
auto_approved: false,
@@ -2449,8 +2539,10 @@ fn apply_patch_manual_flow_snapshot() {
},
);
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "s1".into(),
msg: EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent {
agent_idx: Some(0),
call_id: "c1".into(),
turn_id: "turn-c1".into(),
changes: proposed_changes,
@@ -2472,8 +2564,10 @@ fn apply_patch_manual_flow_snapshot() {
},
);
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "s1".into(),
msg: EventMsg::PatchApplyBegin(PatchApplyBeginEvent {
agent_idx: Some(0),
call_id: "c1".into(),
turn_id: "turn-c1".into(),
auto_approved: false,
@@ -2502,6 +2596,7 @@ fn apply_patch_approval_sends_op_with_submission_id() {
},
);
let ev = ApplyPatchApprovalRequestEvent {
agent_idx: Some(0),
call_id: "call-999".into(),
turn_id: "turn-999".into(),
changes,
@@ -2509,6 +2604,7 @@ fn apply_patch_approval_sends_op_with_submission_id() {
grant_root: None,
};
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "sub-123".into(),
msg: EventMsg::ApplyPatchApprovalRequest(ev),
});
@@ -2540,8 +2636,10 @@ fn apply_patch_full_flow_integration_like() {
FileChange::Add { content: "".into() },
);
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "sub-xyz".into(),
msg: EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent {
agent_idx: Some(0),
call_id: "call-1".into(),
turn_id: "turn-call-1".into(),
changes,
@@ -2581,8 +2679,10 @@ fn apply_patch_full_flow_integration_like() {
FileChange::Add { content: "".into() },
);
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "sub-xyz".into(),
msg: EventMsg::PatchApplyBegin(PatchApplyBeginEvent {
agent_idx: Some(0),
call_id: "call-1".into(),
turn_id: "turn-call-1".into(),
auto_approved: false,
@@ -2595,8 +2695,10 @@ fn apply_patch_full_flow_integration_like() {
FileChange::Add { content: "".into() },
);
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "sub-xyz".into(),
msg: EventMsg::PatchApplyEnd(PatchApplyEndEvent {
agent_idx: Some(0),
call_id: "call-1".into(),
turn_id: "turn-call-1".into(),
stdout: String::from("ok"),
@@ -2620,8 +2722,10 @@ fn apply_patch_untrusted_shows_approval_modal() {
FileChange::Add { content: "".into() },
);
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "sub-1".into(),
msg: EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent {
agent_idx: Some(0),
call_id: "call-1".into(),
turn_id: "turn-call-1".into(),
changes,
@@ -2669,8 +2773,10 @@ fn apply_patch_request_shows_diff_summary() {
},
);
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "sub-apply".into(),
msg: EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent {
agent_idx: Some(0),
call_id: "call-apply".into(),
turn_id: "turn-apply".into(),
changes,
@@ -2739,6 +2845,7 @@ fn plan_update_renders_history_cell() {
],
};
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "sub-1".into(),
msg: EventMsg::PlanUpdate(update),
});
@@ -2760,8 +2867,10 @@ fn stream_error_updates_status_indicator() {
chat.bottom_pane.set_task_running(true);
let msg = "Reconnecting... 2/5";
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "sub-1".into(),
msg: EventMsg::StreamError(StreamErrorEvent {
agent_idx: Some(0),
message: msg.to_string(),
codex_error_info: Some(CodexErrorInfo::Other),
}),
@@ -2783,8 +2892,10 @@ fn stream_error_updates_status_indicator() {
fn warning_event_adds_warning_history_cell() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "sub-1".into(),
msg: EventMsg::Warning(WarningEvent {
agent_idx: Some(0),
message: "test warning message".to_string(),
}),
});
@@ -2804,32 +2915,40 @@ fn multiple_agent_messages_in_single_turn_emit_multiple_headers() {
// Begin turn
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "s1".into(),
msg: EventMsg::TaskStarted(TaskStartedEvent {
agent_idx: Some(0),
model_context_window: None,
}),
});
// First finalized assistant message
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "s1".into(),
msg: EventMsg::AgentMessage(AgentMessageEvent {
agent_idx: Some(0),
message: "First message".into(),
}),
});
// Second finalized assistant message in the same turn
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "s1".into(),
msg: EventMsg::AgentMessage(AgentMessageEvent {
agent_idx: Some(0),
message: "Second message".into(),
}),
});
// End turn
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "s1".into(),
msg: EventMsg::TaskComplete(TaskCompleteEvent {
agent_idx: Some(0),
last_agent_message: None,
}),
});
@@ -2858,14 +2977,18 @@ fn final_reasoning_then_message_without_deltas_are_rendered() {
// No deltas; only final reasoning followed by final message.
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "s1".into(),
msg: EventMsg::AgentReasoning(AgentReasoningEvent {
agent_idx: Some(0),
text: "I will first analyze the request.".into(),
}),
});
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "s1".into(),
msg: EventMsg::AgentMessage(AgentMessageEvent {
agent_idx: Some(0),
message: "Here is the result.".into(),
}),
});
@@ -2885,47 +3008,61 @@ fn deltas_then_same_final_message_are_rendered_snapshot() {
// Stream some reasoning deltas first.
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "s1".into(),
msg: EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent {
agent_idx: Some(0),
delta: "I will ".into(),
}),
});
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "s1".into(),
msg: EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent {
agent_idx: Some(0),
delta: "first analyze the ".into(),
}),
});
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "s1".into(),
msg: EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent {
agent_idx: Some(0),
delta: "request.".into(),
}),
});
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "s1".into(),
msg: EventMsg::AgentReasoning(AgentReasoningEvent {
agent_idx: Some(0),
text: "request.".into(),
}),
});
// Then stream answer deltas, followed by the exact same final message.
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "s1".into(),
msg: EventMsg::AgentMessageDelta(AgentMessageDeltaEvent {
agent_idx: Some(0),
delta: "Here is the ".into(),
}),
});
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "s1".into(),
msg: EventMsg::AgentMessageDelta(AgentMessageDeltaEvent {
agent_idx: Some(0),
delta: "result.".into(),
}),
});
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "s1".into(),
msg: EventMsg::AgentMessage(AgentMessageEvent {
agent_idx: Some(0),
message: "Here is the result.".into(),
}),
});
@@ -2946,9 +3083,9 @@ fn deltas_then_same_final_message_are_rendered_snapshot() {
#[test]
fn chatwidget_exec_and_status_layout_vt100_snapshot() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
chat.handle_codex_event(Event {
chat.handle_codex_event(Event { agent_idx: Some(0),
id: "t1".into(),
msg: EventMsg::AgentMessage(AgentMessageEvent { message: "I’m going to search the repo for where “Change Approved” is rendered to update that view.".into() }),
msg: EventMsg::AgentMessage(AgentMessageEvent { agent_idx: Some(0), message: "I’m going to search the repo for where “Change Approved” is rendered to update that view.".into() }),
});
let command = vec!["bash".into(), "-lc".into(), "rg \"Change Approved\"".into()];
@@ -2966,8 +3103,10 @@ fn chatwidget_exec_and_status_layout_vt100_snapshot() {
];
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "c1".into(),
msg: EventMsg::ExecCommandBegin(ExecCommandBeginEvent {
agent_idx: Some(0),
call_id: "c1".into(),
process_id: None,
turn_id: "turn-1".into(),
@@ -2979,8 +3118,10 @@ fn chatwidget_exec_and_status_layout_vt100_snapshot() {
}),
});
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "c1".into(),
msg: EventMsg::ExecCommandEnd(ExecCommandEndEvent {
agent_idx: Some(0),
call_id: "c1".into(),
process_id: None,
turn_id: "turn-1".into(),
@@ -2998,14 +3139,18 @@ fn chatwidget_exec_and_status_layout_vt100_snapshot() {
}),
});
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "t1".into(),
msg: EventMsg::TaskStarted(TaskStartedEvent {
agent_idx: Some(0),
model_context_window: None,
}),
});
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "t1".into(),
msg: EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent {
agent_idx: Some(0),
delta: "**Investigating rendering code**".into(),
}),
});
@@ -3042,8 +3187,10 @@ fn chatwidget_markdown_code_blocks_vt100_snapshot() {
// Simulate a final agent message via streaming deltas instead of a single message
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "t1".into(),
msg: EventMsg::TaskStarted(TaskStartedEvent {
agent_idx: Some(0),
model_context_window: None,
}),
});
@@ -3090,8 +3237,12 @@ printf 'fenced within fenced\n'
}
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "t1".into(),
msg: EventMsg::AgentMessageDelta(AgentMessageDeltaEvent { delta }),
msg: EventMsg::AgentMessageDelta(AgentMessageDeltaEvent {
agent_idx: Some(0),
delta,
}),
});
// Drive commit ticks and drain emitted history lines into the vt100 buffer.
loop {
@@ -3113,8 +3264,10 @@ printf 'fenced within fenced\n'
// Finalize the stream without sending a final AgentMessage, to flush any tail.
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "t1".into(),
msg: EventMsg::TaskComplete(TaskCompleteEvent {
agent_idx: Some(0),
last_agent_message: None,
}),
});
@@ -3130,8 +3283,10 @@ printf 'fenced within fenced\n'
fn chatwidget_tall() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
chat.handle_codex_event(Event {
agent_idx: Some(0),
id: "t1".into(),
msg: EventMsg::TaskStarted(TaskStartedEvent {
agent_idx: Some(0),
model_context_window: None,
}),
});