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}