Skip to content

Commit 2c9532e

Browse files
yyq1025claude
andcommitted
app: disable list recycling — fixes scroll-up row overlap
Device-verified: with recycleItems on, scrolling up through rows of large height variance (36pt tool rows vs multi-thousand-px assistant chunks) produced visibly overlapping rows — a recycled container carries the previous item's size/position for a beat before the new content's layout lands (legend-list#301 is the same combo; 3.0.4's overlap fixes don't cover it). Recycling is opt-in upstream (default false), so this returns to the default remount-on-reuse mode; fresh-mount scroll cost measured acceptable. A drawDistance bump to 750 did NOT help and slowed first render — not kept. Also pins tool rows via getFixedItemSize (12 + 24×fontScale — authoritative, keep in sync with tool-block.tsx styles), removing the most numerous row kind from size estimation, and corrects the chat-markdown header comment to the current tableMode: 'progressive'. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent a6faa51 commit 2c9532e

2 files changed

Lines changed: 44 additions & 7 deletions

File tree

packages/app/src/components/transcript/chat-panel.tsx

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,12 @@ import { useQueryClient } from "@tanstack/react-query";
1717
import * as Crypto from "expo-crypto";
1818
import { useHeaderHeight } from "expo-router/react-navigation";
1919
import { useCallback, useEffect, useRef, useState } from "react";
20-
import { useColorScheme, View, type ViewProps } from "react-native";
20+
import {
21+
useColorScheme,
22+
useWindowDimensions,
23+
View,
24+
type ViewProps,
25+
} from "react-native";
2126
import { KeyboardStickyView } from "react-native-keyboard-controller";
2227
import Animated, {
2328
useAnimatedProps,
@@ -40,6 +45,14 @@ import {
4045
sendUserMessage,
4146
} from "@/lib/transcript-collection-factory";
4247

48+
// ToolBlock row height, derived from its styles (keep in sync with
49+
// tool-block.tsx): Pressable py-1.5 (12) + one text-base line (lineHeight
50+
// 24, the tallest child — ToolChip is ~18). Both Bash and chip branches are
51+
// single-line by construction (numberOfLines={1}). The text line scales
52+
// with Dynamic Type; the vertical padding doesn't.
53+
const TOOL_ROW_PADDING_V = 12;
54+
const TOOL_ROW_LINE_HEIGHT = 24;
55+
4356
type ChatPanelProps = {
4457
cliSessionId: string;
4558
cwd: string | undefined;
@@ -109,7 +122,21 @@ export function ChatPanel({
109122
}: ChatPanelProps) {
110123
const insets = useSafeAreaInsets();
111124
const headerHeight = useHeaderHeight();
125+
const { fontScale } = useWindowDimensions();
112126
const listRef = useRef<LegendListRef>(null);
127+
128+
// Authoritative height for tool rows (v3's getFixedItemSize — trusted as
129+
// KNOWN, not an estimate, so it must be exact). Pinning the most numerous
130+
// row kind removes it from size estimation entirely, shrinking the
131+
// correction shifts that cause transient row overlap when scrolling up
132+
// through unmeasured content. Text rows return undefined → measured.
133+
const getFixedItemSize = useCallback(
134+
(item: RenderBlock) =>
135+
item.kind === "tool"
136+
? TOOL_ROW_PADDING_V + Math.round(TOOL_ROW_LINE_HEIGHT * fontScale)
137+
: undefined,
138+
[fontScale],
139+
);
113140
const composerRef = useRef<View>(null);
114141
const isDark = useColorScheme() === "dark";
115142
// Tap the workspace status bar → open the working-tree diff in the shared
@@ -296,6 +323,7 @@ export function ChatPanel({
296323
// → fewer "convergence bounds" warns). Also keys the recycle pool so a
297324
// text row isn't recycled into a tool.
298325
getItemType={(item) => (item.kind === "text" ? item.role : item.kind)}
326+
getFixedItemSize={getFixedItemSize}
299327
// Disable iOS's automatic contentInset adjustments — they
300328
// fight KeyboardChatScrollView's own inset management and end
301329
// up double-counting safe area. Per react-native-keyboard-
@@ -370,7 +398,16 @@ export function ChatPanel({
370398
maintainVisibleContentPosition={{ size: true, data: false }}
371399
// iOS pull-down-to-dismiss for the keyboard.
372400
keyboardDismissMode="interactive"
373-
recycleItems
401+
// Recycling DISABLED (verified on device 2026-06-12): with it on,
402+
// scrolling up through rows with large height variance (tool rows
403+
// ~36pt vs assistant chunks up to thousands) produced visible row
404+
// overlap — a recycled container carries the previous item's
405+
// size/position for a beat before the new content's layout lands
406+
// (LegendApp/legend-list#301 is the same combo). Off = every
407+
// entering row is a fresh mount (enriched parse + TextInput);
408+
// scroll fps acceptable on device. Revisit only with an upstream
409+
// fix in hand, verified against the same long-session repro.
410+
recycleItems={false}
374411
// Only push the list up by the keyboard height when the user
375412
// is already pinned at the bottom; anywhere else the keyboard
376413
// floats over the content so what they're reading stays put.

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ import {
2424
* - `streamingAnimation` — fades in newly appended tokens as patch_text
2525
* deltas arrive (every ~200ms during a turn, per envelope from the
2626
* SDK iterator).
27-
* - `streamingConfig.tableMode: 'hidden'` — hides incomplete GFM tables
28-
* during streaming, then reveals them at boundary completion. Avoids
29-
* the single-digit-fps reparse cost we measured on 0.5.0 stable.
30-
* (See project_streaming_markdown_w3.md for the perf observation +
31-
* decision history.)
27+
* - `streamingConfig.tableMode: 'progressive'` — renders GFM tables
28+
* incrementally as rows stream in (0.6 made this viable; 0.5.0's
29+
* full-reparse path measured single-digit fps, which is why V0
30+
* originally shipped 'hidden'. See project_streaming_markdown_w3.md
31+
* for the decision history.)
3232
* - `flavor: 'github'` — enables GFM extensions; commonmark-only would
3333
* drop tables which Claude responses occasionally include.
3434
*

0 commit comments

Comments
 (0)