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:
@@ -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."]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user