Skip to content

Support non-destructive command mode and command availability UX#6378

Open
andremion wants to merge 28 commits intodevelopfrom
redesign/ds-030-command-mode-stash
Open

Support non-destructive command mode and command availability UX#6378
andremion wants to merge 28 commits intodevelopfrom
redesign/ds-030-command-mode-stash

Conversation

@andremion
Copy link
Copy Markdown
Contributor

@andremion andremion commented Apr 17, 2026

Goal

As per https://getstream.slack.com/archives/C0A3JU6C6G3/p1776782919458249

  1. Non-destructive command mode. Entering a slash command stashes the pre-command text/attachments/recording/mentions; cancel restores them, send discards them.
  2. Availability UX. Unavailable commands (all in edit, moderation_set in reply) are dimmed and sorted last; tapping one shows an Error snackbar and keeps the attachment picker open. Typing / in edit is blocked.
  3. Conflict handling. Swiping to reply / entering edit while a command is active is blocked with a "Cancel command to …" snackbar. /name + space auto-enters command mode. Send-time refuses typed/pasted commands that conflict with the current action.

Implementation

ui-commonMessageComposerController gains events: SharedFlow<MessageComposerViewEvent>, a private CommandStash, stash/restore on selectCommand / clearActiveCommand / clearData, and an action-change hook that re-derives suggestions. New Command.isAvailableFor(MessageAction?) + List<Command>.sortedByAvailability(…) (@InternalStreamChatApi). New non-sealed public MessageComposerViewEvent with CommandUnavailable and CancelCommandRequired variants.

performMessageAction emits CancelCommandRequired and returns early when the new action conflicts with the active command. sendMessage runs a text-level guard (InlineCommandPattern) for typed/pasted /moderation … in reply mode. handleCommandSuggestions auto-selects on ^/([a-z]+) $; the stash discriminator treats that form as a pure trigger so cancel doesn't re-auto-select.

composeMessageComposerViewModel forwards events; the VM-bound MessageComposer collects them into a hoistable SnackbarHostState (Error variant). SnackbarPopup now anchors to the composer Row. Suggestion items gain enabled (0.5 alpha). AttachmentCommandPicker renders empty state in edit; AttachmentPickerActions stays open on unavailable taps. StreamSnackbar gains Default/Error/Success/Loading variants; validation snackbars migrated to Error. Toast-based CommandSelectionRouting deleted.

Translations — 5 new keys × 7 locales: command_unavailable_in_{edit,reply}, commands_unavailable_in_edit, cancel_command_to_{edit,reply}.

API — Purely additive (new public types, trailing-defaulted params). apiDump refreshed.

Tests — Controller tests cover stash transitions, edit/reply guards, re-sort, event emission, action blocking, auto-select, chip cancel after auto-select, send-time guard. CommandAvailabilityTest for classification. Paparazzi snapshots refreshed.

🎨 UI Changes

Non-destructive command mode Commands popup Commands picker
Screen_recording_20260423_162543.webm
Screen_recording_20260422_160351.webm
Screen_recording_20260422_160323.webm

Testing

Compose sample app, channel with /giphy (fun_set) and /mute (moderation_set). activeCommandEnabled = true (Compose default).

  1. Stash — text + attachment. Type hello + attach, pick /giphy, cancel → restored.
  2. Stash — in-command text discarded. Type draft, pick /giphy, type cat, cancel → draft.
  3. Stash — voice recording. Record, type /, pick /giphy (recording hidden), cancel → recording back.
  4. Send resets. draft + attach → pick /giphy → type → send → composer empty.
  5. Edit — / blocked. Edit a message, type / → no popup, Error snackbar "Not available while editing".
  6. Reply — moderation dimmed/sorted last. Swipe reply, type / → moderation commands at bottom, dimmed. Tap /mute → Error snackbar. Tap /giphy → activates.
  7. Re-sort on action change. Type / (popup open), swipe reply → popup re-orders.
  8. Attachment picker — empty in edit. Edit → + → Commands → empty state.
  9. Attachment picker — stays open. Reply → + → Commands, tap /mute → picker stays, snackbar fires.
  10. Conflict — block action. Pick /mute, swipe reply → reply not applied, snackbar "Cancel command to reply". Same for Edit.
  11. Conflict — compatible command passes. Pick /giphy, swipe reply → reply applies, /giphy stays active.
  12. Auto-enter. Type /mute (trailing space) → chip appears, input clears. Tap × → chip removed, input empty (not restored to /mute ).
  13. Auto-enter gated. In XML sample (activeCommandEnabled = false) type /giphy → no chip.
  14. Send-time guard. Reply mode, paste /mute @someone, send → not sent, Error snackbar "Not available while replying".

Summary by CodeRabbit

  • New Features

    • Commands are now context-aware, becoming unavailable in edit and reply modes with visual dimming.
    • Enhanced snackbar notifications with improved error, success, and loading visual styling.
  • Tests

    • Added visual tests for command picker and suggestions in different composer modes.

@andremion andremion added the pr:improvement Improvement label Apr 17, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 17, 2026

PR checklist ✅

All required conditions are satisfied:

  • Title length is OK (or ignored by label).
  • At least one pr: label exists.
  • Sections ### Goal, ### Implementation, and ### Testing are filled.

🎉 Great job! This PR is ready for review.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 17, 2026

SDK Size Comparison 📏

SDK Before After Difference Status
stream-chat-android-client 5.82 MB 5.82 MB 0.00 MB 🟢
stream-chat-android-ui-components 11.02 MB 11.02 MB 0.00 MB 🟢
stream-chat-android-compose 12.33 MB 12.35 MB 0.02 MB 🟢

@andremion andremion force-pushed the redesign/ds-030-command-mode-stash branch from c079cca to eb8d70c Compare April 21, 2026 13:52
@andremion
Copy link
Copy Markdown
Contributor Author

@CodeRabbit review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 21, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 21, 2026

Walkthrough

Threads composer MessageAction (Edit/Reply) through attachment pickers and command suggestion UI; adds command availability gating and visual dimming, command stashing/restoration in the composer controller, an events SharedFlow for one-shot view events, snackbar variants/visuals, and localized strings and tests for the new behaviors.

Changes

