merge: upstream main into upstream-merge (preserve core/TUI, adapt exec). build-fast ok

This commit is contained in:
just-every-code
2025-09-18 21:33:38 +00:00
10 changed files with 122 additions and 329 deletions
+1 -1
View File
@@ -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;
+2 -2
View File
@@ -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
+3
View File
@@ -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
View File
@@ -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;
+5 -5
View File
@@ -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
+10 -8
View File
@@ -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",
+1
View File
@@ -1276,6 +1276,7 @@ pub struct TurnAbortedEvent {
pub enum TurnAbortReason {
Interrupted,
Replaced,
ReviewEnded,
}
#[cfg(test)]
@@ -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 "