use super::super::compose::Intent;
use super::super::{Attempt, Outcome};
use super::Seat;
pub(super) fn candidate(ui: &mut egui::Ui, row: &Attempt, seat: &mut Seat) {
let handle = row.diff.handle.clone().unwrap_or_default();
ui.horizontal_wrapped(|ui| {
ui.monospace(&handle);
outcome(ui, &row.outcome);
});
figures(ui, row);
ui.horizontal_wrapped(|ui| {
let picked = seat.compare.iter().any(|h| h == &handle);
if ui
.selectable_label(picked, "compare")
.on_hover_text(
"Pick two candidates to diff their terminal responses below. No key of \
its own: Tab reaches it, Space presses it.",
)
.clicked()
{
super::toggle(&mut seat.compare, &handle, picked);
}
if ui
.button("Deliver")
.on_hover_text(
"Accept this candidate: seed `/deliver` in the composer — add your \
summary of what landed and press Enter. Delivery is the ordinary \
source-to-target delivery; a stale candidate is refused before \
anything merges.",
)
.clicked()
{
seat.intent = Some(Intent::Deliver {
handle: handle.clone(),
});
}
if ui
.button("Retire")
.on_hover_text(
"Release this candidate's worktree; its branch stays readable until \
the project's retention policy expires it. Seeds `/retire` in the \
composer — Enter runs it.",
)
.clicked()
{
seat.intent = Some(Intent::Retire { handle });
}
});
}
fn figures(ui: &mut egui::Ui, row: &Attempt) {
match &row.conversation {
Some(_) => ui.weak(format!(
"{} steps · {} wall · {} tokens",
row.steps,
wall(row.wall_secs),
row.usage.total_tokens()
)),
None => ui.weak("no conversation bound yet"),
};
ui.weak(churn(row)).on_hover_text(
"This candidate's project churn — the same target..source read the rows \
below drill into.",
);
if row.compacted > 0 {
ui.weak(format!("record compacted — {} entries gone", row.compacted))
.on_hover_text(
"lernie's compactor deleted entries from this conversation, so the \
verdicts and the response read the surviving record; anything \
delivered in the squashed span is gone from disk, not hidden.",
);
}
match &row.response {
Some(text) => ui.label(clipped(text)),
None => ui.weak("nothing said yet"),
};
}
fn outcome(ui: &mut egui::Ui, outcome: &Outcome) {
match outcome {
Outcome::Accepted { commit } => {
ui.strong(format!("delivered {}", short(commit)))
.on_hover_text(
"The target's own history records this candidate's delivery — the \
tagged squash, derived at read time.",
);
}
Outcome::Rejected { by: Some(by) } => {
ui.weak(format!("stale — {by} delivered")).on_hover_text(
"A sibling's delivery advanced the target. Rework this candidate \
(message it to incorporate the new target) and it can deliver again.",
);
}
Outcome::Rejected { by: None } => {
ui.weak("discarded").on_hover_text(
"This candidate's branch is gone — retired past the project's \
retention rule.",
);
}
Outcome::Reworked => {
ui.weak("reworked").on_hover_text(
"The source has incorporated the advanced target, so delivery would \
no longer refuse it as stale.",
);
}
Outcome::Pending => {
ui.weak("pending")
.on_hover_text("No delivery yet, this candidate's or any sibling's.");
}
}
}
fn churn(row: &Attempt) -> String {
match &row.diff.change {
crate::workdiff::Change::Diff { files, .. } => {
let (added, removed) = files.iter().fold((0u64, 0u64), |(a, r), f| match &f.churn {
crate::workdiff::Churn::Text { added, removed } => (a + added, r + removed),
crate::workdiff::Churn::Binary => (a, r),
});
format!("+{added} −{removed} across {} files", files.len())
}
crate::workdiff::Change::Absent { missing, .. } => {
format!("no {} yet", missing.join(" and no "))
}
crate::workdiff::Change::Unreadable => "project unreadable".to_owned(),
}
}
fn wall(secs: u64) -> String {
match secs {
s if s >= 3600 => format!("{}h{:02}m", s / 3600, (s % 3600) / 60),
s if s >= 60 => format!("{}m{:02}s", s / 60, s % 60),
s => format!("{s}s"),
}
}
fn clipped(text: &str) -> String {
const CAP: usize = 280;
match text
.char_indices()
.nth(CAP)
.and_then(|(at, _)| text.get(..at))
{
Some(head) => format!("{head}…"),
None => text.to_owned(),
}
}
pub(super) fn short(oid: &str) -> String {
oid.get(..crate::rail::SHORT_OID).unwrap_or(oid).to_owned()
}