Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions codex-rs/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions codex-rs/gui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,29 @@ workspace = true
[dependencies]
anyhow = "1"
clap = { version = "4", features = ["derive"] }
clap_complete = "4"
codex-arg0 = { path = "../arg0" }
codex-chatgpt = { path = "../chatgpt" }
codex-cli = { path = "../cli" }
codex-common = { path = "../common", features = ["cli"] }
codex-core = { path = "../core" }
codex-exec = { path = "../exec" }
codex-login = { path = "../login" }
codex-mcp-server = { path = "../mcp-server" }
codex-protocol = { path = "../protocol" }
codex-protocol-ts = { path = "../protocol-ts" }
codex-tui = { path = "../tui" }
eframe = "0.27"
serde_json = "1"
tokio = { version = "1", features = [
"io-std",
"macros",
"process",
"rt-multi-thread",
"signal",
] }
tracing = "0.1.41"
tracing-subscriber = "0.3.19"

[dev-dependencies]
pretty_assertions = "1"
243 changes: 217 additions & 26 deletions codex-rs/gui/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
use anyhow::Result;
use clap::CommandFactory;
use clap::Parser;
use clap_complete::Shell;
use clap_complete::generate;
use codex_chatgpt::apply_command::ApplyCommand;
use codex_chatgpt::apply_command::run_apply_command;
use codex_cli::LandlockCommand;
use codex_cli::SeatbeltCommand;
use codex_cli::login::run_login_status;
use codex_cli::login::run_login_with_api_key;
use codex_cli::login::run_login_with_chatgpt;
use codex_cli::login::run_logout;
use codex_cli::proto;
use codex_cli::proto::ProtoCli;
use codex_common::CliConfigOverrides;
use codex_core::protocol::FinalOutput;
use codex_exec::Cli as ExecCli;
use codex_tui::Cli as TuiCli;
use eframe::egui::Align;
use eframe::egui::Color32;
Expand All @@ -15,27 +31,211 @@ use std::mem;
use std::path::PathBuf;

/// Command line interface for the graphical Codex client.
///
/// If no subcommand is specified, options will be forwarded to the interactive UI.
#[derive(Debug, Parser)]
#[command(version)]
#[clap(author, version, subcommand_negates_reqs = true, bin_name = "codex")]
pub struct Cli {
#[clap(flatten)]
pub config_overrides: CliConfigOverrides,

/// Launch the terminal UI instead of the graphical interface.
#[arg(long = "tui-mode", default_value_t = false)]
pub tui_mode: bool,

#[clap(flatten)]
pub tui: TuiCli,
interactive: TuiCli,

#[clap(subcommand)]
subcommand: Option<Subcommand>,
}

#[derive(Debug, clap::Subcommand)]
enum Subcommand {
/// Run Codex non-interactively.
#[clap(visible_alias = "e")]
Exec(ExecCli),

/// Manage login.
Login(LoginCommand),

/// Remove stored authentication credentials.
Logout(LogoutCommand),

/// Experimental: run Codex as an MCP server.
Mcp,

/// Run the Protocol stream via stdin/stdout.
#[clap(visible_alias = "p")]
Proto(ProtoCli),

/// Generate shell completion scripts.
Completion(CompletionCommand),

/// Internal debugging commands.
Debug(DebugArgs),

/// Apply the latest diff produced by Codex agent as a `git apply` to your local working tree.
#[clap(visible_alias = "a")]
Apply(ApplyCommand),

/// Internal: generate TypeScript protocol bindings.
#[clap(hide = true)]
GenerateTs(GenerateTsCommand),
}

#[derive(Debug, Parser)]
struct CompletionCommand {
/// Shell to generate completions for
#[clap(value_enum, default_value_t = Shell::Bash)]
shell: Shell,
}

#[derive(Debug, Parser)]
struct DebugArgs {
#[command(subcommand)]
cmd: DebugCommand,
}

#[derive(Debug, clap::Subcommand)]
enum DebugCommand {
/// Run a command under Seatbelt (macOS only).
Seatbelt(SeatbeltCommand),

/// Run a command under Landlock+seccomp (Linux only).
Landlock(LandlockCommand),
}

#[derive(Debug, Parser)]
struct LoginCommand {
#[clap(skip)]
config_overrides: CliConfigOverrides,

#[arg(long = "api-key", value_name = "API_KEY")]
api_key: Option<String>,

#[command(subcommand)]
action: Option<LoginSubcommand>,
}

#[derive(Debug, clap::Subcommand)]
enum LoginSubcommand {
/// Show login status.
Status,
}

#[derive(Debug, Parser)]
struct LogoutCommand {
#[clap(skip)]
config_overrides: CliConfigOverrides,
}

#[derive(Debug, Parser)]
struct GenerateTsCommand {
/// Output directory where .ts files will be written
#[arg(short = 'o', long = "out", value_name = "DIR")]
out_dir: PathBuf,

/// Optional path to the Prettier executable to format generated files
#[arg(short = 'p', long = "prettier", value_name = "PRETTIER_BIN")]
prettier: Option<PathBuf>,
}

