refactor(frontend): remove continuous voice interrupt button - #297
Merged
Conversation
Backend barge-in already cancels TTS, so the call screen no longer needs a user-facing interrupt button.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
LUPENGHAN
added a commit
to LUPENGHAN/timeflow
that referenced
this pull request
Aug 20, 2026
A barge-in that lands after the model already finished delivering a reply cancelled whatever was playing *next*, not the reply the phone was still sounding out. Two halves: Backend: _Turn reset _audio_id before that late interrupted() ran, so it sent AudioCanceled(audio_id=""). Added _last_audio_id, which survives the reset, and send that instead -- the cancellation now names the audio it actually refers to. Frontend: voice.tts.canceled routed stop() through playbackChain, so every PCM chunk already queued was still fed to the native player before the stop landed. It now bypasses the chain via stopPlaybackImmediately(), and chainPlayback() tags each queued operation with a playbackGeneration that stop bumps, so the stale queue is dropped rather than replayed. tts.end and tts.canceled are both matched against currentAudioId/canceledAudioId, so the server's follow-up tts.end for a cancelled reply no longer ends a newer stream or flips interrupted back to listening. The empty-audio_id case is still handled on the client so a not-yet-updated backend cannot stop a newer reply. 1024XEngineer#297 removed only the user-facing interrupt button; backend barge-in still drives voice.tts.canceled, so this path is live. Carried over from 1024XEngineer#245, which this stacked series replaces -- not reminder-scoped, so none of 1024XEngineer#264-1024XEngineer#270 picked it up.
gac0812
pushed a commit
that referenced
this pull request
Aug 20, 2026
…271) * feat(reminder): add SQLite-backed reminder data layer Part of #263. SqliteLocalScheduleReader / SqliteReminderStateStore read and persist against the real local database (ScheduleLocalRepository) instead of in-memory fixtures; geofence_radius_meters is hardcoded to 200m for now (known simplification, see Issue #263 Out of Scope). InMemoryLocalScheduleReader is kept as a non-persisted alternative implementation of the same port. LocalScheduleWriter's post-write hook refreshes the new reader after a voice-driven schedule mutation lands. Only depends on application interfaces already on main and the existing ScheduleLocalRepository -- independent of the audio/location/ notifications adapter PRs in this stack. Removes MockLocalScheduleReader, MockReminderApplication, MockReminderDispositionSync, MockReminderStateStore, mockReminderSchedules. * feat(reminder): wire real engine into the app, drop remaining mocks Swaps the composition root over to the real implementations added in the previous three commits: LocalReminderApplication replaces MockReminderApplication, SqliteLocalScheduleReader/SqliteReminderStateStore replace their Mock counterparts, and every device port (audio/notification/vibration/alarm/location) now points at its real adapter. ExpoLocationMonitor (system geofencing) is used for location monitoring; NativeLocationMonitor (Baidu SDK) stays in the repo but unwired -- see the comment in createAppServices.ts for how to switch. AppProviders/AppRoot gain the reminder permission-request flow (useReminderPermissionsOnLaunch, now driven by an injected AlertDialogPort instead of calling Alert.alert directly, with a settings-page fallback for denied background-location permission) and rebuild() the engine once permissions change. Removes MockReminderPresenter, the last remaining Mock* adapter. * fix(reminder): permission flow stalled after the first prompt Every branch in promptNext() except the overlay/full_screen/battery confirm-and-continue path returns from inside the try block, so the setTimeout(runPrompt, 250) that was meant to advance to the next missing permission -- placed after the try/finally -- was dead code for those branches. In practice: grant notifications, and exact_alarm (the next permission in line) would just never get prompted; same for any declined dialog, or a granted/denied location permission. Moved the continuation check into the finally block so it always runs regardless of which branch returned. Also replaces the old mock-based useReminderPermissionsOnLaunch test (deleted upstream when this hook's signature changed to take an injected AlertDialogPort + onPermissionsUpdated callback, with no replacement written) and drops the now-redundant .gitkeep placeholders left over from directories that have had real files in them since earlier commits in this stack. * fix(voice): remove stale push-to-talk listeners AssistantConversationService.handleClose() nulled unsubscribeConnection without calling it. Switching from push-to-talk to continuous mode makes the shared AuthenticatedWebSocketClient drop and reopen the connection (the two modes negotiate different voiceMode), so the old service stayed subscribed to the new connection's TTS/PCM and pushed the same reply into the player alongside the continuous service -- audible as one sentence played twice, overlapping. dispose() already unsubscribed correctly; only the close path was missing it. Carried over from #245, which this stacked series replaces -- the fix is not reminder-scoped so none of #264-#270 picked it up. * fix(voice): preserve new TTS after interruption A barge-in that lands after the model already finished delivering a reply cancelled whatever was playing *next*, not the reply the phone was still sounding out. Two halves: Backend: _Turn reset _audio_id before that late interrupted() ran, so it sent AudioCanceled(audio_id=""). Added _last_audio_id, which survives the reset, and send that instead -- the cancellation now names the audio it actually refers to. Frontend: voice.tts.canceled routed stop() through playbackChain, so every PCM chunk already queued was still fed to the native player before the stop landed. It now bypasses the chain via stopPlaybackImmediately(), and chainPlayback() tags each queued operation with a playbackGeneration that stop bumps, so the stale queue is dropped rather than replayed. tts.end and tts.canceled are both matched against currentAudioId/canceledAudioId, so the server's follow-up tts.end for a cancelled reply no longer ends a newer stream or flips interrupted back to listening. The empty-audio_id case is still handled on the client so a not-yet-updated backend cannot stop a newer reply. #297 removed only the user-facing interrupt button; backend barge-in still drives voice.tts.canceled, so this path is live. Carried over from #245, which this stacked series replaces -- not reminder-scoped, so none of #264-#270 picked it up. * fix(reminder): reset permission-prompt state when the effect unmounts Code review (PR #271, fennoai): the cleanup only cleared the timer and unsubscribed onAppActive, not awaitingReturnRef/skippedRef. If a user opened settings for exact_alarm (or a denied location permission) and logged out before returning, AppProviders reruns this effect with device=null, but the stale awaitingReturnRef stayed true. On the next login the new effect's promptNext() reads that same ref (it's a component-level useRef, not reset by the effect re-running) and returns immediately every time, and since the app is already active there's no new onAppActive event left to clear it -- every permission prompt stays disabled until the process restarts. Reset both refs in the cleanup so a fresh login starts a clean prompt round. * test(reminder,voice): close Codecov patch-coverage gaps Codecov flagged 60.79% patch coverage across six files from this branch's recent commits. Closed each: - InMemoryLocalScheduleReader.ts: 0% because nothing in the app or tests actually uses it -- only re-exported from two barrels, never imported or instantiated anywhere in feature/reminder-wiring's own history. Deleted the file and its two re-exports instead of testing dead code. - AlertReminderPresenter.ts: new AlertReminderPresenter.test.ts covers every reason-specific message, the title fallback, confirm/snooze dispatch, unsubscribe, and hide()'s suppression window. The `?? '...'` message fallback is unreachable (MESSAGE_BY_REASON already covers every ReminderTriggerReason), so it's istanbul-ignored with a stated reason instead of faked with an invalid reason value. - useReminderPermissionsOnLaunch.ts: added 7 tests for branches the existing suite didn't reach -- denied notifications, failed openSettings on both the direct-settings and location paths, granted location, the bottom settings-redirect branch, a rejected getStatus(), and a dismissed (vs declined) dialog. Its own similarly-unreachable `prompt == null` branch (all 7 DevicePermission values already have a prompt) got the same istanbul-ignore treatment. - AppProviders.tsx: new AppProviders.test.tsx isolates the onPermissionsUpdated -> reminder.rebuild() wiring with a mocked useReminderPermissionsOnLaunch, instead of relying on AppRoot.test.tsx's much heavier integration setup for one line. - AssistantContinuousConversationService.ts: dismissReply() had no coverage at all before this branch touched one line of it (routing through stopPlaybackImmediately()); added a test that drives a reply through voice.tts.start/voice.dialogue.reply and asserts dismissReply() clears it and stops playback. - backend agent.py: the interrupted()-with-nothing-ever-spoken branch (_last_audio_id is None) wasn't exercised; added test_a_barge_in_before_any_reply_started_sends_no_cancellation. Verified: frontend tsc/eslint/prettier clean, Jest 520/520, Vitest 87/87; backend ruff/mypy clean, pytest 97.53% coverage.
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.
Summary
voice.tts.canceled),用户开口即可打断当前播报,无需前端再提供「打断当前对话」按钮。dismissReply的相关前端逻辑;底部只保留「结束对话」。Test plan