goosedump 0.8.2

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

use crate::message::{ConversationMessage, Part, SearchHit};
use serde_json::{Map, Value, json};

#[must_use]
pub fn messages(msgs: &[ConversationMessage]) -> Value {
    let messages: Vec<Value> = msgs.iter().map(message).collect();
    json!({ "messages": messages })
}

fn message(msg: &ConversationMessage) -> Value {
    let mut obj = Map::new();
    obj.insert("entryId".into(), json!(msg.entry_id));
    obj.insert("role".into(), json!(msg.role));
    if let Some(model) = &msg.model {
        obj.insert("model".into(), json!(model));
    }
    if let Some(timestamp) = &msg.timestamp {
        obj.insert("timestamp".into(), json!(timestamp.to_rfc3339()));
    }
    let parts: Vec<Value> = msg.parts.iter().map(part).collect();
    obj.insert("parts".into(), json!(parts));
    Value::Object(obj)
}

fn part(part: &Part) -> Value {
    match part {
        Part::Text(text) => json!({ "type": "text", "text": text }),
        Part::Reasoning(reasoning) => {
            let mut obj = Map::new();
            obj.insert("type".into(), json!("reasoning"));
            obj.insert("text".into(), json!(reasoning.text));
            if !reasoning.signature.is_empty() {
                obj.insert("signature".into(), json!(reasoning.signature));
            }
            if reasoning.redacted {
                obj.insert("redacted".into(), json!(true));
            }
            Value::Object(obj)
        }
        Part::ToolCall(call) => json!({
            "type": "toolCall",
            "id": call.id,
            "name": call.name,
            "arguments": call.arguments,
        }),
        Part::ToolResult(result) => json!({
            "type": "toolResult",
            "callId": result.call_id,
            "toolName": result.tool_name,
            "content": result.content,
            "isError": result.is_error,
        }),
        Part::Image(image) => {
            let mut obj = Map::new();
            obj.insert("type".into(), json!("image"));
            obj.insert("mimeType".into(), json!(image.mime_type));
            obj.insert("data".into(), json!(image.data));
            Value::Object(obj)
        }
        Part::Bash(bash) => json!({
            "type": "bash",
            "command": bash.command,
            "output": bash.output,
        }),
        Part::Passthrough(passthrough) => json!({
            "type": "passthrough",
            "kind": passthrough.kind,
            "raw": passthrough.raw,
        }),
    }
}

#[must_use]
pub fn hits(hits: &[SearchHit]) -> Value {
    let hits: Vec<Value> = hits
        .iter()
        .map(|h| {
            json!({
                "entryId": h.entry_id,
                "score": h.score,
                "role": h.role,
                "text": h.text,
                "files": h.files,
                "terms": h.terms,
            })
        })
        .collect();
    json!(hits)
}