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