use super::{colors, Animation};
use ratatui::{
buffer::Buffer,
layout::Rect,
style::{Color, Modifier, Style},
widgets::Widget,
};
use unicode_width::UnicodeWidthChar;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AgentRole {
Architect,
Coder,
Tester,
Reviewer,
Documenter,
DevOps,
Security,
Performance,
}
impl AgentRole {
pub fn icon(&self) -> &'static str {
match self {
AgentRole::Architect => "๐๏ธ",
AgentRole::Coder => "๐ป",
AgentRole::Tester => "๐งช",
AgentRole::Reviewer => "๐๏ธ",
AgentRole::Documenter => "๐",
AgentRole::DevOps => "๐",
AgentRole::Security => "๐",
AgentRole::Performance => "โก",
}
}
pub fn ascii_icon(&self) -> &'static str {
match self {
AgentRole::Architect => "[A]",
AgentRole::Coder => "[C]",
AgentRole::Tester => "[T]",
AgentRole::Reviewer => "[R]",
AgentRole::Documenter => "[D]",
AgentRole::DevOps => "[O]",
AgentRole::Security => "[S]",
AgentRole::Performance => "[P]",
}
}
pub fn color(&self) -> Color {
match self {
AgentRole::Architect => colors::PRIMARY, AgentRole::Coder => colors::SECONDARY, AgentRole::Tester => colors::ACCENT, AgentRole::Reviewer => colors::PURPLE, AgentRole::Documenter => colors::WARNING, AgentRole::DevOps => colors::ERROR, AgentRole::Security => colors::SUCCESS, AgentRole::Performance => colors::ORANGE, }
}
pub fn name(&self) -> &'static str {
match self {
AgentRole::Architect => "Architect",
AgentRole::Coder => "Coder",
AgentRole::Tester => "Tester",
AgentRole::Reviewer => "Reviewer",
AgentRole::Documenter => "Documenter",
AgentRole::DevOps => "DevOps",
AgentRole::Security => "Security",
AgentRole::Performance => "Performance",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ActivityLevel {
#[default]
Idle,
Low,
Medium,
High,
Max,
Complete,
Error,
}
impl ActivityLevel {
pub fn symbol(&self) -> &'static str {
match self {
ActivityLevel::Idle => "โ",
ActivityLevel::Low => "โ",
ActivityLevel::Medium => "โ",
ActivityLevel::High => "โ",
ActivityLevel::Max => "โ",
ActivityLevel::Complete => "โ",
ActivityLevel::Error => "โ",
}
}
pub fn dots(&self) -> u8 {
match self {
ActivityLevel::Idle => 0,
ActivityLevel::Low => 1,
ActivityLevel::Medium => 2,
ActivityLevel::High => 3,
ActivityLevel::Max => 4,
ActivityLevel::Complete => 5,
ActivityLevel::Error => 0,
}
}
}
pub struct AgentAvatar {
role: AgentRole,
activity: ActivityLevel,
token_count: u64,
pulse_phase: f32,
name: String,
ascii_mode: bool,
}
impl AgentAvatar {
pub fn new(role: AgentRole) -> Self {
Self {
role,
activity: ActivityLevel::Idle,
token_count: 0,
pulse_phase: 0.0,
name: format!("{}-1", role.name()),
ascii_mode: false,
}
}
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = name.into();
self
}
pub fn with_activity(mut self, activity: ActivityLevel) -> Self {
self.activity = activity;
self
}
pub fn with_tokens(mut self, tokens: u64) -> Self {
self.token_count = tokens;
self
}
pub fn ascii_mode(mut self, enabled: bool) -> Self {
self.ascii_mode = enabled;
self
}
pub fn set_activity(&mut self, activity: ActivityLevel) {
self.activity = activity;
}
pub fn set_tokens(&mut self, tokens: u64) {
self.token_count = tokens;
}
pub fn add_tokens(&mut self, tokens: u64) {
self.token_count += tokens;
}
pub fn role(&self) -> AgentRole {
self.role
}
pub fn activity(&self) -> ActivityLevel {
self.activity
}
fn pulse_intensity(&self) -> u8 {
if self.activity == ActivityLevel::Idle {
return 255;
}
let pulse = (self.pulse_phase.sin() + 1.0) / 2.0;
(128.0 + pulse * 127.0) as u8
}
fn pulsed_color(&self) -> Color {
let base = self.role.color();
let intensity = self.pulse_intensity() as f32 / 255.0;
if let Color::Rgb(r, g, b) = base {
Color::Rgb(
(r as f32 * intensity) as u8,
(g as f32 * intensity) as u8,
(b as f32 * intensity) as u8,
)
} else {
base
}
}
fn format_tokens(&self) -> String {
if self.token_count >= 1_000_000 {
format!("{:.1}M", self.token_count as f64 / 1_000_000.0)
} else if self.token_count >= 1_000 {
format!("{}K", self.token_count / 1_000)
} else {
format!("{}", self.token_count)
}
}
}
impl Animation for AgentAvatar {
fn update(&mut self, delta_time: f32) {
let speed = match self.activity {
ActivityLevel::Idle => 0.5,
ActivityLevel::Low => 1.0,
ActivityLevel::Medium => 2.0,
ActivityLevel::High => 3.0,
ActivityLevel::Max => 5.0,
ActivityLevel::Complete => 0.0,
ActivityLevel::Error => 4.0,
};
self.pulse_phase += delta_time * speed;
if self.pulse_phase > std::f32::consts::PI * 2.0 {
self.pulse_phase -= std::f32::consts::PI * 2.0;
}
}
fn is_complete(&self) -> bool {
false }
}
impl Widget for &AgentAvatar {
fn render(self, area: Rect, buf: &mut Buffer) {
if area.width < 12 || area.height < 5 {
return;
}
let pulsed_color = self.pulsed_color();
let border_style = Style::default().fg(pulsed_color);
let content_style = Style::default().fg(Color::White);
buf[(area.x, area.y)]
.set_symbol("โ")
.set_style(border_style);
for x in area.x + 1..area.x + area.width - 1 {
buf[(x, area.y)].set_symbol("โ").set_style(border_style);
}
buf[(area.x + area.width - 1, area.y)]
.set_symbol("โ")
.set_style(border_style);
for y in area.y + 1..area.y + area.height - 1 {
buf[(area.x, y)].set_symbol("โ").set_style(border_style);
buf[(area.x + area.width - 1, y)]
.set_symbol("โ")
.set_style(border_style);
}
buf[(area.x, area.y + area.height - 1)]
.set_symbol("โ")
.set_style(border_style);
for x in area.x + 1..area.x + area.width - 1 {
buf[(x, area.y + area.height - 1)]
.set_symbol("โ")
.set_style(border_style);
}
buf[(area.x + area.width - 1, area.y + area.height - 1)]
.set_symbol("โ")
.set_style(border_style);
let inner_x = area.x + 2;
let inner_y = area.y + 1;
let icon = if self.ascii_mode {
self.role.ascii_icon()
} else {
self.role.icon()
};
let mut x_offset = 0u16;
for ch in icon.chars() {
let char_width = ch.width().unwrap_or(1) as u16;
if inner_x + x_offset + char_width <= area.x + area.width - 1 {
buf[(inner_x + x_offset, inner_y)]
.set_symbol(&ch.to_string())
.set_style(Style::default().fg(self.role.color()));
x_offset += char_width;
}
}
let max_name_len = (area.width - 6).min(10) as usize;
let name_display: String = self.name.chars().take(max_name_len).collect();
for (i, ch) in name_display.chars().enumerate() {
let x = inner_x + x_offset + 1 + i as u16;
if x < area.x + area.width - 1 {
buf[(x, inner_y)]
.set_symbol(&ch.to_string())
.set_style(content_style);
}
}
if area.height > 3 {
let activity_y = inner_y + 1;
let dots_filled = self.activity.dots();
let activity_color = match self.activity {
ActivityLevel::Complete => colors::SUCCESS,
ActivityLevel::Error => colors::ERROR,
_ => pulsed_color,
};
for i in 0..5 {
let symbol = if i < dots_filled { "โ" } else { "โ" };
let x = inner_x + i as u16 * 2;
if x < area.x + area.width - 1 {
buf[(x, activity_y)]
.set_symbol(symbol)
.set_style(Style::default().fg(activity_color));
}
}
let status = self.activity.symbol();
let status_x = inner_x + 11;
if status_x < area.x + area.width - 1 {
buf[(status_x, activity_y)].set_symbol(status).set_style(
Style::default()
.fg(activity_color)
.add_modifier(Modifier::BOLD),
);
}
}
if area.height > 4 {
let token_y = inner_y + 2;
let token_text = format!("๐ซ {}", self.format_tokens());
let display_text = if self.ascii_mode {
format!("* {}", self.format_tokens())
} else {
token_text
};
let mut token_x_offset = 0u16;
for ch in display_text.chars() {
let char_width = ch.width().unwrap_or(1) as u16;
if inner_x + token_x_offset + char_width <= area.x + area.width - 1 {
buf[(inner_x + token_x_offset, token_y)]
.set_symbol(&ch.to_string())
.set_style(Style::default().fg(Color::Gray));
token_x_offset += char_width;
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_agent_role_icon() {
assert_eq!(AgentRole::Coder.icon(), "๐ป");
assert_eq!(AgentRole::Architect.icon(), "๐๏ธ");
}
#[test]
fn test_agent_role_color() {
let color = AgentRole::Coder.color();
assert_eq!(color, colors::SECONDARY);
}
#[test]
fn test_agent_avatar_new() {
let avatar = AgentAvatar::new(AgentRole::Coder);
assert_eq!(avatar.role(), AgentRole::Coder);
assert_eq!(avatar.activity(), ActivityLevel::Idle);
}
#[test]
fn test_agent_avatar_with_activity() {
let avatar = AgentAvatar::new(AgentRole::Tester).with_activity(ActivityLevel::High);
assert_eq!(avatar.activity(), ActivityLevel::High);
}
#[test]
fn test_agent_avatar_format_tokens() {
let avatar1 = AgentAvatar::new(AgentRole::Coder).with_tokens(500);
assert_eq!(avatar1.format_tokens(), "500");
let avatar2 = AgentAvatar::new(AgentRole::Coder).with_tokens(5_000);
assert_eq!(avatar2.format_tokens(), "5K");
let avatar3 = AgentAvatar::new(AgentRole::Coder).with_tokens(1_500_000);
assert_eq!(avatar3.format_tokens(), "1.5M");
}
#[test]
fn test_activity_level_dots() {
assert_eq!(ActivityLevel::Idle.dots(), 0);
assert_eq!(ActivityLevel::Low.dots(), 1);
assert_eq!(ActivityLevel::Max.dots(), 4);
assert_eq!(ActivityLevel::Complete.dots(), 5);
}
#[test]
fn test_all_agent_role_icons() {
assert_eq!(AgentRole::Architect.icon(), "๐๏ธ");
assert_eq!(AgentRole::Tester.icon(), "๐งช");
assert_eq!(AgentRole::Reviewer.icon(), "๐๏ธ");
assert_eq!(AgentRole::Documenter.icon(), "๐");
assert_eq!(AgentRole::DevOps.icon(), "๐");
assert_eq!(AgentRole::Security.icon(), "๐");
assert_eq!(AgentRole::Performance.icon(), "โก");
}
#[test]
fn test_all_agent_role_ascii_icons() {
assert_eq!(AgentRole::Architect.ascii_icon(), "[A]");
assert_eq!(AgentRole::Coder.ascii_icon(), "[C]");
assert_eq!(AgentRole::Tester.ascii_icon(), "[T]");
assert_eq!(AgentRole::Reviewer.ascii_icon(), "[R]");
assert_eq!(AgentRole::Documenter.ascii_icon(), "[D]");
assert_eq!(AgentRole::DevOps.ascii_icon(), "[O]");
assert_eq!(AgentRole::Security.ascii_icon(), "[S]");
assert_eq!(AgentRole::Performance.ascii_icon(), "[P]");
}
#[test]
fn test_all_agent_role_names() {
assert_eq!(AgentRole::Architect.name(), "Architect");
assert_eq!(AgentRole::Coder.name(), "Coder");
assert_eq!(AgentRole::Tester.name(), "Tester");
assert_eq!(AgentRole::Reviewer.name(), "Reviewer");
assert_eq!(AgentRole::Documenter.name(), "Documenter");
assert_eq!(AgentRole::DevOps.name(), "DevOps");
assert_eq!(AgentRole::Security.name(), "Security");
assert_eq!(AgentRole::Performance.name(), "Performance");
}
#[test]
fn test_all_agent_role_colors() {
let _ = AgentRole::Architect.color();
let _ = AgentRole::Tester.color();
let _ = AgentRole::Reviewer.color();
let _ = AgentRole::Documenter.color();
let _ = AgentRole::DevOps.color();
let _ = AgentRole::Security.color();
let _ = AgentRole::Performance.color();
}
#[test]
fn test_all_activity_level_symbols() {
assert_eq!(ActivityLevel::Idle.symbol(), "โ");
assert_eq!(ActivityLevel::Low.symbol(), "โ");
assert_eq!(ActivityLevel::Medium.symbol(), "โ");
assert_eq!(ActivityLevel::High.symbol(), "โ");
assert_eq!(ActivityLevel::Max.symbol(), "โ");
assert_eq!(ActivityLevel::Complete.symbol(), "โ");
assert_eq!(ActivityLevel::Error.symbol(), "โ");
}
#[test]
fn test_with_name() {
let avatar = AgentAvatar::new(AgentRole::Coder).with_name("my-coder");
assert_eq!(avatar.name, "my-coder");
}
#[test]
fn test_ascii_mode() {
let avatar = AgentAvatar::new(AgentRole::Coder).ascii_mode(true);
assert!(avatar.ascii_mode);
}
#[test]
fn test_set_tokens_and_add_tokens() {
let mut avatar = AgentAvatar::new(AgentRole::Coder);
avatar.set_tokens(100);
assert_eq!(avatar.token_count, 100);
avatar.add_tokens(50);
assert_eq!(avatar.token_count, 150);
}
#[test]
fn test_update_advances_pulse() {
let mut avatar = AgentAvatar::new(AgentRole::Coder).with_activity(ActivityLevel::High);
let initial_phase = avatar.pulse_phase;
avatar.update(0.5);
assert!(avatar.pulse_phase > initial_phase);
}
#[test]
fn test_update_wraps_pulse() {
let mut avatar = AgentAvatar::new(AgentRole::Coder).with_activity(ActivityLevel::Max);
for _ in 0..100 {
avatar.update(0.5);
}
assert!(avatar.pulse_phase < std::f32::consts::PI * 2.0);
}
#[test]
fn test_is_complete_always_false() {
let avatar = AgentAvatar::new(AgentRole::Coder);
assert!(!avatar.is_complete());
}
#[test]
fn test_activity_level_default() {
let level = ActivityLevel::default();
assert_eq!(level, ActivityLevel::Idle);
}
#[test]
fn test_medium_dots() {
assert_eq!(ActivityLevel::Medium.dots(), 2);
assert_eq!(ActivityLevel::High.dots(), 3);
assert_eq!(ActivityLevel::Error.dots(), 0);
}
}