Skip to main content

git_iris/agents/
status.rs

1use crate::messages::ColoredMessage;
2use crate::theme::names::tokens;
3use std::sync::{Arc, Mutex};
4use std::time::{Duration, Instant};
5
6/// Safely truncate a string at a character boundary
7fn truncate_at_char_boundary(s: &str, max_bytes: usize) -> &str {
8    if s.len() <= max_bytes {
9        return s;
10    }
11    let mut end = max_bytes;
12    while end > 0 && !s.is_char_boundary(end) {
13        end -= 1;
14    }
15    &s[..end]
16}
17
18/// Status phases for the Iris agent
19#[derive(Debug, Clone, PartialEq)]
20pub enum IrisPhase {
21    Initializing,
22    Planning,
23    ToolExecution { tool_name: String, reason: String },
24    PlanExpansion,
25    Synthesis,
26    Analysis,
27    Generation,
28    Completed,
29    Error(String),
30}
31
32/// Token counting information for live updates
33#[derive(Debug, Clone, Default)]
34pub struct TokenMetrics {
35    pub input_tokens: u32,
36    pub output_tokens: u32,
37    pub total_tokens: u32,
38    pub tokens_per_second: f32,
39    pub estimated_remaining: Option<u32>,
40}
41
42/// Status tracker for Iris agent operations with dynamic messages and live token counting
43#[derive(Debug, Clone)]
44pub struct IrisStatus {
45    pub phase: IrisPhase,
46    pub message: String,
47    /// Theme token for color resolution (e.g., "accent.secondary", "success")
48    pub token: &'static str,
49    pub started_at: Instant,
50    pub current_step: usize,
51    pub total_steps: Option<usize>,
52    pub tokens: TokenMetrics,
53    pub is_streaming: bool,
54}
55
56impl IrisStatus {
57    pub fn new() -> Self {
58        Self {
59            phase: IrisPhase::Initializing,
60            message: "🤖 Initializing...".to_string(),
61            token: tokens::ACCENT_SECONDARY,
62            started_at: Instant::now(),
63            current_step: 0,
64            total_steps: None,
65            tokens: TokenMetrics::default(),
66            is_streaming: false,
67        }
68    }
69
70    /// Create a dynamic status with LLM-generated message (constrained to 80 chars)
71    pub fn dynamic(phase: IrisPhase, message: String, step: usize, total: Option<usize>) -> Self {
72        let token = match phase {
73            IrisPhase::Initializing | IrisPhase::PlanExpansion => tokens::ACCENT_SECONDARY,
74            IrisPhase::Planning => tokens::ACCENT_DEEP,
75            IrisPhase::ToolExecution { .. } | IrisPhase::Completed => tokens::SUCCESS,
76            IrisPhase::Synthesis => tokens::ACCENT_TERTIARY,
77            IrisPhase::Analysis => tokens::WARNING,
78            IrisPhase::Generation => tokens::TEXT_PRIMARY,
79            IrisPhase::Error(_) => tokens::ERROR,
80        };
81
82        // Constrain message to 80 characters as requested
83        let constrained_message = if message.len() > 80 {
84            format!("{}...", truncate_at_char_boundary(&message, 77))
85        } else {
86            message
87        };
88
89        Self {
90            phase,
91            message: constrained_message,
92            token,
93            started_at: Instant::now(),
94            current_step: step,
95            total_steps: total,
96            tokens: TokenMetrics::default(),
97            is_streaming: false,
98        }
99    }
100
101    /// Create dynamic streaming status with live token counting
102    pub fn streaming(
103        message: String,
104        tokens: TokenMetrics,
105        step: usize,
106        total: Option<usize>,
107    ) -> Self {
108        // Constrain message to 80 characters
109        let constrained_message = if message.len() > 80 {
110            format!("{}...", truncate_at_char_boundary(&message, 77))
111        } else {
112            message
113        };
114
115        Self {
116            phase: IrisPhase::Generation,
117            message: constrained_message,
118            token: tokens::TEXT_PRIMARY,
119            started_at: Instant::now(),
120            current_step: step,
121            total_steps: total,
122            tokens,
123            is_streaming: true,
124        }
125    }
126
127    /// Update token metrics during streaming
128    pub fn update_tokens(&mut self, tokens: TokenMetrics) {
129        self.tokens = tokens;
130
131        // Update tokens per second based on elapsed time
132        let elapsed = self.started_at.elapsed().as_secs_f32();
133        if elapsed > 0.0 {
134            #[allow(clippy::cast_precision_loss, clippy::as_conversions)]
135            {
136                self.tokens.tokens_per_second = self.tokens.output_tokens as f32 / elapsed;
137            }
138        }
139    }
140
141    /// Create error status
142    pub fn error(error: &str) -> Self {
143        let constrained_message = if error.len() > 35 {
144            format!("❌ {}...", truncate_at_char_boundary(error, 32))
145        } else {
146            format!("❌ {error}")
147        };
148
149        Self {
150            phase: IrisPhase::Error(error.to_string()),
151            message: constrained_message,
152            token: tokens::ERROR,
153            started_at: Instant::now(),
154            current_step: 0,
155            total_steps: None,
156            tokens: TokenMetrics::default(),
157            is_streaming: false,
158        }
159    }
160
161    /// Create completed status
162    pub fn completed() -> Self {
163        Self {
164            phase: IrisPhase::Completed,
165            message: "🎉 Done!".to_string(),
166            token: tokens::SUCCESS,
167            started_at: Instant::now(),
168            current_step: 0,
169            total_steps: None,
170            tokens: TokenMetrics::default(),
171            is_streaming: false,
172        }
173    }
174
175    pub fn duration(&self) -> Duration {
176        self.started_at.elapsed()
177    }
178
179    #[allow(clippy::cast_precision_loss, clippy::as_conversions)]
180    pub fn progress_percentage(&self) -> f32 {
181        if let Some(total) = self.total_steps {
182            (self.current_step as f32 / total as f32) * 100.0
183        } else {
184            0.0
185        }
186    }
187
188    /// Format status for display - clean and minimal
189    pub fn format_for_display(&self) -> String {
190        // Just the message - clean and elegant
191        self.message.clone()
192    }
193}
194
195impl Default for IrisStatus {
196    fn default() -> Self {
197        Self::new()
198    }
199}
200
201/// Global status tracker for Iris agent
202pub struct IrisStatusTracker {
203    status: Arc<Mutex<IrisStatus>>,
204}
205
206impl IrisStatusTracker {
207    pub fn new() -> Self {
208        Self {
209            status: Arc::new(Mutex::new(IrisStatus::new())),
210        }
211    }
212
213    /// Update status with dynamic message
214    pub fn update(&self, status: IrisStatus) {
215        crate::log_debug!(
216            "📋 Status: Updating to phase: {:?}, message: '{}'",
217            status.phase,
218            status.message
219        );
220        if let Ok(mut current_status) = self.status.lock() {
221            *current_status = status;
222            crate::log_debug!("📋 Status: Update completed successfully");
223        } else {
224            crate::log_debug!("📋 Status: ⚠️ Failed to acquire status lock");
225        }
226    }
227
228    /// Update with dynamic LLM-generated message
229    pub fn update_dynamic(
230        &self,
231        phase: IrisPhase,
232        message: String,
233        step: usize,
234        total: Option<usize>,
235    ) {
236        crate::log_debug!(
237            "🎯 Status: Dynamic update - phase: {:?}, message: '{}', step: {}/{:?}",
238            phase,
239            message,
240            step,
241            total
242        );
243        self.update(IrisStatus::dynamic(phase, message, step, total));
244    }
245
246    /// Update streaming status with token metrics
247    pub fn update_streaming(
248        &self,
249        message: String,
250        tokens: TokenMetrics,
251        step: usize,
252        total: Option<usize>,
253    ) {
254        self.update(IrisStatus::streaming(message, tokens, step, total));
255    }
256
257    /// Update only token metrics for current status
258    pub fn update_tokens(&self, tokens: TokenMetrics) {
259        if let Ok(mut status) = self.status.lock() {
260            status.update_tokens(tokens);
261        }
262    }
263
264    pub fn get_current(&self) -> IrisStatus {
265        self.status.lock().map_or_else(
266            |_| IrisStatus::error("Status lock poisoned"),
267            |guard| guard.clone(),
268        )
269    }
270
271    pub fn get_for_spinner(&self) -> ColoredMessage {
272        let status = self.get_current();
273        ColoredMessage {
274            text: status.format_for_display(),
275            token: status.token,
276        }
277    }
278
279    /// Set error status
280    pub fn error(&self, error: &str) {
281        self.update(IrisStatus::error(error));
282    }
283
284    /// Set completed status
285    pub fn completed(&self) {
286        self.update(IrisStatus::completed());
287    }
288}
289
290impl Default for IrisStatusTracker {
291    fn default() -> Self {
292        Self::new()
293    }
294}
295
296/// Global instance of the Iris status tracker
297pub static IRIS_STATUS: std::sync::LazyLock<IrisStatusTracker> =
298    std::sync::LazyLock::new(IrisStatusTracker::new);
299
300/// Global flag to track if agent mode is enabled (enabled by default)
301pub static AGENT_MODE_ENABLED: std::sync::LazyLock<std::sync::Arc<std::sync::atomic::AtomicBool>> =
302    std::sync::LazyLock::new(|| std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true)));
303
304/// Enable agent mode globally
305pub fn enable_agent_mode() {
306    AGENT_MODE_ENABLED.store(true, std::sync::atomic::Ordering::Relaxed);
307}
308
309/// Check if agent mode is enabled
310pub fn is_agent_mode_enabled() -> bool {
311    AGENT_MODE_ENABLED.load(std::sync::atomic::Ordering::Relaxed)
312}
313
314/// Helper macros for dynamic status updates with LLM messages
315#[macro_export]
316macro_rules! iris_status_dynamic {
317    ($phase:expr, $message:expr, $step:expr) => {
318        $crate::agents::status::IRIS_STATUS.update_dynamic(
319            $phase,
320            $message.to_string(),
321            $step,
322            None,
323        );
324    };
325    ($phase:expr, $message:expr, $step:expr, $total:expr) => {
326        $crate::agents::status::IRIS_STATUS.update_dynamic(
327            $phase,
328            $message.to_string(),
329            $step,
330            Some($total),
331        );
332    };
333}
334
335#[macro_export]
336macro_rules! iris_status_streaming {
337    ($message:expr, $tokens:expr) => {
338        $crate::agents::status::IRIS_STATUS.update_streaming(
339            $message.to_string(),
340            $tokens,
341            0,
342            None,
343        );
344    };
345    ($message:expr, $tokens:expr, $step:expr, $total:expr) => {
346        $crate::agents::status::IRIS_STATUS.update_streaming(
347            $message.to_string(),
348            $tokens,
349            $step,
350            Some($total),
351        );
352    };
353}
354
355#[macro_export]
356macro_rules! iris_status_tokens {
357    ($tokens:expr) => {
358        $crate::agents::status::IRIS_STATUS.update_tokens($tokens);
359    };
360}
361
362#[macro_export]
363macro_rules! iris_status_error {
364    ($error:expr) => {
365        $crate::agents::status::IRIS_STATUS.error($error);
366    };
367}
368
369#[macro_export]
370macro_rules! iris_status_completed {
371    () => {
372        $crate::agents::status::IRIS_STATUS.completed();
373    };
374}