agentvibes 5.10.1 → 5.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/.agentvibes/config.json +15 -1
  2. package/.agentvibes/install-manifest.json +342 -0
  3. package/.claude/audio/ui/CREDITS.txt +16 -0
  4. package/.claude/audio/ui/bling-success.wav +0 -0
  5. package/.claude/commands/agent-vibes/receiver.md +64 -0
  6. package/.claude/commands/agent-vibes-bmad-voices.md +117 -117
  7. package/.claude/commands/agent-vibes-rdp.md +24 -24
  8. package/.claude/config/audio-effects.cfg +5 -4
  9. package/.claude/config/audio-effects.cfg.sample +52 -52
  10. package/.claude/config/background-music-enabled.txt +1 -1
  11. package/.claude/config/background-music-position.txt +27 -0
  12. package/.claude/config/background-music-volume.txt +1 -0
  13. package/.claude/config/background-music.cfg +1 -0
  14. package/.claude/config/background-music.txt +1 -0
  15. package/.claude/config/language.txt +1 -0
  16. package/.claude/config/personality.txt +1 -0
  17. package/.claude/config/reverb-level.txt +1 -0
  18. package/.claude/config/tts-speech-rate.txt +1 -0
  19. package/.claude/config/tts-verbosity.txt +1 -0
  20. package/.claude/docs/TERMUX_SETUP.md +408 -408
  21. package/.claude/github-star-reminder.txt +1 -1
  22. package/.claude/hooks/audio-cache-utils.sh +0 -0
  23. package/.claude/hooks/audio-processor.sh +57 -18
  24. package/.claude/hooks/background-music-manager.sh +0 -0
  25. package/.claude/hooks/bmad-party-speak.sh +0 -0
  26. package/.claude/hooks/bmad-speak-enhanced.sh +0 -0
  27. package/.claude/hooks/bmad-speak.sh +0 -0
  28. package/.claude/hooks/bmad-tts-injector.sh +0 -0
  29. package/.claude/hooks/bmad-voice-manager.sh +0 -0
  30. package/.claude/hooks/clawdbot-receiver-SECURE.sh +0 -0
  31. package/.claude/hooks/clawdbot-receiver.sh +0 -0
  32. package/.claude/hooks/clean-audio-cache.sh +0 -0
  33. package/.claude/hooks/cleanup-cache.sh +0 -0
  34. package/.claude/hooks/configure-rdp-mode.sh +0 -0
  35. package/.claude/hooks/download-extra-voices.sh +0 -0
  36. package/.claude/hooks/effects-manager.sh +0 -0
  37. package/.claude/hooks/github-star-reminder.sh +0 -0
  38. package/.claude/hooks/kokoro-installer.sh +117 -0
  39. package/.claude/hooks/kokoro-server.py +219 -0
  40. package/.claude/hooks/kokoro-tts.py +141 -0
  41. package/.claude/hooks/language-manager.sh +0 -0
  42. package/.claude/hooks/learn-manager.sh +0 -0
  43. package/.claude/hooks/macos-voice-manager.sh +0 -0
  44. package/.claude/hooks/migrate-background-music.sh +0 -0
  45. package/.claude/hooks/migrate-to-agentvibes.sh +0 -0
  46. package/.claude/hooks/optimize-background-music.sh +0 -0
  47. package/.claude/hooks/path-resolver.sh +0 -0
  48. package/.claude/hooks/personality-manager.sh +0 -0
  49. package/.claude/hooks/piper-download-voices.sh +39 -16
  50. package/.claude/hooks/piper-installer.sh +11 -6
  51. package/.claude/hooks/piper-multispeaker-registry.sh +0 -0
  52. package/.claude/hooks/piper-voice-manager.sh +6 -6
  53. package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +3 -1
  54. package/.claude/hooks/play-tts-agentvibes-receiver.sh +1 -0
  55. package/.claude/hooks/play-tts-elevenlabs.sh +360 -0
  56. package/.claude/hooks/play-tts-enhanced.sh +0 -0
  57. package/.claude/hooks/play-tts-kokoro.sh +127 -0
  58. package/.claude/hooks/play-tts-macos.sh +0 -0
  59. package/.claude/hooks/play-tts-piper.sh +0 -0
  60. package/.claude/hooks/play-tts-soprano.sh +0 -0
  61. package/.claude/hooks/play-tts-ssh-remote.sh +9 -2
  62. package/.claude/hooks/play-tts-termux-ssh.sh +3 -1
  63. package/.claude/hooks/play-tts-windows-receiver.sh +0 -0
  64. package/.claude/hooks/play-tts.sh +78 -2
  65. package/.claude/hooks/prepare-release.sh +0 -0
  66. package/.claude/hooks/provider-commands.sh +71 -22
  67. package/.claude/hooks/provider-manager.sh +15 -7
  68. package/.claude/hooks/replay-target-audio.sh +0 -0
  69. package/.claude/hooks/requirements.txt +6 -6
  70. package/.claude/hooks/sentiment-manager.sh +0 -0
  71. package/.claude/hooks/session-start-tts.sh +0 -0
  72. package/.claude/hooks/soprano-gradio-synth.py +139 -139
  73. package/.claude/hooks/speed-manager.sh +0 -0
  74. package/.claude/hooks/stop-tts.sh +0 -0
  75. package/.claude/hooks/termux-installer.sh +0 -0
  76. package/.claude/hooks/translate-manager.sh +0 -0
  77. package/.claude/hooks/translator.py +237 -237
  78. package/.claude/hooks/tts-queue-worker.sh +0 -0
  79. package/.claude/hooks/tts-queue.sh +0 -0
  80. package/.claude/hooks/verbosity-manager.sh +0 -0
  81. package/.claude/hooks/voice-manager.sh +0 -0
  82. package/.claude/hooks-windows/audio-cache-utils.ps1.user.bak +119 -0
  83. package/.claude/hooks-windows/kokoro-server.py +219 -0
  84. package/.claude/hooks-windows/kokoro-tts.py +107 -0
  85. package/.claude/hooks-windows/play-tts-kokoro.ps1 +191 -0
  86. package/.claude/hooks-windows/play-tts.ps1 +29 -1
  87. package/.claude/hooks-windows/soprano-gradio-synth.py +153 -153
  88. package/.claude/hooks-windows/soprano-gradio-synth.py.user.bak +153 -0
  89. package/.claude/piper-voices-dir.txt +1 -0
  90. package/.claude/verbosity.txt +1 -1
  91. package/.clawdbot/README.md +105 -105
  92. package/.mcp.json +6 -19
  93. package/README.md +127 -2116
  94. package/RELEASE_NOTES.md +73 -0
  95. package/WINDOWS-SETUP.md +208 -208
  96. package/bin/agent-vibes +39 -39
  97. package/bin/agentvibes-voice-browser.js +0 -0
  98. package/bin/agentvibes.js +0 -0
  99. package/bin/mcp-server.js +121 -121
  100. package/bin/mcp-server.sh +0 -0
  101. package/bin/test-bmad-pr +78 -78
  102. package/mcp-server/QUICK_START.md +203 -203
  103. package/mcp-server/README.md +345 -345
  104. package/mcp-server/WINDOWS_SETUP.md +0 -0
  105. package/mcp-server/examples/claude_desktop_config.json +11 -11
  106. package/mcp-server/examples/claude_desktop_config_piper.json +9 -9
  107. package/mcp-server/examples/custom_instructions.md +169 -169
  108. package/mcp-server/install-deps.js +0 -0
  109. package/mcp-server/server.py +1807 -1807
  110. package/mcp-server/test_server.py +0 -0
  111. package/package.json +4 -3
  112. package/src/cli/list-personalities.js +110 -110
  113. package/src/cli/list-voices.js +114 -114
  114. package/src/commands/bmad-voices.js +394 -394
  115. package/src/commands/install-mcp.js +3 -3
  116. package/src/console/app.js +25 -9
  117. package/src/console/audio-env.js +85 -1
  118. package/src/console/brand-colors.js +13 -13
  119. package/src/console/constants/personalities.js +44 -44
  120. package/src/console/navigation.js +4 -0
  121. package/src/console/tabs/agents-tab.js +12 -6
  122. package/src/console/tabs/help-tab.js +314 -314
  123. package/src/console/tabs/music-tab.js +7 -25
  124. package/src/console/tabs/readme-tab.js +272 -272
  125. package/src/console/tabs/setup-tab.js +1704 -120
  126. package/src/console/tabs/voices-tab.js +72 -88
  127. package/src/console/widgets/destroy-list.js +25 -25
  128. package/src/console/widgets/format-utils.js +14 -2
  129. package/src/console/widgets/help-bar.js +55 -0
  130. package/src/console/widgets/notice.js +55 -55
  131. package/src/console/widgets/reverb-picker.js +429 -41
  132. package/src/console/widgets/track-picker.js +60 -51
  133. package/src/i18n/de.js +2 -1
  134. package/src/i18n/en.js +1 -0
  135. package/src/i18n/es.js +2 -1
  136. package/src/i18n/fr.js +2 -1
  137. package/src/i18n/hi.js +2 -1
  138. package/src/i18n/ja.js +2 -1
  139. package/src/i18n/ko.js +2 -1
  140. package/src/i18n/pt.js +2 -1
  141. package/src/i18n/strings.js +9 -9
  142. package/src/i18n/zh-CN.js +2 -1
  143. package/src/installer/language-screen.js +31 -31
  144. package/src/installer/music-file-input.js +304 -304
  145. package/src/installer.js +95 -7
  146. package/src/services/config-service.js +264 -264
  147. package/src/services/language-service.js +47 -47
  148. package/src/services/provider-service.js +35 -0
  149. package/src/services/provider-voice-catalog.js +126 -0
  150. package/src/services/tts-engine-service.js +51 -2
  151. package/src/utils/audio-duration-validator.js +53 -10
  152. package/src/utils/audio-format-validator.js +277 -277
  153. package/src/utils/dependency-checker.js +469 -469
  154. package/src/utils/file-ownership-verifier.js +358 -358
  155. package/src/utils/music-file-validator.js +285 -285
  156. package/src/utils/preview-list-prompt.js +144 -136
  157. package/src/utils/provider-validator.js +70 -0
  158. package/src/utils/secure-music-storage.js +412 -412
  159. package/templates/agentvibes-receiver.sh +235 -231
  160. package/templates/audio/welcome-music.mp3 +0 -0
