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}