Cohort / File(s) Summary
API Surface
api/stream-chat-android-compose.api, api/stream-chat-android-ui-common.api
Added/updated public API: MessageAction threaded through picker/params, SnackbarHostState on composer overload, new MessageComposerViewEvent types, and events: SharedFlow on MessageComposerViewModel.
Attachment Picker
src/.../ui/messages/attachments/AttachmentPicker.kt, AttachmentPickerContent.kt, AttachmentSystemPicker.kt, AttachmentCommandPicker.kt, AttachmentPickerMenu.kt, AttachmentPickerActions.kt
Propagate optional messageAction into picker stack; sort/compute command availability via isAvailableFor; visually dim unavailable commands; adjust selection flow to call selectCommand before conditional picker close; add edit/reply preview composables.
Message Composer UI
src/.../ui/messages/composer/MessageComposer.kt, .../internal/suggestions/CommandSuggestionList.kt, CommandSuggestionItem.kt
Composer overload accepts/forwards SnackbarHostState; suggestions receive messageAction; items support enabled visual alpha; VM-bound composer collects events to show snackbars; validation errors use snackbar visuals for some cases.
ViewModel / Controller / Events
ui-common/.../MessageComposerController.kt, compose/.../viewmodel/messages/MessageComposerViewModel.kt, ui-common/.../MessageComposerViewEvent.kt
Controller: adds events one-shot flow, command stashing/restoration, availability gating emitting CommandUnavailable/CancelCommandRequired; ViewModel exposes events: SharedFlow. New MessageComposerViewEvent interface and subtypes added.
Command Availability Utilities & Tests
ui-common/.../CommandAvailability.kt, CommandAvailabilityTest.kt
New Command.isAvailableFor(action?) and List<Command>.sortedByAvailability(action?) to centralize availability logic; unit tests added.
Snackbar Visuals & Tests
src/.../ui/util/StreamSnackbar.kt, StreamSnackbarTest.kt
Introduced StreamSnackbarVisuals and StreamSnackbarVariant (Default, Error, Success, Loading); rendering includes variant-specific leading content; previews and snapshot tests updated/added.
Component Factory & Params
ui/theme/ChatComponentFactory.kt, ChatComponentFactoryParams.kt
Factory params gain messageAction and snackbarHostState properties; factory forwards these through composer and picker calls; command suggestion params include messageAction.
Localization
src/main/res/values/strings.xml, res/values-{es,fr,hi,in,it,ja,ko}/strings.xml
Added new composer command-related string resources (single/plural, cancel prompts) across multiple locales.
UI Tests / Snapshots / E2E helpers
src/test/.../AttachmentCommandPickerTest.kt, CommandSuggestionListTest.kt, MessageComposerScreenTest.kt, StreamSnackbarTest.kt, e2e/.../Wait.kt, compose-sample/.../robots/*.kt
Added/edit snapshot tests for edit/reply modes; updated test helpers and waitForText signature/behavior; expanded controller tests heavily to cover stashing, restoration, and availability scenarios.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant MessageComposer
    participant CommandSuggestionList
    participant MessageComposerController
    participant MessageComposerViewModel
    participant SnackbarHostState

    User->>MessageComposer: Tap command (in Edit mode)
    MessageComposer->>CommandSuggestionList: Render with messageAction=Edit
    CommandSuggestionList->>CommandSuggestionList: Check isAvailableFor(Edit)
    CommandSuggestionList->>User: Show dimmed command (alpha)
    User->>CommandSuggestionList: Tap disabled command
    CommandSuggestionList->>MessageComposerController: selectCommand(unavailableCmd)
    MessageComposerController->>MessageComposerController: Detect unavailable for Edit
    MessageComposerController->>MessageComposerViewModel: Emit CommandUnavailable(Edit)
    MessageComposerViewModel->>SnackbarHostState: Show snackbar (variant=Error)
    SnackbarHostState->>User: Display "Not available while editing"
Loading
sequenceDiagram
    participant User
    participant MessageComposer
    participant AttachmentPicker
    participant MessageComposerController
    participant CommandStash

    User->>MessageComposer: Open attachment picker (Reply mode)
    MessageComposer->>AttachmentPicker: Open with messageAction=Reply
    AttachmentPicker->>AttachmentPicker: sortedByAvailability(Reply)
    AttachmentPicker->>User: Show available/disabled commands
    User->>AttachmentPicker: Select command
    AttachmentPicker->>MessageComposerController: selectCommand(cmd)
    rect rgba(100, 150, 200, 0.5)
        MessageComposerController->>CommandStash: Capture pre-command input/attachments/mentions
    end
    MessageComposerController->>MessageComposerController: Set activeCommand
    MessageComposerController->>AttachmentPicker: closePicker()
    User->>MessageComposer: Clear command
    MessageComposerController->>CommandStash: Restore stashed state
    CommandStash->>MessageComposerController: Provide restored input/attachments/mentions
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

"I hopped through code with floppy ears and cheer,
Commands now know when editing or replying is near.
Snackbars with colors—error, success, and loading light,
Stashed drafts returned—everything snug and right.
—🐇"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.17% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the primary changes: support for non-destructive command mode and command availability UX, which are the two main features of this PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The pull request description is comprehensive and well-structured, covering goal, implementation, UI changes with videos, and detailed testing scenarios.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch redesign/ds-030-command-mode-stash

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentSystemPicker.kt (1)

198-202: ⚠️ Potential issue | 🟡 Minor

Keep the command sheet open for unavailable command taps.

Line 199 dismisses the modal before routing can reject edit/reply-unavailable commands, so a disabled tap shows feedback but also closes the command picker. Gate the dismissal on availability while still firing actions.onCommandSelected(command) for the integrator contract.

Suggested adjustment
+import io.getstream.chat.android.compose.ui.messages.composer.internal.isAvailableFor
+
 ...
                     onCommandSelected = { command ->
-                        showCommandsPickerDialog = false
+                        if (command.isAvailableFor(messageAction)) {
+                            showCommandsPickerDialog = false
+                        }
                         actions.onCommandSelected(command)
                     },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentSystemPicker.kt`
around lines 198 - 202, Call actions.onCommandSelected(command) first and only
set showCommandsPickerDialog = false when the command was actually accepted;
specifically, invoke actions.onCommandSelected(command) before touching
showCommandsPickerDialog and gate dismissal on either the boolean return value
from actions.onCommandSelected (if you change its signature to return true when
accepted) or on a concrete availability flag on the command (e.g.,
command.isAvailable / command.available) — keep messageAction untouched but use
onCommandSelected’s result or command availability to decide whether to set
showCommandsPickerDialog = false.
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentPicker.kt (1)

68-80: ⚠️ Potential issue | 🟠 Major

Do not expose messageAction as a public parameter in AttachmentPicker.

The parameter on line 80 is exposed in the public API signature, adding new surface area that conflicts with the PR objective of no public API additions. Although the default value null maintains backward compatibility at call sites, the signature change affects binary compatibility for compiled consumers. Move the action-aware implementation to an internal composable and preserve the public signature by removing this parameter:

Suggested refactor
 `@Suppress`("LongMethod")
 `@Composable`
 public fun AttachmentPicker(
     attachmentsPickerViewModel: AttachmentsPickerViewModel,
     modifier: Modifier = Modifier,
     messageMode: MessageMode = MessageMode.Normal,
     actions: AttachmentPickerActions = remember(attachmentsPickerViewModel) {
         AttachmentPickerActions.pickerDefaults(attachmentsPickerViewModel)
     },
-    messageAction: MessageAction? = null,
+) {
+    AttachmentPickerInternal(
+        attachmentsPickerViewModel = attachmentsPickerViewModel,
+        modifier = modifier,
+        messageMode = messageMode,
+        actions = actions,
+        messageAction = null,
+    )
+}
+
+@Suppress("LongMethod")
+@Composable
+internal fun AttachmentPickerInternal(
+    attachmentsPickerViewModel: AttachmentsPickerViewModel,
+    modifier: Modifier = Modifier,
+    messageMode: MessageMode = MessageMode.Normal,
+    actions: AttachmentPickerActions = AttachmentPickerActions.pickerDefaults(attachmentsPickerViewModel),
+    messageAction: MessageAction? = null,
 ) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentPicker.kt`
around lines 68 - 80, The public AttachmentPicker composable currently exposes
the messageAction parameter (AttachmentPicker(..., messageAction: MessageAction?
= null)), which expands the public API; remove this parameter from the public
signature and restore the original binary-compatible public API by creating an
internal implementation that accepts messageAction and having the public
AttachmentPicker call that internal version with messageAction = null.
Concretely, add an internal composable (e.g., AttachmentPickerInternal or
AttachmentPickerWithAction) that takes the same parameters plus messageAction:
MessageAction?, move the action-aware logic into it, and change the public
AttachmentPicker to the simpler signature (no messageAction) that delegates to
the internal composable passing messageAction = null; keep symbols like
AttachmentsPickerViewModel, AttachmentPickerActions.pickerDefaults, MessageMode
and the remember(...) usage unchanged.
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/MessageComposer.kt (1)

192-194: ⚠️ Potential issue | 🟡 Minor

Mirror the command availability contract on the stateless overload.

This public overload also receives every command tap, including visually unavailable commands, but its KDoc does not mention that callers must filter and surface feedback themselves.

📝 Proposed KDoc update
- * `@param` onCommandSelected Handler when the user taps on a command suggestion item.
+ * `@param` onCommandSelected Handler for every tap on a command suggestion item, regardless of the
+ * command's availability in the current composer action. This stateless overload does not filter
+ * unavailable commands or show feedback, so callers are responsible for both branches.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/MessageComposer.kt`
around lines 192 - 194, Update the public stateless overload's KDoc for
MessageComposer to mirror the command availability contract: state clearly that
the onCommandSelected handler will receive every command tap (including commands
that are visually marked unavailable) and that callers are responsible for
filtering availability and providing appropriate user feedback; reference the
onCommandSelected parameter in the KDoc so consumers of the stateless overload
understand they must check command availability and handle UI feedback
themselves.
🧹 Nitpick comments (2)
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentPickerMenu.kt (1)

101-102: Include context in the remembered actions keys.

onCommandSelected captures LocalContext.current, but the remember keys do not include it. Add context to avoid stale Toast context if the composition local changes.

♻️ Proposed tweak
-    val actions = remember(attachmentsPickerViewModel, composerViewModel, composerState.action) {
+    val actions = remember(attachmentsPickerViewModel, composerViewModel, composerState.action, context) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentPickerMenu.kt`
around lines 101 - 102, The remember call that builds actions should include the
LocalContext.current (context) in its key list to avoid capturing a stale
Context used by onCommandSelected (which shows a Toast); update the remember
invocation that currently lists attachmentsPickerViewModel, composerViewModel,
composerState.action to also include context so the lambda recreates when the
composition local changes and the Toast uses the current Context.
stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/messages/composer/internal/CommandSelectionRoutingTest.kt (1)

61-87: Add Toast assertions to verify user-visible feedback for unavailable commands.

The tests currently verify callback suppression but skip the Toast branch. Since routeCommandSelection() explicitly calls Toast.makeText() for unavailable commands in reply/edit modes, the tests should assert this side effect to catch resource mismatches or missing Toast calls.

🧪 Example assertion to add
+import io.getstream.chat.android.compose.R
+import org.robolectric.shadows.ShadowToast
+
     `@Test`
     fun `Unavailable command in reply mode does not invoke onAvailable`() {
         var received: Command? = null
 
         routeCommandSelection(
@@
 
         assertNull(received)
+        assertEquals(
+            context.getString(R.string.stream_compose_message_composer_command_unavailable_in_reply),
+            ShadowToast.getTextOfLatestToast(),
+        )
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/messages/composer/internal/CommandSelectionRoutingTest.kt`
around lines 61 - 87, Extend the two tests (`Unavailable command in reply mode
does not invoke onAvailable` and `Unavailable command in edit mode does not
invoke onAvailable`) to also assert that a Toast is shown when the command is
unavailable: after calling routeCommandSelection(...) (which internally calls
Toast.makeText), verify the Toast was created/shown (using the test harness you
already use for UI feedback, e.g., Robolectric ShadowToast or your existing
Toast verifier) and assert its text matches the expected user-facing message for
unavailable commands; keep the existing onAvailable null assertions intact and
reference routeCommandSelection and Toast.makeText in your checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerController.kt`:
- Around line 1011-1019: The stash logic stores selected attachments but only
clears picker-selected attachments (_selectedAttachments) while leaving
_recordingAttachment intact, so a completed recording can remain attached when
entering command mode; update the stash/reset sequence in
MessageComposerController (around commandStash creation and the similar blocks
at the other locations) to also clear _recordingAttachment (set it to null or
appropriate empty state) before calling syncAttachments(), and ensure
commandStash includes the recording attachment if intended (or explicitly
excludes it) so recordings are correctly removed from the composer when stashing
commands and restored on cancel.

---

Outside diff comments:
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentPicker.kt`:
- Around line 68-80: The public AttachmentPicker composable currently exposes
the messageAction parameter (AttachmentPicker(..., messageAction: MessageAction?
= null)), which expands the public API; remove this parameter from the public
signature and restore the original binary-compatible public API by creating an
internal implementation that accepts messageAction and having the public
AttachmentPicker call that internal version with messageAction = null.
Concretely, add an internal composable (e.g., AttachmentPickerInternal or
AttachmentPickerWithAction) that takes the same parameters plus messageAction:
MessageAction?, move the action-aware logic into it, and change the public
AttachmentPicker to the simpler signature (no messageAction) that delegates to
the internal composable passing messageAction = null; keep symbols like
AttachmentsPickerViewModel, AttachmentPickerActions.pickerDefaults, MessageMode
and the remember(...) usage unchanged.

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentSystemPicker.kt`:
- Around line 198-202: Call actions.onCommandSelected(command) first and only
set showCommandsPickerDialog = false when the command was actually accepted;
specifically, invoke actions.onCommandSelected(command) before touching
showCommandsPickerDialog and gate dismissal on either the boolean return value
from actions.onCommandSelected (if you change its signature to return true when
accepted) or on a concrete availability flag on the command (e.g.,
command.isAvailable / command.available) — keep messageAction untouched but use
onCommandSelected’s result or command availability to decide whether to set
showCommandsPickerDialog = false.

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/MessageComposer.kt`:
- Around line 192-194: Update the public stateless overload's KDoc for
MessageComposer to mirror the command availability contract: state clearly that
the onCommandSelected handler will receive every command tap (including commands
that are visually marked unavailable) and that callers are responsible for
filtering availability and providing appropriate user feedback; reference the
onCommandSelected parameter in the KDoc so consumers of the stateless overload
understand they must check command availability and handle UI feedback
themselves.

---

Nitpick comments:
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentPickerMenu.kt`:
- Around line 101-102: The remember call that builds actions should include the
LocalContext.current (context) in its key list to avoid capturing a stale
Context used by onCommandSelected (which shows a Toast); update the remember
invocation that currently lists attachmentsPickerViewModel, composerViewModel,
composerState.action to also include context so the lambda recreates when the
composition local changes and the Toast uses the current Context.

In
`@stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/messages/composer/internal/CommandSelectionRoutingTest.kt`:
- Around line 61-87: Extend the two tests (`Unavailable command in reply mode
does not invoke onAvailable` and `Unavailable command in edit mode does not
invoke onAvailable`) to also assert that a Toast is shown when the command is
unavailable: after calling routeCommandSelection(...) (which internally calls
Toast.makeText), verify the Toast was created/shown (using the test harness you
already use for UI feedback, e.g., Robolectric ShadowToast or your existing
Toast verifier) and assert its text matches the expected user-facing message for
unavailable commands; keep the existing onAvailable null assertions intact and
reference routeCommandSelection and Toast.makeText in your checks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a6c9784f-4859-487a-be46-9ed321c4634a

📥 Commits

Reviewing files that changed from the base of the PR and between fa66d70 and eb8d70c.

⛔ Files ignored due to path filters (5)
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.info_ChannelInfoScreenModelTest_remove_member.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.info_ChannelInfoScreenModelTest_remove_member_in_dark_mode.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages.attachments_AttachmentCommandPickerTest_command_picker.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages.composer.internal.suggestions_CommandSuggestionListTest_command_suggestion_list_in_edit_mode.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages.composer.internal.suggestions_CommandSuggestionListTest_command_suggestion_list_in_reply_mode.png is excluded by !**/*.png
📒 Files selected for processing (20)
  • stream-chat-android-compose/api/stream-chat-android-compose.api
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentCommandPicker.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentPicker.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentPickerActions.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentPickerContent.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentPickerMenu.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentSystemPicker.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/MessageComposer.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/CommandAvailability.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/CommandSelectionRouting.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/suggestions/CommandSuggestionItem.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/suggestions/CommandSuggestionList.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactory.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactoryParams.kt
  • stream-chat-android-compose/src/main/res/values/strings.xml
  • stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/messages/composer/internal/CommandAvailabilityTest.kt
  • stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/messages/composer/internal/CommandSelectionRoutingTest.kt
  • stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/messages/composer/internal/suggestions/CommandSuggestionListTest.kt
  • stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerController.kt
  • stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerControllerTest.kt

