From d642b07fcccdd8554316c61b88743378db43252c Mon Sep 17 00:00:00 2001 From: ae Date: Tue, 5 Aug 2025 23:57:52 -0700 Subject: [PATCH] [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 00000000..39d52473 --- /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 3d498a8e..38f3832b 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 0a2a141e..b7b3c27d 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 393ef4ab..6b03ed78 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 1d153add..76985518 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 da5410d3..eee2a61c 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 86bf765f..6d03be78 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 332d90a6..5b7d9246 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 d82a1660..85dde7a1 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", }