diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index ba71596ecd..57711362ff 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -605,6 +605,17 @@ dependencies = [ "tree-sitter-bash", ] +[[package]] +name = "codex-arg0" +version = "0.0.0" +dependencies = [ + "anyhow", + "codex-core", + "codex-linux-sandbox", + "dotenvy", + "tokio", +] + [[package]] name = "codex-chatgpt" version = "0.0.0" @@ -628,11 +639,11 @@ dependencies = [ "anyhow", "clap", "clap_complete", + "codex-arg0", "codex-chatgpt", "codex-common", "codex-core", "codex-exec", - "codex-linux-sandbox", "codex-login", "codex-mcp-server", "codex-tui", @@ -709,9 +720,9 @@ dependencies = [ "anyhow", "chrono", "clap", + "codex-arg0", "codex-common", "codex-core", - "codex-linux-sandbox", "owo-colors", "serde_json", "shlex", @@ -761,7 +772,6 @@ dependencies = [ "clap", "codex-common", "codex-core", - "dotenvy", "landlock", "libc", "seccompiler", @@ -799,8 +809,8 @@ version = "0.0.0" dependencies = [ "anyhow", "assert_cmd", + "codex-arg0", "codex-core", - "codex-linux-sandbox", "mcp-types", "mcp_test_support", "pretty_assertions", @@ -826,10 +836,10 @@ dependencies = [ "base64 0.22.1", "clap", "codex-ansi-escape", + "codex-arg0", "codex-common", "codex-core", "codex-file-search", - "codex-linux-sandbox", "codex-login", "color-eyre", "crossterm", diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index eba43e548b..190256c211 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -3,6 +3,7 @@ resolver = "2" members = [ "ansi-escape", "apply-patch", + "arg0", "cli", "common", "core", diff --git a/codex-rs/arg0/Cargo.toml b/codex-rs/arg0/Cargo.toml new file mode 100644 index 0000000000..9ad1896746 --- /dev/null +++ b/codex-rs/arg0/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "codex-arg0" +version = { workspace = true } +edition = "2024" + +[lib] +name = "codex_arg0" +path = "src/lib.rs" + +[lints] +workspace = true + +[dependencies] +anyhow = "1" +codex-core = { path = "../core" } +codex-linux-sandbox = { path = "../linux-sandbox" } +dotenvy = "0.15.7" +tokio = { version = "1", features = ["rt-multi-thread"] } diff --git a/codex-rs/linux-sandbox/src/lib.rs b/codex-rs/arg0/src/lib.rs similarity index 84% rename from codex-rs/linux-sandbox/src/lib.rs rename to codex-rs/arg0/src/lib.rs index 960678467c..221f86f01a 100644 --- a/codex-rs/linux-sandbox/src/lib.rs +++ b/codex-rs/arg0/src/lib.rs @@ -1,11 +1,3 @@ -#[cfg(target_os = "linux")] -mod landlock; -#[cfg(target_os = "linux")] -mod linux_run_main; - -#[cfg(target_os = "linux")] -pub use linux_run_main::run_main; - use std::future::Future; use std::path::PathBuf; @@ -24,7 +16,7 @@ use std::path::PathBuf; /// /// This function eliminates duplicated code across the various `main.rs` /// entry-points. -pub fn run_with_sandbox(main_fn: F) -> anyhow::Result<()> +pub fn arg0_dispatch_or_else(main_fn: F) -> anyhow::Result<()> where F: FnOnce(Option) -> Fut, Fut: Future>, @@ -40,7 +32,7 @@ where if exe_name == "codex-linux-sandbox" { // Safety: [`run_main`] never returns. - crate::run_main(); + codex_linux_sandbox::run_main(); } // This modifies the environment, which is not thread-safe, so do this @@ -61,11 +53,6 @@ where }) } -#[cfg(not(target_os = "linux"))] -pub fn run_main() -> ! { - panic!("codex-linux-sandbox is only supported on Linux"); -} - /// Load env vars from ~/.codex/.env and `$(pwd)/.env`. fn load_dotenv() { if let Ok(codex_home) = codex_core::config::find_codex_home() { diff --git a/codex-rs/cli/Cargo.toml b/codex-rs/cli/Cargo.toml index 943788157b..ab98764bed 100644 --- a/codex-rs/cli/Cargo.toml +++ b/codex-rs/cli/Cargo.toml @@ -18,12 +18,12 @@ workspace = true anyhow = "1" clap = { version = "4", features = ["derive"] } clap_complete = "4" +codex-arg0 = { path = "../arg0" } codex-chatgpt = { path = "../chatgpt" } codex-core = { path = "../core" } codex-common = { path = "../common", features = ["cli"] } codex-exec = { path = "../exec" } codex-login = { path = "../login" } -codex-linux-sandbox = { path = "../linux-sandbox" } codex-mcp-server = { path = "../mcp-server" } codex-tui = { path = "../tui" } serde_json = "1" diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index 7916a7dc79..efda03bda4 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -2,6 +2,7 @@ use clap::CommandFactory; use clap::Parser; use clap_complete::Shell; use clap_complete::generate; +use codex_arg0::arg0_dispatch_or_else; use codex_chatgpt::apply_command::ApplyCommand; use codex_chatgpt::apply_command::run_apply_command; use codex_cli::LandlockCommand; @@ -92,7 +93,7 @@ struct LoginCommand { } fn main() -> anyhow::Result<()> { - codex_linux_sandbox::run_with_sandbox(|codex_linux_sandbox_exe| async move { + arg0_dispatch_or_else(|codex_linux_sandbox_exe| async move { cli_main(codex_linux_sandbox_exe).await?; Ok(()) }) diff --git a/codex-rs/exec/Cargo.toml b/codex-rs/exec/Cargo.toml index ed01b78ec8..c9d94deb5a 100644 --- a/codex-rs/exec/Cargo.toml +++ b/codex-rs/exec/Cargo.toml @@ -18,13 +18,13 @@ workspace = true anyhow = "1" chrono = "0.4.40" clap = { version = "4", features = ["derive"] } +codex-arg0 = { path = "../arg0" } codex-core = { path = "../core" } codex-common = { path = "../common", features = [ "cli", "elapsed", "sandbox_summary", ] } -codex-linux-sandbox = { path = "../linux-sandbox" } owo-colors = "4.2.0" serde_json = "1" shlex = "1.3.0" diff --git a/codex-rs/exec/src/main.rs b/codex-rs/exec/src/main.rs index 3a8e1f9411..03ee533ea9 100644 --- a/codex-rs/exec/src/main.rs +++ b/codex-rs/exec/src/main.rs @@ -10,6 +10,7 @@ //! This allows us to ship a completely separate set of functionality as part //! of the `codex-exec` binary. use clap::Parser; +use codex_arg0::arg0_dispatch_or_else; use codex_common::CliConfigOverrides; use codex_exec::Cli; use codex_exec::run_main; @@ -24,7 +25,7 @@ struct TopCli { } fn main() -> anyhow::Result<()> { - codex_linux_sandbox::run_with_sandbox(|codex_linux_sandbox_exe| async move { + arg0_dispatch_or_else(|codex_linux_sandbox_exe| async move { let top_cli = TopCli::parse(); // Merge root-level overrides into inner CLI struct so downstream logic remains unchanged. let mut inner = top_cli.inner; diff --git a/codex-rs/linux-sandbox/Cargo.toml b/codex-rs/linux-sandbox/Cargo.toml index 5c2dea6083..4b173ea17a 100644 --- a/codex-rs/linux-sandbox/Cargo.toml +++ b/codex-rs/linux-sandbox/Cargo.toml @@ -14,15 +14,16 @@ path = "src/lib.rs" [lints] workspace = true -[dependencies] +[target.'cfg(target_os = "linux")'.dependencies] anyhow = "1" clap = { version = "4", features = ["derive"] } codex-common = { path = "../common", features = ["cli"] } codex-core = { path = "../core" } -dotenvy = "0.15.7" -tokio = { version = "1", features = ["rt-multi-thread"] } +libc = "0.2.172" +landlock = "0.4.1" +seccompiler = "0.5.0" -[dev-dependencies] +[target.'cfg(target_os = "linux")'.dev-dependencies] tempfile = "3" tokio = { version = "1", features = [ "io-std", @@ -31,8 +32,3 @@ tokio = { version = "1", features = [ "rt-multi-thread", "signal", ] } - -[target.'cfg(target_os = "linux")'.dependencies] -libc = "0.2.172" -landlock = "0.4.1" -seccompiler = "0.5.0" diff --git a/codex-rs/mcp-server/Cargo.toml b/codex-rs/mcp-server/Cargo.toml index 1088b92481..488ee6a67c 100644 --- a/codex-rs/mcp-server/Cargo.toml +++ b/codex-rs/mcp-server/Cargo.toml @@ -16,8 +16,8 @@ workspace = true [dependencies] anyhow = "1" +codex-arg0 = { path = "../arg0" } codex-core = { path = "../core" } -codex-linux-sandbox = { path = "../linux-sandbox" } mcp-types = { path = "../mcp-types" } schemars = "0.8.22" serde = { version = "1", features = ["derive"] } diff --git a/codex-rs/mcp-server/src/main.rs b/codex-rs/mcp-server/src/main.rs index 51c46c44d2..60ddeeab41 100644 --- a/codex-rs/mcp-server/src/main.rs +++ b/codex-rs/mcp-server/src/main.rs @@ -1,7 +1,8 @@ +use codex_arg0::arg0_dispatch_or_else; use codex_mcp_server::run_main; fn main() -> anyhow::Result<()> { - codex_linux_sandbox::run_with_sandbox(|codex_linux_sandbox_exe| async move { + arg0_dispatch_or_else(|codex_linux_sandbox_exe| async move { run_main(codex_linux_sandbox_exe).await?; Ok(()) }) diff --git a/codex-rs/tui/Cargo.toml b/codex-rs/tui/Cargo.toml index 9d73e3b386..9161cfbb35 100644 --- a/codex-rs/tui/Cargo.toml +++ b/codex-rs/tui/Cargo.toml @@ -19,6 +19,7 @@ anyhow = "1" base64 = "0.22.1" clap = { version = "4", features = ["derive"] } codex-ansi-escape = { path = "../ansi-escape" } +codex-arg0 = { path = "../arg0" } codex-core = { path = "../core" } codex-common = { path = "../common", features = [ "cli", @@ -26,7 +27,6 @@ codex-common = { path = "../common", features = [ "sandbox_summary", ] } codex-file-search = { path = "../file-search" } -codex-linux-sandbox = { path = "../linux-sandbox" } codex-login = { path = "../login" } color-eyre = "0.6.3" crossterm = { version = "0.28.1", features = ["bracketed-paste"] } diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index bdfb6a23e2..6a1bb526ce 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -127,10 +127,6 @@ impl ChatComposer<'_> { .on_entry_response(log_id, offset, entry, &mut self.textarea) } - pub fn set_input_focus(&mut self, has_focus: bool) { - self.update_border(has_focus); - } - pub fn handle_paste(&mut self, pasted: String) -> bool { let char_count = pasted.chars().count(); if char_count > LARGE_PASTE_CHAR_THRESHOLD { @@ -638,13 +634,6 @@ impl ChatComposer<'_> { .border_style(bs.border_style), ); } - - pub(crate) fn is_popup_visible(&self) -> bool { - match self.active_popup { - ActivePopup::Command(_) | ActivePopup::File(_) => true, - ActivePopup::None => false, - } - } } impl WidgetRef for &ChatComposer<'_> { diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index ebec534f21..0ddb36f635 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -104,12 +104,6 @@ impl BottomPane<'_> { } } - /// Update the UI to reflect whether this `BottomPane` has input focus. - pub(crate) fn set_input_focus(&mut self, has_focus: bool) { - self.has_input_focus = has_focus; - self.composer.set_input_focus(has_focus); - } - pub(crate) fn show_ctrl_c_quit_hint(&mut self) { self.ctrl_c_quit_hint = true; self.composer @@ -203,11 +197,6 @@ impl BottomPane<'_> { self.app_event_tx.send(AppEvent::RequestRedraw) } - /// Returns true when a popup inside the composer is visible. - pub(crate) fn is_popup_visible(&self) -> bool { - self.active_view.is_none() && self.composer.is_popup_visible() - } - // --- History helpers --- pub(crate) fn set_history_metadata(&mut self, log_id: u64, entry_count: usize) { diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 6ca8d4c4be..5e839d1419 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -45,7 +45,6 @@ pub(crate) struct ChatWidget<'a> { codex_op_tx: UnboundedSender, conversation_history: ConversationHistoryWidget, bottom_pane: BottomPane<'a>, - input_focus: InputFocus, config: Config, initial_user_message: Option, token_usage: TokenUsage, @@ -56,12 +55,6 @@ pub(crate) struct ChatWidget<'a> { answer_buffer: String, } -#[derive(Clone, Copy, Eq, PartialEq)] -enum InputFocus { - HistoryPane, - BottomPane, -} - struct UserMessage { text: String, image_paths: Vec, @@ -137,7 +130,6 @@ impl ChatWidget<'_> { app_event_tx, has_input_focus: true, }), - input_focus: InputFocus::BottomPane, config, initial_user_message: create_initial_user_message( initial_prompt.unwrap_or_default(), @@ -151,44 +143,17 @@ impl ChatWidget<'_> { pub(crate) fn handle_key_event(&mut self, key_event: KeyEvent) { self.bottom_pane.clear_ctrl_c_quit_hint(); - // Special-case : normally toggles focus between history and bottom panes. - // However, when the slash-command popup is visible we forward the key - // to the bottom pane so it can handle auto-completion. - if matches!(key_event.code, crossterm::event::KeyCode::Tab) - && !self.bottom_pane.is_popup_visible() - { - self.input_focus = match self.input_focus { - InputFocus::HistoryPane => InputFocus::BottomPane, - InputFocus::BottomPane => InputFocus::HistoryPane, - }; - self.conversation_history - .set_input_focus(self.input_focus == InputFocus::HistoryPane); - self.bottom_pane - .set_input_focus(self.input_focus == InputFocus::BottomPane); - self.request_redraw(); - return; - } - match self.input_focus { - InputFocus::HistoryPane => { - let needs_redraw = self.conversation_history.handle_key_event(key_event); - if needs_redraw { - self.request_redraw(); - } + match self.bottom_pane.handle_key_event(key_event) { + InputResult::Submitted(text) => { + self.submit_user_message(text.into()); } - InputFocus::BottomPane => match self.bottom_pane.handle_key_event(key_event) { - InputResult::Submitted(text) => { - self.submit_user_message(text.into()); - } - InputResult::None => {} - }, + InputResult::None => {} } } pub(crate) fn handle_paste(&mut self, text: String) { - if matches!(self.input_focus, InputFocus::BottomPane) { - self.bottom_pane.handle_paste(text); - } + self.bottom_pane.handle_paste(text); } /// Emits the last entry's plain lines from conversation_history, if any. diff --git a/codex-rs/tui/src/conversation_history_widget.rs b/codex-rs/tui/src/conversation_history_widget.rs index d8035eff64..dede0caf5f 100644 --- a/codex-rs/tui/src/conversation_history_widget.rs +++ b/codex-rs/tui/src/conversation_history_widget.rs @@ -5,8 +5,6 @@ use crate::history_cell::PatchEventType; use codex_core::config::Config; use codex_core::protocol::FileChange; use codex_core::protocol::SessionConfiguredEvent; -use crossterm::event::KeyCode; -use crossterm::event::KeyEvent; use ratatui::prelude::*; use ratatui::style::Style; use ratatui::widgets::*; @@ -47,33 +45,6 @@ impl ConversationHistoryWidget { } } - pub(crate) fn set_input_focus(&mut self, has_input_focus: bool) { - self.has_input_focus = has_input_focus; - } - - /// Returns true if it needs a redraw. - pub(crate) fn handle_key_event(&mut self, key_event: KeyEvent) -> bool { - match key_event.code { - KeyCode::Up | KeyCode::Char('k') => { - self.scroll_up(1); - true - } - KeyCode::Down | KeyCode::Char('j') => { - self.scroll_down(1); - true - } - KeyCode::PageUp | KeyCode::Char('b') => { - self.scroll_page_up(); - true - } - KeyCode::PageDown | KeyCode::Char(' ') => { - self.scroll_page_down(); - true - } - _ => false, - } - } - /// Negative delta scrolls up; positive delta scrolls down. pub(crate) fn scroll(&mut self, delta: i32) { match delta.cmp(&0) { @@ -122,53 +93,6 @@ impl ConversationHistoryWidget { } } - /// Scroll up by one full viewport height (Page Up). - fn scroll_page_up(&mut self) { - let viewport_height = self.last_viewport_height.get().max(1); - - // If we are currently in the "stick to bottom" mode, first convert the - // implicit scroll position (`usize::MAX`) into an explicit offset that - // represents the very bottom of the scroll region. This mirrors the - // logic from `scroll_up()`. - if self.scroll_position == usize::MAX { - self.scroll_position = self - .num_rendered_lines - .get() - .saturating_sub(viewport_height); - } - - // Move up by a full page. - self.scroll_position = self.scroll_position.saturating_sub(viewport_height); - } - - /// Scroll down by one full viewport height (Page Down). - fn scroll_page_down(&mut self) { - // Nothing to do if we're already stuck to the bottom. - if self.scroll_position == usize::MAX { - return; - } - - let viewport_height = self.last_viewport_height.get().max(1); - let num_lines = self.num_rendered_lines.get(); - - // Calculate the maximum explicit scroll offset that is still within - // range. This matches the logic in `scroll_down()` and the render - // method. - let max_scroll = num_lines.saturating_sub(viewport_height); - - // Attempt to move down by a full page. - let new_pos = self.scroll_position.saturating_add(viewport_height); - - if new_pos >= max_scroll { - // We have reached (or passed) the bottom – switch back to - // automatic stick‑to‑bottom mode so that subsequent output keeps - // the viewport pinned. - self.scroll_position = usize::MAX; - } else { - self.scroll_position = new_pos; - } - } - pub fn scroll_to_bottom(&mut self) { self.scroll_position = usize::MAX; } diff --git a/codex-rs/tui/src/main.rs b/codex-rs/tui/src/main.rs index fdb3cdaf82..480e56e88e 100644 --- a/codex-rs/tui/src/main.rs +++ b/codex-rs/tui/src/main.rs @@ -1,4 +1,5 @@ use clap::Parser; +use codex_arg0::arg0_dispatch_or_else; use codex_common::CliConfigOverrides; use codex_tui::Cli; use codex_tui::run_main; @@ -13,7 +14,7 @@ struct TopCli { } fn main() -> anyhow::Result<()> { - codex_linux_sandbox::run_with_sandbox(|codex_linux_sandbox_exe| async move { + arg0_dispatch_or_else(|codex_linux_sandbox_exe| async move { let top_cli = TopCli::parse(); let mut inner = top_cli.inner; inner