From d642b07fcccdd8554316c61b88743378db43252c Mon Sep 17 00:00:00 2001 From: ae Date: Tue, 5 Aug 2025 23:57:52 -0700 Subject: [PATCH 1/6] [feat] add /status slash command (#1873) - Added a `/status` command, which will be useful when we update the home screen to print less status. - Moved `create_config_summary_entries` to common since it's used in a few places. - Noticed we inconsistently had periods in slash command descriptions and just removed them everywhere. - Noticed the diff description was overflowing so made it shorter. --- codex-rs/common/src/config_summary.rs | 29 ++++++++ codex-rs/common/src/lib.rs | 4 + codex-rs/exec/src/event_processor.rs | 26 ------- .../src/event_processor_with_human_output.rs | 2 +- .../src/event_processor_with_json_output.rs | 2 +- codex-rs/tui/src/app.rs | 5 ++ codex-rs/tui/src/chatwidget.rs | 7 ++ codex-rs/tui/src/history_cell.rs | 74 +++++++++++++------ codex-rs/tui/src/slash_command.rs | 12 +-- 9 files changed, 105 insertions(+), 56 deletions(-) create mode 100644 codex-rs/common/src/config_summary.rs diff --git a/codex-rs/common/src/config_summary.rs b/codex-rs/common/src/config_summary.rs new file mode 100644 index 0000000000..39d524731f --- /dev/null +++ b/codex-rs/common/src/config_summary.rs @@ -0,0 +1,29 @@ +use codex_core::WireApi; +use codex_core::config::Config; + +use crate::sandbox_summary::summarize_sandbox_policy; + +/// Build a list of key/value pairs summarizing the effective configuration. +pub fn create_config_summary_entries(config: &Config) -> Vec<(&'static str, String)> { + let mut entries = vec![ + ("workdir", config.cwd.display().to_string()), + ("model", config.model.clone()), + ("provider", config.model_provider_id.clone()), + ("approval", config.approval_policy.to_string()), + ("sandbox", summarize_sandbox_policy(&config.sandbox_policy)), + ]; + if config.model_provider.wire_api == WireApi::Responses + && config.model_family.supports_reasoning_summaries + { + entries.push(( + "reasoning effort", + config.model_reasoning_effort.to_string(), + )); + entries.push(( + "reasoning summaries", + config.model_reasoning_summary.to_string(), + )); + } + + entries +} diff --git a/codex-rs/common/src/lib.rs b/codex-rs/common/src/lib.rs index 3d498a8e2c..38f3832bfd 100644 --- a/codex-rs/common/src/lib.rs +++ b/codex-rs/common/src/lib.rs @@ -23,3 +23,7 @@ mod sandbox_summary; #[cfg(feature = "sandbox_summary")] pub use sandbox_summary::summarize_sandbox_policy; + +mod config_summary; + +pub use config_summary::create_config_summary_entries; diff --git a/codex-rs/exec/src/event_processor.rs b/codex-rs/exec/src/event_processor.rs index 0a2a141eca..b7b3c27dc5 100644 --- a/codex-rs/exec/src/event_processor.rs +++ b/codex-rs/exec/src/event_processor.rs @@ -1,7 +1,5 @@ use std::path::Path; -use codex_common::summarize_sandbox_policy; -use codex_core::WireApi; use codex_core::config::Config; use codex_core::protocol::Event; @@ -19,30 +17,6 @@ pub(crate) trait EventProcessor { fn process_event(&mut self, event: Event) -> CodexStatus; } -pub(crate) fn create_config_summary_entries(config: &Config) -> Vec<(&'static str, String)> { - let mut entries = vec![ - ("workdir", config.cwd.display().to_string()), - ("model", config.model.clone()), - ("provider", config.model_provider_id.clone()), - ("approval", config.approval_policy.to_string()), - ("sandbox", summarize_sandbox_policy(&config.sandbox_policy)), - ]; - if config.model_provider.wire_api == WireApi::Responses - && config.model_family.supports_reasoning_summaries - { - entries.push(( - "reasoning effort", - config.model_reasoning_effort.to_string(), - )); - entries.push(( - "reasoning summaries", - config.model_reasoning_summary.to_string(), - )); - } - - entries -} - pub(crate) fn handle_last_message(last_agent_message: Option<&str>, output_file: &Path) { let message = last_agent_message.unwrap_or_default(); write_last_message_file(message, Some(output_file)); diff --git a/codex-rs/exec/src/event_processor_with_human_output.rs b/codex-rs/exec/src/event_processor_with_human_output.rs index 393ef4ab1b..6b03ed7882 100644 --- a/codex-rs/exec/src/event_processor_with_human_output.rs +++ b/codex-rs/exec/src/event_processor_with_human_output.rs @@ -33,8 +33,8 @@ use std::time::Instant; use crate::event_processor::CodexStatus; use crate::event_processor::EventProcessor; -use crate::event_processor::create_config_summary_entries; use crate::event_processor::handle_last_message; +use codex_common::create_config_summary_entries; /// This should be configurable. When used in CI, users may not want to impose /// a limit so they can see the full transcript. diff --git a/codex-rs/exec/src/event_processor_with_json_output.rs b/codex-rs/exec/src/event_processor_with_json_output.rs index 1d153add6e..76985518e6 100644 --- a/codex-rs/exec/src/event_processor_with_json_output.rs +++ b/codex-rs/exec/src/event_processor_with_json_output.rs @@ -9,8 +9,8 @@ use serde_json::json; use crate::event_processor::CodexStatus; use crate::event_processor::EventProcessor; -use crate::event_processor::create_config_summary_entries; use crate::event_processor::handle_last_message; +use codex_common::create_config_summary_entries; pub(crate) struct EventProcessorWithJsonOutput { last_message_path: Option, diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index da5410d392..eee2a61c2e 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -330,6 +330,11 @@ impl App<'_> { widget.add_diff_output(text); } } + SlashCommand::Status => { + if let AppState::Chat { widget } = &mut self.app_state { + widget.add_status_output(); + } + } #[cfg(debug_assertions)] SlashCommand::TestApproval => { use std::collections::HashMap; diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 86bf765f2c..6d03be783b 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -522,6 +522,13 @@ impl ChatWidget<'_> { self.add_to_history(HistoryCell::new_diff_output(diff_output.clone())); } + pub(crate) fn add_status_output(&mut self) { + self.add_to_history(HistoryCell::new_status_output( + &self.config, + &self.token_usage, + )); + } + /// Forward file-search results to the bottom pane. pub(crate) fn apply_file_search_result(&mut self, query: String, matches: Vec) { self.bottom_pane.on_file_search_result(query, matches); diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index 332d90a647..5b7d9246f7 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -3,9 +3,8 @@ use crate::text_block::TextBlock; use crate::text_formatting::format_and_truncate_tool_result; use base64::Engine; use codex_ansi_escape::ansi_escape_line; +use codex_common::create_config_summary_entries; use codex_common::elapsed::format_duration; -use codex_common::summarize_sandbox_policy; -use codex_core::WireApi; use codex_core::config::Config; use codex_core::plan_tool::PlanItemArg; use codex_core::plan_tool::StepStatus; @@ -14,6 +13,7 @@ use codex_core::protocol::FileChange; use codex_core::protocol::McpInvocation; use codex_core::protocol::PatchApplyEndEvent; use codex_core::protocol::SessionConfiguredEvent; +use codex_core::protocol::TokenUsage; use image::DynamicImage; use image::ImageReader; use mcp_types::EmbeddedResourceResource; @@ -114,6 +114,11 @@ pub(crate) enum HistoryCell { view: TextBlock, }, + /// Output from the `/status` command. + StatusOutput { + view: TextBlock, + }, + /// Error event from the backend. ErrorEvent { view: TextBlock, @@ -154,6 +159,7 @@ impl HistoryCell { | HistoryCell::UserPrompt { view } | HistoryCell::BackgroundEvent { view } | HistoryCell::GitDiffOutput { view } + | HistoryCell::StatusOutput { view } | HistoryCell::ErrorEvent { view } | HistoryCell::SessionInfo { view } | HistoryCell::CompletedExecCommand { view } @@ -200,26 +206,7 @@ impl HistoryCell { ]), ]; - let mut entries = vec![ - ("workdir", config.cwd.display().to_string()), - ("model", config.model.clone()), - ("provider", config.model_provider_id.clone()), - ("approval", config.approval_policy.to_string()), - ("sandbox", summarize_sandbox_policy(&config.sandbox_policy)), - ]; - if config.model_provider.wire_api == WireApi::Responses - && config.model_family.supports_reasoning_summaries - { - entries.push(( - "reasoning effort", - config.model_reasoning_effort.to_string(), - )); - entries.push(( - "reasoning summaries", - config.model_reasoning_summary.to_string(), - )); - } - for (key, value) in entries { + for (key, value) in create_config_summary_entries(config) { lines.push(Line::from(vec![format!("{key}: ").bold(), value.into()])); } lines.push(Line::from("")); @@ -476,6 +463,49 @@ impl HistoryCell { } } + pub(crate) fn new_status_output(config: &Config, usage: &TokenUsage) -> Self { + let mut lines: Vec> = Vec::new(); + lines.push(Line::from("/status".magenta())); + + // Config + for (key, value) in create_config_summary_entries(config) { + lines.push(Line::from(vec![format!("{key}: ").bold(), value.into()])); + } + + // Token usage + lines.push(Line::from("")); + lines.push(Line::from("token usage".bold())); + lines.push(Line::from(vec![ + " input: ".bold(), + usage.input_tokens.to_string().into(), + ])); + lines.push(Line::from(vec![ + " cached input: ".bold(), + usage.cached_input_tokens.unwrap_or(0).to_string().into(), + ])); + lines.push(Line::from(vec![ + " output: ".bold(), + usage.output_tokens.to_string().into(), + ])); + lines.push(Line::from(vec![ + " reasoning output: ".bold(), + usage + .reasoning_output_tokens + .unwrap_or(0) + .to_string() + .into(), + ])); + lines.push(Line::from(vec![ + " total: ".bold(), + usage.total_tokens.to_string().into(), + ])); + + lines.push(Line::from("")); + HistoryCell::StatusOutput { + view: TextBlock::new(lines), + } + } + pub(crate) fn new_error_event(message: String) -> Self { let lines: Vec> = vec![ vec!["ERROR: ".red().bold(), message.into()].into(), diff --git a/codex-rs/tui/src/slash_command.rs b/codex-rs/tui/src/slash_command.rs index d82a16608f..85dde7a113 100644 --- a/codex-rs/tui/src/slash_command.rs +++ b/codex-rs/tui/src/slash_command.rs @@ -15,6 +15,7 @@ pub enum SlashCommand { New, Compact, Diff, + Status, Quit, #[cfg(debug_assertions)] TestApproval, @@ -24,12 +25,11 @@ impl SlashCommand { /// User-visible description shown in the popup. pub fn description(self) -> &'static str { match self { - SlashCommand::New => "Start a new chat.", - SlashCommand::Compact => "Compact the chat history.", - SlashCommand::Quit => "Exit the application.", - SlashCommand::Diff => { - "Show git diff of the working directory (including untracked files)" - } + SlashCommand::New => "Start a new chat", + SlashCommand::Compact => "Compact the chat history", + SlashCommand::Quit => "Exit the application", + SlashCommand::Diff => "Show git diff (including untracked files)", + SlashCommand::Status => "Show current session configuration and token usage", #[cfg(debug_assertions)] SlashCommand::TestApproval => "Test approval request", } From cda39e417fb3c4d91f02ccc93acc296b77f5b947 Mon Sep 17 00:00:00 2001 From: Dylan Date: Wed, 6 Aug 2025 00:07:58 -0700 Subject: [PATCH 2/6] [tests] Investigate flakey mcp-server test (#1877) ## Summary Have seen these tests flaking over the course of today on different boxes. `wiremock` seems to be generally written with tokio/threads in mind but based on the weird panics from the tests, let's see if this helps. --- codex-rs/mcp-server/tests/send_message.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codex-rs/mcp-server/tests/send_message.rs b/codex-rs/mcp-server/tests/send_message.rs index fd4b210b0b..fd3718e8f3 100644 --- a/codex-rs/mcp-server/tests/send_message.rs +++ b/codex-rs/mcp-server/tests/send_message.rs @@ -18,7 +18,7 @@ use tokio::time::timeout; const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10); -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[tokio::test] async fn test_send_message_success() { // Spin up a mock completions server that immediately ends the Codex turn. // Two Codex turns hit the mock model (session start + send-user-message). Provide two SSE responses. @@ -105,7 +105,7 @@ async fn test_send_message_success() { drop(server); } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[tokio::test] async fn test_send_message_session_not_found() { // Start MCP without creating a Codex session let codex_home = TempDir::new().expect("tempdir"); From 3e8bcf0247ee37a226dd3c8b1e7e6695ccb9321b Mon Sep 17 00:00:00 2001 From: Dylan Date: Wed, 6 Aug 2025 01:13:31 -0700 Subject: [PATCH 3/6] [prompts] Add (#1869) ## Summary Includes a new user message in the api payload which provides useful environment context for the model, so it knows about things like the current working directory and the sandbox. ## Testing Updated unit tests --- codex-rs/core/src/chat_completions.rs | 6 +- codex-rs/core/src/client.rs | 11 +--- codex-rs/core/src/client_common.rs | 77 ++++++++++++++++++++++- codex-rs/core/src/codex.rs | 9 +++ codex-rs/core/src/git_info.rs | 2 +- codex-rs/core/src/protocol.rs | 3 +- codex-rs/core/tests/client.rs | 47 ++++++++++---- codex-rs/mcp-server/tests/send_message.rs | 2 +- 8 files changed, 126 insertions(+), 31 deletions(-) diff --git a/codex-rs/core/src/chat_completions.rs b/codex-rs/core/src/chat_completions.rs index 98ef7f26cc..dae140bc02 100644 --- a/codex-rs/core/src/chat_completions.rs +++ b/codex-rs/core/src/chat_completions.rs @@ -41,11 +41,9 @@ pub(crate) async fn stream_chat_completions( let full_instructions = prompt.get_full_instructions(model_family); messages.push(json!({"role": "system", "content": full_instructions})); - if let Some(instr) = &prompt.get_formatted_user_instructions() { - messages.push(json!({"role": "user", "content": instr})); - } + let input = prompt.get_formatted_input(); - for item in &prompt.input { + for item in &input { match item { ResponseItem::Message { role, content, .. } => { let mut text = String::new(); diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index e4bb30da26..9748cde7cb 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -34,7 +34,6 @@ use crate::error::Result; use crate::flags::CODEX_RS_SSE_FIXTURE; use crate::model_provider_info::ModelProviderInfo; use crate::model_provider_info::WireApi; -use crate::models::ContentItem; use crate::models::ResponseItem; use crate::openai_tools::create_tools_json_for_responses_api; use crate::protocol::TokenUsage; @@ -146,15 +145,7 @@ impl ModelClient { vec![] }; - let mut input_with_instructions = Vec::with_capacity(prompt.input.len() + 1); - if let Some(ui) = prompt.get_formatted_user_instructions() { - input_with_instructions.push(ResponseItem::Message { - id: None, - role: "user".to_string(), - content: vec![ContentItem::InputText { text: ui }], - }); - } - input_with_instructions.extend(prompt.input.clone()); + let input_with_instructions = prompt.get_formatted_input(); let payload = ResponsesApiRequest { model: &self.config.model, diff --git a/codex-rs/core/src/client_common.rs b/codex-rs/core/src/client_common.rs index 60164f5fde..2ca060f4a4 100644 --- a/codex-rs/core/src/client_common.rs +++ b/codex-rs/core/src/client_common.rs @@ -1,14 +1,20 @@ use crate::config_types::ReasoningEffort as ReasoningEffortConfig; use crate::config_types::ReasoningSummary as ReasoningSummaryConfig; use crate::error::Result; +use crate::git_info::GitInfo; use crate::model_family::ModelFamily; +use crate::models::ContentItem; use crate::models::ResponseItem; use crate::openai_tools::OpenAiTool; +use crate::protocol::AskForApproval; +use crate::protocol::SandboxPolicy; use crate::protocol::TokenUsage; use codex_apply_patch::APPLY_PATCH_TOOL_INSTRUCTIONS; use futures::Stream; use serde::Serialize; use std::borrow::Cow; +use std::fmt::Display; +use std::path::PathBuf; use std::pin::Pin; use std::task::Context; use std::task::Poll; @@ -18,10 +24,49 @@ use tokio::sync::mpsc; /// with this content. const BASE_INSTRUCTIONS: &str = include_str!("../prompt.md"); +/// wraps environment context message in a tag for the model to parse more easily. +const ENVIRONMENT_CONTEXT_START: &str = "\n\n"; +const ENVIRONMENT_CONTEXT_END: &str = "\n\n"; + /// wraps user instructions message in a tag for the model to parse more easily. const USER_INSTRUCTIONS_START: &str = "\n\n"; const USER_INSTRUCTIONS_END: &str = "\n\n"; +#[derive(Debug, Clone)] +pub(crate) struct EnvironmentContext { + pub cwd: PathBuf, + pub git_info: Option, + pub approval_policy: AskForApproval, + pub sandbox_policy: SandboxPolicy, +} + +impl Display for EnvironmentContext { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!( + f, + "Current working directory: {}", + self.cwd.to_string_lossy() + )?; + writeln!(f, "Is directory a git repo: {}", self.git_info.is_some())?; + writeln!(f, "Approval policy: {}", self.approval_policy)?; + writeln!(f, "Sandbox policy: {}", self.sandbox_policy)?; + + let network_access = match self.sandbox_policy.clone() { + SandboxPolicy::DangerFullAccess => "enabled", + SandboxPolicy::ReadOnly => "restricted", + SandboxPolicy::WorkspaceWrite { network_access, .. } => { + if network_access { + "enabled" + } else { + "restricted" + } + } + }; + writeln!(f, "Network access: {network_access}")?; + Ok(()) + } +} + /// API request payload for a single model turn. #[derive(Default, Debug, Clone)] pub struct Prompt { @@ -33,6 +78,10 @@ pub struct Prompt { /// Whether to store response on server side (disable_response_storage = !store). pub store: bool, + /// A list of key-value pairs that will be added as a developer message + /// for the model to use + pub environment_context: Option, + /// Tools available to the model, including additional tools sourced from /// external MCP servers. pub tools: Vec, @@ -54,11 +103,37 @@ impl Prompt { Cow::Owned(sections.join("\n")) } - pub(crate) fn get_formatted_user_instructions(&self) -> Option { + fn get_formatted_user_instructions(&self) -> Option { self.user_instructions .as_ref() .map(|ui| format!("{USER_INSTRUCTIONS_START}{ui}{USER_INSTRUCTIONS_END}")) } + + fn get_formatted_environment_context(&self) -> Option { + self.environment_context + .as_ref() + .map(|ec| format!("{ENVIRONMENT_CONTEXT_START}{ec}{ENVIRONMENT_CONTEXT_END}")) + } + + pub(crate) fn get_formatted_input(&self) -> Vec { + let mut input_with_instructions = Vec::with_capacity(self.input.len() + 2); + if let Some(ec) = self.get_formatted_environment_context() { + input_with_instructions.push(ResponseItem::Message { + id: None, + role: "user".to_string(), + content: vec![ContentItem::InputText { text: ec }], + }); + } + if let Some(ui) = self.get_formatted_user_instructions() { + input_with_instructions.push(ResponseItem::Message { + id: None, + role: "user".to_string(), + content: vec![ContentItem::InputText { text: ui }], + }); + } + input_with_instructions.extend(self.input.clone()); + input_with_instructions + } } #[derive(Debug)] diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index a7ab664ee0..c85b1ce2b9 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -37,6 +37,7 @@ use crate::apply_patch::convert_apply_patch_to_protocol; use crate::apply_patch::get_writable_roots; use crate::apply_patch::{self}; use crate::client::ModelClient; +use crate::client_common::EnvironmentContext; use crate::client_common::Prompt; use crate::client_common::ResponseEvent; use crate::config::Config; @@ -51,6 +52,7 @@ use crate::exec::SandboxType; use crate::exec::StdoutStream; use crate::exec::process_exec_tool_call; use crate::exec_env::create_env; +use crate::git_info::collect_git_info; use crate::mcp_connection_manager::McpConnectionManager; use crate::mcp_tool_call::handle_mcp_tool_call; use crate::models::ContentItem; @@ -1224,6 +1226,12 @@ async fn run_turn( store: !sess.disable_response_storage, tools, base_instructions_override: sess.base_instructions.clone(), + environment_context: Some(EnvironmentContext { + cwd: sess.cwd.clone(), + git_info: collect_git_info(&sess.cwd).await, + approval_policy: sess.approval_policy, + sandbox_policy: sess.sandbox_policy.clone(), + }), }; let mut retries = 0; @@ -1449,6 +1457,7 @@ async fn run_compact_task( input: turn_input, user_instructions: None, store: !sess.disable_response_storage, + environment_context: None, tools: Vec::new(), base_instructions_override: Some(compact_instructions.clone()), }; diff --git a/codex-rs/core/src/git_info.rs b/codex-rs/core/src/git_info.rs index f5dc016e66..52d029f669 100644 --- a/codex-rs/core/src/git_info.rs +++ b/codex-rs/core/src/git_info.rs @@ -9,7 +9,7 @@ use tokio::time::timeout; /// Timeout for git commands to prevent freezing on large repositories const GIT_COMMAND_TIMEOUT: TokioDuration = TokioDuration::from_secs(5); -#[derive(Serialize, Deserialize, Clone)] +#[derive(Serialize, Deserialize, Clone, Debug)] pub struct GitInfo { /// Current commit hash (SHA) #[serde(skip_serializing_if = "Option::is_none")] diff --git a/codex-rs/core/src/protocol.rs b/codex-rs/core/src/protocol.rs index 9bf85ec49a..55000fb6d7 100644 --- a/codex-rs/core/src/protocol.rs +++ b/codex-rs/core/src/protocol.rs @@ -159,7 +159,8 @@ pub enum AskForApproval { } /// Determines execution restrictions for model shell commands. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Display)] +#[strum(serialize_all = "kebab-case")] #[serde(tag = "mode", rename_all = "kebab-case")] pub enum SandboxPolicy { /// No restrictions whatsoever. Use with caution. diff --git a/codex-rs/core/tests/client.rs b/codex-rs/core/tests/client.rs index f493020210..00f91a879e 100644 --- a/codex-rs/core/tests/client.rs +++ b/codex-rs/core/tests/client.rs @@ -1,3 +1,5 @@ +#![allow(clippy::expect_used)] +#![allow(clippy::unwrap_used)] use std::path::PathBuf; use chrono::Utc; @@ -32,6 +34,32 @@ fn sse_completed(id: &str) -> String { load_sse_fixture_with_id("tests/fixtures/completed_template.json", id) } +fn assert_message_role(request_body: &serde_json::Value, role: &str) { + assert_eq!(request_body["role"].as_str().unwrap(), role); +} + +fn assert_message_starts_with(request_body: &serde_json::Value, text: &str) { + let content = request_body["content"][0]["text"] + .as_str() + .expect("invalid message content"); + + assert!( + content.starts_with(text), + "expected message content '{content}' to start with '{text}'" + ); +} + +fn assert_message_ends_with(request_body: &serde_json::Value, text: &str) { + let content = request_body["content"][0]["text"] + .as_str() + .expect("invalid message content"); + + assert!( + content.ends_with(text), + "expected message content '{content}' to end with '{text}'" + ); +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn includes_session_id_and_model_headers_in_request() { #![allow(clippy::unwrap_used)] @@ -371,19 +399,12 @@ async fn includes_user_instructions_message_in_request() { .unwrap() .contains("be nice") ); - assert_eq!(request_body["input"][0]["role"], "user"); - assert!( - request_body["input"][0]["content"][0]["text"] - .as_str() - .unwrap() - .starts_with("\n\nbe nice") - ); - assert!( - request_body["input"][0]["content"][0]["text"] - .as_str() - .unwrap() - .ends_with("") - ); + assert_message_role(&request_body["input"][0], "user"); + assert_message_starts_with(&request_body["input"][0], "\n\n"); + assert_message_ends_with(&request_body["input"][0], ""); + assert_message_role(&request_body["input"][1], "user"); + assert_message_starts_with(&request_body["input"][1], "\n\n"); + assert_message_ends_with(&request_body["input"][1], ""); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] diff --git a/codex-rs/mcp-server/tests/send_message.rs b/codex-rs/mcp-server/tests/send_message.rs index fd3718e8f3..6e1389093c 100644 --- a/codex-rs/mcp-server/tests/send_message.rs +++ b/codex-rs/mcp-server/tests/send_message.rs @@ -99,7 +99,7 @@ async fn test_send_message_success() { response ); // wait for the server to hear the user message - sleep(Duration::from_secs(1)); + sleep(Duration::from_secs(10)); // Ensure the server and tempdir live until end of test drop(server); From dc468d563f1cdf3dfbeeb97dc29543b3ae9442e2 Mon Sep 17 00:00:00 2001 From: Dylan Date: Wed, 6 Aug 2025 08:05:17 -0700 Subject: [PATCH 4/6] [env] Remove git config for now (#1884) ## Summary Forgot to remove this in #1869 last night! Too much of a performance hit on the main thread. We can bring it back via an async thread on startup. --- codex-rs/core/src/client_common.rs | 3 --- codex-rs/core/src/codex.rs | 2 -- codex-rs/mcp-server/tests/send_message.rs | 2 +- 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/codex-rs/core/src/client_common.rs b/codex-rs/core/src/client_common.rs index 2ca060f4a4..b37b1e3f80 100644 --- a/codex-rs/core/src/client_common.rs +++ b/codex-rs/core/src/client_common.rs @@ -1,7 +1,6 @@ use crate::config_types::ReasoningEffort as ReasoningEffortConfig; use crate::config_types::ReasoningSummary as ReasoningSummaryConfig; use crate::error::Result; -use crate::git_info::GitInfo; use crate::model_family::ModelFamily; use crate::models::ContentItem; use crate::models::ResponseItem; @@ -35,7 +34,6 @@ const USER_INSTRUCTIONS_END: &str = "\n\n"; #[derive(Debug, Clone)] pub(crate) struct EnvironmentContext { pub cwd: PathBuf, - pub git_info: Option, pub approval_policy: AskForApproval, pub sandbox_policy: SandboxPolicy, } @@ -47,7 +45,6 @@ impl Display for EnvironmentContext { "Current working directory: {}", self.cwd.to_string_lossy() )?; - writeln!(f, "Is directory a git repo: {}", self.git_info.is_some())?; writeln!(f, "Approval policy: {}", self.approval_policy)?; writeln!(f, "Sandbox policy: {}", self.sandbox_policy)?; diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index c85b1ce2b9..98d13b4cd6 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -52,7 +52,6 @@ use crate::exec::SandboxType; use crate::exec::StdoutStream; use crate::exec::process_exec_tool_call; use crate::exec_env::create_env; -use crate::git_info::collect_git_info; use crate::mcp_connection_manager::McpConnectionManager; use crate::mcp_tool_call::handle_mcp_tool_call; use crate::models::ContentItem; @@ -1228,7 +1227,6 @@ async fn run_turn( base_instructions_override: sess.base_instructions.clone(), environment_context: Some(EnvironmentContext { cwd: sess.cwd.clone(), - git_info: collect_git_info(&sess.cwd).await, approval_policy: sess.approval_policy, sandbox_policy: sess.sandbox_policy.clone(), }), diff --git a/codex-rs/mcp-server/tests/send_message.rs b/codex-rs/mcp-server/tests/send_message.rs index 6e1389093c..f06c1587e3 100644 --- a/codex-rs/mcp-server/tests/send_message.rs +++ b/codex-rs/mcp-server/tests/send_message.rs @@ -99,7 +99,7 @@ async fn test_send_message_success() { response ); // wait for the server to hear the user message - sleep(Duration::from_secs(10)); + sleep(Duration::from_secs(5)); // Ensure the server and tempdir live until end of test drop(server); From ffe24991b7157ca27c78bb3ac4d422225bf7c031 Mon Sep 17 00:00:00 2001 From: Charlie Weems Date: Wed, 6 Aug 2025 09:10:23 -0700 Subject: [PATCH 5/6] Initial implementation of /init (#1822) Basic /init command that appends an instruction to create AGENTS.md to the conversation history. --- INIT.md | 40 +++++++++++++++++ codex-rs/tui/src/app.rs | 7 +++ codex-rs/tui/src/bottom_pane/chat_composer.rs | 44 +++++++++++++++++++ codex-rs/tui/src/bottom_pane/command_popup.rs | 35 +++++++++++++++ codex-rs/tui/src/chatwidget.rs | 10 +++++ codex-rs/tui/src/slash_command.rs | 2 + 6 files changed, 138 insertions(+) create mode 100644 INIT.md diff --git a/INIT.md b/INIT.md new file mode 100644 index 0000000000..b8fd3886b3 --- /dev/null +++ b/INIT.md @@ -0,0 +1,40 @@ +Generate a file named AGENTS.md that serves as a contributor guide for this repository. +Your goal is to produce a clear, concise, and well-structured document with descriptive headings and actionable explanations for each section. +Follow the outline below, but adapt as needed — add sections if relevant, and omit those that do not apply to this project. + +Document Requirements + +- Title the document "Repository Guidelines". +- Use Markdown headings (#, ##, etc.) for structure. +- Keep the document concise. 200-400 words is optimal. +- Keep explanations short, direct, and specific to this repository. +- Provide examples where helpful (commands, directory paths, naming patterns). +- Maintain a professional, instructional tone. + +Recommended Sections + +Project Structure & Module Organization + +- Outline the project structure, including where the source code, tests, and assets are located. + +Build, Test, and Development Commands + +- List key commands for building, testing, and running locally (e.g., npm test, make build). +- Briefly explain what each command does. + +Coding Style & Naming Conventions + +- Specify indentation rules, language-specific style preferences, and naming patterns. +- Include any formatting or linting tools used. + +Testing Guidelines + +- Identify testing frameworks and coverage requirements. +- State test naming conventions and how to run tests. + +Commit & Pull Request Guidelines + +- Summarize commit message conventions found in the project’s Git history. +- Outline pull request requirements (descriptions, linked issues, screenshots, etc.). + +(Optional) Add other sections if relevant, such as Security & Configuration Tips, Architecture Overview, or Agent-Specific Instructions. diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index eee2a61c2e..f1807da1c9 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -300,6 +300,13 @@ impl App<'_> { self.app_state = AppState::Chat { widget: new_widget }; self.app_event_tx.send(AppEvent::RequestRedraw); } + SlashCommand::Init => { + // Guard: do not run if a task is active. + if let AppState::Chat { widget } = &mut self.app_state { + const INIT_PROMPT: &str = include_str!("../../../INIT.md"); + widget.submit_text_message(INIT_PROMPT.to_string()); + } + } SlashCommand::Compact => { if let AppState::Chat { widget } = &mut self.app_state { widget.clear_token_usage(); diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index c9ad719771..f30b980da9 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -729,6 +729,7 @@ impl WidgetRef for &ChatComposer { #[cfg(test)] mod tests { + use crate::app_event::AppEvent; use crate::bottom_pane::AppEventSender; use crate::bottom_pane::ChatComposer; use crate::bottom_pane::InputResult; @@ -1004,6 +1005,49 @@ mod tests { } } + #[test] + fn slash_init_dispatches_command_and_does_not_submit_literal_text() { + use crossterm::event::KeyCode; + use crossterm::event::KeyEvent; + use crossterm::event::KeyModifiers; + use std::sync::mpsc::TryRecvError; + + let (tx, rx) = std::sync::mpsc::channel(); + let sender = AppEventSender::new(tx); + let mut composer = ChatComposer::new(true, sender, false); + + // Type the slash command. + for ch in [ + '/', 'i', 'n', 'i', 't', // "/init" + ] { + let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Char(ch), KeyModifiers::NONE)); + } + + // Press Enter to dispatch the selected command. + let (result, _needs_redraw) = + composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); + + // When a slash command is dispatched, the composer should not submit + // literal text and should clear its textarea. + match result { + InputResult::None => {} + InputResult::Submitted(text) => { + panic!("expected command dispatch, but composer submitted literal text: {text}") + } + } + assert!(composer.textarea.is_empty(), "composer should be cleared"); + + // Verify a DispatchCommand event for the "init" command was sent. + match rx.try_recv() { + Ok(AppEvent::DispatchCommand(cmd)) => { + assert_eq!(cmd.command(), "init"); + } + Ok(_other) => panic!("unexpected app event"), + Err(TryRecvError::Empty) => panic!("expected a DispatchCommand event for '/init'"), + Err(TryRecvError::Disconnected) => panic!("app event channel disconnected"), + } + } + #[test] fn test_multiple_pastes_submission() { use crossterm::event::KeyCode; diff --git a/codex-rs/tui/src/bottom_pane/command_popup.rs b/codex-rs/tui/src/bottom_pane/command_popup.rs index 364a8472dc..1027df1a67 100644 --- a/codex-rs/tui/src/bottom_pane/command_popup.rs +++ b/codex-rs/tui/src/bottom_pane/command_popup.rs @@ -188,3 +188,38 @@ impl WidgetRef for CommandPopup { table.render(area, buf); } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn filter_includes_init_when_typing_prefix() { + let mut popup = CommandPopup::new(); + // Simulate the composer line starting with '/in' so the popup filters + // matching commands by prefix. + popup.on_composer_text_change("/in".to_string()); + + // Access the filtered list via the selected command and ensure that + // one of the matches is the new "init" command. + let matches = popup.filtered_commands(); + assert!( + matches.iter().any(|cmd| cmd.command() == "init"), + "expected '/init' to appear among filtered commands" + ); + } + + #[test] + fn selecting_init_by_exact_match() { + let mut popup = CommandPopup::new(); + popup.on_composer_text_change("/init".to_string()); + + // When an exact match exists, the selected command should be that + // command by default. + let selected = popup.selected_command(); + match selected { + Some(cmd) => assert_eq!(cmd.command(), "init"), + None => panic!("expected a selected command for exact match"), + } + } +} diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 6d03be783b..64b65b3d11 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -575,6 +575,16 @@ impl ChatWidget<'_> { } } + /// Programmatically submit a user text message as if typed in the + /// composer. The text will be added to conversation history and sent to + /// the agent. + pub(crate) fn submit_text_message(&mut self, text: String) { + if text.is_empty() { + return; + } + self.submit_user_message(text.into()); + } + pub(crate) fn token_usage(&self) -> &TokenUsage { &self.token_usage } diff --git a/codex-rs/tui/src/slash_command.rs b/codex-rs/tui/src/slash_command.rs index 85dde7a113..daa663884b 100644 --- a/codex-rs/tui/src/slash_command.rs +++ b/codex-rs/tui/src/slash_command.rs @@ -13,6 +13,7 @@ pub enum SlashCommand { // DO NOT ALPHA-SORT! Enum order is presentation order in the popup, so // more frequently used commands should be listed first. New, + Init, Compact, Diff, Status, @@ -26,6 +27,7 @@ impl SlashCommand { pub fn description(self) -> &'static str { match self { SlashCommand::New => "Start a new chat", + SlashCommand::Init => "Create an AGENTS.md file with instructions for Codex.", SlashCommand::Compact => "Compact the chat history", SlashCommand::Quit => "Exit the application", SlashCommand::Diff => "Show git diff (including untracked files)", From bf506c2ee2caeaca8f569249e25d3ae611171481 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Wed, 6 Aug 2025 09:30:19 -0700 Subject: [PATCH 6/6] chore: rename INIT.md to prompt_for_init_command.md and move closer to usage --- INIT.md => codex-rs/tui/prompt_for_init_command.md | 0 codex-rs/tui/src/app.rs | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename INIT.md => codex-rs/tui/prompt_for_init_command.md (100%) diff --git a/INIT.md b/codex-rs/tui/prompt_for_init_command.md similarity index 100% rename from INIT.md rename to codex-rs/tui/prompt_for_init_command.md diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index f1807da1c9..47e20287bd 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -303,7 +303,7 @@ impl App<'_> { SlashCommand::Init => { // Guard: do not run if a task is active. if let AppState::Chat { widget } = &mut self.app_state { - const INIT_PROMPT: &str = include_str!("../../../INIT.md"); + const INIT_PROMPT: &str = include_str!("../prompt_for_init_command.md"); widget.submit_text_message(INIT_PROMPT.to_string()); } }