feat(tui2): add feature-flagged tui2 frontend (#7793)

Introduce a new codex-tui2 crate that re-exports the existing
interactive TUI surface and delegates run_main directly to codex-tui.
This keeps behavior identical while giving tui2 its own crate for future
viewport work.

Wire the codex CLI to select the frontend via the tui2 feature flag.
When the merged CLI overrides include features.tui2=true (e.g. via
--enable tui2), interactive runs are routed through
codex_tui2::run_main; otherwise they continue to use the original
codex_tui::run_main.

Register Feature::Tui2 in the core feature registry and add the tui2
crate and dependency entries so the new frontend builds alongside the
existing TUI.

This is a stub that only wires up the feature flag for this.

<img width="619" height="364" alt="image"
src="https://github.com/user-attachments/assets/4893f030-932f-471e-a443-63fe6b5d8ed9"
/>
This commit is contained in:
Josh McKinney
2025-12-09 16:23:53 -08:00
committed by GitHub
parent 225a5f7ffb
commit 0c8828c5e2
9 changed files with 161 additions and 12 deletions

13
codex-rs/Cargo.lock generated
View File

@@ -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"

View File

@@ -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" }

View File

@@ -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 }

View File

@@ -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<PathBuf>) -> 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<PathBuf>) -> 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<PathBuf>,
) -> std::io::Result<AppExitInfo> {
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<bool> {
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,

View File

@@ -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,
},
];

29
codex-rs/tui2/Cargo.toml Normal file
View File

@@ -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 }

24
codex-rs/tui2/src/lib.rs Normal file
View File

@@ -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<PathBuf>,
) -> std::io::Result<AppExitInfo> {
#[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
}

32
codex-rs/tui2/src/main.rs Normal file
View File

@@ -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(())
})
}

View File

@@ -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: