fix(chat): keep Send primary while streaming, Stop in the flat slot (#1412)
This commit is contained in:
2 files changed
+42
-15
No files matched your search
+26
-2
@@ -50,8 +50,8 @@ class ComposerReadinessTest {
|
|||||||
compose.runOnIdle { assertEquals(0, sends) }
|
compose.runOnIdle { assertEquals(0, sends) }
|
||||||
compose.runOnIdle { ready.value = true }
|
compose.runOnIdle { ready.value = true }
|
||||||
compose.onNodeWithTag("chat_session_preparing").assertDoesNotExist()
|
compose.onNodeWithTag("chat_session_preparing").assertDoesNotExist()
|
||||||
// Ready + typing = an interruptible generation: Stop replaces the
|
// Ready + typing = an interruptible generation: Send keeps the primary
|
||||||
// action glyph while queue-send stays available in the flat slot.
|
// action slot and Stop moves to the flat slot beside it.
|
||||||
compose.onNodeWithTag("stop_button").assertExists()
|
compose.onNodeWithTag("stop_button").assertExists()
|
||||||
compose.onNodeWithTag("send_button").assertIsEnabled().performClick()
|
compose.onNodeWithTag("send_button").assertIsEnabled().performClick()
|
||||||
compose.runOnIdle { assertEquals(1, sends) }
|
compose.runOnIdle { assertEquals(1, sends) }
|
||||||
@@ -60,4 +60,28 @@ class ComposerReadinessTest {
|
|||||||
compose.onNodeWithTag("send_button").assertIsNotEnabled()
|
compose.onNodeWithTag("send_button").assertIsNotEnabled()
|
||||||
compose.onNodeWithTag("chat_input").assertTextEquals("held draft")
|
compose.onNodeWithTag("chat_input").assertTextEquals("held draft")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun emptyDraftWhileStreamingMakesStopThePrimaryAction() {
|
||||||
|
compose.setContent {
|
||||||
|
var input by remember { mutableStateOf(TextFieldValue("")) }
|
||||||
|
ChatInputBar(
|
||||||
|
inputFieldValue = input,
|
||||||
|
onInputChange = { input = it },
|
||||||
|
onSend = {},
|
||||||
|
onMicTap = {},
|
||||||
|
isListening = false,
|
||||||
|
isAgentTyping = true,
|
||||||
|
canInterrupt = true,
|
||||||
|
isConnected = true,
|
||||||
|
commandCatalog = CommandCatalog(),
|
||||||
|
isSessionReady = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
compose.onNodeWithTag("stop_button").assertExists()
|
||||||
|
compose.onNodeWithTag("send_button").assertDoesNotExist()
|
||||||
|
compose.onNodeWithTag("chat_input").performTextInput("next")
|
||||||
|
compose.onNodeWithTag("send_button").assertExists()
|
||||||
|
compose.onNodeWithTag("stop_button").assertExists()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -501,11 +501,12 @@ fun ComposerToolbar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flat slot — mic while idle; while a generation can be interrupted it
|
// Flat slot — mic while idle. While a generation can be interrupted and a
|
||||||
// keeps queue-send available (the action button carries Stop). During
|
// draft exists, Stop moves here so the rightmost action button stays
|
||||||
// session preparation the mic stays here and the action button is a
|
// Send (desktop parity: a payload keeps Send primary mid-turn; an empty
|
||||||
// disabled send: Stop must not appear when there is nothing to
|
// draft makes Stop primary). During session preparation the mic stays
|
||||||
// interrupt (review, PR #1250).
|
// here and the action button is a disabled send: Stop must not appear
|
||||||
|
// when there is nothing to interrupt (review, PR #1250).
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = showSend,
|
visible = showSend,
|
||||||
enter = fadeIn() + scaleIn(initialScale = 0.8f),
|
enter = fadeIn() + scaleIn(initialScale = 0.8f),
|
||||||
@@ -513,17 +514,16 @@ fun ComposerToolbar(
|
|||||||
) {
|
) {
|
||||||
if (canInterrupt) {
|
if (canInterrupt) {
|
||||||
FilledIconButton(
|
FilledIconButton(
|
||||||
onClick = onSend,
|
onClick = onStopGeneration,
|
||||||
enabled = canSend,
|
|
||||||
colors = flatIconButtonColors(palette),
|
colors = flatIconButtonColors(palette),
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.size(ControlSize)
|
.size(ControlSize)
|
||||||
.testTag("send_button"),
|
.testTag("stop_button"),
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.AutoMirrored.Filled.Send,
|
imageVector = Icons.Default.Stop,
|
||||||
contentDescription = stringResource(R.string.chat_send_desc),
|
contentDescription = stringResource(R.string.chat_voice_stop_generating),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -560,6 +560,9 @@ fun ComposerToolbar(
|
|||||||
// While a recording is locked this button finishes the voice note, so
|
// While a recording is locked this button finishes the voice note, so
|
||||||
// it must stay enabled even when the draft alone would not send
|
// it must stay enabled even when the draft alone would not send
|
||||||
// (review, PR #1280).
|
// (review, PR #1280).
|
||||||
|
// Stop owns the action button only when there is no draft to send;
|
||||||
|
// with a draft it lives in the flat slot above.
|
||||||
|
val actionStops = canInterrupt && !showSend
|
||||||
val actionEnabled =
|
val actionEnabled =
|
||||||
if (isVoiceNoteLocked) {
|
if (isVoiceNoteLocked) {
|
||||||
true
|
true
|
||||||
@@ -588,7 +591,7 @@ fun ComposerToolbar(
|
|||||||
// (review, PR #1280).
|
// (review, PR #1280).
|
||||||
isVoiceNoteLocked -> onMicTap()
|
isVoiceNoteLocked -> onMicTap()
|
||||||
|
|
||||||
canInterrupt -> onStopGeneration()
|
actionStops -> onStopGeneration()
|
||||||
|
|
||||||
showSend -> onSend()
|
showSend -> onSend()
|
||||||
|
|
||||||
@@ -598,7 +601,7 @@ fun ComposerToolbar(
|
|||||||
).testTag(
|
).testTag(
|
||||||
when {
|
when {
|
||||||
isVoiceNoteLocked -> "voice_note_send_button"
|
isVoiceNoteLocked -> "voice_note_send_button"
|
||||||
canInterrupt -> "stop_button"
|
actionStops -> "stop_button"
|
||||||
showSend -> "send_button"
|
showSend -> "send_button"
|
||||||
isListening -> "mic_stop_button"
|
isListening -> "mic_stop_button"
|
||||||
else -> "mic_button"
|
else -> "mic_button"
|
||||||
@@ -610,7 +613,7 @@ fun ComposerToolbar(
|
|||||||
targetState =
|
targetState =
|
||||||
when {
|
when {
|
||||||
isVoiceNoteLocked -> ActionGlyph.SEND
|
isVoiceNoteLocked -> ActionGlyph.SEND
|
||||||
canInterrupt -> ActionGlyph.STOP
|
actionStops -> ActionGlyph.STOP
|
||||||
showSend -> ActionGlyph.SEND
|
showSend -> ActionGlyph.SEND
|
||||||
isListening -> ActionGlyph.STOP
|
isListening -> ActionGlyph.STOP
|
||||||
else -> ActionGlyph.VOICE
|
else -> ActionGlyph.VOICE
|
||||||
|
|||||||
Reference in new issue
Block a user