Skip to main content

git_iris/agents/
status.rs

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