diff --git a/app/src/main/java/com/m57/hermescontrol/ui/bots/group/GroupChatModels.kt b/app/src/main/java/com/m57/hermescontrol/ui/bots/group/GroupChatModels.kt index 269d2c80..95600291 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/bots/group/GroupChatModels.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/bots/group/GroupChatModels.kt @@ -32,9 +32,13 @@ data class GroupChatMessage( val isStreaming: Boolean = false, val thread: String? = null, val isSystem: Boolean = false, + val isPass: Boolean = false, val toolCalls: List = emptyList(), ) +const val STOPPED_SYSTEM_TEXT = "Discussion stopped" +const val CAPPED_SYSTEM_TEXT = "Discussion paused (turn limit reached) — reply to continue" + /** * Pure helper for mention extraction and responder resolution matching Desktop parity. */ diff --git a/app/src/main/java/com/m57/hermescontrol/ui/bots/group/GroupChatScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/bots/group/GroupChatScreen.kt index 40444b94..0bd552fa 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/bots/group/GroupChatScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/bots/group/GroupChatScreen.kt @@ -40,6 +40,7 @@ import androidx.compose.material.icons.filled.ContentCopy import androidx.compose.material.icons.filled.Description import androidx.compose.material.icons.filled.ExpandLess import androidx.compose.material.icons.filled.ExpandMore +import androidx.compose.material.icons.filled.FastForward import androidx.compose.material.icons.filled.Groups import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Settings @@ -845,21 +846,53 @@ private fun GroupMessageCard( } if (message.isSystem) { + val displayText = + when { + message.isPass -> { + val name = message.senderDisplayName.ifBlank { message.senderName } + stringResource(R.string.group_chat_bot_passed, name) + } + + message.text == STOPPED_SYSTEM_TEXT -> { + stringResource(R.string.group_chat_stopped_by_user) + } + + message.text == CAPPED_SYSTEM_TEXT -> { + stringResource(R.string.group_chat_capped_limit) + } + + else -> { + message.text + } + } Box( modifier = modifier.fillMaxWidth().padding(vertical = 4.dp), contentAlignment = Alignment.Center, ) { Surface( shape = RoundedCornerShape(12.dp), - color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f), - border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)), + color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = if (message.isPass) 0.5f else 0.6f), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.4f)), ) { - Text( - text = message.text, - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp), - ) + Row( + modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + if (message.isPass) { + Icon( + imageVector = Icons.Filled.FastForward, + contentDescription = null, + modifier = Modifier.size(12.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), + ) + } + Text( + text = displayText, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } } } } else if (message.isUser) { diff --git a/app/src/main/java/com/m57/hermescontrol/ui/bots/group/GroupChatViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/bots/group/GroupChatViewModel.kt index 191e33b6..e0f0e17a 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/bots/group/GroupChatViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/bots/group/GroupChatViewModel.kt @@ -59,8 +59,6 @@ const val WARN_MAX_CONTINUATION_PASSES = 6 private const val LEASE_STEP_TTL_MS = 45_000L private const val TURN_TIMEOUT_MS = 45_000L private const val HISTORY_LIMIT = 10 -private const val CAPPED_SYSTEM_TEXT = "Discussion paused (turn limit reached) — reply to continue" -private const val STOPPED_SYSTEM_TEXT = "Discussion stopped" class GroupChatViewModel( private var groupName: String = "", @@ -791,6 +789,28 @@ class GroupChatViewModel( state.copy(messages = updatedList) } return replyText.trim() + } else if (replyText != null && isPass(replyText)) { + _uiState.update { state -> + val existing = state.messages.find { it.id == streamMsgId } + val passMsg = + GroupChatMessage( + id = streamMsgId, + senderName = bot.name, + senderDisplayName = bot.effectiveTitle, + isUser = false, + isSystem = true, + isPass = true, + text = "${bot.effectiveTitle} passed", + ) + val updatedList = + if (existing != null) { + state.messages.map { if (it.id == streamMsgId) passMsg else it } + } else { + state.messages + passMsg + } + state.copy(messages = updatedList) + } + return null } else { _uiState.update { state -> state.copy(messages = state.messages.filter { it.id != streamMsgId }) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index e7448f31..2dce4f60 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -1168,6 +1168,7 @@ لا توجد رسائل بعد أشر إلى بوت أو استخدم @all لبدء التعاون. %1$s يفكّر الآن… +تخطى %1$s دوره جارٍ الكتابة… جارٍ التشغيل… لا يوجد مخرجات diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9b391dd1..51a64751 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1283,6 +1283,7 @@ No Messages Yet Mention a bot or use @all to kick off collaboration. %1$s is thinking… + %1$s passed Typing… Running… No output diff --git a/app/src/test/java/com/m57/hermescontrol/ui/bots/group/GroupChatViewModelTest.kt b/app/src/test/java/com/m57/hermescontrol/ui/bots/group/GroupChatViewModelTest.kt index f1a15178..8bad923e 100644 --- a/app/src/test/java/com/m57/hermescontrol/ui/bots/group/GroupChatViewModelTest.kt +++ b/app/src/test/java/com/m57/hermescontrol/ui/bots/group/GroupChatViewModelTest.kt @@ -266,9 +266,17 @@ class GroupChatViewModelTest { testScheduler.runCurrent() val finalState = viewModel.uiState.value - // Only the user message should remain; (pass) is filtered out - assertEquals(1, finalState.messages.size) + // User message plus the visual pass indicator + assertEquals(2, finalState.messages.size) assertTrue(finalState.messages.first().isUser) + val passMsg = finalState.messages.last() + assertTrue(passMsg.isSystem) + assertTrue(passMsg.isPass) + assertEquals("Scout Bot", passMsg.senderDisplayName) + assertEquals("Scout Bot passed", passMsg.text) + + // LLM prompt history only considers non-system messages + assertEquals(1, finalState.messages.filter { !it.isSystem }.size) } @Test