goosedump 0.1.3

Coding agent context data browser
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (C) Jarkko Sakkinen 2026

pub mod crush;
pub mod goose;
pub mod jsonl;
pub mod opencode;

use crate::message::{Context, ContextListing, ConversationMessage, Entry};
use std::collections::HashMap;

pub trait ContextReader {
    fn list_contexts(&self) -> anyhow::Result<Vec<ContextListing>>;
    fn read_context(&self, context_id: &str) -> anyhow::Result<Context>;
}

pub fn active_lineage_ids(entries: &[Entry]) -> Vec<String> {
    let parent_map: HashMap<&str, &str> = entries
        .iter()
        .filter(|e| !e.parent_id.is_empty())
        .map(|e| (e.id.as_str(), e.parent_id.as_str()))
        .collect();

    let mut leaves: Vec<&str> = entries
        .iter()
        .map(|e| e.id.as_str())
        .filter(|&id| !parent_map.values().any(|p| *p == id))
        .collect();
    leaves.sort_by_key(|&id| entries.iter().position(|e| e.id == id));

    let mut active_ids = Vec::new();
    for leaf in leaves {
        let mut current = leaf;
        let mut visited = std::collections::HashSet::new();
        active_ids.push(current.to_string());
        while let Some(&parent) = parent_map.get(current) {
            if parent.is_empty() || !visited.insert(parent) {
                break;
            }
            active_ids.push(parent.to_string());
            current = parent;
        }
    }
    active_ids
}

pub fn filter_messages(
    messages: Vec<ConversationMessage>,
    ids: &[String],
) -> Vec<ConversationMessage> {
    if ids.is_empty() {
        return messages;
    }
    let id_set: std::collections::HashSet<&str> =
        ids.iter().map(std::string::String::as_str).collect();
    messages
        .into_iter()
        .filter(|m| id_set.contains(m.entry_id.as_str()))
        .collect()
}

pub fn parse_id_set(csv: &str) -> Vec<String> {
    crate::text::split_csv(csv)
}