From bfef3613fa07da4039566da676d77c742869c0b3 Mon Sep 17 00:00:00 2001 From: M57 <41645758+Hy4ri@users.noreply.github.com> Date: Sun, 21 Jun 2026 19:04:54 +0300 Subject: [PATCH] fix(#240): parse session_id from params root (#242) * fix(#240): parse session_id from params root Allows correct session mapping when opening app via notification. * fix(#240): set MainActivity launchMode to singleTop Prevents activity destruction and WebSocket reconnection when tapping notifications. * fix(#240): save inline notification replies to Room database * fix(#240): refresh current session when app comes to foreground * fix(#240): use uppercase role string to match Mapper mapping expectations * style: format NotificationReplyReceiver.kt to satisfy ktlint --- app/src/main/AndroidManifest.xml | 1 + .../m57/hermescontrol/data/ws/EventParser.kt | 3 ++- .../notification/NotificationReplyReceiver.kt | 27 +++++++++++++++++++ .../m57/hermescontrol/ui/chat/ChatScreen.kt | 1 + .../hermescontrol/ui/chat/ChatViewModel.kt | 6 +++++ .../hermescontrol/data/ws/EventParserTest.kt | 23 ++++++++++++++++ 6 files changed, 60 insertions(+), 1 deletion(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index cad981d7..8b996e97 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -20,6 +20,7 @@ diff --git a/app/src/main/java/com/m57/hermescontrol/data/ws/EventParser.kt b/app/src/main/java/com/m57/hermescontrol/data/ws/EventParser.kt index ede5cbc0..eaebadbc 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/ws/EventParser.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/ws/EventParser.kt @@ -35,7 +35,8 @@ object EventParser { @Suppress("UNCHECKED_CAST") val payload = params["payload"] as? Map - val sessionId = payload?.get("session_id") as? String + // B7 (Jun 21 2026, kanban t_240): extract session_id from params first, fallback to payload + val sessionId = params["session_id"] as? String ?: payload?.get("session_id") as? String return when (eventType) { "gateway.ready" -> { diff --git a/app/src/main/java/com/m57/hermescontrol/notification/NotificationReplyReceiver.kt b/app/src/main/java/com/m57/hermescontrol/notification/NotificationReplyReceiver.kt index aca2807a..8a75e003 100644 --- a/app/src/main/java/com/m57/hermescontrol/notification/NotificationReplyReceiver.kt +++ b/app/src/main/java/com/m57/hermescontrol/notification/NotificationReplyReceiver.kt @@ -8,6 +8,7 @@ import androidx.core.app.NotificationCompat import androidx.core.app.RemoteInput import com.m57.hermescontrol.R import com.m57.hermescontrol.data.ws.HermesWsClient +import kotlinx.coroutines.launch class NotificationReplyReceiver : BroadcastReceiver() { companion object { @@ -26,6 +27,32 @@ class NotificationReplyReceiver : BroadcastReceiver() { if (!replyText.isNullOrBlank() && !sessionId.isNullOrBlank()) { HermesWsClient.sendMessage(sessionId, replyText) + val pendingResult = goAsync() + kotlinx.coroutines.CoroutineScope(kotlinx.coroutines.Dispatchers.IO).launch { + try { + val db = + com.m57.hermescontrol.data.local.HermesDatabase + .get(context) + val dao = db.chatMessageDao() + val entity = + com.m57.hermescontrol.data.local.ChatMessageEntity( + id = + java.util.UUID + .randomUUID() + .toString(), + sessionId = sessionId, + role = "USER", + content = replyText, + timestamp = System.currentTimeMillis(), + ) + dao.upsert(entity) + } catch (e: Exception) { + android.util.Log.e("NotificationReply", "Failed to save replied message to Room DB", e) + } finally { + pendingResult.finish() + } + } + val repliedNotification = NotificationCompat .Builder(context, ChatNotificationService.CHAT_CHANNEL_ID) diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatScreen.kt index 2421c58e..d3f678c4 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatScreen.kt @@ -169,6 +169,7 @@ fun ChatScreen( androidx.lifecycle.Lifecycle.Event.ON_START -> { NotificationHelper.setAppForeground(context, true) NotificationHelper.stop(context) + viewModel.refreshCurrentSession() } androidx.lifecycle.Lifecycle.Event.ON_STOP -> { diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatViewModel.kt index 8510a635..293e247f 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatViewModel.kt @@ -803,6 +803,12 @@ class ChatViewModel( } } + fun refreshCurrentSession() { + val sessionId = _uiState.value.currentSessionId ?: return + loadCachedMessages(sessionId) + loadSessionMessages(sessionId) + } + fun switchSession(sessionId: String) { if (sessionId == _uiState.value.currentSessionId) return diff --git a/app/src/test/java/com/m57/hermescontrol/data/ws/EventParserTest.kt b/app/src/test/java/com/m57/hermescontrol/data/ws/EventParserTest.kt index 65ef03de..0e55fa19 100644 --- a/app/src/test/java/com/m57/hermescontrol/data/ws/EventParserTest.kt +++ b/app/src/test/java/com/m57/hermescontrol/data/ws/EventParserTest.kt @@ -86,6 +86,29 @@ class EventParserTest { assertEquals("session-1", tokenEvent.sessionId) } + @Test + fun testParseMessageToken_withSessionIdInParams_returnsMessageTokenEvent() { + val response = + JsonRpcResponse( + jsonrpc = "2.0", + id = null, + result = null, + error = null, + method = "event", + params = + mapOf( + "type" to "message.token", + "session_id" to "session-params-1", + "payload" to mapOf("text" to "hello"), + ), + ) + val event = EventParser.parse(response) + assertTrue(event is WsEvent.MessageToken) + val tokenEvent = event as WsEvent.MessageToken + assertEquals("hello", tokenEvent.token) + assertEquals("session-params-1", tokenEvent.sessionId) + } + @Test fun testParseThinkingDelta_returnsThinkingDeltaEvent() { val response =