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/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..b37b1e3f80 100644
--- a/codex-rs/core/src/client_common.rs
+++ b/codex-rs/core/src/client_common.rs
@@ -2,13 +2,18 @@ use crate::config_types::ReasoningEffort as ReasoningEffortConfig;
use crate::config_types::ReasoningSummary as ReasoningSummaryConfig;
use crate::error::Result;
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 +23,47 @@ 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 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, "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 +75,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 +100,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..98d13b4cd6 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;
@@ -1224,6 +1225,11 @@ 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(),
+ approval_policy: sess.approval_policy,
+ sandbox_policy: sess.sandbox_policy.clone(),
+ }),
};
let mut retries = 0;
@@ -1449,6 +1455,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/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/mcp-server/tests/send_message.rs b/codex-rs/mcp-server/tests/send_message.rs
index fd4b210b0b..f06c1587e3 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.
@@ -99,13 +99,13 @@ 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(5));
// Ensure the server and tempdir live until end of test
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");
diff --git a/codex-rs/tui/prompt_for_init_command.md b/codex-rs/tui/prompt_for_init_command.md
new file mode 100644
index 0000000000..b8fd3886b3
--- /dev/null
+++ b/codex-rs/tui/prompt_for_init_command.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 da5410d392..47e20287bd 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!("../prompt_for_init_command.md");
+ widget.submit_text_message(INIT_PROMPT.to_string());
+ }
+ }
SlashCommand::Compact => {
if let AppState::Chat { widget } = &mut self.app_state {
widget.clear_token_usage();
@@ -330,6 +337,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/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 86bf765f2c..64b65b3d11 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);
@@ -568,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/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..daa663884b 100644
--- a/codex-rs/tui/src/slash_command.rs
+++ b/codex-rs/tui/src/slash_command.rs
@@ -13,8 +13,10 @@ 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,
Quit,
#[cfg(debug_assertions)]
TestApproval,
@@ -24,12 +26,12 @@ 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::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)",
+ SlashCommand::Status => "Show current session configuration and token usage",
#[cfg(debug_assertions)]
SlashCommand::TestApproval => "Test approval request",
}