use super::{Fold, fold};
use crate::ui::theme::Speaker;
mod live;
use crate::reply::stream::Stream;
use crate::reply::transcript::{Block, Entry, EntryKind, Transcript};
use live::{half, live_rows, streaming};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Row {
pub who: String,
pub said: String,
pub weight: Speaker,
pub failed: bool,
pub fold: Option<Fold>,
}
impl Row {
pub(super) fn plain(who: String, said: String, weight: Speaker) -> Self {
Self {
who,
said,
weight,
failed: false,
fold: None,
}
}
}
const OPERATOR: [&str; 2] = ["user", "op"];
fn delivered(sender: &str, epitaph: Option<&String>) -> Speaker {
if OPERATOR.contains(&sender) {
Speaker::Operator
} else if epitaph.is_some() {
Speaker::Ended
} else {
Speaker::Peer
}
}
pub fn rows(transcript: &Transcript, live: Option<&Stream>) -> Vec<Row> {
let mut out: Vec<Row> = transcript
.entries
.iter()
.filter(|entry| live.is_none() || !matches!(entry.kind, EntryKind::Streaming { .. }))
.flat_map(entry_rows)
.collect();
out.extend(live.into_iter().flat_map(streaming));
out
}
fn entry_rows(entry: &Entry) -> Vec<Row> {
match &entry.kind {
EntryKind::Delivered {
sender,
sender_name,
epitaph,
body,
} => {
let who = crate::reply::transcript::said_by(sender, sender_name.as_deref());
let weight = delivered(sender, epitaph.as_ref());
vec![Row::plain(
match epitaph {
Some(word) => format!("{who} ({word})"),
None => who,
},
body.clone(),
weight,
)]
}
EntryKind::Model {
model_id, blocks, ..
} => blocks.iter().filter_map(|b| block(model_id, b)).collect(),
EntryKind::ToolResult {
tool_use_id,
content,
is_error,
} => vec![Row {
who: format!(
"{tool_use_id} {}",
if *is_error { "failed" } else { "returned" }
),
said: content.clone(),
weight: Speaker::Peer,
failed: *is_error,
fold: fold::of(content),
}],
EntryKind::Streaming { thinking, text } => live_rows(thinking, text),
EntryKind::Compacted {
first,
last,
summary,
} => vec![Row::plain(
format!("compacted {first}–{last}"),
summary.clone(),
Speaker::Peer,
)],
EntryKind::Raw => vec![Row::plain(
format!("{} (unparsed)", entry.name),
entry.raw.clone(),
Speaker::Peer,
)],
EntryKind::Unknown(word) => vec![Row::plain(
format!("{} ({word}, which this seat cannot read)", entry.name),
entry.raw.clone(),
Speaker::Peer,
)],
}
}
fn block(model_id: &str, block: &Block) -> Option<Row> {
match block {
Block::Text(text) => half(model_id, "", text),
Block::Thinking(text) => half(model_id, "thinking", text),
Block::ToolUse { id, name, input } => Some(Row::plain(
format!("{model_id} → {name} {id}"),
input.clone(),
Speaker::Model,
)),
Block::Unknown(word) => Some(Row::plain(
format!("{model_id} ({word}, which this seat cannot read)"),
String::new(),
Speaker::Model,
)),
}
}