Skip to content

Commit 43eb4cf

Browse files
yyq1025claude
andcommitted
app: wrap enriched runs in a View — stops sub-pixel height creep; gap-based segment spacing
With EnrichedMarkdownText as a direct flex child of the segment column, repeated layout passes (e.g. swiping a sibling code block's horizontal ScrollView) inflate its measured height ~0.5px per pass, visible as growing blank space inside the message. Root cause is upstream: with streamingAnimation enabled, enriched's measureContent fast path returns the live view.bounds — the PREVIOUS layout's pixel-grid-rounded output — as the next measurement input (ShadowMeasurementUtils.h), a measure→round→remeasure feedback loop. A plain View wrapper stabilizes the node's constraints so Yoga stops re-invoking the measure, breaking the loop. A/B-verified on device; TailRunSegment gets the same wrapper for when remend goes live. Segment spacing also moves from per-code-block first/last margins to a single gap on the container — between-segments only, so edge code blocks stay flush for free and the streaming isLast flip re-render goes away. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 2c9532e commit 43eb4cf

1 file changed

Lines changed: 23 additions & 18 deletions

File tree

packages/app/src/lib/markdown/chunked-markdown.tsx

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,28 @@ import { useRemend } from "./remend";
5353
*/
5454

5555
const RunSegment = memo(function RunSegment({ raw }: { raw: string }) {
56-
return <ChatMarkdown markdown={raw} />;
56+
// The plain View wrapper is LOAD-BEARING, not decoration: with enriched
57+
// as a direct flex child of the segment column, repeated layout passes
58+
// (e.g. swiping a sibling code block's ScrollView) inflate its measured
59+
// height by ~0.5px per pass — visible as growing blank space inside the
60+
// message. An ordinary View between the column and enriched breaks that
61+
// measure→round→remeasure loop. Verified by A/B on device 2026-06-12.
62+
return (
63+
<View>
64+
<ChatMarkdown markdown={raw} />
65+
</View>
66+
);
5767
});
5868

59-
/** Tail run while streaming: unterminated inline syntax gets repaired. */
69+
/** Tail run while streaming: unterminated inline syntax gets repaired.
70+
* Same load-bearing View wrapper as RunSegment (see comment there). */
6071
function TailRunSegment({ raw }: { raw: string }) {
6172
const processed = useRemend(raw);
62-
return <ChatMarkdown markdown={processed} />;
73+
return (
74+
<View>
75+
<ChatMarkdown markdown={processed} />
76+
</View>
77+
);
6378
}
6479

6580
function lineKeyOf(tokens: { content: string; color?: string }[]): string {
@@ -101,19 +116,10 @@ const TokenLine = memo(
101116
const CodeBlockSegment = memo(function CodeBlockSegment({
102117
lang,
103118
code,
104-
isFirst,
105-
isLast,
106119
onTokenizeMs,
107120
}: {
108121
lang: string;
109122
code: string;
110-
/** Margins only face NEIGHBORING segments: a message that starts or
111-
* ends with a code block keeps its outer edge flush, matching how
112-
* enriched messages start/end (their first/last block margins don't
113-
* add outer padding either). `isLast` flips once when more content
114-
* streams in after the block — a single cheap re-render. */
115-
isFirst: boolean;
116-
isLast: boolean;
117123
onTokenizeMs?: (ms: number) => void;
118124
}) {
119125
const hl = useHighlighter();
@@ -131,9 +137,7 @@ const CodeBlockSegment = memo(function CodeBlockSegment({
131137

132138
return (
133139
<View
134-
className={`rounded-lg overflow-hidden ${isFirst ? "" : "mt-3"} ${
135-
isLast ? "" : "mb-3"
136-
}`}
140+
className="rounded-lg overflow-hidden"
137141
style={{
138142
backgroundColor: palette.codeBlockBg,
139143
}}
@@ -198,16 +202,17 @@ export function ChunkedMarkdown({
198202

199203
const lastIndex = segments.length - 1;
200204
return (
201-
<View>
205+
// Segment spacing via container gap (12) — only exists BETWEEN
206+
// segments, so first/last code blocks stay flush with the message
207+
// edges for free (no per-segment first/last margin bookkeeping).
208+
<View className="gap-3">
202209
{segments.map((seg: MarkdownSegment, i: number) => {
203210
if (seg.kind === "code") {
204211
return (
205212
<CodeBlockSegment
206213
key={seg.key}
207214
lang={seg.lang}
208215
code={seg.code}
209-
isFirst={i === 0}
210-
isLast={i === lastIndex}
211216
onTokenizeMs={onTokenizeMs}
212217
/>
213218
);

0 commit comments

Comments
 (0)