yog 0.0.1

yog: a balls-oriented session manager for lernie loops (egui frontend)
Documentation
//! The activity accessory (§11): the demoted ops pane, a collapsed bottom
//! chip that expands on demand to the `ops.jsonl` tail — never inline rows in
//! conversation space. Coverage-excluded glue: the chip counts
//! ([`AppModel::activity`]) and the rows ([`AppModel::ops_rows`]) are tested
//! view-models; this file only wires the collapsing header.

use crate::AppModel;
use crate::theme;

/// The collapsed chip (`activity · N ops · M ⚠`, error count in ichor) and,
/// expanded, the ops rows — each opening to the full entry (argv, cwd, exit,
/// stderr), because a trail that hides *why* is not a trail (§7.3). Default
/// collapsed; per-instance viewport state (§13.0).
pub fn accessory(ui: &mut egui::Ui, model: &AppModel) {
    let summary = model.activity();
    let heading = if summary.errors > 0 {
        egui::RichText::new(summary.chip()).color(theme::ICHOR)
    } else {
        egui::RichText::new(summary.chip()).weak()
    };
    egui::CollapsingHeader::new(heading)
        .id_salt("activity-accessory")
        .default_open(false)
        .show(ui, |ui| {
            egui::ScrollArea::vertical()
                .max_height(160.0)
                .show(ui, |ui| {
                    // Liveness is positional over the same rows (§6 retirement).
                    let liveness = crate::opslog::live_failures(model.ops_rows());
                    for (i, (row, live)) in model.ops_rows().iter().zip(liveness).enumerate() {
                        ui.push_id(i, |ui| ops_row(ui, row, live));
                    }
                });
        });
}

/// One expandable ops row (§7.3): the summary heading — ichor red on a **live**
/// failure — opens to the full `ops.jsonl` entry. A *retired* failure (§6: a
/// later clean run of the same verb superseded it) keeps its ⚠ and its record
/// but goes weak: the fact stays, the prominence retires.
fn ops_row(ui: &mut egui::Ui, row: &crate::opslog::OpRow, live: bool) {
    let failed = row.failed();
    // A "⋯" hint marks a row worth expanding — one whose captured streams carried
    // bytes ([`OpRow::has_output`]); a bare row expands only to cwd/exit.
    let more = if row.has_output() { " ⋯" } else { "" };
    let summary = format!(
        "{} {} {}{more}",
        if failed { "⚠" } else { "·" },
        row.ts,
        row.argv
    );
    let heading = if live {
        egui::RichText::new(summary).color(theme::ICHOR)
    } else if failed {
        egui::RichText::new(summary).weak()
    } else {
        egui::RichText::new(summary)
    };
    ui.collapsing(heading, |ui| {
        ui.label(format!("cwd: {}", row.cwd));
        ui.label(format!("exit: {}", row.exit));
        if !row.stdout.is_empty() {
            ui.label("stdout:");
            ui.code(&row.stdout);
        }
        if !row.stderr.is_empty() {
            ui.colored_label(theme::ICHOR, "stderr:");
            ui.code(&row.stderr);
        }
    });
}