feat(group-chat): add ephemeral visual pass indicator when bot passes turn
- Display a subtle centered system pill '[⏩ {bot} passed]' when a bot passes - Style with Material icon (Icons.Filled.FastForward) and surface tokens - Keep pass indicators strictly ephemeral to avoid polluting LLM prompt context - Exclude from cross-device snapshot sync so it disappears on restart/rehydrate - Add English and Arabic localized string resources - Update GroupChatViewModelTest to verify pass indicator presence and LLM filtering Co-authored-by: Luna <luna.m57@agentmail.to>
This commit is contained in:
1 parent
9288883de4
commit
1126cf1477
6 files changed
+79
-12
No files matched your search
@@ -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<GroupChatToolCall> = 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.
|
||||
*/
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 })
|
||||
|
||||
Reference in new issue
Block a user