tui: bring the transcript closer to display mode (#4848)

before
<img width="1161" height="836" alt="Screenshot 2025-10-06 at 3 06 52 PM"
src="https://github.com/user-attachments/assets/7622fd6b-9d37-402f-8651-61c2c55dcbc6"
/>

after
<img width="1161" height="858" alt="Screenshot 2025-10-06 at 3 07 02 PM"
src="https://github.com/user-attachments/assets/1498f327-1d1a-4630-951f-7ca371ab0139"
/>
This commit is contained in:
Jeremy Rose
2025-10-07 16:18:48 -07:00
committed by GitHub
parent 60f9e85c16
commit 0e5d72cc57
13 changed files with 233 additions and 187 deletions

View File

@@ -11,7 +11,6 @@ use crate::markdown::append_markdown;
use crate::render::line_utils::line_to_static;
use crate::render::line_utils::prefix_lines;
use crate::style::user_message_style;
use crate::terminal_palette::default_bg;
use crate::text_formatting::format_and_truncate_tool_result;
use crate::ui_consts::LIVE_PREFIX_COLS;
use crate::wrapping::RtOptions;
@@ -56,10 +55,6 @@ use unicode_width::UnicodeWidthStr;
pub(crate) trait HistoryCell: std::fmt::Debug + Send + Sync + Any {
fn display_lines(&self, width: u16) -> Vec<Line<'static>>;
fn transcript_lines(&self) -> Vec<Line<'static>> {
self.display_lines(u16::MAX)
}
fn desired_height(&self, width: u16) -> u16 {
Paragraph::new(Text::from(self.display_lines(width)))
.wrap(Wrap { trim: false })
@@ -68,6 +63,29 @@ pub(crate) trait HistoryCell: std::fmt::Debug + Send + Sync + Any {
.unwrap_or(0)
}
fn transcript_lines(&self, width: u16) -> Vec<Line<'static>> {
self.display_lines(width)
}
fn desired_transcript_height(&self, width: u16) -> u16 {
let lines = self.transcript_lines(width);
// Workaround for ratatui bug: if there's only one line and it's whitespace-only, ratatui gives 2 lines.
if let [line] = &lines[..]
&& line
.spans
.iter()
.all(|s| s.content.chars().all(char::is_whitespace))
{
return 1;
}
Paragraph::new(Text::from(lines))
.wrap(Wrap { trim: false })
.line_count(width)
.try_into()
.unwrap_or(0)
}
fn is_stream_continuation(&self) -> bool {
false
}
@@ -92,12 +110,10 @@ impl HistoryCell for UserHistoryCell {
fn display_lines(&self, width: u16) -> Vec<Line<'static>> {
let mut lines: Vec<Line<'static>> = Vec::new();
// Use ratatui-aware word wrapping and prefixing to avoid lifetime issues.
let wrap_width = width.saturating_sub(LIVE_PREFIX_COLS); // account for the ▌ prefix and trailing space
let wrap_width = width.saturating_sub(LIVE_PREFIX_COLS);
let style = user_message_style(default_bg());
let style = user_message_style();
// Use our ratatui wrapping helpers for correct styling and lifetimes.
let wrapped = word_wrap_lines(
&self
.message
@@ -113,13 +129,6 @@ impl HistoryCell for UserHistoryCell {
lines.push(Line::from("").style(style));
lines
}
fn transcript_lines(&self) -> Vec<Line<'static>> {
let mut lines: Vec<Line<'static>> = Vec::new();
lines.push("user".cyan().bold().into());
lines.extend(self.message.lines().map(|l| l.to_string().into()));
lines
}
}
#[derive(Debug)]
@@ -127,6 +136,7 @@ pub(crate) struct ReasoningSummaryCell {
_header: String,
content: String,
citation_context: MarkdownCitationContext,
transcript_only: bool,
}
impl ReasoningSummaryCell {
@@ -134,17 +144,17 @@ impl ReasoningSummaryCell {
header: String,
content: String,
citation_context: MarkdownCitationContext,
transcript_only: bool,
) -> Self {
Self {
_header: header,
content,
citation_context,
transcript_only,
}
}
}
impl HistoryCell for ReasoningSummaryCell {
fn display_lines(&self, width: u16) -> Vec<Line<'static>> {
fn lines(&self, width: u16) -> Vec<Line<'static>> {
let mut lines: Vec<Line<'static>> = Vec::new();
append_markdown(
&self.content,
@@ -152,7 +162,7 @@ impl HistoryCell for ReasoningSummaryCell {
&mut lines,
self.citation_context.clone(),
);
let summary_style = Style::default().add_modifier(Modifier::DIM | Modifier::ITALIC);
let summary_style = Style::default().dim().italic();
let summary_lines = lines
.into_iter()
.map(|mut line| {
@@ -172,19 +182,31 @@ impl HistoryCell for ReasoningSummaryCell {
.subsequent_indent(" ".into()),
)
}
}
fn transcript_lines(&self) -> Vec<Line<'static>> {
let mut out: Vec<Line<'static>> = Vec::new();
out.push("thinking".magenta().bold().into());
let mut lines = Vec::new();
append_markdown(
&self.content,
None,
&mut lines,
self.citation_context.clone(),
);
out.extend(lines);
out
impl HistoryCell for ReasoningSummaryCell {
fn display_lines(&self, width: u16) -> Vec<Line<'static>> {
if self.transcript_only {
Vec::new()
} else {
self.lines(width)
}
}
fn desired_height(&self, width: u16) -> u16 {
if self.transcript_only {
0
} else {
self.lines(width).len() as u16
}
}
fn transcript_lines(&self, width: u16) -> Vec<Line<'static>> {
self.lines(width)
}
fn desired_transcript_height(&self, width: u16) -> u16 {
self.lines(width).len() as u16
}
}
@@ -217,15 +239,6 @@ impl HistoryCell for AgentMessageCell {
)
}
fn transcript_lines(&self) -> Vec<Line<'static>> {
let mut out: Vec<Line<'static>> = Vec::new();
if self.is_first_line {
out.push("codex".magenta().bold().into());
}
out.extend(self.lines.clone());
out
}
fn is_stream_continuation(&self) -> bool {
!self.is_first_line
}
@@ -248,21 +261,6 @@ impl HistoryCell for PlainHistoryCell {
}
}
#[derive(Debug)]
pub(crate) struct TranscriptOnlyHistoryCell {
lines: Vec<Line<'static>>,
}
impl HistoryCell for TranscriptOnlyHistoryCell {
fn display_lines(&self, _width: u16) -> Vec<Line<'static>> {
Vec::new()
}
fn transcript_lines(&self) -> Vec<Line<'static>> {
self.lines.clone()
}
}
/// Cyan history cell line showing the current review status.
pub(crate) fn new_review_status_line(message: String) -> PlainHistoryCell {
PlainHistoryCell {
@@ -1050,16 +1048,6 @@ pub(crate) fn new_view_image_tool_call(path: PathBuf, cwd: &Path) -> PlainHistor
PlainHistoryCell { lines }
}
pub(crate) fn new_reasoning_block(
full_reasoning_buffer: String,
config: &Config,
) -> TranscriptOnlyHistoryCell {
let mut lines: Vec<Line<'static>> = Vec::new();
lines.push(Line::from("thinking".magenta().italic()));
append_markdown(&full_reasoning_buffer, None, &mut lines, config);
TranscriptOnlyHistoryCell { lines }
}
pub(crate) fn new_reasoning_summary_block(
full_reasoning_buffer: String,
config: &Config,
@@ -1085,12 +1073,18 @@ pub(crate) fn new_reasoning_summary_block(
header_buffer,
summary_buffer,
config.into(),
false,
));
}
}
}
}
Box::new(new_reasoning_block(full_reasoning_buffer, config))
Box::new(ReasoningSummaryCell::new(
"".to_string(),
full_reasoning_buffer,
config.into(),
true,
))
}
#[derive(Debug)]
@@ -1121,10 +1115,6 @@ impl HistoryCell for FinalMessageSeparator {
vec![Line::from_iter(["".repeat(width as usize).dim()])]
}
}
fn transcript_lines(&self) -> Vec<Line<'static>> {
vec![]
}
}
fn format_mcp_invocation<'a>(invocation: McpInvocation) -> Line<'a> {
@@ -1188,7 +1178,14 @@ mod tests {
}
fn render_transcript(cell: &dyn HistoryCell) -> Vec<String> {
render_lines(&cell.transcript_lines())
render_lines(&cell.transcript_lines(u16::MAX))
}
#[test]
fn empty_agent_message_cell_transcript() {
let cell = AgentMessageCell::new(vec![Line::default()], false);
assert_eq!(cell.transcript_lines(80), vec![Line::from(" ")]);
assert_eq!(cell.desired_transcript_height(80), 1);
}
#[test]
@@ -1883,10 +1880,7 @@ mod tests {
assert_eq!(rendered_display, vec!["• Detailed reasoning goes here."]);
let rendered_transcript = render_transcript(cell.as_ref());
assert_eq!(
rendered_transcript,
vec!["thinking", "Detailed reasoning goes here."]
);
assert_eq!(rendered_transcript, vec!["• Detailed reasoning goes here."]);
}
#[test]
@@ -1898,7 +1892,7 @@ mod tests {
new_reasoning_summary_block("Detailed reasoning goes here.".to_string(), &config);
let rendered = render_transcript(cell.as_ref());
assert_eq!(rendered, vec!["thinking", "Detailed reasoning goes here."]);
assert_eq!(rendered, vec!["Detailed reasoning goes here."]);
}
#[test]
@@ -1912,10 +1906,7 @@ mod tests {
);
let rendered = render_transcript(cell.as_ref());
assert_eq!(
rendered,
vec!["thinking", "**High level reasoning without closing"]
);
assert_eq!(rendered, vec!["• **High level reasoning without closing"]);
}
#[test]
@@ -1929,10 +1920,7 @@ mod tests {
);
let rendered = render_transcript(cell.as_ref());
assert_eq!(
rendered,
vec!["thinking", "High level reasoning without closing"]
);
assert_eq!(rendered, vec!["• High level reasoning without closing"]);
let cell = new_reasoning_summary_block(
"**High level reasoning without closing**\n\n ".to_string(),
@@ -1940,10 +1928,7 @@ mod tests {
);
let rendered = render_transcript(cell.as_ref());
assert_eq!(
rendered,
vec!["thinking", "High level reasoning without closing"]
);
assert_eq!(rendered, vec!["• High level reasoning without closing"]);
}
#[test]
@@ -1960,9 +1945,6 @@ mod tests {
assert_eq!(rendered_display, vec!["• We should fix the bug next."]);
let rendered_transcript = render_transcript(cell.as_ref());
assert_eq!(
rendered_transcript,
vec!["thinking", "We should fix the bug next."]
);
assert_eq!(rendered_transcript, vec!["• We should fix the bug next."]);
}
}