Skip to content

Commit ab2761f

Browse files
christopheraaronhoggChris Hoggsteipete
authored
improve: make native chat scrolling reader-managed (#98258)
* improve native chat scroll behavior * add native chat scroll proof harnesses * fix(android): jump native chat to live content * fix(mobile): harden reader-managed chat scrolling * fix(android): preserve prompt anchor through completion * fix(android): gate follow changes on user scroll * fix(ios): handle optimistic chat rollback * fix(mobile): harden reader boundary transitions * fix(mobile): expose newer content after restore * fix(android): preserve restored reader identity * test(ios): prove reader pause and resume * fix(android): scope restored reader state to session * chore(i18n): sync native chat inventory * fix(mobile): harden reader scroll recovery * chore(i18n): sync native inventory after rebase --------- Co-authored-by: Chris Hogg <[email protected]> Co-authored-by: Peter Steinberger <[email protected]>
1 parent 8c915f0 commit ab2761f

14 files changed

Lines changed: 1325 additions & 160 deletions

File tree

apps/.i18n/native-source.json

Lines changed: 56 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5027,15 +5027,23 @@
50275027
},
50285028
{
50295029
"kind": "ui-call",
5030-
"line": 103,
5030+
"line": 105,
5031+
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatMessageListCard.kt",
5032+
"source": "Jump to latest",
5033+
"surface": "android",
5034+
"id": "native.android.97a0b44d89402809"
5035+
},
5036+
{
5037+
"kind": "ui-call",
5038+
"line": 126,
50315039
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatMessageListCard.kt",
50325040
"source": "Loading session",
50335041
"surface": "android",
50345042
"id": "native.android.69239493702da212"
50355043
},
50365044
{
50375045
"kind": "ui-call",
5038-
"line": 123,
5046+
"line": 146,
50395047
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatMessageListCard.kt",
50405048
"source": "No messages yet",
50415049
"surface": "android",
@@ -5107,7 +5115,7 @@
51075115
},
51085116
{
51095117
"kind": "ui-named-argument",
5110-
"line": 202,
5118+
"line": 201,
51115119
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
51125120
"source": "Chat needs attention",
51135121
"surface": "android",
@@ -5153,145 +5161,153 @@
51535161
"surface": "android",
51545162
"id": "native.android.882cc14a58e7e105"
51555163
},
5164+
{
5165+
"kind": "ui-named-argument",
5166+
"line": 536,
5167+
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
5168+
"source": "Jump to latest",
5169+
"surface": "android",
5170+
"id": "native.android.2c1106e1687ab258"
5171+
},
51565172
{
51575173
"kind": "conditional-branch",
5158-
"line": 535,
5174+
"line": 556,
51595175
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
51605176
"source": "Ready when you are",
51615177
"surface": "android",
51625178
"id": "native.android.9df2c9d68bad169f"
51635179
},
51645180
{
51655181
"kind": "ui-named-argument",
5166-
"line": 566,
5182+
"line": 587,
51675183
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
51685184
"source": "Fix connection",
51695185
"surface": "android",
51705186
"id": "native.android.6bbe3c5b5193d985"
51715187
},
51725188
{
51735189
"kind": "ui-named-argument",
5174-
"line": 567,
5190+
"line": 588,
51755191
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
51765192
"source": "Copy diagnostics",
51775193
"surface": "android",
51785194
"id": "native.android.579c05816c7dfa79"
51795195
},
51805196
{
51815197
"kind": "ui-named-argument",
5182-
"line": 704,
5198+
"line": 725,
51835199
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
51845200
"source": "Tools running",
51855201
"surface": "android",
51865202
"id": "native.android.781f84dfaf7da9d6"
51875203
},
51885204
{
51895205
"kind": "ui-named-argument",
5190-
"line": 709,
5206+
"line": 730,
51915207
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
51925208
"source": "+${toolCalls.size - 4} more",
51935209
"surface": "android",
51945210
"id": "native.android.0fd6b75cadc0ce75"
51955211
},
51965212
{
51975213
"kind": "ui-named-argument",
5198-
"line": 719,
5214+
"line": 740,
51995215
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
52005216
"source": "Thinking",
52015217
"surface": "android",
52025218
"id": "native.android.7e0aea1dca378135"
52035219
},
52045220
{
52055221
"kind": "ui-named-argument",
5206-
"line": 720,
5222+
"line": 741,
52075223
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
52085224
"source": "OpenClaw is preparing a response.",
52095225
"surface": "android",
52105226
"id": "native.android.98a0d4d5bba788c4"
52115227
},
52125228
{
52135229
"kind": "ui-named-argument",
5214-
"line": 813,
5230+
"line": 834,
52155231
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
52165232
"source": "Stop",
52175233
"surface": "android",
52185234
"id": "native.android.76d574acfac94fee"
52195235
},
52205236
{
52215237
"kind": "ui-named-argument",
5222-
"line": 830,
5238+
"line": 851,
52235239
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
52245240
"source": "Gateway offline",
52255241
"surface": "android",
52265242
"id": "native.android.8e3e367df24cae4b"
52275243
},
52285244
{
52295245
"kind": "ui-named-argument",
5230-
"line": 920,
5246+
"line": 941,
52315247
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
52325248
"source": "Attach image",
52335249
"surface": "android",
52345250
"id": "native.android.623e434c83e3c020"
52355251
},
52365252
{
52375253
"kind": "ui-named-argument",
5238-
"line": 935,
5254+
"line": 956,
52395255
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
52405256
"source": "Message OpenClaw",
52415257
"surface": "android",
52425258
"id": "native.android.f1d69eb66c5dfa39"
52435259
},
52445260
{
52455261
"kind": "ui-named-argument",
5246-
"line": 950,
5262+
"line": 971,
52475263
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
52485264
"source": "Voice",
52495265
"surface": "android",
52505266
"id": "native.android.19396391033f4fa8"
52515267
},
52525268
{
52535269
"kind": "ui-named-argument",
5254-
"line": 988,
5270+
"line": 1009,
52555271
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
52565272
"source": "Remove attachment",
52575273
"surface": "android",
52585274
"id": "native.android.5054269049758d6a"
52595275
},
52605276
{
52615277
"kind": "conditional-branch",
5262-
"line": 1000,
5278+
"line": 1021,
52635279
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
52645280
"source": "New chat",
52655281
"surface": "android",
52665282
"id": "native.android.326caa6b92aa9bdf"
52675283
},
52685284
{
52695285
"kind": "conditional-branch",
5270-
"line": 1009,
5286+
"line": 1030,
52715287
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
52725288
"source": "Main",
52735289
"surface": "android",
52745290
"id": "native.android.5fa853a957713a56"
52755291
},
52765292
{
52775293
"kind": "ui-named-argument",
5278-
"line": 1065,
5294+
"line": 1086,
52795295
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
52805296
"source": "Send",
52815297
"surface": "android",
52825298
"id": "native.android.6e2fcc9d67fc1273"
52835299
},
52845300
{
52855301
"kind": "conditional-branch",
5286-
"line": 1104,
5302+
"line": 1125,
52875303
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
52885304
"source": "$contextLabel · ${contextMeterThinkingLabel(thinkingLevel)}",
52895305
"surface": "android",
52905306
"id": "native.android.4e6c67df44d588d3"
52915307
},
52925308
{
52935309
"kind": "ui-named-argument",
5294-
"line": 267,
5310+
"line": 268,
52955311
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatSheetContent.kt",
52965312
"source": "CHAT ERROR",
52975313
"surface": "android",
@@ -19361,25 +19377,41 @@
1936119377
"surface": "apple",
1936219378
"id": "native.apple.43be8a2336e42faf"
1936319379
},
19380+
{
19381+
"kind": "ui-call",
19382+
"line": 416,
19383+
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
19384+
"source": "Jump to latest",
19385+
"surface": "apple",
19386+
"id": "native.apple.f84b674ad395b694"
19387+
},
19388+
{
19389+
"kind": "ui-modifier",
19390+
"line": 427,
19391+
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
19392+
"source": "Jump to latest reply",
19393+
"surface": "apple",
19394+
"id": "native.apple.429d825201b2cb85"
19395+
},
1936419396
{
1936519397
"kind": "ui-named-argument",
19366-
"line": 370,
19398+
"line": 450,
1936719399
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
1936819400
"source": "Refresh",
1936919401
"surface": "apple",
1937019402
"id": "native.apple.9ab586b85834705c"
1937119403
},
1937219404
{
1937319405
"kind": "ui-call",
19374-
"line": 686,
19406+
"line": 853,
1937519407
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
1937619408
"source": "Loading chat",
1937719409
"surface": "apple",
1937819410
"id": "native.apple.28c2691c03030434"
1937919411
},
1938019412
{
1938119413
"kind": "ui-modifier",
19382-
"line": 784,
19414+
"line": 951,
1938319415
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
1938419416
"source": "Dismiss",
1938519417
"surface": "apple",

apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatMessageListCard.kt

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,26 +11,30 @@ import ai.openclaw.app.ui.mobileTextSecondary
1111
import androidx.compose.foundation.layout.Arrangement
1212
import androidx.compose.foundation.layout.Box
1313
import androidx.compose.foundation.layout.Column
14+
import androidx.compose.foundation.layout.Row
1415
import androidx.compose.foundation.layout.fillMaxSize
1516
import androidx.compose.foundation.layout.fillMaxWidth
1617
import androidx.compose.foundation.layout.padding
1718
import androidx.compose.foundation.lazy.LazyColumn
1819
import androidx.compose.foundation.lazy.itemsIndexed
19-
import androidx.compose.foundation.lazy.rememberLazyListState
2020
import androidx.compose.foundation.shape.RoundedCornerShape
21+
import androidx.compose.material.icons.Icons
22+
import androidx.compose.material.icons.filled.ArrowDropDown
2123
import androidx.compose.material3.CircularProgressIndicator
24+
import androidx.compose.material3.Icon
2225
import androidx.compose.material3.Surface
2326
import androidx.compose.material3.Text
2427
import androidx.compose.runtime.Composable
25-
import androidx.compose.runtime.LaunchedEffect
2628
import androidx.compose.runtime.remember
2729
import androidx.compose.ui.Alignment
2830
import androidx.compose.ui.Modifier
31+
import androidx.compose.ui.text.font.FontWeight
2932
import androidx.compose.ui.unit.dp
3033

3134
/** Renders chat history newest-first while preserving stable scroll behavior during streaming. */
3235
@Composable
3336
fun ChatMessageListCard(
37+
sessionKey: String,
3438
messages: List<ChatMessage>,
3539
historyLoading: Boolean,
3640
pendingRunCount: Int,
@@ -39,7 +43,6 @@ fun ChatMessageListCard(
3943
healthOk: Boolean,
4044
modifier: Modifier = Modifier,
4145
) {
42-
val listState = rememberLazyListState()
4346
val timeline =
4447
remember(messages, pendingRunCount, pendingToolCalls, streamingAssistantText) {
4548
buildChatTimeline(
@@ -49,17 +52,17 @@ fun ChatMessageListCard(
4952
streamingAssistantText = streamingAssistantText,
5053
)
5154
}
52-
53-
LaunchedEffect(timeline.scrollTargetIndex, timeline.items.size, pendingRunCount, pendingToolCalls.size) {
54-
timeline.scrollTargetIndex?.let { index ->
55-
listState.animateScrollToItem(index = index)
56-
}
57-
}
55+
val readerScroll =
56+
rememberChatReaderScrollController(
57+
sessionKey = sessionKey,
58+
timeline = timeline,
59+
historyLoading = historyLoading,
60+
)
5861

5962
Box(modifier = modifier.fillMaxWidth()) {
6063
LazyColumn(
6164
modifier = Modifier.fillMaxSize(),
62-
state = listState,
65+
state = readerScroll.listState,
6366
reverseLayout = true,
6467
verticalArrangement = Arrangement.spacedBy(10.dp),
6568
contentPadding =
@@ -83,6 +86,26 @@ fun ChatMessageListCard(
8386
EmptyChatHint(modifier = Modifier.align(Alignment.Center), healthOk = healthOk)
8487
}
8588
}
89+
90+
if (readerScroll.showJumpToLatest) {
91+
Surface(
92+
onClick = readerScroll.jumpToLatest,
93+
modifier = Modifier.align(Alignment.BottomCenter).padding(bottom = 10.dp),
94+
shape = RoundedCornerShape(999.dp),
95+
color = mobileCardSurface.copy(alpha = 0.96f),
96+
border = androidx.compose.foundation.BorderStroke(1.dp, mobileBorder),
97+
shadowElevation = 6.dp,
98+
) {
99+
Row(
100+
modifier = Modifier.padding(horizontal = 12.dp, vertical = 7.dp),
101+
horizontalArrangement = Arrangement.spacedBy(6.dp),
102+
verticalAlignment = Alignment.CenterVertically,
103+
) {
104+
Icon(imageVector = Icons.Default.ArrowDropDown, contentDescription = null, tint = mobileText)
105+
Text("Jump to latest", style = mobileCallout.copy(fontWeight = FontWeight.SemiBold), color = mobileText)
106+
}
107+
}
108+
}
86109
}
87110
}
88111

0 commit comments

Comments
 (0)