SearchSessions performs a full-text search across all persisted sessions, reusing the existing JSON store (no separate index). A message matches when every whitespace-separated query term appears in its content (case-insensitive AND). Results are sorted by match count, descending. maxSnippetsPerSess
(query string, maxSnippetsPerSession int)
| 225 | // (case-insensitive AND). Results are sorted by match count, descending. |
| 226 | // maxSnippetsPerSession caps how many context snippets each hit carries. |
| 227 | func (sm *SessionManager) SearchSessions(query string, maxSnippetsPerSession int) ([]SessionSearchHit, error) { |
| 228 | terms := strings.Fields(strings.ToLower(strings.TrimSpace(query))) |
| 229 | if len(terms) == 0 { |
| 230 | return nil, fmt.Errorf("empty query") |
| 231 | } |
| 232 | if maxSnippetsPerSession <= 0 { |
| 233 | maxSnippetsPerSession = 3 |
| 234 | } |
| 235 | |
| 236 | names, err := sm.ListSessions() |
| 237 | if err != nil { |
| 238 | return nil, err |
| 239 | } |
| 240 | |
| 241 | var hits []SessionSearchHit |
| 242 | for _, name := range names { |
| 243 | sd, err := sm.LoadSessionV2(name) |
| 244 | if err != nil || sd == nil { |
| 245 | continue // skip unreadable sessions rather than abort the search |
| 246 | } |
| 247 | |
| 248 | matches := 0 |
| 249 | var snippets []string |
| 250 | for _, hist := range [][]models.Message{sd.ChatHistory, sd.AgentHistory, sd.CoderHistory, sd.SharedMemory} { |
| 251 | for _, msg := range hist { |
| 252 | if msg.Content == "" { |
| 253 | continue |
| 254 | } |
| 255 | lower := strings.ToLower(msg.Content) |
| 256 | if !containsAllTerms(lower, terms) { |
| 257 | continue |
| 258 | } |
| 259 | matches++ |
| 260 | if len(snippets) < maxSnippetsPerSession { |
| 261 | snippets = append(snippets, msg.Role+": "+snippetAround(msg.Content, lower, terms[0])) |
| 262 | } |
| 263 | } |
| 264 | } |
| 265 | |
| 266 | if matches > 0 { |
| 267 | hits = append(hits, SessionSearchHit{Session: name, Matches: matches, Snippets: snippets}) |
| 268 | } |
| 269 | } |
| 270 | |
| 271 | sort.Slice(hits, func(i, j int) bool { return hits[i].Matches > hits[j].Matches }) |
| 272 | return hits, nil |
| 273 | } |
| 274 | |
| 275 | // containsAllTerms reports whether lowerText contains every term. |
| 276 | func containsAllTerms(lowerText string, terms []string) bool { |