Skip to main content

saya_cli/render/
mod.rs

1use saya_config::OutputFormat;
2use saya_types::{QueryResult, SchemaTree};
3use serde::Serialize;
4mod render_delta;
5mod render_json;
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum RenderFormat {
8    Text,
9    Json,
10    Ndjson,
11}
12impl From<crate::cli::FormatArg> for RenderFormat {
13    fn from(value: crate::cli::FormatArg) -> Self {
14        match value {
15            crate::cli::FormatArg::Text => Self::Text,
16            crate::cli::FormatArg::Json => Self::Json,
17            crate::cli::FormatArg::Ndjson => Self::Ndjson,
18        }
19    }
20}
21impl From<OutputFormat> for RenderFormat {
22    fn from(value: OutputFormat) -> Self {
23        match value {
24            OutputFormat::Text => Self::Text,
25            OutputFormat::Json => Self::Json,
26            OutputFormat::Ndjson => Self::Ndjson,
27        }
28    }
29}
30#[derive(Debug, Clone, Serialize, PartialEq)]
31#[serde(tag = "event", rename_all = "snake_case")]
32pub enum TerminalEvent {
33    AssistantText {
34        text: String,
35    },
36    ToolRequested {
37        name: String,
38        #[serde(default, skip_serializing_if = "Option::is_none")]
39        detail: Option<String>,
40    },
41    ToolCompleted {
42        name: String,
43        summary: String,
44    },
45    ToolDenied {
46        name: String,
47        reason: String,
48    },
49    Complete,
50    Result {
51        message: String,
52    },
53    QueryResult {
54        result: QueryResult,
55    },
56    Schema {
57        schema: SchemaTree,
58    },
59    NotImplemented {
60        feature: String,
61    },
62    Diagnostic {
63        message: String,
64    },
65    Error {
66        message: String,
67    },
68}
69#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct Rendered {
71    pub stdout: String,
72    pub stderr: String,
73}
74
75pub fn render_event(event: &TerminalEvent, format: RenderFormat) -> Rendered {
76    match format {
77        RenderFormat::Text => text_event(event),
78        RenderFormat::Json | RenderFormat::Ndjson => render_json::render(event),
79    }
80}
81
82fn text_event(event: &TerminalEvent) -> Rendered {
83    match event {
84        TerminalEvent::Diagnostic { message } | TerminalEvent::Error { message } => Rendered {
85            stdout: String::new(),
86            stderr: format!("{message}\n"),
87        },
88        TerminalEvent::AssistantText { text } => render_delta::text(text),
89        TerminalEvent::ToolRequested { name, detail } => Rendered {
90            stdout: match detail {
91                Some(detail) => format!("Using read-only tool: {name}\n  {detail}\n"),
92                None => format!("Using read-only tool: {name}\n"),
93            },
94            stderr: String::new(),
95        },
96        TerminalEvent::ToolCompleted { name, summary } => Rendered {
97            stdout: format!("{name}: {summary}\n"),
98            stderr: String::new(),
99        },
100        TerminalEvent::ToolDenied { name, reason } => Rendered {
101            stdout: format!("Approval denied for {name}: {reason}\n"),
102            stderr: String::new(),
103        },
104        TerminalEvent::Complete => Rendered {
105            stdout: "\n".into(),
106            stderr: String::new(),
107        },
108        TerminalEvent::Result { message } => Rendered {
109            stdout: format!("{message}\n"),
110            stderr: String::new(),
111        },
112        TerminalEvent::QueryResult { result } => Rendered {
113            stdout: query_text(result),
114            stderr: String::new(),
115        },
116        TerminalEvent::Schema { schema } => Rendered {
117            stdout: format!("{}\n", schema_text(schema)),
118            stderr: String::new(),
119        },
120        TerminalEvent::NotImplemented { feature } => Rendered {
121            stdout: format!("Not implemented: {feature}\n"),
122            stderr: String::new(),
123        },
124    }
125}
126
127fn query_text(result: &QueryResult) -> String {
128    let mut output = result.columns.join("\t");
129    if !output.is_empty() {
130        output.push('\n');
131    }
132    for row in &result.rows {
133        let value = match row {
134            serde_json::Value::Array(values) => values
135                .iter()
136                .map(display_value)
137                .collect::<Vec<_>>()
138                .join("\t"),
139            value => display_value(value),
140        };
141        output.push_str(&value);
142        output.push('\n');
143    }
144    if result.truncated {
145        output.push_str("[truncated]\n");
146    }
147    output
148}
149
150fn display_value(value: &serde_json::Value) -> String {
151    match value {
152        serde_json::Value::String(value) => value.clone(),
153        value => value.to_string(),
154    }
155}
156
157fn schema_text(schema: &SchemaTree) -> String {
158    schema
159        .databases
160        .iter()
161        .flat_map(|database| {
162            database.schemas.iter().flat_map(move |schema| {
163                schema
164                    .tables
165                    .iter()
166                    .map(move |table| format!("{}.{}.{}", database.name, schema.name, table.name))
167            })
168        })
169        .collect::<Vec<_>>()
170        .join("\n")
171}