Skip to content

Open upstream PR: voice notes and audio in chat - #364

Merged
leepokai merged 37 commits into
leepokai:devfrom
plotarmordev:feat/17-open-upstream-pr-voice-notes-and-audio-i
Nov 12, 2026
Merged

leepokai merged 37 commits into
leepokai:devfrom
plotarmordev:feat/17-open-upstream-pr-voice-notes-and-audio-i

Conversation

@plotarmordev

@plotarmordev plotarmordev commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

Summary

A bot can now send a voice note, and every client plays it in the chat.

  • Sound cards: a shared sound file (mp3, m4a, wav, aac, up to 20 MB) gets a play/pause button in place of its icon on iPhone, Android and desktop. It shows a spinner while the sound loads, then elapsed / length over a progress bar you can tap or drag to move through. If the sound can't be played, the card says "Couldn't play. Tap to retry." with no dialog, and the next tap loads it again. The sound comes through the same checked readFile path as inline images.
  • Voice note inside its message: send_file takes an optional text. With a sound the chat plays, the file goes into that message's bubble instead of a card of its own: the player sits at the top on one row (button, bar, time) with the Markdown text under it. Any other file sent with text is refused. The message counts as one the bot sent, the same as send_message, so the agent's earlier reply text stays in the trace. Older clients show the card alone. The terminal UI shows the file line with the text under it.
  • Accessibility: VoiceOver and TalkBack get one element per card or player, with play/pause, view and download as actions; the bar can be adjusted in five-second steps. On desktop each card is one keyboard stop: Enter or Space does its first action, the arrow keys move the sound, and the card's menu lists every action.
  • Calls and Siri: on iPhone, a call or Siri taking the audio pauses a playing note. Pausing it afterwards leaves alone the audio session a call or dictation took over.
  • Analytics: the host reports voice_note_sent. Each app reports voice_note_played when a tap starts a note from its beginning: played, or failed with which step broke (load or playback). The file and its text are never sent.
  • Docs: docs/features/file-attachments.md, docs/features/chat-messages.md, docs/reference/analytics-events.md and the user manual's web/content/docs/conversations.mdx.

I left the version bump and the WhatsNew.md sections for you to add when you release; my release notes are below.

Validation

  • Host: cargo fmt --check, cargo clippy --all-targets -- -D warnings and cargo test pass.
  • Desktop: npm ci, npm run typecheck and npm test pass (454 tests). In the real Electron app, a captioned voice note published by the host rendered, played, paused and played again; injected open and play failures reported load and playback.
  • iPhone: the CodyncUI tests (215) and CodyncKit tests pass on the iOS Simulator. New tests cover a tap during loading, failure and retry, a card closed mid-load, seeking, a call or Siri pausing a note, and a pause after a call took the audio session. I checked that the last two fail when their fix is removed.
  • Android: the voice-note unit tests pass.
  • Phone walkthrough at this head (1ed7bc27), iPhone (iOS Simulator, Maestro) and Android (emulator), each signed into a local test account with a second host whose bot sent five captioned voice notes (one a corrupt .m4a):
    • Playback: each note loads with a spinner, then plays with elapsed / length and the bar moving.
    • Switching: starting a note while another plays pauses the first, which keeps its place.
    • Failure and retry: the corrupt note shows "Couldn't play. Tap to retry." with no dialog; the next tap loads it again and fails the same way, and the note that was playing keeps playing.
    • Leaving a loading card: with the host paused, a tap left the card loading; going back to the chat list and returning showed it idle, and a tap then played it.
  • Not covered on the phones: a call or Siri taking the audio (the simulator and emulator have no calls); the iPhone tests above cover it.

What's New: iPhone

zh-Hant

  • 機器人傳來的語音訊息和音訊檔現在可以直接在聊天中播放:顯示進度條與時間,點按或拖曳即可跳轉。
  • 帶有語音的訊息會在泡泡頂端顯示播放器,文字顯示在下方。

en-US

  • Voice notes and sound files a bot sends now play right in the chat, with a progress bar and time you can tap or drag to jump around.
  • A message that carries a voice note shows its player at the top of the bubble, with the text underneath.

What's New: Android

zh-Hant

  • 機器人傳來的語音訊息和音訊檔現在可以直接在聊天中播放:顯示進度條與時間,點按或拖曳即可跳轉。
  • 帶有語音的訊息會在泡泡頂端顯示播放器,文字顯示在下方。

en-US

  • Voice notes and sound files a bot sends now play right in the chat, with a progress bar and time you can tap or drag to jump around.
  • A message that carries a voice note shows its player at the top of the bubble, with the text underneath.

