feat: queue prompts while the agent is running (#598)

Allow the chat input to be sent while the agent is mid-turn or awaiting
approval. The backend's prompt.submit busy-input policy already queues any
prompt that lands during a running turn (tui_gateway/server.py:_handle_busy_submit)
and drains it as the next turn, so the message is never dropped.

- ChatInputPolicy: pure, testable send/placeholder decision logic
- ChatScreen: canSend stays enabled while typing/approving; placeholder shows
  'Queued — sends when agent is free' when text is present during a busy turn
- ChatInputPolicyTest: truth-table covering typing, approval-pending,
  disconnected, empty, attachment, and slash-command cases
- strings: chat_input_placeholder_queue

Note: this supersedes the Genesi599 fork's approach (dispatchViaRpc('queue ...')),
which double-submits and relies on a 4018-prone command.dispatch path. The
backend-queued prompt.submit path is the correct, single-submission contract.

Co-authored-by: M57 <m57@hermes-control.app>
This commit is contained in:
M57andM57 authored and GitHub committed 2026-07-15 01:46:29 +03:00
1 parent d1fc477bcd
commit 5376d46683
4 files changed
+135 -4

No files matched your search

@@ -0,0 +1,35 @@
package com.m57.hermescontrol.ui.chat
/**
* Pure, testable decision logic for the chat input bar.
*
* The gateway's `prompt.submit` busy-input policy queues any prompt that lands
* while the agent is mid-turn (tui_gateway/server.py:_handle_busy_submit), so a
* message sent while the agent is typing or awaiting approval is never dropped —
* it runs as the next turn. These helpers keep that contract explicit and
* unit-testable without a Compose/Android harness.
*/
object ChatInputPolicy {
/**
* Whether the send affordance should be enabled.
*
* Unlike slash commands (which were always allowed mid-turn), regular
* prompts are now allowed too: the backend queues them. The only gates are
* a non-empty input (or a pending attachment) and an active connection.
*/
fun canSend(
text: String,
pendingAttachments: List<Any>,
isConnected: Boolean,
): Boolean = (text.isNotBlank() || pendingAttachments.isNotEmpty()) && isConnected
/**
* Whether the input placeholder should read "queued" rather than the plain
* "waiting" hint. Shown only while the agent is typing AND the user has
* already typed something — i.e. a press of send would enqueue a turn.
*/
fun showQueuePlaceholder(
text: String,
isAgentTyping: Boolean,
): Boolean = isAgentTyping && text.isNotBlank()
}
@@ -700,11 +700,12 @@ private fun ChatInputBar(
onFileTap: () -> Unit = {},
onRemoveAttachment: (Int) -> Unit = {},
) {
// Allow sending slash commands even while agent is typing
// Allow sending while the agent is mid-turn or awaiting approval: the
// gateway's prompt.submit busy-input policy queues it as the next turn
// (tui_gateway/server.py:_handle_busy_submit), so the message is never
// dropped. Slash commands were already allowed; regular prompts now are too.
val isSlashCommand = inputText.startsWith("/")
val canSend =
(inputText.isNotBlank() || pendingAttachments.isNotEmpty()) &&
isConnected && (!isAgentTyping || isSlashCommand)
val canSend = ChatInputPolicy.canSend(inputText, pendingAttachments, isConnected)
// Attachment menu state
var showAttachmentMenu by remember { mutableStateOf(false) }
@@ -916,6 +917,8 @@ private fun ChatInputBar(
Text(
if (!isConnected) {
stringResource(R.string.chat_input_placeholder_not_connected)
} else if (ChatInputPolicy.showQueuePlaceholder(inputText, isAgentTyping)) {
stringResource(R.string.chat_input_placeholder_queue)
} else if (isAgentTyping) {
stringResource(R.string.chat_input_placeholder_waiting)
} else {