use crate::messages::ColoredMessage;
use crate::theme::names::tokens;
use std::borrow::Cow;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
fn truncate_at_char_boundary(s: &str, max_bytes: usize) -> &str {
if s.len() <= max_bytes {
return s;
}
let mut end = max_bytes;
while end > 0 && !s.is_char_boundary(end) {
end -= 1;
}
&s[..end]
}
#[derive(Debug, Clone, PartialEq)]
pub enum IrisPhase {
Initializing,
Planning,
ToolExecution { tool_name: String, reason: String },
PlanExpansion,
Synthesis,
Analysis,
Generation,
Completed,
Error(String),
}
#[derive(Debug, Clone, Default)]
pub struct TokenMetrics {
pub input_tokens: u32,
pub output_tokens: u32,
pub total_tokens: u32,
pub tokens_per_second: f32,
pub estimated_remaining: Option<u32>,
}
#[derive(Debug, Clone)]
pub struct IrisStatus {
pub phase: IrisPhase,
pub message: String,
pub token: &'static str,
pub started_at: Instant,
pub current_step: usize,
pub total_steps: Option<usize>,
pub tokens: TokenMetrics,
pub is_streaming: bool,
}
impl IrisStatus {
#[must_use]
pub fn new() -> Self {
Self {
phase: IrisPhase::Initializing,
message: "🤖 Initializing...".to_string(),
token: tokens::ACCENT_SECONDARY,
started_at: Instant::now(),
current_step: 0,
total_steps: None,
tokens: TokenMetrics::default(),
is_streaming: false,
}
}
#[must_use]
pub fn dynamic(phase: IrisPhase, message: String, step: usize, total: Option<usize>) -> Self {
let token = match phase {
IrisPhase::Initializing | IrisPhase::PlanExpansion => tokens::ACCENT_SECONDARY,
IrisPhase::Planning => tokens::ACCENT_DEEP,
IrisPhase::ToolExecution { .. } | IrisPhase::Completed => tokens::SUCCESS,
IrisPhase::Synthesis => tokens::ACCENT_TERTIARY,
IrisPhase::Analysis => tokens::WARNING,
IrisPhase::Generation => tokens::TEXT_PRIMARY,
IrisPhase::Error(_) => tokens::ERROR,
};
let constrained_message = if message.len() > 80 {
format!("{}...", truncate_at_char_boundary(&message, 77))
} else {
message
};
Self {
phase,
message: constrained_message,
token,
started_at: Instant::now(),
current_step: step,
total_steps: total,
tokens: TokenMetrics::default(),
is_streaming: false,
}
}
#[must_use]
pub fn streaming(
message: String,
tokens: TokenMetrics,
step: usize,
total: Option<usize>,
) -> Self {
let constrained_message = if message.len() > 80 {
format!("{}...", truncate_at_char_boundary(&message, 77))
} else {
message
};
Self {
phase: IrisPhase::Generation,
message: constrained_message,
token: tokens::TEXT_PRIMARY,
started_at: Instant::now(),
current_step: step,
total_steps: total,
tokens,
is_streaming: true,
}
}
pub fn update_tokens(&mut self, tokens: TokenMetrics) {
self.tokens = tokens;
let elapsed = self.started_at.elapsed().as_secs_f32();
if elapsed > 0.0 {
#[allow(clippy::cast_precision_loss, clippy::as_conversions)]
{
self.tokens.tokens_per_second = self.tokens.output_tokens as f32 / elapsed;
}
}
}
#[must_use]
pub fn error(error: &str) -> Self {
let constrained_message = if error.len() > 35 {
format!("❌ {}...", truncate_at_char_boundary(error, 32))
} else {
format!("❌ {error}")
};
Self {
phase: IrisPhase::Error(error.to_string()),
message: constrained_message,
token: tokens::ERROR,
started_at: Instant::now(),
current_step: 0,
total_steps: None,
tokens: TokenMetrics::default(),
is_streaming: false,
}
}
#[must_use]
pub fn completed() -> Self {
Self {
phase: IrisPhase::Completed,
message: "🎉 Done!".to_string(),
token: tokens::SUCCESS,
started_at: Instant::now(),
current_step: 0,
total_steps: None,
tokens: TokenMetrics::default(),
is_streaming: false,
}
}
#[must_use]
pub fn duration(&self) -> Duration {
self.started_at.elapsed()
}
#[allow(clippy::cast_precision_loss, clippy::as_conversions)]
#[must_use]
pub fn progress_percentage(&self) -> f32 {
if let Some(total) = self.total_steps {
(self.current_step as f32 / total as f32) * 100.0
} else {
0.0
}
}
#[must_use]
pub fn format_for_display(&self) -> String {
self.message.clone()
}
}
impl Default for IrisStatus {
fn default() -> Self {
Self::new()
}
}
pub struct IrisStatusTracker {
status: Arc<Mutex<IrisStatus>>,
}
impl IrisStatusTracker {
#[must_use]
pub fn new() -> Self {
Self {
status: Arc::new(Mutex::new(IrisStatus::new())),
}
}
pub fn update(&self, status: IrisStatus) {
crate::log_debug!(
"📋 Status: Updating to phase: {:?}, message: '{}'",
status.phase,
status.message
);
if let Ok(mut current_status) = self.status.lock() {
*current_status = status;
crate::log_debug!("📋 Status: Update completed successfully");
} else {
crate::log_debug!("📋 Status: ⚠️ Failed to acquire status lock");
}
}
pub fn update_dynamic(
&self,
phase: IrisPhase,
message: String,
step: usize,
total: Option<usize>,
) {
crate::log_debug!(
"🎯 Status: Dynamic update - phase: {:?}, message: '{}', step: {}/{:?}",
phase,
message,
step,
total
);
self.update(IrisStatus::dynamic(phase, message, step, total));
}
pub fn update_streaming(
&self,
message: String,
tokens: TokenMetrics,
step: usize,
total: Option<usize>,
) {
self.update(IrisStatus::streaming(message, tokens, step, total));
}
pub fn update_tokens(&self, tokens: TokenMetrics) {
if let Ok(mut status) = self.status.lock() {
status.update_tokens(tokens);
}
}
#[must_use]
pub fn get_current(&self) -> IrisStatus {
self.status.lock().map_or_else(
|_| IrisStatus::error("Status lock poisoned"),
|guard| guard.clone(),
)
}
#[must_use]
pub fn get_for_spinner(&self) -> ColoredMessage {
let status = self.get_current();
ColoredMessage {
text: Cow::Owned(status.format_for_display()),
token: status.token,
}
}
pub fn error(&self, error: &str) {
self.update(IrisStatus::error(error));
}
pub fn completed(&self) {
self.update(IrisStatus::completed());
}
}
impl Default for IrisStatusTracker {
fn default() -> Self {
Self::new()
}
}
pub static IRIS_STATUS: std::sync::LazyLock<IrisStatusTracker> =
std::sync::LazyLock::new(IrisStatusTracker::new);
pub static AGENT_MODE_ENABLED: std::sync::LazyLock<std::sync::Arc<std::sync::atomic::AtomicBool>> =
std::sync::LazyLock::new(|| std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true)));
pub fn enable_agent_mode() {
AGENT_MODE_ENABLED.store(true, std::sync::atomic::Ordering::Relaxed);
}
#[must_use]
pub fn is_agent_mode_enabled() -> bool {
AGENT_MODE_ENABLED.load(std::sync::atomic::Ordering::Relaxed)
}
#[macro_export]
macro_rules! iris_status_dynamic {
($phase:expr, $message:expr, $step:expr) => {
$crate::agents::status::IRIS_STATUS.update_dynamic(
$phase,
$message.to_string(),
$step,
None,
);
};
($phase:expr, $message:expr, $step:expr, $total:expr) => {
$crate::agents::status::IRIS_STATUS.update_dynamic(
$phase,
$message.to_string(),
$step,
Some($total),
);
};
}
#[macro_export]
macro_rules! iris_status_streaming {
($message:expr, $tokens:expr) => {
$crate::agents::status::IRIS_STATUS.update_streaming(
$message.to_string(),
$tokens,
0,
None,
);
};
($message:expr, $tokens:expr, $step:expr, $total:expr) => {
$crate::agents::status::IRIS_STATUS.update_streaming(
$message.to_string(),
$tokens,
$step,
Some($total),
);
};
}
#[macro_export]
macro_rules! iris_status_tokens {
($tokens:expr) => {
$crate::agents::status::IRIS_STATUS.update_tokens($tokens);
};
}
#[macro_export]
macro_rules! iris_status_error {
($error:expr) => {
$crate::agents::status::IRIS_STATUS.error($error);
};
}
#[macro_export]
macro_rules! iris_status_completed {
() => {
$crate::agents::status::IRIS_STATUS.completed();
};
}