Skip to content

Commit 6915ba2

Browse files
authored
feat: better UX during refusal (#5260)
<img width="568" height="169" alt="Screenshot 2025-10-16 at 18 28 05" src="https://github.com/user-attachments/assets/f42e8d6d-b7de-4948-b291-a5fbb50b1312" />
1 parent 50f53e7 commit 6915ba2

File tree

6 files changed

+40
-11
lines changed

6 files changed

+40
-11
lines changed

codex-rs/core/src/codex.rs

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -916,6 +916,7 @@ impl Session {
916916
duration,
917917
exit_code,
918918
timed_out: _,
919+
..
919920
} = output;
920921
// Send full stdout/stderr to clients; do not truncate.
921922
let stdout = stdout.text.clone();
@@ -980,15 +981,28 @@ impl Session {
980981
let sub_id = context.sub_id.clone();
981982
let call_id = context.call_id.clone();
982983

983-
self.on_exec_command_begin(turn_diff_tracker.clone(), context.clone())
984-
.await;
985-
984+
let begin_turn_diff = turn_diff_tracker.clone();
985+
let begin_context = context.clone();
986+
let session = self;
986987
let result = self
987988
.services
988989
.executor
989-
.run(request, self, approval_policy, &context)
990+
.run(request, self, approval_policy, &context, move || {
991+
let turn_diff = begin_turn_diff.clone();
992+
let ctx = begin_context.clone();
993+
async move {
994+
session.on_exec_command_begin(turn_diff, ctx).await;
995+
}
996+
})
990997
.await;
991998

999+
if matches!(
1000+
&result,
1001+
Err(ExecError::Function(FunctionCallError::Denied(_)))
1002+
) {
1003+
return result;
1004+
}
1005+
9921006
let normalized = normalize_exec_result(&result);
9931007
let borrowed = normalized.event_output();
9941008

@@ -2262,7 +2276,8 @@ async fn try_run_turn(
22622276
response: Some(response),
22632277
});
22642278
}
2265-
Err(FunctionCallError::RespondToModel(message)) => {
2279+
Err(FunctionCallError::RespondToModel(message))
2280+
| Err(FunctionCallError::Denied(message)) => {
22662281
let response = ResponseInputItem::FunctionCallOutput {
22672282
call_id: String::new(),
22682283
output: FunctionCallOutputPayload {

codex-rs/core/src/executor/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,5 +60,9 @@ pub mod errors {
6060
pub(crate) fn rejection(msg: impl Into<String>) -> Self {
6161
FunctionCallError::RespondToModel(msg.into()).into()
6262
}
63+
64+
pub(crate) fn denied(msg: impl Into<String>) -> Self {
65+
FunctionCallError::Denied(msg.into()).into()
66+
}
6367
}
6468
}

codex-rs/core/src/executor/runner.rs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use std::future::Future;
12
use std::path::PathBuf;
23
use std::sync::Arc;
34
use std::sync::RwLock;
@@ -74,13 +75,18 @@ impl Executor {
7475
/// Runs a prepared execution request end-to-end: prepares parameters, decides on
7576
/// sandbox placement (prompting the user when necessary), launches the command,
7677
/// and lets the backend post-process the final output.
77-
pub(crate) async fn run(
78+
pub(crate) async fn run<F, Fut>(
7879
&self,
7980
mut request: ExecutionRequest,
8081
session: &Session,
8182
approval_policy: AskForApproval,
8283
context: &ExecCommandContext,
83-
) -> Result<ExecToolCallOutput, ExecError> {
84+
on_exec_begin: F,
85+
) -> Result<ExecToolCallOutput, ExecError>
86+
where
87+
F: FnOnce() -> Fut,
88+
Fut: Future<Output = ()>,
89+
{
8490
if matches!(request.mode, ExecutionMode::Shell) {
8591
request.params =
8692
maybe_translate_shell_command(request.params, session, request.use_shell_profile);
@@ -119,7 +125,7 @@ impl Executor {
119125
if sandbox_decision.record_session_approval {
120126
self.approval_cache.insert(request.approval_command.clone());
121127
}
122-
128+
on_exec_begin().await;
123129
// Step 4: Launch the command within the chosen sandbox.
124130
let first_attempt = self
125131
.spawn(
@@ -210,7 +216,7 @@ impl Executor {
210216
Ok(retry_output)
211217
}
212218
ReviewDecision::Denied | ReviewDecision::Abort => {
213-
Err(ExecError::rejection("exec command rejected by user"))
219+
Err(ExecError::denied("exec command rejected by user"))
214220
}
215221
}
216222
}
@@ -301,7 +307,8 @@ pub(crate) fn normalize_exec_result(
301307
}
302308
Err(err) => {
303309
let message = match err {
304-
ExecError::Function(FunctionCallError::RespondToModel(msg)) => msg.clone(),
310+
ExecError::Function(FunctionCallError::RespondToModel(msg))
311+
| ExecError::Function(FunctionCallError::Denied(msg)) => msg.clone(),
305312
ExecError::Codex(e) => get_error_message_ui(e),
306313
err => err.to_string(),
307314
};

codex-rs/core/src/executor/sandbox.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ async fn select_shell_sandbox(
149149
ReviewDecision::Approved => Ok(SandboxDecision::user_override(false)),
150150
ReviewDecision::ApprovedForSession => Ok(SandboxDecision::user_override(true)),
151151
ReviewDecision::Denied | ReviewDecision::Abort => {
152-
Err(ExecError::rejection("exec command rejected by user"))
152+
Err(ExecError::denied("exec command rejected by user"))
153153
}
154154
}
155155
}

codex-rs/core/src/function_tool.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ use thiserror::Error;
44
pub enum FunctionCallError {
55
#[error("{0}")]
66
RespondToModel(String),
7+
#[error("{0}")]
8+
Denied(String),
79
#[error("LocalShellCall without call_id or id")]
810
MissingLocalShellCallId,
911
#[error("Fatal error: {0}")]

codex-rs/core/src/tools/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ fn truncate_function_error(err: FunctionCallError) -> FunctionCallError {
238238
FunctionCallError::RespondToModel(msg) => {
239239
FunctionCallError::RespondToModel(format_exec_output(&msg))
240240
}
241+
FunctionCallError::Denied(msg) => FunctionCallError::Denied(format_exec_output(&msg)),
241242
FunctionCallError::Fatal(msg) => FunctionCallError::Fatal(format_exec_output(&msg)),
242243
other => other,
243244
}

0 commit comments

Comments
 (0)