From 56e23a887f46d954bca87e20b8beec791b52f5bb Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Mon, 16 Mar 2026 18:42:20 +0530 Subject: [PATCH] fix(android): reduce chat recomposition churn --- .../app/ui/chat/ChatMessageListCard.kt | 20 +++++++++++++------ .../openclaw/app/ui/chat/ChatSheetContent.kt | 5 ++++- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatMessageListCard.kt b/apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatMessageListCard.kt index 976972a7831..96d5e7cf7f6 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatMessageListCard.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatMessageListCard.kt @@ -6,12 +6,14 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @@ -34,11 +36,19 @@ fun ChatMessageListCard( modifier: Modifier = Modifier, ) { val listState = rememberLazyListState() + val displayMessages = remember(messages) { messages.asReversed() } + val stream = streamingAssistantText?.trim() - // With reverseLayout the newest item is at index 0 (bottom of screen). - LaunchedEffect(messages.size, pendingRunCount, pendingToolCalls.size, streamingAssistantText) { + // New list items/tool rows should animate into view, but token streaming should not restart + // that animation on every delta. + LaunchedEffect(messages.size, pendingRunCount, pendingToolCalls.size) { listState.animateScrollToItem(index = 0) } + LaunchedEffect(stream) { + if (!stream.isNullOrEmpty()) { + listState.scrollToItem(index = 0) + } + } Box(modifier = modifier.fillMaxWidth()) { LazyColumn( @@ -50,8 +60,6 @@ fun ChatMessageListCard( ) { // With reverseLayout = true, index 0 renders at the BOTTOM. // So we emit newest items first: streaming → tools → typing → messages (newest→oldest). - - val stream = streamingAssistantText?.trim() if (!stream.isNullOrEmpty()) { item(key = "stream") { ChatStreamingAssistantBubble(text = stream) @@ -70,8 +78,8 @@ fun ChatMessageListCard( } } - items(count = messages.size, key = { idx -> messages[messages.size - 1 - idx].id }) { idx -> - ChatMessageBubble(message = messages[messages.size - 1 - idx]) + items(items = displayMessages, key = { it.id }) { message -> + ChatMessageBubble(message = message) } } diff --git a/apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatSheetContent.kt b/apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatSheetContent.kt index a4a93eeceec..201832b9fd3 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatSheetContent.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatSheetContent.kt @@ -160,7 +160,10 @@ private fun ChatThreadSelector( mainSessionKey: String, onSelectSession: (String) -> Unit, ) { - val sessionOptions = resolveSessionChoices(sessionKey, sessions, mainSessionKey = mainSessionKey) + val sessionOptions = + remember(sessionKey, sessions, mainSessionKey) { + resolveSessionChoices(sessionKey, sessions, mainSessionKey = mainSessionKey) + } Row( modifier = Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()),