pub mod cascade;
pub mod drill;
pub mod header;
pub mod mail;
pub mod spine;
use crate::reply::files::{Files, Preview};
use crate::reply::steps::{StepRow, Steps};
use crate::ui::{Model, theme};
pub const OPEN: &str = "records…";
pub const CLOSE: &str = "done";
pub const HEADING: &str = "records";
pub const STEPS_HEAD: &str = "steps";
pub const FILES_HEAD: &str = "files";
pub const NOT_ANSWERED_STEPS: &str = "waiting to hear what this conversation's loop has done";
pub const NO_STEPS: &str = "its loop has taken no step yet";
pub const NOT_ANSWERED_FILES: &str = "waiting to hear what its worktree holds";
pub const NO_WORKTREE: &str = "no worktree stands for this conversation";
pub const EMPTY_WORKTREE: &str = "the worktree holds nothing";
pub const TRUNCATED: &str = "…and more — the listing was cut short";
pub fn render(ui: &mut egui::Ui, model: &mut Model) -> bool {
if !model.showing(crate::ui::Listing::Records) {
return false;
}
ui.heading(HEADING);
if let Some(conversation) = model.conversation.clone() {
ui.label(format!("on {conversation}"));
}
ui.horizontal_wrapped(|ui| {
if ui.button(CLOSE).clicked() {
model.close_records();
}
});
ui.separator();
let (steps, files) = (model.records.steps.clone(), model.records.files.clone());
egui::ScrollArea::vertical()
.id_salt(HEADING)
.auto_shrink(false)
.show(ui, |ui| {
header::render(ui, model);
ui.separator();
steps_half(ui, model, steps.as_ref());
ui.separator();
files_half(ui, files.as_ref());
ui.separator();
spine::render(ui, model);
ui.separator();
mail::render(ui, model);
});
true
}
fn steps_half(ui: &mut egui::Ui, model: &mut Model, steps: Option<&Steps>) {
ui.horizontal_wrapped(|ui| {
ui.label(egui::RichText::new(STEPS_HEAD).strong());
match steps {
None => {
ui.label(NOT_ANSWERED_STEPS);
}
Some(listing) => {
if let Some(said) = orphaned(listing) {
ui.colored_label(theme::NOTICE, said);
}
if listing.rows.is_empty() {
ui.label(NO_STEPS);
}
}
}
});
let Some(listing) = steps else {
return;
};
for row in &listing.rows {
step(ui, model, row);
}
}
fn step(ui: &mut egui::Ui, model: &mut Model, row: &StepRow) {
ui.horizontal_wrapped(|ui| {
ui.label(headline(row));
if let Some(weak) = provenance(row) {
ui.colored_label(theme::tone_ink(&crate::reply::convs::Tone::Weak), weak);
}
drill::control(ui, model, &row.seq);
});
ui.horizontal_wrapped(|ui| {
for said in [wounded(row), auth(row)].into_iter().flatten() {
ui.colored_label(theme::NOTICE, said);
}
});
drill::records(ui, model, &row.seq);
}
fn files_half(ui: &mut egui::Ui, files: Option<&Files>) {
let Some(answer) = files else {
ui.label(egui::RichText::new(FILES_HEAD).strong());
ui.label(NOT_ANSWERED_FILES);
return;
};
ui.horizontal_wrapped(|ui| {
ui.label(egui::RichText::new(FILES_HEAD).strong());
if let Some(dir) = &answer.working_dir {
ui.colored_label(
theme::tone_ink(&crate::reply::convs::Tone::Weak),
format!("working in {dir}"),
);
}
});
match &answer.listing {
None => {
ui.label(NO_WORKTREE);
}
Some(listing) if listing.rows.is_empty() => {
ui.label(EMPTY_WORKTREE);
}
Some(listing) => {
ui.horizontal_wrapped(|ui| {
for row in &listing.rows {
ui.label(entry(row));
}
if listing.truncated {
ui.colored_label(theme::NOTICE, TRUNCATED);
}
});
}
}
if let Some(preview) = &answer.preview {
ui.label(egui::RichText::new(previewed(preview)).monospace());
}
}
pub fn entry(row: &crate::reply::files::FileRow) -> String {
if row.dir {
format!("{}/", row.path)
} else {
format!("{} {} B", row.path, row.size)
}
}
pub fn previewed(preview: &Preview) -> String {
match preview {
Preview::Text(text) => text.clone(),
Preview::Truncated { text, size } => format!("{text}\n… {size} bytes in all"),
Preview::Binary { size } => format!("binary — {size} bytes"),
Preview::Unknown(word) => format!("a {word:?} preview, which this seat cannot show"),
}
}
pub fn orphaned(listing: &Steps) -> Option<String> {
if listing.orphan == crate::reply::steps::NONE {
return None;
}
Some(match &listing.orphan_reason {
Some(reason) => format!("an orphaned {} tail — {reason}", listing.orphan),
None => format!("an orphaned {} tail", listing.orphan),
})
}
pub fn headline(row: &StepRow) -> String {
let said = format!("{} {} — {} tokens", row.seq, row.framing, row.tokens.total);
if row.attempts > 1 {
return format!("{said}, {} attempts", row.attempts);
}
said
}
pub fn provenance(row: &StepRow) -> Option<String> {
let mut parts = Vec::new();
if let (Some(from), Some(to)) = (&row.started_at, &row.ended_at) {
parts.push(format!("{from} to {to}"));
}
if let Some(commit) = &row.commit {
parts.push(format!("at {commit}"));
}
(!parts.is_empty()).then(|| parts.join(" "))
}
pub fn wounded(row: &StepRow) -> Option<String> {
if row.wound == crate::reply::steps::NONE {
return None;
}
Some(match &row.wound_reason {
Some(reason) => format!("wound: {} — {reason}", row.wound),
None => format!("wound: {}", row.wound),
})
}
pub fn auth(row: &StepRow) -> Option<String> {
if row.wound != crate::reply::steps::REFUSED {
return None;
}
Some(match &row.auth_row {
Some(provider) => format!("a sign-in is wanted on {provider}"),
None => "a sign-in is wanted".to_owned(),
})
}
#[cfg(test)]
mod tests;