From 7540f7228a644892bcf1cab7da103abf605e8a11 Mon Sep 17 00:00:00 2001 From: M57 <41645758+Hy4ri@users.noreply.github.com> Date: Sat, 22 Aug 2026 21:58:53 +0300 Subject: [PATCH] fix(chat): show send button for attachment-only sends (#956) (#956) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The AnimatedContent gate on the composer's send button required inputFieldValue.text.isNotBlank() in addition to canSend, so attaching an image or file with an empty input hid the button entirely — the attachment could never be sent on its own. ChatInputPolicy.canSend already allowed attachment-only sends and ChatViewModel.sendMessage already accepted blank text with pending attachments, but the button never rendered to trigger it. Drop the stale text gate so the send affordance tracks canSend (non-empty text OR pending attachments, plus connection). The backend handles image-only turns natively: prompt.submit with empty text + queued images routes through build_native_content_parts, which wraps the pixels with an [Image attached at: ...] hint. Add a Compose UI regression test asserting the send button is displayed with a pending image attachment and an empty input. Co-authored-by: M57 --- .../hermescontrol/ui/chat/ChatScreenTest.kt | 37 +++++++++++++++++++ .../ui/chat/components/ChatComposer.kt | 7 +++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/app/src/androidTest/java/com/m57/hermescontrol/ui/chat/ChatScreenTest.kt b/app/src/androidTest/java/com/m57/hermescontrol/ui/chat/ChatScreenTest.kt index c0b7731b..505fd402 100644 --- a/app/src/androidTest/java/com/m57/hermescontrol/ui/chat/ChatScreenTest.kt +++ b/app/src/androidTest/java/com/m57/hermescontrol/ui/chat/ChatScreenTest.kt @@ -8,6 +8,7 @@ import androidx.compose.ui.test.performTextInput import androidx.core.content.ContextCompat import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.MediumTest +import com.m57.hermescontrol.data.model.Attachment import com.m57.hermescontrol.data.ws.ConnectionStatus import com.m57.hermescontrol.ui.common.ActionProgressController import io.mockk.every @@ -82,4 +83,40 @@ class ChatScreenTest { composeTestRule.onNodeWithTag("send_button").assertIsDisplayed() composeTestRule.onNodeWithTag("mic_button").assertIsDisplayed() } + + @Test + fun chatScreen_sendButtonShownForAttachmentWithEmptyInput() { + // Issue #956: an attachment queued with an EMPTY input must still show + // the send button — the old AnimatedContent gate required non-blank + // text even though ChatInputPolicy.canSend already allowed + // attachment-only sends. + val uiState = + ChatUiState( + connectionStatus = ConnectionStatus.CONNECTED, + pendingAttachments = + listOf( + Attachment( + uri = "content://test/photo.jpg", + name = "photo.jpg", + mimeType = "image/jpeg", + ), + ), + ) + val mockViewModel = mockk(relaxed = true) + every { mockViewModel.uiState } returns MutableStateFlow(uiState).asStateFlow() + every { mockViewModel.streamingState } returns MutableStateFlow(StreamingState()).asStateFlow() + every { mockViewModel.actionProgress } returns + ActionProgressController(scope = CoroutineScope(Dispatchers.Main)) + + composeTestRule.setContent { + ChatScreen( + onOpenDrawer = {}, + sessionId = null, + viewModel = mockViewModel, + ) + } + + composeTestRule.onNodeWithTag("chat_input").assertIsDisplayed() + composeTestRule.onNodeWithTag("send_button").assertIsDisplayed() + } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/components/ChatComposer.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/components/ChatComposer.kt index 6dcaf178..3e1fa8df 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/chat/components/ChatComposer.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/chat/components/ChatComposer.kt @@ -278,9 +278,12 @@ fun ChatInputBar( innerTextField() } - // Send button INSIDE the field + // Send button INSIDE the field. Shown whenever a send is + // possible — text typed OR an attachment pending (issue + // #956): the old text-only gate hid the button entirely + // for attachment-only sends. AnimatedContent( - targetState = canSend && inputFieldValue.text.isNotBlank(), + targetState = canSend, transitionSpec = { (scaleIn(initialScale = 0.8f) + fadeIn()) .togetherWith(scaleOut(targetScale = 0.8f) + fadeOut())