use std::borrow::Cow;
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use crate::localization::{Locale, MessageId, tr};
use crate::palette::{self, UiTheme};
use crate::tools::subagent::{AgentWorkerStatus, FleetRole, SubAgentResult, SubAgentStatus};
use crate::tui::glyphs;
use crate::tui::motion::mode::MotionMode;
use crate::tui::underwater::ShellPhase;
pub const PORTRAIT_WIDTH: usize = 14;
pub const PORTRAIT_HEIGHT: usize = 3;
pub const BADGE_WIDTH: usize = 2;
pub const WORKING_FRAME_MS: u64 = 180;
pub const WORKING_FRAMES: usize = 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum WhaleSpecies {
Scout,
Patch,
Harbor,
Echo,
Keel,
Lantern,
Plain,
}
impl WhaleSpecies {
#[allow(dead_code)]
pub const ALL: [WhaleSpecies; 7] = [
Self::Scout,
Self::Patch,
Self::Harbor,
Self::Echo,
Self::Keel,
Self::Lantern,
Self::Plain,
];
#[must_use]
pub fn for_role_id(role: &str) -> Self {
match role.trim().to_ascii_lowercase().as_str() {
"scout" => Self::Scout,
"builder" => Self::Patch,
"manager" | "planner" => Self::Harbor,
"reviewer" => Self::Lantern,
"verifier" => Self::Keel,
"consultant" | "synthesizer" => Self::Echo,
_ => Self::Plain,
}
}
#[must_use]
pub fn for_fleet_role(role: &FleetRole) -> Self {
Self::for_role_id(role.as_str())
}
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Scout => "Scout",
Self::Patch => "Patch",
Self::Harbor => "Harbor",
Self::Echo => "Echo",
Self::Keel => "Keel",
Self::Lantern => "Lantern",
Self::Plain => "Codewhale",
}
}
#[must_use]
pub fn animal(self, locale: Locale) -> Cow<'static, str> {
tr(
locale,
match self {
Self::Scout => MessageId::WhaleAnimalScout,
Self::Patch => MessageId::WhaleAnimalPatch,
Self::Harbor => MessageId::WhaleAnimalHarbor,
Self::Echo => MessageId::WhaleAnimalEcho,
Self::Keel => MessageId::WhaleAnimalKeel,
Self::Lantern => MessageId::WhaleAnimalLantern,
Self::Plain => MessageId::WhaleAnimalPlain,
},
)
}
#[must_use]
pub fn job(self, locale: Locale) -> Cow<'static, str> {
tr(
locale,
match self {
Self::Scout => MessageId::WhaleJobScout,
Self::Patch => MessageId::WhaleJobPatch,
Self::Harbor => MessageId::WhaleJobHarbor,
Self::Echo => MessageId::WhaleJobEcho,
Self::Keel => MessageId::WhaleJobKeel,
Self::Lantern => MessageId::WhaleJobLantern,
Self::Plain => MessageId::WhaleJobPlain,
},
)
}
#[must_use]
pub const fn badge_glyphs(self) -> (&'static str, &'static str, bool) {
match self {
Self::Scout => ("◂", "▰", true), Self::Patch => ("]", "▰", false), Self::Harbor => ("▚", "▰", false), Self::Echo => (":", "▰", true), Self::Keel => ("━", "▰", false), Self::Lantern => ("◇", "▰", true), Self::Plain => ("·", "▰", true), }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum WhaleState {
Resting,
Offline,
Thinking,
Working,
Blocked,
Waiting,
}
impl WhaleState {
#[allow(dead_code)]
pub const ALL: [WhaleState; 6] = [
Self::Resting,
Self::Thinking,
Self::Working,
Self::Waiting,
Self::Blocked,
Self::Offline,
];
#[allow(dead_code)]
#[must_use]
pub const fn priority(self) -> u8 {
match self {
Self::Waiting => 60,
Self::Blocked => 50,
Self::Working => 40,
Self::Thinking => 30,
Self::Offline => 20,
Self::Resting => 10,
}
}
#[must_use]
pub fn word(self, locale: Locale) -> Cow<'static, str> {
tr(
locale,
match self {
Self::Resting => MessageId::WhaleStateResting,
Self::Thinking => MessageId::WhaleStateThinking,
Self::Working => MessageId::WhaleStateWorking,
Self::Waiting => MessageId::WhaleStateWaiting,
Self::Blocked => MessageId::WhaleStateBlocked,
Self::Offline => MessageId::WhaleStateOffline,
},
)
}
#[must_use]
pub fn for_subagent(agent: &SubAgentResult) -> Self {
if agent.needs_input.is_some() {
return Self::Waiting;
}
if let Some(status) = agent.worker_status {
return match status {
AgentWorkerStatus::WaitingForUser | AgentWorkerStatus::Interrupted => Self::Waiting,
AgentWorkerStatus::Queued
| AgentWorkerStatus::Starting
| AgentWorkerStatus::ModelWait => Self::Thinking,
AgentWorkerStatus::Running | AgentWorkerStatus::RunningTool => Self::Working,
AgentWorkerStatus::Failed => Self::Blocked,
AgentWorkerStatus::Cancelled => Self::Offline,
AgentWorkerStatus::Completed => Self::Resting,
};
}
match agent.status {
SubAgentStatus::Running => Self::Working,
SubAgentStatus::Completed => Self::Resting,
SubAgentStatus::Interrupted(_) => Self::Waiting,
SubAgentStatus::Failed(_) | SubAgentStatus::BudgetExhausted => Self::Blocked,
SubAgentStatus::Cancelled => Self::Offline,
}
}
#[allow(dead_code)]
#[must_use]
pub const fn for_shell_phase(phase: ShellPhase) -> Self {
match phase {
ShellPhase::Idle | ShellPhase::Done => Self::Resting,
ShellPhase::Typing => Self::Thinking,
ShellPhase::Working | ShellPhase::Verifying => Self::Working,
ShellPhase::Waiting | ShellPhase::Approval => Self::Waiting,
ShellPhase::Failed => Self::Blocked,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WhaleInk {
pub body: Color,
pub lantern_body: Color,
pub detail: Color,
pub current: Color,
pub human: Color,
pub bar: Color,
pub dim: Color,
pub scout: Color,
pub patch: Color,
pub harbor: Color,
pub echo: Color,
pub keel: Color,
pub lantern: Color,
}
impl WhaleInk {
#[must_use]
pub fn from_theme(theme: &UiTheme) -> Self {
let surface = theme.surface_bg;
let lift = |color: Color| {
palette::enforce_contrast(color, surface, palette::SECONDARY_CHROME_CONTRAST)
};
let rgb = |(r, g, b): (u8, u8, u8)| Color::Rgb(r, g, b);
Self {
body: lift(theme.accent_action),
lantern_body: lift(theme.text_muted),
detail: theme.text_body,
current: lift(rgb(palette::WHALE_CYAN_RGB)),
human: lift(theme.accent_action),
bar: lift(theme.text_muted),
dim: theme.text_dim,
scout: lift(rgb(palette::WHALE_CYAN_RGB)),
patch: lift(theme.accent_secondary),
harbor: lift(rgb(palette::WHALE_BRAND_ORANGE_RGB)),
echo: lift(rgb(palette::WHALE_BRAND_MAGENTA_RGB)),
keel: lift(theme.warning),
lantern: lift(theme.mode_operate),
}
}
#[must_use]
pub const fn accent(&self, species: WhaleSpecies) -> Color {
match species {
WhaleSpecies::Scout => self.scout,
WhaleSpecies::Patch => self.patch,
WhaleSpecies::Harbor => self.harbor,
WhaleSpecies::Echo => self.echo,
WhaleSpecies::Keel => self.keel,
WhaleSpecies::Lantern => self.lantern,
WhaleSpecies::Plain => self.body,
}
}
#[must_use]
pub const fn body_for(&self, species: WhaleSpecies) -> Color {
match species {
WhaleSpecies::Lantern => self.lantern_body,
_ => self.body,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Ink {
None,
Body,
Accent,
Detail,
Cue,
Human,
Bar,
}
impl Ink {
fn from_code(code: char) -> Self {
match code {
'b' => Self::Body,
'a' => Self::Accent,
'd' => Self::Detail,
'c' => Self::Cue,
'h' => Self::Human,
'i' => Self::Bar,
_ => Self::None,
}
}
}
struct Art {
rows: [&'static str; PORTRAIT_HEIGHT],
inks: [&'static str; PORTRAIT_HEIGHT],
}
const fn art(species: WhaleSpecies) -> Art {
match species {
WhaleSpecies::Scout => Art {
rows: [" ▗▄▄▖ ▚△▞", " ━▐█○██▙━━━▞ ", " ▝▀▀▀▘ "],
inks: [" bbbb bdb", " bbbabbbbbbb ", " bbbbb "],
},
WhaleSpecies::Patch => Art {
rows: [" ▗▄▟▄▄▖ ▚△▞ ", " ▐█·[]▙━━▞ ", " ▝▀▀▀▀▘ "],
inks: [" bbbbbb bdb ", " bbdaabbbb ", " bbbbbb "],
},
WhaleSpecies::Harbor => Art {
rows: [" ▗▄▄▄▄▄▖ ▚△▞", " ▐▒·█○█▙━━▞ ", " ▝▀▚▀▀▀▘ "],
inks: [" bbbbbbb bdb", " bbdbabbbbb ", " bbbbbbb "],
},
WhaleSpecies::Echo => Art {
rows: [" ▄▄▄▄▟▄▖ ▚△▞", " :▐█·███▙━━▞ ", " ·▝▀▞▀▀▀▘ "],
inks: [" bbbbbbb bdb", " abbdbbbbbbb ", " abbbbbbb "],
},
WhaleSpecies::Keel => Art {
rows: [" ▛▀▀▀▀▄▖ ▚△▞", " ▐█·███▙▄▄▞ ", " ▝▀━━━▀▘ "],
inks: [" bbbbbbb bdb", " bbdbbbbbbb ", " bbaaabb "],
},
WhaleSpecies::Lantern => Art {
rows: [" ▗▄▐▄▄▖ ▚△▞ ", " ▐█○█▘▙━━▞ ", " ▝▀▀▀▀▘ "],
inks: [" bbbbbb bdb ", " bbabhbbbb ", " bbbbbb "],
},
WhaleSpecies::Plain => Art {
rows: [" ▗▄▄▄▄▄▖ ▚△▞", " ▐█·███▙━━▞ ", " ▝▀▀▀▀▀▘ "],
inks: [" bbbbbbb bdb", " bbdbbbbbbb ", " bbbbbbb "],
},
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Cell {
glyph: &'static str,
ink: Ink,
}
type Canvas = [[Cell; PORTRAIT_WIDTH]; PORTRAIT_HEIGHT];
fn blank() -> Canvas {
[[Cell {
glyph: " ",
ink: Ink::None,
}; PORTRAIT_WIDTH]; PORTRAIT_HEIGHT]
}
fn glyph_at(row: &'static str, col: usize) -> Option<&'static str> {
let mut indices = row.char_indices().skip(col);
let (start, ch) = indices.next()?;
Some(&row[start..start + ch.len_utf8()])
}
fn compose(species: WhaleSpecies, state: Option<WhaleState>, frame: usize) -> Canvas {
let art = art(species);
let mut canvas = blank();
for (canvas_row, (row, inks)) in canvas.iter_mut().zip(art.rows.iter().zip(art.inks.iter())) {
for (c, cell) in canvas_row.iter_mut().enumerate() {
let glyph = glyph_at(row, c).unwrap_or(" ");
let ink = inks.chars().nth(c).map(Ink::from_code).unwrap_or(Ink::None);
*cell = Cell { glyph, ink };
}
}
let Some(state) = state else {
return canvas;
};
match state {
WhaleState::Resting => {}
WhaleState::Thinking => {
canvas[0][0] = Cell {
glyph: "˚",
ink: Ink::Cue,
};
canvas[0][1] = Cell {
glyph: "˚",
ink: Ink::Cue,
};
}
WhaleState::Working => {
const WAKE: [[&str; 4]; WORKING_FRAMES] = [
["·", " ", " ", " "],
["·", "˚", " ", " "],
[" ", "·", "˚", "·"],
[" ", " ", "˚", "·"],
];
let wake = WAKE[frame % WORKING_FRAMES];
for (i, glyph) in wake.iter().enumerate() {
canvas[2][10 + i] = Cell {
glyph,
ink: Ink::Cue,
};
}
}
WhaleState::Waiting => {
for (r, (left, right)) in [("╭", "─"), ("│", " "), ("╰", "─")].iter().enumerate()
{
canvas[r][0] = Cell {
glyph: left,
ink: Ink::Human,
};
if *right != " " {
canvas[r][1] = Cell {
glyph: right,
ink: Ink::Human,
};
}
}
}
WhaleState::Blocked => {
for row in canvas.iter_mut() {
row[1] = Cell {
glyph: "▌",
ink: Ink::Bar,
};
}
}
WhaleState::Offline => {
for row in canvas.iter_mut() {
for cell in row.iter_mut() {
if cell.glyph == "█" {
cell.glyph = "░";
}
}
}
}
}
canvas
}
fn cell_color(
cell: Cell,
species: WhaleSpecies,
state: Option<WhaleState>,
ink: &WhaleInk,
) -> Color {
if state == Some(WhaleState::Offline) && cell.ink != Ink::None {
return ink.dim;
}
match cell.ink {
Ink::None => ink.dim,
Ink::Body => ink.body_for(species),
Ink::Accent => ink.accent(species),
Ink::Detail => ink.detail,
Ink::Cue => ink.current,
Ink::Human => ink.human,
Ink::Bar => ink.bar,
}
}
#[must_use]
pub const fn working_frame(now_ms: u64, mode: MotionMode) -> usize {
match mode {
MotionMode::Full => ((now_ms / WORKING_FRAME_MS) % WORKING_FRAMES as u64) as usize,
MotionMode::Reduced | MotionMode::Still => 0,
}
}
#[must_use]
pub fn portrait(
species: WhaleSpecies,
state: Option<WhaleState>,
frame: usize,
theme: &UiTheme,
) -> Vec<Line<'static>> {
let ink = WhaleInk::from_theme(theme);
let canvas = compose(species, state, frame);
canvas
.iter()
.map(|row| {
let mut spans: Vec<Span<'static>> = Vec::new();
let mut run = String::new();
let mut run_color: Option<Color> = None;
let flush = |run: &mut String, color: Option<Color>, spans: &mut Vec<Span<'static>>| {
if run.is_empty() {
return;
}
let style = match color {
Some(color) => Style::default().fg(color),
None => Style::default(),
};
spans.push(Span::styled(std::mem::take(run), style));
};
let mut leading = true;
for cell in row {
let color = if cell.ink == Ink::None {
None
} else {
Some(cell_color(*cell, species, state, &ink))
};
if color != run_color && !run.is_empty() {
flush(&mut run, run_color, &mut spans);
}
run_color = color;
if leading && cell.glyph == " " {
run.push('\u{2800}');
} else {
leading = false;
run.push_str(cell.glyph);
}
}
flush(&mut run, run_color, &mut spans);
Line::from(spans)
})
.collect()
}
#[allow(dead_code)] #[must_use]
pub fn portrait_ascii(
species: WhaleSpecies,
state: Option<WhaleState>,
frame: usize,
) -> [String; 3] {
let canvas = compose(species, state, frame);
let mut rows: [String; 3] = Default::default();
for (r, row) in canvas.iter().enumerate() {
for cell in row {
let glyph = cell.glyph;
rows[r].push_str(if glyph.is_ascii() {
glyph
} else {
glyphs::ascii_fallback(glyph).unwrap_or("?")
});
}
}
rows
}
#[allow(dead_code)] #[must_use]
pub fn portrait_text(
species: WhaleSpecies,
state: Option<WhaleState>,
frame: usize,
) -> [String; 3] {
let canvas = compose(species, state, frame);
let mut rows: [String; 3] = Default::default();
for (r, row) in canvas.iter().enumerate() {
for cell in row {
rows[r].push_str(cell.glyph);
}
}
rows
}
#[must_use]
pub fn badge(species: WhaleSpecies, theme: &UiTheme) -> Vec<Span<'static>> {
let ink = WhaleInk::from_theme(theme);
let (feature, body, feature_first) = species.badge_glyphs();
let feature_span = Span::styled(
feature,
Style::default()
.fg(ink.accent(species))
.add_modifier(Modifier::BOLD),
);
let body_span = Span::styled(body, Style::default().fg(ink.body_for(species)));
if feature_first {
vec![feature_span, body_span]
} else {
vec![body_span, feature_span]
}
}
#[allow(dead_code)] #[must_use]
pub fn badge_with_state(
species: WhaleSpecies,
state: Option<WhaleState>,
theme: &UiTheme,
locale: Locale,
) -> Vec<Span<'static>> {
badge_with_state_frame(species, state, 0, theme, locale)
}
#[must_use]
pub fn badge_with_state_frame(
species: WhaleSpecies,
state: Option<WhaleState>,
frame: usize,
theme: &UiTheme,
locale: Locale,
) -> Vec<Span<'static>> {
let mut spans = badge(species, theme);
if let Some(state) = state {
let ink = WhaleInk::from_theme(theme);
let (cue, tone) = state_cue(state, frame, &ink, theme);
spans.push(Span::raw(" "));
if !cue.is_empty() {
spans.push(Span::styled(format!("{cue} "), Style::default().fg(tone)));
}
spans.push(Span::styled(
state.word(locale).into_owned(),
Style::default().fg(tone),
));
}
spans
}
fn state_cue(
state: WhaleState,
frame: usize,
ink: &WhaleInk,
theme: &UiTheme,
) -> (&'static str, Color) {
const WAKE_CUE: [&str; WORKING_FRAMES] = ["·", "˚", "·", "˚"];
match state {
WhaleState::Resting => ("", theme.text_muted),
WhaleState::Thinking => ("˚", ink.current),
WhaleState::Working => (WAKE_CUE[frame % WORKING_FRAMES], ink.current),
WhaleState::Waiting => (glyphs::ATTENTION, ink.human),
WhaleState::Blocked => ("▌", ink.bar),
WhaleState::Offline => ("░", ink.dim),
}
}
#[allow(dead_code)] #[must_use]
pub fn badge_ascii(species: WhaleSpecies) -> String {
let (feature, body, feature_first) = species.badge_glyphs();
let narrow = |glyph: &'static str| -> &'static str {
if glyph.is_ascii() {
glyph
} else {
glyphs::ascii_fallback(glyph).unwrap_or("?")
}
};
if feature_first {
format!("{}{}", narrow(feature), narrow(body))
} else {
format!("{}{}", narrow(body), narrow(feature))
}
}
#[must_use]
pub const fn portrait_fits(width: u16) -> bool {
width as usize >= 60
}
#[cfg(test)]
mod tests {
use super::*;
use crate::palette::contrast_ratio;
use unicode_width::UnicodeWidthStr;
fn theme_dark() -> UiTheme {
palette::UI_THEME
}
#[test]
fn role_table_is_total_and_never_guesses() {
assert_eq!(WhaleSpecies::for_role_id("scout"), WhaleSpecies::Scout);
assert_eq!(WhaleSpecies::for_role_id("builder"), WhaleSpecies::Patch);
assert_eq!(WhaleSpecies::for_role_id("manager"), WhaleSpecies::Harbor);
assert_eq!(WhaleSpecies::for_role_id("planner"), WhaleSpecies::Harbor);
assert_eq!(WhaleSpecies::for_role_id("reviewer"), WhaleSpecies::Lantern);
assert_eq!(WhaleSpecies::for_role_id("verifier"), WhaleSpecies::Keel);
assert_eq!(WhaleSpecies::for_role_id("consultant"), WhaleSpecies::Echo);
assert_eq!(WhaleSpecies::for_role_id("synthesizer"), WhaleSpecies::Echo);
for plain in [
"worker",
"general",
"custom",
"",
"mystery-role",
"Scouting",
] {
assert_eq!(
WhaleSpecies::for_role_id(plain),
WhaleSpecies::Plain,
"{plain}"
);
}
assert_eq!(
WhaleSpecies::for_role_id(" Reviewer "),
WhaleSpecies::Lantern
);
for role in [
FleetRole::Worker,
FleetRole::Scout,
FleetRole::Planner,
FleetRole::Reviewer,
FleetRole::Builder,
FleetRole::Verifier,
FleetRole::Consultant,
FleetRole::Custom,
] {
let _ = WhaleSpecies::for_fleet_role(&role);
}
assert_eq!(
WhaleSpecies::for_fleet_role(&FleetRole::Custom),
WhaleSpecies::Plain
);
}
#[test]
fn every_species_and_state_renders_inside_the_canvas() {
for species in WhaleSpecies::ALL {
let states = std::iter::once(None).chain(WhaleState::ALL.into_iter().map(Some));
for state in states {
for frame in 0..WORKING_FRAMES {
let rows = portrait_text(species, state, frame);
for row in &rows {
assert_eq!(
UnicodeWidthStr::width(row.as_str()),
PORTRAIT_WIDTH,
"{species:?} {state:?} f{frame}: {row:?}"
);
}
let lines = portrait(species, state, frame, &theme_dark());
assert_eq!(lines.len(), PORTRAIT_HEIGHT);
for line in &lines {
assert_eq!(line.width(), PORTRAIT_WIDTH, "{species:?} {state:?}");
}
}
}
}
}
#[test]
fn authored_art_rows_and_ink_maps_agree() {
for species in WhaleSpecies::ALL {
let art = art(species);
for (row, inks) in art.rows.iter().zip(art.inks.iter()) {
assert_eq!(row.chars().count(), PORTRAIT_WIDTH, "{species:?} {row:?}");
assert_eq!(inks.chars().count(), PORTRAIT_WIDTH, "{species:?} {inks:?}");
for (glyph, code) in row.chars().zip(inks.chars()) {
assert_eq!(
glyph == ' ',
code == ' ',
"{species:?}: glyph {glyph:?} vs ink {code:?} in {row:?}"
);
}
}
assert!(art.rows[0].starts_with(" "), "{species:?} row0 cue lane");
assert!(art.rows[2].starts_with(' '), "{species:?} row2 cue lane");
assert!(
art.rows[2].chars().skip(10).all(|c| c == ' '),
"{species:?} wake lane"
);
}
}
#[test]
fn every_glyph_has_an_ascii_fallback_and_ascii_silhouettes_are_pure() {
for species in WhaleSpecies::ALL {
let states = std::iter::once(None).chain(WhaleState::ALL.into_iter().map(Some));
for state in states {
for frame in 0..WORKING_FRAMES {
let unicode = portrait_text(species, state, frame);
for row in &unicode {
for ch in row.chars() {
let glyph = ch.to_string();
assert!(
ch.is_ascii() || glyphs::ascii_fallback(&glyph).is_some(),
"{species:?} {state:?}: {glyph:?} has no ASCII fallback"
);
}
}
let ascii = portrait_ascii(species, state, frame);
for row in &ascii {
assert!(row.is_ascii(), "{species:?} {state:?}: {row:?}");
assert_eq!(row.chars().count(), PORTRAIT_WIDTH);
assert!(!row.contains('?'), "{species:?} {state:?}: {row:?}");
}
}
}
let badge = badge_ascii(species);
assert!(
badge.is_ascii() && badge.chars().count() == BADGE_WIDTH,
"{badge:?}"
);
}
let mut badges: Vec<String> = WhaleSpecies::ALL.iter().map(|s| badge_ascii(*s)).collect();
badges.sort();
badges.dedup();
assert_eq!(badges.len(), WhaleSpecies::ALL.len(), "{badges:?}");
}
#[test]
fn plain_whale_matches_the_codewhale_mark_vocabulary() {
let rows = portrait_text(WhaleSpecies::Plain, None, 0);
assert_eq!(rows[0], " ▗▄▄▄▄▄▖ ▚△▞");
assert_eq!(rows[1], " ▐█·███▙━━▞ ");
assert_eq!(rows[2], " ▝▀▀▀▀▀▘ ");
let ascii = portrait_ascii(WhaleSpecies::Plain, None, 0);
assert_eq!(ascii[0], " .#####. \\^/");
assert_eq!(ascii[1], " |#.####--/ ");
assert_eq!(ascii[2], " .#####. ");
}
#[test]
fn state_cues_follow_the_visual_brief() {
let thinking = portrait_text(WhaleSpecies::Scout, Some(WhaleState::Thinking), 0);
assert!(thinking[0].starts_with("˚˚"), "{thinking:?}");
let working0 = portrait_text(WhaleSpecies::Scout, Some(WhaleState::Working), 0);
assert!(working0[2].ends_with("· "), "{working0:?}");
let working2 = portrait_text(WhaleSpecies::Scout, Some(WhaleState::Working), 2);
assert_ne!(working0[2], working2[2]);
let waiting = portrait_text(WhaleSpecies::Harbor, Some(WhaleState::Waiting), 0);
assert!(
waiting[0].starts_with("╭─") && waiting[1].starts_with('│'),
"{waiting:?}"
);
assert!(waiting[2].starts_with("╰─"), "{waiting:?}");
let blocked = portrait_text(WhaleSpecies::Keel, Some(WhaleState::Blocked), 0);
assert!(
blocked.iter().all(|r| r.chars().nth(1) == Some('▌')),
"{blocked:?}"
);
let offline = portrait_text(WhaleSpecies::Echo, Some(WhaleState::Offline), 0);
assert!(!offline.iter().any(|r| r.contains('█')), "{offline:?}");
assert!(offline[1].contains('░'), "{offline:?}");
for state in [None, Some(WhaleState::Resting)] {
let rows = portrait_text(WhaleSpecies::Plain, state, 0);
assert!(rows.iter().all(|r| r.starts_with(" ")), "{rows:?}");
}
}
#[test]
fn working_wake_only_animates_under_full_motion() {
assert_eq!(working_frame(0, MotionMode::Full), 0);
assert_eq!(working_frame(180, MotionMode::Full), 1);
assert_eq!(working_frame(540, MotionMode::Full), 3);
assert_eq!(working_frame(720, MotionMode::Full), 0);
for now in [0, 180, 360, 540, 90_000] {
assert_eq!(working_frame(now, MotionMode::Reduced), 0);
assert_eq!(working_frame(now, MotionMode::Still), 0);
}
}
#[test]
fn every_state_pairs_a_cue_with_a_word_in_every_shipped_locale() {
let theme = theme_dark();
let ink = WhaleInk::from_theme(&theme);
for state in WhaleState::ALL {
for locale in Locale::shipped_complete() {
let word = state.word(*locale);
assert!(!word.trim().is_empty(), "{state:?} {locale:?}");
let spans = badge_with_state(WhaleSpecies::Scout, Some(state), &theme, *locale);
let text: String = spans.iter().map(|s| s.content.as_ref()).collect();
assert!(text.contains(word.as_ref()), "{state:?} {locale:?}: {text}");
}
let (cue, _) = state_cue(state, 0, &ink, &theme);
if state != WhaleState::Resting {
assert!(!cue.is_empty(), "{state:?} needs a glyph cue");
}
}
let spans = badge_with_state(WhaleSpecies::Patch, None, &theme, Locale::En);
assert_eq!(spans.len(), BADGE_WIDTH);
}
#[test]
fn subagent_state_is_derived_from_runtime_facts_only() {
let mut agent = SubAgentResult {
name: "child-1".into(),
agent_id: "child-1".into(),
context_mode: "fresh".into(),
fork_context: false,
workspace: None,
git_branch: None,
agent_type: FleetRole::Builder,
assignment: crate::tools::subagent::SubAgentAssignment {
objective: "objective".into(),
role: None,
},
model: String::new(),
nickname: None,
status: SubAgentStatus::Running,
worker_status: None,
runtime_permissions: None,
parent_run_id: None,
spawn_depth: 0,
child_route: None,
result: None,
steps_taken: 0,
checkpoint: None,
needs_input: None,
duration_ms: 0,
started_at: None,
from_prior_session: false,
};
assert_eq!(WhaleState::for_subagent(&agent), WhaleState::Working);
agent.status = SubAgentStatus::Completed;
assert_eq!(WhaleState::for_subagent(&agent), WhaleState::Resting);
agent.status = SubAgentStatus::Interrupted("parent".into());
assert_eq!(WhaleState::for_subagent(&agent), WhaleState::Waiting);
agent.status = SubAgentStatus::Failed("boom".into());
assert_eq!(WhaleState::for_subagent(&agent), WhaleState::Blocked);
agent.status = SubAgentStatus::BudgetExhausted;
assert_eq!(WhaleState::for_subagent(&agent), WhaleState::Blocked);
agent.status = SubAgentStatus::Cancelled;
assert_eq!(WhaleState::for_subagent(&agent), WhaleState::Offline);
agent.status = SubAgentStatus::Running;
agent.worker_status = Some(AgentWorkerStatus::ModelWait);
assert_eq!(WhaleState::for_subagent(&agent), WhaleState::Thinking);
agent.worker_status = Some(AgentWorkerStatus::RunningTool);
assert_eq!(WhaleState::for_subagent(&agent), WhaleState::Working);
agent.worker_status = Some(AgentWorkerStatus::WaitingForUser);
assert_eq!(WhaleState::for_subagent(&agent), WhaleState::Waiting);
agent.worker_status = Some(AgentWorkerStatus::Running);
agent.needs_input = Some(crate::tools::subagent::SubAgentNeedsInput {
question: "which branch?".into(),
});
assert_eq!(WhaleState::for_subagent(&agent), WhaleState::Waiting);
assert!(WhaleState::Waiting.priority() > WhaleState::Blocked.priority());
assert!(WhaleState::Blocked.priority() > WhaleState::Working.priority());
assert!(WhaleState::Working.priority() > WhaleState::Thinking.priority());
assert!(WhaleState::Thinking.priority() > WhaleState::Offline.priority());
assert!(WhaleState::Offline.priority() > WhaleState::Resting.priority());
}
#[test]
fn shell_phase_maps_without_inventing_work() {
assert_eq!(
WhaleState::for_shell_phase(ShellPhase::Idle),
WhaleState::Resting
);
assert_eq!(
WhaleState::for_shell_phase(ShellPhase::Done),
WhaleState::Resting
);
assert_eq!(
WhaleState::for_shell_phase(ShellPhase::Typing),
WhaleState::Thinking
);
assert_eq!(
WhaleState::for_shell_phase(ShellPhase::Working),
WhaleState::Working
);
assert_eq!(
WhaleState::for_shell_phase(ShellPhase::Verifying),
WhaleState::Working
);
assert_eq!(
WhaleState::for_shell_phase(ShellPhase::Waiting),
WhaleState::Waiting
);
assert_eq!(
WhaleState::for_shell_phase(ShellPhase::Approval),
WhaleState::Waiting
);
assert_eq!(
WhaleState::for_shell_phase(ShellPhase::Failed),
WhaleState::Blocked
);
}
#[test]
fn badge_accents_meet_secondary_chrome_contrast_on_dark_and_light() {
for theme in [palette::UI_THEME, palette::LIGHT_UI_THEME] {
let ink = WhaleInk::from_theme(&theme);
for species in WhaleSpecies::ALL {
for color in [ink.accent(species), ink.body_for(species)] {
let ratio = contrast_ratio(color, theme.surface_bg)
.unwrap_or_else(|| panic!("{species:?} unresolvable on {}", theme.name));
assert!(
ratio >= palette::SECONDARY_CHROME_CONTRAST,
"{species:?} {color:?} on {} = {ratio:.2}",
theme.name
);
}
}
for color in [ink.current, ink.human, ink.bar] {
let ratio = contrast_ratio(color, theme.surface_bg).unwrap();
assert!(
ratio >= palette::SECONDARY_CHROME_CONTRAST,
"{color:?} {ratio:.2}"
);
}
}
let terminal = palette::TERMINAL_UI_THEME;
let ink = WhaleInk::from_theme(&terminal);
assert_eq!(ink.body, terminal.accent_action);
assert_eq!(ink.keel, terminal.warning);
}
#[test]
fn species_labels_are_localized_in_every_shipped_pack() {
for species in WhaleSpecies::ALL {
for locale in Locale::shipped_complete() {
assert!(!species.animal(*locale).trim().is_empty());
assert!(!species.job(*locale).trim().is_empty());
}
}
assert_eq!(WhaleSpecies::Scout.name(), "Scout");
assert_eq!(WhaleSpecies::Plain.name(), "Codewhale");
}
#[test]
#[allow(clippy::print_stdout)]
fn gallery_renders_every_species_in_every_state() {
for species in WhaleSpecies::ALL {
println!("== {} ({}) ==", species.name(), badge_ascii(species));
for state in std::iter::once(None).chain(WhaleState::ALL.into_iter().map(Some)) {
let rows = portrait_text(species, state, 1);
let ascii = portrait_ascii(species, state, 1);
println!("-- {state:?}");
for (u, a) in rows.iter().zip(ascii.iter()) {
println!("{u} {a}");
}
}
}
}
}