1use crate::messages::ColoredMessage;
2use crate::theme::names::tokens;
3use std::borrow::Cow;
4use std::sync::{Arc, Mutex};
5use std::time::{Duration, Instant};
6
7fn 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#[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#[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#[derive(Debug, Clone)]
44pub struct IrisStatus {
45 pub phase: IrisPhase,
46 pub message: String,
47 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 #[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 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 #[must_use]
105 pub fn streaming(
106 message: String,
107 tokens: TokenMetrics,
108 step: usize,
109 total: Option<usize>,
110 ) -> Self {
111 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 pub fn update_tokens(&mut self, tokens: TokenMetrics) {
132 self.tokens = tokens;
133
134 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 #[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 #[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 #[must_use]
197 pub fn format_for_display(&self) -> String {
198 self.message.clone()
200 }
201}
202
203impl Default for IrisStatus {
204 fn default() -> Self {
205 Self::new()
206 }
207}
208
209pub 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 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 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 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 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 pub fn error(&self, error: &str) {
292 self.update(IrisStatus::error(error));
293 }
294
295 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
307pub static IRIS_STATUS: std::sync::LazyLock<IrisStatusTracker> =
309 std::sync::LazyLock::new(IrisStatusTracker::new);
310
311pub 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
315pub fn enable_agent_mode() {
317 AGENT_MODE_ENABLED.store(true, std::sync::atomic::Ordering::Relaxed);
318}
319
320#[must_use]
322pub fn is_agent_mode_enabled() -> bool {
323 AGENT_MODE_ENABLED.load(std::sync::atomic::Ordering::Relaxed)
324}
325
326#[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}