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:
@@ -134,8 +134,9 @@ impl App {
|
||||
/// Useful when switching sessions to ensure prior history remains visible.
|
||||
pub(crate) fn render_transcript_once(&mut self, tui: &mut tui::Tui) {
|
||||
if !self.transcript_cells.is_empty() {
|
||||
let width = tui.terminal.last_known_screen_size.width;
|
||||
for cell in &self.transcript_cells {
|
||||
tui.insert_history_lines(cell.transcript_lines());
|
||||
tui.insert_history_lines(cell.display_lines(width));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,6 @@ use crate::bottom_pane::prompt_args::prompt_has_numeric_placeholders;
|
||||
use crate::slash_command::SlashCommand;
|
||||
use crate::slash_command::built_in_slash_commands;
|
||||
use crate::style::user_message_style;
|
||||
use crate::terminal_palette;
|
||||
use codex_protocol::custom_prompts::CustomPrompt;
|
||||
use codex_protocol::custom_prompts::PROMPTS_CMD_PREFIX;
|
||||
|
||||
@@ -1533,7 +1532,7 @@ impl WidgetRef for ChatComposer {
|
||||
}
|
||||
}
|
||||
}
|
||||
let style = user_message_style(terminal_palette::default_bg());
|
||||
let style = user_message_style();
|
||||
let mut block_rect = composer_rect;
|
||||
block_rect.y = composer_rect.y.saturating_sub(1);
|
||||
block_rect.height = composer_rect.height.saturating_add(1);
|
||||
|
||||
@@ -20,7 +20,6 @@ use crate::render::RectExt as _;
|
||||
use crate::render::renderable::ColumnRenderable;
|
||||
use crate::render::renderable::Renderable;
|
||||
use crate::style::user_message_style;
|
||||
use crate::terminal_palette;
|
||||
|
||||
use super::CancellationEvent;
|
||||
use super::bottom_pane_view::BottomPaneView;
|
||||
@@ -350,7 +349,7 @@ impl Renderable for ListSelectionView {
|
||||
.areas(area);
|
||||
|
||||
Block::default()
|
||||
.style(user_message_style(terminal_palette::default_bg()))
|
||||
.style(user_message_style())
|
||||
.render(content_area, buf);
|
||||
|
||||
let header_height = self
|
||||
|
||||
@@ -120,6 +120,8 @@ where
|
||||
/// Last known position of the cursor. Used to find the new area when the viewport is inlined
|
||||
/// and the terminal resized.
|
||||
pub last_known_cursor_pos: Position,
|
||||
|
||||
use_custom_flush: bool,
|
||||
}
|
||||
|
||||
impl<B> Drop for Terminal<B>
|
||||
@@ -158,6 +160,7 @@ where
|
||||
viewport_area: Rect::new(0, cursor_pos.y, 0, 0),
|
||||
last_known_screen_size: screen_size,
|
||||
last_known_cursor_pos: cursor_pos,
|
||||
use_custom_flush: true,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -190,6 +193,8 @@ where
|
||||
pub fn flush(&mut self) -> io::Result<()> {
|
||||
let previous_buffer = &self.buffers[1 - self.current];
|
||||
let current_buffer = &self.buffers[self.current];
|
||||
|
||||
if self.use_custom_flush {
|
||||
let updates = diff_buffers(previous_buffer, current_buffer);
|
||||
if let Some(DrawCommand::Put { x, y, .. }) = updates
|
||||
.iter()
|
||||
@@ -199,6 +204,13 @@ where
|
||||
self.last_known_cursor_pos = Position { x: *x, y: *y };
|
||||
}
|
||||
draw(&mut self.backend, updates.into_iter())
|
||||
} else {
|
||||
let updates = previous_buffer.diff(current_buffer);
|
||||
if let Some((x, y, _)) = updates.last() {
|
||||
self.last_known_cursor_pos = Position { x: *x, y: *y };
|
||||
}
|
||||
self.backend.draw(updates.into_iter())
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the Terminal so that internal buffers match the requested area.
|
||||
@@ -408,11 +420,13 @@ fn diff_buffers<'a>(a: &'a Buffer, b: &'a Buffer) -> Vec<DrawCommand<'a>> {
|
||||
|
||||
let x = row
|
||||
.iter()
|
||||
.rposition(|cell| cell.symbol() != " " || cell.bg != bg)
|
||||
.rposition(|cell| {
|
||||
cell.symbol() != " " || cell.bg != bg || cell.modifier != Modifier::empty()
|
||||
})
|
||||
.unwrap_or(0);
|
||||
last_nonblank_column[y as usize] = x as u16;
|
||||
let (x_abs, y_abs) = a.pos_of(row_start + x + 1);
|
||||
if x < (a.area.width as usize).saturating_sub(1) {
|
||||
let (x_abs, y_abs) = a.pos_of(row_start + x + 1);
|
||||
updates.push(DrawCommand::ClearToEnd {
|
||||
x: x_abs,
|
||||
y: y_abs,
|
||||
|
||||
@@ -67,7 +67,7 @@ impl From<DiffSummary> for Box<dyn Renderable> {
|
||||
rows.push(Box::new(path));
|
||||
rows.push(Box::new(RtLine::from("")));
|
||||
rows.push(Box::new(InsetRenderable::new(
|
||||
Box::new(row.change),
|
||||
row.change,
|
||||
Insets::tlbr(0, 2, 0, 0),
|
||||
)));
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ use crate::render::line_utils::push_owned_lines;
|
||||
use crate::shimmer::shimmer_spans;
|
||||
use crate::wrapping::RtOptions;
|
||||
use crate::wrapping::word_wrap_line;
|
||||
use crate::wrapping::word_wrap_lines;
|
||||
use codex_ansi_escape::ansi_escape_line;
|
||||
use codex_common::elapsed::format_duration;
|
||||
use codex_protocol::parse_command::ParsedCommand;
|
||||
@@ -138,17 +139,25 @@ impl HistoryCell for ExecCell {
|
||||
}
|
||||
}
|
||||
|
||||
fn transcript_lines(&self) -> Vec<Line<'static>> {
|
||||
fn desired_transcript_height(&self, width: u16) -> u16 {
|
||||
self.transcript_lines(width).len() as u16
|
||||
}
|
||||
|
||||
fn transcript_lines(&self, width: u16) -> Vec<Line<'static>> {
|
||||
let mut lines: Vec<Line<'static>> = vec![];
|
||||
for call in self.iter_calls() {
|
||||
let cmd_display = strip_bash_lc_and_escape(&call.command);
|
||||
for (i, part) in cmd_display.lines().enumerate() {
|
||||
if i == 0 {
|
||||
lines.push(vec!["$ ".magenta(), part.to_string().into()].into());
|
||||
} else {
|
||||
lines.push(vec![" ".into(), part.to_string().into()].into());
|
||||
}
|
||||
for (i, call) in self.iter_calls().enumerate() {
|
||||
if i > 0 {
|
||||
lines.push("".into());
|
||||
}
|
||||
let script = strip_bash_lc_and_escape(&call.command);
|
||||
let highlighted_script = highlight_bash_to_lines(&script);
|
||||
let cmd_display = word_wrap_lines(
|
||||
&highlighted_script,
|
||||
RtOptions::new(width as usize)
|
||||
.initial_indent("$ ".magenta().into())
|
||||
.subsequent_indent(" ".into()),
|
||||
);
|
||||
lines.extend(cmd_display);
|
||||
|
||||
if let Some(output) = call.output.as_ref() {
|
||||
lines.extend(output.formatted_output.lines().map(ansi_escape_line));
|
||||
@@ -167,7 +176,6 @@ impl HistoryCell for ExecCell {
|
||||
result.push_span(format!(" • {duration}").dim());
|
||||
lines.push(result);
|
||||
}
|
||||
lines.push("".into());
|
||||
}
|
||||
lines
|
||||
}
|
||||
|
||||
@@ -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."]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,13 @@ use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::history_cell::HistoryCell;
|
||||
use crate::history_cell::UserHistoryCell;
|
||||
use crate::key_hint;
|
||||
use crate::key_hint::KeyBinding;
|
||||
use crate::render::Insets;
|
||||
use crate::render::renderable::InsetRenderable;
|
||||
use crate::render::renderable::Renderable;
|
||||
use crate::style::user_message_style;
|
||||
use crate::tui;
|
||||
use crate::tui::TuiEvent;
|
||||
use crossterm::event::KeyCode;
|
||||
@@ -13,6 +17,7 @@ use crossterm::event::KeyEvent;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::buffer::Cell;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::Style;
|
||||
use ratatui::style::Stylize;
|
||||
use ratatui::text::Line;
|
||||
use ratatui::text::Span;
|
||||
@@ -21,7 +26,6 @@ use ratatui::widgets::Clear;
|
||||
use ratatui::widgets::Paragraph;
|
||||
use ratatui::widgets::Widget;
|
||||
use ratatui::widgets::WidgetRef;
|
||||
use ratatui::widgets::Wrap;
|
||||
|
||||
pub(crate) enum Overlay {
|
||||
Transcript(TranscriptOverlay),
|
||||
@@ -317,29 +321,30 @@ impl PagerView {
|
||||
}
|
||||
}
|
||||
|
||||
struct CachedParagraph {
|
||||
paragraph: Paragraph<'static>,
|
||||
/// A renderable that caches its desired height.
|
||||
struct CachedRenderable {
|
||||
renderable: Box<dyn Renderable>,
|
||||
height: std::cell::Cell<Option<u16>>,
|
||||
last_width: std::cell::Cell<Option<u16>>,
|
||||
}
|
||||
|
||||
impl CachedParagraph {
|
||||
fn new(paragraph: Paragraph<'static>) -> Self {
|
||||
impl CachedRenderable {
|
||||
fn new(renderable: Box<dyn Renderable>) -> Self {
|
||||
Self {
|
||||
paragraph,
|
||||
renderable,
|
||||
height: std::cell::Cell::new(None),
|
||||
last_width: std::cell::Cell::new(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Renderable for CachedParagraph {
|
||||
impl Renderable for CachedRenderable {
|
||||
fn render(&self, area: Rect, buf: &mut Buffer) {
|
||||
self.paragraph.render_ref(area, buf);
|
||||
self.renderable.render(area, buf);
|
||||
}
|
||||
fn desired_height(&self, width: u16) -> u16 {
|
||||
if self.last_width.get() != Some(width) {
|
||||
let height = self.paragraph.line_count(width) as u16;
|
||||
let height = self.renderable.desired_height(width);
|
||||
self.height.set(Some(height));
|
||||
self.last_width.set(Some(width));
|
||||
}
|
||||
@@ -347,6 +352,23 @@ impl Renderable for CachedParagraph {
|
||||
}
|
||||
}
|
||||
|
||||
struct CellRenderable {
|
||||
cell: Arc<dyn HistoryCell>,
|
||||
style: Style,
|
||||
}
|
||||
|
||||
impl Renderable for CellRenderable {
|
||||
fn render(&self, area: Rect, buf: &mut Buffer) {
|
||||
let p =
|
||||
Paragraph::new(Text::from(self.cell.transcript_lines(area.width))).style(self.style);
|
||||
p.render(area, buf);
|
||||
}
|
||||
|
||||
fn desired_height(&self, width: u16) -> u16 {
|
||||
self.cell.desired_transcript_height(width)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct TranscriptOverlay {
|
||||
view: PagerView,
|
||||
cells: Vec<Arc<dyn HistoryCell>>,
|
||||
@@ -358,7 +380,7 @@ impl TranscriptOverlay {
|
||||
pub(crate) fn new(transcript_cells: Vec<Arc<dyn HistoryCell>>) -> Self {
|
||||
Self {
|
||||
view: PagerView::new(
|
||||
Self::render_cells_to_texts(&transcript_cells, None),
|
||||
Self::render_cells(&transcript_cells, None),
|
||||
"T R A N S C R I P T".to_string(),
|
||||
usize::MAX,
|
||||
),
|
||||
@@ -368,46 +390,46 @@ impl TranscriptOverlay {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_cells_to_texts(
|
||||
fn render_cells(
|
||||
cells: &[Arc<dyn HistoryCell>],
|
||||
highlight_cell: Option<usize>,
|
||||
) -> Vec<Box<dyn Renderable>> {
|
||||
let mut texts: Vec<Box<dyn Renderable>> = Vec::new();
|
||||
let mut first = true;
|
||||
for (idx, cell) in cells.iter().enumerate() {
|
||||
let mut lines: Vec<Line<'static>> = Vec::new();
|
||||
if !cell.is_stream_continuation() && !first {
|
||||
lines.push(Line::from(""));
|
||||
}
|
||||
let cell_lines = if Some(idx) == highlight_cell {
|
||||
cell.transcript_lines()
|
||||
.into_iter()
|
||||
.map(Stylize::reversed)
|
||||
.collect()
|
||||
cells
|
||||
.iter()
|
||||
.enumerate()
|
||||
.flat_map(|(i, c)| {
|
||||
let mut v: Vec<Box<dyn Renderable>> = Vec::new();
|
||||
let mut cell_renderable = if c.as_any().is::<UserHistoryCell>() {
|
||||
Box::new(CachedRenderable::new(Box::new(CellRenderable {
|
||||
cell: c.clone(),
|
||||
style: if highlight_cell == Some(i) {
|
||||
user_message_style().reversed()
|
||||
} else {
|
||||
cell.transcript_lines()
|
||||
user_message_style()
|
||||
},
|
||||
}))) as Box<dyn Renderable>
|
||||
} else {
|
||||
Box::new(CachedRenderable::new(Box::new(CellRenderable {
|
||||
cell: c.clone(),
|
||||
style: Style::default(),
|
||||
}))) as Box<dyn Renderable>
|
||||
};
|
||||
lines.extend(cell_lines);
|
||||
texts.push(Box::new(CachedParagraph::new(
|
||||
Paragraph::new(Text::from(lines)).wrap(Wrap { trim: false }),
|
||||
)));
|
||||
first = false;
|
||||
if !c.is_stream_continuation() && i > 0 {
|
||||
cell_renderable = Box::new(InsetRenderable::new(
|
||||
cell_renderable,
|
||||
Insets::tlbr(1, 0, 0, 0),
|
||||
));
|
||||
}
|
||||
texts
|
||||
v.push(cell_renderable);
|
||||
v
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn insert_cell(&mut self, cell: Arc<dyn HistoryCell>) {
|
||||
let follow_bottom = self.view.is_scrolled_to_bottom();
|
||||
// Append as a new Text chunk (with a separating blank if needed)
|
||||
let mut lines: Vec<Line<'static>> = Vec::new();
|
||||
if !cell.is_stream_continuation() && !self.cells.is_empty() {
|
||||
lines.push(Line::from(""));
|
||||
}
|
||||
lines.extend(cell.transcript_lines());
|
||||
self.view.renderables.push(Box::new(CachedParagraph::new(
|
||||
Paragraph::new(Text::from(lines)).wrap(Wrap { trim: false }),
|
||||
)));
|
||||
self.cells.push(cell);
|
||||
self.view.renderables = Self::render_cells(&self.cells, self.highlight_cell);
|
||||
if follow_bottom {
|
||||
self.view.scroll_offset = usize::MAX;
|
||||
}
|
||||
@@ -415,7 +437,7 @@ impl TranscriptOverlay {
|
||||
|
||||
pub(crate) fn set_highlight_cell(&mut self, cell: Option<usize>) {
|
||||
self.highlight_cell = cell;
|
||||
self.view.renderables = Self::render_cells_to_texts(&self.cells, self.highlight_cell);
|
||||
self.view.renderables = Self::render_cells(&self.cells, self.highlight_cell);
|
||||
if let Some(idx) = self.highlight_cell {
|
||||
self.view.scroll_chunk_into_view(idx);
|
||||
}
|
||||
@@ -475,8 +497,8 @@ pub(crate) struct StaticOverlay {
|
||||
impl StaticOverlay {
|
||||
pub(crate) fn with_title(lines: Vec<Line<'static>>, title: String) -> Self {
|
||||
Self::with_renderables(
|
||||
vec![Box::new(CachedParagraph::new(Paragraph::new(Text::from(
|
||||
lines,
|
||||
vec![Box::new(CachedRenderable::new(Box::new(Paragraph::new(
|
||||
Text::from(lines),
|
||||
))))],
|
||||
title,
|
||||
)
|
||||
@@ -585,7 +607,7 @@ mod tests {
|
||||
self.lines.clone()
|
||||
}
|
||||
|
||||
fn transcript_lines(&self) -> Vec<Line<'static>> {
|
||||
fn transcript_lines(&self, _width: u16) -> Vec<Line<'static>> {
|
||||
self.lines.clone()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,11 +38,13 @@ pub trait RectExt {
|
||||
|
||||
impl RectExt for Rect {
|
||||
fn inset(&self, insets: Insets) -> Rect {
|
||||
let horizontal = insets.left.saturating_add(insets.right);
|
||||
let vertical = insets.top.saturating_add(insets.bottom);
|
||||
Rect {
|
||||
x: self.x + insets.left,
|
||||
y: self.y + insets.top,
|
||||
width: self.width - insets.left - insets.right,
|
||||
height: self.height - insets.top - insets.bottom,
|
||||
x: self.x.saturating_add(insets.left),
|
||||
y: self.y.saturating_add(insets.top),
|
||||
width: self.width.saturating_sub(horizontal),
|
||||
height: self.height.saturating_sub(vertical),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::text::Line;
|
||||
@@ -12,6 +14,12 @@ pub trait Renderable {
|
||||
fn desired_height(&self, width: u16) -> u16;
|
||||
}
|
||||
|
||||
impl<R: Renderable + 'static> From<R> for Box<dyn Renderable> {
|
||||
fn from(value: R) -> Self {
|
||||
Box::new(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Renderable for () {
|
||||
fn render(&self, _area: Rect, _buf: &mut Buffer) {}
|
||||
fn desired_height(&self, _width: u16) -> u16 {
|
||||
@@ -71,6 +79,15 @@ impl<R: Renderable> Renderable for Option<R> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Renderable> Renderable for Arc<R> {
|
||||
fn render(&self, area: Rect, buf: &mut Buffer) {
|
||||
self.as_ref().render(area, buf);
|
||||
}
|
||||
fn desired_height(&self, width: u16) -> u16 {
|
||||
self.as_ref().desired_height(width)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ColumnRenderable {
|
||||
children: Vec<Box<dyn Renderable>>,
|
||||
}
|
||||
@@ -122,7 +139,10 @@ impl Renderable for InsetRenderable {
|
||||
}
|
||||
|
||||
impl InsetRenderable {
|
||||
pub fn new(child: Box<dyn Renderable>, insets: Insets) -> Self {
|
||||
Self { child, insets }
|
||||
pub fn new(child: impl Into<Box<dyn Renderable>>, insets: Insets) -> Self {
|
||||
Self {
|
||||
child: child.into(),
|
||||
insets,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@ pub(crate) fn log_inbound_app_event(event: &AppEvent) {
|
||||
"ts": now_ts(),
|
||||
"dir": "to_tui",
|
||||
"kind": "insert_history_cell",
|
||||
"lines": cell.transcript_lines().len(),
|
||||
"lines": cell.transcript_lines(u16::MAX).len(),
|
||||
});
|
||||
LOGGER.write_json_line(value);
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@ mod tests {
|
||||
use super::*;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config::ConfigOverrides;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
async fn test_config() -> Config {
|
||||
let overrides = ConfigOverrides {
|
||||
@@ -195,7 +196,7 @@ mod tests {
|
||||
for d in deltas.iter() {
|
||||
ctrl.push(d);
|
||||
while let (Some(cell), idle) = ctrl.on_commit_tick() {
|
||||
lines.extend(cell.transcript_lines());
|
||||
lines.extend(cell.transcript_lines(u16::MAX));
|
||||
if idle {
|
||||
break;
|
||||
}
|
||||
@@ -203,21 +204,14 @@ mod tests {
|
||||
}
|
||||
// Finalize and flush remaining lines now.
|
||||
if let Some(cell) = ctrl.finalize() {
|
||||
lines.extend(cell.transcript_lines());
|
||||
lines.extend(cell.transcript_lines(u16::MAX));
|
||||
}
|
||||
|
||||
let mut flat = lines;
|
||||
// Drop leading blank and header line if present.
|
||||
if !flat.is_empty() && lines_to_plain_strings(&[flat[0].clone()])[0].is_empty() {
|
||||
flat.remove(0);
|
||||
}
|
||||
if !flat.is_empty() {
|
||||
let s0 = lines_to_plain_strings(&[flat[0].clone()])[0].clone();
|
||||
if s0 == "codex" {
|
||||
flat.remove(0);
|
||||
}
|
||||
}
|
||||
let streamed = lines_to_plain_strings(&flat);
|
||||
let streamed: Vec<_> = lines_to_plain_strings(&lines)
|
||||
.into_iter()
|
||||
// skip • and 2-space indentation
|
||||
.map(|s| s.chars().skip(2).collect::<String>())
|
||||
.collect();
|
||||
|
||||
// Full render of the same source
|
||||
let source: String = deltas.iter().copied().collect();
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
use crate::color::blend;
|
||||
use crate::color::is_light;
|
||||
use crate::color::perceptual_distance;
|
||||
use crate::terminal_palette::default_bg;
|
||||
use crate::terminal_palette::terminal_palette;
|
||||
use ratatui::style::Color;
|
||||
use ratatui::style::Style;
|
||||
|
||||
pub fn user_message_style() -> Style {
|
||||
user_message_style_for(default_bg())
|
||||
}
|
||||
|
||||
/// Returns the style for a user-authored message using the provided terminal background.
|
||||
pub fn user_message_style(terminal_bg: Option<(u8, u8, u8)>) -> Style {
|
||||
pub fn user_message_style_for(terminal_bg: Option<(u8, u8, u8)>) -> Style {
|
||||
match terminal_bg {
|
||||
Some(bg) => Style::default().bg(user_message_bg(bg)),
|
||||
None => Style::default(),
|
||||
|
||||
Reference in New Issue
Block a user