Skip to content

Commit 6604608

Browse files
Suppress duplicate assistant output on stdout in interactive sessions (#13082)
Addresses #12566 Summary - stop printing the final assistant message on stdout when the process is running in a terminal so interactive users only see it once - add a helper that gates the stdout emission and cover it with unit tests
1 parent 70ed6cb commit 6604608

File tree

1 file changed

+57
-5
lines changed

1 file changed

+57
-5
lines changed

codex-rs/exec/src/event_processor_with_human_output.rs

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ use owo_colors::Style;
4141
use serde::Deserialize;
4242
use shlex::try_join;
4343
use std::collections::HashMap;
44+
use std::io::IsTerminal;
4445
use std::io::Write;
4546
use std::path::PathBuf;
4647
use std::time::Duration;
@@ -869,12 +870,17 @@ impl EventProcessor for EventProcessorWithHumanOutput {
869870
);
870871
}
871872

872-
// If the user has not piped the final message to a file, they will see
873-
// it twice: once written to stderr as part of the normal event
874-
// processing, and once here on stdout. We print the token summary above
875-
// to help break up the output visually in that case.
873+
// In interactive terminals we already emitted the final assistant
874+
// message on stderr during event processing. Preserve stdout emission
875+
// only for non-interactive use so pipes and scripts still receive the
876+
// final message.
876877
#[allow(clippy::print_stdout)]
877-
if let Some(message) = &self.final_message {
878+
if should_print_final_message_to_stdout(
879+
self.final_message.as_deref(),
880+
std::io::stdout().is_terminal(),
881+
std::io::stderr().is_terminal(),
882+
) && let Some(message) = &self.final_message
883+
{
878884
if message.ends_with('\n') {
879885
print!("{message}");
880886
} else {
@@ -1025,6 +1031,14 @@ impl EventProcessorWithHumanOutput {
10251031
}
10261032
}
10271033

1034+
fn should_print_final_message_to_stdout(
1035+
final_message: Option<&str>,
1036+
stdout_is_terminal: bool,
1037+
stderr_is_terminal: bool,
1038+
) -> bool {
1039+
final_message.is_some() && !(stdout_is_terminal && stderr_is_terminal)
1040+
}
1041+
10281042
struct AgentJobProgressStats {
10291043
processed: usize,
10301044
total: usize,
@@ -1192,3 +1206,41 @@ fn format_mcp_invocation(invocation: &McpInvocation) -> String {
11921206
format!("{fq_tool_name}({args_str})")
11931207
}
11941208
}
1209+
1210+
#[cfg(test)]
1211+
mod tests {
1212+
use super::should_print_final_message_to_stdout;
1213+
use pretty_assertions::assert_eq;
1214+
1215+
#[test]
1216+
fn suppresses_final_stdout_message_when_both_streams_are_terminals() {
1217+
assert_eq!(
1218+
should_print_final_message_to_stdout(Some("hello"), true, true),
1219+
false
1220+
);
1221+
}
1222+
1223+
#[test]
1224+
fn prints_final_stdout_message_when_stdout_is_not_terminal() {
1225+
assert_eq!(
1226+
should_print_final_message_to_stdout(Some("hello"), false, true),
1227+
true
1228+
);
1229+
}
1230+
1231+
#[test]
1232+
fn prints_final_stdout_message_when_stderr_is_not_terminal() {
1233+
assert_eq!(
1234+
should_print_final_message_to_stdout(Some("hello"), true, false),
1235+
true
1236+
);
1237+
}
1238+
1239+
#[test]
1240+
fn does_not_print_when_message_is_missing() {
1241+
assert_eq!(
1242+
should_print_final_message_to_stdout(None, false, false),
1243+
false
1244+
);
1245+
}
1246+
}

0 commit comments

Comments
 (0)