goosedump 0.12.4

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

//! Per-client output behavior behind a single vtable. Each provider knows how
//! to emit a compaction summary (on-disk artifact and on-screen print), render
//! a full context back to its native form, and import a context into its own
//! store. [`Client::behavior`] returns the client's implementation, replacing
//! the per-operation `match client` dispatch blocks that used to live in
//! [`emit`], [`format`], and [`import`].
//!
//! The impls delegate to the per-client routines in those modules, where the
//! rendering helpers live; the trait unifies the signatures (some providers
//! ignore `entries`, `cwd`, or `summary`) so call sites dispatch once through
//! the vtable 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>;
}

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)
    }
}

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)
    }
}

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)
    }
}

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)
    }
}

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)
    }
}

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)
    }
}

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,
        }
    }
}