hermes-agent/apps/desktop/src/components/chat/shiki-highlighter.test.ts
Brooklyn Nicholson 0138282f97 perf(desktop): keep oversized messages from freezing the chat
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.
2026-06-17 08:25:52 -05:00

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)
})
})