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
This commit is contained in:
Eric Traut 2026-02-27 17:31:17 -08:00 committed by GitHub
parent 70ed6cbc71
commit 6604608bad
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -41,6 +41,7 @@ use owo_colors::Style;
use serde::Deserialize;
use shlex::try_join;
use std::collections::HashMap;
use std::io::IsTerminal;
use std::io::Write;
use std::path::PathBuf;
use std::time::Duration;
@ -869,12 +870,17 @@ impl EventProcessor for EventProcessorWithHumanOutput {
);
}
// If the user has not piped the final message to a file, they will see
// it twice: once written to stderr as part of the normal event
// processing, and once here on stdout. We print the token summary above
// to help break up the output visually in that case.
// In interactive terminals we already emitted the final assistant
// message on stderr during event processing. Preserve stdout emission
// only for non-interactive use so pipes and scripts still receive the
// final message.
#[allow(clippy::print_stdout)]
if let Some(message) = &self.final_message {
if should_print_final_message_to_stdout(
self.final_message.as_deref(),
std::io::stdout().is_terminal(),
std::io::stderr().is_terminal(),
) && let Some(message) = &self.final_message
{
if message.ends_with('\n') {
print!("{message}");
} else {
@ -1025,6 +1031,14 @@ impl EventProcessorWithHumanOutput {
}
}
fn should_print_final_message_to_stdout(
final_message: Option<&str>,
stdout_is_terminal: bool,
stderr_is_terminal: bool,
) -> bool {
final_message.is_some() && !(stdout_is_terminal && stderr_is_terminal)
}
struct AgentJobProgressStats {
processed: usize,
total: usize,
@ -1192,3 +1206,41 @@ fn format_mcp_invocation(invocation: &McpInvocation) -> String {
format!("{fq_tool_name}({args_str})")
}
}
#[cfg(test)]
mod tests {
use super::should_print_final_message_to_stdout;
use pretty_assertions::assert_eq;
#[test]
fn suppresses_final_stdout_message_when_both_streams_are_terminals() {
assert_eq!(
should_print_final_message_to_stdout(Some("hello"), true, true),
false
);
}
#[test]
fn prints_final_stdout_message_when_stdout_is_not_terminal() {
assert_eq!(
should_print_final_message_to_stdout(Some("hello"), false, true),
true
);
}
#[test]
fn prints_final_stdout_message_when_stderr_is_not_terminal() {
assert_eq!(
should_print_final_message_to_stdout(Some("hello"), true, false),
true
);
}
#[test]
fn does_not_print_when_message_is_missing() {
assert_eq!(
should_print_final_message_to_stdout(None, false, false),
false
);
}
}