mirror of
https://github.com/openai/codex.git
synced 2026-05-02 02:17:22 +00:00
Move TUI on top of app server (parallel code) (#14717)
This PR replicates the `tui` code directory and creates a temporary parallel `tui_app_server` directory. It also implements a new feature flag `tui_app_server` to select between the two tui implementations. Once the new app-server-based TUI is stabilized, we'll delete the old `tui` directory and feature flag.
This commit is contained in:
@@ -74,6 +74,9 @@ struct MultitoolCli {
|
||||
#[clap(flatten)]
|
||||
pub feature_toggles: FeatureToggles,
|
||||
|
||||
#[clap(flatten)]
|
||||
remote: InteractiveRemoteOptions,
|
||||
|
||||
#[clap(flatten)]
|
||||
interactive: TuiCli,
|
||||
|
||||
@@ -204,6 +207,9 @@ struct ResumeCommand {
|
||||
#[arg(long = "all", default_value_t = false)]
|
||||
all: bool,
|
||||
|
||||
#[clap(flatten)]
|
||||
remote: InteractiveRemoteOptions,
|
||||
|
||||
#[clap(flatten)]
|
||||
config_overrides: TuiCli,
|
||||
}
|
||||
@@ -223,6 +229,9 @@ struct ForkCommand {
|
||||
#[arg(long = "all", default_value_t = false)]
|
||||
all: bool,
|
||||
|
||||
#[clap(flatten)]
|
||||
remote: InteractiveRemoteOptions,
|
||||
|
||||
#[clap(flatten)]
|
||||
config_overrides: TuiCli,
|
||||
}
|
||||
@@ -494,6 +503,15 @@ struct FeatureToggles {
|
||||
disable: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Parser, Clone)]
|
||||
struct InteractiveRemoteOptions {
|
||||
/// Connect the app-server-backed TUI to a remote app server websocket endpoint.
|
||||
///
|
||||
/// Accepted forms: `ws://host:port` or `wss://host:port`.
|
||||
#[arg(long = "remote", value_name = "ADDR")]
|
||||
remote: Option<String>,
|
||||
}
|
||||
|
||||
impl FeatureToggles {
|
||||
fn to_overrides(&self) -> anyhow::Result<Vec<String>> {
|
||||
let mut v = Vec::new();
|
||||
@@ -561,6 +579,7 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
||||
let MultitoolCli {
|
||||
config_overrides: mut root_config_overrides,
|
||||
feature_toggles,
|
||||
remote,
|
||||
mut interactive,
|
||||
subcommand,
|
||||
} = MultitoolCli::parse();
|
||||
@@ -568,6 +587,7 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
||||
// Fold --enable/--disable into config overrides so they flow to all subcommands.
|
||||
let toggle_overrides = feature_toggles.to_overrides()?;
|
||||
root_config_overrides.raw_overrides.extend(toggle_overrides);
|
||||
let root_remote = remote.remote;
|
||||
|
||||
match subcommand {
|
||||
None => {
|
||||
@@ -575,10 +595,12 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
||||
&mut interactive.config_overrides,
|
||||
root_config_overrides.clone(),
|
||||
);
|
||||
let exit_info = run_interactive_tui(interactive, arg0_paths.clone()).await?;
|
||||
let exit_info =
|
||||
run_interactive_tui(interactive, root_remote.clone(), arg0_paths.clone()).await?;
|
||||
handle_app_exit(exit_info)?;
|
||||
}
|
||||
Some(Subcommand::Exec(mut exec_cli)) => {
|
||||
reject_remote_mode_for_subcommand(root_remote.as_deref(), "exec")?;
|
||||
prepend_config_flags(
|
||||
&mut exec_cli.config_overrides,
|
||||
root_config_overrides.clone(),
|
||||
@@ -586,6 +608,7 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
||||
codex_exec::run_main(exec_cli, arg0_paths.clone()).await?;
|
||||
}
|
||||
Some(Subcommand::Review(review_args)) => {
|
||||
reject_remote_mode_for_subcommand(root_remote.as_deref(), "review")?;
|
||||
let mut exec_cli = ExecCli::try_parse_from(["codex", "exec"])?;
|
||||
exec_cli.command = Some(ExecCommand::Review(review_args));
|
||||
prepend_config_flags(
|
||||
@@ -595,15 +618,18 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
||||
codex_exec::run_main(exec_cli, arg0_paths.clone()).await?;
|
||||
}
|
||||
Some(Subcommand::McpServer) => {
|
||||
reject_remote_mode_for_subcommand(root_remote.as_deref(), "mcp-server")?;
|
||||
codex_mcp_server::run_main(arg0_paths.clone(), root_config_overrides).await?;
|
||||
}
|
||||
Some(Subcommand::Mcp(mut mcp_cli)) => {
|
||||
reject_remote_mode_for_subcommand(root_remote.as_deref(), "mcp")?;
|
||||
// Propagate any root-level config overrides (e.g. `-c key=value`).
|
||||
prepend_config_flags(&mut mcp_cli.config_overrides, root_config_overrides.clone());
|
||||
mcp_cli.run().await?;
|
||||
}
|
||||
Some(Subcommand::AppServer(app_server_cli)) => match app_server_cli.subcommand {
|
||||
None => {
|
||||
reject_remote_mode_for_subcommand(root_remote.as_deref(), "app-server")?;
|
||||
let transport = app_server_cli.listen;
|
||||
codex_app_server::run_main_with_transport(
|
||||
arg0_paths.clone(),
|
||||
@@ -615,6 +641,10 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
||||
.await?;
|
||||
}
|
||||
Some(AppServerSubcommand::GenerateTs(gen_cli)) => {
|
||||
reject_remote_mode_for_subcommand(
|
||||
root_remote.as_deref(),
|
||||
"app-server generate-ts",
|
||||
)?;
|
||||
let options = codex_app_server_protocol::GenerateTsOptions {
|
||||
experimental_api: gen_cli.experimental,
|
||||
..Default::default()
|
||||
@@ -626,6 +656,10 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
||||
)?;
|
||||
}
|
||||
Some(AppServerSubcommand::GenerateJsonSchema(gen_cli)) => {
|
||||
reject_remote_mode_for_subcommand(
|
||||
root_remote.as_deref(),
|
||||
"app-server generate-json-schema",
|
||||
)?;
|
||||
codex_app_server_protocol::generate_json_with_experimental(
|
||||
&gen_cli.out_dir,
|
||||
gen_cli.experimental,
|
||||
@@ -634,12 +668,14 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
||||
},
|
||||
#[cfg(target_os = "macos")]
|
||||
Some(Subcommand::App(app_cli)) => {
|
||||
reject_remote_mode_for_subcommand(root_remote.as_deref(), "app")?;
|
||||
app_cmd::run_app(app_cli).await?;
|
||||
}
|
||||
Some(Subcommand::Resume(ResumeCommand {
|
||||
session_id,
|
||||
last,
|
||||
all,
|
||||
remote,
|
||||
config_overrides,
|
||||
})) => {
|
||||
interactive = finalize_resume_interactive(
|
||||
@@ -650,13 +686,19 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
||||
all,
|
||||
config_overrides,
|
||||
);
|
||||
let exit_info = run_interactive_tui(interactive, arg0_paths.clone()).await?;
|
||||
let exit_info = run_interactive_tui(
|
||||
interactive,
|
||||
remote.remote.or(root_remote.clone()),
|
||||
arg0_paths.clone(),
|
||||
)
|
||||
.await?;
|
||||
handle_app_exit(exit_info)?;
|
||||
}
|
||||
Some(Subcommand::Fork(ForkCommand {
|
||||
session_id,
|
||||
last,
|
||||
all,
|
||||
remote,
|
||||
config_overrides,
|
||||
})) => {
|
||||
interactive = finalize_fork_interactive(
|
||||
@@ -667,10 +709,16 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
||||
all,
|
||||
config_overrides,
|
||||
);
|
||||
let exit_info = run_interactive_tui(interactive, arg0_paths.clone()).await?;
|
||||
let exit_info = run_interactive_tui(
|
||||
interactive,
|
||||
remote.remote.or(root_remote.clone()),
|
||||
arg0_paths.clone(),
|
||||
)
|
||||
.await?;
|
||||
handle_app_exit(exit_info)?;
|
||||
}
|
||||
Some(Subcommand::Login(mut login_cli)) => {
|
||||
reject_remote_mode_for_subcommand(root_remote.as_deref(), "login")?;
|
||||
prepend_config_flags(
|
||||
&mut login_cli.config_overrides,
|
||||
root_config_overrides.clone(),
|
||||
@@ -702,6 +750,7 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
||||
}
|
||||
}
|
||||
Some(Subcommand::Logout(mut logout_cli)) => {
|
||||
reject_remote_mode_for_subcommand(root_remote.as_deref(), "logout")?;
|
||||
prepend_config_flags(
|
||||
&mut logout_cli.config_overrides,
|
||||
root_config_overrides.clone(),
|
||||
@@ -709,9 +758,11 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
||||
run_logout(logout_cli.config_overrides).await;
|
||||
}
|
||||
Some(Subcommand::Completion(completion_cli)) => {
|
||||
reject_remote_mode_for_subcommand(root_remote.as_deref(), "completion")?;
|
||||
print_completion(completion_cli);
|
||||
}
|
||||
Some(Subcommand::Cloud(mut cloud_cli)) => {
|
||||
reject_remote_mode_for_subcommand(root_remote.as_deref(), "cloud")?;
|
||||
prepend_config_flags(
|
||||
&mut cloud_cli.config_overrides,
|
||||
root_config_overrides.clone(),
|
||||
@@ -721,6 +772,7 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
||||
}
|
||||
Some(Subcommand::Sandbox(sandbox_args)) => match sandbox_args.cmd {
|
||||
SandboxCommand::Macos(mut seatbelt_cli) => {
|
||||
reject_remote_mode_for_subcommand(root_remote.as_deref(), "sandbox macos")?;
|
||||
prepend_config_flags(
|
||||
&mut seatbelt_cli.config_overrides,
|
||||
root_config_overrides.clone(),
|
||||
@@ -732,6 +784,7 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
||||
.await?;
|
||||
}
|
||||
SandboxCommand::Linux(mut landlock_cli) => {
|
||||
reject_remote_mode_for_subcommand(root_remote.as_deref(), "sandbox linux")?;
|
||||
prepend_config_flags(
|
||||
&mut landlock_cli.config_overrides,
|
||||
root_config_overrides.clone(),
|
||||
@@ -743,6 +796,7 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
||||
.await?;
|
||||
}
|
||||
SandboxCommand::Windows(mut windows_cli) => {
|
||||
reject_remote_mode_for_subcommand(root_remote.as_deref(), "sandbox windows")?;
|
||||
prepend_config_flags(
|
||||
&mut windows_cli.config_overrides,
|
||||
root_config_overrides.clone(),
|
||||
@@ -756,16 +810,22 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
||||
},
|
||||
Some(Subcommand::Debug(DebugCommand { subcommand })) => match subcommand {
|
||||
DebugSubcommand::AppServer(cmd) => {
|
||||
reject_remote_mode_for_subcommand(root_remote.as_deref(), "debug app-server")?;
|
||||
run_debug_app_server_command(cmd).await?;
|
||||
}
|
||||
DebugSubcommand::ClearMemories => {
|
||||
reject_remote_mode_for_subcommand(root_remote.as_deref(), "debug clear-memories")?;
|
||||
run_debug_clear_memories_command(&root_config_overrides, &interactive).await?;
|
||||
}
|
||||
},
|
||||
Some(Subcommand::Execpolicy(ExecpolicyCommand { sub })) => match sub {
|
||||
ExecpolicySubcommand::Check(cmd) => run_execpolicycheck(cmd)?,
|
||||
ExecpolicySubcommand::Check(cmd) => {
|
||||
reject_remote_mode_for_subcommand(root_remote.as_deref(), "execpolicy check")?;
|
||||
run_execpolicycheck(cmd)?
|
||||
}
|
||||
},
|
||||
Some(Subcommand::Apply(mut apply_cli)) => {
|
||||
reject_remote_mode_for_subcommand(root_remote.as_deref(), "apply")?;
|
||||
prepend_config_flags(
|
||||
&mut apply_cli.config_overrides,
|
||||
root_config_overrides.clone(),
|
||||
@@ -773,16 +833,19 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
||||
run_apply_command(apply_cli, None).await?;
|
||||
}
|
||||
Some(Subcommand::ResponsesApiProxy(args)) => {
|
||||
reject_remote_mode_for_subcommand(root_remote.as_deref(), "responses-api-proxy")?;
|
||||
tokio::task::spawn_blocking(move || codex_responses_api_proxy::run_main(args))
|
||||
.await??;
|
||||
}
|
||||
Some(Subcommand::StdioToUds(cmd)) => {
|
||||
reject_remote_mode_for_subcommand(root_remote.as_deref(), "stdio-to-uds")?;
|
||||
let socket_path = cmd.socket_path;
|
||||
tokio::task::spawn_blocking(move || codex_stdio_to_uds::run(socket_path.as_path()))
|
||||
.await??;
|
||||
}
|
||||
Some(Subcommand::Features(FeaturesCli { sub })) => match sub {
|
||||
FeaturesSubcommand::List => {
|
||||
reject_remote_mode_for_subcommand(root_remote.as_deref(), "features list")?;
|
||||
// Respect root-level `-c` overrides plus top-level flags like `--profile`.
|
||||
let mut cli_kv_overrides = root_config_overrides
|
||||
.parse_overrides()
|
||||
@@ -825,9 +888,11 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
||||
}
|
||||
}
|
||||
FeaturesSubcommand::Enable(FeatureSetArgs { feature }) => {
|
||||
reject_remote_mode_for_subcommand(root_remote.as_deref(), "features enable")?;
|
||||
enable_feature_in_config(&interactive, &feature).await?;
|
||||
}
|
||||
FeaturesSubcommand::Disable(FeatureSetArgs { feature }) => {
|
||||
reject_remote_mode_for_subcommand(root_remote.as_deref(), "features disable")?;
|
||||
disable_feature_in_config(&interactive, &feature).await?;
|
||||
}
|
||||
},
|
||||
@@ -949,8 +1014,18 @@ fn prepend_config_flags(
|
||||
.splice(0..0, cli_config_overrides.raw_overrides);
|
||||
}
|
||||
|
||||
fn reject_remote_mode_for_subcommand(remote: Option<&str>, subcommand: &str) -> anyhow::Result<()> {
|
||||
if let Some(remote) = remote {
|
||||
anyhow::bail!(
|
||||
"`--remote {remote}` is only supported for interactive TUI commands, not `codex {subcommand}`"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_interactive_tui(
|
||||
mut interactive: TuiCli,
|
||||
remote: Option<String>,
|
||||
arg0_paths: Arg0DispatchPaths,
|
||||
) -> std::io::Result<AppExitInfo> {
|
||||
if let Some(prompt) = interactive.prompt.take() {
|
||||
@@ -976,12 +1051,93 @@ async fn run_interactive_tui(
|
||||
}
|
||||
}
|
||||
|
||||
codex_tui::run_main(
|
||||
interactive,
|
||||
arg0_paths,
|
||||
codex_core::config_loader::LoaderOverrides::default(),
|
||||
)
|
||||
.await
|
||||
let use_app_server_tui = codex_tui::should_use_app_server_tui(&interactive).await?;
|
||||
let normalized_remote = remote
|
||||
.as_deref()
|
||||
.map(codex_tui_app_server::normalize_remote_addr)
|
||||
.transpose()
|
||||
.map_err(std::io::Error::other)?;
|
||||
if normalized_remote.is_some() && !use_app_server_tui {
|
||||
return Ok(AppExitInfo::fatal(
|
||||
"`--remote` requires the `tui_app_server` feature flag to be enabled.",
|
||||
));
|
||||
}
|
||||
if use_app_server_tui {
|
||||
codex_tui_app_server::run_main(
|
||||
into_app_server_tui_cli(interactive),
|
||||
arg0_paths,
|
||||
codex_core::config_loader::LoaderOverrides::default(),
|
||||
normalized_remote,
|
||||
)
|
||||
.await
|
||||
.map(into_legacy_app_exit_info)
|
||||
} else {
|
||||
codex_tui::run_main(
|
||||
interactive,
|
||||
arg0_paths,
|
||||
codex_core::config_loader::LoaderOverrides::default(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
fn into_app_server_tui_cli(cli: TuiCli) -> codex_tui_app_server::Cli {
|
||||
codex_tui_app_server::Cli {
|
||||
prompt: cli.prompt,
|
||||
images: cli.images,
|
||||
resume_picker: cli.resume_picker,
|
||||
resume_last: cli.resume_last,
|
||||
resume_session_id: cli.resume_session_id,
|
||||
resume_show_all: cli.resume_show_all,
|
||||
fork_picker: cli.fork_picker,
|
||||
fork_last: cli.fork_last,
|
||||
fork_session_id: cli.fork_session_id,
|
||||
fork_show_all: cli.fork_show_all,
|
||||
model: cli.model,
|
||||
oss: cli.oss,
|
||||
oss_provider: cli.oss_provider,
|
||||
config_profile: cli.config_profile,
|
||||
sandbox_mode: cli.sandbox_mode,
|
||||
approval_policy: cli.approval_policy,
|
||||
full_auto: cli.full_auto,
|
||||
dangerously_bypass_approvals_and_sandbox: cli.dangerously_bypass_approvals_and_sandbox,
|
||||
cwd: cli.cwd,
|
||||
web_search: cli.web_search,
|
||||
add_dir: cli.add_dir,
|
||||
no_alt_screen: cli.no_alt_screen,
|
||||
config_overrides: cli.config_overrides,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_legacy_update_action(
|
||||
action: codex_tui_app_server::update_action::UpdateAction,
|
||||
) -> UpdateAction {
|
||||
match action {
|
||||
codex_tui_app_server::update_action::UpdateAction::NpmGlobalLatest => {
|
||||
UpdateAction::NpmGlobalLatest
|
||||
}
|
||||
codex_tui_app_server::update_action::UpdateAction::BunGlobalLatest => {
|
||||
UpdateAction::BunGlobalLatest
|
||||
}
|
||||
codex_tui_app_server::update_action::UpdateAction::BrewUpgrade => UpdateAction::BrewUpgrade,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_legacy_exit_reason(reason: codex_tui_app_server::ExitReason) -> ExitReason {
|
||||
match reason {
|
||||
codex_tui_app_server::ExitReason::UserRequested => ExitReason::UserRequested,
|
||||
codex_tui_app_server::ExitReason::Fatal(message) => ExitReason::Fatal(message),
|
||||
}
|
||||
}
|
||||
|
||||
fn into_legacy_app_exit_info(exit_info: codex_tui_app_server::AppExitInfo) -> AppExitInfo {
|
||||
AppExitInfo {
|
||||
token_usage: exit_info.token_usage,
|
||||
thread_id: exit_info.thread_id,
|
||||
thread_name: exit_info.thread_name,
|
||||
update_action: exit_info.update_action.map(into_legacy_update_action),
|
||||
exit_reason: into_legacy_exit_reason(exit_info.exit_reason),
|
||||
}
|
||||
}
|
||||
|
||||
fn confirm(prompt: &str) -> std::io::Result<bool> {
|
||||
@@ -1114,12 +1270,14 @@ mod tests {
|
||||
config_overrides: root_overrides,
|
||||
subcommand,
|
||||
feature_toggles: _,
|
||||
remote: _,
|
||||
} = cli;
|
||||
|
||||
let Subcommand::Resume(ResumeCommand {
|
||||
session_id,
|
||||
last,
|
||||
all,
|
||||
remote: _,
|
||||
config_overrides: resume_cli,
|
||||
}) = subcommand.expect("resume present")
|
||||
else {
|
||||
@@ -1143,12 +1301,14 @@ mod tests {
|
||||
config_overrides: root_overrides,
|
||||
subcommand,
|
||||
feature_toggles: _,
|
||||
remote: _,
|
||||
} = cli;
|
||||
|
||||
let Subcommand::Fork(ForkCommand {
|
||||
session_id,
|
||||
last,
|
||||
all,
|
||||
remote: _,
|
||||
config_overrides: fork_cli,
|
||||
}) = subcommand.expect("fork present")
|
||||
else {
|
||||
@@ -1449,6 +1609,36 @@ mod tests {
|
||||
assert!(app_server.analytics_default_enabled);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_flag_parses_for_interactive_root() {
|
||||
let cli = MultitoolCli::try_parse_from(["codex", "--remote", "ws://127.0.0.1:4500"])
|
||||
.expect("parse");
|
||||
assert_eq!(cli.remote.remote.as_deref(), Some("ws://127.0.0.1:4500"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_flag_parses_for_resume_subcommand() {
|
||||
let cli =
|
||||
MultitoolCli::try_parse_from(["codex", "resume", "--remote", "ws://127.0.0.1:4500"])
|
||||
.expect("parse");
|
||||
let Subcommand::Resume(ResumeCommand { remote, .. }) =
|
||||
cli.subcommand.expect("resume present")
|
||||
else {
|
||||
panic!("expected resume subcommand");
|
||||
};
|
||||
assert_eq!(remote.remote.as_deref(), Some("ws://127.0.0.1:4500"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reject_remote_mode_for_non_interactive_subcommands() {
|
||||
let err = reject_remote_mode_for_subcommand(Some("127.0.0.1:4500"), "exec")
|
||||
.expect_err("non-interactive subcommands should reject --remote");
|
||||
assert!(
|
||||
err.to_string()
|
||||
.contains("only supported for interactive TUI commands")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_server_listen_websocket_url_parses() {
|
||||
let app_server = app_server_from_args(
|
||||
|
||||
Reference in New Issue
Block a user