fix(chat): resolve compilation errors and formatting violations

This commit is contained in:
Hy4ri committed 2026-06-20 11:12:34 +03:00
1 parent d0eedf053f
commit 5ba266cefd
3 files changed
+40 -26

No files matched your search

@@ -72,10 +72,10 @@ import com.m57.hermescontrol.theme.SystemMessageColor
import com.m57.hermescontrol.theme.ToolChipColor import com.m57.hermescontrol.theme.ToolChipColor
import com.m57.hermescontrol.theme.ToolChipColorLight import com.m57.hermescontrol.theme.ToolChipColorLight
import com.m57.hermescontrol.theme.UserBubble import com.m57.hermescontrol.theme.UserBubble
import kotlinx.coroutines.delay
import java.time.Instant import java.time.Instant
import java.time.ZoneId import java.time.ZoneId
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import kotlinx.coroutines.delay
@Composable @Composable
fun ChatBubble( fun ChatBubble(
@@ -811,7 +811,8 @@ private fun RichText(
) )
} }
private val TIMESTAMP_FORMATTER = DateTimeFormatter.ofPattern("HH:mm") private val TIMESTAMP_FORMATTER =
DateTimeFormatter.ofPattern("HH:mm")
.withZone(ZoneId.systemDefault()) .withZone(ZoneId.systemDefault())
private fun formatTimestamp(timestamp: Long): String { private fun formatTimestamp(timestamp: Long): String {
@@ -171,7 +171,8 @@ fun ChatScreen(
// Auto-scroll to bottom on new messages // Auto-scroll to bottom on new messages
LaunchedEffect(state.messages.size, state.streamingMessage?.content?.length, state.isThinking) { LaunchedEffect(state.messages.size, state.streamingMessage?.content?.length, state.isThinking) {
val totalItems = state.messages.size + val totalItems =
state.messages.size +
(if (state.streamingMessage != null) 1 else 0) + (if (state.streamingMessage != null) 1 else 0) +
(if (state.isThinking) 1 else 0) (if (state.isThinking) 1 else 0)
if (totalItems > 0) { if (totalItems > 0) {
@@ -354,7 +355,8 @@ fun ChatScreen(
.imePadding(), .imePadding(),
) { ) {
androidx.compose.animation.AnimatedVisibility( androidx.compose.animation.AnimatedVisibility(
visible = state.connectionStatus == ConnectionStatus.RECONNECTING || visible =
state.connectionStatus == ConnectionStatus.RECONNECTING ||
state.connectionStatus == ConnectionStatus.DISCONNECTED, state.connectionStatus == ConnectionStatus.DISCONNECTED,
enter = androidx.compose.animation.expandVertically() + androidx.compose.animation.fadeIn(), enter = androidx.compose.animation.expandVertically() + androidx.compose.animation.fadeIn(),
exit = androidx.compose.animation.shrinkVertically() + androidx.compose.animation.fadeOut(), exit = androidx.compose.animation.shrinkVertically() + androidx.compose.animation.fadeOut(),
@@ -365,14 +367,16 @@ fun ChatScreen(
contentColor = MaterialTheme.colorScheme.onErrorContainer, contentColor = MaterialTheme.colorScheme.onErrorContainer,
) { ) {
Row( Row(
modifier = Modifier modifier =
Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp), .padding(horizontal = 16.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
) { ) {
Text( Text(
text = if (state.connectionStatus == ConnectionStatus.RECONNECTING) { text =
if (state.connectionStatus == ConnectionStatus.RECONNECTING) {
"Reconnecting to Hermes…" "Reconnecting to Hermes…"
} else { } else {
"Disconnected from Hermes" "Disconnected from Hermes"
@@ -382,7 +386,8 @@ fun ChatScreen(
if (state.connectionStatus == ConnectionStatus.DISCONNECTED) { if (state.connectionStatus == ConnectionStatus.DISCONNECTED) {
TextButton( TextButton(
onClick = { viewModel.reconnect() }, onClick = { viewModel.reconnect() },
colors = androidx.compose.material3.ButtonDefaults.textButtonColors( colors =
androidx.compose.material3.ButtonDefaults.textButtonColors(
contentColor = MaterialTheme.colorScheme.onErrorContainer, contentColor = MaterialTheme.colorScheme.onErrorContainer,
), ),
) { ) {
@@ -254,13 +254,12 @@ class ChatViewModel(
private fun handleMessageComplete(event: WsEvent.MessageComplete) { private fun handleMessageComplete(event: WsEvent.MessageComplete) {
if (!isCurrentSession(event.sessionId)) return if (!isCurrentSession(event.sessionId)) return
// Finalize: move the streaming message into the main list var finalizedMsg: ChatMessage? = null
val finalizedMsg: ChatMessage var sessionId: String? = null
val sessionId: String?
_uiState.update { state -> _uiState.update { state ->
val streaming = state.streamingMessage val streaming = state.streamingMessage
finalizedMsg = if (streaming != null) { val msg = if (streaming != null) {
streaming.copy( streaming.copy(
content = event.text, content = event.text,
isStreaming = false, isStreaming = false,
@@ -271,9 +270,10 @@ class ChatViewModel(
content = event.text, content = event.text,
) )
} }
finalizedMsg = msg
sessionId = state.currentSessionId sessionId = state.currentSessionId
state.copy( state.copy(
messages = state.messages + finalizedMsg, messages = state.messages + msg,
streamingMessage = null, streamingMessage = null,
isAgentTyping = false, isAgentTyping = false,
isThinking = false, isThinking = false,
@@ -283,10 +283,11 @@ class ChatViewModel(
streamingMessageId = null streamingMessageId = null
// Persist finalized message — OUTSIDE update{} to avoid CAS retry // Persist finalized message — OUTSIDE update{} to avoid CAS retry
val msgToPersist = finalizedMsg
val sid = sessionId ?: _uiState.value.currentSessionId val sid = sessionId ?: _uiState.value.currentSessionId
if (sid != null) { if (msgToPersist != null && sid != null) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
dao.upsert(finalizedMsg.toEntity(sid)) dao.upsert(msgToPersist.toEntity(sid))
} }
} }
} }
@@ -296,16 +297,17 @@ class ChatViewModel(
// If we still have an un-finalized streaming message (no MessageComplete // If we still have an un-finalized streaming message (no MessageComplete
// was sent, which happens on interrupts), fold it into the list. // was sent, which happens on interrupts), fold it into the list.
val orphan: ChatMessage? var orphan: ChatMessage? = null
val sessionId: String? var sessionId: String? = null
_uiState.update { state -> _uiState.update { state ->
val streaming = state.streamingMessage val streaming = state.streamingMessage
orphan = streaming?.copy(isStreaming = false) val msg = streaming?.copy(isStreaming = false)
orphan = msg
sessionId = state.currentSessionId sessionId = state.currentSessionId
if (orphan != null) { if (msg != null) {
state.copy( state.copy(
messages = state.messages + orphan, messages = state.messages + msg,
streamingMessage = null, streamingMessage = null,
isAgentTyping = false, isAgentTyping = false,
isThinking = false, isThinking = false,
@@ -322,10 +324,11 @@ class ChatViewModel(
streamingMessageId = null streamingMessageId = null
// Persist orphaned streaming message // Persist orphaned streaming message
val msgToPersist = orphan
val sid = sessionId ?: _uiState.value.currentSessionId val sid = sessionId ?: _uiState.value.currentSessionId
if (orphan != null && sid != null) { if (msgToPersist != null && sid != null) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
dao.upsert(orphan.toEntity(sid)) dao.upsert(msgToPersist.toEntity(sid))
} }
} }
} }
@@ -358,13 +361,15 @@ class ChatViewModel(
_uiState.update { state -> _uiState.update { state ->
val messages = state.messages.toMutableList() val messages = state.messages.toMutableList()
val toolIdx = messages.indexOfLast { val toolIdx =
messages.indexOfLast {
it.role == MessageRole.TOOL && it.role == MessageRole.TOOL &&
it.toolName == event.name && it.toolName == event.name &&
it.toolStatus == ToolStatus.RUNNING it.toolStatus == ToolStatus.RUNNING
} }
if (toolIdx >= 0) { if (toolIdx >= 0) {
val updated = messages[toolIdx].copy( val updated =
messages[toolIdx].copy(
toolStatus = ToolStatus.COMPLETED, toolStatus = ToolStatus.COMPLETED,
content = event.data.toString(), content = event.data.toString(),
) )
@@ -494,7 +499,8 @@ class ChatViewModel(
return return
} }
val userMessage = ChatMessage( val userMessage =
ChatMessage(
role = MessageRole.USER, role = MessageRole.USER,
content = text, content = text,
) )
@@ -892,11 +898,13 @@ class ChatViewModel(
* responded to. Surfaces a timeout error for the user. * responded to. Surfaces a timeout error for the user.
*/ */
private fun startPendingRequestCleanup() { private fun startPendingRequestCleanup() {
pendingCleanupJob = viewModelScope.launch { pendingCleanupJob =
viewModelScope.launch {
while (true) { while (true) {
delay(PENDING_REQUEST_TIMEOUT_MS) delay(PENDING_REQUEST_TIMEOUT_MS)
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
val stale = pendingRequests.entries.filter { val stale =
pendingRequests.entries.filter {
now - it.value.createdAt > PENDING_REQUEST_TIMEOUT_MS now - it.value.createdAt > PENDING_REQUEST_TIMEOUT_MS
} }
for (entry in stale) { for (entry in stale) {