fix(chat): fix 5 message-rendering correctness bugs (#132)
- MessageBubble: memo comparator only checked the last part's `.text`, which is always undefined for tool parts, so tool-state/token/cost updates never re-rendered when a tool part was last. Replace with a full reference-equality sweep over message + all parts (the store always replaces changed refs, so this catches every real change). - DiffView: computeDiff's O(a.length*b.length) LCS table was unbounded, risking OOM/ANR on large diffs, and the rendered line list was unbounded too. Add a size-guarded fallback (simple truncated remove/add diff) and cap the normal path's rendered lines, both with a truncation marker. Extracted computeDiff into a plain diff-compute.ts module (mirrors src/lib/scroll-config.ts) so it's unit-testable with node:test, which can't render .tsx components. - DiffView: normalize line endings (\r?\n) before diffing so a CRLF vs LF mismatch doesn't show a whole file as changed. - Markdown: the module-scope singleton CustomRenderer's github-slugger never reset, so useMarkdown's keys climbed on every streamed token, remounting the whole subtree. Scope the renderer per `children` via useMemo instead. - Markdown: theme objects used heading1/heading2/heading3/listItem, but react-native-marked's MarkedStyles expects h1/h2/h3/li, so the custom heading/list styling was silently dropped. Rename the keys in both themes. Claude-Session: https://claude.ai/code/session_01T12AhSnQVrSxNnvwfCx2z6 Co-authored-by: engineer <engineer@macbookpro.lan> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -113,17 +113,19 @@ export const MessageBubble = memo(
|
||||
},
|
||||
(prev, next) => {
|
||||
// Only re-render if message content actually changed
|
||||
// This prevents completed messages from re-rendering during streaming
|
||||
if (prev.message.id !== next.message.id) return false
|
||||
// This prevents completed messages from re-rendering during streaming.
|
||||
// The store replaces changed parts/messages with NEW object references,
|
||||
// so a reference-equality sweep over every part catches every real change
|
||||
// (including tool parts, which have no `.text`) while still skipping
|
||||
// unchanged (completed) messages during other messages' streaming.
|
||||
if (prev.message !== next.message) return false
|
||||
if (prev.isDark !== next.isDark) return false
|
||||
if (prev.onLongPress !== next.onLongPress) return false
|
||||
if (prev.parts.length !== next.parts.length) return false
|
||||
// Compare the last part's text content - this is what changes during streaming
|
||||
const prevLast = prev.parts[prev.parts.length - 1]
|
||||
const nextLast = next.parts[next.parts.length - 1]
|
||||
if (!prevLast && !nextLast) return true
|
||||
if (!prevLast || !nextLast) return false
|
||||
return prevLast.type === nextLast.type && prevLast.text === nextLast.text
|
||||
for (let i = 0; i < prev.parts.length; i++) {
|
||||
if (prev.parts[i] !== next.parts[i]) return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user