@andremion andremion force-pushed the redesign/ds-030-command-mode-stash branch from b4a9389 to b4a47f7 Compare April 22, 2026 15:45
@andremion
Copy link
Copy Markdown
Contributor Author

@CodeRabbit full review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 22, 2026

✅ Actions performed

Full review triggered.

@andremion andremion changed the title Preserve composer draft when entering or cancelling a slash command Support non-destructive command mode and command availability UX Apr 22, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (5)
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/suggestions/CommandSuggestionItem.kt (2)

62-62: Stale/incorrect test tag.

testTag("Stream_SuggestionListGiphyButton") on a generic CommandSuggestionItem is misleading — it's used for every command, not just Giphy. This predates the PR so feel free to defer, but while you're touching this composable it may be worth renaming to e.g. Stream_CommandSuggestionItem (sweep call sites/UI tests).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/suggestions/CommandSuggestionItem.kt`
at line 62, The test tag on the CommandSuggestionItem composable is
stale/misleading; replace the hardcoded
.testTag("Stream_SuggestionListGiphyButton") in the CommandSuggestionItem
implementation with a generic, descriptive tag such as
"Stream_CommandSuggestionItem" (or similar) and update any test or call sites
that assert on the old tag to use the new tag; search for the
CommandSuggestionItem composable and its .testTag usage to change the string
constant and adjust UI tests that reference "Stream_SuggestionListGiphyButton".

56-63: Minor: consider accessibility semantics for disabled commands.

Lowering alpha to 0.5 visually communicates "disabled", but the Row remains fully clickable with no semantic signal. For TalkBack users the item is indistinguishable from an enabled one. Since the product decision is that taps must still fire (to surface the explanatory Toast), consider adding a Modifier.semantics { /* stateDescription or disabled = true-ish hint */ } or a content description suffix so assistive tech communicates the unavailable state. Not a blocker.

Also note: the .alpha(...) is placed after .clickable, so the ripple is also dimmed — that's the typical desired effect here, just confirming it's intentional.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/suggestions/CommandSuggestionItem.kt`
around lines 56 - 63, The Row for CommandSuggestionItem visually dims disabled
items via .alpha but lacks accessibility semantics; update the modifier chain
used in Row (the modifier passed into Row, the .clickable {
onCommandSelected(command) } block, and the enabled boolean) to include
Modifier.semantics { stateDescription = if (!enabled) "Unavailable" else
"Available" } or a similar disabled hint so assistive tech (TalkBack)
communicates the unavailable state; keep the clickable behavior (taps still
fire) and ensure the semantics modifier references the same enabled flag (and
testTag "Stream_SuggestionListGiphyButton") so screen readers get the
suffix/description without changing visual/ripple behavior.
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/messages/MessageComposerViewModel.kt (1)

