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:
M57andM57 authored and GitHub committed 2026-08-22 21:58:53 +03:00
1 parent 69578e7818
commit 7540f7228a
2 files changed
+42 -2

No files matched your search

@@ -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<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()
}
}