Skip to content

desktop: show PTT/voice questions in chat instantly, before the reply#9016

Merged
kodjima33 merged 1 commit into
mainfrom
fix/ptt-question-instant-chat
Jul 4, 2026
Merged

desktop: show PTT/voice questions in chat instantly, before the reply#9016
kodjima33 merged 1 commit into
mainfrom
fix/ptt-question-instant-chat

Conversation

@kodjima33

@kodjima33 kodjima33 commented Jul 4, 2026

Copy link
Copy Markdown
Collaborator

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 (recordVoiceTurnrecordCompletedTurn), 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

  • 8 new unit tests on the real ChatProvider (instant-show, no-duplicate, corrected-text reconcile, unchanged legacy path, empty guard, consecutive turns, and two interleaved/out-of-order race cases).
  • Updated two DesktopCoordinatorService contract-string tests to the new recordVoiceTurn/recordVoiceAgentHandoff signatures (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

Review in cubic

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>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 issue found across 6 files

Confidence score: 3/5

  • In desktop/macos/Desktop/Sources/FloatingControlBar/RealtimeHubController.swift, hubDidReceiveAudio/hubDidEmitText can exit early when suppressAssistantOutputForCurrentTurn is 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()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>

@kodjima33 kodjima33 force-pushed the fix/ptt-question-instant-chat branch from d76c3b9 to 9efe2da Compare July 4, 2026 21:49
@kodjima33 kodjima33 merged commit f5e2515 into main Jul 4, 2026
2 checks passed
@kodjima33 kodjima33 deleted the fix/ptt-question-instant-chat branch July 4, 2026 21:49
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.

1 participant