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