use std::time::Duration;
use crate::tui::theme::Theme;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ThinkingKind {
Processing,
Reasoning,
}
pub struct ThinkingFrames;
impl ThinkingFrames {
pub fn frames() -> impl Iterator<Item = String> {
let accent = Theme::ACCENT.to_string();
let reset = Theme::RESET.to_string();
[
format!("{accent} ▓░░░{reset}"),
format!("{accent} ▓▓░░{reset}"),
format!("{accent} ▓▓▓░{reset}"),
format!("{accent} ▓▓▓▓{reset}"),
format!("{accent} ▓▓▓░{reset}"),
format!("{accent} ▓▓░░{reset}"),
format!("{accent} ▓░░░{reset}"),
format!("{accent} ░░░░{reset}"),
]
.into_iter()
.cycle()
}
pub fn frame_delay() -> Duration {
Duration::from_millis(120)
}
}
pub struct ReasoningFrames;
impl ReasoningFrames {
pub fn frames() -> impl Iterator<Item = String> {
let thinking = Theme::THINKING.to_string();
let reset = Theme::RESET.to_string();
[
format!("{thinking} ◇{reset}"),
format!("{thinking} ◆{reset}"),
format!("{thinking} ◇{reset}"),
format!("{thinking} ◇{reset}"),
format!("{thinking} ◆{reset}"),
format!("{thinking} ◇{reset}"),
]
.into_iter()
.cycle()
}
pub fn frame_delay() -> Duration {
Duration::from_millis(200)
}
}
pub fn frames_for_kind(kind: ThinkingKind) -> Box<dyn Iterator<Item = String>> {
match kind {
ThinkingKind::Processing => Box::new(ThinkingFrames::frames()),
ThinkingKind::Reasoning => Box::new(ReasoningFrames::frames()),
}
}
pub fn format_thinking_completed(elapsed: Duration) -> String {
let secs = elapsed.as_secs_f64();
format!(
"{}-- reasoned for {secs:.1}s{}",
Theme::THINKING,
Theme::RESET
)
}
pub fn render_thinking_inline(char_count: Option<usize>, redacted: bool) -> String {
let t = Theme::THINKING;
let r = Theme::RESET;
let summary = if redacted {
format!("{t} ◇ thinking block hidden by provider{r}")
} else if let Some(char_count) = char_count {
format!("{t} ◆ reasoning ({char_count} chars){r}")
} else {
format!("{t} ◆ reasoning{r}")
};
format!("\n{summary}\n")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn frames_cycles_indefinitely() {
let frames: Vec<String> = ThinkingFrames::frames().take(16).collect();
assert_eq!(frames.len(), 16);
let first = &frames[0];
assert_eq!(&frames[8], first); }
#[test]
fn reasoning_frames_cycles_indefinitely() {
let frames: Vec<String> = ReasoningFrames::frames().take(12).collect();
assert_eq!(frames.len(), 12);
let first = &frames[0];
assert_eq!(&frames[6], first); }
#[test]
fn reasoning_frames_uses_thinking_color() {
let frames: Vec<String> = ReasoningFrames::frames().take(6).collect();
for frame in &frames {
assert!(frame.contains(Theme::THINKING));
}
}
#[test]
fn reasoning_frames_different_from_thinking() {
let thinking: Vec<String> = ThinkingFrames::frames().take(6).collect();
let reasoning: Vec<String> = ReasoningFrames::frames().take(6).collect();
assert_ne!(thinking, reasoning);
}
#[test]
fn frames_for_kind_returns_correct_type() {
let processing_frames: Vec<String> =
frames_for_kind(ThinkingKind::Processing).take(2).collect();
let reasoning_frames: Vec<String> =
frames_for_kind(ThinkingKind::Reasoning).take(2).collect();
assert_eq!(processing_frames.len(), 2);
assert_eq!(reasoning_frames.len(), 2);
assert_ne!(processing_frames, reasoning_frames);
}
#[test]
fn thinking_completed_formats_seconds() {
let result = format_thinking_completed(Duration::from_secs_f64(3.5));
assert!(result.contains("reasoned for"));
assert!(result.contains("3.5s"));
assert!(result.contains(Theme::THINKING)); }
#[test]
fn thinking_inline_with_char_count() {
let result = render_thinking_inline(Some(42), false);
assert!(result.contains("reasoning"));
assert!(result.contains("42 chars"));
assert!(result.contains(Theme::THINKING));
}
#[test]
fn thinking_inline_redacted() {
let result = render_thinking_inline(None, true);
assert!(result.contains("hidden by provider"));
}
#[test]
fn thinking_inline_without_count() {
let result = render_thinking_inline(None, false);
assert!(result.contains("reasoning"));
assert!(!result.contains("chars"));
}
}