From e11e329238532b533034e0fad8c8c8fbda50b0e5 Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Tue, 24 Feb 2026 21:57:58 +0530 Subject: [PATCH] refactor(android-chat): move thread selector above composer --- .../openclaw/android/ui/chat/ChatComposer.kt | 85 +------------- .../android/ui/chat/ChatSheetContent.kt | 106 +++++++++++++++++- 2 files changed, 105 insertions(+), 86 deletions(-) diff --git a/apps/android/app/src/main/java/ai/openclaw/android/ui/chat/ChatComposer.kt b/apps/android/app/src/main/java/ai/openclaw/android/ui/chat/ChatComposer.kt index d8795464439..7f71995906b 100644 --- a/apps/android/app/src/main/java/ai/openclaw/android/ui/chat/ChatComposer.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/ui/chat/ChatComposer.kt @@ -41,19 +41,16 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import ai.openclaw.android.chat.ChatSessionEntry import ai.openclaw.android.ui.mobileAccent import ai.openclaw.android.ui.mobileAccentSoft import ai.openclaw.android.ui.mobileBorder import ai.openclaw.android.ui.mobileBorderStrong import ai.openclaw.android.ui.mobileCallout import ai.openclaw.android.ui.mobileCaption1 -import ai.openclaw.android.ui.mobileDanger import ai.openclaw.android.ui.mobileHeadline import ai.openclaw.android.ui.mobileSurface import ai.openclaw.android.ui.mobileText @@ -62,9 +59,6 @@ import ai.openclaw.android.ui.mobileTextTertiary @Composable fun ChatComposer( - sessionKey: String, - sessions: List, - mainSessionKey: String, healthOk: Boolean, thinkingLevel: String, pendingRunCount: Int, @@ -72,7 +66,6 @@ fun ChatComposer( onPickImages: () -> Unit, onRemoveAttachment: (id: String) -> Unit, onSetThinkingLevel: (level: String) -> Unit, - onSelectSession: (sessionKey: String) -> Unit, onRefresh: () -> Unit, onAbort: () -> Unit, onSend: (text: String) -> Unit, @@ -80,62 +73,10 @@ fun ChatComposer( var input by rememberSaveable { mutableStateOf("") } var showThinkingMenu by remember { mutableStateOf(false) } - val sessionOptions = resolveSessionChoices(sessionKey, sessions, mainSessionKey = mainSessionKey) - val currentSessionLabel = - friendlySessionName(sessionOptions.firstOrNull { it.key == sessionKey }?.displayName ?: sessionKey) - val canSend = pendingRunCount == 0 && (input.trim().isNotEmpty() || attachments.isNotEmpty()) && healthOk val sendBusy = pendingRunCount > 0 Column(modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp)) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = "SESSION", - style = mobileCaption1.copy(fontWeight = FontWeight.Bold, letterSpacing = 0.8.sp), - color = mobileTextSecondary, - ) - Row(horizontalArrangement = Arrangement.spacedBy(6.dp), verticalAlignment = Alignment.CenterVertically) { - Text( - text = currentSessionLabel, - style = mobileCallout.copy(fontWeight = FontWeight.SemiBold), - color = mobileText, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - ConnectionPill(healthOk = healthOk) - } - } - - Row( - modifier = Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()), - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - for (entry in sessionOptions) { - val active = entry.key == sessionKey - Surface( - onClick = { onSelectSession(entry.key) }, - shape = RoundedCornerShape(14.dp), - color = if (active) mobileAccent else Color.White, - border = BorderStroke(1.dp, if (active) Color(0xFF154CAD) else mobileBorderStrong), - tonalElevation = 0.dp, - shadowElevation = 0.dp, - ) { - Text( - text = friendlySessionName(entry.displayName ?: entry.key), - style = mobileCaption1.copy(fontWeight = if (active) FontWeight.Bold else FontWeight.SemiBold), - color = if (active) Color.White else mobileText, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp), - ) - } - } - } - Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, @@ -193,10 +134,10 @@ fun ChatComposer( OutlinedTextField( value = input, onValueChange = { input = it }, - modifier = Modifier.fillMaxWidth().height(108.dp), + modifier = Modifier.fillMaxWidth().height(92.dp), placeholder = { Text("Type a message", style = mobileBodyStyle(), color = mobileTextTertiary) }, - minLines = 3, - maxLines = 6, + minLines = 2, + maxLines = 5, textStyle = mobileBodyStyle().copy(color = mobileText), shape = RoundedCornerShape(14.dp), colors = chatTextFieldColors(), @@ -261,26 +202,6 @@ fun ChatComposer( } } -@Composable -private fun ConnectionPill(healthOk: Boolean) { - Surface( - shape = RoundedCornerShape(999.dp), - color = if (healthOk) ai.openclaw.android.ui.mobileSuccessSoft else ai.openclaw.android.ui.mobileWarningSoft, - border = - BorderStroke( - 1.dp, - if (healthOk) ai.openclaw.android.ui.mobileSuccess.copy(alpha = 0.35f) else ai.openclaw.android.ui.mobileWarning.copy(alpha = 0.35f), - ), - ) { - Text( - text = if (healthOk) "Connected" else "Offline", - style = mobileCaption1.copy(fontWeight = FontWeight.SemiBold), - color = if (healthOk) ai.openclaw.android.ui.mobileSuccess else ai.openclaw.android.ui.mobileWarning, - modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp), - ) - } -} - @Composable private fun SecondaryActionButton( label: String, diff --git a/apps/android/app/src/main/java/ai/openclaw/android/ui/chat/ChatSheetContent.kt b/apps/android/app/src/main/java/ai/openclaw/android/ui/chat/ChatSheetContent.kt index 413015bd14b..d1c2743ef04 100644 --- a/apps/android/app/src/main/java/ai/openclaw/android/ui/chat/ChatSheetContent.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/ui/chat/ChatSheetContent.kt @@ -5,11 +5,15 @@ import android.net.Uri import android.util.Base64 import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -21,17 +25,28 @@ import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import ai.openclaw.android.MainViewModel +import ai.openclaw.android.chat.ChatSessionEntry import ai.openclaw.android.chat.OutgoingAttachment import ai.openclaw.android.ui.mobileAccent import ai.openclaw.android.ui.mobileBorder +import ai.openclaw.android.ui.mobileBorderStrong import ai.openclaw.android.ui.mobileCallout +import ai.openclaw.android.ui.mobileCaption1 import ai.openclaw.android.ui.mobileCaption2 import ai.openclaw.android.ui.mobileDanger +import ai.openclaw.android.ui.mobileSuccess +import ai.openclaw.android.ui.mobileSuccessSoft import ai.openclaw.android.ui.mobileText +import ai.openclaw.android.ui.mobileTextSecondary +import ai.openclaw.android.ui.mobileWarning +import ai.openclaw.android.ui.mobileWarningSoft import java.io.ByteArrayOutputStream import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -86,6 +101,14 @@ fun ChatSheetContent(viewModel: MainViewModel) { .padding(horizontal = 20.dp, vertical = 12.dp), verticalArrangement = Arrangement.spacedBy(8.dp), ) { + ChatThreadSelector( + sessionKey = sessionKey, + sessions = sessions, + mainSessionKey = mainSessionKey, + healthOk = healthOk, + onSelectSession = { key -> viewModel.switchChatSession(key) }, + ) + if (!errorText.isNullOrBlank()) { ChatErrorRail(errorText = errorText!!) } @@ -100,9 +123,6 @@ fun ChatSheetContent(viewModel: MainViewModel) { ) ChatComposer( - sessionKey = sessionKey, - sessions = sessions, - mainSessionKey = mainSessionKey, healthOk = healthOk, thinkingLevel = thinkingLevel, pendingRunCount = pendingRunCount, @@ -110,7 +130,6 @@ fun ChatSheetContent(viewModel: MainViewModel) { onPickImages = { pickImages.launch("image/*") }, onRemoveAttachment = { id -> attachments.removeAll { it.id == id } }, onSetThinkingLevel = { level -> viewModel.setChatThinkingLevel(level) }, - onSelectSession = { key -> viewModel.switchChatSession(key) }, onRefresh = { viewModel.refreshChat() viewModel.refreshChatSessions(limit = 200) @@ -133,6 +152,85 @@ fun ChatSheetContent(viewModel: MainViewModel) { } } +@Composable +private fun ChatThreadSelector( + sessionKey: String, + sessions: List, + mainSessionKey: String, + healthOk: Boolean, + onSelectSession: (String) -> Unit, +) { + val sessionOptions = resolveSessionChoices(sessionKey, sessions, mainSessionKey = mainSessionKey) + val currentSessionLabel = + friendlySessionName(sessionOptions.firstOrNull { it.key == sessionKey }?.displayName ?: sessionKey) + + Column(modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = androidx.compose.ui.Alignment.CenterVertically, + ) { + Text( + text = "SESSION", + style = mobileCaption1.copy(fontWeight = FontWeight.Bold, letterSpacing = 0.8.sp), + color = mobileTextSecondary, + ) + Row(horizontalArrangement = Arrangement.spacedBy(6.dp), verticalAlignment = androidx.compose.ui.Alignment.CenterVertically) { + Text( + text = currentSessionLabel, + style = mobileCallout.copy(fontWeight = FontWeight.SemiBold), + color = mobileText, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + ChatConnectionPill(healthOk = healthOk) + } + } + + Row( + modifier = Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + for (entry in sessionOptions) { + val active = entry.key == sessionKey + Surface( + onClick = { onSelectSession(entry.key) }, + shape = RoundedCornerShape(14.dp), + color = if (active) mobileAccent else Color.White, + border = BorderStroke(1.dp, if (active) Color(0xFF154CAD) else mobileBorderStrong), + tonalElevation = 0.dp, + shadowElevation = 0.dp, + ) { + Text( + text = friendlySessionName(entry.displayName ?: entry.key), + style = mobileCaption1.copy(fontWeight = if (active) FontWeight.Bold else FontWeight.SemiBold), + color = if (active) Color.White else mobileText, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp), + ) + } + } + } + } +} + +@Composable +private fun ChatConnectionPill(healthOk: Boolean) { + Surface( + shape = RoundedCornerShape(999.dp), + color = if (healthOk) mobileSuccessSoft else mobileWarningSoft, + border = BorderStroke(1.dp, if (healthOk) mobileSuccess.copy(alpha = 0.35f) else mobileWarning.copy(alpha = 0.35f)), + ) { + Text( + text = if (healthOk) "Connected" else "Offline", + style = mobileCaption1.copy(fontWeight = FontWeight.SemiBold), + color = if (healthOk) mobileSuccess else mobileWarning, + modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp), + ) + } +} + @Composable private fun ChatErrorRail(errorText: String) { Surface(