lernie 0.1.56

lernie: the operator seat — the window and wire client for a yog server
Documentation
//! **Which conversation this is**, over the transcript it is a header for
//! (bl-7b03).
//!
//! The pane carried the word *conversation* and nothing else — on every
//! conversation, in every state. A reader scrolling a transcript could not tell
//! whether what they were reading was still running, what it had cost, or which
//! model had answered; the facts existed, typed and rendered, on the records
//! pane two clicks away.
//!
//! **The column's NAME is still the shell's** (bl-dfda): *conversation* is what
//! the layout paints above the pane and what the narrow shape's bar carries,
//! and a second node wearing it would be two things to tell apart. What is here
//! is not a name but CONTENT — the subject the rest of the pane is about, which
//! is exactly where the records pane puts its own header (§4.32).
//!
//! **Every line is the engine's**, composed by `crate::ui::records::header` and
//! read from there rather than re-derived: the spend is a figure the engine
//! summed, the model is the one its context reading names, the failure clause
//! is the provider's own sentence. Two surfaces reading one answer is one home;
//! two surfaces composing one sentence is two.

use crate::reply::agent::Agent;
use crate::reply::transcript::{EntryKind, Transcript};
use crate::ui::records::header;
use crate::ui::theme;

/// **What the pane says over a conversation nobody has been answered about
/// yet** — the records pane's own sentence, because it is the same absence.
pub use header::NOT_ANSWERED;

/// **The identity line, as one string** — what it is called, how it is
/// resting, and, where the costing line below does not already say it, which
/// model answered.
///
/// The tip the records pane's own `named` carries is left out on purpose: a
/// branch oid is what a `git show` outside this seat takes, and the question
/// this line answers is *am I reading something that is still running*.
///
/// **The glass paints its parts, not this string** ([`render`]): the three
/// clauses carry three inks and a sentence carries one, so the joined form is
/// what a reader outside the window is handed — a CLI line, a test, a
/// message — and the header is the same three facts weighted.
pub fn named(row: &Agent, transcript: &Transcript) -> String {
    let said = format!("{} — {}", row.display, header::resting(row));
    match answered_by(row, transcript) {
        Some(model) => format!("{said} — {model}"),
        None => said,
    }
}

/// **Which model answered, where nothing else on this header says so.**
///
/// Two places can answer and they answer different questions. The engine's
/// context reading names the model it is holding a window open for, which is
/// the one the NEXT turn will use — the better answer, and the one
/// [`costing`] already carries. But a reading exists only while there is one
/// to take, and a **quiescent** conversation has none: that is the state the
/// question is actually asked in, and it is why *nothing in the window says
/// which model answered* was true of every conversation that had finished.
///
/// So the transcript's own last model turn answers when the engine's reading
/// does not. It is a fact this seat already holds, about a turn that
/// happened, and never a prediction about the next one — which is also why it
/// stands down the moment the engine has a reading of its own, rather than
/// being joined beside it.
fn answered_by(row: &Agent, transcript: &Transcript) -> Option<String> {
    if row.context.is_some() {
        return None;
    }
    transcript
        .entries
        .iter()
        .rev()
        .find_map(|entry| match &entry.kind {
            EntryKind::Model { model_id, .. } => Some(model_id.clone()),
            _ => None,
        })
}

/// **The costing line**: what it has spent, and which model it is on.
///
/// One call rather than two facts joined here, because the engine states them
/// together and the joining is already stated once (`header::costing`).
pub fn costing(row: &Agent) -> String {
    header::costing(row)
}

/// Paint the header, or the sentence for a conversation nobody has answered
/// about yet.
pub fn render(ui: &mut egui::Ui, model: &crate::ui::Model) {
    let Some(row) = model.records.agent.as_ref() else {
        ui.colored_label(theme::INK_WEAK, NOT_ANSWERED);
        return;
    };
    // **The header is a place's name, not a line of facts** (bl-f251, item
    // 2; STYLE §2): the name at HEADING size in body ink because it is what
    // the reader came for, the resting clause beside it in that state's own
    // accent because *is this still running* is the question the header
    // exists to answer at a glance. An unknown wire word keeps body ink
    // rather than borrowing a state it is not (`theme::state_ink`).
    ui.horizontal(|ui| {
        ui.label(
            egui::RichText::new(&row.display)
                .heading()
                .color(theme::INK),
        );
        ui.label(egui::RichText::new(header::resting(row)).color(theme::state_ink(&row.state)));
    });
    // **A conversation that died on a bad model id must not look like one that
    // finished**, which is the ball's own sentence: the provider's clause is
    // the one fact that tells the two apart, and it goes above the costing
    // because it is why there is no more of it. It is said in the error accent
    // and not in a note's, because it will not mend itself.
    if let Some(failure) = &row.failure {
        ui.colored_label(theme::accent(theme::State::Error), failure);
    }
    // **One line, one step weaker, for what it is on and what it has spent**:
    // the model that answered where the engine holds no reading of its own,
    // then the costing — which names the model itself where it does.
    ui.horizontal(|ui| {
        if let Some(model_id) = answered_by(row, &model.transcript) {
            ui.label(egui::RichText::new(model_id).color(theme::INK_WEAK));
        }
        ui.colored_label(theme::INK_WEAK, costing(row));
    });
    // **And where it hangs, if it hangs under anything** — the subtree path,
    // in the same weak ink, because a child read without its parent's name is
    // a place with no address.
    if let Some(path) = header::descent(row) {
        ui.colored_label(theme::INK_WEAK, path);
    }
    ui.add_space(theme::space::S);
}

#[cfg(test)]
mod tests;