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}