use std::sync::{Mutex, OnceLock}; use std::time::Duration; /// Emoji shown on the spinner line for whichever activity is currently /// running, so the different phases (and the tool calls within the /// research phase) stay visually distinct at a glance. pub(crate) const RESEARCH_EMOJI: &str = "🔎"; pub(crate) const SEARCH_EMOJI: &str = "🌐"; pub(crate) const FETCH_EMOJI: &str = "📄"; pub(crate) const REVIEW_EMOJI: &str = "🧐"; pub(crate) const REJECTED_EMOJI: &str = "❌"; pub(crate) const REPORT_EMOJI: &str = "✍️"; /// The spinner currently on screen, if any — set by `Spinner::start` and /// cleared on drop. Tool implementations don't otherwise have a handle to /// the active spinner (they're plain functions invoked by the model, not /// passed one down through the tool-calling loop), so `set_activity` lets /// them reach it here instead to reflect what they're doing — e.g. which /// page they're fetching — on the same line. static ACTIVE: OnceLock>> = OnceLock::new(); fn active() -> &'static Mutex> { ACTIVE.get_or_init(|| Mutex::new(None)) } /// A terminal spinner for a research phase, shown only when logging is off — /// with logging on, the trace output already tells the user something is /// happening, and interleaving both would just be noisy. Clearing on drop /// means call sites don't need an explicit "stop" at every early return. pub(crate) struct Spinner(Option); impl Spinner { pub(crate) fn start(enabled: bool, message: impl Into) -> Self { if !enabled { return Self(None); } let bar = indicatif::ProgressBar::new_spinner(); bar.enable_steady_tick(Duration::from_millis(100)); bar.set_style( indicatif::ProgressStyle::with_template("{spinner:.cyan} {msg}") .expect("static template is valid"), ); bar.set_message(message.into()); *active().lock().expect("spinner mutex poisoned") = Some(bar.clone()); Self(Some(bar)) } /// Clears the spinner immediately rather than waiting for drop — for /// callers that need it gone at a precise moment (e.g. right as the /// first chunk of a stream is about to print on the same line) rather /// than whenever the value happens to go out of scope. Idempotent: a /// spinner already stopped, or one that was never enabled, does nothing. pub(crate) fn stop(&mut self) { if let Some(bar) = self.0.take() { bar.finish_and_clear(); *active().lock().expect("spinner mutex poisoned") = None; } } } impl Drop for Spinner { fn drop(&mut self) { self.stop(); } } /// Updates the message of whatever spinner is currently running, if any. /// A no-op when progress display is off (no spinner was ever started, so /// `active()` stays empty) or between phases (the previous `Spinner` has /// already dropped and cleared it). pub(crate) fn set_activity(message: impl Into) { if let Some(bar) = active().lock().expect("spinner mutex poisoned").as_ref() { bar.set_message(message.into()); } }