Skip to content

Commit 4651080

Browse files
committed
fix(mobile): clipped thoughts no longer run into the next reply
1 parent 5c3fed5 commit 4651080

3 files changed

Lines changed: 69 additions & 2 deletions

File tree

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { useId, useState, type ReactNode } from "react";
2+
import { ScrollView, View } from "react-native";
3+
import Svg, { Defs, LinearGradient, Rect, Stop } from "react-native-svg";
4+
5+
import { WorkLogBlock } from "./work-log-layout";
6+
7+
const FADE_HEIGHT = 24;
8+
9+
/** A capped thought box; a clipped thought fades out so the next reply does not read as part of it. */
10+
export function ReasoningGroupScroll(props: {
11+
readonly continues: boolean | undefined;
12+
readonly fadeColor: string;
13+
readonly children: ReactNode;
14+
}) {
15+
const [viewportHeight, setViewportHeight] = useState(0);
16+
const [contentHeight, setContentHeight] = useState(0);
17+
const [atEnd, setAtEnd] = useState(false);
18+
const clipped = contentHeight > viewportHeight + 1;
19+
return (
20+
<WorkLogBlock continues={props.continues}>
21+
<View className="ml-7">
22+
<ScrollView
23+
nestedScrollEnabled
24+
className="max-h-96 py-1"
25+
scrollEventThrottle={64}
26+
onLayout={(event) => setViewportHeight(event.nativeEvent.layout.height)}
27+
onContentSizeChange={(_, height) => setContentHeight(height)}
28+
onScroll={({ nativeEvent }) =>
29+
setAtEnd(
30+
nativeEvent.contentOffset.y + nativeEvent.layoutMeasurement.height >=
31+
nativeEvent.contentSize.height - 1,
32+
)
33+
}
34+
>
35+
{props.children}
36+
</ScrollView>
37+
{clipped && !atEnd ? <BottomFade color={props.fadeColor} /> : null}
38+
</View>
39+
</WorkLogBlock>
40+
);
41+
}
42+
43+
function BottomFade(props: { readonly color: string }) {
44+
const gradientId = `reasoning-fade-${useId().replaceAll(":", "")}`;
45+
return (
46+
<View
47+
pointerEvents="none"
48+
className="absolute inset-x-0 bottom-0"
49+
style={{ height: FADE_HEIGHT }}
50+
>
51+
<Svg width="100%" height="100%">
52+
<Defs>
53+
<LinearGradient id={gradientId} x1="0%" x2="0%" y1="0%" y2="100%">
54+
<Stop offset={0} stopColor={props.color} stopOpacity={0} />
55+
<Stop offset={1} stopColor={props.color} stopOpacity={1} />
56+
</LinearGradient>
57+
</Defs>
58+
<Rect width="100%" height="100%" fill={`url(#${gradientId})`} />
59+
</Svg>
60+
</View>
61+
);
62+
}

‎apps/mobile/src/features/threads/thread-work-log.tsx‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Button } from "../../components/ui/Button";
22
import { SubagentStatusDot } from "./SubagentStatusDot";
33
import { ThreadSubagentGroup } from "./thread-subagent-group";
44
import { ProposedPlanCard } from "./ProposedPlanCard";
5+
import { ReasoningGroupScroll } from "./ReasoningGroupScroll";
56
import {
67
WorkLogLabel,
78
WorkLogBlock,
@@ -542,11 +543,11 @@ export function ThreadWorkLog(props: ThreadWorkLogProps) {
542543
props.activities.every((row) => row.projectedItem.item.type === "reasoning")
543544
) {
544545
return (
545-
<ScrollView nestedScrollEnabled className="ml-7 max-h-96 py-1">
546+
<ReasoningGroupScroll continues={props.continuesWorkLog} fadeColor={props.edgeFadeColor}>
546547
{props.activities.map((row) => (
547548
<View key={row.id}>{props.renderReasoning(row.workEntry.detail ?? "")}</View>
548549
))}
549-
</ScrollView>
550+
</ReasoningGroupScroll>
550551
);
551552
}
552553

‎patches.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,10 @@ Upstream baseline: `b3d382e100` (merged 2026-10-25).
129129
- The menu reports only the selected text and its offsets within one rendered block, so a mobile citation has block-relative offsets and an empty prefix and suffix: web finds the source when the quote occurs once in the response and otherwise shows the saved quote without jumping.
130130
- Upstream's new orchestrator (`de34391427`, #2829) dropped the `expandAssistantCitationsForProvider` call the old `ProviderService` made, so every provider received the raw link with its quote percent-encoded (a Chinese quote reads `%E5%BF%AB…`), from web as well. `ProviderTurnStartService` and `ProviderTurnControlService` (steering) expand it again after projecting composer context; the stored message keeps the link. `ProviderTurnStartService.fork.test.ts` covers sending and steering. Drop that part when upstream restores the expansion, and the rest when upstream mobile creates citations.
131131

132+
## 14. Mobile thought groups keep their margin
133+
134+
- An expanded group of thoughts scrolls inside a box capped at `max-h-96`. Upstream rendered it as a bare `ScrollView`, without the `WorkLogBlock` bottom margin the other work log rows get, so a clipped thought ended flush against the next reply and read as overlapping it. `ReasoningGroupScroll.tsx` wraps the box in `WorkLogBlock` and fades its bottom edge while the thought is clipped and not scrolled to the end; `thread-work-log.tsx` only swaps the `ScrollView` for it. Before/after captures were not taken.
135+
132136
## Fork tests
133137

134138
- Fork test cases live in `*.fork.test.ts` beside the file they cover, each with a copy of the small harness it needs, so upstream can rewrite or delete its own tests without conflicts. Upstream test files carry no fork edits.

0 commit comments

Comments
 (0)