feat(voice): dynamic voice list with preview#2
Closed
heavygee wants to merge 13 commits into
Closed
Conversation
Single canonical guide at docs/operator/AGENTS.md; merge=ours keeps AGENTS.md deleted when syncing tiann/hapi. Upstream PR branches must still be cut from upstream/main only. Co-authored-by: Cursor <cursoragent@cursor.com>
Adds a Voice dropdown in the Voice Assistant settings section, mirroring the existing language picker pattern. - New `web/src/lib/voices.ts` — curated list of 10 pre-made ElevenLabs voices with name, gender, and short description - `VoiceSessionConfig` gains optional `voiceId` field - `startRealtimeSession` and `VoiceContext.startVoice` read `hapi-voice-id` from localStorage and thread it through to the ElevenLabs `startSession` call via `overrides.tts.voice_id` - `buildVoiceAgentConfig` enables `tts.voice_id` override in `platform_settings` alongside the existing `language` override - Settings page renders the picker with Default + 10 named options; selection persists to localStorage immediately Default path: when no voice is stored the existing `cgSgspJ2msm6clMCkdW9` (Jessica) baked into the agent config is used unchanged. NOTE: branch cut from fork main — re-cut from upstream/main before opening a PR to tiann/hapi. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run>
- Add GET /api/voice/voices hub route proxying ElevenLabs /v1/voices (returns empty list gracefully when no API key configured) - Add fetchVoices() to ApiClient and web API layer with VoiceInfo type - Settings voice picker now fetches account voices dynamically on mount, including user's cloned voices (shown with "clone" badge) - Falls back to static built-in voice list if API returns empty - Add play/stop preview button per voice using ElevenLabs preview_url - Fix voice_id → voiceId in ElevenLabs override (SDK camelCase) via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run>
4 tasks
heavygee
added a commit
that referenced
this pull request
Jun 2, 2026
Address PR review (Major #2): the mutation onError was leaving the optimistic message in the visible thread as `failed`, which duplicated the restored composer text -- operator saw a failed bubble plus the same text in the composer, and could stack a stale failed turn next to a fresh send. The composer-restore + inline alert is the single retry surface for this failure mode now, so the optimistic row is dropped via removeOptimisticMessage() in the same handler. retryMessage stays exposed for any future code path that still produces failed rows. Test pins the new behaviour: the row is removed (not flipped to failed) on send failure. Co-authored-by: Cursor <cursoragent@cursor.com>
Owner
Author
|
Closing — superseded by upstream tiann#743 ( Verified the substantive bits already in
The 8 fork-only files in this PR ( Branch |
heavygee
added a commit
that referenced
this pull request
Jun 10, 2026
- Web `CursorMigrationBanner` now renders a "Manual review needed" state for `cursorMigrationState === 'ambiguous'` (Major #1: caller was promoting the metadata flag but no UI surfaced it). - Pin the md5-fixture contract for `workspaceHashFromPath`: raw, no-normalization, trailing-slash-distinct hashes computed via `printf '%s' <path> | md5sum` (Major #2: prevents algorithm drift that would silently revert path-priority discovery to fallback). - Snapshot full candidate set BEFORE the canonical fast-path resolves a single drawer so the `migrator:transplanted` log reports the decision-time count, not a post-rm undercount (Minor #1). - Warn log when canonical-path drawer is missing but readdir hands back exactly one candidate - regression-equivalent behaviour, but the size mismatch warrants a journalctl trail (path-normalization corner case the maintainer can grep for). - Boundary test: `messageCount = 101` (first value above the skip threshold) engages the size sanity check, pinning the cutoff contract (Nit). - Schema docstring on `cursorMigrationState` enum spelling out the banner contract per value (Nit). - syncEngine `getHapiMessageCount` warn-logs `countMessages` throws instead of silently downgrading to 0 (would chronically disable the floor). Drafted with claude-4.6-sonnet-thinking via Cursor; reviewed and tested by the operator. tiann#873. Co-authored-by: Cursor <cursoragent@cursor.com>
heavygee
added a commit
that referenced
this pull request
Jun 13, 2026
…dismissed (HAPI Bot, PR tiann#896) The previous state machine swallowed the migration banner if the operator reloaded the page before clicking dismiss: the migration flag was set on success, and on remount the init logic mapped a flag-set/dismiss-not-set session to 'pre-migrated', a state the banner explicitly refuses to render. Net effect: a migrated session never prompted for affirmative dismissal. Fixes: - Drop the 'pre-migrated' state. The dismissal flag is now the only signal that suppresses the banner; the migration flag alone means 'banner shows until dismissed' (now or after a reload). - Sessions that had nothing to migrate (no v1 entries in localStorage) pre-emptively write BOTH flags - migrated AND dismissed - so the bot's banner-stickiness fix doesn't surface a banner that has nothing to announce on freshly-created v2 sessions. Tests: - New `reload-before-dismiss leaves the banner visible` test pins the fix end-to-end: mount #1 migrates -> 'completed', unmount, mount #2 on the same session reads the localStorage flags and stays 'completed'. - New `opts fresh sessions out of the banner pre-emptively` test pins the no-v1-entries shortcut. - Existing `does not re-migrate on a mount where the migrated flag is already set` updated to assert 'completed' (not the dropped 'pre-migrated'). - Existing `skips migration when localStorage is empty` updated to assert the new 'dismissed' status + the banner-dismissed flag. - Banner test for the 'pre-migrated -> nothing' case removed (the state no longer exists). Co-authored-by: Cursor <cursoragent@cursor.com>
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
GET /api/voice/voiceshub route that proxies the ElevenLabs/v1/voicesendpoint, exposing all voices available to the configured API key — including user voice clonespreview_urlexistsRefs: tiann#686
Test plan
ELEVENLABS_API_KEYset — picker falls back to static list, preview buttons are visible but disabled with tooltip, and a 400 toast appears on startweb/src/routes/settings/index.test.tsxpasses (21/21)