use crossterm::style::Color;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(not(feature = "plugin"), allow(dead_code))]
pub enum AvatarState {
Idle,
Thinking,
Speaking,
Reading,
Writing,
Bash,
Alert,
Error,
Done,
}
impl AvatarState {
pub fn from_tool_name(name: &str) -> Self {
match name {
"read" | "grep" | "find_files" | "list_dir" | "lsp" | "semantic" => Self::Reading,
"write" | "edit" | "apply_patch" | "write_todo_list" => Self::Writing,
"bash" | "shell" => Self::Bash,
_ => Self::Reading,
}
}
}
#[allow(dead_code)]
pub const AVATAR_W: usize = 5;
pub fn art(state: AvatarState, tick: bool) -> &'static str {
use AvatarState::*;
match state {
Idle => {
if tick {
"(o o)"
} else {
"(- -)"
}
}
Thinking => {
if tick {
"(o .)"
} else {
"(. o)"
}
}
Speaking => {
if tick {
"(o o)"
} else {
"(o O)"
}
}
Reading => "[@ @]",
Writing => {
if tick {
"(>_<)"
} else {
"(-_-)"
}
}
Bash => "[$_$]",
Alert => "(O_O)",
Error => "(x_x)",
Done => "(^_^)",
}
}
pub fn color(state: AvatarState) -> Color {
use AvatarState::*;
match state {
Alert => crate::ui::theme::perm(),
Error => crate::ui::theme::error(),
Done => crate::ui::theme::accent(),
_ => crate::ui::theme::agent(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_state_has_uniform_width() {
let states = [
AvatarState::Idle,
AvatarState::Thinking,
AvatarState::Speaking,
AvatarState::Reading,
AvatarState::Writing,
AvatarState::Bash,
AvatarState::Alert,
AvatarState::Error,
AvatarState::Done,
];
for state in states {
for tick in [false, true] {
let face = art(state, tick);
assert_eq!(
face.chars().count(),
AVATAR_W,
"{:?} tick={} is {:?}",
state,
tick,
face,
);
}
}
}
#[test]
fn tool_name_maps_to_state() {
assert_eq!(AvatarState::from_tool_name("read"), AvatarState::Reading);
assert_eq!(AvatarState::from_tool_name("grep"), AvatarState::Reading);
assert_eq!(AvatarState::from_tool_name("edit"), AvatarState::Writing);
assert_eq!(AvatarState::from_tool_name("write"), AvatarState::Writing);
assert_eq!(AvatarState::from_tool_name("bash"), AvatarState::Bash);
assert_eq!(
AvatarState::from_tool_name("mcp_some_tool"),
AvatarState::Reading
);
}
#[test]
fn permission_allow_reset_never_lands_on_alert() {
let gated_tools = [
"read",
"grep",
"find_files",
"list_dir",
"lsp",
"semantic",
"write",
"edit",
"apply_patch",
"write_todo_list",
"bash",
"shell",
"memory",
"skill",
"webfetch",
"task",
"mcp_tool:server:name",
];
for tool in gated_tools {
let state = AvatarState::from_tool_name(tool);
assert!(
matches!(
state,
AvatarState::Reading | AvatarState::Writing | AvatarState::Bash
),
"tool {:?} reset to non-working avatar state {:?}",
tool,
state,
);
assert_ne!(
state,
AvatarState::Alert,
"tool {:?} must not reset to the Alert face",
tool,
);
}
}
}