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 { text: String },
34    ToolRequested { name: String },
35    ToolCompleted { name: String, summary: String },
36    ToolDenied { name: String, reason: String },
37    Complete,
38    Result { message: String },
39    QueryResult { result: QueryResult },
40    Schema { schema: SchemaTree },
41    NotImplemented { feature: String },
42    Diagnostic { message: String },
43    Error { message: String },
44}
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct Rendered {
47    pub stdout: String,
48    pub stderr: String,
49}
50
51pub fn render_event(event: &TerminalEvent, format: RenderFormat) -> Rendered {
52    match format {
53        RenderFormat::Text => text_event(event),
54        RenderFormat::Json | RenderFormat::Ndjson => render_json::render(event),
55    }
56}
57
58fn text_event(event: &TerminalEvent) -> Rendered {
59    match event {
60        TerminalEvent::Diagnostic { message } | TerminalEvent::Error { message } => Rendered {
61            stdout: String::new(),
62            stderr: format!("{message}\n"),
63        },
64        TerminalEvent::AssistantText { text } => render_delta::text(text),
65        TerminalEvent::ToolRequested { name } => Rendered {
66            stdout: format!("Using read-only tool: {name}\n"),
67            stderr: String::new(),
68        },
69        TerminalEvent::ToolCompleted { name, summary } => Rendered {
70            stdout: format!("{name}: {summary}\n"),
71            stderr: String::new(),
72        },
73        TerminalEvent::ToolDenied { name, reason } => Rendered {
74            stdout: format!("Approval denied for {name}: {reason}\n"),
75            stderr: String::new(),
76        },
77        TerminalEvent::Complete => Rendered {
78            stdout: "\n".into(),
79            stderr: String::new(),
80        },
81        TerminalEvent::Result { message } => Rendered {
82            stdout: format!("{message}\n"),
83            stderr: String::new(),
84        },
85        TerminalEvent::QueryResult { result } => Rendered {
86            stdout: query_text(result),
87            stderr: String::new(),
88        },
89        TerminalEvent::Schema { schema } => Rendered {
90            stdout: format!("{}\n", schema_text(schema)),
91            stderr: String::new(),
92        },
93        TerminalEvent::NotImplemented { feature } => Rendered {
94            stdout: format!("Not implemented: {feature}\n"),
95            stderr: String::new(),
96        },
97    }
98}
99
100fn query_text(result: &QueryResult) -> String {
101    let mut output = result.columns.join("\t");
102    if !output.is_empty() {
103        output.push('\n');
104    }
105    for row in &result.rows {
106        let value = match row {
107            serde_json::Value::Array(values) => values
108                .iter()
109                .map(display_value)
110                .collect::<Vec<_>>()
111                .join("\t"),
112            value => display_value(value),
113        };
114        output.push_str(&value);
115        output.push('\n');
116    }
117    if result.truncated {
118        output.push_str("[truncated]\n");
119    }
120    output
121}
122
123fn display_value(value: &serde_json::Value) -> String {
124    match value {
125        serde_json::Value::String(value) => value.clone(),
126        value => value.to_string(),
127    }
128}
129
130fn schema_text(schema: &SchemaTree) -> String {
131    schema
132        .databases
133        .iter()
134        .flat_map(|database| {
135            database.schemas.iter().flat_map(move |schema| {
136                schema
137                    .tables
138                    .iter()
139                    .map(move |table| format!("{}.{}.{}", database.name, schema.name, table.name))
140            })
141        })
142        .collect::<Vec<_>>()
143        .join("\n")
144}