Improve codex remote-control CLI UX (#22878)

## Description

This PR makes `codex remote-control` behave like a foreground CLI
command by default. Running it now starts remote control, waits for
readiness, prints a clear status message with the machine name, and
stays alive until Ctrl-C.

Users who want daemon behavior can use `codex remote-control start`, and
`codex remote-control stop` now prints concise human-readable output.
`--json` remains available for scripts.

Implementation-wise, this now verifies the real app-server state instead
of just assuming startup worked. The CLI starts or connects to
app-server, probes its control socket, calls the `remoteControl/enable`
API, and waits for the remote-control status response/notification
before printing success.

For daemon mode, `codex remote-control start` also reports which managed
app-server binary was used, including its path and best-effort `codex
--version`, so failures are easier to diagnose.

## Examples

Example output:
```
> codex remote-control
Starting app-server with remote control enabled...
This machine is available for remote control as com-97826.
Press Ctrl-C to stop.
```

Error case using daemon (currently expected based on our publicly
released CLI version):
```
> ./target/debug/codex remote-control start
Starting app-server daemon with remote control enabled...
Error: app server did not become ready on /Users/owen/.codex/app-server-control/app-server-control.sock

Daemon used app-server:
  path: /Users/owen/.codex/packages/standalone/current/codex
  version: 0.130.0

Managed app-server stderr (/Users/owen/.codex/app-server-daemon/app-server.stderr.log):
  error: unexpected argument '--remote-control' found
  
  Usage: codex app-server [OPTIONS] [COMMAND]
  
  For more information, try '--help'.

Caused by:
    0: failed to connect to /Users/owen/.codex/app-server-control/app-server-control.sock
    1: No such file or directory (os error 2)
```

## What changed

- `codex remote-control` now runs remote control in the foreground and
prints a Ctrl-C stop hint.
- `codex remote-control start` starts the daemon and waits for remote
control readiness before reporting success.
- `codex remote-control stop` reports stopped/not-running status in
plain language.
- Startup failures now include recent managed app-server stderr to make
daemon issues easier to diagnose.
- Added coverage for CLI output, readiness waiting, foreground shutdown,
and stderr log tailing.
This commit is contained in:
Owen Lin
2026-05-18 13:39:02 -07:00
committed by GitHub
parent 732b12b1ef
commit 1752f374a8
9 changed files with 1611 additions and 158 deletions
+14 -45
View File
@@ -52,6 +52,7 @@ mod doctor;
mod marketplace_cmd;
mod mcp_cmd;
mod plugin_cmd;
mod remote_control_cmd;
mod state_db_recovery;
#[cfg(not(windows))]
mod wsl_paths;
@@ -59,6 +60,7 @@ mod wsl_paths;
use crate::mcp_cmd::McpCli;
use crate::plugin_cmd::PluginCli;
use crate::plugin_cmd::PluginSubcommand;
use crate::remote_control_cmd::RemoteControlCommand;
use doctor::DoctorCommand;
use state_db_recovery as local_state_db;
@@ -565,21 +567,6 @@ struct AppServerBootstrapCommand {
remote_control: bool,
}
#[derive(Debug, Args)]
struct RemoteControlCommand {
#[command(subcommand)]
subcommand: Option<RemoteControlSubcommand>,
}
#[derive(Debug, Clone, Copy, clap::Subcommand)]
enum RemoteControlSubcommand {
/// Start the app-server daemon with remote control enabled.
Start,
/// Stop the app-server daemon.
Stop,
}
#[derive(Debug, Args)]
struct GenerateTsCommand {
/// Output directory where .ts files will be written
@@ -1070,24 +1057,18 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
}
}
Some(Subcommand::RemoteControl(remote_control_cli)) => {
let subcommand_name = remote_control_subcommand_name(&remote_control_cli);
let subcommand_name = remote_control_cli.subcommand_name();
reject_remote_mode_for_subcommand(
root_remote.as_deref(),
root_remote_auth_token_env.as_deref(),
subcommand_name,
)?;
match remote_control_cli
.subcommand
.unwrap_or(RemoteControlSubcommand::Start)
{
RemoteControlSubcommand::Start => {
let output = codex_app_server_daemon::ensure_remote_control_started().await?;
println!("{}", serde_json::to_string(&output)?);
}
RemoteControlSubcommand::Stop => {
print_app_server_daemon_output(AppServerLifecycleCommand::Stop).await?;
}
}
remote_control_cmd::run(
remote_control_cli,
arg0_paths.clone(),
root_config_overrides,
)
.await?;
}
#[cfg(any(target_os = "macos", target_os = "windows"))]
Some(Subcommand::App(app_cli)) => {
@@ -1892,9 +1873,7 @@ fn unsupported_subcommand_name_for_strict_config(
Some(Subcommand::AppServer(app_server)) => {
Some(app_server_subcommand_name(app_server.subcommand.as_ref()))
}
Some(Subcommand::RemoteControl(remote_control)) => {
Some(remote_control_subcommand_name(remote_control))
}
Some(Subcommand::RemoteControl(remote_control)) => Some(remote_control.subcommand_name()),
Some(Subcommand::Mcp(_)) => Some("mcp"),
Some(Subcommand::Plugin(_)) => Some("plugin"),
#[cfg(any(target_os = "macos", target_os = "windows"))]
@@ -1947,14 +1926,6 @@ fn reject_remote_mode_for_app_server_subcommand(
reject_remote_mode_for_subcommand(remote, remote_auth_token_env, subcommand_name)
}
fn remote_control_subcommand_name(command: &RemoteControlCommand) -> &'static str {
match command.subcommand {
None => "remote-control",
Some(RemoteControlSubcommand::Start) => "remote-control start",
Some(RemoteControlSubcommand::Stop) => "remote-control stop",
}
}
fn app_server_subcommand_name(subcommand: Option<&AppServerSubcommand>) -> &'static str {
match subcommand {
None => "app-server",
@@ -2974,12 +2945,10 @@ mod tests {
fn reject_remote_flag_for_remote_control() {
let cli = MultitoolCli::try_parse_from(["codex", "--remote", "unix://", "remote-control"])
.expect("parse");
assert_matches!(
cli.subcommand,
Some(Subcommand::RemoteControl(RemoteControlCommand {
subcommand: None
}))
);
let Some(Subcommand::RemoteControl(remote_control)) = &cli.subcommand else {
panic!("expected remote-control subcommand");
};
assert_eq!(remote_control.subcommand_name(), "remote-control");
let err = reject_remote_mode_for_subcommand(
cli.remote.remote.as_deref(),