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:
Taylor Eernisse
2026-02-14 10:00:22 -05:00
committed by teernisse
parent 96b288ccdd
commit af8fc4af76
2 changed files with 77 additions and 104 deletions

View File

@@ -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<dyn std::error::Error>, 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<dyn std::error::Error>, 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<dyn std::error::Error>> {
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);