use colored::Colorize;
use std::io::Write;
#[derive(Debug, Clone)]
pub struct OutputConfig {
pub format: OutputFormat,
pub verbose: bool,
pub session_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum OutputFormat {
Terminal,
Json,
}
impl OutputConfig {
pub fn new(format: OutputFormat, verbose: bool) -> Self {
Self {
format,
verbose,
session_id: None,
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum Phase {
Observe,
Plan,
Tool,
Verify,
Compact,
Checkpoint,
Ralph,
Done,
Failed,
Session,
}
impl Phase {
fn label(&self) -> &'static str {
match self {
Phase::Observe => "observe",
Phase::Plan => "plan",
Phase::Tool => "tool",
Phase::Verify => "verify",
Phase::Compact => "compact",
Phase::Checkpoint => "checkpoint",
Phase::Ralph => "ralph",
Phase::Done => "done",
Phase::Failed => "failed",
Phase::Session => "session",
}
}
fn colorize(&self, s: &str) -> String {
match self {
Phase::Observe => s.cyan().to_string(),
Phase::Plan => s.yellow().bold().to_string(),
Phase::Tool => s.blue().bold().to_string(),
Phase::Verify => s.green().to_string(),
Phase::Compact => s.magenta().bold().to_string(),
Phase::Checkpoint => s.blue().bold().to_string(),
Phase::Ralph => s.white().bold().to_string(),
Phase::Done => s.green().bold().to_string(),
Phase::Failed => s.red().bold().to_string(),
Phase::Session => s.cyan().to_string(),
}
}
}
#[derive(Clone)]
pub struct Printer {
pub config: OutputConfig,
}
impl Printer {
pub fn new(config: OutputConfig) -> Self {
Self { config }
}
pub fn with_session(mut self, session_id: String) -> Self {
self.config.session_id = Some(session_id);
self
}
pub fn clone_with_session(&self, session_id: String) -> Self {
let mut c = self.clone();
c.config.session_id = Some(session_id);
c
}
pub fn print(&self, phase: Phase, message: &str) {
match self.config.format {
OutputFormat::Terminal => {
let tag = format!("[{}]", phase.label());
let colored_tag = phase.colorize(&tag);
println!("{} {}", colored_tag, message);
}
OutputFormat::Json => {
let obj = serde_json::json!({
"phase": phase.label(),
"message": message,
"session_id": self.config.session_id,
"timestamp": chrono::Utc::now().to_rfc3339(),
});
println!("{}", obj);
}
}
}
pub fn print_tool_call(&self, tool: &str, display: &str, result: &str) {
match self.config.format {
OutputFormat::Terminal => {
let colored_tag = Phase::Tool.colorize("[tool]");
let tool_name = tool.cyan().to_string();
println!("{} {} {}", colored_tag, tool_name, display);
let skip = !self.config.verbose
&& (result == "(executing...)"
|| result.starts_with("DONE:")
|| result.starts_with("FAILED:"));
if !skip {
if result.len() <= 300 {
println!(" → {}", result.trim().dimmed());
} else {
let preview: String = result.chars().take(300).collect();
println!(" → {}…", preview.trim().dimmed());
}
}
}
OutputFormat::Json => {
let obj = serde_json::json!({
"phase": "execute",
"tool": tool,
"display": display,
"result": result,
"session_id": self.config.session_id,
"timestamp": chrono::Utc::now().to_rfc3339(),
});
println!("{}", obj);
}
}
}
pub fn print_error(&self, message: &str) {
match self.config.format {
OutputFormat::Terminal => {
eprintln!("{} {}", "[error]".red().bold(), message.red());
}
OutputFormat::Json => {
let obj = serde_json::json!({
"phase": "error",
"message": message,
"session_id": self.config.session_id,
"timestamp": chrono::Utc::now().to_rfc3339(),
});
eprintln!("{}", obj);
}
}
}
pub fn print_streaming_start(&self) {
if matches!(self.config.format, OutputFormat::Terminal) {
let tag = Phase::Plan.colorize("[plan]");
print!("{} ", tag);
std::io::stdout().flush().ok();
}
}
pub fn print_streaming_token(&self, token: &str) {
if matches!(self.config.format, OutputFormat::Terminal) {
print!("{}", token);
std::io::stdout().flush().ok();
}
}
pub fn print_streaming_end(&self) {
if matches!(self.config.format, OutputFormat::Terminal) {
println!();
}
}
pub fn print_diff(&self, label: &str, old: &str, new: &str) {
use similar::{ChangeTag, TextDiff};
println!("{}", format!("--- {}", label).dimmed());
let diff = TextDiff::from_lines(old, new);
for change in diff.iter_all_changes() {
let line = change.to_string_lossy();
match change.tag() {
ChangeTag::Delete => print!("{}", format!("-{}", line).red()),
ChangeTag::Insert => print!("{}", format!("+{}", line).green()),
ChangeTag::Equal => print!("{}", format!(" {}", line).dimmed()),
}
}
}
pub fn confirm(
&self,
message: &str,
show_diff_option: bool,
show_checkpoint_option: bool,
) -> ConfirmResult {
let options = build_options(show_diff_option, show_checkpoint_option);
loop {
print!("{} {} [{}] ", "[ralph]".white().bold(), message, options);
std::io::stdout().flush().ok();
let mut input = String::new();
if std::io::stdin().read_line(&mut input).is_err() {
return ConfirmResult::No;
}
match input.trim().to_lowercase().as_str() {
"y" | "yes" => return ConfirmResult::Yes,
"n" | "no" | "" => return ConfirmResult::No,
"d" if show_diff_option => return ConfirmResult::ShowDiff,
"c" if show_checkpoint_option => return ConfirmResult::CheckpointAndProceed,
_ => {
println!(" Please enter one of: {}", options);
}
}
}
}
}
fn build_options(diff: bool, checkpoint: bool) -> String {
let mut opts = vec!["y", "N"];
if diff {
opts.push("d(iff)");
}
if checkpoint {
opts.push("c(heckpoint+proceed)");
}
opts.join("/")
}
#[derive(Debug, Clone, PartialEq)]
pub enum ConfirmResult {
Yes,
No,
ShowDiff,
CheckpointAndProceed,
}