fix(chat): show send button for attachment-only sends (#956) (#956)

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 <hy4ri@users.noreply.github.com>
This commit is contained in:
M57
2026-08-22 21:58:53 +03:00
committed by GitHub
co-authored by M57
parent 69578e7818
commit 7540f7228a
2 changed files with 42 additions and 2 deletions
@@ -8,6 +8,7 @@ import androidx.compose.ui.test.performTextInput
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest import androidx.test.filters.MediumTest
import com.m57.hermescontrol.data.model.Attachment
import com.m57.hermescontrol.data.ws.ConnectionStatus import com.m57.hermescontrol.data.ws.ConnectionStatus
import com.m57.hermescontrol.ui.common.ActionProgressController import com.m57.hermescontrol.ui.common.ActionProgressController
import io.mockk.every import io.mockk.every
@@ -82,4 +83,40 @@ class ChatScreenTest {
composeTestRule.onNodeWithTag("send_button").assertIsDisplayed() composeTestRule.onNodeWithTag("send_button").assertIsDisplayed()
composeTestRule.onNodeWithTag("mic_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<ChatViewModel>(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()
}
} }
@@ -278,9 +278,12 @@ fun ChatInputBar(
innerTextField() 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( AnimatedContent(
targetState = canSend && inputFieldValue.text.isNotBlank(), targetState = canSend,
transitionSpec = { transitionSpec = {
(scaleIn(initialScale = 0.8f) + fadeIn()) (scaleIn(initialScale = 0.8f) + fadeIn())
.togetherWith(scaleOut(targetScale = 0.8f) + fadeOut()) .togetherWith(scaleOut(targetScale = 0.8f) + fadeOut())