Skip to content

Commit a6faa51

Browse files
yyq1025claude
andcommitted
app: code-block edge margins + gate scroll FAB until initial scroll settles
Code blocks only carry margins facing neighboring segments — a message starting/ending with a code block keeps its outer edge flush, matching enriched message edges. The FAB was flashing on every session open: during list boot the viewport sits at the top for a few frames and checkAtBottom unconditionally reports isNearEnd=false (the hasActiveInitialScroll skip only guards isEndReached). Gate visibility behind a fabReady shared value set in onLoad, which fires once containers have laid out AND the initial scroll finished. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 0f004ad commit a6faa51

2 files changed

Lines changed: 32 additions & 3 deletions

File tree

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

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,11 +194,23 @@ export function ChatPanel({
194194
// Written by the list on the UI thread (reanimated integration) — drives the
195195
// scroll-to-end FAB without a single JS-side re-render.
196196
const isNearEnd = useSharedValue(true);
197+
// Gates the FAB until the list's initial scroll settles. During boot the
198+
// list sits at the top for a few frames, so checkAtBottom legitimately
199+
// reports isNearEnd=false (the library's hasActiveInitialScroll skip only
200+
// guards isEndReached, not isNearEnd) — without this gate the FAB flashes
201+
// on every session open. `onLoad` fires exactly once, when containers have
202+
// laid out AND the initial scroll finished.
203+
const fabReady = useSharedValue(false);
197204
const scrollFabStyle = useAnimatedStyle(() => ({
198-
opacity: withTiming(isNearEnd.value ? 0 : 1, { duration: 160 }),
205+
opacity: withTiming(fabReady.value && !isNearEnd.value ? 1 : 0, {
206+
duration: 160,
207+
}),
199208
}));
200209
const scrollFabProps = useAnimatedProps<ViewProps>(() => ({
201-
pointerEvents: isNearEnd.value ? ("none" as const) : ("box-none" as const),
210+
pointerEvents:
211+
fabReady.value && !isNearEnd.value
212+
? ("box-none" as const)
213+
: ("none" as const),
202214
}));
203215

204216
// The "real send" handed to InputBar. InputBar already intercepts slash
@@ -347,6 +359,10 @@ export function ChatPanel({
347359
// List → UI-thread state mirror (reanimated integration). `isNearEnd`
348360
// gates the scroll-to-end FAB; no JS re-render involved.
349361
sharedValues={{ isNearEnd }}
362+
// Initial render + initial scroll settled → un-gate the FAB.
363+
onLoad={() => {
364+
fabReady.set(true);
365+
}}
350366
// Stabilize the visible position on size/layout changes (keyboard
351367
// toggle, streaming item growth) but NOT on data adds — with
352368
// maintainScrollAtEnd gone, new data moves nothing by design: the

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,19 @@ const TokenLine = memo(
101101
const CodeBlockSegment = memo(function CodeBlockSegment({
102102
lang,
103103
code,
104+
isFirst,
105+
isLast,
104106
onTokenizeMs,
105107
}: {
106108
lang: string;
107109
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;
108117
onTokenizeMs?: (ms: number) => void;
109118
}) {
110119
const hl = useHighlighter();
@@ -122,7 +131,9 @@ const CodeBlockSegment = memo(function CodeBlockSegment({
122131

123132
return (
124133
<View
125-
className="my-3 rounded-lg overflow-hidden"
134+
className={`rounded-lg overflow-hidden ${isFirst ? "" : "mt-3"} ${
135+
isLast ? "" : "mb-3"
136+
}`}
126137
style={{
127138
backgroundColor: palette.codeBlockBg,
128139
}}
@@ -195,6 +206,8 @@ export function ChunkedMarkdown({
195206
key={seg.key}
196207
lang={seg.lang}
197208
code={seg.code}
209+
isFirst={i === 0}
210+
isLast={i === lastIndex}
198211
onTokenizeMs={onTokenizeMs}
199212
/>
200213
);

0 commit comments

Comments
 (0)