desktop: show PTT/voice questions in chat instantly, before the reply#9016
Merged
Conversation
Voice/PTT questions only appeared in the main chat page at turn end, appended together with the finished assistant reply — invisible until the model answered. Typed messages append optimistically and show immediately; this brings voice to parity. - ChatProvider.beginVoiceUserMessage: optimistically append the user's transcribed question the instant it's known (UI-only, no persist), returning the bubble. - recordCompletedTurn(earlyUserMessageId:): reconcile against THIS turn's bubble by explicit id (update text if language-corrected) instead of appending a duplicate. Per-turn id, not a shared global: voice completions run from async tasks and can land out of order vs the next turn's start, so a shared id would corrupt a newer bubble. - RealtimeHubController: fire the early append at the earliest reliable per-provider signal (OpenAI final input transcript; Gemini's first reply chunk), capturing the bubble id synchronously before the language-ID task. Skip the early show when the provider transcript classifies as a language the user did not pick (likely a Gemini misdetect) — the completed turn's on-device correction shows the right text instead of flashing a wrong-language bubble. Tests: 8 unit tests on the real ChatProvider incl. interleaved and out-of-order completions (no cross-turn corruption). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
1 issue found across 6 files
Confidence score: 3/5
- In
desktop/macos/Desktop/Sources/FloatingControlBar/RealtimeHubController.swift,hubDidReceiveAudio/hubDidEmitTextcan exit early whensuppressAssistantOutputForCurrentTurnis set, so Gemini handoff or suppressed-output turns may skip the new instant-display flow and make voice questions appear to stall or not surface promptly. Before merging, route these suppressed/handoff paths through the same instant-display trigger (or an equivalent fallback) and verify with a targeted handoff/suppressed-turn test run.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="desktop/macos/Desktop/Sources/FloatingControlBar/RealtimeHubController.swift">
<violation number="1" location="desktop/macos/Desktop/Sources/FloatingControlBar/RealtimeHubController.swift:1003">
P2: Voice questions can still miss the new “instant display” path on Gemini handoff/suppressed-output turns, because `hubDidReceiveAudio`/`hubDidEmitText` return early on `suppressAssistantOutputForCurrentTurn` before running `showUserQuestionEarly()`. Showing the user bubble before that suppression guard would preserve the no-meta-output behavior while keeping question display immediate.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| if !text.isEmpty { turnTranscript = text } | ||
| // OpenAI marks the input transcript final before/as the model starts replying — | ||
| // surface the question in chat now instead of waiting for the whole turn. | ||
| showUserQuestionEarly() |
There was a problem hiding this comment.
P2: Voice questions can still miss the new “instant display” path on Gemini handoff/suppressed-output turns, because hubDidReceiveAudio/hubDidEmitText return early on suppressAssistantOutputForCurrentTurn before running showUserQuestionEarly(). Showing the user bubble before that suppression guard would preserve the no-meta-output behavior while keeping question display immediate.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At desktop/macos/Desktop/Sources/FloatingControlBar/RealtimeHubController.swift, line 1003:
<comment>Voice questions can still miss the new “instant display” path on Gemini handoff/suppressed-output turns, because `hubDidReceiveAudio`/`hubDidEmitText` return early on `suppressAssistantOutputForCurrentTurn` before running `showUserQuestionEarly()`. Showing the user bubble before that suppression guard would preserve the no-meta-output behavior while keeping question display immediate.</comment>
<file context>
@@ -986,6 +998,9 @@ final class RealtimeHubController: NSObject, RealtimeHubSessionDelegate {
if !text.isEmpty { turnTranscript = text }
+ // OpenAI marks the input transcript final before/as the model starts replying —
+ // surface the question in chat now instead of waiting for the whole turn.
+ showUserQuestionEarly()
} else {
turnTranscript += text
</file context>
d76c3b9 to
9efe2da
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Voice/PTT questions now appear in the main chat page the instant they're transcribed — the same immediacy as a typed message — instead of only after the assistant finishes replying.
Why
Typed messages append optimistically and show immediately; voice turns were recorded to chat only at turn end (
recordVoiceTurn→recordCompletedTurn), appended together with the finished reply, so the question stayed invisible until the model answered.How
ChatProvider.beginVoiceUserMessage— optimistically append the transcribed question the instant it's known (UI-only, no persist), returning the bubble.recordCompletedTurn(earlyUserMessageId:)— reconcile against THIS turn's bubble by explicit per-turn id (update text if the transcript was language-corrected) instead of appending a duplicate. Per-turn id, not a shared global: voice completions run from async tasks and can land out of order vs the next turn's start, so a shared id would corrupt a newer turn's bubble.RealtimeHubController— fire the early append at the earliest reliable per-provider signal (OpenAI's final input transcript; Gemini's first reply chunk, since it never marks its input transcript final), capturing the bubble id synchronously before the language-ID task. Skip the early show when the provider transcript classifies as a language the user didn't pick (likely a Gemini misdetect) — the completed turn's on-device correction shows the right text instead of flashing a wrong-language bubble.Tests
ChatProvider(instant-show, no-duplicate, corrected-text reconcile, unchanged legacy path, empty guard, consecutive turns, and two interleaved/out-of-order race cases).DesktopCoordinatorServicecontract-string tests to the newrecordVoiceTurn/recordVoiceAgentHandoffsignatures (more specific, not weakened).Verification
Exercised the real hub path via
ptt_test_turn(real turn, PCM audio + forced provider transcript) with English selected: forced provider transcriptイヤホン を つけて(ja) → saved chat bubble corrected to "What is the capital of France?" (used_local_transcript: true). The instant-show and the misdetect gate both confirmed live.Follow-up (separate)
The misdetect correction only runs when a voice language is explicitly selected. Users who see "English ✓" via the settings fallback but never saved it get no correction — a migration to persist that fallback is tracked as follow-up work in the languages workstream.
🤖 Generated with Claude Code