/// Entry point for the graphical Codex client.
pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> Result<()> {
if cli.tui_mode {
let _ = codex_tui::run_main(cli.tui, codex_linux_sandbox_exe).await?;
} else if let Err(err) = run_gui() {
eprintln!("failed to start GUI: {err}");
match cli.subcommand {
None => {
let mut tui_cli = cli.interactive;
prepend_config_flags(&mut tui_cli.config_overrides, cli.config_overrides);
if cli.tui_mode {
let usage = codex_tui::run_main(tui_cli, codex_linux_sandbox_exe).await?;
if !usage.is_zero() {
println!("{}", FinalOutput::from(usage));
}
} else if let Err(err) = run_gui() {
eprintln!("failed to start GUI: {err}");
}
}
Some(Subcommand::Exec(mut exec_cli)) => {
prepend_config_flags(&mut exec_cli.config_overrides, cli.config_overrides);
codex_exec::run_main(exec_cli, codex_linux_sandbox_exe).await?;
}
Some(Subcommand::Mcp) => {
codex_mcp_server::run_main(codex_linux_sandbox_exe, cli.config_overrides).await?;
}
Some(Subcommand::Login(mut login_cli)) => {
prepend_config_flags(&mut login_cli.config_overrides, cli.config_overrides);
match login_cli.action {
Some(LoginSubcommand::Status) => {
run_login_status(login_cli.config_overrides).await;
}
None => {
if let Some(api_key) = login_cli.api_key {
run_login_with_api_key(login_cli.config_overrides, api_key).await;
} else {
run_login_with_chatgpt(login_cli.config_overrides).await;
}
}
}
}
Some(Subcommand::Logout(mut logout_cli)) => {
prepend_config_flags(&mut logout_cli.config_overrides, cli.config_overrides);
run_logout(logout_cli.config_overrides).await;
}
Some(Subcommand::Proto(mut proto_cli)) => {
prepend_config_flags(&mut proto_cli.config_overrides, cli.config_overrides);
proto::run_main(proto_cli).await?;
}
Some(Subcommand::Completion(completion_cli)) => {
print_completion(completion_cli);
}
Some(Subcommand::Debug(debug_args)) => match debug_args.cmd {
DebugCommand::Seatbelt(mut seatbelt_cli) => {
prepend_config_flags(&mut seatbelt_cli.config_overrides, cli.config_overrides);
codex_cli::debug_sandbox::run_command_under_seatbelt(
seatbelt_cli,
codex_linux_sandbox_exe,
)
.await?;
}
DebugCommand::Landlock(mut landlock_cli) => {
prepend_config_flags(&mut landlock_cli.config_overrides, cli.config_overrides);
codex_cli::debug_sandbox::run_command_under_landlock(
landlock_cli,
codex_linux_sandbox_exe,
)
.await?;
}
},
Some(Subcommand::Apply(mut apply_cli)) => {
prepend_config_flags(&mut apply_cli.config_overrides, cli.config_overrides);
run_apply_command(apply_cli, None).await?;
}
Some(Subcommand::GenerateTs(gen_cli)) => {
codex_protocol_ts::generate_ts(&gen_cli.out_dir, gen_cli.prettier.as_deref())?;
}
}

Ok(())
}

/// Prepend root-level overrides so they have lower precedence than
/// CLI-specific ones specified after the subcommand (if any).
fn prepend_config_flags(
subcommand_config_overrides: &mut CliConfigOverrides,
cli_config_overrides: CliConfigOverrides,
) {
subcommand_config_overrides
.raw_overrides
.splice(0..0, cli_config_overrides.raw_overrides);
}

fn print_completion(cmd: CompletionCommand) {
let mut app = Cli::command();
let name = "codex";
generate(cmd.shell, &mut app, name, &mut std::io::stdout());
}

fn run_gui() -> eframe::Result<()> {
let options = eframe::NativeOptions::default();
eframe::run_native("Codex", options, Box::new(|cc| Box::new(CodexGui::new(cc))))
Expand All @@ -46,6 +246,7 @@ struct Message {
color: Color32,
}

#[derive(Default)]
struct CodexGui {
sessions: Vec<String>,
notes: Vec<String>,
Expand All @@ -56,20 +257,6 @@ struct CodexGui {
prompt: String,
}

impl Default for CodexGui {
fn default() -> Self {
Self {
sessions: Vec::new(),
notes: Vec::new(),
messages: Vec::new(),
input: String::new(),
new_session: String::new(),
new_note: String::new(),
prompt: String::new(),
}
}
}

impl CodexGui {
fn new(_cc: &eframe::CreationContext<'_>) -> Self {
Self {
Expand Down Expand Up @@ -218,17 +405,21 @@ mod tests {

#[test]
fn add_session_appends() {
let mut gui = CodexGui::default();
gui.new_session = "S3".into();
let mut gui = CodexGui {
new_session: "S3".into(),
..Default::default()
};
gui.add_session();
assert_eq!(gui.sessions.len(), 3);
assert_eq!(gui.sessions[2], "S3");
assert_eq!(gui.sessions.len(), 1);
assert_eq!(gui.sessions[0], "S3");
}

#[test]
fn send_message_updates_state() {
let mut gui = CodexGui::default();
gui.input = "hi".into();
let mut gui = CodexGui {
input: "hi".into(),
..Default::default()
};
gui.send_message(Color32::from_rgb(1, 2, 3));
assert_eq!(gui.prompt, "hi");
assert_eq!(gui.messages.last().unwrap().text, "hi");
Expand Down
Loading