commit 11044372b4955bf3f63457700fc4f310d87e925a
parent 871d0b548fb3f25cf0b6d4b45a7ae69c7d470a3f
Author: triesap <tyson@radroots.org>
Date: Sat, 27 Jun 2026 07:02:14 +0000
cli: implement quiet terminal rendering
- add essential terminal visibility for quiet failure context
- suppress normal fields, sections, warnings, and next actions in quiet mode
- preserve normal, verbose, and trace rendering behavior
- cover quiet terminal success, failure, and JSON output paths
Diffstat:
4 files changed, 102 insertions(+), 5 deletions(-)
diff --git a/src/out/terminal/errors.rs b/src/out/terminal/errors.rs
@@ -12,7 +12,7 @@ pub fn terminal_error_document(envelope: &OutputEnvelope) -> TerminalDocument {
if let Some(error) = error {
document
.fields
- .push(TerminalField::new("Reason", error.message.clone()));
+ .push(TerminalField::essential("Reason", error.message.clone()));
document
.fields
.push(TerminalField::verbose("Code", error.reason_code.clone()));
diff --git a/src/out/terminal/layout.rs b/src/out/terminal/layout.rs
@@ -22,6 +22,7 @@ impl TerminalSymbol {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TerminalVisibility {
+ Essential,
Normal,
Verbose,
Trace,
@@ -50,6 +51,14 @@ pub struct TerminalField {
}
impl TerminalField {
+ pub fn essential(label: impl Into<String>, value: impl Into<String>) -> Self {
+ Self {
+ label: label.into(),
+ value: value.into(),
+ visibility: TerminalVisibility::Essential,
+ }
+ }
+
pub fn new(label: impl Into<String>, value: impl Into<String>) -> Self {
Self {
label: label.into(),
diff --git a/src/out/terminal/renderer.rs b/src/out/terminal/renderer.rs
@@ -33,9 +33,14 @@ impl Default for TerminalRenderContext {
}
impl TerminalRenderContext {
+ pub fn is_quiet(&self) -> bool {
+ matches!(self.verbosity, TerminalVerbosity::Quiet)
+ }
+
pub fn allows(&self, visibility: TerminalVisibility) -> bool {
match visibility {
- TerminalVisibility::Normal => true,
+ TerminalVisibility::Essential => true,
+ TerminalVisibility::Normal => !self.is_quiet(),
TerminalVisibility::Verbose => {
matches!(
self.verbosity,
@@ -59,14 +64,16 @@ pub fn render_terminal_document(document: &TerminalDocument, cx: &TerminalRender
for section in &document.sections {
write_section(&mut output, section, cx);
}
- if !document.warnings.is_empty() {
+ if !cx.is_quiet() && !document.warnings.is_empty() {
let _ = writeln!(output);
let _ = writeln!(output, "Warnings");
for warning in &document.warnings {
let _ = writeln!(output, " {}: {}", warning.code, warning.message);
}
}
- write_next(&mut output, &document.next);
+ if !cx.is_quiet() {
+ write_next(&mut output, &document.next);
+ }
if let Some(reference) = &document.reference
&& cx.allows(TerminalVisibility::Verbose)
&& !reference.is_empty()
@@ -192,7 +199,9 @@ fn write_reference(output: &mut String, reference: &TerminalReference) {
mod tests {
use super::*;
use crate::out::terminal::actions::TerminalAction;
- use crate::out::terminal::layout::{TerminalField, TerminalHeader, TerminalSymbol};
+ use crate::out::terminal::layout::{
+ TerminalField, TerminalHeader, TerminalSection, TerminalSymbol, TerminalWarning,
+ };
#[test]
fn renders_canonical_receipt_layout() {
@@ -234,4 +243,36 @@ mod tests {
};
assert!(render_terminal_document(&document, &cx).contains("Reference"));
}
+
+ #[test]
+ fn quiet_renders_only_header_and_essential_fields() {
+ let mut document = TerminalDocument::new(TerminalHeader::new(
+ TerminalSymbol::Failure,
+ "Command failed",
+ ))
+ .with_field(TerminalField::essential("Reason", "missing approval"))
+ .with_field(TerminalField::new("State", "not implemented"))
+ .with_section(TerminalSection::lines(
+ "Details",
+ vec!["extra context".to_owned()],
+ ))
+ .with_next(TerminalAction::command(
+ "radroots trade status get ord_test",
+ ));
+ document.warnings = vec![TerminalWarning::new("warn_test", "suppressed")];
+ document.reference = Some(TerminalReference {
+ request_id: Some("req_test".to_owned()),
+ ..TerminalReference::default()
+ });
+
+ let rendered = render_terminal_document(
+ &document,
+ &TerminalRenderContext {
+ verbosity: TerminalVerbosity::Quiet,
+ ..TerminalRenderContext::default()
+ },
+ );
+
+ assert_eq!(rendered, "✕ Command failed\n\n Reason missing approval");
+ }
}
diff --git a/tests/target_cli.rs b/tests/target_cli.rs
@@ -2846,6 +2846,53 @@ fn terminal_global_presentation_flags_are_deterministic() {
}
#[test]
+fn quiet_terminal_success_output_is_header_only() {
+ let output = radroots()
+ .args(["--quiet", "workspace", "get"])
+ .output()
+ .expect("run quiet workspace get");
+
+ assert!(output.status.success());
+ assert!(output.stderr.is_empty());
+ let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
+
+ assert_eq!(stdout, "✓ Workspace ready\n");
+}
+
+#[test]
+fn quiet_terminal_failure_keeps_reason_and_suppresses_optional_body() {
+ let output = radroots()
+ .args(["--quiet", "trade", "event", "watch", "ord_missing"])
+ .output()
+ .expect("run quiet trade event watch");
+
+ assert_eq!(output.status.code(), Some(3));
+ assert!(output.stdout.is_empty());
+ let stderr = String::from_utf8(output.stderr).expect("utf8 stderr");
+
+ assert!(stderr.starts_with("✕ Command failed\n"));
+ assert!(stderr.contains("Reason relay-backed trade event watch is not implemented"));
+ assert!(!stderr.contains("State not implemented"));
+ assert!(!stderr.contains("Next\n"));
+ assert!(serde_json::from_str::<Value>(&stderr).is_err());
+}
+
+#[test]
+fn quiet_does_not_change_json_output_contract() {
+ let output = radroots()
+ .args(["--format", "json", "--quiet", "workspace", "get"])
+ .output()
+ .expect("run quiet json workspace get");
+
+ assert!(output.status.success());
+ assert!(output.stderr.is_empty());
+ let value: Value = serde_json::from_slice(&output.stdout).expect("json envelope");
+
+ assert_eq!(value["operation_id"], "workspace.get");
+ assert_eq!(value["output_format"], "json");
+}
+
+#[test]
fn removed_no_color_flag_is_rejected() {
let output = radroots()
.args(["--no-color", "workspace", "get"])