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 PlanExpansion,
26 Synthesis,
27 Analysis,
28 Generation,
29 Completed,
30 Error(String),
31}
32
33#[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#[derive(Debug, Clone)]
45pub struct IrisStatus {
46 pub phase: IrisPhase,
47 pub message: String,
48 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 #[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 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 #[must_use]
106 pub fn streaming(
107 message: String,
108 tokens: TokenMetrics,
109 step: usize,
110 total: Option<usize>,
111 ) -> Self {
112 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 pub fn update_tokens(&mut self, tokens: TokenMetrics) {
133 self.tokens = tokens;
134
135 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 #[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 #[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 #[must_use]
198 pub fn format_for_display(&self) -> String {
199 self.message.clone()
201 }
202}
203
204impl Default for IrisStatus {
205 fn default() -> Self {
206 Self::new()
207 }
208}
209
210pub 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 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 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 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 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 pub fn error(&self, error: &str) {
293 self.update(IrisStatus::error(error));
294 }
295
296 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
308pub static IRIS_STATUS: std::sync::LazyLock<IrisStatusTracker> =
310 std::sync::LazyLock::new(IrisStatusTracker::new);
311
312pub 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
316pub fn enable_agent_mode() {
318 AGENT_MODE_ENABLED.store(true, std::sync::atomic::Ordering::Relaxed);
319}
320
321#[must_use]
323pub fn is_agent_mode_enabled() -> bool {
324 AGENT_MODE_ENABLED.load(std::sync::atomic::Ordering::Relaxed)
325}
326
327#[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}