1use std::io::{self, Write};
2
3use agent_base::{AgentResult, PlanStepStatus, RuntimeEvent};
4
5use crate::render::EventRenderer;
6
7pub struct TerminalRenderer {
12 show_thinking: bool,
13 show_tool_args: bool,
14 color: bool,
15 writer: Box<dyn Write + Send>,
16 tool_call_count: u32,
17 turn_start: Option<std::time::Instant>,
18 last_assistant_text: String,
19 last_was_thought: bool,
20}
21
22impl TerminalRenderer {
23 pub fn new(show_thinking: bool, show_tool_args: bool, color: bool, writer: Box<dyn Write + Send>) -> Self {
30 Self {
31 show_thinking,
32 show_tool_args,
33 color,
34 writer,
35 tool_call_count: 0,
36 turn_start: None,
37 last_assistant_text: String::new(),
38 last_was_thought: false,
39 }
40 }
41
42 pub fn stdout(show_thinking: bool, show_tool_args: bool, color: bool) -> Self {
43 Self::new(show_thinking, show_tool_args, color, Box::new(io::stdout()))
44 }
45
46 fn green(&self, s: &str) -> String {
47 if self.color { format!("\x1b[32m{}\x1b[0m", s) } else { s.to_string() }
48 }
49
50 fn dim(&self, s: &str) -> String {
51 if self.color { format!("\x1b[2m{}\x1b[0m", s) } else { s.to_string() }
52 }
53
54 fn bold(&self, s: &str) -> String {
55 if self.color { format!("\x1b[1m{}\x1b[0m", s) } else { s.to_string() }
56 }
57
58 fn yellow(&self, s: &str) -> String {
59 if self.color { format!("\x1b[33m{}\x1b[0m", s) } else { s.to_string() }
60 }
61
62 fn subtle(&self, s: &str) -> String {
63 if self.color { format!("\x1b[90m{}\x1b[0m", s) } else { s.to_string() }
64 }
65
66 fn write_line(&mut self, s: &str) -> AgentResult<()> {
67 writeln!(self.writer, "{}", s).map_err(|e| agent_base::AgentError::internal(format!("write error: {e}")))?;
68 self.writer.flush().map_err(|e| agent_base::AgentError::internal(format!("flush error: {e}")))?;
69 Ok(())
70 }
71
72 fn write_text(&mut self, s: &str) -> AgentResult<()> {
74 write!(self.writer, "{}", s).map_err(|e| agent_base::AgentError::internal(format!("write error: {e}")))?;
75 self.writer.flush().map_err(|e| agent_base::AgentError::internal(format!("flush error: {e}")))?;
76 Ok(())
77 }
78}
79
80impl EventRenderer for TerminalRenderer {
81 fn render(&mut self, event: RuntimeEvent) -> AgentResult<()> {
82 if self.turn_start.is_none() {
83 self.turn_start = Some(std::time::Instant::now());
84 }
85
86 match &event {
87 RuntimeEvent::ThoughtDelta { text, .. } => {
88 if self.show_thinking {
89 self.write_text(&self.dim(text))?;
90 }
91 self.last_was_thought = true;
92 },
93 RuntimeEvent::TextDelta { text, .. } => {
94 if self.last_was_thought {
95 let _ = writeln!(self.writer);
96 self.last_was_thought = false;
97 }
98 self.last_assistant_text.push_str(text);
99 self.write_text(text)?;
100 },
101 RuntimeEvent::ToolCallStarted { tool_name, args_json, .. } => {
102 self.last_was_thought = false;
103 self.tool_call_count += 1;
104 if self.show_tool_args {
105 self.write_line(&format!(
106 "\n{} {} {}",
107 self.bold("\u{1F527}"),
108 self.green(tool_name),
109 self.dim(args_json),
110 ))?;
111 } else {
112 self.write_line(&format!("\n{} {}", self.bold("\u{1F527}"), self.green(tool_name),))?;
113 }
114 },
115 RuntimeEvent::ToolCallFinished { tool_name: _, summary, .. } => {
116 let summary_short: String = if summary.chars().count() > 500 {
117 let truncated: String = summary.chars().take(500).collect();
118 format!("{}...", truncated)
119 } else {
120 summary.clone()
121 };
122 self.write_line(&format!(" {} {}", self.dim("→"), self.dim(&summary_short)))?;
123 let _ = writeln!(self.writer);
125 },
126 RuntimeEvent::AwaitingApproval { request, .. } => {
127 self.write_line(&format!("\n⚠️ {} [{:?}] — {}", request.title, request.risk_level, request.message,))?;
128 },
129 RuntimeEvent::PlanUpdated { explanation, plan, .. } => {
130 self.write_line(&format!("\n\u{1F4CB} {}", self.bold("Plan Update")))?;
131 self.write_line(&format!(" {}", self.dim(explanation.as_deref().unwrap_or(""))))?;
132 for item in plan {
133 let icon = match item.status {
134 PlanStepStatus::Completed => "✅",
135 PlanStepStatus::InProgress => "\u{1F504}",
136 PlanStepStatus::Pending => "⏳",
137 };
138 self.write_line(&format!(" {} {}", icon, item.step))?;
139 }
140 let _ = writeln!(self.writer);
141 },
142 RuntimeEvent::RunCancelled { .. } => {
143 self.write_line(&format!("\n{} Cancelled", self.yellow("⚠")))?;
144 },
145 RuntimeEvent::RunFinished { .. } => {},
146 RuntimeEvent::UserEvent { .. } => {},
147 RuntimeEvent::Checkpoint { .. } => {},
148 }
149
150 Ok(())
151 }
152
153 fn finish_turn(&mut self) -> AgentResult<()> {
154 let duration_ms = self.turn_start.map(|s| s.elapsed().as_millis() as u64).unwrap_or(0);
155
156 let duration_str = if duration_ms >= 1000 {
157 format!("{:.1}s", duration_ms as f64 / 1000.0)
158 } else {
159 format!("{}ms", duration_ms)
160 };
161
162 writeln!(
163 self.writer,
164 "\n{}",
165 self.subtle(&format!("· {} elapsed · {} tool call(s)", duration_str, self.tool_call_count)),
166 )
167 .map_err(|e| agent_base::AgentError::internal(format!("write error: {e}")))?;
168
169 self.tool_call_count = 0;
170 self.turn_start = None;
171 self.last_assistant_text.clear();
172 self.last_was_thought = false;
173
174 Ok(())
175 }
176}