refactor(sync): overhaul progress display with stage spinners and summaries
Phase 2 of the UX overhaul. Replaces the old numbered-stage progress system (1/4, 2/4...) and manual indicatif ProgressBar/ProgressStyle setup with the new centralized progress helpers. Sync command changes (src/cli/commands/sync.rs): - Replace stage_spinner(n, total, msg) with stage_spinner_v2(icon, label, status) removing the rigid numbered-stage counter in favor of named stages - Replace manual ProgressBar::new + ProgressStyle::default_bar for docs and embed sub-progress with nested_progress(label, len, robot_mode) - Add finish_stage() calls that display a completion summary with elapsed time, e.g. "Issues 42 issues from 3 projects 1.2s" - Each stage (Issues, MRs, Docs, Embed) now reports what it did on completion rather than just clearing the spinner silently - Embed failure path uses Icons::warning() instead of inline Theme formatting, keeping error display consistent with success path - Remove indicatif direct dependency from sync.rs (now handled by progress module) Main entry point changes (src/main.rs): - Add GlyphMode detection: auto-detect Unicode/Nerd Font support or fall back to ASCII based on --icons flag, --color=never, NO_COLOR, or robot mode - Update all LoreRenderer::init() calls to pass GlyphMode alongside ColorMode for icon-aware rendering throughout the CLI - Overhaul handle_error() formatting: use Icons::error() glyph, bold error text, arrow prefixed action suggestions, and breathing room with blank lines for scannability - Migrate handle_embed() progress bar from manual ProgressBar + ProgressStyle to nested_progress() helper, matching sync command Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
committed by
teernisse
parent
96b288ccdd
commit
af8fc4af76
@@ -1,13 +1,13 @@
|
||||
use crate::cli::render::{self, Theme};
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use crate::cli::render::{self, Icons, Theme, format_number};
|
||||
use serde::Serialize;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::Instant;
|
||||
use tracing::Instrument;
|
||||
use tracing::{info, warn};
|
||||
|
||||
use crate::Config;
|
||||
use crate::cli::progress::stage_spinner;
|
||||
use crate::cli::progress::{finish_stage, nested_progress, stage_spinner_v2};
|
||||
use crate::core::error::Result;
|
||||
use crate::core::metrics::{MetricsLayer, StageTiming};
|
||||
use crate::core::shutdown::ShutdownSignal;
|
||||
@@ -76,23 +76,10 @@ pub async fn run_sync(
|
||||
IngestDisplay::progress_only()
|
||||
};
|
||||
|
||||
let total_stages: u8 = if options.no_docs && options.no_embed {
|
||||
2
|
||||
} else if options.no_docs || options.no_embed {
|
||||
3
|
||||
} else {
|
||||
4
|
||||
};
|
||||
let mut current_stage: u8 = 0;
|
||||
|
||||
current_stage += 1;
|
||||
let spinner = stage_spinner(
|
||||
current_stage,
|
||||
total_stages,
|
||||
"Fetching issues from GitLab...",
|
||||
options.robot_mode,
|
||||
);
|
||||
info!("Sync stage {current_stage}/{total_stages}: ingesting issues");
|
||||
// ── Stage: Issues ──
|
||||
let stage_start = Instant::now();
|
||||
let spinner = stage_spinner_v2(Icons::sync(), "Issues", "fetching...", options.robot_mode);
|
||||
info!("Sync: ingesting issues");
|
||||
let issues_result = run_ingest(
|
||||
config,
|
||||
"issues",
|
||||
@@ -110,21 +97,23 @@ pub async fn run_sync(
|
||||
result.resource_events_fetched += issues_result.resource_events_fetched;
|
||||
result.resource_events_failed += issues_result.resource_events_failed;
|
||||
result.status_enrichment_errors += issues_result.status_enrichment_errors;
|
||||
spinner.finish_and_clear();
|
||||
let issues_summary = format!(
|
||||
"{} issues from {} {}",
|
||||
format_number(result.issues_updated as i64),
|
||||
issues_result.projects_synced,
|
||||
if issues_result.projects_synced == 1 { "project" } else { "projects" }
|
||||
);
|
||||
finish_stage(&spinner, Icons::success(), "Issues", &issues_summary, stage_start.elapsed());
|
||||
|
||||
if signal.is_cancelled() {
|
||||
info!("Shutdown requested after issues stage, returning partial sync results");
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
current_stage += 1;
|
||||
let spinner = stage_spinner(
|
||||
current_stage,
|
||||
total_stages,
|
||||
"Fetching merge requests from GitLab...",
|
||||
options.robot_mode,
|
||||
);
|
||||
info!("Sync stage {current_stage}/{total_stages}: ingesting merge requests");
|
||||
// ── Stage: MRs ──
|
||||
let stage_start = Instant::now();
|
||||
let spinner = stage_spinner_v2(Icons::sync(), "MRs", "fetching...", options.robot_mode);
|
||||
info!("Sync: ingesting merge requests");
|
||||
let mrs_result = run_ingest(
|
||||
config,
|
||||
"mrs",
|
||||
@@ -143,37 +132,26 @@ pub async fn run_sync(
|
||||
result.resource_events_failed += mrs_result.resource_events_failed;
|
||||
result.mr_diffs_fetched += mrs_result.mr_diffs_fetched;
|
||||
result.mr_diffs_failed += mrs_result.mr_diffs_failed;
|
||||
spinner.finish_and_clear();
|
||||
let mrs_summary = format!(
|
||||
"{} merge requests from {} {}",
|
||||
format_number(result.mrs_updated as i64),
|
||||
mrs_result.projects_synced,
|
||||
if mrs_result.projects_synced == 1 { "project" } else { "projects" }
|
||||
);
|
||||
finish_stage(&spinner, Icons::success(), "MRs", &mrs_summary, stage_start.elapsed());
|
||||
|
||||
if signal.is_cancelled() {
|
||||
info!("Shutdown requested after MRs stage, returning partial sync results");
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
// ── Stage: Docs ──
|
||||
if !options.no_docs {
|
||||
current_stage += 1;
|
||||
let spinner = stage_spinner(
|
||||
current_stage,
|
||||
total_stages,
|
||||
"Processing documents...",
|
||||
options.robot_mode,
|
||||
);
|
||||
info!("Sync stage {current_stage}/{total_stages}: generating documents");
|
||||
let stage_start = Instant::now();
|
||||
let spinner = stage_spinner_v2(Icons::sync(), "Docs", "generating...", options.robot_mode);
|
||||
info!("Sync: generating documents");
|
||||
|
||||
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 = nested_progress("Docs", 0, options.robot_mode);
|
||||
let docs_bar_clone = docs_bar.clone();
|
||||
let tick_started = Arc::new(AtomicBool::new(false));
|
||||
let tick_started_clone = Arc::clone(&tick_started);
|
||||
@@ -189,35 +167,22 @@ pub async fn run_sync(
|
||||
let docs_result = run_generate_docs(config, options.full, None, Some(docs_cb))?;
|
||||
result.documents_regenerated = docs_result.regenerated;
|
||||
docs_bar.finish_and_clear();
|
||||
spinner.finish_and_clear();
|
||||
let docs_summary = format!(
|
||||
"{} documents generated",
|
||||
format_number(result.documents_regenerated as i64),
|
||||
);
|
||||
finish_stage(&spinner, Icons::success(), "Docs", &docs_summary, stage_start.elapsed());
|
||||
} else {
|
||||
info!("Sync: skipping document generation (--no-docs)");
|
||||
}
|
||||
|
||||
// ── Stage: Embed ──
|
||||
if !options.no_embed {
|
||||
current_stage += 1;
|
||||
let spinner = stage_spinner(
|
||||
current_stage,
|
||||
total_stages,
|
||||
"Generating embeddings...",
|
||||
options.robot_mode,
|
||||
);
|
||||
info!("Sync stage {current_stage}/{total_stages}: embedding documents");
|
||||
let stage_start = Instant::now();
|
||||
let spinner = stage_spinner_v2(Icons::sync(), "Embed", "preparing...", options.robot_mode);
|
||||
info!("Sync: embedding documents");
|
||||
|
||||
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 = nested_progress("Embed", 0, options.robot_mode);
|
||||
let embed_bar_clone = embed_bar.clone();
|
||||
let tick_started = Arc::new(AtomicBool::new(false));
|
||||
let tick_started_clone = Arc::clone(&tick_started);
|
||||
@@ -234,14 +199,16 @@ pub async fn run_sync(
|
||||
Ok(embed_result) => {
|
||||
result.documents_embedded = embed_result.docs_embedded;
|
||||
embed_bar.finish_and_clear();
|
||||
spinner.finish_and_clear();
|
||||
let embed_summary = format!(
|
||||
"{} chunks embedded",
|
||||
format_number(embed_result.chunks_embedded as i64),
|
||||
);
|
||||
finish_stage(&spinner, Icons::success(), "Embed", &embed_summary, stage_start.elapsed());
|
||||
}
|
||||
Err(e) => {
|
||||
embed_bar.finish_and_clear();
|
||||
spinner.finish_and_clear();
|
||||
if !options.robot_mode {
|
||||
eprintln!(" {} Embedding skipped ({})", Theme::warning().render("warn"), e);
|
||||
}
|
||||
let warn_summary = format!("skipped ({})", e);
|
||||
finish_stage(&spinner, Icons::warning(), "Embed", &warn_summary, stage_start.elapsed());
|
||||
warn!(error = %e, "Embedding stage failed (Ollama may be unavailable), continuing");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user