Compare commits
3 Commits
2da1a228b3
...
59f65b127a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59f65b127a | ||
|
|
f36e900570 | ||
|
|
e2efc61beb |
@@ -7,6 +7,7 @@ use tracing::Instrument;
|
||||
use tracing::{info, warn};
|
||||
|
||||
use crate::Config;
|
||||
use crate::cli::progress::stage_spinner;
|
||||
use crate::core::error::Result;
|
||||
use crate::core::metrics::{MetricsLayer, StageTiming};
|
||||
use crate::core::shutdown::ShutdownSignal;
|
||||
@@ -42,22 +43,6 @@ pub struct SyncResult {
|
||||
pub status_enrichment_errors: usize,
|
||||
}
|
||||
|
||||
fn stage_spinner(stage: u8, total: u8, msg: &str, robot_mode: bool) -> ProgressBar {
|
||||
if robot_mode {
|
||||
return ProgressBar::hidden();
|
||||
}
|
||||
let pb = crate::cli::progress::multi().add(ProgressBar::new_spinner());
|
||||
pb.set_style(
|
||||
ProgressStyle::default_spinner()
|
||||
.template("{spinner:.blue} {prefix} {msg}")
|
||||
.expect("valid template"),
|
||||
);
|
||||
pb.enable_steady_tick(std::time::Duration::from_millis(80));
|
||||
pb.set_prefix(format!("[{stage}/{total}]"));
|
||||
pb.set_message(msg.to_string());
|
||||
pb
|
||||
}
|
||||
|
||||
pub async fn run_sync(
|
||||
config: &Config,
|
||||
options: SyncOptions,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use indicatif::MultiProgress;
|
||||
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
|
||||
use std::io::Write;
|
||||
use std::sync::LazyLock;
|
||||
use tracing_subscriber::fmt::MakeWriter;
|
||||
@@ -9,6 +9,26 @@ pub fn multi() -> &'static MultiProgress {
|
||||
&MULTI
|
||||
}
|
||||
|
||||
/// Create a spinner for a numbered pipeline stage.
|
||||
///
|
||||
/// Returns a hidden (no-op) bar in robot mode so callers can use
|
||||
/// the same code path regardless of output mode.
|
||||
pub fn stage_spinner(stage: u8, total: u8, msg: &str, robot_mode: bool) -> ProgressBar {
|
||||
if robot_mode {
|
||||
return ProgressBar::hidden();
|
||||
}
|
||||
let pb = multi().add(ProgressBar::new_spinner());
|
||||
pb.set_style(
|
||||
ProgressStyle::default_spinner()
|
||||
.template("{spinner:.blue} {prefix} {msg}")
|
||||
.expect("valid template"),
|
||||
);
|
||||
pb.enable_steady_tick(std::time::Duration::from_millis(80));
|
||||
pb.set_prefix(format!("[{stage}/{total}]"));
|
||||
pb.set_message(msg.to_string());
|
||||
pb
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SuspendingWriter;
|
||||
|
||||
@@ -50,7 +70,6 @@ impl<'a> MakeWriter<'a> for SuspendingWriter {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use indicatif::ProgressBar;
|
||||
|
||||
#[test]
|
||||
fn multi_returns_same_instance() {
|
||||
@@ -88,4 +107,35 @@ mod tests {
|
||||
let w = MakeWriter::make_writer(&writer);
|
||||
drop(w);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stage_spinner_robot_mode_returns_hidden() {
|
||||
let pb = stage_spinner(1, 3, "Testing...", true);
|
||||
assert!(pb.is_hidden());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stage_spinner_human_mode_sets_properties() {
|
||||
// In non-TTY test environments, MultiProgress may report bars as
|
||||
// hidden. Verify the human-mode code path by checking that prefix
|
||||
// and message are configured (robot-mode returns a bare hidden bar).
|
||||
let pb = stage_spinner(1, 3, "Testing...", false);
|
||||
assert_eq!(pb.prefix(), "[1/3]");
|
||||
assert_eq!(pb.message(), "Testing...");
|
||||
pb.finish_and_clear();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stage_spinner_sets_prefix_format() {
|
||||
let pb = stage_spinner(2, 5, "Working...", false);
|
||||
assert_eq!(pb.prefix(), "[2/5]");
|
||||
pb.finish_and_clear();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stage_spinner_sets_message() {
|
||||
let pb = stage_spinner(1, 3, "Seeding timeline...", false);
|
||||
assert_eq!(pb.message(), "Seeding timeline...");
|
||||
pb.finish_and_clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1784,6 +1784,7 @@ async fn handle_timeline(
|
||||
max_seeds: args.max_seeds,
|
||||
max_entities: args.max_entities,
|
||||
max_evidence: args.max_evidence,
|
||||
robot_mode,
|
||||
};
|
||||
|
||||
let result = run_timeline(&config, ¶ms).await?;
|
||||
@@ -1828,6 +1829,12 @@ async fn handle_search(
|
||||
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 response = run_search(
|
||||
&config,
|
||||
@@ -1839,6 +1846,7 @@ async fn handle_search(
|
||||
)
|
||||
.await?;
|
||||
let elapsed_ms = start.elapsed().as_millis() as u64;
|
||||
spinner.finish_and_clear();
|
||||
|
||||
if robot_mode {
|
||||
print_search_results_json(&response, elapsed_ms, args.fields.as_deref());
|
||||
|
||||
@@ -52,12 +52,18 @@ pub fn to_fts_query(raw: &str, mode: FtsQueryMode) -> String {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
// FTS5 boolean operators are case-sensitive uppercase keywords.
|
||||
// Pass them through unquoted so users can write "switch AND health".
|
||||
const FTS5_OPERATORS: &[&str] = &["AND", "OR", "NOT", "NEAR"];
|
||||
|
||||
let mut result = String::with_capacity(trimmed.len() + 20);
|
||||
for (i, token) in trimmed.split_whitespace().enumerate() {
|
||||
if i > 0 {
|
||||
result.push(' ');
|
||||
}
|
||||
if let Some(stem) = token.strip_suffix('*')
|
||||
if FTS5_OPERATORS.contains(&token) {
|
||||
result.push_str(token);
|
||||
} else if let Some(stem) = token.strip_suffix('*')
|
||||
&& !stem.is_empty()
|
||||
&& stem.chars().all(|c| c.is_alphanumeric() || c == '_')
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user