Skip to content

Commit 375f4e1

Browse files
committed
fix: defer markdown code line number sync during streaming
Avoids syncing code line numbers while markdown is still streaming Keeps code block wrapping and line numbers stable after render Updates code block layout to support deferred gutter insertion
1 parent 9f0b061 commit 375f4e1

2 files changed

Lines changed: 25 additions & 10 deletions

File tree

packages/ui/src/components/chat/MarkdownRendererImpl.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -860,6 +860,7 @@ const mermaidColorsFromTheme = (theme: Theme) => ({
860860

861861
const useDecorateContext = (
862862
currentTheme: Theme,
863+
deferCodeLineNumberSync: boolean,
863864
onPreviewLoopback?: (url: string) => void,
864865
): DecorateContext => {
865866
const { t } = useI18n();
@@ -895,8 +896,8 @@ const useDecorateContext = (
895896
return {};
896897
}
897898
});
898-
return { labels, codeBlockLineWrap, onToggleCodeBlockLineWrap: toggleCodeBlockLineWrap, renderMermaid, onPreviewLoopback };
899-
}, [currentTheme, labels, codeBlockLineWrap, toggleCodeBlockLineWrap, onPreviewLoopback]);
899+
return { labels, codeBlockLineWrap, deferCodeLineNumberSync, onToggleCodeBlockLineWrap: toggleCodeBlockLineWrap, renderMermaid, onPreviewLoopback };
900+
}, [currentTheme, labels, codeBlockLineWrap, deferCodeLineNumberSync, toggleCodeBlockLineWrap, onPreviewLoopback]);
900901
};
901902

902903
// Runs the async render pipeline into the container and keeps a stable
@@ -984,7 +985,9 @@ const useMorphdomMarkdown = ({
984985
existing[i]?.remove();
985986
}
986987

987-
scheduleMarkdownCodeLineNumberSync(target);
988+
if (!ctx.deferCodeLineNumberSync) {
989+
scheduleMarkdownCodeLineNumberSync(target);
990+
}
988991
});
989992

990993
return () => {
@@ -1012,8 +1015,9 @@ const useMorphdomMarkdown = ({
10121015
const container = containerRef.current;
10131016
const target = container?.querySelector<HTMLElement>('[data-markdown-content]') ?? container;
10141017
if (!target) return;
1018+
if (ctx.deferCodeLineNumberSync) return;
10151019
applyMarkdownCodeBlockWrapState(target, ctx.codeBlockLineWrap, ctx.labels);
1016-
}, [containerRef, ctx.codeBlockLineWrap, ctx.labels]);
1020+
}, [containerRef, ctx.codeBlockLineWrap, ctx.deferCodeLineNumberSync, ctx.labels]);
10171021

