* 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 <hy4ri@users.noreply.github.com>
This commit is contained in:
1 parent
4e2fb05077
commit
c181d1d593
4 files changed
+50
No files matched your search
@@ -434,6 +434,23 @@ object HermesWsClient {
|
|||||||
onSent = onSent,
|
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 ─────────────────────────────────────────────────────────
|
// ── Internal ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private fun openSocket() {
|
private fun openSocket() {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ object WsMethods {
|
|||||||
const val SESSION_RESUME = "session.resume"
|
const val SESSION_RESUME = "session.resume"
|
||||||
const val SESSION_CREATE = "session.create"
|
const val SESSION_CREATE = "session.create"
|
||||||
const val SESSION_INTERRUPT = "session.interrupt"
|
const val SESSION_INTERRUPT = "session.interrupt"
|
||||||
|
const val SESSION_REDIRECT = "session.redirect"
|
||||||
const val SESSION_DELETE = "session.delete"
|
const val SESSION_DELETE = "session.delete"
|
||||||
const val SESSION_TITLE = "session.title"
|
const val SESSION_TITLE = "session.title"
|
||||||
const val SESSION_BRANCH = "session.branch"
|
const val SESSION_BRANCH = "session.branch"
|
||||||
|
|||||||
@@ -765,6 +765,8 @@ class ChatViewModel(
|
|||||||
val attachments = _uiState.value.pendingAttachments.toList()
|
val attachments = _uiState.value.pendingAttachments.toList()
|
||||||
clearAttachments()
|
clearAttachments()
|
||||||
|
|
||||||
|
val wasStreaming = _uiState.value.isAgentTyping
|
||||||
|
|
||||||
val userMessage =
|
val userMessage =
|
||||||
ChatMessage(
|
ChatMessage(
|
||||||
role = MessageRole.USER,
|
role = MessageRole.USER,
|
||||||
@@ -857,6 +859,18 @@ class ChatViewModel(
|
|||||||
if (text.isNotBlank()) "\n\n$text" else ""
|
if (text.isNotBlank()) "\n\n$text" else ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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(
|
wsClient.sendMessage(
|
||||||
agentSessionId,
|
agentSessionId,
|
||||||
fullText,
|
fullText,
|
||||||
@@ -864,6 +878,7 @@ class ChatViewModel(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Read and encode a `content://` or `file://` URI to Base64 via ContentResolver, avoiding large allocations. */
|
/** Read and encode a `content://` or `file://` URI to Base64 via ContentResolver, avoiding large allocations. */
|
||||||
private suspend fun readContentUriBase64(uriString: String): String? =
|
private suspend fun readContentUriBase64(uriString: String): String? =
|
||||||
|
|||||||
@@ -1117,6 +1117,23 @@ class ChatViewModelTest {
|
|||||||
verify { HermesWsClient.sendMessage(sessionId, "Hello Hermes", any()) }
|
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 ───────────────────────────────────────────────────────
|
// ── Session switch ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Reference in new issue
Block a user