1use anyhow::Result;
2use crossterm::{
3 execute,
4 style::{Color, Print, ResetColor, SetForegroundColor},
5};
6use serde_json::Value;
7use std::io::stdout;
8
9pub fn strip_markdown_code_blocks(s: &str) -> String {
11 let s = s.trim();
12
13 if s.starts_with("```json") && s.ends_with("```") {
15 let without_start = s.strip_prefix("```json").unwrap_or(s);
17 let content = without_start.strip_suffix("```").unwrap_or(without_start);
18 return content.trim().to_string();
19 }
20 s.to_string()
23}
24
25#[allow(dead_code)]
27#[derive(Debug, Clone, PartialEq)]
28enum MarkdownState {
29 Normal,
30 Heading(usize), Bold,
32 Italic,
33 BoldItalic,
34 CodeBlock(String), InlineCode,
36 UnorderedList(usize), OrderedList(usize, usize), Link,
39 LinkUrl,
40 Blockquote(usize), }
42
43pub struct MarkdownStreamRenderer {
44 buffer: String,
45 in_response: bool,
46 depth: i32,
47 extracted: String,
48 state_stack: Vec<MarkdownState>,
49 current_line: String,
50}
51
52impl MarkdownStreamRenderer {
53 pub fn new() -> Self {
54 Self {
55 buffer: String::new(),
56 in_response: false,
57 depth: 0,
58 extracted: String::new(),
59 state_stack: vec![MarkdownState::Normal],
60 current_line: String::new(),
61 }
62 }
63
64 pub fn process_chunk(&mut self, chunk: &str) -> String {
65 self.buffer.push_str(chunk);
66 let mut output = String::new();
67
68 if !self.in_response && self.buffer.contains(r#""response":"#) {
70 if let Some(pos) = self.buffer.find(r#""response":"#) {
71 let safe_start = pos + r#""response":"#.len();
73 let chars: Vec<char> = self.buffer.chars().collect();
74 if safe_start < self.buffer.len() {
75 let new_buffer: String = chars.into_iter().skip(safe_start).collect();
77 self.buffer = new_buffer;
78 } else {
79 self.buffer.clear();
80 }
81 self.in_response = true;
82 self.depth = 0;
83 }
84 }
85
86 if self.in_response {
88 let chars: Vec<char> = self.buffer.chars().collect();
89 let mut i = 0;
90
91 while i < chars.len() {
92 let c = chars[i];
93
94 let is_escape_quote = i > 0 && chars[i - 1] == '\\' && c == '"';
96
97 if c == '"' && !is_escape_quote {
98 if self.depth == 0 {
99 self.depth = 1;
101 i += 1; continue;
103 } else {
104 self.depth = 0;
106 self.in_response = false;
107 break;
108 }
109 }
110
111 if self.depth == 1 {
112 if c == '\\' && i + 1 < chars.len() {
113 let next_char = chars[i + 1];
115 if next_char == 'n' {
116 output.push('\n');
117 } else if next_char == '"' {
118 output.push('"');
119 } else if next_char == '\\' {
120 output.push('\\');
121 } else {
122 output.push(c);
123 i -= 1; }
125 i += 1; } else if self.extracted.chars().last() == Some('\\') {
127 if c == 'n' {
129 output.push('\n');
130 self.extracted.pop();
132 } else if c == '"' {
133 output.push('"');
134 self.extracted.pop();
135 } else if c == '\\' {
136 output.push('\\');
137 self.extracted.pop();
138 } else {
139 output.push(c);
141 }
142 } else {
143 output.push(c);
144 }
145 }
146
147 i += 1;
148 }
149
150 if !self.in_response {
152 self.buffer.clear();
153 } else if i < chars.len() {
154 self.buffer = chars.into_iter().skip(i).collect();
155 } else {
156 self.buffer.clear();
157 }
158 }
159 self.extracted.push_str(&output);
160 let output = if output.chars().last() == Some('\\') {
161 output.pop();
162 output
163 } else {
164 output
165 };
166 if !output.is_empty() {
168 let _ = self.render_increment(&output);
169 return String::new(); }
171
172 output
173 }
174
175 fn current_state(&self) -> &MarkdownState {
176 self.state_stack.last().unwrap_or(&MarkdownState::Normal)
177 }
178
179 fn push_state(&mut self, state: MarkdownState) {
180 self.state_stack.push(state);
181 }
182
183 fn pop_state(&mut self) -> Option<MarkdownState> {
184 self.state_stack.pop()
185 }
186
187 fn render_increment(&mut self, text: &str) -> Result<()> {
188 self.process_text(text)?;
191
192 Ok(())
195 }
196
197 fn process_text(&mut self, text: &str) -> Result<()> {
198 let mut chars = text.chars().peekable();
199
200 while let Some(c) = chars.next() {
201 self.current_line.push(c);
203
204 match c {
206 '#' => {
207 if self.current_line.trim() == "#" {
209 let mut level = 1;
211
212 let mut lookahead = chars.clone();
214 while lookahead.next_if_eq(&'#').is_some() {
215 level += 1;
216 }
217
218 let color = match level {
220 1 => Color::Magenta,
221 2 => Color::DarkMagenta,
222 3 => Color::Cyan,
223 _ => Color::White,
224 };
225
226 execute!(stdout(), SetForegroundColor(color), Print("#"))?;
228
229 self.push_state(MarkdownState::Heading(level));
231 } else {
232 self.print_with_current_style("#")?;
234 }
235 }
236 '*' => {
237 if self.current_line.trim() == "*" && chars.peek() == Some(&' ') {
239 execute!(stdout(), SetForegroundColor(Color::Green), Print("*"))?;
241 self.push_state(MarkdownState::UnorderedList(0));
242 } else if self.current_line.ends_with("**") {
243 execute!(stdout(), SetForegroundColor(Color::Yellow), Print("**"))?;
245 self.current_line.pop(); self.current_line.pop(); match self.current_state() {
250 MarkdownState::Bold => {
251 let _ = self.pop_state();
252 execute!(stdout(), ResetColor)?;
253 }
254 _ => self.push_state(MarkdownState::Bold),
255 }
256 } else if self.current_line.ends_with("*") && !self.current_line.ends_with("**")
257 {
258 execute!(stdout(), SetForegroundColor(Color::Blue), Print("*"))?;
260 self.current_line.pop(); match self.current_state() {
264 MarkdownState::Italic => {
265 let _ = self.pop_state();
266 execute!(stdout(), ResetColor)?;
267 }
268 _ => self.push_state(MarkdownState::Italic),
269 }
270 } else {
271 self.print_with_current_style("*")?;
273 }
274 }
275 '`' => {
276 if self.current_line.ends_with("```") {
278 execute!(stdout(), SetForegroundColor(Color::Yellow), Print("```"))?;
280 self.current_line.pop(); self.current_line.pop(); self.current_line.pop(); match self.current_state() {
286 MarkdownState::CodeBlock(_) => {
287 let _ = self.pop_state();
288 execute!(stdout(), ResetColor)?;
289 }
290 _ => self.push_state(MarkdownState::CodeBlock(String::new())),
291 }
292 } else if self.current_line.ends_with("`") {
293 execute!(stdout(), SetForegroundColor(Color::Yellow), Print("`"))?;
295 self.current_line.pop(); match self.current_state() {
299 MarkdownState::InlineCode => {
300 let _ = self.pop_state();
301 execute!(stdout(), ResetColor)?;
302 }
303 _ => self.push_state(MarkdownState::InlineCode),
304 }
305 } else {
306 self.print_with_current_style("`")?;
308 }
309 }
310 '[' => {
311 execute!(stdout(), SetForegroundColor(Color::Blue), Print("["))?;
313 self.push_state(MarkdownState::Link);
314 }
315 ']' => {
316 execute!(stdout(), SetForegroundColor(Color::Blue), Print("]"))?;
318
319 if matches!(self.current_state(), MarkdownState::Link) {
321 self.pop_state();
322
323 if chars.peek() == Some(&'(') {
325 self.push_state(MarkdownState::LinkUrl);
326 }
327 }
328 }
329 '(' => {
330 if matches!(self.current_state(), MarkdownState::LinkUrl) {
331 execute!(stdout(), SetForegroundColor(Color::DarkBlue), Print("("))?;
333 } else {
334 self.print_with_current_style("(")?;
336 }
337 }
338 ')' => {
339 if matches!(self.current_state(), MarkdownState::LinkUrl) {
340 execute!(stdout(), SetForegroundColor(Color::DarkBlue), Print(")"))?;
342 self.pop_state();
343 } else {
344 self.print_with_current_style(")")?;
346 }
347 }
348 '1'..='9' => {
349 if self.current_line.trim().len() == 1 && chars.peek() == Some(&'.') {
351 execute!(stdout(), SetForegroundColor(Color::Green), Print(c))?;
353 } else {
354 self.print_with_current_style(c.to_string().as_str())?;
356 }
357 }
358 '.' => {
359 if self.current_line.trim().len() >= 1
360 && self
361 .current_line
362 .trim()
363 .chars()
364 .next()
365 .unwrap()
366 .is_digit(10)
367 && self.current_line.trim().ends_with('.')
368 && chars.peek() == Some(&' ')
369 {
370 execute!(stdout(), SetForegroundColor(Color::Green), Print("."))?;
372 self.push_state(MarkdownState::OrderedList(0, 0));
373 } else {
374 self.print_with_current_style(".")?;
376 }
377 }
378 '>' => {
379 if self.current_line.trim() == ">" {
381 execute!(stdout(), SetForegroundColor(Color::Cyan), Print(">"))?;
382 self.push_state(MarkdownState::Blockquote(1));
383 } else {
384 self.print_with_current_style(">")?;
386 }
387 }
388 '\n' => {
389 execute!(stdout(), Print("\n"))?;
391 self.current_line.clear();
392
393 self.reset_line_states()?;
395 }
396 _ => {
397 self.print_with_current_style(c.to_string().as_str())?;
399 }
400 }
401 }
402
403 Ok(())
404 }
405
406 fn print_with_current_style(&self, text: &str) -> Result<()> {
408 match self.current_state() {
409 MarkdownState::Normal => {
410 execute!(stdout(), ResetColor, Print(text))?;
411 }
412 MarkdownState::Heading(level) => {
413 let color = match level {
414 1 => Color::Magenta,
415 2 => Color::DarkMagenta,
416 3 => Color::Cyan,
417 _ => Color::White,
418 };
419 execute!(stdout(), SetForegroundColor(color), Print(text))?;
420 }
421 MarkdownState::Bold => {
422 execute!(stdout(), SetForegroundColor(Color::Yellow), Print(text))?;
423 }
424 MarkdownState::Italic => {
425 execute!(stdout(), SetForegroundColor(Color::Blue), Print(text))?;
426 }
427 MarkdownState::BoldItalic => {
428 execute!(stdout(), SetForegroundColor(Color::Magenta), Print(text))?;
429 }
430 MarkdownState::CodeBlock(_) => {
431 execute!(stdout(), SetForegroundColor(Color::Yellow), Print(text))?;
432 }
433 MarkdownState::InlineCode => {
434 execute!(stdout(), SetForegroundColor(Color::Yellow), Print(text))?;
435 }
436 MarkdownState::Link => {
437 execute!(stdout(), SetForegroundColor(Color::Blue), Print(text))?;
438 }
439 MarkdownState::LinkUrl => {
440 execute!(stdout(), SetForegroundColor(Color::DarkBlue), Print(text))?;
441 }
442 MarkdownState::UnorderedList(_) | MarkdownState::OrderedList(_, _) => {
443 execute!(stdout(), SetForegroundColor(Color::Green), Print(text))?;
444 }
445 MarkdownState::Blockquote(_) => {
446 execute!(stdout(), SetForegroundColor(Color::Cyan), Print(text))?;
447 }
448 }
449
450 Ok(())
451 }
452
453 fn reset_line_states(&mut self) -> Result<()> {
454 match self.current_state() {
456 MarkdownState::Heading(_) => {
457 self.pop_state();
458 execute!(stdout(), ResetColor)?;
459 }
460 MarkdownState::UnorderedList(_) => {
461 self.pop_state();
462 }
463 MarkdownState::OrderedList(_, _) => {
464 self.pop_state();
465 }
466 MarkdownState::Blockquote(_) => {
467 self.pop_state();
468 }
469 _ => {}
470 }
471
472 Ok(())
473 }
474}
475
476pub fn extract_response(partial: &str) -> Option<&str> {
478 let response_marker = r#""response": ""#;
480 let start_pos = partial.find(response_marker)?;
481 let start_pos = start_pos + response_marker.len();
482
483 let response_substring = &partial[start_pos..];
485
486 let mut depth = 0;
488 let mut is_escaped = false;
489 let mut end_pos = 0;
490
491 for (i, c) in response_substring.char_indices() {
492 if is_escaped {
493 is_escaped = false;
494 continue;
495 }
496
497 if c == '\\' {
498 is_escaped = true;
499 continue;
500 }
501
502 if c == '"' && depth == 0 {
504 end_pos = i;
505 break;
506 }
507
508 match c {
510 '[' => depth += 1,
511 ']' => {
512 if depth > 0 {
513 depth -= 1
514 }
515 }
516 '{' => depth += 1,
517 '}' => {
518 if depth > 0 {
519 depth -= 1
520 }
521 }
522 _ => {}
523 }
524 }
525
526 if end_pos == 0 {
528 Some(response_substring)
530 } else {
531 let response = &response_substring[..end_pos];
533
534 Some(response)
536 }
537}
538
539pub fn contains_end_tag(content: &str) -> bool {
541 let clean_content = strip_markdown_code_blocks(content);
543
544 match serde_json::from_str::<Value>(&clean_content) {
546 Ok(json) => {
547 if json
548 .get("finished")
549 .and_then(Value::as_bool)
550 .unwrap_or(false)
551 {
552 return true;
553 }
554 }
555 Err(_) => {}
556 }
557 return false;
558}
559
560pub fn contains_tool_call(content: &str) -> Option<(String, String)> {
561 let clean_content = strip_markdown_code_blocks(content);
563
564 match serde_json::from_str::<Value>(&clean_content) {
566 Ok(json) => {
567 if let Some(tool) = json.get("tool") {
568 if tool.is_null() {
569 return None;
570 }
571
572 let tool_name = tool.get("name")?.as_str()?;
573 let tool_content = tool.get("content")?.as_str()?;
574
575 return Some((tool_name.to_string(), tool_content.to_string()));
576 }
577 }
578 Err(_) => {}
579 }
580
581 None
582}
583
584pub fn extract_tool_content(content: &str) -> Option<String> {
585 if let Some(tool_content) = extract_tool_content_json(content) {
587 return Some(tool_content);
588 }
589 None
590}
591
592fn extract_tool_content_json(content: &str) -> Option<String> {
593 let clean_content = strip_markdown_code_blocks(content);
595
596 match serde_json::from_str::<Value>(&clean_content) {
597 Ok(json) => {
598 if let Some(tool) = json.get("tool") {
599 if tool.is_object() && tool.get("name")? == "cli" {
600 return tool.get("content")?.as_str().map(String::from);
601 }
602 }
603 None
604 }
605 Err(_) => None,
606 }
607}