Repository navigation
feat(group-chat): render the shared composer toolbar in a room - #1945
reachrazamair wants to merge 7 commits into
Conversation
Step 5 of the Group Chat composer parity plan. GroupChatInput drops its own bottom row and renders ToolbarControls with GROUP_CHAT_COMPOSER_CAPABILITIES. The room's Read-Only toggle moves into the toolbar unchanged: toolbarExtras on desktop, and a new sheetExtras slot on a phone, so the room keeps the control in every layout. What a room user sees: - Desktop: the toolbar row is laid out as in AI chat. The Read-Only tooltip now shows its live, rebindable chord, and the Prompt Composer tooltip reads the live binding the same way AI chat's does. - Web-desktop: Attach reads "Upload Image to Host", and an Upload button copies local files to the host and mentions them in the draft (the existing handler, which already accepted a room; the draft is flushed first, as a drop does). - Touch devices with speech recognition: a mic button dictates into the draft, which persists like typing. - Narrow window: Read-Only and the Enter toggle fold behind a "..." button. - Phone: the row holds attach, upload (web-desktop), mic, Send and "...". The bell column and the Prompt Composer button are not shown, as in AI chat. Send keeps the room's rules: disabled without text, and "Queue message" in the warning tint while the room is busy. "..." opens a sheet with a Read-Only switch. The Enter toggle is not shown on a phone, as in AI chat. - Never shown in a room: Save to History, the permission pill, Thinking, and the model and effort pills. Shared pieces: - PhoneSheetSwitchRow (PhoneSheetRows): the on/off row extracted from the sheet's History row, which now renders through it unchanged. - ComposerOptionsSheet gains extraRows, drawn after its sections. - ToolbarControls gains sheetExtras, phone Send props (sendTitle, sendDisabled, sendTone, same meaning as ComposerSendColumn), and the image input's id, report name and file check as props (defaults are the AI composer's values). The per-tab agent props are now optional, read only when their capability is on. The sheet mounts when it has a section or surface rows, and the phone "..." is drawn only when it opens something, except in AI terminal mode, which keeps its existing "..." with no sheet behind it (a known gap, not changed here). AI chat is unchanged; every existing ToolbarControls, ComposerOptionsSheet and InputArea test passes without modification. CLI-UI-PARITY.md records composer upload as an open gap (no verb: an agent is already on the host).
…bar (composer index lines trimmed to symbols and paths)
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. 🗂️ Base branches to auto review: 2
Please check the settings in the CodeRabbit UI or the
|
[Medium impact] The changes affect composer workflows across Group Chat layouts and shared phone controls. Fix late dictation results overwriting room drafts before merging. Findings
SummaryGroup Chat now uses the shared composer toolbar.
Intentional limits stated by reachrazamair: phone layouts omit the bell, Prompt Composer, and Enter toggle; AI terminal mode keeps its known empty options button; room image support remains independent of the moderator; upload has no planned CLI verb because agents already run on the host. Diagram%%{init: {'theme': 'neutral'}}%%
flowchart LR
Room[GroupChatInput] --> Toolbar[ToolbarControls]
Toolbar --> Desktop[Desktop controls]
Toolbar --> Phone[Phone Send and options]
Phone --> Sheet[ComposerOptionsSheet]
Sheet --> Switch[Read-Only switch]
Toolbar --> Voice[useVoiceInput]
Voice --> Draft[Room draft]
Reviews (1) · Last reviewed commit: "Merge refactor/composer-capabilities int..." · Reviewed by Greptile |
| const voice = useVoiceInput({ | ||
| currentValue: message, | ||
| onTranscriptionChange: setDraftValue, | ||
| focusRef: inputRef, | ||
| }); |
There was a problem hiding this comment.
Dictation overwrites room drafts
Dictation keeps running when the user switches rooms or sends a message. useVoiceInput keeps the text and setDraftValue callback from when listening started. After a room switch, another speech result replaces the new room's visible draft while saving it under the old room's ID. After sending, another result puts the sent text back into the cleared draft.
Cancel recognition and ignore late results at both boundaries. Stopping alone can still deliver a final result.
There was a problem hiding this comment.
Fixed: useVoiceInput gained cancelVoiceInput(), which aborts the recognizer and drops every event the cancelled dictation still delivers (results, error, end) and returns the mic to idle. The room calls it on a room switch and on send, with tests for a late result after each and for uninterrupted dictation.
The Progress section and the Step sequence table still said steps 2-5 were on unmerged stacked branches. Step 2 merged into rc as #1890 (9d6f5b2), so its Progress row cites the rc merge commit as step 1's does, and its Step sequence note says merged. Steps 3, 4 and 5 name their PRs (#1891, #1892, #1945), and step 3 now sits off rc rather than on step 2. The step commits in both tables are unchanged: each branch's tip moved on 2026-10-28 only by a CLAUDE.md index trim and the merges that carry it up the stack, which complete no rows. The Progress intro says so.
…bar (IME guard and picker Escape fixes) # Conflicts: # docs/agent-guides/CANONICAL-UTILITIES.md
…ate results useVoiceInput's only way out was stopVoiceInput, which asks the recognizer to stop() and lets its final result land in the draft. That is right when the user taps the mic off, and wrong when the draft no longer belongs to the dictation (it was sent, or the composer now shows another conversation): the result is written with the text and callback captured when listening started. cancelVoiceInput() abort()s instead, which per the spec raises no further result, and marks the dictation cancelled, because a browser can still deliver an event it had already queued. Every later onstart, onresult, onerror and onend of a cancelled dictation is ignored, so it cannot write the draft, refocus the input, or flip isListening on a dictation started after it. isListening is false as soon as cancel returns, so the mic shows idle. Each start opens its own session for this. Additive: stopVoiceInput, toggle and unmount behave as before, and AI chat does not call the new function, so its behaviour is unchanged.
…is sent Greptile P1 on #1945. A dictation keeps the draft text and setDraftValue from when it started, so a result arriving after a room switch replaced the new room's visible draft while saving it under the old room's id, and a result arriving after a send put the sent text back into the cleared draft. GroupChatInput now calls the hook's cancelVoiceInput() in the room-switch effect (before the old draft is flushed and the new one loaded) and in handleSend (with the draft cleared). Cancel aborts the recognizer and drops any result it still delivers, since a stop alone can still produce a final result, and the mic shows idle again. The useVoiceInput call and setDraftValue move above the room-switch effect so both can reach it. Group Chat behaviour changes: - Switching rooms or sending ends a dictation in progress; the mic returns to idle and words spoken after that point are not written anywhere.
Brings current origin/rc up through step 4. Resolves the conflict in docs/agent-guides/CLI-UI-PARITY.md: rc now keeps Open gaps for buildable work only and lists actions left without a verb on purpose under Not mirrored by design. The composer Upload entry (no CLI verb, none planned) therefore moves there as a bullet with its text unchanged; rc's rows are kept as they are.
Stacked on #1892 (and #1891 below it; #1890 is merged). Base is
refactor/composer-capabilities. Step 5 of the Group Chat composer parity plan (Plans/group-chat-composer-parity.md). No visual or behaviour change in AI chat.What changed
GroupChatInput drops its own bottom row and renders
ToolbarControlswithGROUP_CHAT_COMPOSER_CAPABILITIES. The room's Read-Only toggle moves into the toolbar unchanged:toolbarExtrason desktop, and a newsheetExtrasslot on a phone, so the room keeps the control in every layout.What a room user sees:
(Ctrl+R), and the Prompt Composer tooltip reads the live binding(Ctrl+Shift+P)the same way AI chat's does.Show options/Hide options).Shared pieces:
PhoneSheetSwitchRow(PhoneSheetRows): the on/off row extracted from the sheet's History row, which now renders through it unchanged.ComposerOptionsSheetgainsextraRows, drawn after its sections.ToolbarControlsgainssheetExtras, phone Send props (sendTitle,sendDisabled,sendTone, same meaning asComposerSendColumn), and the image input's id, report name and file check as props (defaults are the AI composer's values). The per-tab agent props are now optional, read only when their capability is on. The sheet mounts when it has a section or surface rows, and the phone "..." is drawn only when it opens something, except in AI terminal mode, which keeps its existing "..." with no sheet behind it (a known gap, not changed here).CLI-UI-PARITY.mdrecords composer upload as an open gap (no verb: an agent is already on the host).In-App Verification
Exercised in the running app with a seeded synthetic Group Chat demo dataset across desktop dev (
scripts/dev.mjs) and web-desktop (dist/web-desktop).Verified Live in the Running App
(Ctrl+R).PromptComposerModalwith room session context; tooltip shows live chord(Ctrl+Shift+P).disabled = true, title"Send message", border greyrgb(68, 71, 90).disabled = false, title"Send message", accent purplergb(189, 147, 249).disabled = false, title"Queue message", warning orangergb(255, 184, 108).Show options).Hide options).rgb(68, 71, 90)), enabled accent when typed (rgb(189, 147, 249)), warning orange when busy (rgb(255, 184, 108)).@...path to the draft.group-chat-image-inputstages the preview thumbnail in the room'sStagedImagesStrip.rgb(68, 71, 90)), enabled accent when draft contains upload mention and staged image (rgb(189, 147, 249)), warning orange when busy (rgb(255, 184, 108)).rgb(68, 71, 90)), enabled accent when typed (rgb(189, 147, 249)), warning orange when busy (rgb(255, 184, 108)).ComposerOptionsSheetwith Read-Only switch row (PhoneSheetSwitchRow).aria-checkedfrom"false"to"true"and status text from "Off" to "On".Verified via Unit Tests Only
GroupChatInput.toolbar.test.tsx):SpeechRecognition/webkitSpeechRecognition).useVoiceInput,GroupChatInput, andToolbarControlsare tested inGroupChatInput.toolbar.test.tsx.What Was Not Checked Live
data-runtime="web-desktop").Note on Queued Items Investigation
During initial testing, an empty queued message card was observed above the composer when injecting synthetic test items into
groupChatQueues. Investigation confirmed:GroupChatQueuedItem(src/shared/group-chat-types.ts:433) definestext: string, notcontent.{ content: "..." }instead of{ text: "..." }, leavingentry.textundefined andQueuedItemRowrendering empty text.(entry) => ({ ... text: entry.text ... })inGroupChatInput.tsxwas untouched by commit58c0df2dfand behaves identically onrefactor/composer-capabilities(step 4).{ text: "..." }displays the card correctly on both branches.Tests
src/__tests__/renderer/components/GroupChatInput.toolbar.test.tsx(new tests covering room toolbar integration and capabilities).src/__tests__/renderer/components/GroupChatInput.test.tsx.src/__tests__/renderer/components/InputArea/components/ToolbarControls.test.tsx.src/__tests__/renderer/components/InputArea/ComposerOptionsSheet.test.tsx.src/__tests__/renderer/components/ui/PhoneBottomSheet.test.tsx.src/__tests__/renderer/components/ChatComposer/*.npm run docs:verifypasses with 0 findings.npm run lintclean.