2026-08-17 10:52:56 +00:00
|
|
|
use std::sync::{Mutex, OnceLock};
|
2026-08-14 17:32:10 +00:00
|
|
|
use std::time::Duration;
|
|
|
|
|
|
2026-08-17 10:52:56 +00:00
|
|
|
/// 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<Mutex<Option<indicatif::ProgressBar>>> = OnceLock::new();
|
|
|
|
|
|
|
|
|
|
fn active() -> &'static Mutex<Option<indicatif::ProgressBar>> {
|
|
|
|
|
ACTIVE.get_or_init(|| Mutex::new(None))
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-14 17:32:10 +00:00
|
|
|
/// 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<indicatif::ProgressBar>);
|
|
|
|
|
|
|
|
|
|
impl Spinner {
|
2026-08-17 10:52:56 +00:00
|
|
|
pub(crate) fn start(enabled: bool, message: impl Into<String>) -> Self {
|
2026-08-14 17:32:10 +00:00
|
|
|
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"),
|
|
|
|
|
);
|
2026-08-17 10:52:56 +00:00
|
|
|
bar.set_message(message.into());
|
|
|
|
|
|
|
|
|
|
*active().lock().expect("spinner mutex poisoned") = Some(bar.clone());
|
2026-08-14 17:32:10 +00:00
|
|
|
|
|
|
|
|
Self(Some(bar))
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 11:21:23 +00:00
|
|
|
/// 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() {
|
2026-08-14 17:32:10 +00:00
|
|
|
bar.finish_and_clear();
|
2026-08-17 10:52:56 +00:00
|
|
|
*active().lock().expect("spinner mutex poisoned") = None;
|
2026-08-14 17:32:10 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-17 10:52:56 +00:00
|
|
|
|
2026-08-17 11:21:23 +00:00
|
|
|
impl Drop for Spinner {
|
|
|
|
|
fn drop(&mut self) {
|
|
|
|
|
self.stop();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 10:52:56 +00:00
|
|
|
/// 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<String>) {
|
|
|
|
|
if let Some(bar) = active().lock().expect("spinner mutex poisoned").as_ref() {
|
|
|
|
|
bar.set_message(message.into());
|
|
|
|
|
}
|
|
|
|
|
}
|