use std::time::Duration; use std::time::Instant; use codex_core::protocol::ExecCommandSource; use codex_protocol::parse_command::ParsedCommand; #[derive(Clone, Debug, Default)] pub(crate) struct CommandOutput { pub(crate) exit_code: i32, /// The aggregated stderr + stdout interleaved. pub(crate) aggregated_output: String, /// The formatted output of the command, as seen by the model. pub(crate) formatted_output: String, } #[derive(Debug, Clone)] pub(crate) struct ExecCall { pub(crate) call_id: String, pub(crate) command: Vec, pub(crate) parsed: Vec, pub(crate) output: Option, pub(crate) source: ExecCommandSource, pub(crate) start_time: Option, pub(crate) duration: Option, pub(crate) interaction_input: Option, } #[derive(Debug)] pub(crate) struct ExecCell { pub(crate) calls: Vec, animations_enabled: bool, } impl ExecCell { pub(crate) fn new(call: ExecCall, animations_enabled: bool) -> Self { Self { calls: vec![call], animations_enabled, } } pub(crate) fn with_added_call( &self, call_id: String, command: Vec, parsed: Vec, source: ExecCommandSource, interaction_input: Option, ) -> Option { let call = ExecCall { call_id, command, parsed, output: None, source, start_time: Some(Instant::now()), duration: None, interaction_input, }; if self.is_exploring_cell() && Self::is_exploring_call(&call) { Some(Self { calls: [self.calls.clone(), vec![call]].concat(), animations_enabled: self.animations_enabled, }) } else { None } } pub(crate) fn complete_call( &mut self, call_id: &str, output: CommandOutput, duration: Duration, ) { if let Some(call) = self.calls.iter_mut().rev().find(|c| c.call_id == call_id) { call.output = Some(output); call.duration = Some(duration); call.start_time = None; } } pub(crate) fn should_flush(&self) -> bool { !self.is_exploring_cell() && self.calls.iter().all(|c| c.output.is_some()) } pub(crate) fn mark_failed(&mut self) { for call in self.calls.iter_mut() { if call.output.is_none() { let elapsed = call .start_time .map(|st| st.elapsed()) .unwrap_or_else(|| Duration::from_millis(0)); call.start_time = None; call.duration = Some(elapsed); call.output = Some(CommandOutput { exit_code: 1, formatted_output: String::new(), aggregated_output: String::new(), }); } } } pub(crate) fn is_exploring_cell(&self) -> bool { self.calls.iter().all(Self::is_exploring_call) } pub(crate) fn is_active(&self) -> bool { self.calls.iter().any(|c| c.output.is_none()) } pub(crate) fn active_start_time(&self) -> Option { self.calls .iter() .find(|c| c.output.is_none()) .and_then(|c| c.start_time) } pub(crate) fn animations_enabled(&self) -> bool { self.animations_enabled } pub(crate) fn iter_calls(&self) -> impl Iterator { self.calls.iter() } pub(super) fn is_exploring_call(call: &ExecCall) -> bool { !matches!(call.source, ExecCommandSource::UserShell) && !call.parsed.is_empty() && call.parsed.iter().all(|p| { matches!( p, ParsedCommand::Read { .. } | ParsedCommand::ListFiles { .. } | ParsedCommand::Search { .. } ) }) } } impl ExecCall { pub(crate) fn is_user_shell_command(&self) -> bool { matches!(self.source, ExecCommandSource::UserShell) } pub(crate) fn is_unified_exec_interaction(&self) -> bool { matches!(self.source, ExecCommandSource::UnifiedExecInteraction) } }