• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

dcdpr / jp / 21076945327

16 Jan 2026 06:37PM UTC coverage: 52.243% (+0.3%) from 51.91%
21076945327

push

github

web-flow
enhance(cli, config): Improve reasoning display and configuration merging (#382)

Update the CLI stream handler to improve the visual separation of
reasoning content by adding additional vertical spacing. This change
ensures that the transition from the model's reasoning to its final
response is more distinct in the terminal output.

The `jp_config` crate now correctly merges LLM provider aliases using a
nested map merge strategy, allowing user-defined aliases to be combined
with defaults rather than overwriting them.

Additionally, this commit:
- Improves the `UnknownId` error message in `jp_conversation` to include
the relevant conversation ID.
- Updates the project's Rust toolchain and components to a newer nightly
version.
- Refines clippy configuration to allow common technical identifiers.
- Cleans up unused tool listing logic in the query editor.

---------

Signed-off-by: Jean Mertz <git@jeanmertz.com>

2 of 8 new or added lines in 4 files covered. (25.0%)

2 existing lines in 2 files now uncovered.

10179 of 19484 relevant lines covered (52.24%)

119.95 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/crates/jp_cli/src/cmd/query/response_handler.rs
1
use std::{fs, path::PathBuf, sync::Arc, time::Duration};
2

3
use crossterm::style::{Color, Stylize as _};
4
use jp_config::style::{LinkStyle, StyleConfig};
5
use jp_printer::{PrintableExt as _, Printer};
6
use jp_term::{code, osc::hyperlink};
7
use termimad::FmtText;
8

9
use super::{Line, LineVariant, RenderMode};
10
use crate::Error;
11

12
#[derive(Debug)]
13
pub(super) struct ResponseHandler {
14
    /// How to render the response.
15
    pub render_mode: RenderMode,
16

17
    /// Whether to render tool call request/results.
18
    pub render_tool_calls: bool,
19

20
    /// The streamed, unprocessed lines received from the LLM.
21
    received: Vec<String>,
22

23
    pub printer: Arc<Printer>,
24

25
    /// The lines that have been parsed so far.
26
    ///
27
    /// If `should_stream` is `true`, these lines have been printed to the
28
    /// terminal. Otherwise they will be printed when the response handler is
29
    /// finished.
30
    pub parsed: Vec<String>,
31

32
    /// A temporary buffer of data received from the LLM.
33
    pub buffer: String,
34

35
    in_fenced_code_block: bool,
36
    // (language, code)
37
    code_buffer: (Option<String>, Vec<String>),
38
    code_line: usize,
39

40
    // The last index of the line that ends a code block.
41
    // (streamed, printed)
42
    last_fenced_code_block_end: (usize, usize),
43
}
44

45
impl ResponseHandler {
46
    pub fn new(render_mode: RenderMode, render_tool_calls: bool, printer: Arc<Printer>) -> Self {
×
47
        Self {
×
48
            render_mode,
×
49
            render_tool_calls,
×
50
            printer,
×
51
            received: vec![],
×
52
            parsed: vec![],
×
53
            buffer: String::new(),
×
54
            in_fenced_code_block: false,
×
55
            code_buffer: (None, vec![]),
×
56
            code_line: 0,
×
57
            last_fenced_code_block_end: (0, 0),
×
58
        }
×
59
    }
×
60

61
    pub fn drain(&mut self, style: &StyleConfig, raw: bool) -> Result<(), Error> {
×
62
        if self.buffer.is_empty() {
×
63
            return Ok(());
×
64
        }
×
65

66
        let line = Line::new(
×
67
            self.buffer.drain(..).collect(),
×
68
            self.in_fenced_code_block,
×
69
            raw,
×
70
        );
71

72
        self.handle_inner(line, style)
×
73
    }
×
74

75
    fn handle_inner(&mut self, line: Line, style: &StyleConfig) -> Result<(), Error> {
×
76
        let Line { content, variant } = line;
×
77
        self.received.push(content);
×
78

79
        let delay = match variant {
×
80
            LineVariant::Code => style.typewriter.code_delay.into(),
×
81
            LineVariant::Normal => style.typewriter.text_delay.into(),
×
82
            _ => Duration::ZERO,
×
83
        };
84

85
        let lines = self.handle_line(&variant, style)?;
×
86
        if !matches!(self.render_mode, RenderMode::Buffered) {
×
87
            self.printer.print(lines.join("\n").typewriter(delay));
×
88
        }
×
89

90
        self.parsed.extend(lines);
×
91

92
        Ok(())
×
93
    }
×
94

95
    #[expect(clippy::too_many_lines)]
96
    fn handle_line(
×
97
        &mut self,
×
98
        variant: &LineVariant,
×
99
        style: &StyleConfig,
×
100
    ) -> Result<Vec<String>, Error> {
×
101
        let Some(content) = self.received.last().map(String::as_str) else {
×
102
            return Ok(vec![]);
×
103
        };
104

105
        match variant {
×
106
            LineVariant::Raw => Ok(content.lines().map(str::to_owned).collect()),
×
107
            LineVariant::Code => {
108
                self.code_line += 1;
×
109
                self.code_buffer.1.push(content.to_owned());
×
110

111
                let mut buf = String::new();
×
112
                let config = code::Config {
×
113
                    language: self.code_buffer.0.clone(),
×
114
                    theme: style.code.color.then(|| style.code.theme.clone()),
×
115
                };
116

117
                if !code::format(content, &mut buf, &config)? {
×
118
                    let config = code::Config {
×
119
                        language: None,
×
120
                        theme: config.theme,
×
121
                    };
×
122

123
                    code::format(content, &mut buf, &config)?;
×
124
                }
×
125

126
                if style.code.line_numbers {
×
127
                    buf.insert_str(
×
128
                        0,
×
129
                        &format!("{:2} │ ", self.code_line)
×
130
                            .with(Color::AnsiValue(238))
×
131
                            .to_string(),
×
132
                    );
×
133
                }
×
134

135
                Ok(vec![buf])
×
136
            }
137
            LineVariant::FencedCodeBlockStart { language } => {
×
138
                self.code_buffer.0.clone_from(language);
×
139
                self.code_buffer.1.clear();
×
140
                self.code_line = 0;
×
141
                self.in_fenced_code_block = true;
×
142

143
                Ok(vec![content.with(Color::AnsiValue(238)).to_string()])
×
144
            }
145
            LineVariant::FencedCodeBlockEnd { indent } => {
×
146
                self.last_fenced_code_block_end = (self.received.len(), self.parsed.len() + 2);
×
147

148
                let path = self.persist_code_block()?;
×
149
                let mut links = vec![];
×
150

151
                match style.code.file_link {
×
152
                    LinkStyle::Off => {}
×
153
                    LinkStyle::Full => {
×
154
                        links.push(format!("{}see: {}", " ".repeat(*indent), path.display()));
×
155
                    }
×
156
                    LinkStyle::Osc8 => {
×
157
                        links.push(format!(
×
158
                            "{}[{}]",
×
159
                            " ".repeat(*indent),
×
160
                            hyperlink(
×
161
                                format!("file://{}", path.display()),
×
162
                                "open in editor".red().to_string()
×
163
                            )
×
164
                        ));
×
165
                    }
×
166
                }
167

168
                match style.code.copy_link {
×
169
                    LinkStyle::Off => {}
×
170
                    LinkStyle::Full => {
×
171
                        links.push(format!(
×
172
                            "{}copy: copy://{}",
×
173
                            " ".repeat(*indent),
×
174
                            path.display()
×
175
                        ));
×
176
                    }
×
177
                    LinkStyle::Osc8 => {
×
178
                        links.push(format!(
×
179
                            "{}[{}]",
×
180
                            " ".repeat(*indent),
×
181
                            hyperlink(
×
182
                                format!("copy://{}", path.display()),
×
183
                                "copy to clipboard".red().to_string()
×
184
                            )
×
185
                        ));
×
186
                    }
×
187
                }
188

189
                self.in_fenced_code_block = false;
×
190

191
                let mut lines = vec![content.with(Color::AnsiValue(238)).to_string()];
×
192
                if !links.is_empty() {
×
193
                    lines.push(links.join(" "));
×
194
                }
×
195

196
                Ok(lines)
×
197
            }
198
            LineVariant::Normal => {
199
                // We feed all the lines for markdown formatting, but only
200
                // print the last one, as the others are already printed.
201
                //
202
                // This helps the parser to use previous context to apply
203
                // the correct formatting to the current line.
204
                //
205
                // We only care about the lines after the last code block
206
                // end, because a) formatting context is reset after a code
207
                // block, and b) we dot not limit the line length of code, makes
208
                // it impossible to correctly find the non-printed lines based
209
                // on wrapped vs non-wrapped lines.
210
                let lines = self
×
211
                    .received
×
212
                    .iter()
×
213
                    .skip(self.last_fenced_code_block_end.0)
×
214
                    .cloned()
×
215
                    .collect::<Vec<_>>();
×
216

217
                // `termimad` removes empty lines at the start or end, but we
218
                // want to keep them as we will have more lines to print.
219
                let empty_lines_start_count = lines.iter().take_while(|s| s.is_empty()).count();
×
220
                let empty_lines_end_count = lines.iter().rev().take_while(|s| s.is_empty()).count();
×
221

222
                let options = comrak::Options {
×
NEW
223
                    render: comrak::options::Render {
×
NEW
224
                        r#unsafe: true,
×
225
                        prefer_fenced: true,
×
226
                        experimental_minimize_commonmark: true,
×
227
                        ..Default::default()
×
228
                    },
×
229
                    ..Default::default()
×
230
                };
×
231

232
                let formatted = comrak::markdown_to_commonmark(&lines.join("\n"), &options);
×
233

234
                let mut formatted =
×
235
                    FmtText::from(&termimad::MadSkin::default(), &formatted, Some(100)).to_string();
×
236

237
                for _ in 0..empty_lines_start_count {
×
238
                    formatted.insert(0, '\n');
×
239
                }
×
240

241
                // Only add an extra newline if we have more than one line,
242
                // otherwise a single empty line will be interpreted as both a
243
                // missing start and end newline.
244
                if lines.iter().any(|s| !s.is_empty()) {
×
245
                    for _ in 0..empty_lines_end_count {
×
246
                        formatted.push('\n');
×
247
                    }
×
248
                }
×
249

250
                let lines = formatted
×
251
                    .lines()
×
252
                    .skip(self.parsed.len() - self.last_fenced_code_block_end.1)
×
253
                    .map(ToOwned::to_owned)
×
254
                    .collect::<Vec<_>>();
×
255

256
                Ok(lines)
×
257
            }
258
        }
259
    }
×
260

261
    fn persist_code_block(&self) -> Result<PathBuf, Error> {
×
262
        let code = self.code_buffer.1.clone();
×
263
        let language = self.code_buffer.0.as_deref().unwrap_or("txt");
×
264
        let ext = match language {
×
265
            "c++" => "cpp",
×
266
            "javascript" => "js",
×
267
            "python" => "py",
×
268
            "ruby" => "rb",
×
269
            "rust" => "rs",
×
270
            "typescript" => "ts",
×
271
            lang => lang,
×
272
        };
273

274
        let millis = std::time::SystemTime::now()
×
275
            .duration_since(std::time::UNIX_EPOCH)
×
276
            .unwrap_or_default()
×
277
            .subsec_millis();
×
278
        let path = std::env::temp_dir().join(format!("code_{millis}.{ext}"));
×
279

280
        fs::write(&path, code.join("\n"))?;
×
281

282
        Ok(path)
×
283
    }
×
284
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc