Skip to main content

ralph_workflow/json_parser/delta_display/
formatter.rs

1// Delta display formatter.
2//
3// Contains the DeltaDisplayFormatter for consistent styling across parsers.
4
5/// Delta display formatter
6///
7/// Formats delta content for user display with consistent styling across all parsers.
8pub struct DeltaDisplayFormatter {
9    /// Whether to mark partial content visually
10    mark_partial: bool,
11}
12
13impl DeltaDisplayFormatter {
14    /// Create a new formatter with default settings
15    #[must_use] 
16    pub const fn new() -> Self {
17        Self { mark_partial: true }
18    }
19
20    /// Format thinking content specifically.
21    ///
22    /// Thinking content has special formatting to distinguish it from regular text.
23    ///
24    /// # Terminal Mode Behavior
25    ///
26    /// - `TerminalMode::Full` / `TerminalMode::Basic`: May include ANSI colors.
27    /// - `TerminalMode::None`: MUST be plain text (no ANSI sequences).
28    #[must_use]
29    pub fn format_thinking(
30        &self,
31        content: &str,
32        prefix: &str,
33        colors: Colors,
34        terminal_mode: crate::json_parser::terminal::TerminalMode,
35    ) -> String {
36        use crate::json_parser::terminal::TerminalMode;
37        match terminal_mode {
38            TerminalMode::None => format!("[{prefix}] Thinking: {content}\n"),
39            TerminalMode::Full | TerminalMode::Basic => {
40                format_thinking_with_colors(content, prefix, colors, self.mark_partial)
41            }
42        }
43    }
44
45    /// Format tool input specifically
46    ///
47    /// Tool input is shown with appropriate styling.
48    ///
49    /// # Terminal Mode Behavior
50    ///
51    /// - **Full mode (TTY):** Renders the full `[prefix]   └─ content` pattern
52    ///   for each delta, providing real-time feedback with clarity about which
53    ///   agent's tool is being invoked.
54    ///
55    /// - **Basic/None modes (non-TTY):** Suppresses per-delta output to prevent
56    ///   repeated prefixed lines in logs and CI output. Tool input is accumulated
57    ///   and rendered ONCE at completion boundaries (`message_stop`).
58    ///
59    /// # CCS Spam Prevention (Bug Fix)
60    ///
61    /// This implementation prevents repeated "[ccs/glm]" and "[ccs/codex]" prefixed
62    /// lines for tool input deltas in non-TTY modes. The fix is validated with
63    /// comprehensive regression tests that simulate real-world streaming scenarios.
64    ///
65    /// See comprehensive regression tests:
66    /// - `tests/integration_tests/ccs_delta_spam_systematic_reproduction.rs` (NEW: systematic reproduction test)
67    /// - `tests/integration_tests/ccs_all_delta_types_spam_reproduction.rs` (comprehensive coverage)
68    /// - `tests/integration_tests/ccs_nuclear_spam_test.rs` (tool input with 500+ deltas)
69    /// - `tests/integration_tests/ccs_streaming_spam_all_deltas.rs` (all delta types including tool input)
70    ///
71    /// # Future Enhancement
72    ///
73    /// For streaming tool inputs with multiple deltas in Full mode, consider suppressing
74    /// the `[prefix]` on continuation lines to reduce visual noise:
75    /// - First tool input line: `[prefix] Tool: name`
76    /// - Continuation: `           └─ more input` (aligned, no prefix)
77    ///
78    /// This would require tracking whether the prefix has been displayed
79    /// for the current tool block, likely via the streaming session state.
80    #[must_use]
81    pub fn format_tool_input(
82        &self,
83        content: &str,
84        prefix: &str,
85        colors: Colors,
86        terminal_mode: crate::json_parser::terminal::TerminalMode,
87    ) -> String {
88        use crate::json_parser::terminal::TerminalMode;
89        match terminal_mode {
90            // In Full mode, render tool input deltas as they arrive for real-time feedback
91            TerminalMode::Full => format_tool_input_full(content, prefix, colors, self.mark_partial),
92            // SUPPRESS per-delta tool input in non-TTY modes.
93            // Tool input will be rendered ONCE at tool completion or message_stop.
94            TerminalMode::Basic | TerminalMode::None => String::new(),
95        }
96    }
97}
98
99fn format_thinking_with_colors(
100    content: &str,
101    prefix: &str,
102    colors: Colors,
103    mark_partial: bool,
104) -> String {
105    if mark_partial {
106        format!(
107            "{}[{}]{} {}Thinking: {}{}{}\n",
108            colors.dim(),
109            prefix,
110            colors.reset(),
111            colors.dim(),
112            colors.cyan(),
113            content,
114            colors.reset()
115        )
116    } else {
117        format!(
118            "{}[{}]{} {}Thinking: {}{}{}\n",
119            colors.dim(),
120            prefix,
121            colors.reset(),
122            colors.cyan(),
123            colors.reset(),
124            content,
125            colors.reset()
126        )
127    }
128}
129
130fn format_tool_input_full(
131    content: &str,
132    prefix: &str,
133    colors: Colors,
134    mark_partial: bool,
135) -> String {
136    if mark_partial {
137        format!(
138            "{}[{}]{} {}  └─ {}{}{}\n",
139            colors.dim(),
140            prefix,
141            colors.reset(),
142            colors.dim(),
143            colors.reset(),
144            content,
145            colors.reset()
146        )
147    } else {
148        format!(
149            "{}[{}]{} {}  └─ {}{}\n",
150            colors.dim(),
151            prefix,
152            colors.reset(),
153            colors.reset(),
154            content,
155            colors.reset()
156        )
157    }
158}
159
160impl Default for DeltaDisplayFormatter {
161    fn default() -> Self {
162        Self::new()
163    }
164}