From 345a38502d12f65578f6ca74fd87a3e93f09a081 Mon Sep 17 00:00:00 2001 From: Reilly Wood <163153147+rgwood-dd@users.noreply.github.com> Date: Fri, 6 Jun 2025 16:29:37 -0700 Subject: [PATCH 1/3] codex-rs: Rename `/clear` to `/new`, make it start an entirely new chat (#1264) I noticed that `/clear` wasn't fully clearing chat history; it would clear the chat history widgets _in the UI_, but the LLM still had access to information from previous messages. This PR renames `/clear` to `/new` for clarity as per Michael's suggestion, resetting `app_state` to a fresh `ChatWidget`. --- codex-rs/tui/src/app.rs | 30 ++++++++++++++----- codex-rs/tui/src/chatwidget.rs | 5 ---- .../tui/src/conversation_history_widget.rs | 6 ---- codex-rs/tui/src/slash_command.rs | 4 +-- 4 files changed, 24 insertions(+), 21 deletions(-) diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 8f35a3507f..ff61b5c941 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -40,6 +40,9 @@ pub(crate) struct App<'a> { app_event_rx: Receiver, app_state: AppState<'a>, + /// Config is stored here so we can recreate ChatWidgets as needed. + config: Config, + /// Stored parameters needed to instantiate the ChatWidget later, e.g., /// after dismissing the Git-repo warning. chat_args: Option, @@ -122,7 +125,7 @@ impl<'a> App<'a> { screen: LoginScreen::new(app_event_tx.clone(), config.codex_home.clone()), }, Some(ChatWidgetArgs { - config, + config: config.clone(), initial_prompt, initial_images, }), @@ -133,14 +136,18 @@ impl<'a> App<'a> { screen: GitWarningScreen::new(), }, Some(ChatWidgetArgs { - config, + config: config.clone(), initial_prompt, initial_images, }), ) } else { - let chat_widget = - ChatWidget::new(config, app_event_tx.clone(), initial_prompt, initial_images); + let chat_widget = ChatWidget::new( + config.clone(), + app_event_tx.clone(), + initial_prompt, + initial_images, + ); ( AppState::Chat { widget: Box::new(chat_widget), @@ -153,6 +160,7 @@ impl<'a> App<'a> { app_event_tx, app_event_rx, app_state, + config, chat_args, } } @@ -224,10 +232,16 @@ impl<'a> App<'a> { AppState::Login { .. } | AppState::GitWarning { .. } => {} }, AppEvent::DispatchCommand(command) => match command { - SlashCommand::Clear => match &mut self.app_state { - AppState::Chat { widget } => widget.clear_conversation_history(), - AppState::Login { .. } | AppState::GitWarning { .. } => {} - }, + SlashCommand::New => { + let new_widget = Box::new(ChatWidget::new( + self.config.clone(), + self.app_event_tx.clone(), + None, + Vec::new(), + )); + self.app_state = AppState::Chat { widget: new_widget }; + self.app_event_tx.send(AppEvent::Redraw); + } SlashCommand::ToggleMouseMode => { if let Err(e) = mouse_capture.toggle() { tracing::error!("Failed to toggle mouse mode: {e}"); diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 63f3bc727a..bd5197c73b 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -207,11 +207,6 @@ impl ChatWidget<'_> { self.conversation_history.scroll_to_bottom(); } - pub(crate) fn clear_conversation_history(&mut self) { - self.conversation_history.clear(); - self.request_redraw(); - } - pub(crate) fn handle_codex_event(&mut self, event: Event) { let Event { id, msg } = event; match msg { diff --git a/codex-rs/tui/src/conversation_history_widget.rs b/codex-rs/tui/src/conversation_history_widget.rs index a23e00d776..714ac074a7 100644 --- a/codex-rs/tui/src/conversation_history_widget.rs +++ b/codex-rs/tui/src/conversation_history_widget.rs @@ -245,12 +245,6 @@ impl ConversationHistoryWidget { }); } - /// Remove all history entries and reset scrolling. - pub fn clear(&mut self) { - self.entries.clear(); - self.scroll_position = usize::MAX; - } - pub fn record_completed_exec_command( &mut self, call_id: String, diff --git a/codex-rs/tui/src/slash_command.rs b/codex-rs/tui/src/slash_command.rs index cd6da4dac9..bfc02ceb13 100644 --- a/codex-rs/tui/src/slash_command.rs +++ b/codex-rs/tui/src/slash_command.rs @@ -12,7 +12,7 @@ use strum_macros::IntoStaticStr; )] #[strum(serialize_all = "kebab-case")] pub enum SlashCommand { - Clear, + New, ToggleMouseMode, Quit, } @@ -21,7 +21,7 @@ impl SlashCommand { /// User-visible description shown in the popup. pub fn description(self) -> &'static str { match self { - SlashCommand::Clear => "Clear the chat history.", + SlashCommand::New => "Start a new chat.", SlashCommand::ToggleMouseMode => { "Toggle mouse mode (enable for scrolling, disable for text selection)" } From b73426c1c40187ca13c74c03912a681072c2884f Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Fri, 6 Jun 2025 18:32:10 -0700 Subject: [PATCH 2/3] docs: update codex-rs/README.md to list new features in the Rust CLI (#1267) Let users know about what the Rust CLI supports that the TypeScript CLI doesn't! --- codex-rs/README.md | 40 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/codex-rs/README.md b/codex-rs/README.md index 7126d60b4f..caa21639fb 100644 --- a/codex-rs/README.md +++ b/codex-rs/README.md @@ -13,20 +13,52 @@ codex You can also download a platform-specific release directly from our [GitHub Releases](https://github.com/openai/codex/releases). -## Config +## What's new in the Rust CLI -Codex supports a rich set of configuration options. See [`config.md`](./config.md) for details. +While we are [working to close the gap between the TypeScript and Rust implementations of Codex CLI](https://github.com/openai/codex/issues/1262), note that the Rust CLI has a number of features that the TypeScript CLI does not! -## Model Context Protocol Support +### Config + +Codex supports a rich set of configuration options. Note that the Rust CLI uses `config.toml` instead of `config.json`. See [`config.md`](./config.md) for details. + +### Model Context Protocol Support Codex CLI functions as an MCP client that can connect to MCP servers on startup. See the [`mcp_servers`](./config.md#mcp_servers) section in the configuration documentation for details. -It is still experimental, but you can also launch Codex as an MCP _server_ by running `codex mcp`. Using the [`@modelcontextprotocol/inspector`](https://github.com/modelcontextprotocol/inspector) is +It is still experimental, but you can also launch Codex as an MCP _server_ by running `codex mcp`. Use the [`@modelcontextprotocol/inspector`](https://github.com/modelcontextprotocol/inspector) to try it out: ```shell npx @modelcontextprotocol/inspector codex mcp ``` +### Notifications + +You can enable notifications by configuring a script that is run whenever the agent finishes a turn. The [notify documentation](./config.md#notify) includes a detailed example that explains how to get desktop notifications via [terminal-notifier](https://github.com/julienXX/terminal-notifier) on macOS. + +### `codex exec` to run Codex programmatially/non-interactively + +To run Codex non-interactively, run `codex exec PROMPT` (you can also pass the prompt via `stdin`) and Codex will work on your task until it decides that it is done and exits. Output is printed to the terminal directly. You can set the `RUST_LOG` environment variable to see more about what's going on. + +### `--cd`/`-C` flag + +Sometimes it is not convenient to `cd` to the directory you want Codex to use as the "working root" before running Codex. Fortunately, `codex` supports a `--cd` option so you can specify whatever folder you want. You can confirm that Codex is honoring `--cd` by double-checking the **workdir** it reports in the TUI at the start of a new session. + +### Experimenting with the Codex Sandbox + +To test to see what happens when a command is run under the sandbox provided by Codex, we provide the following subcommands in Codex CLI: + +``` +# macOS +codex debug seatbelt [-s SANDBOX_PERMISSION]... [COMMAND]... + +# Linux +codex debug landlock [-s SANDBOX_PERMISSION]... [COMMAND]... +``` + +You can experiment with different values of `-s` to see what permissions the `COMMAND` needs to execute successfully. + +Note that the exact API for the `-s` flag is currently in flux. See https://github.com/openai/codex/issues/1248 for details. + ## Code Organization This folder is the root of a Cargo workspace. It contains quite a bit of experimental code, but here are the key crates: From 5d8ec161e8e1c8756d8d4d508ae9e9a04e332067 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Mon, 9 Jun 2025 16:12:43 -0400 Subject: [PATCH 3/3] feat: list-models subcommand for full CLI --- codex-rs/Cargo.lock | 1 + codex-rs/cli/Cargo.toml | 2 +- codex-rs/cli/src/list_models.rs | 55 +++++++++++++++++++++++ codex-rs/cli/src/main.rs | 12 +++++ codex-rs/common/Cargo.toml | 7 +++ codex-rs/common/src/lib.rs | 10 +++++ codex-rs/common/src/model_list.rs | 74 +++++++++++++++++++++++++++++++ 7 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 codex-rs/cli/src/list_models.rs create mode 100644 codex-rs/common/src/model_list.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 66b4fa3e00..87854aad32 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -600,6 +600,7 @@ version = "0.0.0" dependencies = [ "clap", "codex-core", + "reqwest", "serde", "toml", ] diff --git a/codex-rs/cli/Cargo.toml b/codex-rs/cli/Cargo.toml index 78fd08a7d3..663a4228e9 100644 --- a/codex-rs/cli/Cargo.toml +++ b/codex-rs/cli/Cargo.toml @@ -18,7 +18,7 @@ workspace = true anyhow = "1" clap = { version = "4", features = ["derive"] } codex-core = { path = "../core" } -codex-common = { path = "../common", features = ["cli"] } +codex-common = { path = "../common", features = ["cli", "model-list"] } codex-exec = { path = "../exec" } codex-login = { path = "../login" } codex-linux-sandbox = { path = "../linux-sandbox" } diff --git a/codex-rs/cli/src/list_models.rs b/codex-rs/cli/src/list_models.rs new file mode 100644 index 0000000000..3d3b2789d6 --- /dev/null +++ b/codex-rs/cli/src/list_models.rs @@ -0,0 +1,55 @@ +use clap::Parser; + +use codex_common::CliConfigOverrides; +use codex_core::config::Config; +use codex_core::config::ConfigOverrides; + +/// Print the list of models available for the configured (or overridden) +/// provider. +#[derive(Debug, Parser)] +pub struct ListModelsCli { + /// Optional provider override. When set this value is used instead of the + /// `model_provider_id` configured in `~/.codex/config.toml`. + #[clap(long)] + pub provider: Option, + + /// Arbitrary `-c key=value` overrides that apply **in addition** to the + /// `--provider` flag. + #[clap(flatten)] + pub config_overrides: CliConfigOverrides, +} + +impl ListModelsCli { + pub async fn run(self) -> anyhow::Result<()> { + // Compose strongly-typed overrides. The provider flag, if specified, + // is translated into the corresponding field inside `ConfigOverrides`. + let overrides = ConfigOverrides { + model: None, + config_profile: None, + approval_policy: None, + sandbox_policy: None, + cwd: None, + model_provider: self.provider.clone(), + codex_linux_sandbox_exe: None, + }; + + // Parse the raw `-c` overrides early so we can bail with a useful + // error message if the user supplied an invalid value. + let cli_kv_overrides = self + .config_overrides + .parse_overrides() + .map_err(anyhow::Error::msg)?; + + // Load the merged configuration. + let cfg = Config::load_with_cli_overrides(cli_kv_overrides, overrides)?; + + // Retrieve the model list. + let models = codex_common::fetch_available_models(cfg.model_provider).await?; + + for m in models { + println!("{m}"); + } + + Ok(()) + } +} diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index 0e9ba01827..5ec2d27da9 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -9,6 +9,7 @@ use codex_tui::Cli as TuiCli; use std::path::PathBuf; use crate::proto::ProtoCli; +mod list_models; /// Codex CLI /// @@ -43,6 +44,10 @@ enum Subcommand { /// Experimental: run Codex as an MCP server. Mcp, + /// List models for the configured or specified provider. + #[clap(name = "list-models", visible_alias = "lm")] + ListModels(crate::list_models::ListModelsCli), + /// Run the Protocol stream via stdin/stdout #[clap(visible_alias = "p")] Proto(ProtoCli), @@ -121,6 +126,13 @@ async fn cli_main(codex_linux_sandbox_exe: Option) -> anyhow::Result<() .await?; } }, + Some(Subcommand::ListModels(list_cli)) => { + // Combine root-level overrides with subcommand-specific ones so + // that the latter take precedence. + let mut list_cli = list_cli; + prepend_config_flags(&mut list_cli.config_overrides, cli.config_overrides); + list_cli.run().await?; + } } Ok(()) diff --git a/codex-rs/common/Cargo.toml b/codex-rs/common/Cargo.toml index b4b658dabf..390c20be78 100644 --- a/codex-rs/common/Cargo.toml +++ b/codex-rs/common/Cargo.toml @@ -11,8 +11,15 @@ clap = { version = "4", features = ["derive", "wrap_help"], optional = true } codex-core = { path = "../core" } toml = { version = "0.8", optional = true } serde = { version = "1", optional = true } +reqwest = { version = "0.12", features = ["json"], optional = true } [features] # Separate feature so that `clap` is not a mandatory dependency. cli = ["clap", "toml", "serde"] elapsed = [] + +# Helper functionality for querying the list of available models from a model +# provider. This is intentionally behind a separate opt-in feature so that +# downstream crates that do not need it avoid pulling in the additional heavy +# dependencies (`reqwest`, etc.). +model-list = ["reqwest"] diff --git a/codex-rs/common/src/lib.rs b/codex-rs/common/src/lib.rs index c2283640cb..2bcd505fc3 100644 --- a/codex-rs/common/src/lib.rs +++ b/codex-rs/common/src/lib.rs @@ -14,3 +14,13 @@ mod config_override; #[cfg(feature = "cli")] pub use config_override::CliConfigOverrides; + +// ------------------------------------------------------------------------- +// Optional helpers for querying the list of available models. +// ------------------------------------------------------------------------- + +#[cfg(feature = "model-list")] +mod model_list; + +#[cfg(feature = "model-list")] +pub use model_list::fetch_available_models; diff --git a/codex-rs/common/src/model_list.rs b/codex-rs/common/src/model_list.rs new file mode 100644 index 0000000000..870cfd82b2 --- /dev/null +++ b/codex-rs/common/src/model_list.rs @@ -0,0 +1,74 @@ +//! Helper for fetching the list of models that are available for a given +//! [`ModelProviderInfo`] instance. +//! +//! The implementation is intentionally lightweight and only covers the subset +//! of the OpenAI-compatible REST API that is required to discover available +//! model *identifiers*. At the time of writing all providers supported by +//! Codex expose a `GET /models` endpoint that returns a JSON payload in the +//! following canonical form: +//! +//! ```json +//! { +//! "object": "list", +//! "data": [ +//! { "id": "o3", "object": "model" }, +//! { "id": "o4-mini", "object": "model" } +//! ] +//! } +//! ``` +//! +//! We purposefully parse *only* the `id` fields that callers care about and +//! ignore any additional metadata so that the function keeps working even if +//! upstream providers add new attributes. + +use codex_core::error::{CodexErr, Result}; +use codex_core::ModelProviderInfo; +use reqwest::StatusCode; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +struct ModelsResponse { + data: Vec, +} + +#[derive(Debug, Deserialize)] +struct ModelId { + id: String, +} + +/// Fetch the list of available model identifiers from the given provider. +/// +/// The caller must ensure that the provider's API key can be resolved via +/// [`ModelProviderInfo::api_key`] – if this fails the function returns a +/// [`CodexErr::EnvVar`]. Any network or JSON parsing failures are forwarded +/// to the caller. +#[allow(clippy::needless_pass_by_value)] +pub async fn fetch_available_models(provider: ModelProviderInfo) -> Result> { + let api_key = provider.api_key()?; + + let base_url = provider.base_url.trim_end_matches('/'); + let url = format!("{base_url}/models"); + + // Build the request. For providers that require authentication we send + // the token via the standard Bearer mechanism. Providers like Ollama do + // not require a token – in that case we just omit the header. + let client = reqwest::Client::new(); + let mut req = client.get(&url); + if let Some(token) = api_key { + req = req.bearer_auth(token); + } + + + + let resp = req.send().await?; + + match resp.status() { + StatusCode::OK => { + let json: ModelsResponse = resp.json().await?; + let mut models: Vec = json.data.into_iter().map(|m| m.id).collect(); + models.sort(); + Ok(models) + } + _ => Err(CodexErr::Reqwest(resp.error_for_status().unwrap_err())), + } +}