use super::{Fold, LIVE, fold};
use crate::reply::stream::Stream;
use crate::reply::transcript::{Block, Entry, EntryKind, Transcript};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Row {
pub who: String,
pub said: String,
pub fold: Option<Fold>,
}
impl Row {
fn plain(who: String, said: String) -> Self {
Self {
who,
said,
fold: None,
}
}
}
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,
epitaph,
body,
} => vec![Row::plain(
match epitaph {
Some(word) => format!("{sender} ({word})"),
None => sender.clone(),
},
body.clone(),
)],
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(),
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(),
)],
EntryKind::Raw => vec![Row::plain(
format!("{} (unparsed)", entry.name),
entry.raw.clone(),
)],
EntryKind::Unknown(word) => vec![Row::plain(
format!("{} ({word}, which this seat cannot read)", entry.name),
entry.raw.clone(),
)],
}
}
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(),
)),
Block::Unknown(word) => Some(Row::plain(
format!("{model_id} ({word}, which this seat cannot read)"),
String::new(),
)),
}
}
fn streaming(stream: &Stream) -> Vec<Row> {
live_rows(
stream.thinking.as_deref().unwrap_or_default(),
stream.text.as_deref().unwrap_or_default(),
)
}
fn half(speaker: &str, mark: &str, said: &str) -> Option<Row> {
(!said.is_empty()).then(|| {
Row::plain(
if mark.is_empty() {
speaker.to_owned()
} else {
format!("{speaker} ({mark})")
},
said.to_owned(),
)
})
}
fn live_rows(thinking: &str, text: &str) -> Vec<Row> {
[("thinking", thinking), ("", text)]
.into_iter()
.filter_map(|(mark, said)| half(LIVE, mark, said))
.collect()
}