Skip to main content

vtcode_core/utils/ansi/
compact.rs

1//! Compact command-group rendering.
2
3use super::*;
4
5use crate::utils::transcript;
6
7use anyhow::Result;
8use vtcode_commons::formatting::{RAN_COMMAND_CONTINUATION_WIDTH, RAN_COMMAND_FIRST_WIDTH};
9use vtcode_commons::tool_types::CompactStr;
10use vtcode_commons::ui_protocol::{CompactActivityMetadata, ToolOutputId};
11
12impl AnsiRenderer {
13    ///
14    /// The grouping state is intentionally kept in the renderer rather than
15    /// the persisted execution event stream. Callers use this before a turn
16    /// ends or before rendering a non-command result.
17    pub fn flush_compact_command_group(&mut self) {
18        self.compact_command_group = None;
19    }
20
21    fn next_compact_activity(
22        &mut self,
23        command: String,
24        hidden_line_count: usize,
25        suffix: Option<String>,
26        review_anchor: Option<ToolOutputId>,
27    ) -> (CompactActivityMetadata, bool) {
28        if let Some(group) = &mut self.compact_command_group {
29            group.command_count = group.command_count.saturating_add(1);
30            group.command = None;
31            group.hidden_line_count = group.hidden_line_count.saturating_add(hidden_line_count);
32            group.suffix = None;
33            if let Some(review_anchor) = review_anchor {
34                if group.review_anchor.is_none() {
35                    group.review_anchor = Some(review_anchor);
36                }
37                if !group.review_anchors.contains(&review_anchor) {
38                    group.review_anchors.push(review_anchor);
39                }
40            }
41            return (group.clone(), true);
42        }
43
44        let group_id = self.next_compact_group_id;
45        self.next_compact_group_id = self.next_compact_group_id.wrapping_add(1);
46        let activity = CompactActivityMetadata {
47            group_id,
48            command_count: 1,
49            command: Some(command.into()),
50            hidden_line_count,
51            suffix: suffix.map(CompactStr::from),
52            review_anchor,
53            review_anchors: review_anchor.into_iter().collect(),
54        };
55        self.compact_command_group = Some(activity.clone());
56        (activity, false)
57    }
58
59    /// Render one compact successful command activity row, coalescing it with
60    /// the immediately preceding successful command row when possible.
61    pub fn render_compact_command_activity(
62        &mut self,
63        command: impl Into<String>,
64        hidden_line_count: usize,
65        suffix: Option<String>,
66        review_anchor: Option<ToolOutputId>,
67    ) -> Result<()> {
68        let command = command.into();
69        if !self.supports_inline_ui() {
70            // Plain-text fallback (no inline UI): wrap long commands with
71            // explicit `\` continuations and `│` gutters so a chained
72            // `git add … && git commit …` stays readable instead of terminal
73            // word-wrap without a continuation marker.
74            let wrapped = vtcode_commons::formatting::wrap_shell_command_with_continuations(
75                &command,
76                RAN_COMMAND_FIRST_WIDTH,
77                RAN_COMMAND_CONTINUATION_WIDTH,
78            );
79            if wrapped.is_empty() {
80                return self.line(MessageStyle::Info, "• Ran command");
81            }
82            self.line(MessageStyle::Info, &format!("• Ran {}", wrapped[0]))?;
83            for segment in wrapped.iter().skip(1) {
84                self.line(MessageStyle::Info, &format!("  │ {segment}"))?;
85            }
86            return Ok(());
87        }
88
89        let (activity, replaces_previous) =
90            self.next_compact_activity(command, hidden_line_count, suffix, review_anchor);
91        let text = activity.display_text();
92        if let Some(sink) = &self.sink {
93            if replaces_previous {
94                sink.handle.replace_compact_activity(activity);
95                transcript::replace_last(1, std::slice::from_ref(&text));
96            } else {
97                sink.handle.append_compact_activity(activity);
98                transcript::append(&text);
99            }
100        }
101        self.last_line_was_empty = false;
102        Ok(())
103    }
104
105    /// Collapse the live PTY preview into a compact activity row after the
106    /// command completes. The complete capture is recorded separately.
107    pub fn collapse_pty_block_to_compact_activity(
108        &mut self,
109        command: impl Into<String>,
110        hidden_line_count: usize,
111        suffix: Option<String>,
112        review_anchor: Option<ToolOutputId>,
113    ) -> Result<()> {
114        if !self.supports_inline_ui() {
115            return Ok(());
116        }
117
118        let (activity, replaces_previous) =
119            self.next_compact_activity(command.into(), hidden_line_count, suffix, review_anchor);
120        let text = activity.display_text();
121        if let Some(sink) = &self.sink {
122            sink.handle.collapse_pty_block(activity);
123            if replaces_previous {
124                transcript::replace_last(1, std::slice::from_ref(&text));
125            } else {
126                transcript::append(&text);
127            }
128        }
129        self.last_line_was_empty = false;
130        Ok(())
131    }
132}