Skip to main content

trace_stream/
sink.rs

1// Copyright (c) 2026 Enzo Lombardi
2// SPDX-License-Identifier: MIT
3
4//! Routes [`crate::viz`] stream output into the markdown token renderer.
5
6use std::io::Write;
7
8use crate::render::TokenRenderer;
9use crate::viz::RenderSink;
10
11/// ANSI reset, emitted around error banners.
12const ANSI_RESET: &str = "\x1b[0m";
13
14/// Routes viz output into the markdown token renderer.
15#[derive(Debug)]
16pub struct TerminalSink<W: Write> {
17    renderer: TokenRenderer<W>,
18}
19
20impl<W: Write> TerminalSink<W> {
21    /// Wraps a token renderer as a [`RenderSink`].
22    pub fn new(renderer: TokenRenderer<W>) -> Self {
23        Self { renderer }
24    }
25
26    /// Borrows the underlying renderer, for callers that need to drive it
27    /// directly (fence handling, capture, `finish`).
28    pub fn renderer_mut(&mut self) -> &mut TokenRenderer<W> {
29        &mut self.renderer
30    }
31
32    /// Consumes the sink and returns the renderer.
33    pub fn into_renderer(self) -> TokenRenderer<W> {
34        self.renderer
35    }
36}
37
38impl<W: Write> RenderSink for TerminalSink<W> {
39    fn visible_text(&mut self, text: &str) {
40        self.renderer.set_in_think(false);
41        self.renderer.write(text);
42    }
43    fn think_text(&mut self, text: &str) {
44        self.renderer.set_in_think(true);
45        self.renderer.write(text);
46    }
47    fn tool_text(&mut self, text: &str) {
48        // Tool banners carry their own styling and must render verbatim; going
49        // through `write` would markdown-process them and eat `*`/`_`/backtick
50        // out of param values (e.g. `pattern=**/mod.rs`).
51        self.renderer.set_in_think(false);
52        self.renderer.plain(text);
53    }
54    fn error_text(&mut self, text: &str) {
55        self.renderer.set_in_think(false);
56        self.renderer.color("\x1b[1;31m");
57        self.renderer.plain(text);
58        self.renderer.color(ANSI_RESET);
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65    use crate::render::RenderOptions;
66
67    #[test]
68    fn tool_text_is_not_markdown_processed() {
69        let opts = RenderOptions {
70            use_color: false,
71            format_thinking: true,
72            format_markdown: true,
73        };
74        let mut sink = TerminalSink::new(TokenRenderer::new(Vec::new(), opts));
75        sink.tool_text("pattern=**/mod.rs");
76        let out = String::from_utf8(sink.into_renderer().into_sink()).unwrap();
77        assert!(
78            out.contains("**/mod.rs"),
79            "tool banners must render verbatim, got {out:?}"
80        );
81    }
82}