diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index b0fc4f1a61..1e2a344103 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -633,14 +633,11 @@ name = "codex-session" version = "0.1.0" dependencies = [ "anyhow", - "bytes", "chrono", "clap 4.5.37", "codex-core", "codex-exec", "codex-repl", - "codex-tui", - "crossterm 0.27.0", "dirs 5.0.1", "libc", "names", @@ -666,7 +663,7 @@ dependencies = [ "codex-ansi-escape", "codex-core", "color-eyre", - "crossterm 0.28.1", + "crossterm", "ratatui", "shlex", "tokio", @@ -792,22 +789,6 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" -[[package]] -name = "crossterm" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" -dependencies = [ - "bitflags 2.9.0", - "crossterm_winapi", - "libc", - "mio 0.8.11", - "parking_lot", - "signal-hook", - "signal-hook-mio", - "winapi", -] - [[package]] name = "crossterm" version = "0.28.1" @@ -816,7 +797,7 @@ checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ "bitflags 2.9.0", "crossterm_winapi", - "mio 1.0.3", + "mio", "parking_lot", "rustix 0.38.44", "signal-hook", @@ -2158,18 +2139,6 @@ dependencies = [ "adler", ] -[[package]] -name = "mio" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" -dependencies = [ - "libc", - "log", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.48.0", -] - [[package]] name = "mio" version = "1.0.3" @@ -2754,7 +2723,7 @@ dependencies = [ "bitflags 2.9.0", "cassowary", "compact_str", - "crossterm 0.28.1", + "crossterm", "indoc", "instability", "itertools 0.13.0", @@ -3275,8 +3244,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" dependencies = [ "libc", - "mio 0.8.11", - "mio 1.0.3", + "mio", "signal-hook", ] @@ -3752,7 +3720,7 @@ dependencies = [ "backtrace", "bytes", "libc", - "mio 1.0.3", + "mio", "pin-project-lite", "signal-hook-registry", "socket2", @@ -4001,7 +3969,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a5318dd619ed73c52a9417ad19046724effc1287fb75cdcc4eca1d6ac1acbae" dependencies = [ - "crossterm 0.28.1", + "crossterm", "ratatui", "unicode-width 0.2.0", ] diff --git a/codex-rs/session/Cargo.toml b/codex-rs/session/Cargo.toml index d48e65e038..21d2801a5d 100644 --- a/codex-rs/session/Cargo.toml +++ b/codex-rs/session/Cargo.toml @@ -26,10 +26,8 @@ tracing = { version = "0.1.41", features = ["log"] } tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } # --- additions for PTY/TUI support --- -bytes = "1.5" # Raw terminal handling when attaching to TUI sessions -crossterm = "0.27" # PTY helpers (unix only) @@ -50,7 +48,6 @@ nix = { version = "0.27", default-features = false, features = ["process", "sign # Re-use the codex-exec library for its CLI definition codex_exec = { package = "codex-exec", path = "../exec" } codex_repl = { package = "codex-repl", path = "../repl" } -codex_tui = { package = "codex-tui", path = "../tui" } [dev-dependencies] tempfile = "3" diff --git a/codex-rs/session/src/cli.rs b/codex-rs/session/src/cli.rs index 66859be23a..35fb894805 100644 --- a/codex-rs/session/src/cli.rs +++ b/codex-rs/session/src/cli.rs @@ -56,7 +56,6 @@ impl Cli { Commands::Delete(x) => x.run().await, Commands::Logs(x) => x.run().await, Commands::List(x) => x.run().await, - Commands::Mux(x) => x.run().await, } } } @@ -74,9 +73,7 @@ enum Commands { /// List all known sessions. List(ListCmd), - /// Internal helper process: PTY multiplexer daemon (hidden). - #[command(hide = true, name = "__mux")] - Mux(MuxCmd), + // (previous mux variant removed) } // ----------------------------------------------------------------------------- @@ -90,8 +87,6 @@ enum AgentKind { /// Interactive Read-Eval-Print-Loop agent. Repl(ReplCreateCmd), - /// Full-screen Terminal User Interface agent. - Tui(TuiCreateCmd), } #[derive(Args)] @@ -116,11 +111,6 @@ pub struct ReplCreateCmd { repl_cli: codex_repl::Cli, } -#[derive(Args)] -pub struct TuiCreateCmd { - #[clap(flatten)] - tui_cli: codex_tui::Cli, -} impl CreateCmd { pub async fn run(self) -> Result<()> { @@ -146,12 +136,6 @@ impl CreateCmd { let preview = cmd.repl_cli.prompt.as_ref().map(|p| truncate_preview(p)); (child.id().unwrap_or_default(), preview, store::SessionKind::Repl) } - AgentKind::Tui(cmd) => { - let args = build_tui_args(&cmd.tui_cli); - let child = spawn::spawn_tui(&paths, &args).await?; - let preview = cmd.tui_cli.prompt.as_ref().map(|p| truncate_preview(p)); - (child.id().unwrap_or_default(), preview, store::SessionKind::Tui) - } }; // Persist metadata **after** the process has been spawned so we can record its PID. @@ -169,29 +153,7 @@ impl CreateCmd { } } -// ----------------------------------------------------------------------------- -// internal mux helper sub-command (hidden) - -#[derive(Args)] -pub struct MuxCmd { - /// Raw PTY master file descriptor passed from the parent process. - #[arg(long)] - fd: i32, - - /// Path to the Unix-domain socket that clients attach to. - #[arg(long)] - sock: std::path::PathBuf, - - /// Path to the binary stdout log file. - #[arg(long)] - log: std::path::PathBuf, -} - -impl MuxCmd { - pub async fn run(self) -> Result<()> { - crate::spawn::mux_main(self.fd, self.sock, self.log).await - } -} +// (mux helper removed) fn truncate_preview(p: &str) -> String { let slice: String = p.chars().take(40).collect(); @@ -315,61 +277,6 @@ fn build_repl_args(cli: &codex_repl::Cli) -> Vec { // For the first implementation we forward only a minimal subset of options that // are already handled in the REPL helper above. Future work can extend this // with the full flag surface. -fn build_tui_args(cli: &codex_tui::Cli) -> Vec { - let mut args = Vec::new(); - - // Positional prompt argument (optional) – must be last. - - if let Some(model) = &cli.model { - args.push("--model".into()); - args.push(model.clone()); - } - - for img in &cli.images { - args.push("--image".into()); - args.push(img.to_string_lossy().into_owned()); - } - - if cli.skip_git_repo_check { - args.push("--skip-git-repo-check".into()); - } - - if cli.disable_response_storage { - args.push("--disable-response-storage".into()); - } - - // Approval + sandbox policies - args.push("--ask-for-approval".into()); - args.push(match cli.approval_policy { - codex_core::ApprovalModeCliArg::OnFailure => "on-failure".into(), - codex_core::ApprovalModeCliArg::UnlessAllowListed => "unless-allow-listed".into(), - codex_core::ApprovalModeCliArg::Never => "never".into(), - }); - - args.push("--sandbox".into()); - args.push(match cli.sandbox_policy { - codex_core::SandboxModeCliArg::NetworkRestricted => "network-restricted".into(), - codex_core::SandboxModeCliArg::FileWriteRestricted => "file-write-restricted".into(), - codex_core::SandboxModeCliArg::NetworkAndFileWriteRestricted => - "network-and-file-write-restricted".into(), - codex_core::SandboxModeCliArg::DangerousNoRestrictions => - "dangerous-no-restrictions".into(), - }); - - // Convenience flags - if cli.full_auto { - args.push("--full-auto".into()); - } - if cli.suggest { - args.push("--suggest".into()); - } - - if let Some(prompt) = &cli.prompt { - args.push(prompt.clone()); - } - - args -} // ----------------------------------------------------------------------------- // attach @@ -389,18 +296,7 @@ impl AttachCmd { let id = store::resolve_selector(&self.id)?; let paths = store::paths_for(&id)?; - // Load meta in order to decide which attach strategy to use. - let meta_bytes = std::fs::read(&paths.meta)?; - let meta: store::SessionMeta = serde_json::from_slice(&meta_bytes)?; - - match meta.kind { - store::SessionKind::Exec | store::SessionKind::Repl => { - self.attach_line_oriented(&id, &paths).await - } - store::SessionKind::Tui => { - self.attach_tui(&paths).await - } - } + self.attach_line_oriented(&id, &paths).await } // ------------------------------------------------------------------ @@ -457,77 +353,7 @@ impl AttachCmd { Ok(()) } - // ------------------------------------------------------------------ - // TUI attach: raw byte forwarding over unix socket - async fn attach_tui(&self, paths: &store::Paths) -> Result<()> { - #[cfg(unix)] - { - use crossterm::{execute, terminal}; - use tokio::io; - use tokio::net::UnixStream; - - // RAII guard for raw mode + alternate screen. - struct TermGuard; - impl TermGuard { - fn new() -> anyhow::Result { - execute!(std::io::stdout(), terminal::EnterAlternateScreen)?; - terminal::enable_raw_mode()?; - Ok(Self) - } - } - impl Drop for TermGuard { - fn drop(&mut self) { - let _ = terminal::disable_raw_mode(); - let _ = execute!(std::io::stdout(), terminal::LeaveAlternateScreen); - } - } - - let _term = TermGuard::new()?; - - let sock_path = paths.dir.join("sock"); - if !sock_path.exists() { - anyhow::bail!( - "tui session socket not found ({}). Is the session fully initialised?", - sock_path.display() - ); - } - - // Connect to session socket. - let stream = UnixStream::connect(&sock_path).await?; - let (mut reader, mut writer) = stream.into_split(); - - let mut stdin = tokio::io::stdin(); - let mut stdout = tokio::io::stdout(); - - let mut to_stdout = tokio::spawn(async move { - let _ = io::copy(&mut reader, &mut stdout).await; - }); - - let mut to_socket = tokio::spawn(async move { - let _ = io::copy(&mut stdin, &mut writer).await; - }); - - // Wait for either direction, then abort the other. - tokio::select! { - _ = &mut to_stdout => { - to_socket.abort(); - } - _ = &mut to_socket => { - to_stdout.abort(); - } - } - - let _ = to_stdout.await; - let _ = to_socket.await; - - Ok(()) - } - - #[cfg(not(unix))] - { - anyhow::bail!("tui sessions are only supported on Unix at the moment"); - } - } + // (TUI attach removed) } // ----------------------------------------------------------------------------- diff --git a/codex-rs/session/src/spawn.rs b/codex-rs/session/src/spawn.rs index 2159786e34..7decb84a46 100644 --- a/codex-rs/session/src/spawn.rs +++ b/codex-rs/session/src/spawn.rs @@ -1,20 +1,12 @@ -//! Spawn detached Codex agent processes (exec, repl, tui). -//! -//! The *exec* and *repl* helpers reuse the original FIFO/pipe strategy while -//! the new **tui** flavour allocates a pseudo-terminal so the crossterm / -//! ratatui application sees a *real* tty. A small socket fan-out forwards raw -//! bytes between the PTY and every `codex-session attach` client. +//! Spawn detached Codex agent processes for exec and repl sessions. use crate::store::Paths; use anyhow::{Context, Result}; use std::fs::OpenOptions; use tokio::process::{Child, Command}; -#[cfg(unix)] -use std::os::unix::process::CommandExt; // for pre_exec - // ----------------------------------------------------------------------------- -// exec – non-interactive batch agent (stdin = /dev/null) +// exec – non-interactive batch agent pub fn spawn_exec(paths: &Paths, exec_args: &[String]) -> Result { #[cfg(unix)] @@ -37,7 +29,6 @@ pub fn spawn_exec(paths: &Paths, exec_args: &[String]) -> Result { .stdout(stdout) .stderr(stderr); - // Detach session so the child is not killed with the parent. unsafe { cmd.pre_exec(|| { if libc::setsid() == -1 { @@ -76,12 +67,12 @@ pub fn spawn_exec(paths: &Paths, exec_args: &[String]) -> Result { .creation_flags(DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP); let child = cmd.spawn().context("failed to spawn codex-exec")?; - return Ok(child); + Ok(child) } } // ----------------------------------------------------------------------------- -// repl – interactive but **line-oriented** (FIFO for stdin) +// repl – interactive FIFO stdin pub fn spawn_repl(paths: &Paths, repl_args: &[String]) -> Result { #[cfg(unix)] @@ -89,10 +80,8 @@ pub fn spawn_repl(paths: &Paths, repl_args: &[String]) -> Result { use std::io; use std::os::unix::ffi::OsStrExt; - // Ensure the FIFO exists (create with 600 permissions). if !paths.stdin.exists() { let c_path = std::ffi::CString::new(paths.stdin.as_os_str().as_bytes()).unwrap(); - // SAFETY: libc call, check return value. let res = unsafe { libc::mkfifo(c_path.as_ptr(), 0o600) }; if res != 0 { let err = std::io::Error::last_os_error(); @@ -102,8 +91,6 @@ pub fn spawn_repl(paths: &Paths, repl_args: &[String]) -> Result { } } - // Open the FIFO read-write so `open()` does **not** block even though - // no external writer is connected yet. let stdin = OpenOptions::new() .read(true) .write(true) @@ -135,223 +122,12 @@ pub fn spawn_repl(paths: &Paths, repl_args: &[String]) -> Result { } let child = cmd.spawn().context("failed to spawn codex-repl")?; - return Ok(child); + Ok(child) } #[cfg(windows)] { - anyhow::bail!("codex-repl sessions are not yet supported on Windows"); + anyhow::bail!("codex-repl sessions are not supported on Windows yet"); } } -// ----------------------------------------------------------------------------- -// tui – full terminal UI (PTY + socket fan-out) - -use bytes::Bytes; - -#[cfg(unix)] -use { - nix::unistd::dup, - std::os::unix::io::{FromRawFd, IntoRawFd, RawFd}, -}; - -/// Spawn `codex-tui` inside a pseudo-terminal and start the background -/// multiplexer so future `attach` commands can talk to it. -pub async fn spawn_tui(paths: &Paths, tui_args: &[String]) -> Result { - #[cfg(unix)] - { - use std::io; - - // 1. PTY allocation --------------------------------------------------- - let pty = nix::pty::openpty(None, None).context("openpty failed")?; - - let slave_fd: RawFd = pty.slave.into_raw_fd(); - let master_fd: RawFd = pty.master.into_raw_fd(); - - // Ensure master_fd is inheritable (clear FD_CLOEXEC) - { - use nix::fcntl::{fcntl, FcntlArg, FdFlag}; - let _ = fcntl(master_fd, FcntlArg::F_SETFD(FdFlag::empty())); - } - - // 2. Spawn codex-tui -------------------------------------------------- - let make_stdio = |fd: RawFd| unsafe { std::process::Stdio::from_raw_fd(fd) }; - let stdin = make_stdio(dup(slave_fd)?); - let stdout = make_stdio(dup(slave_fd)?); - let stderr = make_stdio(slave_fd); - - let mut tui_cmd = Command::new("codex-tui"); - tui_cmd.args(tui_args) - .stdin(stdin) - .stdout(stdout) - .stderr(stderr); - - unsafe { - tui_cmd.pre_exec(|| { - if libc::setsid() == -1 { - return Err(io::Error::last_os_error()); - } - libc::signal(libc::SIGHUP, libc::SIG_IGN); - Ok(()) - }); - } - - let child = tui_cmd.spawn().context("failed to spawn codex-tui")?; - - // 3. Spawn mux helper process --------------------------------------- - - let sock_path = paths.dir.join("sock"); - if sock_path.exists() { - let _ = std::fs::remove_file(&sock_path); - } - - let current_exe = std::env::current_exe()?; - let mut mux_cmd = std::process::Command::new(current_exe); - mux_cmd.arg("__mux") - .arg("--fd").arg(format!("{master_fd}")) - .arg("--sock").arg(&sock_path) - .arg("--log").arg(&paths.stdout) - .stdin(std::process::Stdio::null()) - .stdout(std::process::Stdio::null()) - .stderr(std::process::Stdio::null()); - - // Detach mux process (own session) - unsafe { - mux_cmd.pre_exec(|| { - libc::setsid(); - Ok(()) - }); - } - - let _ = mux_cmd.spawn().context("failed to spawn mux helper")?; - - Ok(child) - } - - #[cfg(not(unix))] - { - anyhow::bail!("tui sessions are only supported on Unix right now"); - } -} - -#[cfg(unix)] -async fn spawn_client( - sock: tokio::net::UnixStream, - pty_write_fd: RawFd, - tx: &tokio::sync::broadcast::Sender, -) { - use tokio::io::AsyncWriteExt; - - let (mut s_read, mut s_write) = sock.into_split(); - - // Clone PTY master *write* side for this client - let pty_write = unsafe { - tokio::fs::File::from_std(std::fs::File::from_raw_fd(pty_write_fd)) - }; - let mut pty_write = pty_write; - - // subscribe - let mut rx = tx.subscribe(); - - // socket → pty - let to_pty = tokio::spawn(async move { - tokio::io::copy(&mut s_read, &mut pty_write).await.ok(); - }); - - // pty broadcast → socket - let from_pty = tokio::spawn(async move { - while let Ok(bytes) = rx.recv().await { - if s_write.write_all(&bytes).await.is_err() { - break; - } - } - }); - - let _ = tokio::join!(to_pty, from_pty); -} - -/// Actual multiplexer event loop that runs inside the forked daemon process. -#[cfg(unix)] -#[cfg(unix)] -pub async fn mux_main(master_fd: RawFd, sock_path: std::path::PathBuf, stdout_log: std::path::PathBuf) -> anyhow::Result<()> { - use tokio::{net::UnixListener, sync::broadcast}; - use tokio::io::{AsyncReadExt, AsyncWriteExt}; - - // Bind socket (should succeed; stale already removed). - let listener = match UnixListener::bind(&sock_path) { - Ok(l) => l, - Err(e) => { - eprintln!("socket bind failed: {e}"); - return Ok(()); - } - }; - - // Async read handle for PTY master. - let master_read = unsafe { - tokio::fs::File::from_std(std::fs::File::from_raw_fd(dup(master_fd).expect("dup master"))) - }; - - // binary log file - let mut log_file = match tokio::fs::OpenOptions::new() - .create(true) - .append(true) - .open(&stdout_log) - .await - { - Ok(f) => f, - Err(e) => { - eprintln!("log open failed: {e}"); - return Ok(()); - } - }; - - let (tx, _) = broadcast::channel::(64); - - // Reader task - let tx_read = tx.clone(); - tokio::spawn(async move { - let mut buf = [0u8; 4096]; - let mut r = master_read; - loop { - match r.read(&mut buf).await { - Ok(0) => break, - Ok(n) => { - let _ = log_file.write_all(&buf[..n]).await; - let _ = tx_read.send(Bytes::copy_from_slice(&buf[..n])); - } - Err(e) => { - eprintln!("pty read error: {e}"); - break; - } - } - } - }); - - // Accept-loop - loop { - match listener.accept().await { - Ok((sock, _)) => { - match dup(master_fd) { - Ok(fd) => { - let tx_c = tx.clone(); - tokio::spawn(async move { - spawn_client(sock, fd, &tx_c).await; - }); - } - Err(e) => eprintln!("dup failed: {e}"), - } - } - Err(e) => { - eprintln!("accept error: {e}"); - break; - } - } - } - - Ok(()) -} - -#[cfg(not(unix))] -pub async fn mux_main(_fd: i32, _sock: std::path::PathBuf, _log: std::path::PathBuf) -> anyhow::Result<()> { - anyhow::bail!("tui sessions are only supported on unix"); -} diff --git a/codex-rs/session/src/store.rs b/codex-rs/session/src/store.rs index ca924f9fd7..2f81b14fe4 100644 --- a/codex-rs/session/src/store.rs +++ b/codex-rs/session/src/store.rs @@ -59,8 +59,6 @@ pub enum SessionKind { Exec, /// Line-oriented interactive session – `codex-repl`. Repl, - /// Full terminal-UI session (crossterm / ratatui) – `codex-tui`. - Tui, } impl Default for SessionKind {