Skip to main content

vtcode_commons/ui_protocol/
tool_summary.rs

1//! Data exchanged by compact per-call tool summaries.
2
3use crate::tool_types::CompactStr;
4
5/// Session-local key for a complete tool-output capture.
6///
7/// This is deliberately a UI protocol identifier. It is not persisted in
8/// `ThreadEvent` data and must not be treated as a tool-call identity outside
9/// the live terminal session.
10pub type ToolOutputId = u64;
11
12/// Presentation metadata for a compact command activity row.
13///
14/// The complete command output is sent through `RecordToolOutput` separately.
15/// Keeping this small metadata object independent means compact rendering can
16/// replace a row without truncating, reordering, or otherwise changing the
17/// captured stdout, stderr, PTY, or spool-backed transcript.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct CompactActivityMetadata {
20    /// Identifier shared by all rows in one contiguous successful-command group.
21    pub group_id: u64,
22    /// Number of successful commands represented by the row.
23    pub command_count: usize,
24    /// The command text for a single-command row. Grouped rows leave this empty.
25    pub command: Option<CompactStr>,
26    /// Number of complete output lines hidden behind the review affordance.
27    pub hidden_line_count: usize,
28    /// Optional status or artifact text that remains visible in the row.
29    pub suffix: Option<CompactStr>,
30    /// First complete capture represented by the row, used as the review focus.
31    pub review_anchor: Option<ToolOutputId>,
32    /// All complete captures represented by the row, in render order.
33    ///
34    /// `review_anchor` remains the first capture for compatibility with the
35    /// click protocol; this list lets the UI re-anchor every member of a
36    /// grouped row without guessing from transcript text.
37    pub review_anchors: Vec<ToolOutputId>,
38}
39
40impl CompactActivityMetadata {
41    /// Return the compact row text without the UI-only review affordance.
42    pub fn display_text(&self) -> String {
43        let mut text = if self.command_count > 1 {
44            format!("• Ran {} commands", self.command_count)
45        } else {
46            format!("• Ran {}", self.command.as_deref().unwrap_or("command"))
47        };
48
49        if self.command_count == 1 && self.hidden_line_count > 0 {
50            text.push_str(&format!(" · … +{} lines", self.hidden_line_count));
51        }
52        if let Some(suffix) = self.suffix.as_deref().filter(|suffix| !suffix.is_empty()) {
53            text.push_str(" · ");
54            text.push_str(suffix);
55        }
56        text
57    }
58}
59
60/// UI-only payload that reopens a completed-edit diff as full-viewport review.
61///
62/// Not part of `ThreadEvent`. The transcript notice text is the activation
63/// target; `unified` is the retained preview used when before/after are absent.
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct DiffReviewAnchor {
66    /// Workspace-visible file path for the review header.
67    pub file_path: String,
68    /// Retained unified preview body.
69    pub unified: String,
70    /// Rows omitted from the compact transcript body, when known.
71    pub omitted_lines: u64,
72    /// Exact notice text that should activate this review.
73    pub notice: String,
74}
75
76/// Generic fallback labels that must never win path-based notice matching.
77#[must_use]
78pub fn is_generic_diff_review_path(path: &str) -> bool {
79    path.is_empty() || path == "diff" || path == "file" || path.starts_with("diff.")
80}
81
82/// Shared expand-notice copy for clipped completed-edit diff bodies.
83///
84/// `safety_capped` is retained so callers can distinguish a width-cap clip
85/// from vertical omission without changing the user-facing wording.
86#[must_use]
87pub fn diff_review_notice(file_path: &str, omitted_lines: u64, safety_capped: bool) -> String {
88    let _ = safety_capped;
89    if omitted_lines > 0 {
90        format!("… +{omitted_lines} lines — review full diff for {file_path}")
91    } else {
92        format!("… diff truncated — review full diff for {file_path}")
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::CompactActivityMetadata;
99
100    #[test]
101    fn compact_activity_display_includes_single_command_output_count() {
102        let activity = CompactActivityMetadata {
103            group_id: 1,
104            command_count: 1,
105            command: Some("cargo check".into()),
106            hidden_line_count: 3,
107            suffix: None,
108            review_anchor: Some(7),
109            review_anchors: vec![7],
110        };
111
112        assert_eq!(activity.display_text(), "• Ran cargo check · … +3 lines");
113    }
114
115    #[test]
116    fn compact_activity_display_collapses_grouped_commands() {
117        let activity = CompactActivityMetadata {
118            group_id: 2,
119            command_count: 4,
120            command: None,
121            hidden_line_count: 12,
122            suffix: Some("output retained".into()),
123            review_anchor: Some(9),
124            review_anchors: vec![9],
125        };
126
127        assert_eq!(activity.display_text(), "• Ran 4 commands · output retained");
128    }
129}
130
131#[derive(Debug, Clone, PartialEq, Eq)]
132pub struct CompactToolSummaryLine {
133    pub kind: CompactToolSummaryLineKind,
134    pub text: String,
135}
136
137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub enum CompactToolSummaryLineKind {
139    Info,
140    Detail,
141}