diff --git a/src/cli/commands/sync.rs b/src/cli/commands/sync.rs index 2de1d22..2526da0 100644 --- a/src/cli/commands/sync.rs +++ b/src/cli/commands/sync.rs @@ -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"); } } diff --git a/src/main.rs b/src/main.rs index 42ba478..18b0248 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,7 +25,7 @@ use lore::cli::commands::{ run_list_issues, run_list_mrs, run_search, run_show_issue, run_show_mr, run_stats, run_sync, run_sync_status, run_timeline, run_who, }; -use lore::cli::render::{ColorMode, LoreRenderer, Theme}; +use lore::cli::render::{ColorMode, GlyphMode, Icons, LoreRenderer, Theme}; use lore::cli::robot::{RobotMeta, strip_schemas}; use lore::cli::{ Cli, Commands, CountArgs, EmbedArgs, GenerateDocsArgs, IngestArgs, IssuesArgs, MrsArgs, @@ -145,24 +145,29 @@ async fn main() { } // I1: Respect NO_COLOR convention (https://no-color.org/) + let force_ascii = robot_mode + || cli.color == "never" + || std::env::var("NO_COLOR").is_ok_and(|v| !v.is_empty()); + let glyphs = GlyphMode::detect(cli.icons.as_deref(), force_ascii); + if std::env::var("NO_COLOR").is_ok_and(|v| !v.is_empty()) { - LoreRenderer::init(ColorMode::Never); + LoreRenderer::init(ColorMode::Never, glyphs); console::set_colors_enabled(false); } else { match cli.color.as_str() { "never" => { - LoreRenderer::init(ColorMode::Never); + LoreRenderer::init(ColorMode::Never, glyphs); console::set_colors_enabled(false); } "always" => { - LoreRenderer::init(ColorMode::Always); + LoreRenderer::init(ColorMode::Always, glyphs); console::set_colors_enabled(true); } "auto" => { - LoreRenderer::init(ColorMode::Auto); + LoreRenderer::init(ColorMode::Auto, glyphs); } other => { - LoreRenderer::init(ColorMode::Auto); + LoreRenderer::init(ColorMode::Auto, glyphs); eprintln!("Warning: unknown color mode '{}', using auto", other); } } @@ -409,21 +414,28 @@ fn handle_error(e: Box, robot_mode: bool) -> ! { ); std::process::exit(gi_error.exit_code()); } else { - eprintln!("{} {}", Theme::error().render("Error:"), gi_error); + eprintln!(); + eprintln!( + " {} {}", + Theme::error().render(Icons::error()), + Theme::error().bold().render(&gi_error.to_string()) + ); if let Some(suggestion) = gi_error.suggestion() { - eprintln!("{} {}", Theme::warning().render("Hint:"), suggestion); + eprintln!(); + eprintln!(" {suggestion}"); } let actions = gi_error.actions(); if !actions.is_empty() { eprintln!(); for action in &actions { eprintln!( - " {} {}", - Theme::dim().render("$"), + " {} {}", + Theme::dim().render("\u{2192}"), Theme::bold().render(action) ); } } + eprintln!(); std::process::exit(gi_error.exit_code()); } } @@ -443,7 +455,13 @@ fn handle_error(e: Box, robot_mode: bool) -> ! { }) ); } else { - eprintln!("{} {}", Theme::error().render("Error:"), e); + eprintln!(); + eprintln!( + " {} {}", + Theme::error().render(Icons::error()), + Theme::error().bold().render(&e.to_string()) + ); + eprintln!(); } std::process::exit(1); } @@ -1966,7 +1984,6 @@ async fn handle_embed( args: EmbedArgs, robot_mode: bool, ) -> Result<(), Box> { - use indicatif::{ProgressBar, ProgressStyle}; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; @@ -1985,18 +2002,7 @@ async fn handle_embed( std::process::exit(130); }); - let embed_bar = if robot_mode { - ProgressBar::hidden() - } else { - let b = lore::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 = lore::cli::progress::nested_progress("Embedding", 0, robot_mode); let bar_clone = embed_bar.clone(); let tick_started = Arc::new(AtomicBool::new(false)); let tick_clone = Arc::clone(&tick_started);