ralph_workflow/json_parser/codex/parser.rs
1/// Codex event parser
2pub struct CodexParser {
3 colors: Colors,
4 verbosity: Verbosity,
5 /// Relative path to log file (if logging enabled)
6 log_path: Option<PathBuf>,
7 display_name: String,
8 /// Unified streaming session for state tracking
9 streaming_session: Rc<RefCell<StreamingSession>>,
10 /// Delta accumulator for reasoning content (which uses special display)
11 /// Note: We keep this for reasoning only, as it uses `DeltaDisplayFormatter`
12 reasoning_accumulator: Rc<RefCell<super::types::DeltaAccumulator>>,
13 /// Turn counter for generating synthetic turn IDs
14 turn_counter: Rc<RefCell<u64>>,
15 /// Terminal mode for output formatting
16 terminal_mode: RefCell<TerminalMode>,
17 /// Whether to show streaming quality metrics
18 show_streaming_metrics: bool,
19 /// Output printer for capturing or displaying output
20 printer: SharedPrinter,
21 /// Track last rendered content for append-only streaming pattern.
22 ///
23 /// Maps content key (e.g., "`text:agent_msg`", "thinking:reasoning") to the
24 /// last rendered sanitized content. Used to compute the new suffix for each
25 /// delta and emit only the incremental change, avoiding cursor movement and
26 /// wrapping issues.
27 last_rendered_content: RefCell<std::collections::HashMap<String, String>>,
28}
29
30impl CodexParser {
31 pub(crate) fn new(colors: Colors, verbosity: Verbosity) -> Self {
32 Self::with_printer(colors, verbosity, super::printer::shared_stdout())
33 }
34
35 /// Create a new `CodexParser` with a custom printer.
36 pub(crate) fn with_printer(
37 colors: Colors,
38 verbosity: Verbosity,
39 printer: SharedPrinter,
40 ) -> Self {
41 let verbose_warnings = matches!(verbosity, Verbosity::Debug);
42 let streaming_session = StreamingSession::new().with_verbose_warnings(verbose_warnings);
43
44 // Use the printer's is_terminal method to validate it's connected correctly
45 let _printer_is_terminal = printer.borrow().is_terminal();
46
47 Self {
48 colors,
49 verbosity,
50 log_path: None,
51 display_name: "Codex".to_string(),
52 streaming_session: Rc::new(RefCell::new(streaming_session)),
53 reasoning_accumulator: Rc::new(RefCell::new(super::types::DeltaAccumulator::new())),
54 turn_counter: Rc::new(RefCell::new(0)),
55 terminal_mode: RefCell::new(TerminalMode::detect()),
56 show_streaming_metrics: false,
57 printer,
58 last_rendered_content: RefCell::new(std::collections::HashMap::new()),
59 }
60 }
61
62 pub(crate) const fn with_show_streaming_metrics(mut self, show: bool) -> Self {
63 self.show_streaming_metrics = show;
64 self
65 }
66
67 pub(crate) fn with_display_name(mut self, display_name: &str) -> Self {
68 self.display_name = display_name.to_string();
69 self
70 }
71
72 /// Configure log file path.
73 ///
74 /// The workspace is passed to `parse_stream` separately.
75 pub(crate) fn with_log_file(mut self, path: &str) -> Self {
76 self.log_path = Some(PathBuf::from(path));
77 self
78 }
79
80 #[cfg(any(test, feature = "test-utils"))]
81 #[must_use]
82 pub fn with_terminal_mode(self, mode: TerminalMode) -> Self {
83 *self.terminal_mode.borrow_mut() = mode;
84 self
85 }
86
87 // ===== Test utilities (available with test-utils feature) =====
88
89 /// Create a new parser with a custom printer (for testing).
90 ///
91 /// This method is public when the `test-utils` feature is enabled,
92 /// allowing integration tests (in this repository) to create parsers with custom printers.
93 ///
94 /// Note: downstream crates should avoid relying on this API in production builds.
95 #[cfg(any(test, feature = "test-utils"))]
96 pub fn with_printer_for_test(
97 colors: Colors,
98 verbosity: Verbosity,
99 printer: SharedPrinter,
100 ) -> Self {
101 Self::with_printer(colors, verbosity, printer)
102 }
103
104 /// Set the log file path (for testing).
105 ///
106 /// This method is public when the `test-utils` feature is enabled,
107 /// allowing integration tests to configure log file path.
108 #[cfg(any(test, feature = "test-utils"))]
109 #[must_use]
110 pub fn with_log_file_for_test(mut self, path: &str) -> Self {
111 self.log_path = Some(PathBuf::from(path));
112 self
113 }
114
115 /// Set the display name (for testing).
116 ///
117 /// This method is public when the `test-utils` feature is enabled,
118 /// allowing integration tests to configure display name.
119 #[cfg(any(test, feature = "test-utils"))]
120 #[must_use]
121 pub fn with_display_name_for_test(mut self, display_name: &str) -> Self {
122 self.display_name = display_name.to_string();
123 self
124 }
125
126 /// Parse a stream of JSON events (for testing).
127 ///
128 /// This method is public when the `test-utils` feature is enabled,
129 /// allowing integration tests to invoke parsing.
130 ///
131 /// # Errors
132 ///
133 /// Returns an error if stream parsing or file operations fail.
134 #[cfg(any(test, feature = "test-utils"))]
135 pub fn parse_stream_for_test<R: std::io::BufRead>(
136 &self,
137 reader: R,
138 workspace: &dyn Workspace,
139 ) -> std::io::Result<()> {
140 self.parse_stream(reader, workspace)
141 }
142
143 /// Get a shared reference to the printer.
144 ///
145 /// This allows tests, monitoring, and other code to access the printer after parsing
146 /// to verify output content, check for duplicates, or capture output for analysis.
147 /// Only available with the `test-utils` feature.
148 #[cfg(any(test, feature = "test-utils"))]
149 pub fn printer(&self) -> SharedPrinter {
150 Rc::clone(&self.printer)
151 }
152
153 /// Get streaming quality metrics from the current session.
154 ///
155 /// This provides insight into the deduplication and streaming quality of the
156 /// parsing session. Only available with the `test-utils` feature.
157 #[cfg(any(test, feature = "test-utils"))]
158 pub fn streaming_metrics(&self) -> StreamingQualityMetrics {
159 self.streaming_session
160 .borrow()
161 .get_streaming_quality_metrics()
162 }
163
164 /// Convert output string to Option, returning None if empty.
165 #[inline]
166 fn optional_output(output: String) -> Option<String> {
167 if output.is_empty() {
168 None
169 } else {
170 Some(output)
171 }
172 }
173}