yog 0.0.1

yog: a balls-oriented session manager for lernie loops (egui frontend)
Documentation
//! egui widget: the §11 Altitude-2 Transcript tab.
//!
//! A scrollable list over the [`Transcript`] view-model — delivered
//! messages (sender-tagged), model text and thinking, tool call/result
//! pairs (an in-progress call pulses, consistent with the git_tree tool
//! pulse), and the live streaming tail (visually distinct). `raw` flips the
//! whole tab to verbatim bytes for every entry (§11: "every tab has a Raw
//! toggle showing verbatim bytes") — a pure function of the view-model and
//! that one bit, so both branches are headless shape-walk-testable; the
//! click that flips it is shell glue (§11 widget-split discipline).

use super::{Block, Entry, EntryKind, Transcript};
use crate::theme;

/// Render the transcript. `raw` shows verbatim backing bytes for every
/// entry; otherwise each entry renders its parsed projection.
pub fn render(ui: &mut egui::Ui, transcript: &Transcript, raw: bool) {
    egui::ScrollArea::vertical().show(ui, |ui| {
        if transcript.entries.is_empty() {
            ui.label("(no messages yet)");
            return;
        }
        for entry in &transcript.entries {
            if raw {
                render_raw(ui, entry);
            } else {
                render_entry(ui, transcript, entry);
            }
        }
    });
}

/// Verbatim backing bytes under a filename header — the Raw view of any
/// entry, and the fallback for an unparseable (`Raw`) entry.
fn render_raw(ui: &mut egui::Ui, entry: &Entry) {
    ui.monospace(&entry.name);
    ui.monospace(String::from_utf8_lossy(&entry.raw).to_string());
}

fn render_entry(ui: &mut egui::Ui, transcript: &Transcript, entry: &Entry) {
    match &entry.kind {
        EntryKind::Delivered { sender, body } => {
            ui.strong(format!("{sender}:"));
            ui.label(body);
        }
        EntryKind::Model { model_id, blocks } => {
            ui.weak(model_id);
            for block in blocks {
                render_block(ui, transcript, block);
            }
        }
        EntryKind::ToolResult {
            tool_use_id,
            content,
            is_error,
        } => render_tool_result(ui, tool_use_id, content, *is_error),
        EntryKind::Streaming { text } => render_streaming(ui, text),
        EntryKind::Raw => render_raw(ui, entry),
    }
}

fn render_block(ui: &mut egui::Ui, transcript: &Transcript, block: &Block) {
    match block {
        Block::Text(text) => {
            ui.label(text);
        }
        Block::Thinking(text) => {
            ui.weak(format!("thinking: {text}"));
        }
        Block::ToolUse {
            id,
            name,
            input_summary,
        } => render_tool_use(ui, transcript.tool_in_progress(id), id, name, input_summary),
    }
}

/// A `tool_use` call chip: `name(input-summary)` plus the id. An in-progress
/// call pulses and pulls a near-term repaint, exactly as the git_tree tool
/// indicator does.
fn render_tool_use(ui: &mut egui::Ui, in_progress: bool, id: &str, name: &str, input: &str) {
    ui.horizontal(|ui| {
        ui.monospace("⚙");
        let chip = format!("{name}({input})");
        if in_progress {
            let time = ui.ctx().input(|i| i.time);
            ui.colored_label(theme::pulse(time), chip);
            ui.label("(in-flight)");
            ui.ctx().request_repaint_after(theme::PULSE_REPAINT_DELAY);
        } else {
            ui.label(chip);
        }
        ui.weak(id);
    });
}

fn render_tool_result(ui: &mut egui::Ui, tool_use_id: &str, content: &str, is_error: bool) {
    ui.horizontal(|ui| {
        let (glyph, color) = if is_error {
            ("✗", theme::ICHOR)
        } else {
            ("✓", theme::HYDRA)
        };
        ui.colored_label(color, glyph);
        ui.weak(tool_use_id);
    });
    ui.monospace(content);
}

fn render_streaming(ui: &mut egui::Ui, text: &str) {
    // Spectral blue — the same hue as the `InFlight` state badge, so the
    // streaming tail reads as "this agent is talking right now".
    ui.colored_label(theme::SPECTRE, "live");
    ui.label(text);
}