Skip to main content

aft/
ndjson_text.rs

1//! NDJSON text builder for standalone command responses.
2//!
3//! Provides the authorized NDJSON call-site for `render_trailer` (R13).
4//! If an envelope is present in the response, renders the trailer and suppresses
5//! legacy clauses; if absent, preserves the pre-spec text byte-unchanged.
6
7use crate::list_envelope::{derive_wire_key, render_trailer, ListEnvelope};
8use serde_json::Value;
9
10/// Render agent-facing text for an NDJSON response.
11///
12/// If an envelope is present for the given list surface, renders the trailer.
13/// If absent, returns `base_text` unchanged.
14pub fn build_ndjson_text(
15    base_text: &str,
16    data: &Value,
17    list_id: Option<&str>,
18    is_text_surface: bool,
19) -> String {
20    let Some(id) = list_id else {
21        return base_text.to_string();
22    };
23
24    let wire_key = derive_wire_key(id, is_text_surface);
25    let envelope_val = data
26        .get(&wire_key)
27        .or_else(|| data.get("payload").and_then(|p| p.get(&wire_key)))
28        .or_else(|| {
29            data.get("details")
30                .or_else(|| data.get("payload").and_then(|p| p.get("details")))
31                .and_then(|d| d.get(&wire_key))
32        });
33
34    let Some(val) = envelope_val else {
35        return base_text.to_string();
36    };
37
38    let Ok(envelope) = serde_json::from_value::<ListEnvelope>(val.clone()) else {
39        return base_text.to_string();
40    };
41
42    if is_text_surface {
43        // For text surfaces such as bash, the trailer is already integrated
44        // into the text payload; pass through unchanged.
45        return base_text.to_string();
46    }
47
48    if let Some(trailer) = render_trailer(&envelope) {
49        if base_text.is_empty() {
50            trailer
51        } else {
52            format!("{base_text}\n\n{trailer}")
53        }
54    } else {
55        base_text.to_string()
56    }
57}