1use super::*;
4
5use crate::utils::transcript;
6
7use crate::ui::markdown::{MarkdownLine, RenderMarkdownOptions, render_markdown_to_lines_with_options};
8use crate::ui::theme;
9use crate::ui::tui::{InlineListItem, InlineListSearchConfig, InlineListSelection, InlineMessageKind};
10use crate::utils::ansi_capabilities::AnsiCapabilities;
11use anstyle::{Reset, Style};
12use anyhow::{Result, anyhow};
13use std::io::Write;
14use unicode_width::UnicodeWidthStr;
15
16impl AnsiRenderer {
17 pub fn set_table_max_width(&mut self, max_width: Option<usize>) {
18 if let Some(sink) = &mut self.sink {
19 sink.table_max_width = max_width;
20 sink.table_max_width_override = max_width;
21 }
22 }
23
24 pub(super) fn should_render_style(&self, style: MessageStyle) -> bool {
25 self.reasoning_visible || !matches!(style, MessageStyle::Reasoning | MessageStyle::ReasoningEmphasis)
26 }
27
28 fn is_diagnostic_error_style(style: MessageStyle) -> bool {
29 matches!(style, MessageStyle::Error | MessageStyle::ToolError)
30 }
31
32 fn log_transcript_error(text: &str, style: MessageStyle, suppressed_in_tui: bool) {
33 tracing::error!(
34 target: "vtcode_transcript",
35 style = ?style,
36 suppressed_in_tui,
37 message = %text,
38 "diagnostic error output"
39 );
40 }
41
42 pub(super) fn indent_for_style(&self, style: MessageStyle) -> &'static str {
43 if self.screen_reader_mode && matches!(style, MessageStyle::Reasoning | MessageStyle::ReasoningEmphasis) {
44 " [reasoning] "
45 } else {
46 style.indent()
47 }
48 }
49
50 pub fn capabilities(&self) -> &AnsiCapabilities {
52 &self.capabilities
53 }
54
55 pub fn diff_content_width(&self, style: MessageStyle) -> Option<usize> {
62 let indent_width = UnicodeWidthStr::width(self.indent_for_style(style));
63 let kind = Self::message_kind(style);
64 let frame_width = self
65 .sink
66 .as_ref()
67 .map(|sink| transcript_table_frame_width(kind, sink.handle.agent_label_frame_width()))
68 .unwrap_or_default();
69 let terminal_width = self
73 .sink
74 .as_ref()
75 .and_then(|sink| {
76 sink.table_max_width_override
77 .or_else(|| crossterm::terminal::size().ok().map(|(width, _)| usize::from(width)))
78 .or(sink.table_max_width)
79 })
80 .or_else(|| crossterm::terminal::size().ok().map(|(width, _)| usize::from(width)));
81
82 terminal_width.map(|width| width.saturating_sub(indent_width).saturating_sub(frame_width))
83 }
84
85 pub fn should_use_unicode_formatting(&self) -> bool {
87 self.capabilities.should_use_unicode_boxes()
88 }
89
90 pub fn supports_256_colors(&self) -> bool {
92 self.capabilities.supports_256_colors()
93 }
94
95 pub fn supports_true_color(&self) -> bool {
97 self.capabilities.supports_true_color()
98 }
99
100 pub fn should_use_unicode(&self) -> bool {
102 self.capabilities.unicode_support
103 }
104
105 pub fn show_list_modal(
106 &mut self,
107 title: &str,
108 lines: Vec<String>,
109 items: Vec<InlineListItem>,
110 selected: Option<InlineListSelection>,
111 search: Option<InlineListSearchConfig>,
112 ) {
113 if let Some(sink) = &self.sink {
114 sink.show_list_modal(title.into(), lines, items, selected, search);
115 }
116 }
117
118 pub fn show_list_modal_with_footer(
119 &mut self,
120 title: &str,
121 lines: Vec<String>,
122 items: Vec<InlineListItem>,
123 selected: Option<InlineListSelection>,
124 search: Option<InlineListSearchConfig>,
125 footer_hint: Option<String>,
126 ) {
127 self.show_list_modal_with_status(title, lines, items, selected, search, footer_hint, None);
128 }
129
130 pub fn show_list_modal_with_status(
132 &mut self,
133 title: &str,
134 lines: Vec<String>,
135 items: Vec<InlineListItem>,
136 selected: Option<InlineListSelection>,
137 search: Option<InlineListSearchConfig>,
138 footer_hint: Option<String>,
139 status: Option<vtcode_commons::ui_protocol::InlineStatus>,
140 ) {
141 if let Some(sink) = &self.sink {
142 sink.show_list_modal_with_status(title.into(), lines, items, selected, search, footer_hint, status);
143 }
144 }
145
146 pub fn show_secure_prompt_modal(&mut self, title: &str, lines: Vec<String>, prompt_label: String) {
147 if let Some(sink) = &self.sink {
148 sink.show_secure_prompt_modal(title.into(), lines, prompt_label);
149 }
150 }
151
152 pub fn close_modal(&mut self) {
153 if let Some(sink) = &self.sink {
154 sink.close_modal();
155 }
156 }
157
158 pub fn clear_screen(&mut self) {
159 self.flush_compact_command_group();
160 if let Some(sink) = &self.sink {
161 sink.handle.clear_screen();
162 }
163 }
164
165 pub fn push(&mut self, text: &str) {
167 self.buffer.push_str(text);
168 }
169
170 pub fn flush(&mut self, style: MessageStyle) -> Result<()> {
172 self.flush_compact_command_group();
173 if !self.should_render_style(style) {
174 self.buffer.clear();
175 return Ok(());
176 }
177 let indent = self.indent_for_style(style);
178 if let Some(sink) = &mut self.sink {
179 self.last_line_was_empty = self.buffer.is_empty() && indent.is_empty();
181 sink.write_line(style.style(), indent, &self.buffer, Self::message_kind(style))?;
182 self.buffer.clear();
183 return Ok(());
184 }
185 let style = style.style();
186 if self.color {
187 writeln!(self.writer, "{style}{}{Reset}", self.buffer)?;
188 } else {
189 writeln!(self.writer, "{}", self.buffer)?;
190 }
191 self.writer.flush()?;
192 transcript::append(&self.buffer);
193 self.last_line_was_empty = self.buffer.is_empty();
195 self.buffer.clear();
196 Ok(())
197 }
198
199 pub fn line(&mut self, style: MessageStyle, text: &str) -> Result<()> {
201 self.flush_compact_command_group();
202 if !self.should_render_style(style) {
203 return Ok(());
204 }
205 let suppress_transcript =
206 Self::is_diagnostic_error_style(style) && self.sink.is_some() && !self.show_diagnostics_in_transcript;
207 if Self::is_diagnostic_error_style(style) {
208 Self::log_transcript_error(text, style, self.sink.is_some());
209 }
210 if matches!(style, MessageStyle::Response | MessageStyle::Reasoning | MessageStyle::ReasoningEmphasis) {
211 return self.render_markdown(style, text);
212 }
213 if matches!(style, MessageStyle::Output | MessageStyle::ToolOutput) {
214 let stripped = crate::utils::ansi_parser::strip_ansi(text);
215 self.buffer.clear();
216 if looks_like_diff(&stripped) {
217 self.buffer.push_str("```diff\n");
218 } else {
219 self.buffer.push_str("```\n");
220 }
221 self.buffer.push_str(&stripped);
222 self.buffer.push_str("\n```");
223 let fenced = std::mem::take(&mut self.buffer);
224 return self.render_markdown(style, &fenced);
225 }
226 if matches!(style, MessageStyle::ToolDetail) {
227 if contains_markdown_fence(text) {
228 let stripped = crate::utils::ansi_parser::strip_ansi(text);
229 return self.render_markdown(style, &stripped);
230 }
231 if looks_like_diff(text) {
232 let stripped = crate::utils::ansi_parser::strip_ansi(text);
233 self.buffer.clear();
234 self.buffer.push_str("```diff\n");
235 self.buffer.push_str(&stripped);
236 self.buffer.push_str("\n```");
237 let fenced = std::mem::take(&mut self.buffer);
238 return self.render_markdown(style, &fenced);
239 }
240 }
241 let indent = style.indent();
242 let dont_split = matches!(style, MessageStyle::Tool | MessageStyle::ToolDetail);
243
244 if let Some(sink) = &mut self.sink {
245 sink.write_multiline_with_transcript(
246 style.style(),
247 indent,
248 text,
249 Self::message_kind(style),
250 !suppress_transcript,
251 None,
252 )?;
253 return Ok(());
254 }
255
256 if text.contains('\n') && !dont_split {
257 for line in text.lines() {
258 self.buffer.clear();
259 if !indent.is_empty() && !line.is_empty() {
260 self.buffer.push_str(indent);
261 }
262 self.buffer.push_str(line);
263 self.flush(style)?;
264 }
265 Ok(())
266 } else {
267 self.buffer.clear();
268 if !indent.is_empty() && !text.is_empty() {
269 self.buffer.push_str(indent);
270 }
271 self.buffer.push_str(text);
272 self.flush(style)
273 }
274 }
275
276 pub fn pty_continuation_line(&mut self, text: &str) -> Result<()> {
282 self.flush_compact_command_group();
283 let style = MessageStyle::ToolOutput;
284 let indent = style.indent();
285 let kind = Self::message_kind(style);
286 if let Some(sink) = &mut self.sink {
287 sink.write_multiline_with_transcript(style.style(), indent, text, kind, true, None)?;
288 return Ok(());
289 }
290 self.buffer.clear();
291 if !indent.is_empty() && !text.is_empty() {
292 self.buffer.push_str(indent);
293 }
294 self.buffer.push_str(text);
295 self.flush(style)
296 }
297
298 pub fn hyperlink_line(&mut self, style: MessageStyle, url: &str) -> Result<()> {
303 self.flush_compact_command_group();
304 if !self.should_render_style(style) {
305 return Ok(());
306 }
307 let indent = style.indent();
308 if let Some(sink) = &mut self.sink {
309 let linked = format!(
310 "{}{}{}",
311 vtcode_commons::ansi_codes::hyperlink_open(url),
312 url,
313 vtcode_commons::ansi_codes::hyperlink_close(),
314 );
315 sink.write_multiline_with_transcript(
316 style.style(),
317 indent,
318 &linked,
319 Self::message_kind(style),
320 true,
321 None,
322 )?;
323 self.last_line_was_empty = false;
324 return Ok(());
325 }
326 self.buffer.clear();
327 if !indent.is_empty() {
328 self.buffer.push_str(indent);
329 }
330 self.buffer.push_str(&vtcode_commons::ansi_codes::hyperlink_open(url));
331 self.buffer.push_str(url);
332 self.buffer.push_str(&vtcode_commons::ansi_codes::hyperlink_close());
333 let ansi_style = style.style();
334 if self.color {
335 writeln!(self.writer, "{ansi_style}{}{Reset}", self.buffer)?;
336 } else {
337 writeln!(self.writer, "{}", self.buffer)?;
338 }
339 self.writer.flush()?;
340 transcript::append(url);
341 self.last_line_was_empty = false;
342 self.buffer.clear();
343 Ok(())
344 }
345
346 pub fn append_paste_placeholder(&mut self, message: &str, line_count: usize) -> Result<()> {
348 self.flush_compact_command_group();
349 if let Some(sink) = &self.sink {
350 sink.handle
351 .append_pasted_message(InlineMessageKind::User, message.to_string(), line_count);
352 transcript::append(message);
353 self.last_line_was_empty = message.trim().is_empty();
354 return Ok(());
355 }
356 self.line(MessageStyle::User, message)
357 }
358
359 pub fn inline_with_style(&mut self, style: MessageStyle, text: &str) -> Result<()> {
361 self.flush_compact_command_group();
362 if !self.should_render_style(style) {
363 return Ok(());
364 }
365 if let Some(sink) = &mut self.sink {
366 sink.write_inline(style.style(), text, Self::message_kind(style));
367 return Ok(());
368 }
369 let ansi_style = style.style();
370 if self.color {
371 write!(self.writer, "{ansi_style}{text}{Reset}")?;
372 } else {
373 write!(self.writer, "{text}")?;
374 }
375 self.writer.flush()?;
376 Ok(())
377 }
378
379 pub fn line_with_style(&mut self, style: Style, text: &str) -> Result<()> {
381 self.line_with_override_style(MessageStyle::Info, style, text)
382 }
383
384 pub fn line_with_override_style(&mut self, fallback: MessageStyle, style: Style, text: &str) -> Result<()> {
386 self.flush_compact_command_group();
387 let tool_output_id = self.pending_tool_output_anchor.take();
388 if !self.should_render_style(fallback) {
389 return Ok(());
390 }
391 let suppress_transcript =
392 Self::is_diagnostic_error_style(fallback) && self.sink.is_some() && !self.show_diagnostics_in_transcript;
393 if Self::is_diagnostic_error_style(fallback) {
394 Self::log_transcript_error(text, fallback, self.sink.is_some());
395 }
396 let kind = Self::message_kind(fallback);
397 let indent = self.indent_for_style(fallback);
398 if let Some(sink) = &mut self.sink {
399 sink.write_multiline_with_transcript(style, indent, text, kind, !suppress_transcript, tool_output_id)?;
400 self.last_line_was_empty = text.trim().is_empty();
401 return Ok(());
402 }
403 let mut combined;
404 let display = if !indent.is_empty() && !text.is_empty() {
405 combined = String::with_capacity(indent.len() + text.len());
406 combined.push_str(indent);
407 combined.push_str(text);
408 combined.as_str()
409 } else {
410 text
411 };
412 if self.color {
413 writeln!(self.writer, "{style}{display}{Reset}")?;
414 } else {
415 writeln!(self.writer, "{display}")?;
416 }
417 self.writer.flush()?;
418 transcript::append(display);
419 self.last_line_was_empty = text.trim().is_empty();
420 Ok(())
421 }
422
423 pub fn line_if_not_empty(&mut self, style: MessageStyle) -> Result<()> {
425 if !self.was_previous_line_empty() {
426 self.line(style, "")
427 } else {
428 Ok(())
429 }
430 }
431
432 pub fn raw_line(&mut self, text: &str) -> Result<()> {
434 self.flush_compact_command_group();
435 writeln!(self.writer, "{text}")?;
436 self.writer.flush()?;
437 transcript::append(text);
438 Ok(())
439 }
440
441 pub fn render_token_delta(&mut self, delta: &str) -> Result<()> {
442 self.inline_with_style(MessageStyle::Response, delta)
443 }
444
445 pub fn stream_markdown_response(&mut self, text: &str, previous_line_count: usize) -> Result<usize> {
446 let text = crate::utils::ansi_parser::strip_ansi(text);
448 let text = &text;
449
450 let styles = theme::active_styles();
451 let style = MessageStyle::Response;
452 let base_style = style.style();
453 let indent = style.indent();
454 if let Some(sink) = &mut self.sink {
455 if sink.table_max_width_override.is_none()
457 && let Ok((w, _)) = crossterm::terminal::size()
458 {
459 sink.table_max_width = Some(w as usize);
460 }
461 let table_max_width = sink.table_content_width(Self::message_kind(style), indent);
462 let (prepared, plain_lines, last_empty) =
463 sink.prepare_markdown_lines_with_table_width(text, indent, base_style, true, true, table_max_width);
464 let line_count = prepared.len();
465 sink.replace_inline_lines(previous_line_count, prepared, &plain_lines, Self::message_kind(style));
466 self.last_line_was_empty = last_empty;
467 return Ok(line_count);
468 }
469
470 let highlight_cfg = if self.highlight_config.enabled {
471 Some(&self.highlight_config)
472 } else {
473 None
474 };
475 let mut lines = render_markdown_to_lines_with_options(
476 text,
477 base_style,
478 &styles,
479 highlight_cfg,
480 RenderMarkdownOptions::default(),
481 );
482 if lines.is_empty() {
483 lines.push(MarkdownLine::default());
484 }
485
486 Err(anyhow!("stream_markdown_response requires an inline sink"))
487 }
488}