goosedump 0.7.0

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

use std::path::PathBuf;

use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;

#[derive(Debug, Clone)]
pub struct ConversationMessage {
    pub entry_id: String,
    pub kind: MessageKind,
}

impl ConversationMessage {
    #[must_use]
    pub fn new(entry_id: impl Into<String>, kind: MessageKind) -> Self {
        Self {
            entry_id: entry_id.into(),
            kind,
        }
    }

    /// The conversational role (`user`/`assistant`) this message carries.
    #[must_use]
    pub fn role(&self) -> &str {
        match &self.kind {
            MessageKind::TextContent(tc) => tc.role.as_str(),
            MessageKind::AssistantResponse(_) => "assistant",
            MessageKind::ToolResultData(_) | MessageKind::BashOutput(_) => "user",
        }
    }

    /// A human-readable role label for structured search/grep results
    /// (`tool/<name>` for tool results, `bash` for shell output).
    #[must_use]
    pub fn role_label(&self) -> String {
        match &self.kind {
            MessageKind::TextContent(tc) => {
                if tc.role.is_empty() {
                    "unknown".to_string()
                } else {
                    tc.role.clone()
                }
            }
            MessageKind::AssistantResponse(_) => "assistant".to_string(),
            MessageKind::ToolResultData(tr) => format!("tool/{}", tr.tool_name),
            MessageKind::BashOutput(_) => "bash".to_string(),
        }
    }
}

#[derive(Debug, Clone)]
pub enum MessageKind {
    TextContent(TextContent),
    AssistantResponse(AssistantResponse),
    ToolResultData(ToolResultData),
    BashOutput(BashOutput),
}

#[derive(Debug, Clone)]
pub struct TextContent {
    pub role: String,
    pub text: String,
}

#[derive(Debug, Clone)]
pub struct AssistantResponse {
    pub thinking: Vec<String>,
    pub tool_calls: Vec<ToolCall>,
    pub text: String,
}

#[derive(Debug, Clone)]
pub struct ToolResultData {
    /// Correlation id of the call this result answers (provider `tool_use_id`
    /// / `call_id`); empty when the source format does not address results by
    /// id. Lets renderers re-pair a result with its originating call.
    pub call_id: String,
    pub tool_name: String,
    pub content: String,
    pub is_error: bool,
}

#[derive(Debug, Clone)]
pub struct BashOutput {
    pub command: String,
    pub output: String,
}

#[derive(Debug, Clone)]
pub struct ToolCall {
    /// Provider call id (`tool_use.id` / `call_id`); empty when the source
    /// format does not assign one. Lets renderers correlate a later tool
    /// result back to this call.
    pub id: String,
    pub name: String,
    pub arguments: JsonValue,
}

#[derive(Debug, Clone)]
pub struct Entry {
    pub id: String,
    pub parent_id: String,
}

#[derive(Debug, Clone)]
pub struct Context {
    pub entries: Vec<Entry>,
    pub messages: Vec<ConversationMessage>,
    /// Working directory recorded in the session, when the source format
    /// carries one. Re-emitted by renderers whose native format has a `cwd`
    /// slot (e.g. the Pi/Codex session header).
    pub cwd: Option<String>,
}

#[derive(Debug, Clone)]
pub struct SearchHit {
    pub entry_id: String,
    pub score: f64,
    pub role: String,
    pub text: String,
    pub files: Vec<String>,
    pub terms: Vec<String>,
}

#[derive(Debug, Clone)]
pub struct ContextListing {
    pub id: String,
    pub provider_id: ProviderId,
    pub path: PathBuf,
    pub native_id: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ProviderId {
    Claude {
        timestamp: String,
        cwd: String,
    },
    Codex {
        timestamp: String,
        cwd: String,
    },
    Opencode {
        count: u32,
        first_time: String,
        last_time: String,
    },
    Crush {
        count: u32,
    },
    Goose {
        name: String,
        count: u32,
        cwd: String,
    },
    Gemini {
        timestamp: String,
        count: u32,
    },
    Pi {
        cwd: String,
    },
}

impl ProviderId {
    #[must_use]
    pub fn sort_key(&self) -> &str {
        match self {
            Self::Claude { timestamp, .. }
            | Self::Codex { timestamp, .. }
            | Self::Gemini { timestamp, .. } => timestamp,
            Self::Opencode { last_time, .. } => last_time,
            Self::Crush { .. } | Self::Goose { .. } | Self::Pi { .. } => "",
        }
    }

    #[must_use]
    pub fn timestamp(&self) -> &str {
        match self {
            Self::Claude { timestamp, .. }
            | Self::Codex { timestamp, .. }
            | Self::Gemini { timestamp, .. } => timestamp,
            _ => "-",
        }
    }

    #[must_use]
    pub fn cwd(&self) -> &str {
        match self {
            Self::Claude { cwd, .. }
            | Self::Codex { cwd, .. }
            | Self::Goose { cwd, .. }
            | Self::Pi { cwd } => cwd,
            _ => "-",
        }
    }

    #[must_use]
    pub fn count(&self) -> String {
        match self {
            Self::Opencode { count, .. }
            | Self::Crush { count }
            | Self::Goose { count, .. }
            | Self::Gemini { count, .. } => count.to_string(),
            _ => "-".to_string(),
        }
    }

    #[must_use]
    pub fn name(&self) -> &str {
        match self {
            Self::Goose { name, .. } if !name.is_empty() => name,
            _ => "-",
        }
    }

    #[must_use]
    pub fn time_range(&self) -> String {
        match self {
            Self::Opencode {
                first_time,
                last_time,
                ..
            } => format!("{first_time}..{last_time}"),
            _ => "-".to_string(),
        }
    }
}