feat(cli): add pipeline progress spinners to timeline and search

Adds numbered stage spinners ([1/3], [2/3], [3/3]) to the timeline
pipeline stages (seed, expand, collect) so users see activity during
longer queries. TimelineParams gains a robot_mode field to suppress
spinners in JSON output mode.

Adds a [1/1] spinner to the search command for consistency, using the
shared stage_spinner from cli/progress.

Also refactors wrap_snippet() to delegate to wrap_text() with a 4-line
cap, eliminating the duplicated word-wrapping logic.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
teernisse
2026-02-13 14:56:10 -05:00
parent e2efc61beb
commit f36e900570
2 changed files with 17 additions and 19 deletions

View File

@@ -2,6 +2,7 @@ use console::{Alignment, pad_str, style};
use serde::Serialize;
use crate::Config;
use crate::cli::progress::stage_spinner;
use crate::core::db::create_connection;
use crate::core::error::{LoreError, Result};
use crate::core::paths::get_db_path;
@@ -26,6 +27,7 @@ pub struct TimelineParams {
pub max_seeds: usize,
pub max_entities: usize,
pub max_evidence: usize,
pub robot_mode: bool,
}
/// Run the full timeline pipeline: SEED -> EXPAND -> COLLECT.
@@ -60,6 +62,7 @@ pub async fn run_timeline(config: &Config, params: &TimelineParams) -> Result<Ti
});
// Stage 1+2: SEED + HYDRATE (hybrid search with FTS fallback)
let spinner = stage_spinner(1, 3, "Seeding timeline...", params.robot_mode);
let seed_result = seed_timeline(
&conn,
Some(&client),
@@ -70,8 +73,10 @@ pub async fn run_timeline(config: &Config, params: &TimelineParams) -> Result<Ti
params.max_evidence,
)
.await?;
spinner.finish_and_clear();
// Stage 3: EXPAND
let spinner = stage_spinner(2, 3, "Expanding cross-references...", params.robot_mode);
let expand_result = expand_timeline(
&conn,
&seed_result.seed_entities,
@@ -79,8 +84,10 @@ pub async fn run_timeline(config: &Config, params: &TimelineParams) -> Result<Ti
params.expand_mentions,
params.max_entities,
)?;
spinner.finish_and_clear();
// Stage 4: COLLECT
let spinner = stage_spinner(3, 3, "Collecting events...", params.robot_mode);
let (events, total_before_limit) = collect_events(
&conn,
&seed_result.seed_entities,
@@ -90,6 +97,7 @@ pub async fn run_timeline(config: &Config, params: &TimelineParams) -> Result<Ti
since_ms,
params.limit,
)?;
spinner.finish_and_clear();
Ok(TimelineResult {
query: params.query.clone(),
@@ -276,25 +284,7 @@ fn wrap_text(text: &str, width: usize) -> Vec<String> {
}
fn wrap_snippet(text: &str, width: usize) -> Vec<String> {
let mut lines = Vec::new();
let mut current = String::new();
for word in text.split_whitespace() {
if current.is_empty() {
current = word.to_string();
} else if current.len() + 1 + word.len() <= width {
current.push(' ');
current.push_str(word);
} else {
lines.push(current);
current = word.to_string();
}
}
if !current.is_empty() {
lines.push(current);
}
// Cap at 4 lines
let mut lines = wrap_text(text, width);
lines.truncate(4);
lines
}