Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 3.8k
perf(mothership): restore static markdown parse for settled chat messages#5751
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Uh oh!
There was an error while loading. Please reload this page.
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Jump to file
Failed to load files.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
68 changes: 50 additions & 18 deletions
68 ...ce/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -11,7 +11,7 @@ import 'prismjs/components/prism-bash' | ||
| import 'prismjs/components/prism-css' | ||
| import 'prismjs/components/prism-markup' | ||
| import '@sim/emcn/components/code/code.css' | ||
| import { Checkbox, CopyCodeButton, cn, highlight, languages } from '@sim/emcn' | ||
| import { Checkbox, CopyCodeButton, cn, languages, highlight as prismHighlight } from '@sim/emcn' | ||
| import { decodeVfsSegmentSafe } from '@/lib/copilot/vfs/path-utils' | ||
| import { extractTextContent } from '@/lib/core/utils/react-node-text' | ||
| import { ContextMentionIcon } from '@/app/workspace/[workspaceId]/home/components/context-mention-icon' | ||
| @@ -161,6 +161,38 @@ function fileIconLabel(ref: string, fallback: string): string { | ||
| return fallback | ||
| } | ||
| /** | ||
| * Bounded LRU cache for Prism highlight output. Chat rows are virtualized, so a | ||
| * message re-highlights every time it scrolls back into view; a component | ||
| * `useMemo` would not survive the unmount, so the cache lives at module scope. | ||
| * Output for an unregistered language is never cached — it renders through the | ||
| * JavaScript fallback, so caching it would keep serving that stale render if the | ||
| * real grammar registers later in the session. | ||
| */ | ||
| const HIGHLIGHT_CACHE_LIMIT = 512 | ||
| const highlightCache = new Map<string, string>() | ||
| function highlight(code: string, language: string): string { | ||
| const resolved = LANG_ALIASES[language] || language || 'javascript' | ||
| const grammar = languages[resolved] | ||
| if (!grammar) return prismHighlight(code, languages.javascript, resolved) | ||
| const key = `${resolved}\n${code}` | ||
| const cached = highlightCache.get(key) | ||
| if (cached !== undefined) { | ||
| highlightCache.delete(key) | ||
| highlightCache.set(key, cached) | ||
| return cached | ||
| } | ||
| const html = prismHighlight(code, grammar, resolved) | ||
| highlightCache.set(key, html) | ||
| if (highlightCache.size > HIGHLIGHT_CACHE_LIMIT) { | ||
| const oldest = highlightCache.keys().next().value | ||
| if (oldest !== undefined) highlightCache.delete(oldest) | ||
| } | ||
| return html | ||
| } | ||
| const MARKDOWN_COMPONENTS = { | ||
| table({ children }: { children?: React.ReactNode }) { | ||
| return ( | ||
| @@ -207,9 +239,7 @@ const MARKDOWN_COMPONENTS = { | ||
| ) | ||
| } | ||
| const resolved = LANG_ALIASES[language] || language || 'javascript' | ||
| const grammar = languages[resolved] || languages.javascript | ||
| const html = highlight(codeString.trimEnd(), grammar, resolved) | ||
| const html = highlight(codeString.trimEnd(), language) | ||
| return ( | ||
| <div className='not-prose my-6 overflow-hidden rounded-lg border border-[var(--divider)]'> | ||
| @@ -412,9 +442,9 @@ function ChatContentInner({ | ||
| * position (`E`/`qe` in streamdown 2.5), so a re-parse of unchanged content | ||
| * without the animate plugin bails at every unoverridden element (`p`, | ||
| * `strong`, `tr`, headings, …) and leaves the stale per-char span DOM in | ||
| * place. Every instance renders through the streaming parser (see | ||
| * `streamingTree` below) so the remount only sheds the spans, never | ||
| * re-interprets the markdown. | ||
| * place. The settled instance keeps the streaming parser (`parserTree` | ||
| * below) so the remount only sheds the spans, never re-interprets the | ||
| * markdown. | ||
| * | ||
| * The drain is deliberately one-way: a stream that resumes afterwards | ||
| * (reconnect/continuation) reveals paced but unfaded, because re-arming | ||
| @@ -459,18 +489,19 @@ function ChatContentInner({ | ||
| }, [isRevealing, animationDrained, streamedThisSession]) | ||
| /** | ||
| * Every mount renders through the streaming parser (remend + | ||
| * incomplete-markdown repair + block-split) — `mode='static'` is never used. | ||
| * The two pipelines parse edge-case markdown differently (unbalanced fences, | ||
| * list continuation across blocks), so a message you watched stream would | ||
| * render subtly differently from the same message reloaded from the DB; one | ||
| * pipeline makes in-session and refreshed renders byte-identical. The rows | ||
| * are virtualized, so only visible messages pay the block-split mount cost. | ||
| * `streamingTree` (the remount key and animation props) still drops at | ||
| * drain, so a settled instance re-renders through the SAME parser minus the | ||
| * per-word animation spans — identical pixels. | ||
| * `parserTree` (drives `mode`) stays latched for the mount's life: streaming | ||
| * mode is the only one that applies remend/incomplete-markdown repair and | ||
| * block-split parsing, so a settled message must KEEP the streaming parser — | ||
| * swapping to `mode='static'` at drain re-parses the same source through a | ||
| * different pipeline (no remend, whole-doc parse) and visibly flashes on any | ||
| * reply with unbalanced markdown. `streamingTree` (drives the remount key | ||
| * and animation props) additionally drops at drain, so the settled instance | ||
| * re-renders through the SAME parser minus the per-word animation spans — | ||
| * byte-identical pixels. Only never-streamed mounts (reloaded history) | ||
| * render static. | ||
| */ | ||
| const streamingTree = (isRevealing || streamedThisSession) && !animationDrained | ||
| const parserTree = isRevealing || streamedThisSession | ||
| const streamingTree = parserTree && !animationDrained | ||
| /** | ||
| * One-way fade cutoff (see {@link FADE_MAX_REVEALED_CHARS}). Latched so a | ||
| @@ -573,6 +604,7 @@ function ChatContentInner({ | ||
| > | ||
| <Streamdown | ||
| key={streamingTree ? 'stream' : 'settled'} | ||
| mode={parserTree ? undefined : 'static'} | ||
waleedlatif1 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| animated={fadeActive ? STREAM_ANIMATION : false} | ||
| isAnimating={streamingTree} | ||
| components={MARKDOWN_COMPONENTS} | ||
Oops, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.