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, value_to_display};
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| {
94                    item.get(&col.name)
95                        .map_or_else(String::new, value_to_display)
96                })
97                .collect()
98        })
99        .collect();
100
101    CliService::table(&headers, &rows);
102}
103
104fn render_list(artifact: &ListArtifact) {
105    for item in &artifact.items {
106        CliService::subsection(&item.title);
107        if !item.summary.is_empty() {
108            CliService::output(&item.summary);
109        }
110        if !item.link.is_empty() {
111            CliService::output(&item.link);
112        }
113    }
114}
115
116fn render_card(artifact: &PresentationCardArtifact) {
117    CliService::section(&artifact.title);
118    if let Some(subtitle) = &artifact.subtitle {
119        CliService::output(subtitle);
120    }
121    for section in &artifact.sections {
122        CliService::subsection(&section.heading);
123        CliService::output(&section.content);
124    }
125}
126
127fn render_chart(artifact: &ChartArtifact) {
128    let mut headers: Vec<&str> = vec!["label"];
129    for dataset in &artifact.datasets {
130        headers.push(&dataset.label);
131    }
132
133    let rows: Vec<Vec<String>> = artifact
134        .labels
135        .iter()
136        .enumerate()
137        .map(|(row, label)| {
138            let mut cells = vec![label.clone()];
139            for dataset in &artifact.datasets {
140                cells.push(
141                    dataset
142                        .data
143                        .get(row)
144                        .map_or_else(String::new, ToString::to_string),
145                );
146            }
147            cells
148        })
149        .collect();
150
151    CliService::table(&headers, &rows);
152}