Skip to main content

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    pub fn with_terminal_mode(self, mode: TerminalMode) -> Self {
82        *self.terminal_mode.borrow_mut() = mode;
83        self
84    }
85
86    // ===== Test utilities (available with test-utils feature) =====
87
88    /// Create a new parser with a custom printer (for testing).
89    ///
90    /// This method is public when the `test-utils` feature is enabled,
91    /// allowing integration tests (in this repository) to create parsers with custom printers.
92    ///
93    /// Note: downstream crates should avoid relying on this API in production builds.
94    #[cfg(any(test, feature = "test-utils"))]
95    pub fn with_printer_for_test(
96        colors: Colors,
97        verbosity: Verbosity,
98        printer: SharedPrinter,
99    ) -> Self {
100        Self::with_printer(colors, verbosity, printer)
101    }
102
103    /// Set the log file path (for testing).
104    ///
105    /// This method is public when the `test-utils` feature is enabled,
106    /// allowing integration tests to configure log file path.
107    #[cfg(any(test, feature = "test-utils"))]
108    pub fn with_log_file_for_test(mut self, path: &str) -> Self {
109        self.log_path = Some(PathBuf::from(path));
110        self
111    }
112
113    /// Set the display name (for testing).
114    ///
115    /// This method is public when the `test-utils` feature is enabled,
116    /// allowing integration tests to configure display name.
117    #[cfg(any(test, feature = "test-utils"))]
118    pub fn with_display_name_for_test(mut self, display_name: &str) -> Self {
119        self.display_name = display_name.to_string();
120        self
121    }
122
123    /// Parse a stream of JSON events (for testing).
124    ///
125    /// This method is public when the `test-utils` feature is enabled,
126    /// allowing integration tests to invoke parsing.
127    #[cfg(any(test, feature = "test-utils"))]
128    pub fn parse_stream_for_test<R: std::io::BufRead>(
129        &self,
130        reader: R,
131        workspace: &dyn Workspace,
132    ) -> std::io::Result<()> {
133        self.parse_stream(reader, workspace)
134    }
135
136    /// Get a shared reference to the printer.
137    ///
138    /// This allows tests, monitoring, and other code to access the printer after parsing
139    /// to verify output content, check for duplicates, or capture output for analysis.
140    /// Only available with the `test-utils` feature.
141    #[cfg(any(test, feature = "test-utils"))]
142    pub fn printer(&self) -> SharedPrinter {
143        Rc::clone(&self.printer)
144    }
145
146    /// Get streaming quality metrics from the current session.
147    ///
148    /// This provides insight into the deduplication and streaming quality of the
149    /// parsing session. Only available with the `test-utils` feature.
150    #[cfg(any(test, feature = "test-utils"))]
151    pub fn streaming_metrics(&self) -> StreamingQualityMetrics {
152        self.streaming_session
153            .borrow()
154            .get_streaming_quality_metrics()
155    }
156
157    /// Convert output string to Option, returning None if empty.
158    #[inline]
159    fn optional_output(output: String) -> Option<String> {
160        if output.is_empty() {
161            None
162        } else {
163            Some(output)
164        }
165    }
166}