60-64: LGTM — consider documenting replay/buffer semantics.

Exposing events as a pass-through SharedFlow is clean. Since this is a public API for one-shot events, consumers will care whether late collectors receive replays. Consider mentioning the underlying flow's replay/extraBufferCapacity behavior (from MessageComposerController.events) in the KDoc so integrators don't accidentally miss or double-handle events on configuration changes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/messages/MessageComposerViewModel.kt`
around lines 60 - 64, Add KDoc to the public val events on
MessageComposerViewModel clarifying the replay/extraBufferCapacity semantics
inherited from MessageComposerController.events: state that this SharedFlow is a
one-shot events stream of MessageComposerViewEvent, whether late collectors will
receive replays (replay = X) and how extraBufferCapacity affects backpressure or
missed events on configuration changes, and note recommended consumer behavior
(e.g., collect in lifecycleScope or use replay cache handling) so integrators
know if they must handle duplicates on reconnection or configuration change;
reference the underlying MessageComposerController.events in the KDoc.
stream-chat-android-compose/src/main/res/values-it/strings.xml (1)

156-158: Minor: alphabetical ordering broken.

The rest of this file is alphabetically sorted, but these three new stream_compose_message_composer_command(s)_unavailable_* entries are inserted between ..._file_not_supported (line 155) and ..._instant_commands (line 159), which places command_unavailable_in_* out of order (should come before file_not_supported). Same issue applies to the other localized files and values/strings.xml for consistency. Not functional, just housekeeping.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@stream-chat-android-compose/src/main/res/values-it/strings.xml` around lines
156 - 158, These three new string resources
(stream_compose_message_composer_command_unavailable_in_edit,
stream_compose_message_composer_command_unavailable_in_reply,
stream_compose_message_composer_commands_unavailable_in_edit) are inserted out
of alphabetical order; move them so their keys appear alphabetically before
stream_compose_message_composer_file_not_supported in this values-it/strings.xml
and apply the same reorder to the matching entries in other locale files and
values/strings.xml to keep all string files consistently sorted.
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/StreamSnackbar.kt (1)

