From 1d003aeac2171dedfb555c71e70d91c6da627c1b Mon Sep 17 00:00:00 2001 From: Taylor Eernisse Date: Wed, 4 Feb 2026 15:02:13 -0500 Subject: [PATCH] fix(sync): Replace text-only progress with animated bars for docs/embed stages Stages 3 (generate-docs) and 4 (embed) reported progress by appending "(N/M)" text to the stage spinner message, while stages 1-2 (ingest) used dedicated indicatif progress bars with animated [====> ] rendering registered with the global MultiProgress. This visual inconsistency was introduced when progress callbacks were wired through in 266ed78. Replace the spinner.set_message() callbacks with proper ProgressBar instances that match the ingest stage pattern: - Create a bar-style ProgressBar registered via multi().add() - Use the same template/progress_chars as the ingest discussion bars - Lazy-init the tick via AtomicBool to avoid showing the bar before the first callback fires (matching how ingest enables ticks only at DiscussionSyncStarted) - Update set_length on every callback for the docs stage, since the regenerator's estimated_total can grow if new dirty items are queued during processing (using .max() internally) - Clean up both the sub-bar and stage spinner on completion/error Co-Authored-By: Claude Opus 4.5 --- src/cli/commands/sync.rs | 65 +++++++++++++++++++++++++++++++++++----- 1 file changed, 57 insertions(+), 8 deletions(-) diff --git a/src/cli/commands/sync.rs b/src/cli/commands/sync.rs index 4ce59d7..969e531 100644 --- a/src/cli/commands/sync.rs +++ b/src/cli/commands/sync.rs @@ -3,6 +3,8 @@ use console::style; use indicatif::{ProgressBar, ProgressStyle}; use serde::Serialize; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; use tracing::Instrument; use tracing::{info, warn}; @@ -160,16 +162,39 @@ pub async fn run_sync( options.robot_mode, ); info!("Sync stage {current_stage}/{total_stages}: generating documents"); - let docs_spinner = spinner.clone(); + + // Create a dedicated progress bar matching the ingest stage style + let docs_bar = if options.robot_mode { + ProgressBar::hidden() + } else { + let b = crate::cli::progress::multi().add(ProgressBar::new(0)); + b.set_style( + ProgressStyle::default_bar() + .template( + " {spinner:.blue} Processing documents [{bar:30.cyan/dim}] {pos}/{len}", + ) + .unwrap() + .progress_chars("=> "), + ); + b + }; + let docs_bar_clone = docs_bar.clone(); + let tick_started = Arc::new(AtomicBool::new(false)); + let tick_started_clone = Arc::clone(&tick_started); let docs_cb: Box = Box::new(move |processed, total| { if total > 0 { - docs_spinner.set_message(format!( - "Processing documents... ({processed}/{total})" - )); + if !tick_started_clone.swap(true, Ordering::Relaxed) { + docs_bar_clone.enable_steady_tick(std::time::Duration::from_millis(100)); + } + // Update length every callback — the regenerator's estimated_total + // can grow if new dirty items are queued during processing. + docs_bar_clone.set_length(total as u64); + docs_bar_clone.set_position(processed as u64); } }); let docs_result = run_generate_docs(config, false, None, Some(docs_cb))?; result.documents_regenerated = docs_result.regenerated; + docs_bar.finish_and_clear(); spinner.finish_and_clear(); } else { info!("Sync: skipping document generation (--no-docs)"); @@ -185,19 +210,43 @@ pub async fn run_sync( options.robot_mode, ); info!("Sync stage {current_stage}/{total_stages}: embedding documents"); - let embed_spinner = spinner.clone(); + + // Create a dedicated progress bar matching the ingest stage style + let embed_bar = if options.robot_mode { + ProgressBar::hidden() + } else { + let b = crate::cli::progress::multi().add(ProgressBar::new(0)); + b.set_style( + ProgressStyle::default_bar() + .template( + " {spinner:.blue} Generating embeddings [{bar:30.cyan/dim}] {pos}/{len}", + ) + .unwrap() + .progress_chars("=> "), + ); + b + }; + let embed_bar_clone = embed_bar.clone(); + let tick_started = Arc::new(AtomicBool::new(false)); + let tick_started_clone = Arc::clone(&tick_started); let embed_cb: Box = Box::new(move |processed, total| { - embed_spinner.set_message(format!( - "Embedding documents... ({processed}/{total})" - )); + if total > 0 { + if !tick_started_clone.swap(true, Ordering::Relaxed) { + embed_bar_clone.enable_steady_tick(std::time::Duration::from_millis(100)); + } + embed_bar_clone.set_length(total as u64); + embed_bar_clone.set_position(processed as u64); + } }); match run_embed(config, options.full, false, Some(embed_cb)).await { Ok(embed_result) => { result.documents_embedded = embed_result.embedded; + embed_bar.finish_and_clear(); spinner.finish_and_clear(); } Err(e) => { // Graceful degradation: Ollama down is a warning, not an error + embed_bar.finish_and_clear(); spinner.finish_and_clear(); if !options.robot_mode { eprintln!(" {} Embedding skipped ({})", style("warn").yellow(), e);