use std::time::Duration; use std::time::Instant; use codex_protocol::parse_command::ParsedCommand; #[derive(Clone, Debug)] pub(crate) struct CommandOutput { pub(crate) exit_code: i32, pub(crate) stdout: String, pub(crate) stderr: String, 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) start_time: Option, pub(crate) duration: Option, } #[derive(Debug)] pub(crate) struct ExecCell { pub(crate) calls: Vec, } impl ExecCell { pub(crate) fn new(call: ExecCall) -> Self { Self { calls: vec![call] } } pub(crate) fn with_added_call( &self, call_id: String, command: Vec, parsed: Vec, ) -> Option { let call = ExecCall { call_id, command, parsed, output: None, start_time: Some(Instant::now()), duration: None, }; if self.is_exploring_cell() && Self::is_exploring_call(&call) { Some(Self { calls: [self.calls.clone(), vec![call]].concat(), }) } 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, stdout: String::new(), stderr: String::new(), formatted_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 iter_calls(&self) -> impl Iterator { self.calls.iter() } pub(super) fn is_exploring_call(call: &ExecCall) -> bool { !call.parsed.is_empty() && call.parsed.iter().all(|p| { matches!( p, ParsedCommand::Read { .. } | ParsedCommand::ListFiles { .. } | ParsedCommand::Search { .. } ) }) } }