78-90: Apply the external modifier to the outermost composable.

modifier is currently attached to the inner Surface; move it to the wrapping Box and keep the Surface modifier internal.

♻️ Proposed adjustment
- * `@param` modifier Modifier applied to the inner [Surface] (e.g. to control width or padding).
+ * `@param` modifier Modifier applied to the snackbar container.
@@
-    Box(modifier = Modifier.padding(StreamTokens.spacingMd)) {
+    Box(modifier = modifier.padding(StreamTokens.spacingMd)) {
         Surface(
-            modifier = modifier.shadow(4.dp, shape = SnackbarShape),
+            modifier = Modifier.shadow(4.dp, shape = SnackbarShape),

Based on learnings, “In Compose UI components within the stream-chat-android-compose module, always apply modifiers to the outermost composable in the hierarchy.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/StreamSnackbar.kt`
around lines 78 - 90, The external modifier passed to StreamSnackbar should be
applied to the outermost composable: move the parameter `modifier` from the
inner Surface to the wrapping Box (combine it with the existing Box padding,
e.g. use modifier.then(Modifier.padding(StreamTokens.spacingMd)) or
modifier.padding(...)) and keep the Surface's internal modifiers (shadow(4.dp,
shape = SnackbarShape), shape, color, etc.) unchanged; update the StreamSnackbar
function so Box receives the external modifier and Surface uses only
component-internal modifiers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentCommandPicker.kt`:
- Around line 66-106: The current edit branch in AttachmentCommandPicker returns
EmptyContent, which hides the command list and prevents onCommandSelected and
unavailable-command feedback from running; instead render the same command list
UI used in the non-edit branch but pass enabled = false for each CommandItem
when messageAction is an Edit so commands remain visible and tappable (use the
existing sortedCommands remember block and items iteration with CommandItem,
setting enabled to command.isAvailableFor(messageAction) && messageAction !is
Edit or simply false when Edit) to preserve feedback while showing disabled
state.

In
`@stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerController.kt`:
- Around line 431-436: The observer on _messageActions only re-sorts suggestions
but leaves a previously-selected activeCommand set even when the new action
(found as activeAction from actions.lastOrNull) no longer supports it; update
the handler so that before calling _state.update { it.copy(action =
activeAction) } you clear the active command if it's invalid for the new action
(e.g., set activeCommand/nullField to null or call the existing command-clear
method), then call handleCommandSuggestions(), and ensure buildNewMessage uses
the cleared state; reference _messageActions, activeAction, _state.update,
handleCommandSuggestions, activeCommand and buildNewMessage when making the
change.

---

Nitpick comments:
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/suggestions/CommandSuggestionItem.kt`:
- Line 62: The test tag on the CommandSuggestionItem composable is
stale/misleading; replace the hardcoded
.testTag("Stream_SuggestionListGiphyButton") in the CommandSuggestionItem
implementation with a generic, descriptive tag such as
"Stream_CommandSuggestionItem" (or similar) and update any test or call sites
that assert on the old tag to use the new tag; search for the
CommandSuggestionItem composable and its .testTag usage to change the string
constant and adjust UI tests that reference "Stream_SuggestionListGiphyButton".
- Around line 56-63: The Row for CommandSuggestionItem visually dims disabled
items via .alpha but lacks accessibility semantics; update the modifier chain
used in Row (the modifier passed into Row, the .clickable {
onCommandSelected(command) } block, and the enabled boolean) to include
Modifier.semantics { stateDescription = if (!enabled) "Unavailable" else
"Available" } or a similar disabled hint so assistive tech (TalkBack)
communicates the unavailable state; keep the clickable behavior (taps still
fire) and ensure the semantics modifier references the same enabled flag (and
testTag "Stream_SuggestionListGiphyButton") so screen readers get the
suffix/description without changing visual/ripple behavior.

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/StreamSnackbar.kt`:
- Around line 78-90: The external modifier passed to StreamSnackbar should be
applied to the outermost composable: move the parameter `modifier` from the
inner Surface to the wrapping Box (combine it with the existing Box padding,
e.g. use modifier.then(Modifier.padding(StreamTokens.spacingMd)) or
modifier.padding(...)) and keep the Surface's internal modifiers (shadow(4.dp,
shape = SnackbarShape), shape, color, etc.) unchanged; update the StreamSnackbar
function so Box receives the external modifier and Surface uses only
component-internal modifiers.

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/messages/MessageComposerViewModel.kt`:
- Around line 60-64: Add KDoc to the public val events on
MessageComposerViewModel clarifying the replay/extraBufferCapacity semantics
inherited from MessageComposerController.events: state that this SharedFlow is a
one-shot events stream of MessageComposerViewEvent, whether late collectors will
receive replays (replay = X) and how extraBufferCapacity affects backpressure or
missed events on configuration changes, and note recommended consumer behavior
(e.g., collect in lifecycleScope or use replay cache handling) so integrators
know if they must handle duplicates on reconnection or configuration change;
reference the underlying MessageComposerController.events in the KDoc.

In `@stream-chat-android-compose/src/main/res/values-it/strings.xml`:
- Around line 156-158: These three new string resources
(stream_compose_message_composer_command_unavailable_in_edit,
stream_compose_message_composer_command_unavailable_in_reply,
stream_compose_message_composer_commands_unavailable_in_edit) are inserted out
of alphabetical order; move them so their keys appear alphabetically before
stream_compose_message_composer_file_not_supported in this values-it/strings.xml
and apply the same reorder to the matching entries in other locale files and
values/strings.xml to keep all string files consistently sorted.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 2a4d7608-9dfa-47d3-b8cc-23abb56b878e

📥 Commits

Reviewing files that changed from the base of the PR and between fa66d70 and b4a47f7.

⛔ Files ignored due to path filters (13)
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.info_ChannelInfoScreenModelTest_remove_member.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.info_ChannelInfoScreenModelTest_remove_member_in_dark_mode.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages.attachments_AttachmentCommandPickerTest_command_picker.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages.attachments_AttachmentCommandPickerTest_command_picker_in_edit_mode.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages.attachments_AttachmentCommandPickerTest_command_picker_in_reply_mode.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages.composer.internal.suggestions_CommandSuggestionListTest_command_suggestion_list_in_edit_mode.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages.composer.internal.suggestions_CommandSuggestionListTest_command_suggestion_list_in_reply_mode.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.util_StreamSnackbarTest_snackbar_default.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.util_StreamSnackbarTest_snackbar_error.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.util_StreamSnackbarTest_snackbar_loading.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.util_StreamSnackbarTest_snackbar_message_only.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.util_StreamSnackbarTest_snackbar_success.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.util_StreamSnackbarTest_snackbar_with_action.png is excluded by !**/*.png
📒 Files selected for processing (32)
  • stream-chat-android-compose/api/stream-chat-android-compose.api
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentCommandPicker.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentPicker.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentPickerActions.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentPickerContent.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentPickerMenu.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentSystemPicker.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/MessageComposer.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/suggestions/CommandSuggestionItem.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/suggestions/CommandSuggestionList.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactory.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactoryParams.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/StreamSnackbar.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/messages/MessageComposerViewModel.kt
  • stream-chat-android-compose/src/main/res/values-es/strings.xml
  • stream-chat-android-compose/src/main/res/values-fr/strings.xml
  • stream-chat-android-compose/src/main/res/values-hi/strings.xml
  • stream-chat-android-compose/src/main/res/values-in/strings.xml
  • stream-chat-android-compose/src/main/res/values-it/strings.xml
  • stream-chat-android-compose/src/main/res/values-ja/strings.xml
  • stream-chat-android-compose/src/main/res/values-ko/strings.xml
  • stream-chat-android-compose/src/main/res/values/strings.xml
  • stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentCommandPickerTest.kt
  • stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/messages/composer/MessageComposerScreenTest.kt
  • stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/messages/composer/internal/suggestions/CommandSuggestionListTest.kt
  • stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/util/StreamSnackbarTest.kt
  • stream-chat-android-ui-common/api/stream-chat-android-ui-common.api
  • stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerController.kt
  • stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/messages/composer/CommandAvailability.kt
  • stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/messages/composer/MessageComposerViewEvent.kt
  • stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerControllerTest.kt
  • stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/state/messages/composer/CommandAvailabilityTest.kt

@andremion
Copy link
Copy Markdown
Contributor Author

@CodeRabbit full review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 23, 2026

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/MessageComposer.kt (1)

413-425: ⚠️ Potential issue | 🟡 Minor

Update LaunchedEffect dependency key to trigger when the error changes, not just the list count.

LaunchedEffect(validationErrors.size) fails to detect transitions where firstValidationError changes but the list size remains the same (e.g., replacing one AttachmentSizeExceeded error with another having different maxAttachmentSize). This prevents the snackbar/toast from displaying for the new error.

Proposed effect key
-        LaunchedEffect(validationErrors.size) {
+        LaunchedEffect(firstValidationError, errorMessage, snackbarHostState) {
             if (firstValidationError is ValidationError.ContainsLinksWhenNotAllowed ||
                 firstValidationError is ValidationError.AttachmentSizeExceeded
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/MessageComposer.kt`
around lines 413 - 425, LaunchedEffect is currently keyed on
validationErrors.size which misses changes when firstValidationError or
errorMessage change without size changing; update the effect key to include the
actual error value(s) so it retriggers (e.g., use
LaunchedEffect(firstValidationError, errorMessage) or
LaunchedEffect(validationErrors) instead of
LaunchedEffect(validationErrors.size)) and keep the existing
snackbarHostState.showSnackbar call inside the effect so the snackbar appears
for new/different ValidationError values.
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactoryParams.kt (1)

913-930: ⚠️ Potential issue | 🟠 Major

Ensure .api file updates accompany public data class constructor changes.

Adding trailing parameters to public Kotlin data class constructors changes the JVM ABI. The project has kotlinx-binary-compatibility-validator configured with .api files to track these changes. Confirm this PR includes updated .api files (run ./gradlew apiDump if needed) and that the team intentionally approves the binary-incompatible changes before merge.

Also applies to: 1006-1009, 2353-2358, 2406-2415, 2484-2489, 2503-2510

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactoryParams.kt`
around lines 913 - 930, You added new constructor parameters to the public data
class MessageComposerParams which changes the JVM ABI; run ./gradlew apiDump to
regenerate the kotlinx-binary-compatibility-validator .api files, commit the
updated .api entries, and confirm the team intentionally accepts this
binary-incompatible change before merging; apply the same process for other
affected public constructors mentioned (lines referenced: the other data classes
around the file such as the ones at the ranges 1006-1009, 2353-2358, 2406-2415,
2484-2489, 2503-2510) so the API dump reflects all added trailing params.
🧹 Nitpick comments (4)
stream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/uiautomator/Wait.kt (1)

65-72: Add a small delay to avoid tight UiAutomator polling.

When the selector is absent or the text has not settled, this loop repeatedly calls device.findObject until timeout, which can burn CPU and make e2e runs noisier.

Proposed refinement
     while (System.currentTimeMillis() < endTime) {
         val actual = currentTextOrNull()
         if (actual != null) {
             lastText = actual
             val matches = if (mustBeEqual) actual == expectedText else actual.contains(expectedText)
             if (matches) return actual
         }
+        Thread.sleep(50)
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/uiautomator/Wait.kt`
around lines 65 - 72, The tight polling loop in Wait.kt repeatedly calls
currentTextOrNull() until timeout; add a small sleep between iterations (e.g.,
25–100ms) to prevent busy-waiting: after checking/matching the actual text in
the while loop, when there is no match (or actual is null) call Thread.sleep
with a short delay and properly handle InterruptedException by restoring the
thread interrupt status; update the loop around currentTextOrNull() (the while
block shown) to include this delay so UiAutomator polling is throttled.
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentCommandPicker.kt (1)

122-152: Apply the caller modifier only to the outer row.

modifier is already consumed by the outer Row at Line 123; reusing it on the nested Row at Line 152 can duplicate caller-provided padding/background/test semantics.

♻️ Proposed fix
         Row(
-            modifier = modifier,
             verticalAlignment = Alignment.CenterVertically,
             horizontalArrangement = Arrangement.spacedBy(StreamTokens.spacingSm),
         ) {

Based on learnings, in Compose UI components within stream-chat-android-compose, always apply modifiers to the outermost composable in the hierarchy.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentCommandPicker.kt`
around lines 122 - 152, The nested Row inside AttachmentCommandPicker is reusing
the caller-supplied "modifier" (already applied to the outer Row), which can
duplicate caller padding/background/test semantics; change the inner Row to use
a fresh Modifier (e.g., Modifier or Modifier.fillMaxWidth()) instead of
"modifier" so the caller modifier is only applied once at the outer Row; update
the inner Row's "modifier = modifier" to "modifier = Modifier" (or another local
Modifier chain) to fix.
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/StreamSnackbar.kt (1)

78-90: Apply modifier to the outer snackbar container.

The caller-provided modifier should wrap the whole snackbar, including the container padding; keep the shadow on the inner Surface.

Proposed modifier placement
- * `@param` modifier Modifier applied to the inner [Surface] (e.g. to control width or padding).
+ * `@param` modifier Modifier applied to the outer snackbar container.
@@
-    Box(modifier = Modifier.padding(StreamTokens.spacingMd)) {
+    Box(modifier = modifier.padding(StreamTokens.spacingMd)) {
         Surface(
-            modifier = modifier.shadow(4.dp, shape = SnackbarShape),
+            modifier = Modifier.shadow(4.dp, shape = SnackbarShape),

Based on learnings, in Compose UI components within stream-chat-android-compose, always apply modifiers to the outermost composable in the hierarchy.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/StreamSnackbar.kt`
around lines 78 - 90, The passed-in modifier is currently applied to the inner
Surface; update StreamSnackbar so the caller-provided modifier is applied to the
outer Box (wrapping the padding) and the Surface keeps the shadow/shape styling
(keep Surface.shadow(4.dp, shape = SnackbarShape) but remove modifier from
Surface and instead apply
modifier.then(Modifier.padding(StreamTokens.spacingMd)) to the Box), ensuring
the outermost composable uses the external Modifier while the Surface retains
its visual decorations.
stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerControllerTest.kt (1)

1327-1355: Assert the send guard directly.

This test verifies the snackbar event, but not the “message is not sent” part from the test name. Add an explicit never() verification so a future implementation cannot emit the event and still call sendMessage.

Proposed test assertion
             // When / Then
             controller.events.test {
-                controller.sendMessage(message, callback = mock())
+                val callback: Call.Callback<Message> = mock()
+                controller.sendMessage(message, callback = callback)
                 advanceUntilIdle()
 
                 assertEquals(MessageComposerViewEvent.CommandUnavailable(replyAction), awaitItem())
+                verify(fixture.chatClient, never()).sendMessage(any(), any(), any(), any())
                 cancelAndIgnoreRemainingEvents()
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerControllerTest.kt`
around lines 1327 - 1355, The test currently asserts the CommandUnavailable
event but not that the message was prevented from being sent; after calling
controller.sendMessage(message, callback = mock()) and advanceUntilIdle(), add a
Mockito verification that the underlying sending backend is never invoked (e.g.,
verify(mockedChatClient or mockedMessageSender, never()).sendMessage(...) or the
concrete backend method used by MessageComposerController) to ensure sendMessage
on the transport/client was not called. Place this verify(...) immediately after
the event assertion to explicitly assert the "message is not sent" behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/robots/UserRobotChannelListAsserts.kt`:
- Line 40: The assertion currently calls
Channel.messagePreview.waitForText(expectedPreview) and then compares to
trimEnd(), which can mask timeouts when only trailing whitespace differs; change
the wait to a trim-aware or substring wait so the UI is considered ready before
trimming—e.g., call the wait helper with a substring/trim option or wait for
expectedPreview.trimEnd() using a waitForSubstring/trim-aware overload of
Channel.messagePreview.waitForText, then assertEquals against the trimmed text.

In `@stream-chat-android-compose/api/stream-chat-android-compose.api`:
- Line 1752: The public API removed legacy JVM signatures (e.g.,
AttachmentPicker and AttachmentPickerParams constructors lacking MessageAction)
which is a binary-breaking change; restore compatibility by adding JVM-overload
methods/constructors that match the old descriptors (provide
AttachmentPicker(...) overloads and AttachmentPickerParams(...) constructors
without the MessageAction parameter) that delegate to the new implementations,
or alternatively mark this change as an intentional breaking API and regenerate
the API dump; ensure you update/verify symbols AttachmentPicker,
AttachmentPickerParams and any other affected top-level functions listed (1941,
2191–2201, 2264–2280, 2305–2317, 2344–2359, 4522–4531, 4711–4748) and then run
./gradlew apiDump to refresh the .api file.

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentSystemPicker.kt`:
- Line 65: The system command picker is being dismissed too early in
AttachmentSystemPicker when a command item is tapped; move or guard the
bottom-sheet dismissal so it only runs if the command is available (the same
availability check used by AttachmentPickerActions.consumePickerSession). Update
the tap handler (e.g., the onClick/onCommandSelected in AttachmentSystemPicker)
to first check the command availability and only call the picker dismissal/close
routine when available, otherwise skip dismissal and let
AttachmentPickerActions.consumePickerSession handle unavailable commands.

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/MessageComposer.kt`:
- Around line 160-171: The LaunchedEffect is currently keyed by Unit so swapping
MessageComposerViewModel keeps collecting the old viewModel.events; change the
effect to key on the ViewModel instance (e.g., LaunchedEffect(viewModel) or
LaunchedEffect(viewModel.events)) so collection cancels and restarts when the
MessageComposerViewModel changes, then continue collecting viewModel.events and
showing snackbars via snackbarHostState as before.

In
`@stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerController.kt`:
- Around line 1248-1254: The suppressSuggestionsInEditMode function currently
emits MessageComposerViewEvent.CommandUnavailable for any trigger token; change
it to first check whether there are any registered commands and whether the
current CommandText (use CommandText parsing helpers) could match at least one
command before emitting. Specifically, inside suppressSuggestionsInEditMode
(which reads config.activeCommandEnabled, activeAction and
CommandText.isTrigger(messageText)), query the commands list (from the same
source used elsewhere for suggestions) and only call _state.update { ... } and
_events.tryEmit(MessageComposerViewEvent.CommandUnavailable(action)) when there
is at least one command in the channel but none match the parsed trigger; if no
commands exist or the prefix cannot possibly match any command, simply return
false without emitting.

---

Outside diff comments:
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/MessageComposer.kt`:
- Around line 413-425: LaunchedEffect is currently keyed on
validationErrors.size which misses changes when firstValidationError or
errorMessage change without size changing; update the effect key to include the
actual error value(s) so it retriggers (e.g., use
LaunchedEffect(firstValidationError, errorMessage) or
LaunchedEffect(validationErrors) instead of
LaunchedEffect(validationErrors.size)) and keep the existing
snackbarHostState.showSnackbar call inside the effect so the snackbar appears
for new/different ValidationError values.

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactoryParams.kt`:
- Around line 913-930: You added new constructor parameters to the public data
class MessageComposerParams which changes the JVM ABI; run ./gradlew apiDump to
regenerate the kotlinx-binary-compatibility-validator .api files, commit the
updated .api entries, and confirm the team intentionally accepts this
binary-incompatible change before merging; apply the same process for other
affected public constructors mentioned (lines referenced: the other data classes
around the file such as the ones at the ranges 1006-1009, 2353-2358, 2406-2415,
2484-2489, 2503-2510) so the API dump reflects all added trailing params.

---

Nitpick comments:
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentCommandPicker.kt`:
- Around line 122-152: The nested Row inside AttachmentCommandPicker is reusing
the caller-supplied "modifier" (already applied to the outer Row), which can
duplicate caller padding/background/test semantics; change the inner Row to use
a fresh Modifier (e.g., Modifier or Modifier.fillMaxWidth()) instead of
"modifier" so the caller modifier is only applied once at the outer Row; update
the inner Row's "modifier = modifier" to "modifier = Modifier" (or another local
Modifier chain) to fix.

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/StreamSnackbar.kt`:
- Around line 78-90: The passed-in modifier is currently applied to the inner
Surface; update StreamSnackbar so the caller-provided modifier is applied to the
outer Box (wrapping the padding) and the Surface keeps the shadow/shape styling
(keep Surface.shadow(4.dp, shape = SnackbarShape) but remove modifier from
Surface and instead apply
modifier.then(Modifier.padding(StreamTokens.spacingMd)) to the Box), ensuring
the outermost composable uses the external Modifier while the Surface retains
its visual decorations.

In
`@stream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/uiautomator/Wait.kt`:
- Around line 65-72: The tight polling loop in Wait.kt repeatedly calls
currentTextOrNull() until timeout; add a small sleep between iterations (e.g.,
25–100ms) to prevent busy-waiting: after checking/matching the actual text in
the while loop, when there is no match (or actual is null) call Thread.sleep
with a short delay and properly handle InterruptedException by restoring the
thread interrupt status; update the loop around currentTextOrNull() (the while
block shown) to include this delay so UiAutomator polling is throttled.

In
`@stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerControllerTest.kt`:
- Around line 1327-1355: The test currently asserts the CommandUnavailable event
but not that the message was prevented from being sent; after calling
controller.sendMessage(message, callback = mock()) and advanceUntilIdle(), add a
Mockito verification that the underlying sending backend is never invoked (e.g.,
verify(mockedChatClient or mockedMessageSender, never()).sendMessage(...) or the
concrete backend method used by MessageComposerController) to ensure sendMessage
on the transport/client was not called. Place this verify(...) immediately after
the event assertion to explicitly assert the "message is not sent" behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 8c2af675-bf0f-4dec-961d-7cc458af8222

📥 Commits

Reviewing files that changed from the base of the PR and between fa66d70 and c18a65f.

⛔ Files ignored due to path filters (13)
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.info_ChannelInfoScreenModelTest_remove_member.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.channel.info_ChannelInfoScreenModelTest_remove_member_in_dark_mode.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages.attachments_AttachmentCommandPickerTest_command_picker.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages.attachments_AttachmentCommandPickerTest_command_picker_in_edit_mode.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages.attachments_AttachmentCommandPickerTest_command_picker_in_reply_mode.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages.composer.internal.suggestions_CommandSuggestionListTest_command_suggestion_list_in_edit_mode.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages.composer.internal.suggestions_CommandSuggestionListTest_command_suggestion_list_in_reply_mode.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.util_StreamSnackbarTest_snackbar_default.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.util_StreamSnackbarTest_snackbar_error.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.util_StreamSnackbarTest_snackbar_loading.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.util_StreamSnackbarTest_snackbar_message_only.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.util_StreamSnackbarTest_snackbar_success.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.util_StreamSnackbarTest_snackbar_with_action.png is excluded by !**/*.png
📒 Files selected for processing (35)
  • stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/robots/UserRobotChannelListAsserts.kt
  • stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/robots/UserRobotMessageListAsserts.kt
  • stream-chat-android-compose/api/stream-chat-android-compose.api
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentCommandPicker.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentPicker.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentPickerActions.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentPickerContent.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentPickerMenu.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentSystemPicker.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/MessageComposer.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/suggestions/CommandSuggestionItem.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/suggestions/CommandSuggestionList.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactory.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactoryParams.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/StreamSnackbar.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/messages/MessageComposerViewModel.kt
  • stream-chat-android-compose/src/main/res/values-es/strings.xml
  • stream-chat-android-compose/src/main/res/values-fr/strings.xml
  • stream-chat-android-compose/src/main/res/values-hi/strings.xml
  • stream-chat-android-compose/src/main/res/values-in/strings.xml
  • stream-chat-android-compose/src/main/res/values-it/strings.xml
  • stream-chat-android-compose/src/main/res/values-ja/strings.xml
  • stream-chat-android-compose/src/main/res/values-ko/strings.xml
  • stream-chat-android-compose/src/main/res/values/strings.xml
  • stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentCommandPickerTest.kt
  • stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/messages/composer/MessageComposerScreenTest.kt
  • stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/messages/composer/internal/suggestions/CommandSuggestionListTest.kt
  • stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/util/StreamSnackbarTest.kt
  • stream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/uiautomator/Wait.kt
  • stream-chat-android-ui-common/api/stream-chat-android-ui-common.api
  • stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerController.kt
  • stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/messages/composer/CommandAvailability.kt
  • stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/messages/composer/MessageComposerViewEvent.kt
  • stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerControllerTest.kt
  • stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/state/messages/composer/CommandAvailabilityTest.kt

Comment thread stream-chat-android-compose/api/stream-chat-android-compose.api
@andremion andremion marked this pull request as ready for review April 23, 2026 15:31
@andremion andremion requested a review from a team as a code owner April 23, 2026 15:31
…mands in the message composer.

- Move `CommandAvailability` and related tests to `ui-common` module.
- Add `MessageComposerNotice` interface and `CommandUnavailable` notice type to track transient composer feedback.
- Update `MessageComposerState` and `MessageComposerController` to support emitting and dismissing notices.
- Refactor `MessageComposerController` to block command selection and suggestions when in `Edit` mode or when moderation commands are used in `Reply` mode, emitting a notice instead.
- Update UI components and internal routing to use the relocated `isAvailableFor` utility.
…ndle transient UI feedback as one-shot side effects.

- Rename `MessageComposerNotice` to `MessageComposerViewEvent`.
- Replace the `notices` list in `MessageComposerState` with an `events` `SharedFlow` in `MessageComposerController`.
- Remove `dismissNotice()` and `emitNotice()` in favor of direct event emission.
- Update `MessageComposerController` to emit `CommandUnavailable` events when commands are triggered in incompatible modes (e.g., during editing).
- Update unit tests to collect and verify events using the `test` library.
- Removed `CommandSelectionRouting.kt` and its associated tests, moving command availability logic into the `MessageComposerViewModel`.
- Added `snackbarHostState` to `MessageComposerParams` and `MessageComposer` to support displaying validation errors and transient notifications like unavailable commands.
- Updated `MessageComposer` to collect events from `MessageComposerViewModel` and show relevant snackbar messages.
- Updated `AttachmentPickerMenu` and `ChatComponentFactory` to align with the new command selection and snackbar logic.
- Exposed `events` flow in `MessageComposerViewModel` and updated public API signatures.
- Introduced `StreamSnackbarVariant` (Default, Error, Success, Loading) and `StreamSnackbarVisuals`.
- Updated `StreamSnackbar` to render leading icons or a loading indicator based on the variant.
- Added new previews and Paparazzi snapshot tests for all snackbar variants.
- Updated `StreamSnackbarTest` to use landscape orientation for snapshots.
- Add `CancelCommandRequired` event to `MessageComposerViewEvent`.
- Implement logic in `MessageComposerController` to block actions (Edit/Reply) if an incompatible command is active.
- Introduce auto-selection of commands when typing `/{command} `.
- Add localized string resources for "Cancel command to edit/reply" across multiple languages.
- Update UI components to display snackbars for the new event.
- Add comprehensive unit tests for command auto-selection and action blocking.
Introduced a `Config.activeCommandEnabled` check to preserve pre-DS-030 behavior for the XML SDK. When disabled, the controller skips command availability validation in edit/reply modes and maintains server-provided command ordering instead of sorting by availability.
@andremion andremion force-pushed the redesign/ds-030-command-mode-stash branch from 8a1a9a1 to 0b0025d Compare April 24, 2026 08:54
@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
76.5% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr:improvement Improvement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant