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:
@@ -2,6 +2,7 @@ use console::{Alignment, pad_str, style};
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::Config;
|
use crate::Config;
|
||||||
|
use crate::cli::progress::stage_spinner;
|
||||||
use crate::core::db::create_connection;
|
use crate::core::db::create_connection;
|
||||||
use crate::core::error::{LoreError, Result};
|
use crate::core::error::{LoreError, Result};
|
||||||
use crate::core::paths::get_db_path;
|
use crate::core::paths::get_db_path;
|
||||||
@@ -26,6 +27,7 @@ pub struct TimelineParams {
|
|||||||
pub max_seeds: usize,
|
pub max_seeds: usize,
|
||||||
pub max_entities: usize,
|
pub max_entities: usize,
|
||||||
pub max_evidence: usize,
|
pub max_evidence: usize,
|
||||||
|
pub robot_mode: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run the full timeline pipeline: SEED -> EXPAND -> COLLECT.
|
/// 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)
|
// 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(
|
let seed_result = seed_timeline(
|
||||||
&conn,
|
&conn,
|
||||||
Some(&client),
|
Some(&client),
|
||||||
@@ -70,8 +73,10 @@ pub async fn run_timeline(config: &Config, params: &TimelineParams) -> Result<Ti
|
|||||||
params.max_evidence,
|
params.max_evidence,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
spinner.finish_and_clear();
|
||||||
|
|
||||||
// Stage 3: EXPAND
|
// Stage 3: EXPAND
|
||||||
|
let spinner = stage_spinner(2, 3, "Expanding cross-references...", params.robot_mode);
|
||||||
let expand_result = expand_timeline(
|
let expand_result = expand_timeline(
|
||||||
&conn,
|
&conn,
|
||||||
&seed_result.seed_entities,
|
&seed_result.seed_entities,
|
||||||
@@ -79,8 +84,10 @@ pub async fn run_timeline(config: &Config, params: &TimelineParams) -> Result<Ti
|
|||||||
params.expand_mentions,
|
params.expand_mentions,
|
||||||
params.max_entities,
|
params.max_entities,
|
||||||
)?;
|
)?;
|
||||||
|
spinner.finish_and_clear();
|
||||||
|
|
||||||
// Stage 4: COLLECT
|
// Stage 4: COLLECT
|
||||||
|
let spinner = stage_spinner(3, 3, "Collecting events...", params.robot_mode);
|
||||||
let (events, total_before_limit) = collect_events(
|
let (events, total_before_limit) = collect_events(
|
||||||
&conn,
|
&conn,
|
||||||
&seed_result.seed_entities,
|
&seed_result.seed_entities,
|
||||||
@@ -90,6 +97,7 @@ pub async fn run_timeline(config: &Config, params: &TimelineParams) -> Result<Ti
|
|||||||
since_ms,
|
since_ms,
|
||||||
params.limit,
|
params.limit,
|
||||||
)?;
|
)?;
|
||||||
|
spinner.finish_and_clear();
|
||||||
|
|
||||||
Ok(TimelineResult {
|
Ok(TimelineResult {
|
||||||
query: params.query.clone(),
|
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> {
|
fn wrap_snippet(text: &str, width: usize) -> Vec<String> {
|
||||||
let mut lines = Vec::new();
|
let mut lines = wrap_text(text, width);
|
||||||
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
|
|
||||||
lines.truncate(4);
|
lines.truncate(4);
|
||||||
lines
|
lines
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1784,6 +1784,7 @@ async fn handle_timeline(
|
|||||||
max_seeds: args.max_seeds,
|
max_seeds: args.max_seeds,
|
||||||
max_entities: args.max_entities,
|
max_entities: args.max_entities,
|
||||||
max_evidence: args.max_evidence,
|
max_evidence: args.max_evidence,
|
||||||
|
robot_mode,
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = run_timeline(&config, ¶ms).await?;
|
let result = run_timeline(&config, ¶ms).await?;
|
||||||
@@ -1828,6 +1829,12 @@ async fn handle_search(
|
|||||||
limit: args.limit,
|
limit: args.limit,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let spinner = lore::cli::progress::stage_spinner(
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
&format!("Searching ({})...", args.mode),
|
||||||
|
robot_mode,
|
||||||
|
);
|
||||||
let start = std::time::Instant::now();
|
let start = std::time::Instant::now();
|
||||||
let response = run_search(
|
let response = run_search(
|
||||||
&config,
|
&config,
|
||||||
@@ -1839,6 +1846,7 @@ async fn handle_search(
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let elapsed_ms = start.elapsed().as_millis() as u64;
|
let elapsed_ms = start.elapsed().as_millis() as u64;
|
||||||
|
spinner.finish_and_clear();
|
||||||
|
|
||||||
if robot_mode {
|
if robot_mode {
|
||||||
print_search_results_json(&response, elapsed_ms, args.fields.as_deref());
|
print_search_results_json(&response, elapsed_ms, args.fields.as_deref());
|
||||||
|
|||||||
Reference in New Issue
Block a user