diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6d4709cf12..ba2cdca504 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,8 +8,8 @@ repos: additional_dependencies: [PyYAML, toml, pydantic] files: ^agentydragon/tasks/.* - id: cargo-build - name: Check Rust workspace builds - entry: bash -lc 'cd codex-rs && RUSTFLAGS="-D warnings" cargo build --workspace --locked' + name: Check Rust workspace and linux-sandbox compile + entry: bash -lc 'cd codex-rs && RUSTFLAGS="-D warnings" cargo build --workspace --locked --all-targets && cargo build -p codex-linux-sandbox --locked --all-targets' language: system pass_filenames: false require_serial: true diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index 536d0bf563..6f3b51cf78 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -4,14 +4,14 @@ use codex_cli::SeatbeltCommand; use codex_cli::login::run_login_with_chatgpt; use codex_cli::proto; use codex_common::CliConfigOverrides; +use codex_core::config::find_codex_home; use codex_exec::Cli as ExecCli; use codex_tui::Cli as TuiCli; +use serde::de::Error as SerdeError; +use std::io::ErrorKind; use std::path::PathBuf; use std::{env, fs, process}; -use std::io::ErrorKind; -use toml::{self, value::Table, Value}; -use serde::de::Error as SerdeError; -use codex_core::config::find_codex_home; +use toml::{self, Value, value::Table}; use uuid::Uuid; use crate::proto::ProtoCli; @@ -42,7 +42,8 @@ struct MultitoolCli { fn parse_toml_value(raw: &str) -> Result { let wrapped = format!("_x_ = {raw}"); let table: Table = toml::from_str(&wrapped)?; - table.get("_x_") + table + .get("_x_") .cloned() .ok_or_else(|| SerdeError::custom("missing sentinel")) } @@ -152,9 +153,7 @@ async fn cli_main(codex_linux_sandbox_exe: Option) -> anyhow::Result<() } // Open in editor from $EDITOR or fall back to vi. let editor = env::var_os("EDITOR").unwrap_or_else(|| "vi".into()); - let status = process::Command::new(editor) - .arg(&config_path) - .status()?; + let status = process::Command::new(editor).arg(&config_path).status()?; if !status.success() { std::process::exit(status.code().unwrap_or(1)); } @@ -243,13 +242,15 @@ fn apply_override(root: &mut toml::Value, path: &str, value: toml::Value) { } match current { toml::Value::Table(tbl) => { - current = tbl.entry((*part).to_string()) + current = tbl + .entry((*part).to_string()) .or_insert_with(|| toml::Value::Table(Table::new())); } _ => { *current = toml::Value::Table(Table::new()); if let toml::Value::Table(tbl) = current { - current = tbl.entry((*part).to_string()) + current = tbl + .entry((*part).to_string()) .or_insert_with(|| toml::Value::Table(Table::new())); } } @@ -268,7 +269,9 @@ mod tests { #[test] fn config_subcommands_help() { let mut cmd = MultitoolCli::command(); - let cfg = cmd.find_subcommand_mut("config").expect("config subcommand not found"); + let cfg = cmd + .find_subcommand_mut("config") + .expect("config subcommand not found"); let mut buf = Vec::new(); cfg.write_long_help(&mut buf).unwrap(); let help = String::from_utf8(buf).unwrap(); diff --git a/codex-rs/cli/tests/config_cmd.rs b/codex-rs/cli/tests/config_cmd.rs index b737e0f58f..0934d10fbe 100644 --- a/codex-rs/cli/tests/config_cmd.rs +++ b/codex-rs/cli/tests/config_cmd.rs @@ -2,8 +2,8 @@ /// This uses `CARGO_BIN_EXE_codex` to locate the compiled binary. #[cfg(test)] mod cli_config { - use std::process::Command; use std::fs; + use std::process::Command; use tempfile; use toml; diff --git a/codex-rs/core/src/chat_completions.rs b/codex-rs/core/src/chat_completions.rs index 2e8963b1ee..664ffb0c9e 100644 --- a/codex-rs/core/src/chat_completions.rs +++ b/codex-rs/core/src/chat_completions.rs @@ -66,7 +66,11 @@ pub(crate) async fn stream_chat_completions( } messages.push(json!({"role": role, "content": text})); } - ResponseItem::FunctionCall { name, arguments, call_id } => { + ResponseItem::FunctionCall { + name, + arguments, + call_id, + } => { // Mark tool invocation in-flight pending_call = Some(call_id.clone()); messages.push(json!({ @@ -79,7 +83,9 @@ pub(crate) async fn stream_chat_completions( }] })); } - ResponseItem::LocalShellCall { id, status, action, .. } => { + ResponseItem::LocalShellCall { + id, status, action, .. + } => { // Mark shell-call invocation in-flight by id if let Some(call_id) = id { pending_call = Some(call_id.clone()); diff --git a/codex-rs/core/src/client_common.rs b/codex-rs/core/src/client_common.rs index 0ae8d69778..e0d3d237c9 100644 --- a/codex-rs/core/src/client_common.rs +++ b/codex-rs/core/src/client_common.rs @@ -48,11 +48,12 @@ impl Prompt { match std::env::var("CODEX_BASE_INSTRUCTIONS_FILE") { Ok(ref path) if !path.is_empty() && path != "-" => { // Override built-in prompt: read file or abort - let contents = std::fs::read_to_string(path) - .unwrap_or_else(|e| panic!( + let contents = std::fs::read_to_string(path).unwrap_or_else(|e| { + panic!( "failed to read CODEX_BASE_INSTRUCTIONS_FILE '{}': {e}", path - )); + ) + }); sections.push(contents); } Ok(_) => { diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 22c0572d24..5014d124c2 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -37,7 +37,7 @@ use crate::WireApi; use crate::client::ModelClient; use crate::client_common::Prompt; use crate::client_common::ResponseEvent; -use crate::config::{Config, AutoAllowPredicate}; +use crate::config::{AutoAllowPredicate, Config}; use crate::config_types::ShellEnvironmentPolicy; use crate::conversation_history::ConversationHistory; use crate::error::CodexErr; @@ -83,8 +83,10 @@ use crate::protocol::Submission; use crate::protocol::TaskCompleteEvent; use crate::rollout::RolloutRecorder; use crate::safety::SafetyCheck; -use crate::safety::{assess_command_safety, evaluate_auto_allow_predicates, get_platform_sandbox, AutoAllowVote}; use crate::safety::assess_patch_safety; +use crate::safety::{ + AutoAllowVote, assess_command_safety, evaluate_auto_allow_predicates, get_platform_sandbox, +}; use crate::user_notification::UserNotification; use crate::util::backoff; diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs index dfa0c1b9a8..7b95abfad0 100644 --- a/codex-rs/core/src/config.rs +++ b/codex-rs/core/src/config.rs @@ -452,9 +452,7 @@ impl Config { .or(config_profile.approval_policy) .or(cfg.approval_policy) .unwrap_or_else(AskForApproval::default), - auto_allow: config_profile - .auto_allow - .unwrap_or(cfg.auto_allow), + auto_allow: config_profile.auto_allow.unwrap_or(cfg.auto_allow), sandbox_policy, shell_environment_policy, disable_response_storage: config_profile @@ -802,6 +800,7 @@ disable_response_storage = true model_provider_id: "openai".to_string(), model_provider: fixture.openai_provider.clone(), approval_policy: AskForApproval::Never, + auto_allow: Vec::new(), sandbox_policy: SandboxPolicy::new_read_only_policy(), shell_environment_policy: ShellEnvironmentPolicy::default(), disable_response_storage: false, @@ -844,6 +843,7 @@ disable_response_storage = true model_provider_id: "openai-chat-completions".to_string(), model_provider: fixture.openai_chat_completions_provider.clone(), approval_policy: AskForApproval::UnlessAllowListed, + auto_allow: Vec::new(), sandbox_policy: SandboxPolicy::new_read_only_policy(), shell_environment_policy: ShellEnvironmentPolicy::default(), disable_response_storage: false, @@ -901,6 +901,7 @@ disable_response_storage = true model_provider_id: "openai".to_string(), model_provider: fixture.openai_provider.clone(), approval_policy: AskForApproval::OnFailure, + auto_allow: Vec::new(), sandbox_policy: SandboxPolicy::new_read_only_policy(), shell_environment_policy: ShellEnvironmentPolicy::default(), disable_response_storage: true, diff --git a/codex-rs/core/src/config_profile.rs b/codex-rs/core/src/config_profile.rs index dfcb0d947d..ef2c1c6fee 100644 --- a/codex-rs/core/src/config_profile.rs +++ b/codex-rs/core/src/config_profile.rs @@ -1,7 +1,7 @@ use serde::Deserialize; -use crate::protocol::AskForApproval; use crate::config::AutoAllowPredicate; +use crate::protocol::AskForApproval; /// Collection of common configuration options that a user can define as a unit /// in `config.toml`. diff --git a/codex-rs/core/src/config_types.rs b/codex-rs/core/src/config_types.rs index b54039edb5..9e3ce165d6 100644 --- a/codex-rs/core/src/config_types.rs +++ b/codex-rs/core/src/config_types.rs @@ -129,7 +129,9 @@ fn default_composer_max_rows() -> usize { /// Default editor: `$VISUAL`, then `$EDITOR`, falling back to `nvim`. fn default_editor() -> String { - std::env::var("VISUAL").or_else(|_| std::env::var("EDITOR")).unwrap_or_else(|_| "nvim".into()) + std::env::var("VISUAL") + .or_else(|_| std::env::var("EDITOR")) + .unwrap_or_else(|_| "nvim".into()) } /// Default timeout in seconds for the second Ctrl+D confirmation to exit the TUI. diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index 66523db3cf..108a59181c 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -27,7 +27,9 @@ mod model_provider_info; pub use model_provider_info::ModelProviderInfo; pub use model_provider_info::WireApi; mod models; -pub use models::{ContentItem, ReasoningItemReasoningSummary, ResponseItem, FunctionCallOutputPayload}; +pub use models::{ + ContentItem, FunctionCallOutputPayload, ReasoningItemReasoningSummary, ResponseItem, +}; pub mod openai_api_key; mod openai_tools; mod project_doc; @@ -37,4 +39,4 @@ mod safety; mod user_notification; pub mod util; -pub use client_common::{model_supports_reasoning_summaries, Prompt}; +pub use client_common::{Prompt, model_supports_reasoning_summaries}; diff --git a/codex-rs/core/src/protocol.rs b/codex-rs/core/src/protocol.rs index e17dec975e..fb3029dcbb 100644 --- a/codex-rs/core/src/protocol.rs +++ b/codex-rs/core/src/protocol.rs @@ -256,7 +256,8 @@ impl SandboxPolicy { impl SandboxPolicy { /// Grant disk-write permission for the specified folder. pub fn allow_disk_write_folder(&mut self, folder: std::path::PathBuf) { - self.permissions.push(SandboxPermission::DiskWriteFolder { folder }); + self.permissions + .push(SandboxPermission::DiskWriteFolder { folder }); } /// Revoke any disk-write permission for the specified folder. diff --git a/codex-rs/core/src/safety.rs b/codex-rs/core/src/safety.rs index 08bc3cd85a..1c96c40898 100644 --- a/codex-rs/core/src/safety.rs +++ b/codex-rs/core/src/safety.rs @@ -6,11 +6,11 @@ use std::path::PathBuf; use codex_apply_patch::ApplyPatchAction; use codex_apply_patch::ApplyPatchFileChange; +use crate::config::AutoAllowPredicate; use crate::exec::SandboxType; use crate::is_safe_command::is_known_safe_command; use crate::protocol::AskForApproval; use crate::protocol::SandboxPolicy; -use crate::config::AutoAllowPredicate; #[derive(Debug)] pub enum SafetyCheck { @@ -295,13 +295,21 @@ mod tests { std::fs::set_permissions(&deny_script, perms2).unwrap(); // Allow script should return Allow - let preds = vec![AutoAllowPredicate { script: allow_script.to_string_lossy().into() }]; + let preds = vec![AutoAllowPredicate { + script: allow_script.to_string_lossy().into(), + }]; let vote = evaluate_auto_allow_predicates(&["cmd".to_string()], &preds); assert_eq!(vote, AutoAllowVote::Allow); // Deny script takes precedence over allow - let preds2 = vec![AutoAllowPredicate { script: deny_script.to_string_lossy().into() }, - AutoAllowPredicate { script: allow_script.to_string_lossy().into() }]; + let preds2 = vec![ + AutoAllowPredicate { + script: deny_script.to_string_lossy().into(), + }, + AutoAllowPredicate { + script: allow_script.to_string_lossy().into(), + }, + ]; let vote2 = evaluate_auto_allow_predicates(&["cmd".to_string()], &preds2); assert_eq!(vote2, AutoAllowVote::Deny); @@ -336,9 +344,15 @@ mod tests { // All scripts no-opinion or error yields NoOpinion let preds = vec![ - AutoAllowPredicate { script: noop_script.to_string_lossy().into() }, - AutoAllowPredicate { script: unknown_script.to_string_lossy().into() }, - AutoAllowPredicate { script: error_script.to_string_lossy().into() }, + AutoAllowPredicate { + script: noop_script.to_string_lossy().into(), + }, + AutoAllowPredicate { + script: unknown_script.to_string_lossy().into(), + }, + AutoAllowPredicate { + script: error_script.to_string_lossy().into(), + }, ]; let vote = evaluate_auto_allow_predicates(&["cmd".to_string()], &preds); assert_eq!(vote, AutoAllowVote::NoOpinion); @@ -362,8 +376,12 @@ mod tests { std::fs::set_permissions(&allow_script, perms2).unwrap(); let preds = vec![ - AutoAllowPredicate { script: noop_script.to_string_lossy().into() }, - AutoAllowPredicate { script: allow_script.to_string_lossy().into() }, + AutoAllowPredicate { + script: noop_script.to_string_lossy().into(), + }, + AutoAllowPredicate { + script: allow_script.to_string_lossy().into(), + }, ]; let vote = evaluate_auto_allow_predicates(&["cmd".to_string()], &preds); assert_eq!(vote, AutoAllowVote::Allow); diff --git a/codex-rs/core/tests/guard_tool_output_sequencing.rs b/codex-rs/core/tests/guard_tool_output_sequencing.rs index 86add44a41..bc3bfea77c 100644 --- a/codex-rs/core/tests/guard_tool_output_sequencing.rs +++ b/codex-rs/core/tests/guard_tool_output_sequencing.rs @@ -1,8 +1,8 @@ -use serde_json::{json, Value}; -use codex_core::{Prompt, ResponseItem, ContentItem, FunctionCallOutputPayload}; +use codex_core::{ContentItem, FunctionCallOutputPayload, ResponseItem}; +use serde_json::{Value, json}; /// Reproduce the `messages` JSON construction from `stream_chat_completions` -fn build_messages(input: Vec, model: &str) -> Vec { +fn build_messages(input: Vec, _model: &str) -> Vec { let mut messages = Vec::new(); let mut pending = None::; let mut buf_user = Vec::new(); @@ -30,7 +30,11 @@ fn build_messages(input: Vec, model: &str) -> Vec { } messages.push(json!({"role": role, "content": text})); } - ResponseItem::FunctionCall { name, arguments, call_id } => { + ResponseItem::FunctionCall { + name, + arguments, + call_id, + } => { pending = Some(call_id.clone()); messages.push(json!({ "role": "assistant", "content": null, @@ -38,7 +42,9 @@ fn build_messages(input: Vec, model: &str) -> Vec { })); } ResponseItem::FunctionCallOutput { call_id, output } => { - messages.push(json!({"role": "tool", "tool_call_id": call_id, "content": output.content})); + messages.push( + json!({"role": "tool", "tool_call_id": call_id, "content": output.content}), + ); if pending.as_ref() == Some(&call_id) { pending = None; for m in buf_user.drain(..) { @@ -52,7 +58,8 @@ fn build_messages(input: Vec, model: &str) -> Vec { // cancellation: no output arrived if let Some(call_id) = pending { - messages.push(json!({"role": "tool", "tool_call_id": call_id, "content": "Tool cancelled"})); + messages + .push(json!({"role": "tool", "tool_call_id": call_id, "content": "Tool cancelled"})); for m in buf_user.drain(..) { messages.push(m); } @@ -63,36 +70,82 @@ fn build_messages(input: Vec, model: &str) -> Vec { #[test] fn normal_flow_no_buffer() { - let input = vec![ResponseItem::Message { role: "user".into(), content: vec![ContentItem::InputText { text: "hi".into() }] }]; + let input = vec![ResponseItem::Message { + role: "user".into(), + content: vec![ContentItem::InputText { text: "hi".into() }], + }]; let msgs = build_messages(input, "m1"); - assert_eq!(msgs.iter().filter(|m| m["role"] == json!("user")).count(), 1); + assert_eq!( + msgs.iter().filter(|m| m["role"] == json!("user")).count(), + 1 + ); } #[test] fn buffer_and_flush_on_output() { let call_id = "c1".to_string(); let input = vec![ - ResponseItem::FunctionCall { name: "f".into(), arguments: "{}".into(), call_id: call_id.clone() }, - ResponseItem::Message { role: "user".into(), content: vec![ContentItem::InputText { text: "late".into() }] }, - ResponseItem::FunctionCallOutput { call_id: call_id.clone(), output: FunctionCallOutputPayload { content: "ok".into(), success: None } }, + ResponseItem::FunctionCall { + name: "f".into(), + arguments: "{}".into(), + call_id: call_id.clone(), + }, + ResponseItem::Message { + role: "user".into(), + content: vec![ContentItem::InputText { + text: "late".into(), + }], + }, + ResponseItem::FunctionCallOutput { + call_id: call_id.clone(), + output: FunctionCallOutputPayload { + content: "ok".into(), + success: None, + }, + }, ]; let msgs = build_messages(input, "m1"); // order: system, functioncall, tool output, then buffered user let roles: Vec<_> = msgs.iter().map(|m| m["role"].clone()).collect(); - assert_eq!(roles.as_slice(), &[json!("system"), json!("assistant"), json!("tool"), json!("user")]); + assert_eq!( + roles.as_slice(), + &[ + json!("system"), + json!("assistant"), + json!("tool"), + json!("user") + ] + ); } #[test] fn buffer_and_cancel() { let call_id = "c2".to_string(); let input = vec![ - ResponseItem::FunctionCall { name: "f".into(), arguments: "{}".into(), call_id: call_id.clone() }, - ResponseItem::Message { role: "user".into(), content: vec![ContentItem::InputText { text: "oops".into() }] }, + ResponseItem::FunctionCall { + name: "f".into(), + arguments: "{}".into(), + call_id: call_id.clone(), + }, + ResponseItem::Message { + role: "user".into(), + content: vec![ContentItem::InputText { + text: "oops".into(), + }], + }, ]; let msgs = build_messages(input, "m1"); // expect system, functioncall, fake cancel, then user let roles: Vec<_> = msgs.iter().map(|m| m["role"].clone()).collect(); - assert_eq!(roles.as_slice(), &[json!("system"), json!("assistant"), json!("tool"), json!("user")]); + assert_eq!( + roles.as_slice(), + &[ + json!("system"), + json!("assistant"), + json!("tool"), + json!("user") + ] + ); // cancellation message content assert_eq!(msgs[2]["content"], json!("Tool cancelled")); } diff --git a/codex-rs/linux-sandbox/src/linux_run_main.rs b/codex-rs/linux-sandbox/src/linux_run_main.rs index 7e68bf9eb0..1f260faf8b 100644 --- a/codex-rs/linux-sandbox/src/linux_run_main.rs +++ b/codex-rs/linux-sandbox/src/linux_run_main.rs @@ -5,8 +5,6 @@ use std::ffi::CString; use libc; use crate::landlock::apply_sandbox_policy_to_current_thread; -use codex_core::config::{Config, ConfigOverrides}; -use codex_core::util::{find_git_root, relative_path_from_git_root}; #[derive(Debug, Parser)] pub struct LandlockCommand { @@ -26,48 +24,11 @@ pub fn run_main() -> ! { None => codex_core::protocol::SandboxPolicy::new_read_only_policy(), }; - // Determine working directory inside the session, possibly auto-mounting the repo. + // Determine working directory inside the session. let mut cwd = match std::env::current_dir() { Ok(cwd) => cwd, Err(e) => panic!("failed to getcwd(): {e:?}"), }; - // Load configuration to check auto_mount_repo flag - let config = match codex_core::config::Config::load_with_cli_overrides( - Vec::new(), - codex_core::config::ConfigOverrides::default(), - ) { - Ok(cfg) => cfg, - Err(e) => panic!("failed to load config for auto-mount: {e:?}"), - }; - if config.tui.auto_mount_repo { - if let Some(root) = codex_core::util::find_git_root(&cwd) { - // Compute relative subpath - let rel = codex_core::util::relative_path_from_git_root(&cwd).unwrap_or_default(); - let mount_prefix = std::path::PathBuf::from(&config.tui.mount_prefix); - // Create mount target - std::fs::create_dir_all(&mount_prefix).unwrap_or_else(|e| { - panic!("failed to create mount prefix {mount_prefix:?}: {e:?}") - }); - // Bind-mount repository root into session - let src = std::ffi::CString::new(root.to_string_lossy().as_ref()) - .expect("invalid git root path"); - let dst = std::ffi::CString::new(mount_prefix.to_string_lossy().as_ref()) - .expect("invalid mount prefix path"); - unsafe { - libc::mount( - src.as_ptr(), - dst.as_ptr(), - std::ptr::null(), - libc::MS_BIND, - std::ptr::null(), - ); - } - // Change working directory to corresponding subfolder under mount - cwd = mount_prefix.join(rel); - std::env::set_current_dir(&cwd) - .unwrap_or_else(|e| panic!("failed to chdir to {cwd:?}: {e:?}")); - } - } if let Err(e) = apply_sandbox_policy_to_current_thread(&sandbox_policy, &cwd) { panic!("error running landlock: {e:?}"); diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 034b667ac5..9c77dbce6a 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -1,7 +1,7 @@ use crate::app_event::AppEvent; -use crate::confirm_ctrl_d::ConfirmCtrlD; use crate::app_event_sender::AppEventSender; use crate::chatwidget::ChatWidget; +use crate::confirm_ctrl_d::ConfirmCtrlD; use crate::git_warning_screen::GitWarningOutcome; use crate::git_warning_screen::GitWarningScreen; use crate::login_screen::LoginScreen; @@ -69,14 +69,18 @@ struct ChatWidgetArgs { } /// Parse raw argument string for `/mount-add host=... container=... mode=...`. -fn parse_mount_add_args(raw: &str) -> Result<(std::path::PathBuf, std::path::PathBuf, String), String> { +fn parse_mount_add_args( + raw: &str, +) -> Result<(std::path::PathBuf, std::path::PathBuf, String), String> { let mut host = None; let mut container = None; let mut mode = "rw".to_string(); for token in raw.split_whitespace() { let mut parts = token.splitn(2, '='); let key = parts.next().unwrap(); - let value = parts.next().ok_or_else(|| format!("invalid argument '{}'", token))?; + let value = parts + .next() + .ok_or_else(|| format!("invalid argument '{}'", token))?; match key { "host" => host = Some(std::path::PathBuf::from(value)), "container" => container = Some(std::path::PathBuf::from(value)), @@ -95,7 +99,9 @@ fn parse_mount_remove_args(raw: &str) -> Result { for token in raw.split_whitespace() { let mut parts = token.splitn(2, '='); let key = parts.next().unwrap(); - let value = parts.next().ok_or_else(|| format!("invalid argument '{}'", token))?; + let value = parts + .next() + .ok_or_else(|| format!("invalid argument '{}'", token))?; if key == "container" { container = Some(std::path::PathBuf::from(value)); } else { @@ -347,15 +353,19 @@ impl<'a> App<'a> { let _ = child.wait(); } Err(err) => { - let _ = tx.send(AppEvent::LatestLog( - format!("Failed to spawn inspect-env: {err}") - )); + let _ = tx.send(AppEvent::LatestLog(format!( + "Failed to spawn inspect-env: {err}" + ))); } } let _ = tx.send(AppEvent::Redraw); }); } - AppEvent::MountAdd { host, container, mode } => { + AppEvent::MountAdd { + host, + container, + mode, + } => { if let Err(err) = do_mount_add(&mut self.config, &host, &container, &mode) { tracing::error!("mount-add failed: {err}"); } @@ -405,22 +415,22 @@ impl<'a> App<'a> { } } } - KeyEvent { - code: KeyCode::Char('d'), - modifiers: crossterm::event::KeyModifiers::CONTROL, - .. - } => { - // Handle Ctrl+D exit confirmation when enabled. - let now = Instant::now(); - if self.confirm_ctrl_d.handle(now) { - break; + KeyEvent { + code: KeyCode::Char('d'), + modifiers: crossterm::event::KeyModifiers::CONTROL, + .. + } => { + // Handle Ctrl+D exit confirmation when enabled. + let now = Instant::now(); + if self.confirm_ctrl_d.handle(now) { + break; + } + if let AppState::Chat { widget } = &mut self.app_state { + widget.show_exit_confirmation_prompt( + "Press Ctrl+D again to confirm exit".to_string(), + ); + } } - if let AppState::Chat { widget } = &mut self.app_state { - widget.show_exit_confirmation_prompt( - "Press Ctrl+D again to confirm exit".to_string(), - ); - } - } _ => { self.dispatch_key_event(key_event); } @@ -482,7 +492,9 @@ impl<'a> App<'a> { if let AppState::Chat { widget } = &mut self.app_state { widget.push_inspect_env(); } - let _ = self.app_event_tx.send(AppEvent::InlineInspectEnv(String::new())); + let _ = self + .app_event_tx + .send(AppEvent::InlineInspectEnv(String::new())); } SlashCommand::Shell => { if let AppState::Chat { widget } = &mut self.app_state { @@ -496,8 +508,13 @@ impl<'a> App<'a> { widget.handle_shell_command(cmd); self.app_event_tx.send(AppEvent::Redraw); } - }, - AppEvent::ShellCommandResult { call_id, stdout, stderr, exit_code } => { + } + AppEvent::ShellCommandResult { + call_id, + stdout, + stderr, + exit_code, + } => { if let AppState::Chat { widget } = &mut self.app_state { widget.handle_shell_command_result(call_id, stdout, stderr, exit_code); self.app_event_tx.send(AppEvent::Redraw); diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index 70e8873bda..a5beb758ce 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -63,3 +63,54 @@ pub(crate) enum AppEvent { exit_code: i32, }, } + +impl PartialEq for AppEvent { + fn eq(&self, other: &Self) -> bool { + use AppEvent::*; + match (self, other) { + (CodexEvent(_), CodexEvent(_)) => true, + (Redraw, Redraw) => true, + (KeyEvent(a), KeyEvent(b)) => a == b, + (Scroll(a), Scroll(b)) => a == b, + (ExitRequest, ExitRequest) => true, + (CodexOp(a), CodexOp(b)) => a == b, + (LatestLog(a), LatestLog(b)) => a == b, + (DispatchCommand(a), DispatchCommand(b)) => a == b, + (InlineMountAdd(a), InlineMountAdd(b)) => a == b, + (InlineMountRemove(a), InlineMountRemove(b)) => a == b, + (InlineInspectEnv(a), InlineInspectEnv(b)) => a == b, + ( + MountAdd { + host: h1, + container: c1, + mode: m1, + }, + MountAdd { + host: h2, + container: c2, + mode: m2, + }, + ) => h1 == h2 && c1 == c2 && m1 == m2, + (MountRemove { container: c1 }, MountRemove { container: c2 }) => c1 == c2, + (ConfigReloadRequest(a), ConfigReloadRequest(b)) => a == b, + (ConfigReloadApply, ConfigReloadApply) => true, + (ConfigReloadIgnore, ConfigReloadIgnore) => true, + (ShellCommand(a), ShellCommand(b)) => a == b, + ( + ShellCommandResult { + call_id: i1, + stdout: o1, + stderr: e1, + exit_code: x1, + }, + ShellCommandResult { + call_id: i2, + stdout: o2, + stderr: e2, + exit_code: x2, + }, + ) => i1 == i2 && o1 == o2 && e1 == e2 && x1 == x2, + _ => false, + } + } +} diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index a1389cd65f..5bd9f5bb66 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -172,7 +172,12 @@ impl ChatComposer<'_> { } (InputResult::None, true) } - Input { key: Key::Enter, shift: false, alt: false, ctrl: false } => { + Input { + key: Key::Enter, + shift: false, + alt: false, + ctrl: false, + } => { if let Some(cmd) = popup.selected_command() { // Inline DSL for mount-add/remove with args or dispatch other commands. let first_line = self @@ -181,7 +186,10 @@ impl ChatComposer<'_> { .first() .map(|s| s.as_str()) .unwrap_or(""); - let stripped = first_line.trim_start().strip_prefix('/').unwrap_or(first_line); + let stripped = first_line + .trim_start() + .strip_prefix('/') + .unwrap_or(first_line); let mut parts = stripped.splitn(2, char::is_whitespace); let _cmd_token = parts.next().unwrap_or(""); let args = parts.next().unwrap_or("").trim_start(); @@ -191,7 +199,9 @@ impl ChatComposer<'_> { self.command_popup = None; return (InputResult::None, true); } - if !args.is_empty() && (*cmd == SlashCommand::MountAdd || *cmd == SlashCommand::MountRemove) { + if !args.is_empty() + && (*cmd == SlashCommand::MountAdd || *cmd == SlashCommand::MountRemove) + { let ev = if *cmd == SlashCommand::MountAdd { AppEvent::InlineMountAdd(args.to_string()) } else { @@ -261,15 +271,28 @@ impl ChatComposer<'_> { (InputResult::Submitted(text), true) } } - Input { key: Key::Enter, .. } - | Input { key: Key::Char('j'), ctrl: true, alt: false, shift: false } => { + Input { + key: Key::Enter, .. + } + | Input { + key: Key::Char('j'), + ctrl: true, + alt: false, + shift: false, + } => { self.textarea.insert_newline(); (InputResult::None, true) } - Input { key: Key::Char('m'), ctrl: true, alt: false, shift: false } => { + Input { + key: Key::Char('m'), + ctrl: true, + alt: false, + shift: false, + } => { // Toggle shell-command mode and prompt/exit accordingly self.shell_mode = !self.shell_mode; - self.app_event_tx.send(AppEvent::DispatchCommand(SlashCommand::Shell)); + self.app_event_tx + .send(AppEvent::DispatchCommand(SlashCommand::Shell)); (InputResult::None, true) } input => self.handle_input_basic(input), @@ -302,7 +325,9 @@ impl ChatComposer<'_> { } let path = tmp.path(); // Determine editor: VISUAL > EDITOR > nvim - let editor = std::env::var("VISUAL").or_else(|_| std::env::var("EDITOR")).unwrap_or_else(|_| "nvim".into()); + let editor = std::env::var("VISUAL") + .or_else(|_| std::env::var("EDITOR")) + .unwrap_or_else(|_| "nvim".into()); // Launch editor and wait for exit if let Err(e) = Command::new(editor).arg(path).status() { tracing::error!("failed to launch editor: {e}"); @@ -381,10 +406,8 @@ impl ChatComposer<'_> { let bs = if self.shell_mode { BlockState { - right_title: Line::from( - "Shell mode – Enter to run | Ctrl+M to exit shell mode", - ) - .alignment(Alignment::Right), + right_title: Line::from("Shell mode – Enter to run | Ctrl+M to exit shell mode") + .alignment(Alignment::Right), border_style: Style::default().fg(Color::Red), } } else if has_focus { @@ -451,7 +474,12 @@ impl WidgetRef for &ChatComposer<'_> { } else { Color::Red }; - buf.set_string(area.x + 1, area.y + area.height - 1, text, Style::default().fg(color)); + buf.set_string( + area.x + 1, + area.y + area.height - 1, + text, + Style::default().fg(color), + ); } } } diff --git a/codex-rs/tui/src/bottom_pane/config_reload_view.rs b/codex-rs/tui/src/bottom_pane/config_reload_view.rs index 4cf669c522..c3a7ef745a 100644 --- a/codex-rs/tui/src/bottom_pane/config_reload_view.rs +++ b/codex-rs/tui/src/bottom_pane/config_reload_view.rs @@ -1,12 +1,12 @@ use crossterm::event::{KeyCode, KeyEvent}; use ratatui::buffer::Buffer; use ratatui::layout::Rect; -use ratatui::widgets::{Block, Borders, BorderType, Paragraph}; use ratatui::prelude::Widget; +use ratatui::widgets::{Block, BorderType, Borders, Paragraph}; +use super::{BottomPane, BottomPaneView}; use crate::app_event::AppEvent; use crate::app_event_sender::AppEventSender; -use super::{BottomPane, BottomPaneView}; /// BottomPane view displaying the diff and prompting to apply or ignore. pub(crate) struct ConfigReloadView { @@ -18,7 +18,11 @@ pub(crate) struct ConfigReloadView { impl ConfigReloadView { /// Create a new view with the unified diff of config changes. pub fn new(diff: String, app_event_tx: AppEventSender) -> Self { - Self { diff, app_event_tx, done: false } + Self { + diff, + app_event_tx, + done: false, + } } } @@ -51,7 +55,9 @@ impl<'a> BottomPaneView<'a> for ConfigReloadView { .borders(Borders::ALL) .border_type(BorderType::Rounded) .title("Config changed (Enter=Apply Esc=Ignore)"); - Paragraph::new(self.diff.clone()).block(block).render(area, buf); + Paragraph::new(self.diff.clone()) + .block(block) + .render(area, buf); } fn should_hide_when_task_is_done(&mut self) -> bool { diff --git a/codex-rs/tui/src/bottom_pane/inspect_env_view.rs b/codex-rs/tui/src/bottom_pane/inspect_env_view.rs index 5fac6e639b..d4e30d1c89 100644 --- a/codex-rs/tui/src/bottom_pane/inspect_env_view.rs +++ b/codex-rs/tui/src/bottom_pane/inspect_env_view.rs @@ -1,11 +1,11 @@ use crossterm::event::{KeyCode, KeyEvent}; use ratatui::buffer::Buffer; use ratatui::layout::Rect; -use ratatui::widgets::{Block, Borders, BorderType, Paragraph}; use ratatui::prelude::Widget; +use ratatui::widgets::{Block, BorderType, Borders, Paragraph}; -use super::{BottomPane, BottomPaneView}; use super::bottom_pane_view::ConditionalUpdate; +use super::{BottomPane, BottomPaneView}; /// View for displaying the output of `codex inspect-env` in the bottom pane. pub(crate) struct InspectEnvView { @@ -16,7 +16,10 @@ pub(crate) struct InspectEnvView { impl InspectEnvView { /// Create a new inspect-env view. pub fn new() -> Self { - Self { lines: Vec::new(), done: false } + Self { + lines: Vec::new(), + done: false, + } } } @@ -71,14 +74,19 @@ mod tests { let mut view = InspectEnvView::new(); view.update_status_text("line1".to_string()); view.update_status_text("line2".to_string()); - let area = Rect { x: 0, y: 0, width: 10, height: 3 }; + let area = Rect { + x: 0, + y: 0, + width: 10, + height: 3, + }; let mut buf = Buffer::empty(area); view.render(area, &mut buf); // Collect all cell symbols into a flat string and verify the lines are present - let content: String = buf - .content() - .iter() - .fold(String::new(), |mut acc, cell| { acc.push_str(cell.symbol()); acc }); + let content: String = buf.content().iter().fold(String::new(), |mut acc, cell| { + acc.push_str(cell.symbol()); + acc + }); assert!(content.contains("line1")); assert!(content.contains("line2")); } diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index 72a3770b4c..2be75e482b 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -12,25 +12,25 @@ use crate::app_event_sender::AppEventSender; use crate::user_approval_widget::ApprovalRequest; mod approval_modal_view; -mod mount_view; -mod shell_command_view; -mod inspect_env_view; mod bottom_pane_view; mod chat_composer; mod chat_composer_history; mod command_popup; -mod status_indicator_view; mod config_reload_view; +mod inspect_env_view; +mod mount_view; +mod shell_command_view; +mod status_indicator_view; pub(crate) use chat_composer::ChatComposer; pub(crate) use chat_composer::InputResult; use approval_modal_view::ApprovalModalView; +use config_reload_view::ConfigReloadView; +use inspect_env_view::InspectEnvView; use mount_view::{MountAddView, MountRemoveView}; use shell_command_view::ShellCommandView; -use inspect_env_view::InspectEnvView; use status_indicator_view::StatusIndicatorView; -use config_reload_view::ConfigReloadView; /// Pane displayed in the lower half of the chat UI. pub(crate) struct BottomPane<'a> { @@ -266,9 +266,9 @@ impl WidgetRef for &BottomPane<'_> { #[cfg(test)] mod tests { use super::*; - use std::sync::mpsc; use crate::app_event::AppEvent; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; + use std::sync::mpsc; /// Construct a BottomPane with default parameters for testing. fn make_pane() -> BottomPane<'static> { @@ -309,7 +309,12 @@ mod tests { // Status indicator overlay remains active assert!(pane.active_view.is_some()); // The overlay should be a StatusIndicatorView - assert!(pane.active_view.as_mut().unwrap().should_hide_when_task_is_done()); + assert!( + pane.active_view + .as_mut() + .unwrap() + .should_hide_when_task_is_done() + ); } #[test] @@ -361,9 +366,14 @@ mod tests { // Overlay remains active until task is marked done assert!(pane.active_view.is_some()); // Should still be showing the status indicator overlay - assert!(pane.active_view.as_mut().unwrap().should_hide_when_task_is_done()); + assert!( + pane.active_view + .as_mut() + .unwrap() + .should_hide_when_task_is_done() + ); // The composer buffer should be cleared after submission - let content = pane.composer.textarea.lines().join("\n"); + let content = pane.composer.get_input_text(); assert_eq!(content, ""); } } diff --git a/codex-rs/tui/src/bottom_pane/shell_command_view.rs b/codex-rs/tui/src/bottom_pane/shell_command_view.rs index 2b9c9dbb75..a9398a9ab1 100644 --- a/codex-rs/tui/src/bottom_pane/shell_command_view.rs +++ b/codex-rs/tui/src/bottom_pane/shell_command_view.rs @@ -3,7 +3,7 @@ use ratatui::buffer::Buffer; use ratatui::layout::Rect; use ratatui::prelude::Widget; use ratatui::widgets::{Block, BorderType, Borders, Paragraph}; -use tui_input::{backend::crossterm::EventHandler, Input}; +use tui_input::{Input, backend::crossterm::EventHandler}; use super::BottomPane; use super::BottomPaneView; @@ -30,7 +30,12 @@ impl ShellCommandView { impl<'a> BottomPaneView<'a> for ShellCommandView { fn handle_key_event(&mut self, pane: &mut BottomPane<'a>, key_event: KeyEvent) { // Exit shell prompt on Ctrl+M - if let KeyEvent { code: KeyCode::Char('m'), modifiers: KeyModifiers::CONTROL, .. } = key_event { + if let KeyEvent { + code: KeyCode::Char('m'), + modifiers: KeyModifiers::CONTROL, + .. + } = key_event + { self.done = true; pane.request_redraw(); return; @@ -91,7 +96,10 @@ mod tests { composer_max_rows: 1, }); // Enter command 'a' - view.handle_key_event(&mut pane, KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE)); + view.handle_key_event( + &mut pane, + KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE), + ); view.handle_key_event(&mut pane, KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); // Skip initial redraw event(s) let mut event; diff --git a/codex-rs/tui/src/bottom_pane/status_indicator_view.rs b/codex-rs/tui/src/bottom_pane/status_indicator_view.rs index 9698925532..1fdf10c35c 100644 --- a/codex-rs/tui/src/bottom_pane/status_indicator_view.rs +++ b/codex-rs/tui/src/bottom_pane/status_indicator_view.rs @@ -42,7 +42,12 @@ impl<'a> BottomPaneView<'a> for StatusIndicatorView { // Render the status overlay in a floating box immediately above the textarea let h = self.view.get_height(); let y = area.y.saturating_sub(h); - let rect = Rect { x: area.x, y, width: area.width, height: h }; + let rect = Rect { + x: area.x, + y, + width: area.width, + height: h, + }; self.view.render_ref(rect, buf); } } diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index b3be3b1930..24a5fe8293 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -1,6 +1,9 @@ use std::path::PathBuf; use std::sync::Arc; +use codex_core::ContentItem; +use codex_core::ReasoningItemReasoningSummary; +use codex_core::ResponseItem; use codex_core::codex_wrapper::init_codex; use codex_core::config::Config; use codex_core::protocol::AgentMessageEvent; @@ -18,9 +21,6 @@ use codex_core::protocol::McpToolCallEndEvent; use codex_core::protocol::Op; use codex_core::protocol::PatchApplyBeginEvent; use codex_core::protocol::TaskCompleteEvent; -use codex_core::ReasoningItemReasoningSummary; -use codex_core::ResponseItem; -use codex_core::ContentItem; use crossterm::event::KeyEvent; use ratatui::buffer::Buffer; use ratatui::layout::Constraint; @@ -214,7 +214,8 @@ impl ChatWidget<'_> { // Only show text portion in conversation history for now. if !text.is_empty() { - self.conversation_history.add_user_message(&self.config, text); + self.conversation_history + .add_user_message(&self.config, text); } self.conversation_history.scroll_to_bottom(); } @@ -237,7 +238,8 @@ impl ChatWidget<'_> { .collect::>() .join(""); if role.eq_ignore_ascii_case("user") { - self.conversation_history.add_user_message(&self.config, text); + self.conversation_history + .add_user_message(&self.config, text); } else { self.conversation_history .add_agent_message(&self.config, text); @@ -290,7 +292,9 @@ impl ChatWidget<'_> { // record raw item for context-left calculation self.history_items.push(ResponseItem::Message { role: "assistant".to_string(), - content: vec![ContentItem::OutputText { text: message.clone() }], + content: vec![ContentItem::OutputText { + text: message.clone(), + }], }); self.request_redraw(); } @@ -310,7 +314,8 @@ impl ChatWidget<'_> { }) => { self.bottom_pane.set_task_running(false); // update context-left after turn completes - let pct = calculate_context_percent_remaining(&self.history_items, &self.config.model); + let pct = + calculate_context_percent_remaining(&self.history_items, &self.config.model); self.bottom_pane.set_context_percent(pct); self.request_redraw(); } @@ -318,7 +323,8 @@ impl ChatWidget<'_> { self.conversation_history.add_error(message); self.bottom_pane.set_task_running(false); // update context-left after error - let pct = calculate_context_percent_remaining(&self.history_items, &self.config.model); + let pct = + calculate_context_percent_remaining(&self.history_items, &self.config.model); self.bottom_pane.set_context_percent(pct); } EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent { @@ -350,8 +356,11 @@ impl ChatWidget<'_> { // prompt before they have seen *what* is being requested. // ------------------------------------------------------------------ - self.conversation_history - .add_patch_event(&self.config, PatchEventType::ApprovalRequest, changes); + self.conversation_history.add_patch_event( + &self.config, + PatchEventType::ApprovalRequest, + changes, + ); self.conversation_history.scroll_to_bottom(); @@ -380,8 +389,11 @@ impl ChatWidget<'_> { }) => { // Even when a patch is auto‑approved we still display the // summary so the user can follow along. - self.conversation_history - .add_patch_event(&self.config, PatchEventType::ApplyBegin { auto_approved }, changes); + self.conversation_history.add_patch_event( + &self.config, + PatchEventType::ApplyBegin { auto_approved }, + changes, + ); if !auto_approved { self.conversation_history.scroll_to_bottom(); } @@ -494,27 +506,48 @@ impl ChatWidget<'_> { self.next_shell_call_id += 1; // Split command into arguments, fallback to raw string if parse fails let args = shlex::split(&cmd).unwrap_or_else(|| vec![cmd.clone()]); - self.conversation_history.add_active_exec_command(call_id.clone(), args.clone()); + self.conversation_history + .add_active_exec_command(call_id.clone(), args.clone()); let tx = self.app_event_tx.clone(); // Spawn execution in background tokio::spawn(async move { - let output = std::process::Command::new("sh").arg("-c").arg(&cmd).output(); + let output = std::process::Command::new("sh") + .arg("-c") + .arg(&cmd) + .output(); match output { Ok(out) => { let stdout = String::from_utf8_lossy(&out.stdout).into_owned(); let stderr = String::from_utf8_lossy(&out.stderr).into_owned(); let code = out.status.code().unwrap_or(-1); - tx.send(AppEvent::ShellCommandResult { call_id, stdout, stderr, exit_code: code }); + tx.send(AppEvent::ShellCommandResult { + call_id, + stdout, + stderr, + exit_code: code, + }); } Err(e) => { - tx.send(AppEvent::ShellCommandResult { call_id, stdout: String::new(), stderr: e.to_string(), exit_code: -1 }); + tx.send(AppEvent::ShellCommandResult { + call_id, + stdout: String::new(), + stderr: e.to_string(), + exit_code: -1, + }); } } }); } /// Handle completion of a shell command: display its result. - pub fn handle_shell_command_result(&mut self, call_id: String, stdout: String, stderr: String, exit_code: i32) { - self.conversation_history.record_completed_exec_command(call_id, stdout, stderr, exit_code); + pub fn handle_shell_command_result( + &mut self, + call_id: String, + stdout: String, + stderr: String, + exit_code: i32, + ) { + self.conversation_history + .record_completed_exec_command(call_id, stdout, stderr, exit_code); } fn request_redraw(&mut self) { diff --git a/codex-rs/tui/src/config_reload.rs b/codex-rs/tui/src/config_reload.rs index 8f379dec5a..5b43f56ab6 100644 --- a/codex-rs/tui/src/config_reload.rs +++ b/codex-rs/tui/src/config_reload.rs @@ -17,6 +17,10 @@ mod tests { let old = "a\nb\nc\n"; let new = "a\nx\nc\n"; let diff = generate_diff(old, new); - assert!(diff.contains("-b\n+x\n"), "Unexpected diff output: {}", diff); + assert!( + diff.contains("-b\n+x\n"), + "Unexpected diff output: {}", + diff + ); } } diff --git a/codex-rs/tui/src/context.rs b/codex-rs/tui/src/context.rs index cb23c8da55..1f925cd746 100644 --- a/codex-rs/tui/src/context.rs +++ b/codex-rs/tui/src/context.rs @@ -1,8 +1,8 @@ //! Utilities for computing approximate token usage and remaining context percentage //! in the TUI, mirroring the JS heuristics in `calculateContextPercentRemaining`. -use codex_core::ResponseItem; use codex_core::ContentItem; +use codex_core::ResponseItem; /// Roughly estimate number of model tokens represented by the given response items. /// Counts characters in text and function-call items, divides by 4 and rounds up. @@ -10,18 +10,21 @@ pub fn approximate_tokens_used(items: &[ResponseItem]) -> usize { let mut char_count = 0; for item in items { match item { - ResponseItem::Message { role, content } if role.eq_ignore_ascii_case("user") - || role.eq_ignore_ascii_case("assistant") => + ResponseItem::Message { role, content } + if role.eq_ignore_ascii_case("user") || role.eq_ignore_ascii_case("assistant") => { for ci in content { match ci { - ContentItem::InputText { text } - | ContentItem::OutputText { text } => char_count += text.len(), + ContentItem::InputText { text } | ContentItem::OutputText { text } => { + char_count += text.len() + } _ => {} } } } - ResponseItem::FunctionCall { name, arguments, .. } => { + ResponseItem::FunctionCall { + name, arguments, .. + } => { char_count += name.len(); char_count += arguments.len(); } @@ -54,10 +57,7 @@ pub fn max_tokens_for_model(model: &str) -> usize { /// Compute the percentage of tokens remaining in context for a given model. /// Returns a floating-point percent (0.0–100.0). -pub fn calculate_context_percent_remaining( - items: &[ResponseItem], - model: &str, -) -> f64 { +pub fn calculate_context_percent_remaining(items: &[ResponseItem], model: &str) -> f64 { let used = approximate_tokens_used(items); let max = max_tokens_for_model(model); let remaining = max.saturating_sub(used); diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index fb8ce37037..0c3700cfce 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -6,11 +6,11 @@ use crate::text_formatting::format_and_truncate_tool_result; use base64::Engine; use codex_ansi_escape::ansi_escape_line; use codex_common::elapsed::format_duration; +use codex_core::WireApi; use codex_core::config::Config; use codex_core::model_supports_reasoning_summaries; use codex_core::protocol::FileChange; use codex_core::protocol::SessionConfiguredEvent; -use codex_core::WireApi; use image::DynamicImage; use image::GenericImageView; use image::ImageReader; @@ -22,9 +22,9 @@ use ratatui::style::Modifier; use ratatui::style::Style; use ratatui::text::Line as RtLine; use ratatui::text::Span as RtSpan; -use ratatui_image::picker::ProtocolType; use ratatui_image::Image as TuiImage; use ratatui_image::Resize as ImgResize; +use ratatui_image::picker::ProtocolType; use std::collections::HashMap; use std::io::Cursor; use std::path::PathBuf; @@ -76,7 +76,8 @@ pub(crate) enum PatchEventType { /// Represents an event to display in the conversation history. Returns its /// `Vec>` representation to make it easier to display in a /// scrollable list. -pub(crate) enum HistoryCell { +/// High‑level cells in the conversation history, rendered as text blocks. +pub enum HistoryCell { /// Welcome message. WelcomeMessage { view: TextBlock }, @@ -219,121 +220,130 @@ impl HistoryCell { } } -pub(crate) fn new_user_prompt(config: &Config, message: String) -> Self { - let body: Vec> = message - .lines() - .map(|l| RtLine::from(l.to_string())) - .collect(); - let label = RtSpan::styled( - "user".to_string(), - Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD), - ); - // Render sender and content according to sender_break_line; insert message spacing if configured - let mut lines = if config.tui.sender_break_line { - let mut l = Vec::new(); - // label on its own line - l.push(RtLine::from(vec![label.clone()])); - // then message body lines - l.extend(body.clone()); - l - } else { - // combine sender label and first line of body, indenting subsequent lines - let mut l = Vec::new(); - if let Some(first) = body.get(0) { - let mut spans = vec![label.clone(), RtSpan::raw(" ".to_string())]; - spans.extend(first.spans.clone()); - l.push(RtLine::from(spans).style(first.style)); - let indent = " ".to_string(); - for ln in body.iter().skip(1) { - let text: String = ln.spans.iter().map(|s| s.content.clone()).collect(); - l.push(RtLine::from(indent.clone() + &text)); - } - } else { + /// Create a user prompt cell for testing or rendering user messages. + pub fn new_user_prompt(config: &Config, message: String) -> Self { + let body: Vec> = message + .lines() + .map(|l| RtLine::from(l.to_string())) + .collect(); + let label = RtSpan::styled( + "user".to_string(), + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + ); + // Render sender and content according to sender_break_line; insert message spacing if configured + let mut lines = if config.tui.sender_break_line { + let mut l = Vec::new(); + // label on its own line l.push(RtLine::from(vec![label.clone()])); + // then message body lines + l.extend(body.clone()); + l + } else { + // combine sender label and first line of body, indenting subsequent lines + let mut l = Vec::new(); + if let Some(first) = body.get(0) { + let mut spans = vec![label.clone(), RtSpan::raw(" ".to_string())]; + spans.extend(first.spans.clone()); + l.push(RtLine::from(spans).style(first.style)); + let indent = " ".to_string(); + for ln in body.iter().skip(1) { + let text: String = ln.spans.iter().map(|s| s.content.clone()).collect(); + l.push(RtLine::from(indent.clone() + &text)); + } + } else { + l.push(RtLine::from(vec![label.clone()])); + } + l + }; + if config.tui.message_spacing { + lines.push(RtLine::from("")); + } + HistoryCell::UserPrompt { + view: TextBlock::new(lines), } - l - }; - if config.tui.message_spacing { - lines.push(RtLine::from("")); } - HistoryCell::UserPrompt { - view: TextBlock::new(lines), - } -} - pub(crate) fn new_agent_message(config: &Config, message: String) -> Self { + /// Create an agent message cell for testing or rendering agent messages. + pub fn new_agent_message(config: &Config, message: String) -> Self { let mut md_lines: Vec> = Vec::new(); append_markdown(&message, &mut md_lines, config); - let label = RtSpan::styled( - "codex".to_string(), - Style::default().fg(Color::Magenta).add_modifier(Modifier::BOLD), - ); - // Render sender and content according to sender_break_line; insert message spacing if configured - let mut lines = if config.tui.sender_break_line { - let mut l = Vec::new(); - l.push(RtLine::from(vec![label.clone()])); - l.extend(md_lines.clone()); - l - } else { - let mut l = Vec::new(); - if let Some(first) = md_lines.get(0) { - let mut spans = vec![label.clone(), RtSpan::raw(" ".to_string())]; - spans.extend(first.spans.clone()); - l.push(RtLine::from(spans).style(first.style)); - let indent = " ".to_string(); - for ln in md_lines.iter().skip(1) { - let text: String = ln.spans.iter().map(|s| s.content.clone()).collect(); - l.push(RtLine::from(indent.clone() + &text)); - } - } else { + let label = RtSpan::styled( + "codex".to_string(), + Style::default() + .fg(Color::Magenta) + .add_modifier(Modifier::BOLD), + ); + // Render sender and content according to sender_break_line; insert message spacing if configured + let mut lines = if config.tui.sender_break_line { + let mut l = Vec::new(); l.push(RtLine::from(vec![label.clone()])); + l.extend(md_lines.clone()); + l + } else { + let mut l = Vec::new(); + if let Some(first) = md_lines.get(0) { + let mut spans = vec![label.clone(), RtSpan::raw(" ".to_string())]; + spans.extend(first.spans.clone()); + l.push(RtLine::from(spans).style(first.style)); + let indent = " ".to_string(); + for ln in md_lines.iter().skip(1) { + let text: String = ln.spans.iter().map(|s| s.content.clone()).collect(); + l.push(RtLine::from(indent.clone() + &text)); + } + } else { + l.push(RtLine::from(vec![label.clone()])); + } + l + }; + if config.tui.message_spacing { + lines.push(RtLine::from("")); + } + HistoryCell::AgentMessage { + view: TextBlock::new(lines), } - l - }; - if config.tui.message_spacing { - lines.push(RtLine::from("")); } - HistoryCell::AgentMessage { - view: TextBlock::new(lines), - } -} - pub(crate) fn new_agent_reasoning(config: &Config, text: String) -> Self { + /// Create an agent reasoning cell for testing or rendering agent reasoning. + pub fn new_agent_reasoning(config: &Config, text: String) -> Self { let mut md_lines: Vec> = Vec::new(); append_markdown(&text, &mut md_lines, config); - let label = RtSpan::styled( - "thinking".to_string(), - Style::default().fg(Color::Magenta).add_modifier(Modifier::ITALIC), - ); - // Render sender and content according to sender_break_line; insert message spacing if configured - let mut lines = if config.tui.sender_break_line { - let mut l = Vec::new(); - l.push(RtLine::from(vec![label.clone()])); - l.extend(md_lines.clone()); - l - } else { - let mut l = Vec::new(); - if let Some(first) = md_lines.get(0) { - let mut spans = vec![label.clone(), RtSpan::raw(" ".to_string())]; - spans.extend(first.spans.clone()); - l.push(RtLine::from(spans).style(first.style)); - let indent = " ".to_string(); - for ln in md_lines.iter().skip(1) { - let text: String = ln.spans.iter().map(|s| s.content.clone()).collect(); - l.push(RtLine::from(indent.clone() + &text)); - } - } else { + let label = RtSpan::styled( + "thinking".to_string(), + Style::default() + .fg(Color::Magenta) + .add_modifier(Modifier::ITALIC), + ); + // Render sender and content according to sender_break_line; insert message spacing if configured + let mut lines = if config.tui.sender_break_line { + let mut l = Vec::new(); l.push(RtLine::from(vec![label.clone()])); + l.extend(md_lines.clone()); + l + } else { + let mut l = Vec::new(); + if let Some(first) = md_lines.get(0) { + let mut spans = vec![label.clone(), RtSpan::raw(" ".to_string())]; + spans.extend(first.spans.clone()); + l.push(RtLine::from(spans).style(first.style)); + let indent = " ".to_string(); + for ln in md_lines.iter().skip(1) { + let text: String = ln.spans.iter().map(|s| s.content.clone()).collect(); + l.push(RtLine::from(indent.clone() + &text)); + } + } else { + l.push(RtLine::from(vec![label.clone()])); + } + l + }; + if config.tui.message_spacing { + lines.push(RtLine::from("")); + } + HistoryCell::AgentReasoning { + view: TextBlock::new(lines), } - l - }; - if config.tui.message_spacing { - lines.push(RtLine::from("")); } - HistoryCell::AgentReasoning { - view: TextBlock::new(lines), - } -} pub(crate) fn new_active_exec_command(call_id: String, command: Vec) -> Self { let command_escaped = strip_bash_lc_and_escape(&command); @@ -603,44 +613,65 @@ pub(crate) fn new_user_prompt(config: &Config, message: String) -> Self { /// Create a new `PendingPatch` cell that lists the file‑level summary of /// a proposed patch. The summary lines should already be formatted (e.g. /// "A path/to/file.rs"). -pub(crate) fn new_patch_event(config: &Config, event_type: PatchEventType, changes: HashMap) -> Self { - // Handle applied patch immediately. - if let PatchEventType::ApplyBegin { auto_approved: false } = event_type { - let lines = vec![RtLine::from("patch applied".magenta().bold())]; - return Self::PendingPatch { view: TextBlock::new(lines) }; - } - let title = match event_type { - PatchEventType::ApprovalRequest => "proposed patch", - PatchEventType::ApplyBegin { auto_approved: true } => "applying patch", - _ => unreachable!(), - }; - let summary = create_diff_summary(changes); - let body: Vec> = summary.into_iter().map(|line| { - if line.starts_with('+') { - RtLine::from(line).green() - } else if line.starts_with('-') { - RtLine::from(line).red() - } else if let Some(idx) = line.find(' ') { - let kind = line[..idx].to_string(); - let rest = line[idx + 1..].to_string(); - let style_for = |fg| Style::default().fg(fg).add_modifier(Modifier::BOLD); - let kind_span = match kind.as_str() { - "A" => RtSpan::styled(kind.clone(), style_for(Color::Green)), - "D" => RtSpan::styled(kind.clone(), style_for(Color::Red)), - "M" => RtSpan::styled(kind.clone(), style_for(Color::Yellow)), - "R" | "C" => RtSpan::styled(kind.clone(), style_for(Color::Cyan)), - _ => RtSpan::raw(kind.clone()), + pub(crate) fn new_patch_event( + config: &Config, + event_type: PatchEventType, + changes: HashMap, + ) -> Self { + // Handle applied patch immediately. + if let PatchEventType::ApplyBegin { + auto_approved: false, + } = event_type + { + let lines = vec![RtLine::from("patch applied".magenta().bold())]; + return Self::PendingPatch { + view: TextBlock::new(lines), }; - RtLine::from(vec![kind_span, RtSpan::raw(" "), RtSpan::raw(rest)]) - } else { - RtLine::from(line) } - }).collect(); - let label = RtSpan::styled(title.to_string(), Style::default().fg(Color::Magenta).add_modifier(Modifier::BOLD)); - let mut lines = render_header_body(config, label, body); - lines.push(RtLine::from("")); - HistoryCell::PendingPatch { view: TextBlock::new(lines) } -} + let title = match event_type { + PatchEventType::ApprovalRequest => "proposed patch", + PatchEventType::ApplyBegin { + auto_approved: true, + } => "applying patch", + _ => unreachable!(), + }; + let summary = create_diff_summary(changes); + let body: Vec> = summary + .into_iter() + .map(|line| { + if line.starts_with('+') { + RtLine::from(line).green() + } else if line.starts_with('-') { + RtLine::from(line).red() + } else if let Some(idx) = line.find(' ') { + let kind = line[..idx].to_string(); + let rest = line[idx + 1..].to_string(); + let style_for = |fg| Style::default().fg(fg).add_modifier(Modifier::BOLD); + let kind_span = match kind.as_str() { + "A" => RtSpan::styled(kind.clone(), style_for(Color::Green)), + "D" => RtSpan::styled(kind.clone(), style_for(Color::Red)), + "M" => RtSpan::styled(kind.clone(), style_for(Color::Yellow)), + "R" | "C" => RtSpan::styled(kind.clone(), style_for(Color::Cyan)), + _ => RtSpan::raw(kind.clone()), + }; + RtLine::from(vec![kind_span, RtSpan::raw(" "), RtSpan::raw(rest)]) + } else { + RtLine::from(line) + } + }) + .collect(); + let label = RtSpan::styled( + title.to_string(), + Style::default() + .fg(Color::Magenta) + .add_modifier(Modifier::BOLD), + ); + let mut lines = render_header_body(config, label, body); + lines.push(RtLine::from("")); + HistoryCell::PendingPatch { + view: TextBlock::new(lines), + } + } } // --------------------------------------------------------------------------- @@ -764,7 +795,7 @@ fn create_diff_summary(changes: HashMap) -> Vec { /// available space. Keeping the resized copy around saves a costly rescale /// between the back-to-back `height()` and `render_window()` calls that the /// scroll-view performs while laying out the UI. -pub(crate) struct ImageRenderCache { +pub struct ImageRenderCache { /// Width in *terminal cells* the cached image was generated for. width_cells: u16, /// Height in *terminal rows* that the conversation cell must occupy so diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index a60b47dbcc..677f62edc5 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -31,24 +31,24 @@ mod cell_widget; mod chatwidget; mod citation_regex; mod cli; +mod config_reload; +mod confirm_ctrl_d; +pub mod context; mod conversation_history_widget; mod exec_command; mod git_warning_screen; -mod history_cell; +pub mod history_cell; mod log_layer; mod login_screen; mod markdown; mod mouse_capture; mod scroll_event_helper; mod slash_command; -mod confirm_ctrl_d; mod status_indicator_widget; -pub mod context; -mod text_block; +pub mod text_block; mod text_formatting; mod tui; mod user_approval_widget; -mod config_reload; pub use cli::Cli; @@ -222,16 +222,19 @@ fn run_ratatui_app( let app_event_tx = app.event_sender(); let config_path = config.codex_home.join("config.toml"); std::thread::spawn(move || { - use notify::{Watcher, RecursiveMode, RecommendedWatcher, EventKind}; + use notify::{EventKind, RecommendedWatcher, RecursiveMode, Watcher}; use std::sync::mpsc::channel; use std::time::Duration; let (tx, rx) = channel(); - let mut watcher: RecommendedWatcher = - Watcher::new(tx, notify::Config::default()).unwrap_or_else(|e| { + let mut watcher: RecommendedWatcher = Watcher::new(tx, notify::Config::default()) + .unwrap_or_else(|e| { tracing::error!("config watcher failed: {e}"); std::process::exit(1); }); - if watcher.watch(&config_path, RecursiveMode::NonRecursive).is_err() { + if watcher + .watch(&config_path, RecursiveMode::NonRecursive) + .is_err() + { tracing::error!("Failed to watch config.toml"); return; } @@ -244,9 +247,8 @@ fn run_ratatui_app( if new != last { let diff = crate::config_reload::generate_diff(&last, &new); last = new.clone(); - app_event_tx.send( - crate::app_event::AppEvent::ConfigReloadRequest(diff) - ); + app_event_tx + .send(crate::app_event::AppEvent::ConfigReloadRequest(diff)); } } } diff --git a/codex-rs/tui/src/markdown.rs b/codex-rs/tui/src/markdown.rs index 8212cbf63a..f5c9ed1d05 100644 --- a/codex-rs/tui/src/markdown.rs +++ b/codex-rs/tui/src/markdown.rs @@ -13,7 +13,12 @@ pub(crate) fn append_markdown( config: &Config, ) { let mut new_lines = Vec::new(); - append_markdown_with_opener_and_cwd(markdown_source, &mut new_lines, config.file_opener, &config.cwd); + append_markdown_with_opener_and_cwd( + markdown_source, + &mut new_lines, + config.file_opener, + &config.cwd, + ); if config.tui.markdown_compact { for line in collapse_heading_blank_lines(new_lines) { lines.push(line); @@ -66,7 +71,11 @@ fn collapse_heading_blank_lines(lines: Vec>) -> Vec> let mut result = Vec::with_capacity(lines.len()); let mut prev_was_heading = false; for line in lines { - let content = line.spans.iter().map(|s| s.content.clone()).collect::(); + let content = line + .spans + .iter() + .map(|s| s.content.clone()) + .collect::(); if prev_was_heading && content.trim().is_empty() { continue; } diff --git a/codex-rs/tui/src/slash_command.rs b/codex-rs/tui/src/slash_command.rs index fd2d4fdb3b..862314f691 100644 --- a/codex-rs/tui/src/slash_command.rs +++ b/codex-rs/tui/src/slash_command.rs @@ -32,13 +32,15 @@ impl SlashCommand { pub fn description(self) -> &'static str { match self { SlashCommand::New => "Start a new chat.", - SlashCommand::ToggleMouseMode => - "Toggle mouse mode (enable for scrolling, disable for text selection)", - SlashCommand::EditPrompt => - "Open external editor to edit the current prompt.", + SlashCommand::ToggleMouseMode => { + "Toggle mouse mode (enable for scrolling, disable for text selection)" + } + SlashCommand::EditPrompt => "Open external editor to edit the current prompt.", SlashCommand::MountAdd => "Add a mount: host path → container path.", SlashCommand::MountRemove => "Remove a mount by container path.", - SlashCommand::InspectEnv => "Inspect sandbox and container environment (mounts, permissions, network)", + SlashCommand::InspectEnv => { + "Inspect sandbox and container environment (mounts, permissions, network)" + } SlashCommand::Shell => "Run a shell command in the container.", SlashCommand::Quit => "Exit the application.", } diff --git a/codex-rs/tui/src/status_indicator_widget.rs b/codex-rs/tui/src/status_indicator_widget.rs index 8581d7ac2f..e15896b741 100644 --- a/codex-rs/tui/src/status_indicator_widget.rs +++ b/codex-rs/tui/src/status_indicator_widget.rs @@ -99,8 +99,7 @@ impl WidgetRef for StatusIndicatorWidget { fn render_ref(&self, area: Rect, buf: &mut Buffer) { // Flush status text to the top of the input border, without extra padding // Remove surrounding border for a cleaner in-line status display - let block = Block::default() - .padding(Padding::new(0, 0, 0, 0)); + let block = Block::default().padding(Padding::new(0, 0, 0, 0)); // Animated 3‑dot pattern inside brackets. The *active* dot is bold // white, the others are dim. const DOT_COUNT: usize = 3; diff --git a/codex-rs/tui/src/text_block.rs b/codex-rs/tui/src/text_block.rs index 2c68d90f11..b011f99cc3 100644 --- a/codex-rs/tui/src/text_block.rs +++ b/codex-rs/tui/src/text_block.rs @@ -3,13 +3,16 @@ use ratatui::prelude::*; /// A simple widget that just displays a list of `Line`s via a `Paragraph`. /// This is the default rendering backend for most `HistoryCell` variants. +/// A simple widget that displays a list of lines via a paragraph. #[derive(Clone)] -pub(crate) struct TextBlock { - pub(crate) lines: Vec>, +pub struct TextBlock { + /// The content lines to render. + pub lines: Vec>, } impl TextBlock { - pub(crate) fn new(lines: Vec>) -> Self { + /// Create a new text block from preformatted lines. + pub fn new(lines: Vec>) -> Self { Self { lines } } } diff --git a/codex-rs/tui/src/user_approval_widget.rs b/codex-rs/tui/src/user_approval_widget.rs index aa143b8538..b312b2d5ba 100644 --- a/codex-rs/tui/src/user_approval_widget.rs +++ b/codex-rs/tui/src/user_approval_widget.rs @@ -110,7 +110,10 @@ fn truncate_middle(text: &str, max_len: usize) -> String { /// Build the dynamic session-scoped approval label for the given command string. fn session_scoped_label(cmd: &str, max_len: usize) -> String { let snippet = truncate_middle(cmd, max_len); - format!("Yes, always allow running `{}` for this session (a)", snippet) + format!( + "Yes, always allow running `{}` for this session (a)", + snippet + ) } /// Internal mode the widget is in – mirrors the TypeScript component. @@ -385,7 +388,9 @@ impl WidgetRef for &UserApprovalWidget<'_> { .enumerate() .map(|(idx, opt)| { let label = if idx == 1 { - dynamic_label.clone().unwrap_or_else(|| opt.label.to_string()) + dynamic_label + .clone() + .unwrap_or_else(|| opt.label.to_string()) } else { opt.label.to_string() }; @@ -423,10 +428,10 @@ impl WidgetRef for &UserApprovalWidget<'_> { mod tests { use super::*; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; - use std::sync::mpsc; - use ratatui::buffer::{Buffer, Cell}; + use ratatui::buffer::Buffer; use ratatui::layout::Rect; - use ratatui::style::{Color, Style}; + use ratatui::style::Color; + use std::sync::mpsc; #[test] fn esc_in_input_mode_cancels_input_and_preserves_value() { @@ -473,7 +478,10 @@ mod tests { #[test] fn test_session_scoped_label_embeds_snippet() { let label = session_scoped_label("say hello", 50); - assert_eq!(label, "Yes, always allow running `say hello` for this session (a)"); + assert_eq!( + label, + "Yes, always allow running `say hello` for this session (a)" + ); let long_cmd = "x".repeat(100); let truncated = session_scoped_label(&long_cmd, 10); assert!(truncated.starts_with("Yes, always allow running `")); @@ -515,8 +523,20 @@ mod tests { for row in area.y..area.y + area.height { for col in area.x..area.x + area.width { let cell = buf.cell((col, row)).unwrap(); - assert_ne!(cell.bg, Color::Reset, "Found transparent cell at ({}, {})", col, row); - assert_ne!(cell.bg, Color::Red, "Found unfilled cell at ({}, {})", col, row); + assert_ne!( + cell.bg, + Color::Reset, + "Found transparent cell at ({}, {})", + col, + row + ); + assert_ne!( + cell.bg, + Color::Red, + "Found unfilled cell at ({}, {})", + col, + row + ); } } } diff --git a/codex-rs/tui/tests/context_percent.rs b/codex-rs/tui/tests/context_percent.rs index 809c578ec6..e28cc45867 100644 --- a/codex-rs/tui/tests/context_percent.rs +++ b/codex-rs/tui/tests/context_percent.rs @@ -1,19 +1,25 @@ use codex_core::{ContentItem, ResponseItem}; -use codex_tui::context::{approximate_tokens_used, calculate_context_percent_remaining, max_tokens_for_model}; +use codex_tui::context::{ + approximate_tokens_used, calculate_context_percent_remaining, max_tokens_for_model, +}; #[test] fn test_approximate_tokens_used_texts() { // 4 chars -> 1 token let items = vec![ResponseItem::Message { role: "user".into(), - content: vec![ContentItem::InputText { text: "abcd".into() }], + content: vec![ContentItem::InputText { + text: "abcd".into(), + }], }]; assert_eq!(approximate_tokens_used(&items), 1); // 7 chars -> 2 tokens let items = vec![ResponseItem::Message { role: "assistant".into(), - content: vec![ContentItem::OutputText { text: "example".into() }], + content: vec![ContentItem::OutputText { + text: "example".into(), + }], }]; assert_eq!(approximate_tokens_used(&items), 2); } diff --git a/codex-rs/tui/tests/message_layout.rs b/codex-rs/tui/tests/message_layout.rs index 7919b76e3a..49c41d6244 100644 --- a/codex-rs/tui/tests/message_layout.rs +++ b/codex-rs/tui/tests/message_layout.rs @@ -1,6 +1,6 @@ -use tempfile::TempDir; -use codex_core::config::{Config, ConfigToml, ConfigOverrides}; +use codex_core::config::{Config, ConfigOverrides, ConfigToml}; use codex_tui::history_cell::HistoryCell; +use tempfile::TempDir; /// Extract plain string content of each line for comparison. fn lines_from_userprompt(view: &codex_tui::text_block::TextBlock) -> Vec { @@ -42,9 +42,11 @@ fn test_user_message_layout_combinations() { if message_spacing { expected.push(String::new()); } - assert_eq!(got, expected, + assert_eq!( + got, expected, "Layout mismatch for sender_break_line={}, message_spacing={}", - sender_break, message_spacing); + sender_break, message_spacing + ); } } } @@ -81,9 +83,11 @@ fn test_agent_message_layout_combinations() { if message_spacing { expected.push(String::new()); } - assert_eq!(got, expected, + assert_eq!( + got, expected, "Agent layout mismatch for sender_break_line={}, message_spacing={}", - sender_break, message_spacing); + sender_break, message_spacing + ); } } } @@ -120,9 +124,11 @@ fn test_agent_reasoning_layout_combinations() { if message_spacing { expected.push(String::new()); } - assert_eq!(got, expected, + assert_eq!( + got, expected, "Reasoning layout mismatch for sender_break_line={}, message_spacing={}", - sender_break, message_spacing); + sender_break, message_spacing + ); } } }