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,92 @@
package com.m57.hermescontrol.ui.chat
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
/**
* Issue #589-followup — Queue prompts while the agent is running.
*
* Asserts the input-bar decision contract the backend relies on: a prompt sent
* while the agent is typing or awaiting approval must be allowed (the gateway
* queues it via prompt.submit's busy-input policy), and the placeholder should
* signal "queued" when the user has text ready during a busy turn.
*/
class ChatInputPolicyTest {
@Test
fun canSend_allowsRegularPromptWhileAgentTyping() {
assertTrue(
"regular prompt must be sendable while agent is typing",
ChatInputPolicy.canSend("next task", emptyList(), isConnected = true),
)
}
@Test
fun canSend_allowsRegularPromptWhileApprovalPending() {
// Approval-pending surfaces as isAgentTyping=false in this client, but
// the gateway still queues the prompt.submit — connection + non-empty
// text are the only gates, so it must be allowed.
assertTrue(
"regular prompt must be sendable while an approval is pending",
ChatInputPolicy.canSend("yes, proceed", emptyList(), isConnected = true),
)
}
@Test
fun canSend_blocksWhenDisconnected() {
assertFalse(
"no sends while disconnected",
ChatInputPolicy.canSend("next task", emptyList(), isConnected = false),
)
}
@Test
fun canSend_blocksWhenInputEmptyAndNoAttachments() {
assertFalse(
"empty input with no attachments must not enable send",
ChatInputPolicy.canSend("", emptyList(), isConnected = true),
)
}
@Test
fun canSend_allowsAttachmentWithEmptyText() {
assertTrue(
"a pending attachment enables send even with empty text",
ChatInputPolicy.canSend("", listOf("att"), isConnected = true),
)
}
@Test
fun showQueuePlaceholder_trueWhenTypingAndHasText() {
assertTrue(
"placeholder should read 'queued' while typing with text present",
ChatInputPolicy.showQueuePlaceholder("next task", isAgentTyping = true),
)
}
@Test
fun showQueuePlaceholder_falseWhenTypingButNoText() {
assertFalse(
"plain 'waiting' hint when typing but input empty",
ChatInputPolicy.showQueuePlaceholder("", isAgentTyping = true),
)
}
@Test
fun showQueuePlaceholder_falseWhenIdle() {
assertFalse(
"no 'queued' hint when the agent is idle",
ChatInputPolicy.showQueuePlaceholder("next task", isAgentTyping = false),
)
}
@Test
fun slashCommandStillRoutesThroughSendPath() {
// Slash commands were already allowed mid-turn; ensure the policy does
// not regress that by blocking a non-blank slash command.
assertTrue(
"/stop must remain sendable while typing",
ChatInputPolicy.canSend("/stop", emptyList(), isConnected = true),
)
}
}