1use crate::messages::ColoredMessage;
2use crate::theme::names::tokens;
3use std::sync::{Arc, Mutex};
4use std::time::{Duration, Instant};
5
6fn 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#[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#[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 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 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 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 pub fn streaming(
103 message: String,
104 tokens: TokenMetrics,
105 step: usize,
106 total: Option<usize>,
107 ) -> Self {
108 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 pub fn update_tokens(&mut self, tokens: TokenMetrics) {
129 self.tokens = tokens;
130
131 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 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 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 pub fn format_for_display(&self) -> String {
190 self.message.clone()
192 }
193}
194
195impl Default for IrisStatus {
196 fn default() -> Self {
197 Self::new()
198 }
199}
200
201pub 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 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 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 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 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 pub fn error(&self, error: &str) {
281 self.update(IrisStatus::error(error));
282 }
283
284 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
296pub static IRIS_STATUS: std::sync::LazyLock<IrisStatusTracker> =
298 std::sync::LazyLock::new(IrisStatusTracker::new);
299
300pub 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
304pub fn enable_agent_mode() {
306 AGENT_MODE_ENABLED.store(true, std::sync::atomic::Ordering::Relaxed);
307}
308
309pub fn is_agent_mode_enabled() -> bool {
311 AGENT_MODE_ENABLED.load(std::sync::atomic::Ordering::Relaxed)
312}
313
314#[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}