vtcode_core/utils/session_archive/
search.rs1use std::path::PathBuf;
2
3use anyhow::{Context, Result};
4use chrono::{DateTime, Utc};
5use regex::RegexBuilder;
6use serde::{Deserialize, Serialize};
7
8use super::list_recent_sessions;
9use crate::llm::provider::{MessageContent, MessageRole};
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct SearchResult {
13 pub session_id: String,
14 pub session_path: PathBuf,
15 pub timestamp: DateTime<Utc>,
16 pub message_index: usize,
17 pub role: MessageRole,
18 pub content_snippet: String,
19 pub score: f32, }
21
22pub async fn search_sessions(query: &str, session_limit: usize, max_results: usize) -> Result<Vec<SearchResult>> {
25 if query.trim().is_empty() || max_results == 0 {
26 return Ok(Vec::new());
27 }
28
29 let listings = list_recent_sessions(session_limit).await?;
30 let matcher = RegexBuilder::new(®ex::escape(query))
31 .case_insensitive(true)
32 .build()
33 .context("failed to compile session search query")?;
34 let mut results = Vec::new();
35
36 for listing in listings {
37 for (idx, msg) in listing.snapshot.messages.iter().enumerate() {
38 let content = match &msg.content {
39 MessageContent::Text(t) => t.as_str(),
40 MessageContent::Parts(_) => continue,
41 };
42
43 if let Some(matched) = matcher.find(content) {
44 let snippet = search_result_snippet(content, matched.start(), matched.end());
45
46 results.push(SearchResult {
47 session_id: listing.identifier(),
48 session_path: listing.path.clone(),
49 timestamp: listing.snapshot.started_at,
50 message_index: idx,
51 role: msg.role,
52 content_snippet: snippet,
53 score: 1.0,
54 });
55
56 if results.len() >= max_results {
57 break;
58 }
59 }
60 }
61 if results.len() >= max_results {
62 break;
63 }
64 }
65
66 Ok(results)
67}
68
69fn search_result_snippet(content: &str, match_start: usize, match_end: usize) -> String {
70 const CONTEXT_BYTES: usize = 50;
71
72 let start = floor_char_boundary(content, match_start.saturating_sub(CONTEXT_BYTES));
73 let end = ceil_char_boundary(content, (match_end + CONTEXT_BYTES).min(content.len()));
74
75 let mut snippet = String::new();
76 if start > 0 {
77 snippet.push_str("...");
78 }
79 snippet.push_str(&content[start..end].replace('\n', " "));
80 if end < content.len() {
81 snippet.push_str("...");
82 }
83 snippet
84}
85
86fn floor_char_boundary(content: &str, mut index: usize) -> usize {
87 while index > 0 && !content.is_char_boundary(index) {
88 index -= 1;
89 }
90 index
91}
92
93fn ceil_char_boundary(content: &str, mut index: usize) -> usize {
94 while index < content.len() && !content.is_char_boundary(index) {
95 index += 1;
96 }
97 index
98}