A multi-MB message (logged bundle, huge tool dump) froze the renderer before any paint: Streamdown runs `preprocess` + `marked` lex over the whole string synchronously in a useMemo, an uninterruptible long task that no try/catch or content-visibility can help (our JS runs before the browser ever skips layout). Tiered fix: - Message gate: past 200KB, bypass markdown entirely and render the raw text in `content-visibility:auto` line-chunks — synchronous work is bounded to a string split, the browser virtualizes layout natively, and every line stays in the DOM (selectable, find-in-page). - Code-block budget: past 3k lines / 150KB, skip Shiki (which emits a span per token) and render plain, chunked the same way. - Collapse/expand: a reusable ExpandableBlock clamps code blocks and the huge-text fallback to a 120px preview with a gradient + chevron, expanding to 300px. The inner element is always a scroll container so the content-visibility chunks stay lazily laid out in both states. No content is ever dropped; the copy button (card header) always yields the full block.
38 lines
1.3 KiB
TypeScript
38 lines
1.3 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
|
|
import { chunkByLines, exceedsHighlightBudget } from '@/components/chat/shiki-highlighter'
|
|
|
|
describe('exceedsHighlightBudget', () => {
|
|
it('highlights normal-sized blocks', () => {
|
|
expect(exceedsHighlightBudget('const x = 1\n'.repeat(100))).toBe(false)
|
|
})
|
|
|
|
it('skips highlighting past the line budget', () => {
|
|
expect(exceedsHighlightBudget('x\n'.repeat(5_000))).toBe(true)
|
|
})
|
|
|
|
it('skips highlighting past the char budget on few lines', () => {
|
|
expect(exceedsHighlightBudget('a'.repeat(200_000))).toBe(true)
|
|
})
|
|
|
|
it('short-circuits on char budget before line loop', () => {
|
|
expect(exceedsHighlightBudget('y\n'.repeat(250_000))).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('chunkByLines', () => {
|
|
it('keeps a small block as a single chunk', () => {
|
|
const code = 'a\nb\nc'
|
|
expect(chunkByLines(code, 200)).toEqual([{ text: code, lines: 3 }])
|
|
})
|
|
|
|
it('splits a large block and reconstructs it losslessly', () => {
|
|
const code = Array.from({ length: 1000 }, (_, i) => `line ${i}`).join('\n')
|
|
const chunks = chunkByLines(code, 200)
|
|
|
|
expect(chunks).toHaveLength(5)
|
|
expect(chunks.map(chunk => chunk.text).join('\n')).toBe(code)
|
|
expect(chunks.reduce((sum, chunk) => sum + chunk.lines, 0)).toBe(1000)
|
|
})
|
|
})
|