hermes-agent/apps/desktop/src/components/chat/code-card.tsx
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

78 lines
2.3 KiB
TypeScript

import * as React from 'react'
import { Codicon, type CodiconProps } from '@/components/ui/codicon'
import { cn } from '@/lib/utils'
/**
* Rounded-card shell for fenced code (and any equivalent: diffs, raw payloads,
* etc.) sized for the conversation column. Mirrors the expanded tool-row
* pattern so code blocks read as the same family of artifact.
*/
function CodeCard({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
className={cn(
'min-w-0 max-w-full overflow-hidden rounded-[0.625rem] border border-border text-[length:var(--conversation-tool-font-size)] text-muted-foreground',
className
)}
data-slot="code-card"
{...props}
/>
)
}
function CodeCardHeader({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
className={cn('flex items-center justify-between gap-2 border-b border-border px-2 py-1.5', className)}
data-slot="code-card-header"
{...props}
/>
)
}
function CodeCardTitle({ className, children, ...props }: React.ComponentProps<'span'>) {
return (
<span
className={cn(
'flex min-w-0 items-center gap-1.5 truncate text-[length:var(--conversation-tool-font-size)] font-medium leading-(--conversation-line-height) text-foreground/80',
className
)}
data-slot="code-card-title"
{...props}
>
{children}
</span>
)
}
function CodeCardIcon({ className, ...props }: CodiconProps) {
return (
<Codicon
className={cn('shrink-0 text-[0.875rem] leading-none text-muted-foreground', className)}
data-slot="code-card-icon"
{...props}
/>
)
}
function CodeCardSubtitle({ className, ...props }: React.ComponentProps<'span'>) {
return (
<span className={cn('font-normal text-muted-foreground', className)} data-slot="code-card-subtitle" {...props} />
)
}
function CodeCardBody({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
className={cn(
'font-mono text-[0.7rem] leading-relaxed text-foreground/90 [&_pre]:m-0 [&_pre]:overflow-x-auto [&_pre]:bg-transparent! [&_pre]:px-2 [&_pre]:py-1.5 [&_pre]:font-mono [&_pre]:leading-relaxed',
className
)}
data-slot="code-card-body"
{...props}
/>
)
}
export { CodeCard, CodeCardBody, CodeCardHeader, CodeCardIcon, CodeCardSubtitle, CodeCardTitle }