Skip to main content

vtcode_core/utils/ansi/
core.rs

1//! Renderer construction and display-state accessors.
2
3use super::*;
4
5use crate::config::ToolDisplayMode;
6use crate::config::loader::SyntaxHighlightingConfig;
7use crate::ui::tui::{InlineHandle, InlineMessageKind};
8use crate::utils::ansi_capabilities::AnsiCapabilities;
9use anstream::{AutoStream, ColorChoice};
10use std::io;
11use vtcode_commons::color_policy::{self, ColorOutputPolicySource};
12use vtcode_commons::ui_protocol::ToolOutputId;
13
14impl AnsiRenderer {
15    /// Create a new renderer for stdout
16    pub fn stdout() -> Self {
17        let mut capabilities = AnsiCapabilities::detect();
18        let policy = color_policy::current_color_output_policy();
19
20        if !policy.enabled {
21            capabilities.no_color = true;
22            capabilities.force_color = false;
23        } else if matches!(
24            policy.source,
25            ColorOutputPolicySource::CliColorAlways | ColorOutputPolicySource::ConfigOverride
26        ) {
27            capabilities.no_color = false;
28            capabilities.force_color = true;
29        }
30
31        let color = capabilities.supports_color();
32        let choice = if !color {
33            ColorChoice::Never
34        } else if matches!(
35            policy.source,
36            ColorOutputPolicySource::CliColorAlways | ColorOutputPolicySource::ConfigOverride
37        ) {
38            ColorChoice::Always
39        } else {
40            ColorChoice::Auto
41        };
42        Self {
43            writer: AutoStream::new(io::stdout(), choice),
44            buffer: String::with_capacity(1024),
45            color,
46            sink: None,
47            last_line_was_empty: false,
48            highlight_config: SyntaxHighlightingConfig::default(),
49            capabilities,
50            reasoning_visible: true,
51            screen_reader_mode: false,
52            show_diagnostics_in_transcript: false,
53            tool_display_mode: ToolDisplayMode::Compact,
54            diff_preview_mode: vtcode_commons::ui_protocol::DiffPreviewMode::Inline,
55            compact_command_group: None,
56            next_compact_group_id: 0,
57            pending_tool_output_anchor: None,
58            session_expand_anchor: None,
59            session_body: false,
60        }
61    }
62
63    /// Create a renderer that forwards output to the inline UI session handle
64    pub fn with_inline_ui(handle: InlineHandle, highlight_config: SyntaxHighlightingConfig) -> Self {
65        let mut renderer = Self::stdout();
66        renderer.highlight_config = highlight_config.clone();
67        renderer.sink = Some(InlineSink::new(handle, highlight_config));
68        renderer.last_line_was_empty = false;
69        renderer
70    }
71
72    /// Own a presentation-only authentication wait on the interactive terminal.
73    #[cfg(feature = "tui")]
74    pub fn program_status_wait(
75        &self,
76        kind: vtcode_commons::program_status::InteractionKind,
77    ) -> Option<vtcode_ui::tui::core_tui::types::ProgramStatusWaitGuard> {
78        self.sink.as_ref().map(|sink| sink.handle.program_status_wait(kind))
79    }
80
81    /// Override the syntax highlighting configuration.
82    pub fn set_highlight_config(&mut self, config: SyntaxHighlightingConfig) {
83        if let Some(sink) = &mut self.sink {
84            sink.set_highlight_config(config.clone());
85        }
86        self.highlight_config = config;
87    }
88
89    /// Associate the next summary line with a captured tool output block.
90    ///
91    /// This edge is UI-only. It is carried directly on the summary command so
92    /// repeated commands remain associated with their own captures even when
93    /// completions are interleaved.
94    pub fn set_next_tool_output_anchor(&mut self, id: ToolOutputId) {
95        self.pending_tool_output_anchor = Some(id);
96    }
97
98    /// Remember which capture a forthcoming exec-session expand notice should
99    /// open. Unlike [`Self::set_next_tool_output_anchor`] this survives body
100    /// lines and is consumed only when the notice is emitted.
101    pub fn set_session_expand_anchor(&mut self, id: ToolOutputId) {
102        self.session_expand_anchor = Some(id);
103    }
104
105    /// Take the pending session expand anchor, if any.
106    pub fn take_session_expand_anchor(&mut self) -> Option<ToolOutputId> {
107        self.session_expand_anchor.take()
108    }
109
110    /// Mark the upcoming stream body as an exec-session stdin/stdout capture
111    /// (dim + expand), even when the tool name alone would look like a launch.
112    pub fn set_session_body(&mut self, session_body: bool) {
113        self.session_body = session_body;
114    }
115
116    /// Whether the upcoming stream body should use the exec-session path.
117    pub fn session_body_active(&self) -> bool {
118        self.session_body
119    }
120
121    /// Check if the last line rendered was empty
122    pub fn was_previous_line_empty(&self) -> bool {
123        self.last_line_was_empty
124    }
125
126    pub(super) fn message_kind(style: MessageStyle) -> InlineMessageKind {
127        style.message_kind()
128    }
129
130    pub fn supports_streaming_markdown(&self) -> bool {
131        self.sink.is_some()
132    }
133
134    /// Determine whether the renderer is connected to the inline UI.
135    ///
136    /// Inline rendering uses the terminal session scrollback, so tool output should
137    /// avoid truncation that would otherwise be applied in compact CLI mode.
138    pub fn prefers_untruncated_output(&self) -> bool {
139        self.sink.is_some()
140    }
141
142    pub fn supports_inline_ui(&self) -> bool {
143        self.sink.is_some()
144    }
145
146    pub fn set_reasoning_visible(&mut self, visible: bool) {
147        self.reasoning_visible = visible;
148    }
149
150    pub fn reasoning_visible(&self) -> bool {
151        self.reasoning_visible
152    }
153
154    /// Whether rendered output is streamed into an inline TUI sink (as opposed to
155    /// written directly to a terminal writer).
156    pub fn writes_to_inline_sink(&self) -> bool {
157        self.sink.is_some()
158    }
159
160    pub fn set_screen_reader_mode(&mut self, enabled: bool) {
161        self.screen_reader_mode = enabled;
162    }
163
164    pub fn set_show_diagnostics_in_transcript(&mut self, enabled: bool) {
165        self.show_diagnostics_in_transcript = if cfg!(debug_assertions) { enabled } else { false };
166    }
167
168    pub fn set_tool_display_mode(&mut self, mode: ToolDisplayMode) {
169        self.flush_compact_command_group();
170        self.tool_display_mode = match mode {
171            ToolDisplayMode::Compact => ToolDisplayMode::Compact,
172            ToolDisplayMode::Expanded | ToolDisplayMode::Unknown => ToolDisplayMode::Expanded,
173        };
174    }
175
176    pub fn tool_display_mode(&self) -> ToolDisplayMode {
177        self.tool_display_mode
178    }
179
180    /// Whether per-call tool transitions render in compact (grouped) form.
181    ///
182    /// Single source of truth for the compact-vs-expanded decision so every
183    /// call site agrees; the stored mode is already normalized to
184    /// `Compact`/`Expanded` by [`Self::set_tool_display_mode`].
185    pub fn is_compact_display(&self) -> bool {
186        self.tool_display_mode == ToolDisplayMode::Compact
187    }
188
189    pub fn set_diff_preview_mode(&mut self, mode: vtcode_commons::ui_protocol::DiffPreviewMode) {
190        self.diff_preview_mode = mode;
191    }
192
193    /// Attach a completed-edit review payload so the TUI can expand on demand.
194    pub fn record_diff_review(&self, anchor: vtcode_commons::ui_protocol::DiffReviewAnchor) {
195        if let Some(sink) = &self.sink {
196            sink.handle.record_diff_review(anchor);
197        }
198    }
199
200    pub fn diff_preview_mode(&self) -> vtcode_commons::ui_protocol::DiffPreviewMode {
201        self.diff_preview_mode
202    }
203
204    pub fn toggle_tool_display_mode(&mut self) -> ToolDisplayMode {
205        self.flush_compact_command_group();
206        let next = match self.tool_display_mode {
207            ToolDisplayMode::Expanded | ToolDisplayMode::Unknown => ToolDisplayMode::Compact,
208            ToolDisplayMode::Compact => ToolDisplayMode::Expanded,
209        };
210        self.tool_display_mode = next;
211        next
212    }
213}