vtcode_tui/core_tui/widgets/
transcript.rs1use ratatui::{
2 buffer::Buffer,
3 layout::Rect,
4 style::{Color, Style},
5 widgets::{Block, Clear, Paragraph, Widget},
6};
7
8use crate::config::constants::ui;
9use crate::ui::tui::session::terminal_capabilities;
10use crate::ui::tui::session::{Session, TranscriptLine};
11
12pub struct TranscriptWidget<'a> {
28 session: &'a mut Session,
29 show_scrollbar: bool,
30 custom_style: Option<Style>,
31}
32
33impl<'a> TranscriptWidget<'a> {
34 pub fn new(session: &'a mut Session) -> Self {
36 Self {
37 session,
38 show_scrollbar: false,
39 custom_style: None,
40 }
41 }
42
43 #[must_use]
45 pub fn show_scrollbar(mut self, show: bool) -> Self {
46 self.show_scrollbar = show;
47 self
48 }
49
50 #[must_use]
52 pub fn custom_style(mut self, style: Style) -> Self {
53 self.custom_style = Some(style);
54 self
55 }
56}
57
58impl<'a> Widget for TranscriptWidget<'a> {
59 fn render(self, area: Rect, buf: &mut Buffer) {
60 if area.height == 0 || area.width == 0 {
61 self.session.set_transcript_area(None);
62 self.session.clear_transcript_file_link_targets();
63 return;
64 }
65
66 let block = Block::new()
67 .border_type(terminal_capabilities::get_border_type())
68 .style(self.session.styles.default_style())
69 .border_style(self.session.styles.border_style());
70
71 let inner = block.inner(area);
72 block.render(area, buf);
73
74 if inner.height == 0 || inner.width == 0 {
75 self.session.set_transcript_area(None);
76 self.session.clear_transcript_file_link_targets();
77 return;
78 }
79 self.session.set_transcript_area(Some(inner));
80
81 let effective_height = inner.height.min(ui::TUI_MAX_VIEWPORT_HEIGHT);
84 let effective_width = inner.width.min(ui::TUI_MAX_VIEWPORT_WIDTH);
85
86 self.session.apply_transcript_rows(effective_height);
87
88 let content_width = effective_width;
89 if content_width == 0 {
90 self.session.clear_transcript_file_link_targets();
91 return;
92 }
93 self.session.apply_transcript_width(content_width);
94
95 let viewport_rows = effective_height as usize;
96 let padding = usize::from(ui::INLINE_TRANSCRIPT_BOTTOM_PADDING);
97 let effective_padding = padding.min(viewport_rows.saturating_sub(1));
98 let total_rows = self.session.total_transcript_rows(content_width) + effective_padding;
99 let (top_offset, _clamped_total_rows) = self
100 .session
101 .prepare_transcript_scroll(total_rows, viewport_rows);
102 let vertical_offset = top_offset.min(self.session.scroll_manager.max_offset());
103 self.session.transcript_view_top = vertical_offset;
104
105 let visible_start = vertical_offset;
106 let scroll_area = inner;
107
108 let cached_lines = self.session.collect_transcript_window_cached(
110 content_width,
111 visible_start,
112 viewport_rows,
113 );
114
115 let fill_count = viewport_rows.saturating_sub(cached_lines.len());
117 let needs_mutation = fill_count > 0 || !self.session.queued_inputs.is_empty();
118
119 let visible_lines = if needs_mutation {
122 let mut lines = cached_lines.to_vec();
124 if fill_count > 0 {
125 let target_len = lines.len() + fill_count;
126 lines.resize_with(target_len, TranscriptLine::default);
127 }
128 self.session.overlay_queue_lines(&mut lines, content_width);
129 lines
130 } else {
131 cached_lines.to_vec()
133 };
134 let visible_lines = self
135 .session
136 .decorate_visible_transcript_links(visible_lines, scroll_area);
137
138 if self.session.transcript_content_changed {
141 Clear.render(scroll_area, buf);
142 self.session.transcript_content_changed = false;
143 }
144 let paragraph =
145 Paragraph::new(visible_lines.clone()).style(self.session.styles.default_style());
146 paragraph.render(scroll_area, buf);
147 apply_full_width_line_backgrounds(buf, scroll_area, &visible_lines);
148 }
149}
150
151fn line_background(line: &ratatui::text::Line<'_>) -> Option<Color> {
152 line.spans.iter().find_map(|span| span.style.bg)
153}
154
155fn apply_full_width_line_backgrounds(
156 buf: &mut Buffer,
157 area: Rect,
158 lines: &[ratatui::text::Line<'_>],
159) {
160 if area.width == 0 || area.height == 0 {
161 return;
162 }
163
164 let max_rows = usize::from(area.height).min(lines.len());
165 for (row, line) in lines.iter().take(max_rows).enumerate() {
166 if let Some(bg) = line_background(line) {
167 let row_rect = Rect::new(area.x, area.y + row as u16, area.width, 1);
168 buf.set_style(row_rect, Style::default().bg(bg));
169 }
170 }
171}