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