diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 0b38db1eb4..aa1f72b4b4 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1040,6 +1040,7 @@ dependencies = [ "codex-rmcp-client", "codex-stdio-to-uds", "codex-tui", + "codex-tui2", "codex-windows-sandbox", "ctor 0.5.0", "libc", @@ -1637,6 +1638,18 @@ dependencies = [ "vt100", ] +[[package]] +name = "codex-tui2" +version = "0.0.0" +dependencies = [ + "anyhow", + "clap", + "codex-arg0", + "codex-common", + "codex-core", + "codex-tui", +] + [[package]] name = "codex-utils-cache" version = "0.0.0" diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 9f55f67ce3..bd62c72d5f 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -34,6 +34,7 @@ members = [ "stdio-to-uds", "otel", "tui", + "tui2", "utils/git", "utils/cache", "utils/image", @@ -88,6 +89,7 @@ codex-responses-api-proxy = { path = "responses-api-proxy" } codex-rmcp-client = { path = "rmcp-client" } codex-stdio-to-uds = { path = "stdio-to-uds" } codex-tui = { path = "tui" } +codex-tui2 = { path = "tui2" } codex-utils-cache = { path = "utils/cache" } codex-utils-image = { path = "utils/image" } codex-utils-json-to-toml = { path = "utils/json-to-toml" } diff --git a/codex-rs/cli/Cargo.toml b/codex-rs/cli/Cargo.toml index 6c80a12595..84e6e9acaf 100644 --- a/codex-rs/cli/Cargo.toml +++ b/codex-rs/cli/Cargo.toml @@ -36,6 +36,7 @@ codex-responses-api-proxy = { workspace = true } codex-rmcp-client = { workspace = true } codex-stdio-to-uds = { workspace = true } codex-tui = { workspace = true } +codex-tui2 = { workspace = true } ctor = { workspace = true } libc = { workspace = true } owo-colors = { workspace = true } diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index 6cff73e86d..c3788f83f4 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -25,6 +25,7 @@ use codex_responses_api_proxy::Args as ResponsesApiProxyArgs; use codex_tui::AppExitInfo; use codex_tui::Cli as TuiCli; use codex_tui::update_action::UpdateAction; +use codex_tui2 as tui2; use owo_colors::OwoColorize; use std::path::PathBuf; use supports_color::Stream; @@ -37,6 +38,11 @@ use crate::mcp_cmd::McpCli; use codex_core::config::Config; use codex_core::config::ConfigOverrides; +use codex_core::config::find_codex_home; +use codex_core::config::load_config_as_toml_with_cli_overrides; +use codex_core::features::Feature; +use codex_core::features::FeatureOverrides; +use codex_core::features::Features; use codex_core::features::is_known_feature_key; /// Codex CLI @@ -444,7 +450,7 @@ async fn cli_main(codex_linux_sandbox_exe: Option) -> anyhow::Result<() &mut interactive.config_overrides, root_config_overrides.clone(), ); - let exit_info = codex_tui::run_main(interactive, codex_linux_sandbox_exe).await?; + let exit_info = run_interactive_tui(interactive, codex_linux_sandbox_exe).await?; handle_app_exit(exit_info)?; } Some(Subcommand::Exec(mut exec_cli)) => { @@ -499,7 +505,7 @@ async fn cli_main(codex_linux_sandbox_exe: Option) -> anyhow::Result<() all, config_overrides, ); - let exit_info = codex_tui::run_main(interactive, codex_linux_sandbox_exe).await?; + let exit_info = run_interactive_tui(interactive, codex_linux_sandbox_exe).await?; handle_app_exit(exit_info)?; } Some(Subcommand::Login(mut login_cli)) => { @@ -650,6 +656,39 @@ fn prepend_config_flags( .splice(0..0, cli_config_overrides.raw_overrides); } +/// Run the interactive Codex TUI, dispatching to either the legacy implementation or the +/// experimental TUI v2 shim based on feature flags resolved from config. +async fn run_interactive_tui( + interactive: TuiCli, + codex_linux_sandbox_exe: Option, +) -> std::io::Result { + if is_tui2_enabled(&interactive).await? { + tui2::run_main(interactive, codex_linux_sandbox_exe).await + } else { + codex_tui::run_main(interactive, codex_linux_sandbox_exe).await + } +} + +/// Returns `Ok(true)` when the resolved configuration enables the `tui2` feature flag. +/// +/// This performs a lightweight config load (honoring the same precedence as the lower-level TUI +/// bootstrap: `$CODEX_HOME`, config.toml, profile, and CLI `-c` overrides) solely to decide which +/// TUI frontend to launch. The full configuration is still loaded later by the interactive TUI. +async fn is_tui2_enabled(cli: &TuiCli) -> std::io::Result { + let raw_overrides = cli.config_overrides.raw_overrides.clone(); + let overrides_cli = codex_common::CliConfigOverrides { raw_overrides }; + let cli_kv_overrides = overrides_cli + .parse_overrides() + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?; + + let codex_home = find_codex_home()?; + let config_toml = load_config_as_toml_with_cli_overrides(&codex_home, cli_kv_overrides).await?; + let config_profile = config_toml.get_config_profile(cli.config_profile.clone())?; + let overrides = FeatureOverrides::default(); + let features = Features::from_config(&config_toml, &config_profile, overrides); + Ok(features.enabled(Feature::Tui2)) +} + /// Build the final `TuiCli` for a `codex resume` invocation. fn finalize_resume_interactive( mut interactive: TuiCli, diff --git a/codex-rs/core/src/features.rs b/codex-rs/core/src/features.rs index d0b8e7e8da..d714f8e85e 100644 --- a/codex-rs/core/src/features.rs +++ b/codex-rs/core/src/features.rs @@ -62,6 +62,8 @@ pub enum Feature { Skills, /// Experimental shell snapshotting. ShellSnapshot, + /// Experimental TUI v2 (viewport) implementation. + Tui2, } impl Feature { @@ -367,4 +369,10 @@ pub const FEATURES: &[FeatureSpec] = &[ stage: Stage::Experimental, default_enabled: false, }, + FeatureSpec { + id: Feature::Tui2, + key: "tui2", + stage: Stage::Experimental, + default_enabled: false, + }, ]; diff --git a/codex-rs/tui2/Cargo.toml b/codex-rs/tui2/Cargo.toml new file mode 100644 index 0000000000..fececb1503 --- /dev/null +++ b/codex-rs/tui2/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "codex-tui2" +version.workspace = true +edition.workspace = true +license.workspace = true + +[lib] +name = "codex_tui2" +path = "src/lib.rs" + +[[bin]] +name = "codex-tui2" +path = "src/main.rs" + +[features] +# Keep feature surface aligned with codex-tui while tui2 delegates to it. +vt100-tests = [] +debug-logs = [] + +[lints] +workspace = true + +[dependencies] +anyhow = { workspace = true } +clap = { workspace = true, features = ["derive"] } +codex-arg0 = { workspace = true } +codex-common = { workspace = true } +codex-core = { workspace = true } +codex-tui = { workspace = true } diff --git a/codex-rs/tui2/src/lib.rs b/codex-rs/tui2/src/lib.rs new file mode 100644 index 0000000000..502efa76a0 --- /dev/null +++ b/codex-rs/tui2/src/lib.rs @@ -0,0 +1,24 @@ +#![deny(clippy::print_stdout, clippy::print_stderr)] +#![deny(clippy::disallowed_methods)] + +use std::path::PathBuf; + +pub use codex_tui::AppExitInfo; +pub use codex_tui::Cli; +pub use codex_tui::update_action; + +/// Entry point for the experimental TUI v2 crate. +/// +/// Currently this is a thin shim that delegates to the existing `codex-tui` +/// implementation so behavior and rendering remain identical while the new +/// viewport is developed behind a feature toggle. +pub async fn run_main( + cli: Cli, + codex_linux_sandbox_exe: Option, +) -> std::io::Result { + #[allow(clippy::print_stdout)] // for now + { + println!("Note: You are running the experimental TUI v2 implementation."); + } + codex_tui::run_main(cli, codex_linux_sandbox_exe).await +} diff --git a/codex-rs/tui2/src/main.rs b/codex-rs/tui2/src/main.rs new file mode 100644 index 0000000000..b50d994d80 --- /dev/null +++ b/codex-rs/tui2/src/main.rs @@ -0,0 +1,32 @@ +use clap::Parser; +use codex_arg0::arg0_dispatch_or_else; +use codex_common::CliConfigOverrides; +use codex_core::protocol::FinalOutput; +use codex_tui2::Cli; +use codex_tui2::run_main; + +#[derive(Parser, Debug)] +struct TopCli { + #[clap(flatten)] + config_overrides: CliConfigOverrides, + + #[clap(flatten)] + inner: Cli, +} + +fn main() -> anyhow::Result<()> { + arg0_dispatch_or_else(|codex_linux_sandbox_exe| async move { + let top_cli = TopCli::parse(); + let mut inner = top_cli.inner; + inner + .config_overrides + .raw_overrides + .splice(0..0, top_cli.config_overrides.raw_overrides); + let exit_info = run_main(inner, codex_linux_sandbox_exe).await?; + let token_usage = exit_info.token_usage; + if !token_usage.is_zero() { + println!("{}", FinalOutput::from(token_usage)); + } + Ok(()) + }) +} diff --git a/docs/config.md b/docs/config.md index 08ff2aa349..4e78da7ac6 100644 --- a/docs/config.md +++ b/docs/config.md @@ -39,16 +39,17 @@ web_search_request = true # allow the model to request web searches Supported features: -| Key | Default | Stage | Description | -| ----------------------------------------- | :-----: | ------------ | ---------------------------------------------------- | -| `unified_exec` | false | Experimental | Use the unified PTY-backed exec tool | -| `rmcp_client` | false | Experimental | Enable oauth support for streamable HTTP MCP servers | -| `apply_patch_freeform` | false | Beta | Include the freeform `apply_patch` tool | -| `view_image_tool` | true | Stable | Include the `view_image` tool | -| `web_search_request` | false | Stable | Allow the model to issue web searches | -| `experimental_sandbox_command_assessment` | false | Experimental | Enable model-based sandbox risk assessment | -| `ghost_commit` | false | Experimental | Create a ghost commit each turn | -| `enable_experimental_windows_sandbox` | false | Experimental | Use the Windows restricted-token sandbox | +| Key | Default | Stage | Description | +| ----------------------------------------- | :-----: | ------------ | ----------------------------------------------------- | +| `unified_exec` | false | Experimental | Use the unified PTY-backed exec tool | +| `rmcp_client` | false | Experimental | Enable oauth support for streamable HTTP MCP servers | +| `apply_patch_freeform` | false | Beta | Include the freeform `apply_patch` tool | +| `view_image_tool` | true | Stable | Include the `view_image` tool | +| `web_search_request` | false | Stable | Allow the model to issue web searches | +| `experimental_sandbox_command_assessment` | false | Experimental | Enable model-based sandbox risk assessment | +| `ghost_commit` | false | Experimental | Create a ghost commit each turn | +| `enable_experimental_windows_sandbox` | false | Experimental | Use the Windows restricted-token sandbox | +| `tui2` | false | Experimental | Use the experimental TUI v2 (viewport) implementation | Notes: