Skip to content

feat(chat): composer toolbar + attach menu and voice clip - #2147

Merged
alexprate merged 3 commits into
mainfrom
cursor/composer-toolbar-plus-mic-ef7a
Apr 13, 2026
Merged

feat(chat): composer toolbar + attach menu and voice clip#2147
alexprate merged 3 commits into
mainfrom
cursor/composer-toolbar-plus-mic-ef7a

Conversation

@webguru-hypha

@webguru-hypha webguru-hypha commented Apr 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Updates the Human chat composer bottom icon row to match the requested layout:

  • + opens a dropdown with Photo or image, Video, and File — each still uses the same hidden <input type="file"> flows and pushDrafts, so multi-select and draft preview / send behavior are unchanged.
  • Emoji — unchanged HumanChatPanelEmojiPicker behavior.
  • @disabled button (read-only placeholder until mentions exist).
  • MicrophoneMediaRecorder voice clip: first click starts recording, second click stops and appends an audio file draft via the same pushDrafts(..., 'file') path as other documents (no Matrix protocol change).

The Bold shortcut button was removed from the footer (formatting remains on the text selection floating toolbar).

i18n

New HumanChatPanel keys in en, de, es, fr, pt for attach menu labels, mention placeholder, voice actions, and errors.

Testing

  • pnpm run format:fix
  • pnpm --filter @hypha-platform/epics check-types

Note on PR 2139

That PR focuses on message hover / overflow / edit flows; it does not add a composer microphone. Voice capture here follows a minimal browser MediaRecorder → File → existing draft pattern aligned with “voice as attachment” without changing send/upload code paths.

Open in Web Open in Cursor 

Summary by CodeRabbit

  • New Features

    • Voice message recording with start/stop controls, microphone permission handling and user-facing error messages
    • Redesigned composer attachment UI with a dropdown for images, videos and files; video attachments supported
    • Disabled/placeholder mentions control in the composer (mentions coming soon)
  • Localization

    • Added German, English, Spanish, French and Portuguese translations for composer attachment actions, mention placeholder, voice controls and error text

@coderabbitai

coderabbitai Bot commented Apr 12, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@cursor[bot] has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 49 minutes and 24 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 49 minutes and 24 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1d9c6b5c-685b-4402-9701-add035d11ce5

📥 Commits

Reviewing files that changed from the base of the PR and between 2e9a242 and df2403a.

📒 Files selected for processing (1)
  • packages/epics/src/common/human-chat-panel/human-chat-panel-chat-bar.tsx

Walkthrough

Adds attachment dropdown (image/video/file), hidden video input, and MediaRecorder-based voice recording to the chat composer; pushes created file drafts, adjusts composer footer layout and controls, and adds i18n keys across five locales.

Changes

Cohort / File(s) Summary
Chat Composer with Voice & Attachment Features
packages/epics/src/common/human-chat-panel/human-chat-panel-chat-bar.tsx
Replaced per-button attach UI with a dropdown menu, added hidden video file input, implemented voice recording (MediaRecorder lifecycle, chunk collection, audio File creation with .m4a/.webm logic), pushDrafts integration, aria/state updates, and footer layout changes.
Internationalization Strings
packages/i18n/src/messages/{en,de,es,fr,pt}.json
Updated HumanChatPanel.newlineHint and added composer keys: composerAttachMenu, composerAttachImage, composerAttachVideo, composerAttachFile, mentionNotAvailable, composerVoiceRecord, composerVoiceStop, voiceRecordingNotSupported, voiceMicPermissionDenied.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant ChatBar as Chat Bar
    participant MediaAPI as Browser MediaRecorder API
    participant FileBuilder as Audio File Builder
    participant Drafts as Draft Attachment Manager

    User->>ChatBar: Click "record" (mic)
    ChatBar->>MediaAPI: request microphone access
    MediaAPI-->>ChatBar: permission granted / stream
    ChatBar->>MediaAPI: create MediaRecorder (choose supported MIME)
    MediaAPI-->>ChatBar: recording started
    Note over MediaAPI: collect audio chunks

    User->>ChatBar: Click "stop"
    ChatBar->>MediaAPI: stop recorder
    MediaAPI-->>ChatBar: chunks available
    ChatBar->>FileBuilder: convert chunks -> File (.m4a/.webm)
    FileBuilder-->>ChatBar: audio File
    ChatBar->>Drafts: pushDrafts([File], 'file')
    Drafts-->>ChatBar: draft added
    ChatBar->>User: update UI (clear recording state)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • alexprate
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title follows the conventional commits format with 'feat' type and 'chat' scope, and clearly describes the main changes: composer toolbar updates with attach menu and voice recording capabilities.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/composer-toolbar-plus-mic-ef7a

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.

