1use colored::Colorize;
2use std::io::Write;
3
4#[derive(Debug, Clone)]
5pub struct OutputConfig {
6 pub format: OutputFormat,
7 pub verbose: bool,
8 pub session_id: Option<String>,
9}
10
11#[derive(Debug, Clone, PartialEq)]
12pub enum OutputFormat {
13 Terminal,
14 Json,
15}
16
17impl OutputConfig {
18 pub fn new(format: OutputFormat, verbose: bool) -> Self {
19 Self {
20 format,
21 verbose,
22 session_id: None,
23 }
24 }
25}
26
27#[derive(Debug, Clone, Copy)]
29pub enum Phase {
30 Observe,
31 Plan,
32 Tool,
33 Verify,
34 Compact,
35 Checkpoint,
36 Ralph,
37 Done,
38 Failed,
39 Session,
40}
41
42impl Phase {
43 fn label(&self) -> &'static str {
44 match self {
45 Phase::Observe => "observe",
46 Phase::Plan => "plan",
47 Phase::Tool => "tool",
48 Phase::Verify => "verify",
49 Phase::Compact => "compact",
50 Phase::Checkpoint => "checkpoint",
51 Phase::Ralph => "ralph",
52 Phase::Done => "done",
53 Phase::Failed => "failed",
54 Phase::Session => "session",
55 }
56 }
57
58 fn colorize(&self, s: &str) -> String {
59 match self {
60 Phase::Observe => s.cyan().to_string(),
61 Phase::Plan => s.yellow().bold().to_string(),
62 Phase::Tool => s.blue().bold().to_string(),
63 Phase::Verify => s.green().to_string(),
64 Phase::Compact => s.magenta().bold().to_string(),
65 Phase::Checkpoint => s.blue().bold().to_string(),
66 Phase::Ralph => s.white().bold().to_string(),
67 Phase::Done => s.green().bold().to_string(),
68 Phase::Failed => s.red().bold().to_string(),
69 Phase::Session => s.cyan().to_string(),
70 }
71 }
72}
73
74#[derive(Clone)]
75pub struct Printer {
76 pub config: OutputConfig,
77}
78
79impl Printer {
80 pub fn new(config: OutputConfig) -> Self {
81 Self { config }
82 }
83
84 pub fn with_session(mut self, session_id: String) -> Self {
85 self.config.session_id = Some(session_id);
86 self
87 }
88
89 pub fn clone_with_session(&self, session_id: String) -> Self {
90 let mut c = self.clone();
91 c.config.session_id = Some(session_id);
92 c
93 }
94
95 pub fn print(&self, phase: Phase, message: &str) {
96 match self.config.format {
97 OutputFormat::Terminal => {
98 let tag = format!("[{}]", phase.label());
99 let colored_tag = phase.colorize(&tag);
100 println!("{} {}", colored_tag, message);
101 }
102 OutputFormat::Json => {
103 let obj = serde_json::json!({
104 "phase": phase.label(),
105 "message": message,
106 "session_id": self.config.session_id,
107 "timestamp": chrono::Utc::now().to_rfc3339(),
108 });
109 println!("{}", obj);
110 }
111 }
112 }
113
114 pub fn print_tool_call(&self, tool: &str, display: &str, result: &str) {
117 match self.config.format {
118 OutputFormat::Terminal => {
119 let colored_tag = Phase::Tool.colorize("[tool]");
120 let tool_name = tool.cyan().to_string();
121 println!("{} {} {}", colored_tag, tool_name, display);
122 let skip = !self.config.verbose
124 && (result == "(executing...)"
125 || result.starts_with("DONE:")
126 || result.starts_with("FAILED:"));
127 if !skip {
128 if result.len() <= 300 {
129 println!(" → {}", result.trim().dimmed());
130 } else {
131 let preview: String = result.chars().take(300).collect();
132 println!(" → {}…", preview.trim().dimmed());
133 }
134 }
135 }
136 OutputFormat::Json => {
137 let obj = serde_json::json!({
138 "phase": "execute",
139 "tool": tool,
140 "display": display,
141 "result": result,
142 "session_id": self.config.session_id,
143 "timestamp": chrono::Utc::now().to_rfc3339(),
144 });
145 println!("{}", obj);
146 }
147 }
148 }
149
150 pub fn print_error(&self, message: &str) {
151 match self.config.format {
152 OutputFormat::Terminal => {
153 eprintln!("{} {}", "[error]".red().bold(), message.red());
154 }
155 OutputFormat::Json => {
156 let obj = serde_json::json!({
157 "phase": "error",
158 "message": message,
159 "session_id": self.config.session_id,
160 "timestamp": chrono::Utc::now().to_rfc3339(),
161 });
162 eprintln!("{}", obj);
163 }
164 }
165 }
166
167 pub fn print_streaming_start(&self) {
169 if matches!(self.config.format, OutputFormat::Terminal) {
170 let tag = Phase::Plan.colorize("[plan]");
171 print!("{} ", tag);
172 std::io::stdout().flush().ok();
173 }
174 }
175
176 pub fn print_streaming_token(&self, token: &str) {
178 if matches!(self.config.format, OutputFormat::Terminal) {
179 print!("{}", token);
180 std::io::stdout().flush().ok();
181 }
182 }
183
184 pub fn print_streaming_end(&self) {
186 if matches!(self.config.format, OutputFormat::Terminal) {
187 println!();
188 }
189 }
190
191 pub fn print_diff(&self, label: &str, old: &str, new: &str) {
192 use similar::{ChangeTag, TextDiff};
193 println!("{}", format!("--- {}", label).dimmed());
194 let diff = TextDiff::from_lines(old, new);
195 for change in diff.iter_all_changes() {
196 let line = change.to_string_lossy();
197 match change.tag() {
198 ChangeTag::Delete => print!("{}", format!("-{}", line).red()),
199 ChangeTag::Insert => print!("{}", format!("+{}", line).green()),
200 ChangeTag::Equal => print!("{}", format!(" {}", line).dimmed()),
201 }
202 }
203 }
204
205 pub fn confirm(
208 &self,
209 message: &str,
210 show_diff_option: bool,
211 show_checkpoint_option: bool,
212 ) -> ConfirmResult {
213 let options = build_options(show_diff_option, show_checkpoint_option);
214 loop {
215 print!("{} {} [{}] ", "[ralph]".white().bold(), message, options);
216 std::io::stdout().flush().ok();
217 let mut input = String::new();
218 if std::io::stdin().read_line(&mut input).is_err() {
219 return ConfirmResult::No;
220 }
221 match input.trim().to_lowercase().as_str() {
222 "y" | "yes" => return ConfirmResult::Yes,
223 "n" | "no" | "" => return ConfirmResult::No,
224 "d" if show_diff_option => return ConfirmResult::ShowDiff,
225 "c" if show_checkpoint_option => return ConfirmResult::CheckpointAndProceed,
226 _ => {
227 println!(" Please enter one of: {}", options);
228 }
229 }
230 }
231 }
232}
233
234fn build_options(diff: bool, checkpoint: bool) -> String {
235 let mut opts = vec!["y", "N"];
236 if diff {
237 opts.push("d(iff)");
238 }
239 if checkpoint {
240 opts.push("c(heckpoint+proceed)");
241 }
242 opts.join("/")
243}
244
245#[derive(Debug, Clone, PartialEq)]
246pub enum ConfirmResult {
247 Yes,
248 No,
249 ShowDiff,
250 CheckpointAndProceed,
251}