yog 0.0.1

yog: a balls-oriented session manager for lernie loops (egui frontend)
Documentation
//! egui widget: the §11 Altitude-2 Inbox tab — the agent's undelivered
//! deposits (`✉n`), explained.
//!
//! A pure function of the [`Deposit`] listing (§2.11): each deposit's framing
//! (sender), its asserted facts (`from` / `deposited_at`, plus `epitaph` /
//! `terminal_ref` on a result message), and its body verbatim. No click, so
//! the whole tab is a headless shape-walk-tested pure render; the Flush =
//! `lernie scan` action is the composer's Scan verb, wired in the shell.

use super::{Deposit, Epitaph};

/// Render the inbox listing, oldest-first. An empty inbox shows a placeholder
/// (the common quiescent case, not an error).
pub fn render(ui: &mut egui::Ui, deposits: &[Deposit]) {
    egui::ScrollArea::vertical().show(ui, |ui| {
        if deposits.is_empty() {
            ui.label("(no deposits)");
            return;
        }
        for deposit in deposits {
            render_deposit(ui, deposit);
            ui.separator();
        }
    });
}

/// One deposit: a `from · deposited_at` header, the result-message epitaph +
/// terminal ref when present (§2.6), then the body verbatim. Absent fields
/// read as `?` rather than vanishing, so a hand-edited deposit stays legible.
fn render_deposit(ui: &mut egui::Ui, deposit: &Deposit) {
    let from = deposit.sender.as_deref().unwrap_or("?");
    let at = deposit.deposited_at.as_deref().unwrap_or("?");
    // Brazen — the same hue as the tree's `✉n` badge, so "pending mail"
    // reads as one signal across altitudes.
    ui.label(
        egui::RichText::new(format!("✉ {from} · {at}"))
            .color(crate::theme::BRAZEN)
            .strong(),
    );
    if let Some(epitaph) = &deposit.epitaph {
        ui.weak(format!("epitaph: {}", epitaph_label(epitaph)));
    }
    if let Some(terminal) = &deposit.terminal_ref {
        ui.weak(format!("terminal: {terminal}"));
    }
    ui.label(&deposit.body);
}

/// The on-screen wording of an [`Epitaph`] (§2.6); an unknown forward-compat
/// value rides through verbatim.
fn epitaph_label(epitaph: &Epitaph) -> String {
    match epitaph {
        Epitaph::FinalResponse => "final-response".to_string(),
        Epitaph::Stopped => "stopped".to_string(),
        Epitaph::BudgetExhausted => "budget-exhausted".to_string(),
        Epitaph::Died => "died".to_string(),
        Epitaph::Unknown(value) => value.clone(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn screen() -> egui::RawInput {
        egui::RawInput {
            screen_rect: Some(egui::Rect::from_min_size(
                egui::Pos2::ZERO,
                egui::vec2(1024.0, 4096.0),
            )),
            ..Default::default()
        }
    }

    fn collect_text(shape: &egui::Shape, out: &mut String) {
        match shape {
            egui::Shape::Text(t) => {
                out.push_str(t.galley.text());
                out.push('\n');
            }
            egui::Shape::Vec(shapes) => shapes.iter().for_each(|s| collect_text(s, out)),
            _ => {}
        }
    }

    fn painted(deposits: &[Deposit]) -> String {
        let ctx = egui::Context::default();
        let out = ctx.run(screen(), |ctx| {
            egui::CentralPanel::default().show(ctx, |ui| render(ui, deposits));
        });
        let mut text = String::new();
        for clipped in out.shapes {
            collect_text(&clipped.shape, &mut text);
        }
        text
    }

    #[test]
    fn empty_inbox_shows_placeholder() {
        assert!(painted(&[]).contains("(no deposits)"));
    }

    #[test]
    fn ordinary_deposit_renders_header_and_body() {
        let d = Deposit {
            sender: Some("user".into()),
            deposited_at: Some("2026-07-17T12:00:00Z".into()),
            epitaph: None,
            terminal_ref: None,
            body: "please continue".into(),
        };
        let text = painted(std::slice::from_ref(&d));
        assert!(
            text.contains("✉ user · 2026-07-17T12:00:00Z"),
            "got:\n{text}"
        );
        assert!(text.contains("please continue"));
        // No epitaph line for a plain deposit.
        assert!(!text.contains("epitaph:"));
    }

    #[test]
    fn result_message_renders_every_epitaph_and_terminal_ref() {
        let cases = [
            (Epitaph::FinalResponse, "final-response"),
            (Epitaph::Stopped, "stopped"),
            (Epitaph::BudgetExhausted, "budget-exhausted"),
            (Epitaph::Died, "died"),
            (Epitaph::Unknown("wat".into()), "wat"),
        ];
        for (epitaph, label) in cases {
            let d = Deposit {
                sender: Some("c-1".into()),
                deposited_at: Some("t".into()),
                epitaph: Some(epitaph),
                terminal_ref: Some("deadbeef".into()),
                body: String::new(),
            };
            let text = painted(std::slice::from_ref(&d));
            assert!(text.contains(&format!("epitaph: {label}")), "got:\n{text}");
            assert!(text.contains("terminal: deadbeef"));
        }
    }

    #[test]
    fn absent_fields_render_as_question_marks() {
        let d = Deposit {
            sender: None,
            deposited_at: None,
            epitaph: None,
            terminal_ref: None,
            body: "raw".into(),
        };
        let text = painted(std::slice::from_ref(&d));
        assert!(text.contains("✉ ? · ?"), "got:\n{text}");
    }
}