Skip to content

Commit c87df3c

Browse files
committed
fix(gui): resolve overlapping lines in thinking and tool result blocks
- Preserve newlines in thinking/tool content by normalizing single \n to paragraph breaks before markdown parsing (markdown treats \n as space) - Add markdown_to_md_blocks_with_options() with preserve_newlines flag - Add line spacing (Theme.space-xs) between rich text lines in RichParagraph, UserBubble, HeadingBlock, ListItemBlock, BlockQuoteBlock - Increase block spacing in MarkdownContent to Theme.space-sm - Use preserve_newlines for thinking and tool result parsing in bridge, sessions, and build_tool_result_blocks - Clean up sven-tui markdown imports and formatting
1 parent 90cbe07 commit c87df3c

File tree

4 files changed

+51
-468
lines changed

4 files changed

+51
-468
lines changed

crates/sven-gui/src/bridge.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ use crate::{
3636
search::new_shared_search,
3737
sessions::{
3838
build_tool_result_blocks, chat_document_to_plain_messages, delete_session_from_disk,
39-
format_fields_json, markdown_to_md_blocks, markdown_to_plain_messages,
39+
format_fields_json, markdown_to_md_blocks_with_options, markdown_to_plain_messages,
4040
save_session_to_disk, strip_inline_markdown,
4141
},
4242
ChatMessage, CompletionEntry, MainWindow, MdBlock, PickerItem, QuestionItem, QueueItem,
@@ -1768,7 +1768,7 @@ impl SvenApp {
17681768
.find(|l| !l.trim().is_empty())
17691769
.unwrap_or("")
17701770
.to_string();
1771-
let sub_blocks = markdown_to_md_blocks(&content);
1771+
let sub_blocks = markdown_to_md_blocks_with_options(&content, true);
17721772
pm.lock().unwrap().push_back(PlainChatMessage {
17731773
message_type: "thinking",
17741774
content: stripped,

crates/sven-gui/src/sessions.rs

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -233,13 +233,53 @@ pub fn format_fields_json(fields: &[(String, String)]) -> String {
233233
.join("\n")
234234
}
235235

236+
/// Normalize newlines so that each single `\n` becomes a paragraph break in markdown.
237+
/// In standard markdown, single newlines are soft breaks (spaces); double newlines
238+
/// create paragraphs. This preserves the intended line structure for thinking and
239+
/// tool result content.
240+
fn normalize_newlines_for_paragraphs(text: &str) -> String {
241+
let mut result = String::with_capacity(text.len() + text.matches('\n').count());
242+
let mut prev_was_newline = false;
243+
for c in text.chars() {
244+
if c == '\n' {
245+
result.push('\n');
246+
if !prev_was_newline {
247+
result.push('\n');
248+
}
249+
prev_was_newline = true;
250+
} else {
251+
result.push(c);
252+
prev_was_newline = false;
253+
}
254+
}
255+
result
256+
}
257+
236258
/// Convert markdown text into a flat list of `PlainMdBlock`s suitable for use
237259
/// as sub-blocks inside `ThinkingBubble` or `ToolCallBubble`.
238260
///
239261
/// Applies the same pipeline as `markdown_to_plain_messages`: block parsing,
240262
/// inline run extraction, line wrapping, and syntax highlighting for code.
263+
///
264+
/// When `preserve_newlines` is true (for thinking/tool-result content), single
265+
/// newlines are normalized to paragraph breaks so multi-line content renders
266+
/// with proper line spacing instead of collapsing into one line.
241267
pub fn markdown_to_md_blocks(text: &str) -> Vec<PlainMdBlock> {
242-
let blocks = parse_markdown_blocks(text);
268+
markdown_to_md_blocks_with_options(text, false)
269+
}
270+
271+
/// Like `markdown_to_md_blocks` but with options. Use `preserve_newlines: true`
272+
/// for thinking and tool-result content to ensure proper line spacing.
273+
pub fn markdown_to_md_blocks_with_options(
274+
text: &str,
275+
preserve_newlines: bool,
276+
) -> Vec<PlainMdBlock> {
277+
let text = if preserve_newlines {
278+
normalize_newlines_for_paragraphs(text)
279+
} else {
280+
text.to_string()
281+
};
282+
let blocks = parse_markdown_blocks(&text);
243283
let mut result = Vec::with_capacity(blocks.len());
244284

245285
for block in blocks {
@@ -370,7 +410,7 @@ pub fn build_tool_result_blocks(result: &str, tool_name: Option<&str>) -> Vec<Pl
370410
blocks
371411
}
372412
} else {
373-
let blocks = markdown_to_md_blocks(result);
413+
let blocks = markdown_to_md_blocks_with_options(result, true);
374414
if blocks.is_empty() {
375415
vec![PlainMdBlock {
376416
kind: "paragraph",
@@ -602,7 +642,7 @@ pub fn chat_document_to_plain_messages(doc: &ChatDocument) -> Vec<PlainChatMessa
602642
out.extend(markdown_to_plain_messages(content, "assistant"));
603643
}
604644
TurnRecord::Thinking { content } => {
605-
let sub_blocks = markdown_to_md_blocks(content);
645+
let sub_blocks = markdown_to_md_blocks_with_options(content, true);
606646
let preview = content
607647
.lines()
608648
.find(|l| !l.trim().is_empty())

crates/sven-gui/ui/chat-pane.slint

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ component UserBubble inherits Rectangle {
216216

217217
// Rich rendering: pre-wrapped lines from Rust
218218
if rich-lines.length > 0: VerticalLayout {
219-
spacing: 0;
219+
spacing: Theme.space-xs;
220220
alignment: start;
221221
width: available-width - 2 * Theme.space-md;
222222

@@ -286,7 +286,7 @@ component RichParagraph inherits Rectangle {
286286

287287
// Rich rendering: pre-wrapped lines from Rust
288288
if rich-lines.length > 0: VerticalLayout {
289-
spacing: 0;
289+
spacing: Theme.space-xs;
290290
alignment: start;
291291
width: available-width - 2 * Theme.space-md;
292292

@@ -512,7 +512,7 @@ component HeadingBlock {
512512

513513
// Rich inline formatting when available
514514
if rich-lines.length > 0: VerticalLayout {
515-
spacing: 0;
515+
spacing: Theme.space-xs;
516516
alignment: start;
517517
width: available-width - 2 * Theme.space-md;
518518

@@ -575,7 +575,7 @@ component ListItemBlock {
575575

576576
// Rich pre-wrapped lines
577577
if rich-lines.length > 0: VerticalLayout {
578-
spacing: 0;
578+
spacing: Theme.space-xs;
579579
alignment: start;
580580
width: text-width;
581581

@@ -668,7 +668,7 @@ component BlockQuoteBlock {
668668

669669
// Rich lines when available
670670
if rich-lines.length > 0: VerticalLayout {
671-
spacing: 0;
671+
spacing: Theme.space-xs;
672672
alignment: start;
673673
width: inner-width;
674674

@@ -776,7 +776,7 @@ component MarkdownContent {
776776
preferred-height: content-layout.preferred-height;
777777

778778
content-layout := VerticalLayout {
779-
spacing: Theme.space-xs;
779+
spacing: Theme.space-sm;
780780
alignment: start;
781781
width: available-width;
782782

0 commit comments

Comments
 (0)