Skip to main content

systemprompt_cli/shared/command_result/
render.rs

1//! Terminal rendering for [`CommandOutput`].
2//!
3//! `json`/`yaml` formats emit the [`CliArtifact`] verbatim; `table` renders per
4//! artifact variant for an interactive terminal.
5//!
6//! Copyright (c) systemprompt.io — Business Source License 1.1.
7//! See <https://systemprompt.io> for licensing details.
8
9use systemprompt_logging::CliService;
10use systemprompt_models::artifacts::{
11    ChartArtifact, CliArtifact, ListArtifact, PresentationCardArtifact, TableArtifact,
12};
13
14use super::CommandOutput;
15use crate::cli_settings::{CliConfig, OutputFormat};
16
17pub fn render_result(result: &CommandOutput, config: &CliConfig) {
18    if result.should_skip_render() {
19        return;
20    }
21
22    match config.output_format() {
23        OutputFormat::Json => CliService::json(result.artifact()),
24        OutputFormat::Yaml => CliService::yaml(result.artifact()),
25        OutputFormat::Table => render_terminal(result),
26    }
27}
28
29fn render_terminal(result: &CommandOutput) {
30    if let Some(title) = result.title() {
31        CliService::section(title);
32    }
33
34    match result.artifact() {
35        CliArtifact::Text { artifact } => {
36            if result.title().is_none()
37                && let Some(title) = &artifact.title
38            {
39                CliService::section(title);
40            }
41            CliService::output(&artifact.content);
42        },
43        CliArtifact::CopyPasteText { artifact } => {
44            if result.title().is_none()
45                && let Some(title) = &artifact.title
46            {
47                CliService::section(title);
48            }
49            CliService::output(&artifact.content);
50        },
51        CliArtifact::Table { artifact } => render_table(artifact),
52        CliArtifact::List { artifact } => render_list(artifact),
53        CliArtifact::PresentationCard { artifact } => render_card(artifact),
54        CliArtifact::Dashboard { artifact } => {
55            if result.title().is_none() {
56                CliService::section(&artifact.title);
57            }
58            if let Some(description) = &artifact.description {
59                CliService::output(description);
60            }
61        },
62        CliArtifact::Chart { artifact } => render_chart(artifact),
63        CliArtifact::Audio { artifact } => CliService::output(&artifact.src),
64        CliArtifact::Image { artifact } => CliService::output(&artifact.src),
65        CliArtifact::Video { artifact } => CliService::output(&artifact.src),
66        CliArtifact::Message { artifact } => {
67            for line in &artifact.messages {
68                match line.level.as_str() {
69                    "success" => CliService::success(&line.text),
70                    "warning" => CliService::warning(&line.text),
71                    "error" => CliService::error(&line.text),
72                    _ => CliService::info(&line.text),
73                }
74            }
75        },
76    }
77}
78
79fn render_table(artifact: &TableArtifact) {
80    let headers: Vec<&str> = artifact
81        .columns
82        .iter()
83        .map(|c| c.label.as_deref().unwrap_or(&c.name))
84        .collect();
85
86    let rows: Vec<Vec<String>> = artifact
87        .items
88        .iter()
89        .map(|item| {
90            artifact
91                .columns
92                .iter()
93                .map(|col| item.get(&col.name).map_or_else(String::new, cell_display))
94                .collect()
95        })
96        .collect();
97
98    CliService::table(&headers, &rows);
99}
100
101fn render_list(artifact: &ListArtifact) {
102    for item in &artifact.items {
103        CliService::subsection(&item.title);
104        if !item.summary.is_empty() {
105            CliService::output(&item.summary);
106        }
107        if !item.link.is_empty() {
108            CliService::output(&item.link);
109        }
110    }
111}
112
113fn render_card(artifact: &PresentationCardArtifact) {
114    CliService::section(&artifact.title);
115    if let Some(subtitle) = &artifact.subtitle {
116        CliService::output(subtitle);
117    }
118    for section in &artifact.sections {
119        CliService::subsection(&section.heading);
120        CliService::output(&section.content_display());
121    }
122}
123
124fn render_chart(artifact: &ChartArtifact) {
125    let mut headers: Vec<&str> = vec!["label"];
126    for dataset in &artifact.datasets {
127        headers.push(&dataset.label);
128    }
129
130    let rows: Vec<Vec<String>> = artifact
131        .labels
132        .iter()
133        .enumerate()
134        .map(|(row, label)| {
135            let mut cells = vec![label.clone()];
136            for dataset in &artifact.datasets {
137                cells.push(
138                    dataset
139                        .data
140                        .get(row)
141                        .map_or_else(String::new, ToString::to_string),
142                );
143            }
144            cells
145        })
146        .collect();
147
148    CliService::table(&headers, &rows);
149}
150
151fn cell_display(value: &serde_json::Value) -> String {
152    match value {
153        serde_json::Value::String(s) => s.clone(),
154        serde_json::Value::Null => String::new(),
155        other => other.to_string(),
156    }
157}