Skip to content

Commit 316669b

Browse files
replicas-connector[bot]claudechitaliangreptile-apps[bot]
authored
fix: [ENG-3815] Preserve line breaks in markdown rendering for Gemini models (#5443)
* fix: preserve line breaks in markdown rendering for Gemini models Fixes line break formatting for Gemini model responses in the Chat view. Single newlines were being collapsed by the markdown renderer (Streamdown) following standard markdown behavior, causing text to appear run together. This change adds a `preserveLineBreaksForMarkdown` utility that converts single newlines to double newlines (paragraph breaks) for proper display, while preserving code blocks and existing double newlines. Affected components: - TextMessage.tsx - ChatOnlyView.tsx - AssistantToolCalls.tsx - Realtime.tsx Fixes ENG-3815 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Update web/lib/textHelpers.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --------- Co-authored-by: replicas-connector[bot] <replicas-connector[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Justin Torre <justintorre75@gmail.com> Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
1 parent 7de58e7 commit 316669b

File tree

5 files changed

+44
-4
lines changed

5 files changed

+44
-4
lines changed

web/components/templates/requests/components/ChatOnlyView.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
import { JsonRenderer } from "./chatComponent/single/JsonRenderer";
2222
import { Streamdown } from "streamdown";
2323
import type { BundledTheme } from "shiki";
24+
import { preserveLineBreaksForMarkdown } from "@/lib/textHelpers";
2425

2526
const shikiTheme: [BundledTheme, BundledTheme] = ["vitesse-light", "vitesse-dark"];
2627

@@ -269,7 +270,9 @@ function ChatBubble({
269270
: "text-purple-900 dark:text-purple-100"
270271
)}
271272
>
272-
<Streamdown shikiTheme={shikiTheme}>{displayContent}</Streamdown>
273+
<Streamdown shikiTheme={shikiTheme}>
274+
{preserveLineBreaksForMarkdown(displayContent)}
275+
</Streamdown>
273276
</div>
274277

275278
{/* Expand/collapse button for long messages */}

web/components/templates/requests/components/Realtime.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
} from "react-icons/pi";
2626
import { Streamdown } from "streamdown";
2727
import type { BundledTheme } from "shiki";
28+
import { preserveLineBreaksForMarkdown } from "@/lib/textHelpers";
2829

2930
const shikiTheme: [BundledTheme, BundledTheme] = ["vitesse-light", "vitesse-dark"];
3031

@@ -642,7 +643,9 @@ const SessionUpdate: React.FC<SessionUpdateProps> = ({ content }) => {
642643
}`}
643644
>
644645
<div className="prose prose-sm dark:prose-invert prose-headings:text-slate-50 prose-p:text-slate-200 prose-a:text-cyan-200 hover:prose-a:text-cyan-100 prose-blockquote:border-slate-400 prose-blockquote:text-slate-300 prose-strong:text-white prose-em:text-slate-300 prose-code:text-yellow-200 prose-pre:bg-slate-800/50 prose-pre:text-slate-200 prose-ol:text-slate-200 prose-ul:text-slate-200 prose-li:text-slate-200 [&_ol>li::marker]:text-white [&_ul>li::marker]:text-white">
645-
<Streamdown shikiTheme={shikiTheme}>{sessionData.instructions}</Streamdown>
646+
<Streamdown shikiTheme={shikiTheme}>
647+
{preserveLineBreaksForMarkdown(sessionData.instructions)}
648+
</Streamdown>
646649
</div>
647650
</div>
648651
</div>

web/components/templates/requests/components/chatComponent/single/AssistantToolCalls.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
} from "@helicone-package/llm-mapper/types";
99
import { Streamdown } from "streamdown";
1010
import type { BundledTheme } from "shiki";
11+
import { preserveLineBreaksForMarkdown } from "@/lib/textHelpers";
1112

1213
const shikiTheme: [BundledTheme, BundledTheme] = ["vitesse-light", "vitesse-dark"];
1314

@@ -65,7 +66,9 @@ export default function AssistantToolCalls({
6566
) : (
6667
content && (
6768
<div className="w-full whitespace-pre-wrap break-words p-2 text-xs">
68-
<Streamdown shikiTheme={shikiTheme}>{content}</Streamdown>
69+
<Streamdown shikiTheme={shikiTheme}>
70+
{preserveLineBreaksForMarkdown(content)}
71+
</Streamdown>
6972
</div>
7073
)
7174
)}

web/components/templates/requests/components/chatComponent/single/TextMessage.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { Skeleton } from "@/components/ui/skeleton";
88
import CitationAnnotations from "./CitationAnnotations";
99
import { Streamdown } from "streamdown";
1010
import type { BundledTheme } from "shiki";
11+
import { preserveLineBreaksForMarkdown } from "@/lib/textHelpers";
1112

1213
const shikiTheme: [BundledTheme, BundledTheme] = ["vitesse-light", "vitesse-dark"];
1314

@@ -122,7 +123,9 @@ export default function TextMessage({
122123
{displayContent ? (
123124
<>
124125
<div className="w-full whitespace-pre-wrap break-words text-sm">
125-
<Streamdown shikiTheme={shikiTheme}>{displayContent}</Streamdown>
126+
<Streamdown shikiTheme={shikiTheme}>
127+
{preserveLineBreaksForMarkdown(displayContent)}
128+
</Streamdown>
126129
</div>
127130
{annotations && annotations.length > 0 && (
128131
<CitationAnnotations

web/lib/textHelpers.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,31 @@
11
export function numberWithCommas(x: number) {
22
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
33
}
4+
5+
/**
6+
* Converts single newlines to double newlines for proper markdown rendering.
7+
* Preserves existing double newlines (paragraph breaks) and code blocks.
8+
*
9+
* In standard markdown, single newlines are treated as spaces.
10+
* This function ensures line breaks are preserved when rendering with markdown renderers.
11+
*
12+
* @param text - The text to process
13+
* @returns The text with single newlines converted to double newlines (except in code blocks)
14+
*/
15+
export function preserveLineBreaksForMarkdown(text: string): string {
16+
// Split by code blocks to preserve formatting inside them
17+
const codeBlockRegex = /(```[\s\S]*?```|`[^`\n]+`)/g;
18+
const parts = text.split(codeBlockRegex);
19+
20+
return parts
21+
.map((part, index) => {
22+
// Odd indices are code blocks, preserve them as-is
23+
if (index % 2 === 1) {
24+
return part;
25+
}
26+
// For non-code parts, convert single newlines to double newlines
27+
// but don't affect existing double newlines
28+
return part.replace(/\n(?!\n)/g, "\n\n");
29+
})
30+
.join("");
31+
}

0 commit comments

Comments
 (0)