10181022
React.useEffect(() => {
10191023
const container = containerRef.current;
@@ -1081,7 +1085,7 @@ const MarkdownRendererImpl: React.FC<MarkdownRendererProps> = ({
10811085
useExternalLinkInteractions({ containerRef });
10821086

10831087
const syntaxVars = React.useMemo(() => getMarkdownSyntaxVars(currentTheme), [currentTheme]);
1084-
const ctx = useDecorateContext(currentTheme, effectiveDirectory ? handlePreviewLoopback : undefined);
1088+
const ctx = useDecorateContext(currentTheme, live, effectiveDirectory ? handlePreviewLoopback : undefined);
10851089
const cacheKey = `markdown-${part?.id ? `part-${part.id}` : `message-${messageId}`}`;
10861090

10871091
useMorphdomMarkdown({ containerRef, text: pacedText, streaming: live, cacheKey, syntaxVars, ctx });
@@ -1164,7 +1168,7 @@ const SimpleMarkdownRendererImpl: React.FC<{
11641168
useExternalLinkInteractions({ containerRef, enabled: !disableLinkSafety });
11651169

11661170
const syntaxVars = React.useMemo(() => getMarkdownSyntaxVars(currentTheme), [currentTheme]);
1167-
const ctx = useDecorateContext(currentTheme);
1171+
const ctx = useDecorateContext(currentTheme, false);
11681172

11691173
useMorphdomMarkdown({
11701174
containerRef,

packages/ui/src/components/chat/markdown/decorate.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export type DecorateLabels = {
2424
export type DecorateContext = {
2525
labels: DecorateLabels;
2626
codeBlockLineWrap: boolean;
27+
deferCodeLineNumberSync?: boolean;
2728
onToggleCodeBlockLineWrap?: () => void;
2829
// Renders a mermaid block source to svg/ascii using current theme colors.
2930
renderMermaid: (source: string) => MermaidRender;
@@ -97,7 +98,7 @@ const createCodeLineNumbers = (pre: HTMLPreElement): HTMLDivElement => {
9798
const gutter = document.createElement('div');
9899
gutter.setAttribute('data-md-code-line-numbers', '');
99100
gutter.setAttribute('aria-hidden', 'true');
100-
gutter.className = 'select-none border-r border-border/50 pr-3 text-right text-muted-foreground/45';
101+
gutter.className = 'min-w-8 shrink-0 select-none border-r border-border/50 pr-3 text-right font-mono text-[13px] text-muted-foreground/45';
101102

102103
const text = pre.textContent ?? '';
103104
const lineCount = Math.max(1, text.endsWith('\n') ? text.split('\n').length - 1 : text.split('\n').length);
@@ -194,6 +195,12 @@ export const scheduleMarkdownCodeLineNumberSync = (root: HTMLElement): void => {
194195
export const applyMarkdownCodeBlockWrapState = (root: HTMLElement, enabled: boolean, labels: DecorateLabels): void => {
195196
const wrappers = root.querySelectorAll<HTMLElement>('[data-component="markdown-code"]');
196197
for (const wrapper of Array.from(wrappers)) {
198+
const body = wrapper.querySelector<HTMLElement>('[data-md-code-body]');
199+
const pre = wrapper.querySelector<HTMLPreElement>('pre');
200+
if (body && pre && !body.querySelector('[data-md-code-line-numbers]')) {
201+
body.classList.add('flex', 'gap-3');
202+
body.insertBefore(createCodeLineNumbers(pre), pre);
203+
}
197204
applyCodeBlockWrapState(wrapper, enabled, labels);
198205
}
199206
scheduleMarkdownCodeLineNumberSync(root);
@@ -258,18 +265,22 @@ const decorateCodeBlocks = (root: HTMLElement, ctx: DecorateContext): void => {
258265

259266
const body = document.createElement('div');
260267
body.setAttribute('data-md-code-body', '');
261-
body.className = 'flex gap-3 px-3 py-2.5 overflow-x-auto';
268+
body.className = ctx.deferCodeLineNumberSync ? 'px-3 py-2.5 overflow-x-auto' : 'flex gap-3 px-3 py-2.5 overflow-x-auto';
262269

263270
parent.replaceChild(wrapper, pre);
264271
pre.style.margin = '0';
265272
pre.style.background = 'transparent';
266273
pre.classList.add('min-w-0', 'w-full', 'flex-1');
267-
body.appendChild(createCodeLineNumbers(pre));
274+
if (!ctx.deferCodeLineNumberSync) {
275+
body.appendChild(createCodeLineNumbers(pre));
276+
}
268277
body.appendChild(pre);
269278
wrapper.appendChild(header);
270279
wrapper.appendChild(body);
271280
applyCodeBlockWrapState(wrapper, ctx.codeBlockLineWrap, ctx.labels);
272-
scheduleMarkdownCodeLineNumberSync(wrapper);
281+
if (!ctx.deferCodeLineNumberSync) {
282+
scheduleMarkdownCodeLineNumberSync(wrapper);
283+
}
273284
}
274285
};
275286

0 commit comments

Comments
 (0)