send_file takes an optional text: the file, a sound the chat plays
(mp3, m4a, wav or aac up to 20 MiB), then goes in that message's bubble
instead of a card of its own, and the message counts as one the bot
sent. Any other file with text is refused. The terminal UI shows the
file line with the text under it.
A shared sound file gets a play/pause button in place of its icon,
fetched through the same verified readFile path as inline images: a
spinner while it loads, elapsed / length over a progress bar that a tap
or drag moves through, and "Couldn't play. Tap to retry." on failure.
A message carrying a voice note shows the player at the top of its
bubble, play button, bar and time on one row, with the text under it.
I added the new strings to every language table, which the desktop
app shares.
The same card and in-message player as on iPhone, with a click or the
arrow keys to move through a sound. Each file card is one keyboard and
screen-reader stop: Enter or Space does its first action and its menu
lists play, view and download.
The same card and in-message player as on iPhone, with TalkBack
actions for play, pause and download.
How a sound file plays on its card, and how send_file's text puts a
voice note inside its message.
A voice note counts as a message the bot sent, so like send_message it
clears the last reply text: what the agent wrote before it no longer
becomes the turn's final reply once a steered message joins.
Pausing before play() settles rejects that play with AbortError; the
player no longer treats it as a failure that drops the loaded sound.
A tap whose scope goes while the sound loads (a dismissed preview) left
the card loading for good; it now goes back to play.
voice_note_sent from the host when a bot's send_file carries text, and
voice_note_played (played or failed) from each app when a tap starts a
note from its beginning; never the file or the text.
A note interrupted by a call or Siri now shows paused instead of
playing. Pausing it no longer turns off an audio session that a call
or dictation took in the meantime.
…oke at

A failed voice_note_played now carries error: load (fetching or opening
the sound) or playback (starting it), so failures can be told apart
without reporting the file, its content or the raw error.
A call, Siri or dictation that takes the audio now has tests: an
interruption pauses a playing note, and pausing a note after a call
switched the session to play-and-record leaves the call's session
active. Session deactivation is injected like the note's fetch so the
test can see it.
@github-actions github-actions Bot added enhancement New feature or request P3: low Nice to have: features, questions, polish. labels Nov 7, 2026
@greptile-apps

greptile-apps Bot commented Nov 7, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] No new blocking issue was established in this review.

Summary

Type: feature | Areas: host, iOS, Android, desktop, TUI, web, docs | Priority: P3: low

Adds voice-note playback and captioned audio messages across the chat clients.

  • Sound files play right in chat with a seek bar and retry controls.
  • Bots can send a voice note with its message in one chat bubble.
  • iPhone voice-note playback pauses for calls and leaves their audio session alone.

Diagram

sequenceDiagram
  participant Bot
  participant Host
  participant Chat as Chat client
  Bot->>Host: send_file(path, text)
  Host->>Host: Check text and save file snapshot
  Host-->>Chat: Message with voiceNote and playsInChat
  Chat->>Host: Read file chunks
  Chat->>Chat: Check size and SHA-256
  Chat->>Chat: Play audio above message text
Loading

Reviews (16) · Last reviewed commit: "Merge remote-tracking branch 'origin/dev..." · Reviewed by Greptile

Each note's player took the shared audio session, so pausing one cut off another still playing. Starting a note now pauses the one sounding, so only one owns the session, as Android's audio focus already does.
A decode error after playback started left the card on Pause. iOS now handles an unsuccessful finish and decode errors, Android a MediaPlayer error, both dropping the player so the next tap loads again. Android also prepares the player asynchronously so opening a long note never blocks the chat.
@plotarmordev

Copy link
Copy Markdown
Contributor Author

Thanks for the review. I fixed all five in 08b2585..1ed7bc2:

  1. One note stops another (08b2585): starting a voice note on iOS now pauses the one already playing, so only one player owns the audio session at a time. Android already behaves this way through audio focus. New test: startingASecondVoiceNotePausesTheFirstSoItKeepsTheAudio.
  2. Broken recordings miss retry state (e648a98): PlayDone now reports an unsuccessful finish and audioPlayerDecodeErrorDidOccur, and SharedAudio sends both to fail(). On Android, MediaSound sets an error listener that drops the player and sets the card to Failed, so the next tap reloads it. New test: a sound that breaks while playing fails the card and the next tap loads again.
  3. Opening audio freezes chat (e648a98): MediaSound.open now uses prepareAsync() and suspends until it is prepared or fails, so the UI thread never blocks.
  4. File cards ignore double-tap (3ec697a): a VoiceOver double tap now runs the card's first action (play, view, else download), the same order Android and desktop use.
  5. Menu keys move the recording (bc6dcfe): cardKeyAction now also handles seeking, so an open menu blocks the arrow keys as it already blocked Enter and Space. I extended the test to check this.

Checks: desktop npm run typecheck && npm test (454 pass); iOS CodyncUITests (216 pass); Android SharedAudioTest, assembleDebug and lintDebug. I did not repeat the full-app walkthrough for these fixes.

@plotarmordev

Copy link
Copy Markdown
Contributor Author

I walked both phone apps through it at 1ed7bc2 against a real host: playback, switching notes mid-play, a corrupt note's failure and retry, and leaving a card while it loads, on the iPhone simulator and the Android emulator. All behaved as described; results are in Validation. Calls and Siri can't be exercised on a simulator, so those stay covered by the iPhone tests. No code changed.

…-open-upstream-pr-voice-notes-and-audio-i

# Conflicts:
#	apps/desktop/src/renderer/components/Overlay.tsx
#	apps/ios/Kit/Sources/CodyncKit/Resources/Languages/de.json
#	apps/ios/Kit/Sources/CodyncKit/Resources/Languages/es.json
#	apps/ios/Kit/Sources/CodyncKit/Resources/Languages/fr.json
#	apps/ios/Kit/Sources/CodyncKit/Resources/Languages/it.json
#	apps/ios/Kit/Sources/CodyncKit/Resources/Languages/ja.json
#	apps/ios/Kit/Sources/CodyncKit/Resources/Languages/ko.json
#	apps/ios/Kit/Sources/CodyncKit/Resources/Languages/pt-BR.json
#	apps/ios/Kit/Sources/CodyncKit/Resources/Languages/ru.json
#	apps/ios/Kit/Sources/CodyncKit/Resources/Languages/zh-Hans.json
#	apps/ios/Kit/Sources/CodyncKit/Resources/Languages/zh-Hant.json
A load the card outlived (stopped while it fetched) no longer installs
its player or resets the state, so a tap made since keeps its player,
controls and clock.
@plotarmordev

Copy link
Copy Markdown
Contributor Author

Fixed in afa8098: prepared now checks the tap's generation right after the fetch returns and throws before touching player, done, duration or state, so a stale load leaves the newer tap's player and controls alone. I added aStaleLoadFinishingAfterANewerTapLeavesThatTapPlaying, which answers the newer load first and the stale one second; it fails without the fix and passes with it.

…-open-upstream-pr-voice-notes-and-audio-i

# Conflicts:
#	host/src/analytics/reported.rs
@plotarmordev

Copy link
Copy Markdown
Contributor Author

I fixed the desktop retry caption in 3eac701 and added the exact Click phrase to all ten language catalogs. File cards and voice-note bubbles subscribe to language changes, so an existing error also updates immediately. I verified a corrupt note, retry, and French-to-English switching in the real Electron app; the new Playwright regression passes, along with desktop typechecking, 488 unit tests and the docs check. The full Playwright run was not green: 17 passed, 5 skipped and 8 failed, with later flows stranded on the welcome screen after the sign-in tests. I made the new voice-note test enter without an account when needed and verified it passes after that suite; I left unrelated tests unchanged. Android already translates its Tap caption and uses per-player audio focus; the terminal has no audio playback.

@plotarmordev

Copy link
Copy Markdown
Contributor Author

I fixed the stale recording settings in 3eac701. SharedAudio now checks active call, dictation and WebRTC ownership instead of the saved category; after recording ends it explicitly resets playback mode/options and owns the matching cleanup. I added simulator regressions for an ended call and live-then-stopped dictation, and updated the takeover test to activate a real call session. All 454 Swift package tests pass; the iOS Dev app also built, installed and launched. Physical microphone/routing validation remains unavailable because no physical iPhone is connected; Maestro is not installed here.

…-open-upstream-pr-voice-notes-and-audio-i

# Conflicts:
#	host/src/analytics/reported.rs
TalkBack's adjust passed its requested value straight to seek, so a note
jumped by a share of its length rather than the five seconds iOS and the
desktop move. Each adjust now steps five seconds in the direction asked;
dragging and tapping the bar still seek continuously.
@plotarmordev

Copy link
Copy Markdown
Contributor Author

Fixed in a4d3bbb: on Android each TalkBack adjust now moves the note five seconds in the direction asked, like VoiceOver on iOS and the arrow keys on desktop. Tapping or dragging the bar still seeks continuously. seekStepTarget has a unit test in FileCardCaptionTest. I couldn't try TalkBack itself here because there's no emulator running.

…-open-upstream-pr-voice-notes-and-audio-i

# Conflicts:
#	host/src/analytics/reported.rs
Right-clicking a shared file card or a voice note's player opened only
its file actions: Reply, Copy, reactions and Show what it did were
gone. The card's menu now lists its own actions, then the message's,
with the message's reactions above, as long-press does on the phones.
@plotarmordev

Copy link
Copy Markdown
Contributor Author

Fixed both in 0284da4 and 332cb6b.

  • File cards lose message actions: right-clicking a file card or a voice note's player now opens its own actions (Play, View, Download), then the message's (Copy, Reply, Show what it did) with its reactions above, like a long-press on the phones. I extended e2e/voice-note.spec.ts to check the menu; it fails on the previous head and passes now.
  • Audio cleanup errors disappear: SharedAudio's default deactivate now logs a failed setActive(false) instead of using try?. Nothing more to handle there: the note has already stopped.

plotarmordev and others added 4 commits November 11, 2026 14:44
…-open-upstream-pr-voice-notes-and-audio-i

# Conflicts:
#	host/src/analytics.rs
…-open-upstream-pr-voice-notes-and-audio-i

# Conflicts:
#	apps/android/app/src/main/kotlin/com/codync/android/AnalyticsEvent.kt
#	apps/ios/Kit/Sources/CodyncKit/Analytics/AnalyticsEvent.swift
A bot's voice note plays at the top of its message with the text under it,
sound files play with a seekable progress bar on every app, and the iPhone
and Android apps get an end-to-end check for both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@plotarmordev

Copy link
Copy Markdown
Contributor Author

Merged dev into the branch (10d1558) and resolved the conflict in the analytics event lists: both sides' events are kept, voice_note_played alongside conversation_find_opened/conversation_find_searched, on iOS and Android. Host fmt/clippy/tests, the iOS Kit tests, the Android checks and the desktop typecheck/tests all pass locally.

leepokai and others added 7 commits November 12, 2026 18:52
# Conflicts:
#	apps/Codync.xcodeproj/project.pbxproj
#	apps/android/WhatsNew.md
#	apps/desktop/package-lock.json
#	apps/desktop/package.json
#	apps/ios/WhatsNew.md
#	apps/project.yml
#	host/Cargo.lock
#	host/Cargo.toml
…-open-upstream-pr-voice-notes-and-audio-i

# Conflicts:
#	apps/android/app/src/main/kotlin/com/codync/android/AttachmentList.kt
#	apps/android/app/src/main/kotlin/com/codync/android/ChatRows.kt
#	apps/android/app/src/main/kotlin/com/codync/android/FileDownloadCard.kt
#	apps/desktop/src/renderer/store/file-downloads.ts
#	apps/desktop/src/renderer/views/thread/Attachments.tsx
#	apps/desktop/src/renderer/views/thread/ChatRows.tsx
#	apps/desktop/src/renderer/views/thread/FileDownloadCard.tsx
#	apps/desktop/src/renderer/views/thread/attachments.css
#	apps/ios/Kit/Sources/CodyncUI/Store/FileDownloads.swift
#	apps/ios/Kit/Sources/CodyncUI/Thread/ChatRows.swift
#	apps/ios/Kit/Sources/CodyncUI/Thread/FileDownloadCard.swift
#	web/content/docs/conversations.mdx
# Conflicts:
#	apps/android/WhatsNew.md
#	apps/android/app/src/main/kotlin/com/codync/android/AttachmentList.kt
#	apps/android/app/src/main/kotlin/com/codync/android/ChatRows.kt
#	apps/android/app/src/main/kotlin/com/codync/android/FileDownloadCard.kt
#	apps/desktop/src/renderer/store/file-downloads.ts
#	apps/desktop/src/renderer/views/thread/Attachments.tsx
#	apps/desktop/src/renderer/views/thread/ChatRows.tsx
#	apps/desktop/src/renderer/views/thread/FileDownloadCard.tsx
#	apps/desktop/src/renderer/views/thread/attachments.css
#	apps/ios/Kit/Sources/CodyncUI/Store/FileDownloads.swift
#	apps/ios/Kit/Sources/CodyncUI/Thread/ChatRows.swift
#	apps/ios/Kit/Sources/CodyncUI/Thread/FileDownloadCard.swift
#	apps/ios/WhatsNew.md
#	web/content/docs/conversations.mdx
# Conflicts:
#	apps/android/WhatsNew.md
#	apps/ios/WhatsNew.md
# Conflicts:
#	apps/android/WhatsNew.md
#	apps/ios/WhatsNew.md
@leepokai
leepokai merged commit abb1c32 into leepokai:dev Nov 12, 2026
15 checks passed
@github-actions

Copy link
Copy Markdown

Thanks for your first merged contribution, @plotarmordev! Join the Codync Discord with the Contributor role in one step: https://discord.codync.dev

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

Labels

enhancement New feature or request P3: low Nice to have: features, questions, polish.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants