From 9518387ddb6f314ad8585203edcc16a5cf9918f3 Mon Sep 17 00:00:00 2001 From: Abhishek Bhardwaj Date: Fri, 12 Sep 2025 22:33:09 -0700 Subject: [PATCH] feature: Add "!cmd" user shell execution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - protocol: add Op::RunUserShellCommand to model a user-initiated one-off command - core: handle new Op by spawning a cancellable task that runs the command using the user’s default shell; stream output via ExecCommand* events; send TaskStarted/TaskComplete; track as current_task so Interrupt works - tui: detect leading '!' in composer submission and dispatch Op::RunUserShellCommand instead of sending a user message No changes to sandbox env var behavior; uses existing exec pipeline and event types. --- codex-rs/core/src/codex.rs | 100 ++++++++++++++++ codex-rs/core/tests/suite/mod.rs | 1 + codex-rs/core/tests/suite/user_shell_cmd.rs | 112 ++++++++++++++++++ .../src/event_processor_with_human_output.rs | 1 + codex-rs/protocol/src/protocol.rs | 29 +++++ codex-rs/tui/src/chatwidget.rs | 21 +++- codex-rs/tui/src/chatwidget/tests.rs | 3 + codex-rs/tui/src/history_cell.rs | 55 +++++++-- 8 files changed, 307 insertions(+), 15 deletions(-) create mode 100644 codex-rs/core/tests/suite/user_shell_cmd.rs diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 9aa8486a0d..f2e02bbe35 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -128,6 +128,7 @@ use codex_protocol::models::ResponseInputItem; use codex_protocol::models::ResponseItem; use codex_protocol::models::ShellToolCallParams; use codex_protocol::protocol::InitialHistory; +use uuid::Uuid; mod compact; @@ -770,6 +771,7 @@ impl Session { command_for_display, cwd, apply_patch, + user_initiated_shell_command, } = exec_command_context; let msg = match apply_patch { Some(ApplyPatchCommandContext { @@ -792,6 +794,7 @@ impl Session { .into_iter() .map(Into::into) .collect(), + user_initiated_shell_command, }), }; let event = Event { @@ -1029,6 +1032,7 @@ pub(crate) struct ExecCommandContext { pub(crate) command_for_display: Vec, pub(crate) cwd: PathBuf, pub(crate) apply_patch: Option, + pub(crate) user_initiated_shell_command: bool, } #[derive(Clone, Debug)] @@ -1474,6 +1478,101 @@ async fn submission_loop( }; sess.send_event(event).await; } + Op::RunUserShellCommand { command } => { + // Spawn a cancellable one-off shell command task so we can process + // further Ops (e.g., Interrupt) while it runs. + let sess_clone = sess.clone(); + let turn_context = Arc::clone(&turn_context); + let sub_id = sub.id.clone(); + let handle = tokio::spawn(async move { + // Announce a running task so the UI can show a spinner and block input. + let event = Event { + id: sub_id.clone(), + msg: EventMsg::TaskStarted(TaskStartedEvent { + model_context_window: turn_context.client.get_model_context_window(), + }), + }; + sess_clone.send_event(event).await; + + // Build a shell invocation in the user's default shell. + let shell_invocation = sess_clone + .user_shell + // Why we pass a ["bash", "-lc",