@@ -571,7 +571,7 @@ async function downloadPiperVoices(agentVibesDir) {
571
571
  short: 'LibriTTS 900'
572
572
  },
573
573
  {
574
- name: chalk.cyan('Full pack') + chalk.gray(' — 11 voices including LibriTTS (~250 MB, all BMAD agent voices)'),
574
+ name: chalk.cyan('Full pack') + chalk.gray(' — 17 voices including LibriTTS (~360 MB, all voices-tab curated + BMAD agent voices)'),
575
575
  value: 'full',
576
576
  short: 'Full pack'
577
577
  },
@@ -618,8 +618,8 @@ async function downloadPiperVoices(agentVibesDir) {
618
618
  console.log(chalk.yellow('\n⚠️ Download failed — try later with: /agent-vibes:add\n'));
619
619
  }
620
620
  } else if (voiceChoice === 'full') {
621
- console.log(chalk.cyan('\n📥 Downloading full voice pack (~250 MB)...\n'));
622
- console.log(chalk.gray(' This includes all BMAD agent voices plus LibriTTS 900 speakers.\n'));
621
+ console.log(chalk.cyan('\n📥 Downloading full voice pack (~360 MB)...\n'));
622
+ console.log(chalk.gray(' This includes all voices-tab curated voices, BMAD agent voices, and LibriTTS 900 speakers.\n'));
623
623
  try {
624
624
  if (fs.existsSync(fullPackScript)) {
625
625
  execFileSync('bash', [fullPackScript, '--yes'], { // NOSONAR
@@ -9,7 +9,8 @@
9
9
 
10
10
  import blessed from 'blessed';
11
11
  import path from 'node:path';
12
- import { readFileSync } from 'node:fs';
12
+ import { readFileSync, existsSync } from 'node:fs';
13
+ import os from 'node:os';
13
14
  import { fileURLToPath } from 'node:url';
14
15
  import { spawnSync, execFileSync } from 'node:child_process';
15
16
  import { NavigationService, TAB_ORDER } from '../services/navigation-service.js';
@@ -630,21 +631,36 @@ export class AgentVibesConsole {
630
631
  try { execFileSync('which', [bin], { stdio: 'ignore', timeout: 2000 }); return true; } // NOSONAR
631
632
  catch { return false; }
632
633
  };
634
+ const _pyHas = (mod) => {
635
+ try { execFileSync('python3', ['-c', `import ${mod}`], { stdio: 'ignore', timeout: 3000 }); return true; } // NOSONAR
636
+ catch { return false; }
637
+ };
638
+ const _elKeySet = () => {
639
+ if (process.env.ELEVENLABS_API_KEY) return true;
640
+ try {
641
+ const kf = path.join(os.homedir(), '.agentvibes', 'elevenlabs-key.txt');
642
+ return existsSync(kf) && readFileSync(kf, 'utf8').trim().length > 0;
643
+ } catch { return false; }
644
+ };
633
645
  const detected = {
634
- piper: _has('piper'),
635
- soprano: _has('soprano'),
636
- sapi: process.platform === 'win32',
637
- macos: process.platform === 'darwin' && _has('say'),
646
+ piper: _has('piper'),
647
+ kokoro: _pyHas('kokoro'),
648
+ elevenlabs: _elKeySet(),
649
+ soprano: _has('soprano'),
650
+ sapi: process.platform === 'win32',
651
+ macos: process.platform === 'darwin' && _has('say'),
638
652
  };
639
653
 
640
654
  // Build provider status badges: ● Name (green if detected, grey if not)
641
655
  const on = (label) => `{green-fg}●{/green-fg} ${label}`;
642
656
  const off = (label) => `{#546e7a-fg}● ${label}{/#546e7a-fg}`;
643
657
  const badges = [
644
- detected.piper ? on('Piper') : off('Piper'),
645
- detected.soprano ? on('Soprano') : off('Soprano'),
646
- detected.sapi ? on('Windows SAPI') : off('Windows SAPI'),
647
- detected.macos ? on('Mac Say') : off('Mac Say'),
658
+ detected.piper ? on('Piper') : off('Piper'),
659
+ detected.kokoro ? on('Kokoro') : off('Kokoro'),
660
+ detected.elevenlabs ? on('ElevenLabs') : off('ElevenLabs'),
661
+ detected.soprano ? on('Soprano') : off('Soprano'),
662
+ detected.sapi ? on('Windows SAPI') : off('Windows SAPI'),
663
+ detected.macos ? on('Mac Say') : off('Mac Say'),
648
664
  ].join(' ');
649
665
 
650
666
  const footer = blessed.box({
@@ -8,7 +8,7 @@
8
8
  import fs from 'node:fs';
9
9
  import path from 'node:path';
10
10
  import os from 'node:os';
11
- import { spawnSync } from 'node:child_process';
11
+ import { spawn, spawnSync } from 'node:child_process';
12
12
 
13
13
  /**
14
14
  * Build a spawn environment with correct PULSE_SERVER handling.
@@ -151,6 +151,90 @@ export function detectMp3Player(env) {
151
151
  return _detect(MP3_PLAYERS, env);
152
152
  }
153
153
 
154
+ /**
155
+ * Returns true when running headless with PulseAudio over TCP.
156
+ * SDL2-based players (ffplay) fail in this configuration with exit 123.
157
+ */
158
+ function _isHeadlessPulse(env) {
159
+ return !env.DISPLAY && typeof env.PULSE_SERVER === 'string' && env.PULSE_SERVER.startsWith('tcp:');
160
+ }
161
+
162
+ /**
163
+ * Spawn an MP3 player for the given track path.
164
+ *
165
+ * On headless servers with PulseAudio TCP, SDL2-based players (ffplay) fail
166
+ * because DISPLAY is not set. In that case uses ffmpeg→pacat pipe.
167
+ *
168
+ * Returns a process-like object with:
169
+ * .pid — primary process PID
170
+ * .kill() — stops playback, killing all spawned processes
171
+ * .on(event, cb) — supports 'exit' and 'error'
172
+ *
173
+ * Returns null if no suitable player or required binaries are unavailable.
174
+ *
175
+ * @param {string} trackPath - Absolute path to the MP3 file
176
+ * @param {Object} [env] - Environment (defaults to buildAudioEnv())
177
+ * @returns {{ pid: number, kill: Function, on: Function }|null}
178
+ */
179
+ export function spawnMp3Player(trackPath, env) {
180
+ env = env ?? buildAudioEnv();
181
+
182
+ if (_isHeadlessPulse(env)) {
183
+ // Check required binaries are present.
184
+ // S4036: standard system media tools resolved via the user's local PATH on
185
+ // their own machine — no untrusted PATH, absolute paths aren't portable
186
+ // across distros/Homebrew/Nix. Risk accepted.
187
+ if (spawnSync('which', ['ffmpeg'], { stdio: 'ignore', env }).status !== 0) return null; // NOSONAR
188
+ if (spawnSync('which', ['pacat'], { stdio: 'ignore', env }).status !== 0) return null; // NOSONAR
189
+
190
+ const ff = spawn('ffmpeg', // NOSONAR — see S4036 note above
191
+ ['-i', trackPath, '-f', 's16le', '-ac', '2', '-ar', '44100', 'pipe:1', '-loglevel', 'quiet'],
192
+ { stdio: ['ignore', 'pipe', 'ignore'], env },
193
+ );
194
+ const pa = spawn('pacat', // NOSONAR — see S4036 note above
195
+ ['--playback', '--format=s16le', '--channels=2', '--rate=44100'],
196
+ { stdio: ['pipe', 'ignore', 'ignore'], env },
197
+ );
198
+ ff.stdout.on('error', () => {});
199
+ pa.stdin.on('error', () => {});
200
+ ff.stdout.pipe(pa.stdin);
201
+
202
+ const _listeners = { exit: [], error: [] };
203
+ pa.on('exit', (code, sig) => _listeners.exit.forEach(cb => cb(code, sig)));
204
+ ff.on('error', err => _listeners.error.forEach(cb => cb(err)));
205
+ pa.on('error', err => _listeners.error.forEach(cb => cb(err)));
206
+
207
+ return {
208
+ pid: ff.pid,
209
+ kill() { try { ff.kill(); } catch {} try { pa.kill(); } catch {} },
210
+ on(event, cb) { if (_listeners[event]) _listeners[event].push(cb); },
211
+ };
212
+ }
213
+
214
+ const player = detectMp3Player(env);
215
+ if (!player) return null;
216
+
217
+ const isWin = process.platform === 'win32' && !process.env.WSL_DISTRO_NAME;
218
+ const proc = spawn(player.bin, player.args(trackPath), {
219
+ stdio: 'ignore', detached: !isWin, windowsHide: true, env,
220
+ });
221
+
222
+ return {
223
+ pid: proc.pid,
224
+ kill() {
225
+ try {
226
+ if (isWin) {
227
+ spawn('taskkill', ['/F', '/T', '/PID', String(proc.pid)], // NOSONAR
228
+ { stdio: 'ignore', windowsHide: true });
229
+ } else {
230
+ process.kill(-proc.pid, 'SIGTERM');
231
+ }
232
+ } catch (e) { if (e.code !== 'ESRCH') { /* ignore */ } }
233
+ },
234
+ on(event, cb) { proc.on(event, cb); },
235
+ };
236
+ }
237
+
154
238
  /**
155
239
  * Detect the best available WAV player.
156
240
  * On Windows, uses PowerShell SoundPlayer as built-in fallback.
@@ -1,13 +1,13 @@
1
- /**
2
- * AgentVibes TUI — Brand Color Constants
3
- *
4
- * Single source of truth for the two primary brand colors.
5
- * Change BRAND_PINK or BRAND_BLUE here to update every modal title,
6
- * button, and the "Vibes" logotype across the entire TUI.
7
- */
8
-
9
- /** Magenta-pink used for modal titles and the "Vibes" logotype. */
10
- export const BRAND_PINK = '#f06292'; // Light magenta — Pink 300
11
-
12
- /** Indigo blue used for default button backgrounds and primary accents. */
13
- export const BRAND_BLUE = '#3949ab';
1
+ /**
2
+ * AgentVibes TUI — Brand Color Constants
3
+ *
4
+ * Single source of truth for the two primary brand colors.
5
+ * Change BRAND_PINK or BRAND_BLUE here to update every modal title,
6
+ * button, and the "Vibes" logotype across the entire TUI.
7
+ */
8
+
9
+ /** Magenta-pink used for modal titles and the "Vibes" logotype. */
10
+ export const BRAND_PINK = '#f06292'; // Light magenta — Pink 300
11
+
12
+ /** Indigo blue used for default button backgrounds and primary accents. */
13
+ export const BRAND_BLUE = '#3949ab';
@@ -1,44 +1,44 @@
1
- /**
2
- * AgentVibes — Canonical personality constants.
3
- *
4
- * Single source of truth for personality names and their associated emoji
5
- * glyphs. All TUI modules (settings-tab, agents-tab, personality-picker …)
6
- * import from here; src/installer.js maintains its own copy because it
7
- * predates the TUI and uses a different module-load path.
8
- *
9
- * Exported:
10
- * PERSONALITY_EMOJIS — Map of personality name → emoji string
11
- * PERSONALITIES — Ordered array of personality names (canonical order
12
- * used for picker lists)
13
- */
14
-
15
- export const PERSONALITY_EMOJIS = Object.freeze({
16
- angry: '😠',
17
- annoying: '😤',
18
- crass: '🤬',
19
- dramatic: '🎭',
20
- 'dry-humor': '😐',
21
- flirty: '😘',
22
- funny: '😂',
23
- grandpa: '👴',
24
- millennial: '🙄',
25
- moody: '😒',
26
- none: '😊',
27
- normal: '😊',
28
- pirate: '⚓',
29
- poetic: '📜',
30
- professional: '👔',
31
- rapper: '🎤',
32
- robot: '🤖',
33
- sarcastic: '😏',
34
- sassy: '💁',
35
- 'surfer-dude':'🏄',
36
- zen: '🧘',
37
- });
38
-
39
- export const PERSONALITIES = Object.freeze([
40
- 'none', 'angry', 'annoying', 'crass', 'dramatic', 'dry-humor',
41
- 'flirty', 'funny', 'grandpa', 'millennial', 'moody', 'normal',
42
- 'pirate', 'poetic', 'professional', 'rapper', 'robot', 'sarcastic',
43
- 'sassy', 'surfer-dude', 'zen',
44
- ]);
1
+ /**
2
+ * AgentVibes — Canonical personality constants.
3
+ *
4
+ * Single source of truth for personality names and their associated emoji
5
+ * glyphs. All TUI modules (settings-tab, agents-tab, personality-picker …)
6
+ * import from here; src/installer.js maintains its own copy because it
7
+ * predates the TUI and uses a different module-load path.
8
+ *
9
+ * Exported:
10
+ * PERSONALITY_EMOJIS — Map of personality name → emoji string
11
+ * PERSONALITIES — Ordered array of personality names (canonical order
12
+ * used for picker lists)
13
+ */
14
+
15
+ export const PERSONALITY_EMOJIS = Object.freeze({
16
+ angry: '😠',
17
+ annoying: '😤',
18
+ crass: '🤬',
19
+ dramatic: '🎭',
20
+ 'dry-humor': '😐',
21
+ flirty: '😘',
22
+ funny: '😂',
23
+ grandpa: '👴',
24
+ millennial: '🙄',
25
+ moody: '😒',
26
+ none: '😊',
27
+ normal: '😊',
28
+ pirate: '⚓',
29
+ poetic: '📜',
30
+ professional: '👔',
31
+ rapper: '🎤',
32
+ robot: '🤖',
33
+ sarcastic: '😏',
34
+ sassy: '💁',
35
+ 'surfer-dude':'🏄',
36
+ zen: '🧘',
37
+ });
38
+
39
+ export const PERSONALITIES = Object.freeze([
40
+ 'none', 'angry', 'annoying', 'crass', 'dramatic', 'dry-humor',
41
+ 'flirty', 'funny', 'grandpa', 'millennial', 'moody', 'normal',
42
+ 'pirate', 'poetic', 'professional', 'rapper', 'robot', 'sarcastic',
43
+ 'sassy', 'surfer-dude', 'zen',
44
+ ]);
@@ -40,8 +40,12 @@ const KEY_TO_TAB = {
40
40
  export function setupNavigation(screen, navigationService, focusMainTabBar) {
41
41
  // Tab switching shortcuts — one handler per key (both cases)
42
42
  // When a modal is open, force-close all modals then switch tabs.
43
+ // Exception: if already on the target tab, pressing its shortcut is a no-op —
44
+ // the user is trying to do something within the tab (e.g. 'I' in setup means
45
+ // "install", not "re-navigate to setup and close all my pickers").
43
46
  for (const [key, tabId] of Object.entries(KEY_TO_TAB)) {
44
47
  screen.key([key], () => {
48
+ if (navigationService.getActiveTab() === tabId) return;
45
49
  if (navigationService.isModalOpen()) {
46
50
  navigationService.forceCloseAll();
47
51
  setTimeout(() => navigationService.switchTab(tabId), 0);
@@ -20,6 +20,7 @@ import {
20
20
  getFavorites, getThumbsDown, toggleThumbsUp, toggleThumbsDown,
21
21
  } from './voices-tab.js';
22
22
  import { buildAudioEnv, detectWavPlayer, detectRemoteLlm } from '../audio-env.js';
23
+ import { voicesForProvider } from '../../services/provider-voice-catalog.js';
23
24
  import { destroyList } from '../widgets/destroy-list.js';
24
25
  import { BRAND_PINK } from '../brand-colors.js';
25
26
  import { t } from '../../i18n/strings.js';
@@ -642,7 +643,7 @@ ${_tl('bmadDesc')}
642
643
  const FIELDS = [
643
644
  { key: 'voice', label: 'Voice', getValue: () => draft.voice || '(global default)' },
644
645
  { key: 'pretext', label: 'Pretext', getValue: () => draft.pretext || '(default)' },
645
- { key: 'reverbPreset', label: 'Reverb', getValue: () => formatReverbState(draft.reverbPreset) },
646
+ { key: 'reverbPreset', label: 'Audio Effects', getValue: () => formatReverbState(draft.reverbPreset) },
646
647
  { key: 'personality', label: 'Personality', getValue: () => {
647
648
  const p = draft.personality;
648
649
  const emoji = PERSONALITY_EMOJIS[p] || '';
@@ -1494,13 +1495,18 @@ ${_tl('bmadDesc')}
1494
1495
  }
1495
1496
 
1496
1497
  function _autoAssignVoices() {
1497
- const installed = scanInstalledVoices();
1498
+ // Pull the pool from whichever provider is currently active, not always Piper.
1499
+ // Agent profiles store only a voice id (the active provider is global), so
1500
+ // switching the provider and re-running auto-assign rebinds every agent to a
1501
+ // voice from that provider (Kokoro, ElevenLabs, soprano, …).
1502
+ const provider = providerService?.getActiveProvider?.() ?? 'piper';
1503
+ const installed = voicesForProvider(provider, { scanInstalledVoices, getVoiceMeta });
1498
1504
  if (installed.length === 0) return false;
1499
1505
 
1500
- // Separate voices by gender
1501
- const femaleVoices = _shuffleArray(installed.filter(v => getVoiceMeta(v).gender === 'Female'));
1502
- const maleVoices = _shuffleArray(installed.filter(v => getVoiceMeta(v).gender === 'Male'));
1503
- const otherVoices = _shuffleArray(installed.filter(v => !['Male', 'Female'].includes(getVoiceMeta(v).gender)));
1506
+ // installed is [{ id, gender }] — separate the ids by gender.
1507
+ const femaleVoices = _shuffleArray(installed.filter(v => v.gender === 'Female').map(v => v.id));
1508
+ const maleVoices = _shuffleArray(installed.filter(v => v.gender === 'Male').map(v => v.id));
1509
+ const otherVoices = _shuffleArray(installed.filter(v => !['Male', 'Female'].includes(v.gender)).map(v => v.id));
1504
1510
 
1505
1511
  // Separate agents by gender
1506
1512
  const femaleAgents = _agents.filter(a => _inferAgentGender(a) === 'Female');