mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
merge: upstream main into upstream-merge (preserve core/TUI, adapt exec). build-fast ok
This commit is contained in:
@@ -43,7 +43,7 @@ use crate::protocol::WebSearchCompleteEvent;
|
||||
use codex_protocol::models::WebSearchAction;
|
||||
use codex_protocol::protocol::RolloutItem;
|
||||
|
||||
mod compact;
|
||||
pub mod compact;
|
||||
use self::compact::build_compacted_history;
|
||||
use self::compact::collect_user_messages;
|
||||
|
||||
|
||||
@@ -185,7 +185,7 @@ fn map_summary(s: ReasoningSummaryConfig) -> codex_protocol::config_types::Reaso
|
||||
}
|
||||
}
|
||||
|
||||
fn content_items_to_text(content: &[ContentItem]) -> Option<String> {
|
||||
pub fn content_items_to_text(content: &[ContentItem]) -> Option<String> {
|
||||
let mut pieces = Vec::new();
|
||||
for item in content {
|
||||
match item {
|
||||
@@ -217,7 +217,7 @@ pub(crate) fn collect_user_messages(items: &[ResponseItem]) -> Vec<String> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn is_session_prefix_message(text: &str) -> bool {
|
||||
pub fn is_session_prefix_message(text: &str) -> bool {
|
||||
matches!(
|
||||
InputMessageKind::from(("user", text)),
|
||||
InputMessageKind::UserInstructions | InputMessageKind::EnvironmentContext
|
||||
|
||||
@@ -23,3 +23,6 @@ pub use session_manager::result_into_payload;
|
||||
// Upstream removed the global SESSION_MANAGER; we now manage a per-session
|
||||
// instance. Keep the ExecSessionManager name for minimal churn.
|
||||
pub type ExecSessionManager = session_manager::SessionManager;
|
||||
|
||||
// Re-export ExecCommandSession for crate-internal consumers.
|
||||
pub(crate) use exec_command_session::ExecCommandSession;
|
||||
|
||||
+40
-30
@@ -1,34 +1,27 @@
|
||||
//! Root of the `codex-core` library.
|
||||
//! This module provides the core functionality for the codex CLI.
|
||||
|
||||
// Prevent accidental direct writes to stdout/stderr in library code. All
|
||||
// user-visible output must go through the appropriate abstraction (e.g.,
|
||||
// the TUI or the tracing stack).
|
||||
#![deny(clippy::print_stdout, clippy::print_stderr)]
|
||||
|
||||
mod agent_tool;
|
||||
pub mod agent_defaults;
|
||||
mod apply_patch;
|
||||
pub mod auth;
|
||||
mod bash;
|
||||
pub mod bash;
|
||||
mod chat_completions;
|
||||
mod client;
|
||||
mod client_common;
|
||||
pub mod codex;
|
||||
pub mod debug_logger;
|
||||
pub mod slash_commands;
|
||||
pub use codex::Codex;
|
||||
pub use codex::CodexSpawnOk;
|
||||
mod codex_conversation;
|
||||
pub mod token_data;
|
||||
pub use codex_conversation::CodexConversation;
|
||||
pub mod config;
|
||||
pub mod config_edit;
|
||||
pub mod config_profile;
|
||||
pub mod config_types;
|
||||
pub mod config_edit;
|
||||
mod conversation_history;
|
||||
mod dry_run_guard;
|
||||
pub mod custom_prompts;
|
||||
pub mod debug_logger;
|
||||
mod environment_context;
|
||||
pub mod error;
|
||||
pub mod exec;
|
||||
@@ -36,21 +29,31 @@ mod exec_command;
|
||||
pub mod exec_env;
|
||||
mod flags;
|
||||
pub mod git_info;
|
||||
pub mod git_worktree;
|
||||
mod image_comparison;
|
||||
pub mod internal_storage;
|
||||
mod is_safe_command;
|
||||
pub mod landlock;
|
||||
pub mod mcp_connection_manager;
|
||||
pub mod http_client;
|
||||
mod mcp_connection_manager;
|
||||
mod mcp_tool_call;
|
||||
mod message_history;
|
||||
mod model_provider_info;
|
||||
pub mod agent_defaults;
|
||||
mod agent_tool;
|
||||
mod dry_run_guard;
|
||||
mod image_comparison;
|
||||
pub mod git_worktree;
|
||||
pub mod slash_commands;
|
||||
pub mod parse_command;
|
||||
mod truncate;
|
||||
mod unified_exec;
|
||||
mod user_instructions;
|
||||
pub use model_provider_info::BUILT_IN_OSS_MODEL_PROVIDER_ID;
|
||||
pub use model_provider_info::ModelProviderInfo;
|
||||
pub use model_provider_info::WireApi;
|
||||
pub use model_provider_info::built_in_model_providers;
|
||||
pub use model_provider_info::create_oss_provider_with_base_url;
|
||||
mod conversation_manager;
|
||||
pub mod protocol;
|
||||
mod event_mapping;
|
||||
pub mod review_format;
|
||||
pub use codex_protocol::protocol::InitialHistory;
|
||||
@@ -72,32 +75,39 @@ pub mod shell;
|
||||
pub mod spawn;
|
||||
pub mod terminal;
|
||||
mod tool_apply_patch;
|
||||
mod user_instructions;
|
||||
pub mod turn_diff_tracker;
|
||||
pub use rollout::ARCHIVED_SESSIONS_SUBDIR;
|
||||
pub use rollout::RolloutRecorder;
|
||||
pub use rollout::SESSIONS_SUBDIR;
|
||||
pub use rollout::SessionMeta;
|
||||
pub use rollout::find_conversation_path_by_id_str;
|
||||
pub use rollout::list::ConversationItem;
|
||||
pub use rollout::list::ConversationsPage;
|
||||
pub use rollout::list::Cursor;
|
||||
pub use rollout::list::find_conversation_path_by_id_str;
|
||||
pub use rollout::recorder::RolloutRecorder;
|
||||
mod user_notification;
|
||||
pub mod util;
|
||||
pub mod http_client;
|
||||
|
||||
pub use apply_patch::CODEX_APPLY_PATCH_ARG1;
|
||||
pub use safety::get_platform_sandbox;
|
||||
// Use our local protocol definitions to preserve custom events and input items.
|
||||
pub mod protocol;
|
||||
// Optionally expose upstream protocol config enums for callers that need them.
|
||||
// Use our internal protocol module for crate-internal types and helpers.
|
||||
// External callers should rely on specific re-exports below.
|
||||
// Re-export protocol config enums to ensure call sites can use the same types
|
||||
// as those in the protocol crate when constructing protocol messages.
|
||||
pub use codex_protocol::config_types as protocol_config_types;
|
||||
// Re-export protocol models for compatibility with existing imports.
|
||||
// Preserve `codex_core::models::...` imports as an alias to the protocol models.
|
||||
pub use codex_protocol::models as models;
|
||||
// Public re-exports for API compatibility with downstream users and tests.
|
||||
// Keep these stable to avoid breaking callers.
|
||||
pub use crate::client::ModelClient;
|
||||
pub use crate::client_common::Prompt;
|
||||
pub use crate::client_common::TextFormat;
|
||||
pub use crate::client_common::ResponseEvent;
|
||||
pub use crate::client_common::ResponseStream;
|
||||
// Upstream also exports REVIEW_PROMPT; include it to preserve compatibility.
|
||||
pub use crate::client_common::REVIEW_PROMPT;
|
||||
|
||||
pub use client::ModelClient;
|
||||
pub use client_common::Prompt;
|
||||
pub use client_common::TextFormat;
|
||||
pub use client_common::REVIEW_PROMPT;
|
||||
pub use client_common::ResponseEvent;
|
||||
pub use client_common::ResponseStream;
|
||||
pub use codex::compact::content_items_to_text;
|
||||
pub use codex::compact::is_session_prefix_message;
|
||||
pub use codex_protocol::models::ContentItem;
|
||||
pub use codex_protocol::models::LocalShellAction;
|
||||
pub use codex_protocol::models::LocalShellExecAction;
|
||||
pub use codex_protocol::models::LocalShellStatus;
|
||||
pub use codex_protocol::models::ReasoningItemContent;
|
||||
pub use codex_protocol::models::ResponseItem;
|
||||
|
||||
@@ -22,14 +22,14 @@ pub fn format_review_findings_block(
|
||||
selection: Option<&[bool]>,
|
||||
) -> String {
|
||||
let mut lines: Vec<String> = Vec::new();
|
||||
lines.push(String::new());
|
||||
|
||||
// Header
|
||||
let header = if findings.len() > 1 {
|
||||
"Full review comments:"
|
||||
if findings.len() > 1 {
|
||||
lines.push("Full review comments:".to_string());
|
||||
} else {
|
||||
"Review comment:"
|
||||
};
|
||||
lines.push(header.to_string());
|
||||
lines.push("Review comment:".to_string());
|
||||
}
|
||||
|
||||
for (idx, item) in findings.iter().enumerate() {
|
||||
lines.push(String::new());
|
||||
|
||||
@@ -74,7 +74,7 @@ async fn compact_resume_and_fork_preserve_model_history_view() {
|
||||
"compact+resume test expects resumed path {resumed_path:?} to exist",
|
||||
);
|
||||
|
||||
let forked = fork_conversation(&manager, &config, resumed_path, 1).await;
|
||||
let forked = fork_conversation(&manager, &config, resumed_path, 2).await;
|
||||
user_turn(&forked, "AFTER_FORK").await;
|
||||
|
||||
// 3. Capture the requests to the model and validate the history slices.
|
||||
@@ -100,17 +100,15 @@ async fn compact_resume_and_fork_preserve_model_history_view() {
|
||||
"after-resume input should have at least as many items as after-compact",
|
||||
);
|
||||
assert_eq!(compact_arr.as_slice(), &resume_arr[..compact_arr.len()]);
|
||||
eprint!(
|
||||
"len of compact: {}, len of fork: {}",
|
||||
compact_arr.len(),
|
||||
fork_arr.len()
|
||||
);
|
||||
eprintln!("input_after_fork:{}", json!(input_after_fork));
|
||||
|
||||
assert!(
|
||||
compact_arr.len() <= fork_arr.len(),
|
||||
"after-fork input should have at least as many items as after-compact",
|
||||
);
|
||||
assert_eq!(compact_arr.as_slice(), &fork_arr[..compact_arr.len()]);
|
||||
assert_eq!(
|
||||
&compact_arr.as_slice()[..compact_arr.len()],
|
||||
&fork_arr[..compact_arr.len()]
|
||||
);
|
||||
|
||||
let prompt = requests[0]["instructions"]
|
||||
.as_str()
|
||||
@@ -824,14 +822,15 @@ async fn resume_conversation(
|
||||
conversation
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
async fn fork_conversation(
|
||||
manager: &ConversationManager,
|
||||
config: &Config,
|
||||
path: std::path::PathBuf,
|
||||
back_steps: usize,
|
||||
nth_user_message: usize,
|
||||
) -> Arc<CodexConversation> {
|
||||
let NewConversation { conversation, .. } = manager
|
||||
.fork_conversation(back_steps, config.clone(), path)
|
||||
.fork_conversation(nth_user_message, config.clone(), path)
|
||||
.await
|
||||
.expect("fork conversation");
|
||||
conversation
|
||||
|
||||
@@ -5,6 +5,8 @@ use codex_core::ModelProviderInfo;
|
||||
use codex_core::NewConversation;
|
||||
use codex_core::ResponseItem;
|
||||
use codex_core::built_in_model_providers;
|
||||
use codex_core::content_items_to_text;
|
||||
use codex_core::is_session_prefix_message;
|
||||
use codex_core::protocol::ConversationPathResponseEvent;
|
||||
use codex_core::protocol::EventMsg;
|
||||
use codex_core::protocol::InputItem;
|
||||
@@ -104,13 +106,16 @@ async fn fork_conversation_twice_drops_to_first_message() {
|
||||
items
|
||||
};
|
||||
|
||||
// Compute expected prefixes after each fork by truncating base rollout at nth-from-last user input.
|
||||
// Compute expected prefixes after each fork by truncating base rollout
|
||||
// strictly before the nth user input (0-based).
|
||||
let base_items = read_items(&base_path);
|
||||
let find_user_input_positions = |items: &[RolloutItem]| -> Vec<usize> {
|
||||
let mut pos = Vec::new();
|
||||
for (i, it) in items.iter().enumerate() {
|
||||
if let RolloutItem::ResponseItem(ResponseItem::Message { role, content, .. }) = it
|
||||
&& role == "user"
|
||||
&& content_items_to_text(content)
|
||||
.is_some_and(|text| !is_session_prefix_message(&text))
|
||||
{
|
||||
// Consider any user message as an input boundary; recorder stores both EventMsg and ResponseItem.
|
||||
// We specifically look for input items, which are represented as ContentItem::InputText.
|
||||
@@ -126,11 +131,8 @@ async fn fork_conversation_twice_drops_to_first_message() {
|
||||
};
|
||||
let user_inputs = find_user_input_positions(&base_items);
|
||||
|
||||
// After dropping last user input (n=1), cut strictly before that input if present, else empty.
|
||||
let cut1 = user_inputs
|
||||
.get(user_inputs.len().saturating_sub(1))
|
||||
.copied()
|
||||
.unwrap_or(0);
|
||||
// After cutting at nth user input (n=1 → second user message), cut strictly before that input.
|
||||
let cut1 = user_inputs.get(1).copied().unwrap_or(0);
|
||||
let expected_after_first: Vec<RolloutItem> = base_items[..cut1].to_vec();
|
||||
|
||||
// After dropping again (n=1 on fork1), compute expected relative to fork1's rollout.
|
||||
@@ -161,12 +163,12 @@ async fn fork_conversation_twice_drops_to_first_message() {
|
||||
serde_json::to_value(&expected_after_first).unwrap()
|
||||
);
|
||||
|
||||
// Fork again with n=1 → drops the (new) last user message, leaving only the first.
|
||||
// Fork again with n=0 → drops the (new) last user message, leaving only the first.
|
||||
let NewConversation {
|
||||
conversation: codex_fork2,
|
||||
..
|
||||
} = conversation_manager
|
||||
.fork_conversation(1, config_for_fork.clone(), fork1_path.clone())
|
||||
.fork_conversation(0, config_for_fork.clone(), fork1_path.clone())
|
||||
.await
|
||||
.expect("fork 2");
|
||||
|
||||
|
||||
@@ -20,10 +20,13 @@ use codex_core::protocol::McpToolCallEndEvent;
|
||||
use codex_core::protocol::PatchApplyBeginEvent;
|
||||
use codex_core::protocol::PatchApplyEndEvent;
|
||||
use codex_core::protocol::SessionConfiguredEvent;
|
||||
// Stream errors are surfaced via Error events in our fork
|
||||
use codex_core::protocol::TaskCompleteEvent;
|
||||
use codex_protocol::protocol::TurnAbortReason;
|
||||
use codex_core::protocol::TurnDiffEvent;
|
||||
use codex_core::protocol::WebSearchBeginEvent;
|
||||
use codex_core::protocol::WebSearchCompleteEvent;
|
||||
use codex_protocol::num_format::format_with_separators;
|
||||
use owo_colors::OwoColorize;
|
||||
use owo_colors::Style;
|
||||
use shlex::try_join;
|
||||
@@ -63,8 +66,6 @@ pub(crate) struct EventProcessorWithHumanOutput {
|
||||
reasoning_started: bool,
|
||||
raw_reasoning_started: bool,
|
||||
last_message_path: Option<PathBuf>,
|
||||
had_error: bool,
|
||||
answer_bullet_filter: LeadingBulletFilter,
|
||||
}
|
||||
|
||||
impl EventProcessorWithHumanOutput {
|
||||
@@ -93,8 +94,6 @@ impl EventProcessorWithHumanOutput {
|
||||
reasoning_started: false,
|
||||
raw_reasoning_started: false,
|
||||
last_message_path,
|
||||
had_error: false,
|
||||
answer_bullet_filter: LeadingBulletFilter::default(),
|
||||
}
|
||||
} else {
|
||||
Self {
|
||||
@@ -113,8 +112,6 @@ impl EventProcessorWithHumanOutput {
|
||||
reasoning_started: false,
|
||||
raw_reasoning_started: false,
|
||||
last_message_path,
|
||||
had_error: false,
|
||||
answer_bullet_filter: LeadingBulletFilter::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,99 +126,6 @@ struct PatchApplyBegin {
|
||||
auto_approved: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum LeadingBulletState {
|
||||
Pending,
|
||||
Removed,
|
||||
Keep,
|
||||
}
|
||||
|
||||
impl Default for LeadingBulletState {
|
||||
fn default() -> Self {
|
||||
Self::Pending
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct LeadingBulletFilter {
|
||||
state: LeadingBulletState,
|
||||
buffer: String,
|
||||
}
|
||||
|
||||
impl LeadingBulletFilter {
|
||||
fn reset(&mut self) {
|
||||
self.state = LeadingBulletState::Pending;
|
||||
self.buffer.clear();
|
||||
}
|
||||
|
||||
fn ingest_delta(&mut self, delta: &str) -> Option<String> {
|
||||
match self.state {
|
||||
LeadingBulletState::Pending => {
|
||||
self.buffer.push_str(delta);
|
||||
self.finalize_pending()
|
||||
}
|
||||
LeadingBulletState::Removed | LeadingBulletState::Keep => Some(delta.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn sanitize_full(&mut self, text: &str) -> String {
|
||||
self.reset();
|
||||
self.buffer.push_str(text);
|
||||
match self.finalize_pending() {
|
||||
Some(out) => out,
|
||||
None => {
|
||||
if self.buffer == "-" {
|
||||
self.buffer.clear();
|
||||
self.state = LeadingBulletState::Removed;
|
||||
String::new()
|
||||
} else {
|
||||
self.state = LeadingBulletState::Keep;
|
||||
std::mem::take(&mut self.buffer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn finalize_pending(&mut self) -> Option<String> {
|
||||
if self.buffer.is_empty() {
|
||||
return Some(String::new());
|
||||
}
|
||||
|
||||
let mut chars = self.buffer.char_indices();
|
||||
let Some((_, first)) = chars.next() else {
|
||||
return Some(String::new());
|
||||
};
|
||||
if first != '-' {
|
||||
self.state = LeadingBulletState::Keep;
|
||||
return Some(std::mem::take(&mut self.buffer));
|
||||
}
|
||||
|
||||
match chars.next() {
|
||||
Some((second_idx, second_char)) => {
|
||||
if matches!(second_char, ' ' | '\t') {
|
||||
let drain_end = second_idx + second_char.len_utf8();
|
||||
self.buffer.drain(..drain_end);
|
||||
self.state = LeadingBulletState::Removed;
|
||||
Some(std::mem::take(&mut self.buffer))
|
||||
} else if matches!(second_char, '\n' | '\r') {
|
||||
self.buffer.drain(..second_idx);
|
||||
self.state = LeadingBulletState::Removed;
|
||||
Some(std::mem::take(&mut self.buffer))
|
||||
} else if second_char.is_whitespace() {
|
||||
let drain_end = second_idx + second_char.len_utf8();
|
||||
self.buffer.drain(..drain_end);
|
||||
self.state = LeadingBulletState::Removed;
|
||||
Some(std::mem::take(&mut self.buffer))
|
||||
} else {
|
||||
self.state = LeadingBulletState::Keep;
|
||||
Some(std::mem::take(&mut self.buffer))
|
||||
}
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Timestamped println helper. The timestamp is styled with self.dimmed.
|
||||
#[macro_export]
|
||||
macro_rules! ts_println {
|
||||
@@ -238,8 +142,12 @@ impl EventProcessor for EventProcessorWithHumanOutput {
|
||||
/// for the session. This mirrors the information shown in the TUI welcome
|
||||
/// screen.
|
||||
fn print_config_summary(&mut self, config: &Config, prompt: &str) {
|
||||
let version = codex_version::version();
|
||||
ts_println!(self, "Code v{}\n--------", version);
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
ts_println!(
|
||||
self,
|
||||
"OpenAI Codex v{} (research preview)\n--------",
|
||||
VERSION
|
||||
);
|
||||
|
||||
let entries = create_config_summary_entries(config);
|
||||
|
||||
@@ -266,12 +174,10 @@ impl EventProcessor for EventProcessorWithHumanOutput {
|
||||
EventMsg::Error(ErrorEvent { message }) => {
|
||||
let prefix = "ERROR:".style(self.red);
|
||||
ts_println!(self, "{prefix} {message}");
|
||||
self.had_error = true;
|
||||
}
|
||||
EventMsg::BackgroundEvent(BackgroundEventEvent { message }) => {
|
||||
ts_println!(self, "{}", message.style(self.dimmed));
|
||||
}
|
||||
// Stream errors are surfaced as Error/Background events in core
|
||||
EventMsg::TaskStarted => {
|
||||
// Ignore.
|
||||
}
|
||||
@@ -281,22 +187,22 @@ impl EventProcessor for EventProcessorWithHumanOutput {
|
||||
}
|
||||
return CodexStatus::InitiateShutdown;
|
||||
}
|
||||
EventMsg::TokenCount(token_usage) => {
|
||||
ts_println!(self, "tokens used: {}", token_usage.blended_total());
|
||||
EventMsg::TokenCount(ev) => {
|
||||
// Our fork carries TokenUsage directly
|
||||
ts_println!(
|
||||
self,
|
||||
"tokens used: {}",
|
||||
format_with_separators(ev.blended_total())
|
||||
);
|
||||
}
|
||||
EventMsg::AgentMessageDelta(AgentMessageDeltaEvent { delta }) => {
|
||||
if !self.answer_started {
|
||||
ts_println!(self, "{}\n", "codex".style(self.italic).style(self.magenta));
|
||||
self.answer_started = true;
|
||||
self.answer_bullet_filter.reset();
|
||||
}
|
||||
if let Some(out) = self.answer_bullet_filter.ingest_delta(&delta) {
|
||||
if !out.is_empty() {
|
||||
print!("{out}");
|
||||
#[expect(clippy::expect_used)]
|
||||
std::io::stdout().flush().expect("could not flush stdout");
|
||||
}
|
||||
}
|
||||
print!("{delta}");
|
||||
#[expect(clippy::expect_used)]
|
||||
std::io::stdout().flush().expect("could not flush stdout");
|
||||
}
|
||||
EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent { delta }) => {
|
||||
if !self.show_agent_reasoning {
|
||||
@@ -352,18 +258,15 @@ impl EventProcessor for EventProcessorWithHumanOutput {
|
||||
// if answer_started is false, this means we haven't received any
|
||||
// delta. Thus, we need to print the message as a new answer.
|
||||
if !self.answer_started {
|
||||
let sanitized = self.answer_bullet_filter.sanitize_full(&message);
|
||||
ts_println!(
|
||||
self,
|
||||
"{}\n{}",
|
||||
"codex".style(self.italic).style(self.magenta),
|
||||
sanitized,
|
||||
message,
|
||||
);
|
||||
self.answer_bullet_filter.reset();
|
||||
} else {
|
||||
println!();
|
||||
self.answer_started = false;
|
||||
self.answer_bullet_filter.reset();
|
||||
}
|
||||
}
|
||||
EventMsg::ExecCommandBegin(ExecCommandBeginEvent {
|
||||
@@ -373,7 +276,7 @@ impl EventProcessor for EventProcessorWithHumanOutput {
|
||||
parsed_cmd: _,
|
||||
}) => {
|
||||
self.call_id_to_command.insert(
|
||||
call_id.clone(),
|
||||
call_id,
|
||||
ExecCommandBegin {
|
||||
command: command.clone(),
|
||||
},
|
||||
@@ -387,13 +290,7 @@ impl EventProcessor for EventProcessorWithHumanOutput {
|
||||
);
|
||||
}
|
||||
EventMsg::ExecCommandOutputDelta(_) => {}
|
||||
EventMsg::ExecCommandEnd(ExecCommandEndEvent {
|
||||
call_id,
|
||||
stdout,
|
||||
stderr,
|
||||
duration,
|
||||
exit_code,
|
||||
}) => {
|
||||
EventMsg::ExecCommandEnd(ExecCommandEndEvent { call_id, stdout, stderr, duration, exit_code }) => {
|
||||
let exec_command = self.call_id_to_command.remove(&call_id);
|
||||
let (duration, call) = if let Some(ExecCommandBegin { command, .. }) = exec_command
|
||||
{
|
||||
@@ -404,44 +301,23 @@ impl EventProcessor for EventProcessorWithHumanOutput {
|
||||
} else {
|
||||
("".to_string(), format!("exec('{call_id}')"))
|
||||
};
|
||||
|
||||
// Always compute truncated stdout and stderr so we can present
|
||||
// a clear separation when the command fails. The model cannot
|
||||
// rely on ANSI colors, so we include a plain "ERROR" divider
|
||||
// before stderr in the failure case.
|
||||
let truncated_stdout = stdout
|
||||
let output = if exit_code == 0 { &stdout } else { &stderr };
|
||||
let truncated_output = output
|
||||
.lines()
|
||||
.take(MAX_OUTPUT_LINES_FOR_EXEC_TOOL_CALL)
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
let truncated_stderr = stderr
|
||||
.lines()
|
||||
.take(MAX_OUTPUT_LINES_FOR_EXEC_TOOL_CALL)
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
|
||||
match exit_code {
|
||||
0 => {
|
||||
let title = format!("{call} succeeded{duration}:");
|
||||
ts_println!(self, "{}", title.style(self.green));
|
||||
if !truncated_stdout.is_empty() {
|
||||
println!("{}", truncated_stdout.style(self.dimmed));
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let title = format!("{call} exited {exit_code}{duration}:");
|
||||
ts_println!(self, "{}", title.style(self.red));
|
||||
if !truncated_stdout.is_empty() {
|
||||
println!("{}", truncated_stdout.style(self.dimmed));
|
||||
println!();
|
||||
}
|
||||
// Separator visible to both humans and the model
|
||||
println!("ERROR");
|
||||
if !truncated_stderr.is_empty() {
|
||||
println!("{}", truncated_stderr);
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("{}", truncated_output.style(self.dimmed));
|
||||
}
|
||||
EventMsg::McpToolCallBegin(McpToolCallBeginEvent {
|
||||
call_id: _,
|
||||
@@ -484,11 +360,10 @@ impl EventProcessor for EventProcessorWithHumanOutput {
|
||||
}
|
||||
}
|
||||
}
|
||||
EventMsg::WebSearchBegin(WebSearchBeginEvent { .. }) => {}
|
||||
EventMsg::WebSearchBegin(WebSearchBeginEvent { call_id: _, .. }) => {}
|
||||
EventMsg::WebSearchComplete(WebSearchCompleteEvent { call_id: _, query }) => {
|
||||
if let Some(query) = query {
|
||||
ts_println!(self, "🌐 Search: {query}");
|
||||
}
|
||||
let q = query.unwrap_or_default();
|
||||
ts_println!(self, "🌐 Searched: {q}");
|
||||
}
|
||||
EventMsg::PatchApplyBegin(PatchApplyBeginEvent {
|
||||
call_id,
|
||||
@@ -498,7 +373,7 @@ impl EventProcessor for EventProcessorWithHumanOutput {
|
||||
// Store metadata so we can calculate duration later when we
|
||||
// receive the corresponding PatchApplyEnd event.
|
||||
self.call_id_to_patch.insert(
|
||||
call_id.clone(),
|
||||
call_id,
|
||||
PatchApplyBegin {
|
||||
start_time: Instant::now(),
|
||||
auto_approved,
|
||||
@@ -605,15 +480,8 @@ impl EventProcessor for EventProcessorWithHumanOutput {
|
||||
}
|
||||
}
|
||||
EventMsg::TurnDiff(TurnDiffEvent { unified_diff }) => {
|
||||
// Suppress noisy full-turn diffs in CI unless explicitly allowed.
|
||||
// Set CODE_SUPPRESS_TURN_DIFF=1 in CI to silence this block.
|
||||
let suppress = std::env::var("CODE_SUPPRESS_TURN_DIFF")
|
||||
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
|
||||
.unwrap_or(false);
|
||||
if !suppress {
|
||||
ts_println!(self, "{}", "turn diff:".style(self.magenta));
|
||||
println!("{unified_diff}");
|
||||
}
|
||||
ts_println!(self, "{}", "turn diff:".style(self.magenta));
|
||||
println!("{unified_diff}");
|
||||
}
|
||||
EventMsg::ExecApprovalRequest(_) => {
|
||||
// Should we exit?
|
||||
@@ -637,139 +505,49 @@ impl EventProcessor for EventProcessorWithHumanOutput {
|
||||
}
|
||||
}
|
||||
EventMsg::SessionConfigured(session_configured_event) => {
|
||||
let SessionConfiguredEvent {
|
||||
session_id,
|
||||
model,
|
||||
history_log_id: _,
|
||||
history_entry_count: _,
|
||||
} = session_configured_event;
|
||||
let SessionConfiguredEvent { session_id: conversation_id, model, .. } = session_configured_event;
|
||||
|
||||
ts_println!(
|
||||
self,
|
||||
"{} {}",
|
||||
"codex session".style(self.magenta).style(self.bold),
|
||||
session_id.to_string().style(self.dimmed)
|
||||
conversation_id.to_string().style(self.dimmed)
|
||||
);
|
||||
|
||||
ts_println!(self, "model: {}", model);
|
||||
println!();
|
||||
}
|
||||
EventMsg::PlanUpdate(plan_update_event) => {
|
||||
let UpdatePlanArgs { name, plan } = plan_update_event;
|
||||
ts_println!(self, "name: {name:?}");
|
||||
let UpdatePlanArgs { plan, .. } = plan_update_event;
|
||||
ts_println!(self, "plan: {plan:?}");
|
||||
}
|
||||
EventMsg::GetHistoryEntryResponse(_) => {
|
||||
// Currently ignored in exec output.
|
||||
}
|
||||
EventMsg::ReplayHistory(_) => {
|
||||
// Replay is a TUI concern; ignore in headless output
|
||||
}
|
||||
EventMsg::BrowserScreenshotUpdate(_) => {
|
||||
// Currently ignored in exec output.
|
||||
}
|
||||
EventMsg::AgentStatusUpdate(_) => {
|
||||
// Currently ignored in exec output.
|
||||
}
|
||||
|
||||
EventMsg::TurnAborted(abort_reason) => match abort_reason.reason {
|
||||
TurnAbortReason::Interrupted => {
|
||||
ts_println!(self, "task interrupted");
|
||||
}
|
||||
TurnAbortReason::Replaced => {
|
||||
ts_println!(self, "task aborted: replaced by a new task");
|
||||
}
|
||||
TurnAbortReason::ReviewEnded => {
|
||||
ts_println!(self, "task aborted: review ended");
|
||||
}
|
||||
},
|
||||
EventMsg::ShutdownComplete => return CodexStatus::Shutdown,
|
||||
EventMsg::CustomToolCallBegin(event) => {
|
||||
ts_println!(
|
||||
self,
|
||||
"{} {}",
|
||||
"tool".style(self.magenta),
|
||||
event.tool_name.style(self.bold),
|
||||
);
|
||||
if let Some(params) = &event.parameters {
|
||||
if let Ok(formatted) = serde_json::to_string_pretty(params) {
|
||||
for line in formatted.lines() {
|
||||
println!("{}", line.style(self.dimmed));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
EventMsg::CustomToolCallEnd(event) => {
|
||||
let status = if event.result.is_ok() {
|
||||
"success".style(self.green)
|
||||
} else {
|
||||
"failed".style(self.red)
|
||||
};
|
||||
ts_println!(
|
||||
self,
|
||||
"{} {} {}",
|
||||
"tool".style(self.magenta),
|
||||
event.tool_name.style(self.bold),
|
||||
status,
|
||||
);
|
||||
// Print the tool's textual result for visibility in exec mode
|
||||
match &event.result {
|
||||
Ok(content) => {
|
||||
if !content.is_empty() {
|
||||
// Keep output concise; print as-is (it may be pre-formatted)
|
||||
for line in content.lines() {
|
||||
println!("{}", line.style(self.dimmed));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
if !err.is_empty() {
|
||||
for line in err.lines() {
|
||||
println!("{}", line.style(self.red));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
EventMsg::ConversationPath(_) => {}
|
||||
EventMsg::TurnAborted(_) => {}
|
||||
EventMsg::UserMessage(_) => {}
|
||||
EventMsg::EnteredReviewMode(_) => {}
|
||||
EventMsg::ExitedReviewMode(_) => {}
|
||||
// Ignore other events that are not user-visible in exec output
|
||||
_ => {}
|
||||
}
|
||||
CodexStatus::Running
|
||||
}
|
||||
|
||||
// exit_code handled by CLI; suppress unused warnings by omitting method.
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn filter_sanitizes_full_message() {
|
||||
let mut filter = LeadingBulletFilter::default();
|
||||
let sanitized = filter.sanitize_full("- Quick summary\nMore");
|
||||
assert_eq!(sanitized, "Quick summary\nMore");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_handles_multi_delta_stream() {
|
||||
let mut filter = LeadingBulletFilter::default();
|
||||
assert!(
|
||||
filter.ingest_delta("-").is_none(),
|
||||
"first dash should defer"
|
||||
);
|
||||
let out = filter
|
||||
.ingest_delta(" Lead")
|
||||
.expect("delta should produce output");
|
||||
assert_eq!(out, "Lead");
|
||||
let subsequent = filter
|
||||
.ingest_delta("ing continues")
|
||||
.expect("subsequent delta");
|
||||
assert_eq!(subsequent, "ing continues");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_keeps_non_bullet_prefix() {
|
||||
let mut filter = LeadingBulletFilter::default();
|
||||
let sanitized = filter.sanitize_full("-value remains");
|
||||
assert_eq!(sanitized, "-value remains");
|
||||
}
|
||||
}
|
||||
|
||||
// Extend trait with exit_code override
|
||||
// Add exit_code method to the existing EventProcessor impl
|
||||
|
||||
fn escape_command(command: &[String]) -> String {
|
||||
try_join(command.iter().map(|s| s.as_str())).unwrap_or_else(|_| command.join(" "))
|
||||
}
|
||||
@@ -777,7 +555,7 @@ fn escape_command(command: &[String]) -> String {
|
||||
fn format_file_change(change: &FileChange) -> &'static str {
|
||||
match change {
|
||||
FileChange::Add { .. } => "A",
|
||||
FileChange::Delete { .. } => "D",
|
||||
FileChange::Delete => "D",
|
||||
FileChange::Update {
|
||||
move_path: Some(_), ..
|
||||
} => "R",
|
||||
|
||||
@@ -1276,6 +1276,7 @@ pub struct TurnAbortedEvent {
|
||||
pub enum TurnAbortReason {
|
||||
Interrupted,
|
||||
Replaced,
|
||||
ReviewEnded,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
+2
-2
@@ -4,10 +4,10 @@ expression: term.backend()
|
||||
---
|
||||
"/ T R A N S C R I P T / / / / / / / / / "
|
||||
"alpha "
|
||||
" "
|
||||
"beta "
|
||||
" "
|
||||
"gamma "
|
||||
"~ "
|
||||
"~ "
|
||||
"───────────────────────────────── 100% ─"
|
||||
" ↑/↓ scroll PgUp/PgDn page Home/End "
|
||||
" q quit Esc edit prev "
|
||||
|
||||
Reference in New Issue
Block a user