1use crate::core::agent::AgentEvents;
2use serde_json::Value;
3use std::io::{self, Write};
4use std::sync::{Arc, Mutex};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum ThinkingMode {
8 Collapsed,
9 Expanded,
10}
11
12#[derive(Debug, Clone)]
13pub struct LiveRender {
14 inner: Arc<Mutex<RenderState>>,
15}
16
17#[derive(Debug)]
18struct RenderState {
19 thinking_mode: ThinkingMode,
20 thinking_placeholder_shown: bool,
21 thinking_line_open: bool,
22 assistant_line_open: bool,
23}
24
25impl LiveRender {
26 pub fn new() -> Self {
27 Self {
28 inner: Arc::new(Mutex::new(RenderState {
29 thinking_mode: ThinkingMode::Collapsed,
30 thinking_placeholder_shown: false,
31 thinking_line_open: false,
32 assistant_line_open: false,
33 })),
34 }
35 }
36
37 pub fn begin_turn(&self) {
38 if let Ok(mut state) = self.inner.lock() {
39 state.thinking_placeholder_shown = false;
40 state.thinking_line_open = false;
41 state.assistant_line_open = false;
42 }
43 }
44
45 pub fn toggle_thinking_mode(&self) -> ThinkingMode {
46 if let Ok(mut state) = self.inner.lock() {
47 state.thinking_mode = match state.thinking_mode {
48 ThinkingMode::Collapsed => ThinkingMode::Expanded,
49 ThinkingMode::Expanded => ThinkingMode::Collapsed,
50 };
51 state.thinking_mode
52 } else {
53 ThinkingMode::Collapsed
54 }
55 }
56
57 pub fn thinking_mode(&self) -> ThinkingMode {
58 self.inner
59 .lock()
60 .map(|s| s.thinking_mode)
61 .unwrap_or(ThinkingMode::Collapsed)
62 }
63}
64
65impl Default for LiveRender {
66 fn default() -> Self {
67 Self::new()
68 }
69}
70
71impl AgentEvents for LiveRender {
72 fn on_thinking(&self, text: &str) {
73 let Ok(mut state) = self.inner.lock() else {
74 return;
75 };
76
77 match state.thinking_mode {
78 ThinkingMode::Collapsed => {
79 if !state.thinking_placeholder_shown {
80 if state.assistant_line_open {
81 println!();
82 state.assistant_line_open = false;
83 }
84 println!("thinking… (toggle with :thinking)");
85 state.thinking_placeholder_shown = true;
86 }
87 }
88 ThinkingMode::Expanded => {
89 if state.assistant_line_open {
90 println!();
91 state.assistant_line_open = false;
92 }
93 if !state.thinking_line_open {
94 print!("thinking> ");
95 state.thinking_line_open = true;
96 }
97 print!("{}", text);
98 let _ = io::stdout().flush();
99 }
100 }
101 }
102
103 fn on_tool_start(&self, name: &str, args: &Value) {
104 let Ok(mut state) = self.inner.lock() else {
105 return;
106 };
107 if state.assistant_line_open || state.thinking_line_open {
108 println!();
109 state.assistant_line_open = false;
110 state.thinking_line_open = false;
111 }
112 println!("tool:{}> start {}", name, format_args_preview(args, 220));
113 }
114
115 fn on_tool_end(&self, name: &str, result: &crate::tool::ToolResult) {
116 let Ok(mut state) = self.inner.lock() else {
117 return;
118 };
119 if state.assistant_line_open || state.thinking_line_open {
120 println!();
121 state.assistant_line_open = false;
122 state.thinking_line_open = false;
123 }
124 let status = if result.is_error { "error" } else { "ok" };
125 println!(
126 "tool:{}> {} {}",
127 name,
128 status,
129 truncate_text(&result.summary, 220)
130 );
131 }
132
133 fn on_assistant_delta(&self, delta: &str) {
134 let Ok(mut state) = self.inner.lock() else {
135 return;
136 };
137 if state.thinking_line_open {
138 println!();
139 state.thinking_line_open = false;
140 }
141 if !state.assistant_line_open {
142 print!("assistant> ");
143 state.assistant_line_open = true;
144 }
145 print!("{}", delta);
146 let _ = io::stdout().flush();
147 }
148
149 fn on_assistant_done(&self) {
150 let Ok(mut state) = self.inner.lock() else {
151 return;
152 };
153 if state.thinking_line_open || state.assistant_line_open {
154 println!();
155 state.thinking_line_open = false;
156 state.assistant_line_open = false;
157 }
158 }
159}
160
161pub fn print_assistant(text: &str) {
162 println!("assistant> {}", text);
163}
164
165pub fn print_tool_log(name: &str, message: &str) {
166 println!("tool:{}> {}", name, message);
167}
168
169pub fn print_error(message: &str) {
170 eprintln!("error: {}", message);
171}
172
173pub fn print_info(message: &str) {
174 println!("info: {}", message);
175}
176
177pub fn prompt_user() -> io::Result<String> {
178 print!("you> ");
179 io::stdout().flush()?;
180 let mut input = String::new();
181 io::stdin().read_line(&mut input)?;
182 Ok(input.trim().to_string())
183}
184
185pub fn confirm(prompt: &str) -> io::Result<bool> {
186 print!("{} [y/N]: ", prompt);
187 io::stdout().flush()?;
188 let mut input = String::new();
189 io::stdin().read_line(&mut input)?;
190 let normalized = input.trim().to_ascii_lowercase();
191 Ok(normalized == "y" || normalized == "yes")
192}
193
194pub fn ask_questions(
195 questions: &[crate::core::QuestionPrompt],
196) -> io::Result<crate::core::QuestionAnswers> {
197 let mut answers = Vec::with_capacity(questions.len());
198
199 for question in questions {
200 println!();
201 println!("{}", question.question);
202 println!();
203
204 for (index, option) in question.options.iter().enumerate() {
205 println!("{}. {}", index + 1, option.label);
206 if !option.description.trim().is_empty() {
207 println!(" {}", option.description);
208 }
209 }
210
211 let custom_index = if question.custom {
212 let index = question.options.len() + 1;
213 println!("{}. Type your own answer", index);
214 Some(index)
215 } else {
216 None
217 };
218
219 if question.multiple {
220 print!("Select option numbers (comma-separated), or press enter to skip: ");
221 } else {
222 print!("Select an option number, or press enter to skip: ");
223 }
224 io::stdout().flush()?;
225
226 let mut input = String::new();
227 io::stdin().read_line(&mut input)?;
228 let trimmed = input.trim();
229
230 if trimmed.is_empty() {
231 answers.push(Vec::new());
232 continue;
233 }
234
235 let mut selected = Vec::new();
236 let tokens = if question.multiple {
237 trimmed
238 .split(',')
239 .map(str::trim)
240 .filter(|token| !token.is_empty())
241 .collect::<Vec<_>>()
242 } else {
243 vec![trimmed]
244 };
245
246 for token in tokens {
247 let Ok(choice) = token.parse::<usize>() else {
248 continue;
249 };
250
251 if let Some(index) = custom_index
252 && choice == index
253 {
254 print!("Type your own answer: ");
255 io::stdout().flush()?;
256 let mut custom = String::new();
257 io::stdin().read_line(&mut custom)?;
258 let custom = custom.trim();
259 if !custom.is_empty() {
260 selected.push(custom.to_string());
261 }
262 continue;
263 }
264
265 if let Some(option) = question.options.get(choice.saturating_sub(1)) {
266 selected.push(option.label.clone());
267 }
268 }
269
270 selected.sort();
271 selected.dedup();
272 answers.push(selected);
273 }
274
275 Ok(answers)
276}
277
278pub fn format_args_preview(args: &Value, max_len: usize) -> String {
279 let compact = serde_json::to_string(args).unwrap_or_else(|_| "{}".to_string());
280 truncate_text(&compact, max_len)
281}
282
283pub fn truncate_text(input: &str, max_len: usize) -> String {
284 if max_len == 0 {
285 return String::new();
286 }
287
288 let mut chars = input.chars();
289 let head: String = chars.by_ref().take(max_len).collect();
290 if chars.next().is_some() {
291 format!("{}…", head)
292 } else {
293 head
294 }
295}