diff --git a/apps/android/app/src/main/java/com/steipete/clawdis/node/ui/chat/ChatComposer.kt b/apps/android/app/src/main/java/com/steipete/clawdis/node/ui/chat/ChatComposer.kt index da1760d64..c278a20f9 100644 --- a/apps/android/app/src/main/java/com/steipete/clawdis/node/ui/chat/ChatComposer.kt +++ b/apps/android/app/src/main/java/com/steipete/clawdis/node/ui/chat/ChatComposer.kt @@ -15,6 +15,7 @@ import androidx.compose.foundation.horizontalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowUpward import androidx.compose.material.icons.filled.AttachFile +import androidx.compose.material.icons.filled.FolderOpen import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Stop import androidx.compose.material3.ButtonDefaults @@ -41,6 +42,7 @@ import androidx.compose.ui.unit.dp @Composable fun ChatComposer( + sessionKey: String, healthOk: Boolean, thinkingLevel: String, pendingRunCount: Int, @@ -49,6 +51,7 @@ fun ChatComposer( onPickImages: () -> Unit, onRemoveAttachment: (id: String) -> Unit, onSetThinkingLevel: (level: String) -> Unit, + onShowSessions: () -> Unit, onRefresh: () -> Unit, onAbort: () -> Unit, onSend: (text: String) -> Unit, @@ -88,6 +91,10 @@ fun ChatComposer( Spacer(modifier = Modifier.weight(1f)) + FilledTonalIconButton(onClick = onShowSessions, modifier = Modifier.size(42.dp)) { + Icon(Icons.Default.FolderOpen, contentDescription = "Sessions") + } + FilledTonalIconButton(onClick = onRefresh, modifier = Modifier.size(42.dp)) { Icon(Icons.Default.Refresh, contentDescription = "Refresh") } @@ -111,6 +118,7 @@ fun ChatComposer( ) Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + ConnectionPill(sessionKey = sessionKey, healthOk = healthOk) Spacer(modifier = Modifier.weight(1f)) if (pendingRunCount > 0) { @@ -147,6 +155,32 @@ fun ChatComposer( } } +@Composable +private fun ConnectionPill(sessionKey: String, healthOk: Boolean) { + Surface( + shape = RoundedCornerShape(999.dp), + color = MaterialTheme.colorScheme.surfaceContainerHighest, + ) { + Row( + modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Surface( + modifier = Modifier.size(7.dp), + shape = androidx.compose.foundation.shape.CircleShape, + color = if (healthOk) Color(0xFF2ECC71) else Color(0xFFF39C12), + ) {} + Text(sessionKey, style = MaterialTheme.typography.labelSmall) + Text( + if (healthOk) "Connected" else "Connecting…", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } +} + @Composable private fun ThinkingMenuItem( value: String, diff --git a/apps/android/app/src/main/java/com/steipete/clawdis/node/ui/chat/ChatMessageListCard.kt b/apps/android/app/src/main/java/com/steipete/clawdis/node/ui/chat/ChatMessageListCard.kt index 3f0217897..c1fa737de 100644 --- a/apps/android/app/src/main/java/com/steipete/clawdis/node/ui/chat/ChatMessageListCard.kt +++ b/apps/android/app/src/main/java/com/steipete/clawdis/node/ui/chat/ChatMessageListCard.kt @@ -3,45 +3,32 @@ package com.steipete.clawdis.node.ui.chat import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowCircleDown -import androidx.compose.material.icons.filled.Refresh -import androidx.compose.material.icons.filled.FolderOpen import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults -import androidx.compose.material3.FilledTonalIconButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha -import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import com.steipete.clawdis.node.chat.ChatMessage import com.steipete.clawdis.node.chat.ChatPendingToolCall @Composable fun ChatMessageListCard( - sessionKey: String, - isBridgeConnected: Boolean, - healthOk: Boolean, messages: List, pendingRunCount: Int, pendingToolCalls: List, streamingAssistantText: String?, - onShowSessions: () -> Unit, - onRefresh: () -> Unit, modifier: Modifier = Modifier, ) { val listState = rememberLazyListState() @@ -70,7 +57,7 @@ fun ChatMessageListCard( modifier = Modifier.fillMaxSize(), state = listState, verticalArrangement = Arrangement.spacedBy(14.dp), - contentPadding = androidx.compose.foundation.layout.PaddingValues(top = 44.dp, bottom = 12.dp, start = 12.dp, end = 12.dp), + contentPadding = androidx.compose.foundation.layout.PaddingValues(top = 12.dp, bottom = 12.dp, start = 12.dp, end = 12.dp), ) { items(count = messages.size, key = { idx -> messages[idx].id }) { idx -> ChatMessageBubble(message = messages[idx]) @@ -96,15 +83,6 @@ fun ChatMessageListCard( } } - ChatStatusPill( - sessionKey = sessionKey, - isBridgeConnected = isBridgeConnected, - healthOk = healthOk, - onShowSessions = onShowSessions, - onRefresh = onRefresh, - modifier = Modifier.align(Alignment.TopStart).padding(10.dp), - ) - if (messages.isEmpty() && pendingRunCount == 0 && pendingToolCalls.isEmpty() && streamingAssistantText.isNullOrBlank()) { EmptyChatHint(modifier = Modifier.align(Alignment.Center)) } @@ -112,70 +90,6 @@ fun ChatMessageListCard( } } -@Composable -private fun ChatStatusPill( - sessionKey: String, - isBridgeConnected: Boolean, - healthOk: Boolean, - onShowSessions: () -> Unit, - onRefresh: () -> Unit, - modifier: Modifier = Modifier, -) { - val statusText = - when { - !isBridgeConnected -> "Offline" - healthOk -> "Connected" - else -> "Connecting…" - } - val statusColor = - when { - !isBridgeConnected -> Color(0xFF9E9E9E) - healthOk -> Color(0xFF2ECC71) - else -> Color(0xFFF39C12) - } - - Surface( - modifier = modifier, - shape = MaterialTheme.shapes.large, - color = MaterialTheme.colorScheme.surfaceContainerHighest.copy(alpha = 0.96f), - shadowElevation = 1.dp, - tonalElevation = 0.dp, - ) { - Row( - modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - Surface( - modifier = Modifier.size(7.dp), - shape = androidx.compose.foundation.shape.CircleShape, - color = statusColor, - ) {} - - Text( - text = sessionKey, - style = MaterialTheme.typography.labelMedium, - ) - Text( - text = statusText, - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.alpha(0.9f), - ) - - Spacer(modifier = Modifier.weight(1f)) - - FilledTonalIconButton(onClick = onShowSessions, modifier = Modifier.size(34.dp)) { - Icon(Icons.Default.FolderOpen, contentDescription = "Sessions") - } - - FilledTonalIconButton(onClick = onRefresh, modifier = Modifier.size(34.dp)) { - Icon(Icons.Default.Refresh, contentDescription = "Refresh") - } - } - } -} - @Composable private fun EmptyChatHint(modifier: Modifier = Modifier) { Row( diff --git a/apps/android/app/src/main/java/com/steipete/clawdis/node/ui/chat/ChatSheetContent.kt b/apps/android/app/src/main/java/com/steipete/clawdis/node/ui/chat/ChatSheetContent.kt index 4d26a3bd0..35372ddc7 100644 --- a/apps/android/app/src/main/java/com/steipete/clawdis/node/ui/chat/ChatSheetContent.kt +++ b/apps/android/app/src/main/java/com/steipete/clawdis/node/ui/chat/ChatSheetContent.kt @@ -33,7 +33,6 @@ fun ChatSheetContent(viewModel: MainViewModel) { val messages by viewModel.chatMessages.collectAsState() val errorText by viewModel.chatError.collectAsState() val pendingRunCount by viewModel.pendingRunCount.collectAsState() - val isBridgeConnected by viewModel.isConnected.collectAsState() val healthOk by viewModel.chatHealthOk.collectAsState() val sessionKey by viewModel.chatSessionKey.collectAsState() val thinkingLevel by viewModel.chatThinkingLevel.collectAsState() @@ -79,19 +78,15 @@ fun ChatSheetContent(viewModel: MainViewModel) { verticalArrangement = Arrangement.spacedBy(10.dp), ) { ChatMessageListCard( - sessionKey = sessionKey, - isBridgeConnected = isBridgeConnected, - healthOk = healthOk, messages = messages, pendingRunCount = pendingRunCount, pendingToolCalls = pendingToolCalls, streamingAssistantText = streamingAssistantText, - onShowSessions = { showSessions = true }, - onRefresh = { viewModel.refreshChat() }, modifier = Modifier.weight(1f, fill = true), ) ChatComposer( + sessionKey = sessionKey, healthOk = healthOk, thinkingLevel = thinkingLevel, pendingRunCount = pendingRunCount, @@ -100,6 +95,7 @@ fun ChatSheetContent(viewModel: MainViewModel) { onPickImages = { pickImages.launch("image/*") }, onRemoveAttachment = { id -> attachments.removeAll { it.id == id } }, onSetThinkingLevel = { level -> viewModel.setChatThinkingLevel(level) }, + onShowSessions = { showSessions = true }, onRefresh = { viewModel.refreshChat() }, onAbort = { viewModel.abortChat() }, onSend = { text ->