goosedump 0.12.5

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

//! Per-client capabilities behind vtables.
//!
//! [`ClientBehavior`] is the write/render half: compaction emit, native render,
//! and import. [`crate::resolver::ProviderStore`] is the read/list half: open a
//! context reader and enumerate sessions. Both share the same per-provider unit
//! structs so adding a client is one type with two trait impls.
//!
//! [`Client::behavior`] returns the write vtable. The impls delegate to
//! [`emit`], [`format`], and [`import`]; the trait unifies signatures so call
//! sites dispatch once instead of matching on every variant.

use crate::compact::Summary;
use crate::message::{Context, ConversationMessage, Entry};
use crate::{Client, emit, format, import};

/// How a provider turns goosedump's IR into its native artifacts.
pub(crate) trait ClientBehavior {
    /// Native on-disk compaction artifact (was `emit::render`).
    fn render_summary(&self, summary: &Summary, context_id: &str, cwd: Option<&str>) -> String;
    /// On-screen compaction print (was `emit::print`).
    fn print_summary(&self, summary: &Summary) -> String;
    /// Render a full context back to native form (was `format::render`).
    fn render_context(
        &self,
        entries: &[Entry],
        messages: &[ConversationMessage],
        context_id: &str,
        cwd: Option<&str>,
    ) -> String;
    /// Persist a context into the provider's store, returning the new id.
    fn import_context(&self, ctx: &Context) -> anyhow::Result<String>;
}

pub(crate) struct Claude;
impl ClientBehavior for Claude {
    fn render_summary(&self, summary: &Summary, context_id: &str, cwd: Option<&str>) -> String {
        emit::render_claude(summary, context_id, cwd)
    }
    fn print_summary(&self, summary: &Summary) -> String {
        emit::print_claude(summary)
    }
    fn render_context(
        &self,
        entries: &[Entry],
        messages: &[ConversationMessage],
        context_id: &str,
        cwd: Option<&str>,
    ) -> String {
        format::render_claude(entries, messages, context_id, cwd)
    }
    fn import_context(&self, ctx: &Context) -> anyhow::Result<String> {
        import::write_claude(ctx)
    }
}

pub(crate) struct Codex;
impl ClientBehavior for Codex {
    fn render_summary(&self, summary: &Summary, context_id: &str, cwd: Option<&str>) -> String {
        emit::render_codex(summary, context_id, cwd)
    }
    fn print_summary(&self, _summary: &Summary) -> String {
        emit::print_codex()
    }
    fn render_context(
        &self,
        _entries: &[Entry],
        messages: &[ConversationMessage],
        context_id: &str,
        cwd: Option<&str>,
    ) -> String {
        format::render_codex(messages, context_id, cwd)
    }
    fn import_context(&self, ctx: &Context) -> anyhow::Result<String> {
        import::write_codex(ctx)
    }
}

pub(crate) struct Crush;
impl ClientBehavior for Crush {
    fn render_summary(&self, summary: &Summary, context_id: &str, _cwd: Option<&str>) -> String {
        emit::render_crush(summary, context_id)
    }
    fn print_summary(&self, summary: &Summary) -> String {
        emit::print_crush(summary)
    }
    fn render_context(
        &self,
        _entries: &[Entry],
        messages: &[ConversationMessage],
        context_id: &str,
        _cwd: Option<&str>,
    ) -> String {
        format::render_crush(messages, context_id)
    }
    fn import_context(&self, ctx: &Context) -> anyhow::Result<String> {
        import::write_crush(ctx)
    }
}

pub(crate) struct Gemini;
impl ClientBehavior for Gemini {
    fn render_summary(&self, summary: &Summary, context_id: &str, cwd: Option<&str>) -> String {
        emit::render_gemini(summary, context_id, cwd)
    }
    fn print_summary(&self, summary: &Summary) -> String {
        emit::print_gemini(summary)
    }
    fn render_context(
        &self,
        _entries: &[Entry],
        messages: &[ConversationMessage],
        context_id: &str,
        _cwd: Option<&str>,
    ) -> String {
        format::render_gemini(messages, context_id)
    }
    fn import_context(&self, ctx: &Context) -> anyhow::Result<String> {
        import::write_gemini(ctx)
    }
}

pub(crate) struct Goose;
impl ClientBehavior for Goose {
    fn render_summary(&self, summary: &Summary, context_id: &str, cwd: Option<&str>) -> String {
        emit::render_goose(summary, context_id, cwd)
    }
    fn print_summary(&self, _summary: &Summary) -> String {
        emit::print_goose()
    }
    fn render_context(
        &self,
        _entries: &[Entry],
        messages: &[ConversationMessage],
        context_id: &str,
        cwd: Option<&str>,
    ) -> String {
        format::render_goose(messages, context_id, cwd)
    }
    fn import_context(&self, ctx: &Context) -> anyhow::Result<String> {
        import::write_goose(ctx)
    }
}

pub(crate) struct Opencode;
impl ClientBehavior for Opencode {
    fn render_summary(&self, summary: &Summary, context_id: &str, cwd: Option<&str>) -> String {
        emit::render_opencode(summary, context_id, cwd)
    }
    fn print_summary(&self, summary: &Summary) -> String {
        emit::print_opencode(summary)
    }
    fn render_context(
        &self,
        entries: &[Entry],
        messages: &[ConversationMessage],
        context_id: &str,
        cwd: Option<&str>,
    ) -> String {
        format::render_opencode(entries, messages, context_id, cwd)
    }
    fn import_context(&self, ctx: &Context) -> anyhow::Result<String> {
        import::write_opencode(ctx)
    }
}

pub(crate) struct Pi;
impl ClientBehavior for Pi {
    fn render_summary(&self, summary: &Summary, context_id: &str, cwd: Option<&str>) -> String {
        emit::render_pi(summary, context_id, cwd)
    }
    fn print_summary(&self, summary: &Summary) -> String {
        emit::print_pi(summary)
    }
    fn render_context(
        &self,
        entries: &[Entry],
        messages: &[ConversationMessage],
        context_id: &str,
        cwd: Option<&str>,
    ) -> String {
        format::render_pi(entries, messages, context_id, cwd)
    }
    fn import_context(&self, ctx: &Context) -> anyhow::Result<String> {
        import::write_pi(ctx)
    }
}

impl Client {
    /// The client's output behavior, dispatched through a vtable rather than a
    /// per-call-site `match`.
    pub(crate) fn behavior(self) -> &'static dyn ClientBehavior {
        match self {
            Self::Claude => &Claude,
            Self::Codex => &Codex,
            Self::Crush => &Crush,
            Self::Gemini => &Gemini,
            Self::Goose => &Goose,
            Self::Opencode => &Opencode,
            Self::Pi => &Pi,
        }
    }
}