From c181d1d593d025891445e007596cef9097c2a57e Mon Sep 17 00:00:00 2001 From: M57 <41645758+Hy4ri@users.noreply.github.com> Date: Sat, 25 Jul 2026 18:15:39 +0300 Subject: [PATCH] Adopt session.redirect (issue #710) (#714) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add session.redirect affordance (issue #710) Route send through session.redirect while a turn is streaming and the prompt is plain text (no attachments, not a /command); otherwise prompt.submit. Adds WsMethods.SESSION_REDIRECT + HermesWsClient.sendRedirect. No separate button — the composer already sends mid-turn, so send just does the redirect. * fix(chat): capture typing state before sendMessage for redirect Capture wasStreaming state before updating uiState.isAgentTyping so normal sends call prompt.submit. --------- Co-authored-by: M57 --- .../hermescontrol/data/ws/HermesWsClient.kt | 17 +++++++++++++ .../m57/hermescontrol/data/ws/WsMethods.kt | 1 + .../hermescontrol/ui/chat/ChatViewModel.kt | 25 +++++++++++++++---- .../ui/chat/ChatViewModelTest.kt | 17 +++++++++++++ 4 files changed, 55 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/m57/hermescontrol/data/ws/HermesWsClient.kt b/app/src/main/java/com/m57/hermescontrol/data/ws/HermesWsClient.kt index c2a4ff0e..3de67808 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/ws/HermesWsClient.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/ws/HermesWsClient.kt @@ -434,6 +434,23 @@ object HermesWsClient { onSent = onSent, ) + /** + * Convenience: redirect the active model turn while it is still generating + * (backend `session.redirect`). Fire-and-forget — the backend either rewrites + * the live turn, queues the correction as the next turn, or rejects it (the + * caller's ViewModel handles the `4010` fall-back to [sendMessage]). + */ + fun sendRedirect( + sessionId: String, + text: String, + onSent: ((String) -> Unit)? = null, + ): String = + send( + method = WsMethods.SESSION_REDIRECT, + params = mapOf("session_id" to sessionId, "text" to text), + onSent = onSent, + ) + // ── Internal ───────────────────────────────────────────────────────── private fun openSocket() { diff --git a/app/src/main/java/com/m57/hermescontrol/data/ws/WsMethods.kt b/app/src/main/java/com/m57/hermescontrol/data/ws/WsMethods.kt index 1327d224..e8e3a1fe 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/ws/WsMethods.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/ws/WsMethods.kt @@ -10,6 +10,7 @@ object WsMethods { const val SESSION_RESUME = "session.resume" const val SESSION_CREATE = "session.create" const val SESSION_INTERRUPT = "session.interrupt" + const val SESSION_REDIRECT = "session.redirect" const val SESSION_DELETE = "session.delete" const val SESSION_TITLE = "session.title" const val SESSION_BRANCH = "session.branch" 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 cfac70a0..733b6267 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 @@ -765,6 +765,8 @@ class ChatViewModel( val attachments = _uiState.value.pendingAttachments.toList() clearAttachments() + val wasStreaming = _uiState.value.isAgentTyping + val userMessage = ChatMessage( role = MessageRole.USER, @@ -857,11 +859,24 @@ class ChatViewModel( if (text.isNotBlank()) "\n\n$text" else "" } - wsClient.sendMessage( - agentSessionId, - fullText, - onSent = { id -> trackRequest(id, WsMethods.PROMPT_SUBMIT) }, - ) + // While a turn is actively streaming and this is a plain text prompt + // (no attachments — session.redirect carries text only), steer the + // in-flight turn via session.redirect instead of queueing a fresh + // prompt.submit. The backend rewrites the live turn when it can, or + // queues the correction as the next turn otherwise (issue #710). + if (wasStreaming && attachments.isEmpty()) { + wsClient.sendRedirect( + agentSessionId, + fullText, + onSent = { id -> trackRequest(id, WsMethods.SESSION_REDIRECT) }, + ) + } else { + wsClient.sendMessage( + agentSessionId, + fullText, + onSent = { id -> trackRequest(id, WsMethods.PROMPT_SUBMIT) }, + ) + } } } diff --git a/app/src/test/java/com/m57/hermescontrol/ui/chat/ChatViewModelTest.kt b/app/src/test/java/com/m57/hermescontrol/ui/chat/ChatViewModelTest.kt index bfa4caa9..5c014a86 100644 --- a/app/src/test/java/com/m57/hermescontrol/ui/chat/ChatViewModelTest.kt +++ b/app/src/test/java/com/m57/hermescontrol/ui/chat/ChatViewModelTest.kt @@ -1117,6 +1117,23 @@ class ChatViewModelTest { verify { HermesWsClient.sendMessage(sessionId, "Hello Hermes", any()) } } + @Test + fun testSendMessageRedirectWhenStreaming() = + runTest { + val (viewModel, sessionId) = createViewModelWithSession() + + // First send starts streaming + viewModel.sendMessage("Hello Hermes") + advanceUntilIdle() + assertTrue(viewModel.uiState.value.isAgentTyping) + + // Second send while streaming triggers redirect + viewModel.sendMessage("Wait, correction") + advanceUntilIdle() + + verify { HermesWsClient.sendRedirect(sessionId, "Wait, correction", any()) } + } + // ── Session switch ─────────────────────────────────────────────────────── @Test