@alexprate
alexprate force-pushed the cursor/composer-toolbar-plus-mic-ef7a branch 2 times, most recently from 0e74a31 to 06e7a41 Compare April 13, 2026 01:37
@alexprate
alexprate marked this pull request as ready for review April 13, 2026 01:53
coderabbitai[bot]
coderabbitai Bot previously requested changes Apr 13, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
packages/epics/src/common/human-chat-panel/human-chat-panel-chat-bar.tsx (1)

237-250: ⚠️ Potential issue | 🟠 Major

Append the voice clip against the latest draft list.

mr.onstop closes over pushDrafts, and pushDrafts clones the draftAttachments prop from the render that started recording. If the user adds/removes attachments while recording, stopping will rebuild from that stale snapshot and can overwrite those newer draft changes.

🛠️ Suggested fix
 export function HumanChatPanelChatBar({
   value,
   onChange,
   onSend,
@@
   const mediaRecorderRef = useRef<MediaRecorder | null>(null);
   const mediaStreamRef = useRef<MediaStream | null>(null);
+  const draftAttachmentsRef = useRef(draftAttachments);
   const [isVoiceRecording, setIsVoiceRecording] = useState(false);
   const [voiceError, setVoiceError] = useState<string | null>(null);
@@
+  useEffect(() => {
+    draftAttachmentsRef.current = draftAttachments;
+  }, [draftAttachments]);
+
   const pushDrafts = useCallback(
     (files: FileList | File[], kind: 'file' | 'image') => {
       if (!onDraftAttachmentsChange) return;
       const arr = Array.from(files);
-      const next: ChatDraftAttachment[] = [...draftAttachments];
+      const next: ChatDraftAttachment[] = [...draftAttachmentsRef.current];
       for (const file of arr) {
         if (kind === 'image' && !file.type.startsWith('image/')) {
           continue;
         }

Also applies to: 512-540, 626-643

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

In `@packages/epics/src/common/human-chat-panel/human-chat-panel-chat-bar.tsx`
around lines 237 - 250, mr.onstop currently closes over the render-time
draftAttachments and pushDrafts, so when recording stops it rebuilds attachments
from a stale snapshot and can overwrite user changes; update the logic in the
media recorder stop handler (mr.onstop) and in pushDrafts to read/merge against
a fresh source (e.g., keep a draftAttachmentsRef that you update in a useEffect
when prop draftAttachments changes, or use the state setter functional form like
setDraftAttachments(prev => [...prev, newVoiceClip]) ) and append the created
voice Blob (from recordedChunksRef) to that latest list instead of cloning the
prop captured at start of recording to avoid losing concurrent edits.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/epics/src/common/human-chat-panel/human-chat-panel-chat-bar.tsx`:
- Around line 1103-1111: Update the shipped composer footer copy to remove the
stale mention hint by editing the HumanChatPanel.newlineHint localization
entries: find the HumanChatPanel.newlineHint key in all locale/resource files
and replace the string that references "@" or mentions instructions with text
that reflects the current behavior (e.g., remove any "use @ to mention" clause
and keep only the newline/submit hint). Ensure every locale translation is
updated consistently (or marked for translation) so the UI copy matches the
disabled AtSign button in human-chat-panel-chat-bar.tsx.
- Around line 248-250: recordedChunksRef being a single shared ref causes chunk
mixing between rapid stop/start cycles; change the recorder logic so each
MediaRecorder instance gets its own chunks array (e.g., create a new local const
recordedChunks = [] inside the startRecording function and capture it in the
mediaRecorder.ondataavailable/onstop handlers or attach it as a property on the
MediaRecorder instance) instead of pushing into the module-level
recordedChunksRef; ensure mediaRecorder.stop() handlers use that per-recorder
array to build the Blob and that you clear/remove that per-recorder storage on
finish to avoid memory leaks (update references in the start/stop handlers and
anywhere recordedChunksRef is used, e.g., in the component methods that create
and stop the recorder).

---

Outside diff comments:
In `@packages/epics/src/common/human-chat-panel/human-chat-panel-chat-bar.tsx`:
- Around line 237-250: mr.onstop currently closes over the render-time
draftAttachments and pushDrafts, so when recording stops it rebuilds attachments
from a stale snapshot and can overwrite user changes; update the logic in the
media recorder stop handler (mr.onstop) and in pushDrafts to read/merge against
a fresh source (e.g., keep a draftAttachmentsRef that you update in a useEffect
when prop draftAttachments changes, or use the state setter functional form like
setDraftAttachments(prev => [...prev, newVoiceClip]) ) and append the created
voice Blob (from recordedChunksRef) to that latest list instead of cloning the
prop captured at start of recording to avoid losing concurrent edits.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2bb3f45a-aed4-4c14-b67e-a9dd3bad9e6c

📥 Commits

Reviewing files that changed from the base of the PR and between 41929c1 and 06e7a41.

📒 Files selected for processing (6)
  • packages/epics/src/common/human-chat-panel/human-chat-panel-chat-bar.tsx
  • packages/i18n/src/messages/de.json
  • packages/i18n/src/messages/en.json
  • packages/i18n/src/messages/es.json
  • packages/i18n/src/messages/fr.json
  • packages/i18n/src/messages/pt.json

Comment thread packages/epics/src/common/human-chat-panel/human-chat-panel-chat-bar.tsx Outdated
Replace paperclip/image/bold row with + attach submenu (image, video,
file), emoji, disabled @, and mic recording that adds audio via the same
draft pipeline as file uploads. Keeps hidden file inputs and attachment UI.

Co-authored-by: webguru-hypha <webguru-hypha@users.noreply.github.com>
@alexprate
alexprate force-pushed the cursor/composer-toolbar-plus-mic-ef7a branch from 06e7a41 to 73ab825 Compare April 13, 2026 02:08

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
packages/epics/src/common/human-chat-panel/human-chat-panel-chat-bar.tsx (1)

65-77: 🛠️ Refactor suggestion | 🟠 Major

Tighten the draft-attachment contract for the new media controls.

The new + and mic actions are always interactive, but they silently no-op when onDraftAttachmentsChange is missing. Since this prop is still optional, the component API now allows rendering shipped controls that can never attach anything.

Suggested fix
 type HumanChatPanelChatBarProps = {
   value: string;
   onChange: (value: string) => void;
   onSend: () => void;
   placeholder?: string;
   channelName?: string;
   replyPreview?: ReplyPreview;
   editPreview?: EditPreview;
   draftAttachments?: ChatDraftAttachment[];
-  onDraftAttachmentsChange?: (next: ChatDraftAttachment[]) => void;
+  onDraftAttachmentsChange: (next: ChatDraftAttachment[]) => void;
 };

Also applies to: 522-549, 599-600, 1055-1142

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

In `@packages/epics/src/common/human-chat-panel/human-chat-panel-chat-bar.tsx`
around lines 65 - 77, The prop contract allows rendering interactive media
controls that silently no-op if onDraftAttachmentsChange is missing; update
HumanChatPanelChatBarProps usage so the '+' and mic actions never render or are
disabled when onDraftAttachmentsChange is undefined (or make
onDraftAttachmentsChange required): locate the HumanChatPanelChatBar component
and its handlers for adding attachments and recording (the click/press handlers
tied to the '+' and mic UI), and change them to either hide/disable those
controls when onDraftAttachmentsChange is not provided or change the prop type
to require onDraftAttachmentsChange whenever draftAttachments is present,
ensuring all code paths that update draftAttachments call
onDraftAttachmentsChange only when it exists.
♻️ Duplicate comments (2)
packages/epics/src/common/human-chat-panel/human-chat-panel-chat-bar.tsx (2)

248-248: ⚠️ Potential issue | 🟠 Major

Keep recording chunks scoped to one MediaRecorder instance.

recordedChunksRef is shared across sessions. After mr.stop(), dataavailable/onstop finish asynchronously, so a quick stop→start can clear or reuse the same array before the first recorder flushes. That can drop the first clip or mix bytes from two recordings into one file.

Suggested fix
-  const recordedChunksRef = useRef<BlobPart[]>([]);
   const [isVoiceRecording, setIsVoiceRecording] = useState(false);
   const [voiceError, setVoiceError] = useState<string | null>(null);
...
-      recordedChunksRef.current = [];
+      const recordedChunks: BlobPart[] = [];
       const preferredTypes = [
         'audio/webm;codecs=opus',
         'audio/webm',
         'audio/mp4',
       ];
...
       mediaRecorderRef.current = mr;
       mr.ondataavailable = (ev) => {
         if (ev.data && ev.data.size > 0) {
-          recordedChunksRef.current.push(ev.data);
+          recordedChunks.push(ev.data);
         }
       };
       mr.onstop = () => {
         for (const track of stream.getTracks()) {
           track.stop();
         }
         mediaStreamRef.current = null;
         mediaRecorderRef.current = null;
-        const chunks = recordedChunksRef.current;
-        recordedChunksRef.current = [];
-        const blob = new Blob(chunks, { type: mr.mimeType || 'audio/webm' });
+        const blob = new Blob(recordedChunks, {
+          type: mr.mimeType || 'audio/webm',
+        });
What do the MediaRecorder spec/MDN docs say about `dataavailable` and `stop` event ordering after `MediaRecorder.stop()`, and is it safe to reuse one shared chunk array across rapid stop→start recording sessions?

Also applies to: 616-644

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

In `@packages/epics/src/common/human-chat-panel/human-chat-panel-chat-bar.tsx` at
line 248, recordedChunksRef is shared across recorder sessions which can cause
chunk loss or mixing when stop() triggers asynchronous dataavailable/onstop
events; instead create a chunks array scoped to each MediaRecorder instance
(e.g., allocate a new local array inside the start/initialize function and
capture it in the MediaRecorder event handlers, or associate a chunks array with
the recorder via a WeakMap or a property on the recorder) so that handlers for a
previous mr always push into its own array and you only use/clear the shared ref
after that recorder has fully finished; update places referencing
recordedChunksRef (the MediaRecorder creation/start logic and the
dataavailable/onstop handlers) to use the per-recorder array.

1113-1121: ⚠️ Potential issue | 🟡 Minor

Update the footer hint now that mentions are disabled.

The @ button is now a read-only placeholder, but Line 1163 still renders HumanChatPanel.newlineHint, and the locale strings still say @ to mention (for example, Line 1620 in packages/i18n/src/messages/en.json and packages/i18n/src/messages/pt.json). Please remove that instruction across locales so the shipped copy matches the actual behavior.

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

In `@packages/epics/src/common/human-chat-panel/human-chat-panel-chat-bar.tsx`
around lines 1113 - 1121, The footer hint still instructs users to use "@" to
mention even though the AtSign button is now a disabled placeholder; update the
UI and locales so the copy matches behavior by removing or changing the mention
instruction. In human-chat-panel-chat-bar.tsx, remove or update the usage of
HumanChatPanel.newlineHint (or the code path that renders the mention hint) so
it no longer references mentioning via "@", and then update the corresponding
locale entries in the i18n message files (e.g., the keys in
packages/i18n/src/messages/en.json and packages/i18n/src/messages/pt.json that
currently say "@ to mention") to a neutral newline/send hint that does not
instruct using "@". Ensure you modify the exact symbols
HumanChatPanel.newlineHint and the locale keys so UI text and translations no
longer promise mention functionality.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@packages/epics/src/common/human-chat-panel/human-chat-panel-chat-bar.tsx`:
- Around line 65-77: The prop contract allows rendering interactive media
controls that silently no-op if onDraftAttachmentsChange is missing; update
HumanChatPanelChatBarProps usage so the '+' and mic actions never render or are
disabled when onDraftAttachmentsChange is undefined (or make
onDraftAttachmentsChange required): locate the HumanChatPanelChatBar component
and its handlers for adding attachments and recording (the click/press handlers
tied to the '+' and mic UI), and change them to either hide/disable those
controls when onDraftAttachmentsChange is not provided or change the prop type
to require onDraftAttachmentsChange whenever draftAttachments is present,
ensuring all code paths that update draftAttachments call
onDraftAttachmentsChange only when it exists.

---

Duplicate comments:
In `@packages/epics/src/common/human-chat-panel/human-chat-panel-chat-bar.tsx`:
- Line 248: recordedChunksRef is shared across recorder sessions which can cause
chunk loss or mixing when stop() triggers asynchronous dataavailable/onstop
events; instead create a chunks array scoped to each MediaRecorder instance
(e.g., allocate a new local array inside the start/initialize function and
capture it in the MediaRecorder event handlers, or associate a chunks array with
the recorder via a WeakMap or a property on the recorder) so that handlers for a
previous mr always push into its own array and you only use/clear the shared ref
after that recorder has fully finished; update places referencing
recordedChunksRef (the MediaRecorder creation/start logic and the
dataavailable/onstop handlers) to use the per-recorder array.
- Around line 1113-1121: The footer hint still instructs users to use "@" to
mention even though the AtSign button is now a disabled placeholder; update the
UI and locales so the copy matches behavior by removing or changing the mention
instruction. In human-chat-panel-chat-bar.tsx, remove or update the usage of
HumanChatPanel.newlineHint (or the code path that renders the mention hint) so
it no longer references mentioning via "@", and then update the corresponding
locale entries in the i18n message files (e.g., the keys in
packages/i18n/src/messages/en.json and packages/i18n/src/messages/pt.json that
currently say "@ to mention") to a neutral newline/send hint that does not
instruct using "@". Ensure you modify the exact symbols
HumanChatPanel.newlineHint and the locale keys so UI text and translations no
longer promise mention functionality.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ea39cbbb-25b3-4f78-ae0e-56a3f272355e

📥 Commits

Reviewing files that changed from the base of the PR and between 06e7a41 and 73ab825.

📒 Files selected for processing (6)
  • packages/epics/src/common/human-chat-panel/human-chat-panel-chat-bar.tsx
  • packages/i18n/src/messages/de.json
  • packages/i18n/src/messages/en.json
  • packages/i18n/src/messages/es.json
  • packages/i18n/src/messages/fr.json
  • packages/i18n/src/messages/pt.json

Mentions are not available in the composer yet; align footer copy with the
disabled @ control across locales.

Co-authored-by: webguru-hypha <webguru-hypha@users.noreply.github.com>
@alexprate
alexprate self-requested a review April 13, 2026 19:51
@alexprate
alexprate enabled auto-merge April 13, 2026 19:51
Use a local chunks array captured by ondataavailable/onstop instead of a
shared ref so rapid stop/start cannot mix blobs (CodeRabbit).

Co-authored-by: webguru-hypha <webguru-hypha@users.noreply.github.com>
@alexprate
alexprate added this pull request to the merge queue Apr 13, 2026
Merged via the queue into main with commit 42ad633 Apr 13, 2026
7 of 8 checks passed
@alexprate
alexprate deleted the cursor/composer-toolbar-plus-mic-ef7a branch April 13, 2026 20:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants