use super::{Row, RowClass, Tone};
use crate::theme::{Role, message_role};
use crate::transcript::{Block, Entry, EntryKind, Transcript};
mod build;
pub(crate) use build::key;
use build::{row, with_size};
const GAP_GLYPH: &str = "✂";
const COMPACTED_HOVER: &str = "These entries were removed: lernie's compactor squashed them out of the record and \
wrote a summary in their place. The counter proves which entries are gone; nothing \
on disk says which summary replaced which span, so every summary this conversation \
has opens from its first cut mark, in the order they were written.";
const NO_SUMMARY: &str = "(no summary on this mark)";
pub(super) fn push_entry(
transcript: &Transcript,
entry: &Entry,
speaker: &str,
out: &mut Vec<Row>,
) {
match &entry.kind {
EntryKind::Delivered {
sender,
epitaph,
body,
} if body.is_empty() => out.push(row(
key(&entry.name, 0),
delivered_prefix(sender, epitaph.as_ref()),
"(no message body)",
RowClass::Response,
Tone::Weak,
Some(message_role(sender, epitaph.is_some())),
)),
EntryKind::Delivered {
sender,
epitaph,
body,
} => out.push(row(
key(&entry.name, 0),
delivered_prefix(sender, epitaph.as_ref()),
body,
RowClass::Response,
Tone::Plain,
Some(message_role(sender, epitaph.is_some())),
)),
EntryKind::Model {
model_id, blocks, ..
} if blocks.is_empty() => out.push(Row {
hover: model_hover(model_id),
..row(
key(&entry.name, 0),
format!("{speaker}:"),
"(no content blocks)",
RowClass::Other,
Tone::Weak,
Some(Role::Model),
)
}),
EntryKind::Model {
model_id, blocks, ..
} => {
for (i, block) in blocks.iter().enumerate() {
out.push(block_row(
transcript,
key(&entry.name, i),
speaker,
model_id,
block,
));
}
}
EntryKind::ToolResult {
content, is_error, ..
} => {
let (glyph, _, phrase) = crate::theme::tool_result_badge(*is_error);
let tone = if *is_error { Tone::Bad } else { Tone::Good };
out.push(with_size(row(
key(&entry.name, 0),
format!("{glyph} {phrase}"),
content,
RowClass::Other,
tone,
None,
)));
}
EntryKind::Streaming { thinking, text } => {
push_streaming(&entry.name, thinking, text, out);
}
EntryKind::Compacted {
first,
last,
summary,
} => out.push(compacted_row(&entry.name, *first, *last, summary)),
EntryKind::Raw => out.push(row(
key(&entry.name, 0),
entry.name.clone(),
&String::from_utf8_lossy(&entry.raw),
RowClass::Other,
Tone::Weak,
None,
)),
}
}
fn block_row(
transcript: &Transcript,
key: String,
speaker: &str,
model_id: &str,
block: &Block,
) -> Row {
match block {
Block::Text(text) => Row {
hover: model_hover(model_id),
..row(
key,
format!("{speaker}:"),
text,
RowClass::Response,
Tone::Plain,
Some(Role::Model),
)
},
Block::Thinking(text) => row(
key,
"thinking:".to_string(),
text,
RowClass::Other,
Tone::Weak,
None,
),
Block::ToolUse {
id,
name,
input_summary,
} => {
let running = transcript.tool_in_progress(id);
let prefix = if running {
format!("⚙ {name} — running")
} else {
format!("⚙ {name}")
};
let tone = if running { Tone::InFlight } else { Tone::Plain };
row(key, prefix, input_summary, RowClass::Other, tone, None)
}
}
}
fn delivered_prefix(sender: &str, epitaph: Option<&crate::inboxview::Epitaph>) -> String {
match epitaph {
Some(epitaph) => format!("{sender} ended: {}", epitaph.label()),
None => format!("{sender}:"),
}
}
fn push_streaming(name: &str, thinking: &str, text: &str, out: &mut Vec<Row>) {
if !thinking.is_empty() {
out.push(row(
key(name, 0),
"thinking:".to_string(),
thinking,
RowClass::Other,
Tone::Live,
None,
));
}
if !text.is_empty() {
out.push(row(
key(name, 1),
"live:".to_string(),
text,
RowClass::Response,
Tone::Live,
Some(Role::Model),
));
}
}
fn compacted_row(name: &str, first: usize, last: usize, summary: &str) -> Row {
Row {
hover: COMPACTED_HOVER.to_string(),
..row(
key(name, 0),
compacted_prefix(first, last),
if summary.is_empty() {
NO_SUMMARY
} else {
summary
},
RowClass::Other,
Tone::Weak,
None,
)
}
}
fn compacted_prefix(first: usize, last: usize) -> String {
let count = last.saturating_sub(first) + 1;
let span = if first == last {
format!("{first:03}")
} else {
format!("{first:03}–{last:03}")
};
let entries = if count == 1 { "entry" } else { "entries" };
format!("{GAP_GLYPH} {count} {entries} compacted away — {span}")
}
fn model_hover(model_id: &str) -> String {
format!("ran on {model_id} — the model is config, not the speaker")
}