agentvibes 4.4.1 → 4.5.0

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 (50) hide show
  1. package/.agentvibes/config.json +4 -4
  2. package/.claude/config/reverb-level.txt +1 -1
  3. package/.claude/github-star-reminder.txt +1 -1
  4. package/.claude/hooks-windows/bmad-speak.ps1 +112 -0
  5. package/.claude/hooks-windows/play-tts-piper.ps1 +3 -4
  6. package/.claude/hooks-windows/play-tts-sapi.ps1 +3 -4
  7. package/.claude/hooks-windows/play-tts-soprano.ps1 +2 -3
  8. package/.claude/hooks-windows/play-tts-termux-ssh.ps1 +138 -0
  9. package/.claude/hooks-windows/play-tts.ps1 +14 -6
  10. package/.claude/hooks-windows/provider-manager.ps1 +16 -1
  11. package/CLAUDE.md +4 -0
  12. package/README.md +39 -9
  13. package/RELEASE_NOTES.md +39 -0
  14. package/bin/agent-vibes +1 -1
  15. package/bin/agentvibes-voice-browser.js +1 -1
  16. package/bin/bmad-speak.js +52 -0
  17. package/bin/mcp-server.js +1 -1
  18. package/bin/test-bmad-pr +1 -1
  19. package/package.json +1 -1
  20. package/setup-windows.ps1 +4 -4
  21. package/src/console/app.js +58 -11
  22. package/src/console/tabs/agents-tab.js +61 -65
  23. package/src/console/tabs/help-tab.js +107 -54
  24. package/src/console/tabs/install-tab.js +107 -47
  25. package/src/console/tabs/music-tab.js +1030 -1011
  26. package/src/console/tabs/placeholder-tab.js +27 -0
  27. package/src/console/tabs/readme-tab.js +9 -7
  28. package/src/console/tabs/receiver-tab.js +23 -12
  29. package/src/console/tabs/settings-tab.js +4001 -3783
  30. package/src/console/tabs/voices-tab.js +1680 -1653
  31. package/src/console/widgets/personality-picker.js +35 -7
  32. package/src/console/widgets/reverb-picker.js +9 -6
  33. package/src/console/widgets/track-picker.js +6 -1
  34. package/src/i18n/de.js +201 -0
  35. package/src/i18n/en.js +201 -0
  36. package/src/i18n/es.js +201 -0
  37. package/src/i18n/fr.js +201 -0
  38. package/src/i18n/hi.js +201 -0
  39. package/src/i18n/ja.js +201 -0
  40. package/src/i18n/ko.js +201 -0
  41. package/src/i18n/pt.js +201 -0
  42. package/src/i18n/strings.js +54 -0
  43. package/src/i18n/zh-CN.js +201 -0
  44. package/src/installer/language-screen.js +31 -0
  45. package/src/installer.js +79 -25
  46. package/src/services/language-service.js +47 -0
  47. package/src/utils/file-ownership-verifier.js +2 -2
  48. package/src/utils/provider-validator.js +9 -13
  49. package/.claude/hooks-windows/play-tts-windows-piper.ps1 +0 -209
  50. package/.claude/hooks-windows/play-tts-windows-sapi.ps1 +0 -108
package/setup-windows.ps1 CHANGED
@@ -283,8 +283,8 @@ if (-not (Test-Path $HooksDir)) {
283
283
  $HookScriptInfo = @(
284
284
  @{ Name = "play-tts.ps1"; Desc = "Main TTS router - dispatches to active provider" },
285
285
  @{ Name = "play-tts-soprano.ps1"; Desc = "Soprano neural voice provider (fastest)" },
286
- @{ Name = "play-tts-windows-piper.ps1"; Desc = "Piper offline neural voice provider" },
287
- @{ Name = "play-tts-windows-sapi.ps1"; Desc = "Windows built-in SAPI voice provider" },
286
+ @{ Name = "play-tts-piper.ps1"; Desc = "Piper offline neural voice provider" },
287
+ @{ Name = "play-tts-sapi.ps1"; Desc = "Windows built-in SAPI voice provider" },
288
288
  @{ Name = "provider-manager.ps1"; Desc = "Switch between TTS providers" },
289
289
  @{ Name = "voice-manager-windows.ps1"; Desc = "Browse and select voice models" },
290
290
  @{ Name = "audio-cache-utils.ps1"; Desc = "Manage TTS audio file cache" },
@@ -765,11 +765,11 @@ try {
765
765
  }
766
766
  }
767
767
  "piper" {
768
- & "$HooksDir\play-tts-windows-piper.ps1" $TestMessage | Out-Null
768
+ & "$HooksDir\play-tts-piper.ps1" $TestMessage | Out-Null
769
769
  Write-Ok "Piper TTS is working"
770
770
  }
771
771
  "sapi" {
772
- & "$HooksDir\play-tts-windows-sapi.ps1" $TestMessage | Out-Null
772
+ & "$HooksDir\play-tts-sapi.ps1" $TestMessage | Out-Null
773
773
  Write-Ok "Windows SAPI is working"
774
774
  }
775
775
  }
@@ -14,7 +14,9 @@ import { fileURLToPath } from 'node:url';
14
14
  import { spawnSync, execFileSync } from 'node:child_process';
15
15
  import { NavigationService, TAB_ORDER } from '../services/navigation-service.js';
16
16
  import { setupNavigation } from './navigation.js';
17
- import { createPlaceholderTab, TAB_DISPLAY_LABELS, TAB_SHORTCUT_KEYS } from './tabs/placeholder-tab.js';
17
+ import { createPlaceholderTab, TAB_DISPLAY_LABELS, TAB_SHORTCUT_KEYS, getTabLabel } from './tabs/placeholder-tab.js';
18
+ import { LanguageService } from '../services/language-service.js';
19
+ import { t } from '../i18n/strings.js';
18
20
  import { FOOTER_CONFIG, DEFAULT_FOOTER_COLOR } from './footer-config.js';
19
21
  import { createModalOverlay } from './modals/modal-overlay.js';
20
22
  import { BRAND_PINK } from './brand-colors.js';
@@ -169,7 +171,7 @@ export class AgentVibesConsole {
169
171
  });
170
172
 
171
173
  // Row 1: subtitle
172
- blessed.text({
174
+ this._headerSubtitleText = blessed.text({
173
175
  parent: this.headerBox,
174
176
  top: 1,
175
177
  left: 2,
@@ -180,7 +182,7 @@ export class AgentVibesConsole {
180
182
  });
181
183
 
182
184
  // Row 1: Quit shortcut — left-anchored after "Customization Tool" (18 chars at left:2)
183
- blessed.text({
185
+ this._headerQuitText = blessed.text({
184
186
  parent: this.headerBox,
185
187
  top: 1,
186
188
  left: 22,
@@ -291,9 +293,11 @@ export class AgentVibesConsole {
291
293
 
292
294
  // One box per tab — direct screen children at absolute top:3. No tag parsing, no wrapping.
293
295
  this._tabItems = {};
296
+ this._tabItemXOffsets = {}; // track x positions for label refresh
294
297
  let xOffset = 1;
295
298
  for (const id of TAB_ORDER) {
296
- const label = TAB_DISPLAY_LABELS[id];
299
+ const lang = this._languageService?.getLang() ?? 'en';
300
+ const label = getTabLabel(id, lang);
297
301
  const shortcutKey = TAB_SHORTCUT_KEYS[id] || label[0];
298
302
  const text = ` [${shortcutKey}] ${label} `;
299
303
  const el = blessed.box({
@@ -304,11 +308,13 @@ export class AgentVibesConsole {
304
308
  height: 1,
305
309
  content: text,
306
310
  tags: false,
311
+ wrap: false,
307
312
  keys: true,
308
313
  focusable: true,
309
314
  style: { fg: COLORS.focusCyan, bg: COLORS.tabBarBg },
310
315
  });
311
316
  this._tabItems[id] = el;
317
+ this._tabItemXOffsets[id] = xOffset;
312
318
  xOffset += text.length + 1; // 1-space gap between tabs
313
319
  }
314
320
 
@@ -359,9 +365,8 @@ export class AgentVibesConsole {
359
365
  const el = this._tabItems[tabIds[i]];
360
366
 
361
367
  // Blinking block cursor: replace trailing space with █, toggle at 500ms
362
- const _tabLabel = TAB_DISPLAY_LABELS[tabIds[i]];
363
- const _baseContent = ` [${_tabLabel[0]}] ${_tabLabel} `;
364
- const _blockContent = _baseContent.slice(0, -1) + '█';
368
+ // Always derive from current el.content so language changes are preserved.
369
+ const _getBaseContent = () => el.content.replace(/█$/, ' ');
365
370
  let _cursorInterval = null;
366
371
  let _cursorOn = false;
367
372
 
@@ -369,18 +374,21 @@ export class AgentVibesConsole {
369
374
  el.style.fg = 'white';
370
375
  el.style.bg = '#9c27b0'; // purple — cursor on this tab item
371
376
  _cursorOn = true;
372
- el.setContent(_blockContent);
377
+ const _base = _getBaseContent();
378
+ const _block = _base.slice(0, -1) + '█';
379
+ el.setContent(_block);
373
380
  this.screen.render();
374
381
  if (_cursorInterval) { clearInterval(_cursorInterval); _cursorInterval = null; }
375
382
  _cursorInterval = setInterval(() => {
376
383
  _cursorOn = !_cursorOn;
377
- el.setContent(_cursorOn ? _blockContent : _baseContent);
384
+ const b = _getBaseContent();
385
+ el.setContent(_cursorOn ? b.slice(0, -1) + '█' : b);
378
386
  this.screen.render();
379
387
  }, 500);
380
388
  });
381
389
  el.on('blur', () => {
382
390
  if (_cursorInterval) { clearInterval(_cursorInterval); _cursorInterval = null; }
383
- el.setContent(_baseContent);
391
+ el.setContent(_getBaseContent());
384
392
  // navigationService set up after _createTabBar, but blur fires lazily — safe
385
393
  this._updateTabBar(this.navigationService?.getActiveTab() ?? tabIds[0]);
386
394
  this.screen.render();
@@ -474,13 +482,47 @@ export class AgentVibesConsole {
474
482
  }
475
483
  }
476
484
 
485
+ // ---------------------------------------------------------------------------
486
+ // Private: Refresh all chrome strings (header subtitle, tab bar labels) when lang changes
487
+
488
+ _refreshChrome(lang) {
489
+ // Update header subtitle "Customization Tool"
490
+ if (this._headerSubtitleText) {
491
+ this._headerSubtitleText.setContent(`{green-fg}${t(lang, 'customizationTool')}{/green-fg}`);
492
+ }
493
+ if (this._headerQuitText) {
494
+ this._headerQuitText.setContent(`{#ef9a9a-fg}${t(lang, 'quitLabel')}{/#ef9a9a-fg}`);
495
+ }
496
+
497
+ // Update tab bar item labels — resize and reposition to fit translated labels
498
+ let xOffset = 1;
499
+ for (const id of TAB_ORDER) {
500
+ const el = this._tabItems?.[id];
501
+ if (!el) continue;
502
+ const label = getTabLabel(id, lang);
503
+ const shortcutKey = TAB_SHORTCUT_KEYS[id] || label[0];
504
+ const text = ` [${shortcutKey}] ${label} `;
505
+ el.left = xOffset;
506
+ el.width = text.length;
507
+ el.setContent(text);
508
+ xOffset += text.length + 1;
509
+ }
510
+
511
+ // Update active tab's footer text if it supports language-aware footer
512
+ const activeId = this.navigationService?.getActiveTab();
513
+ if (activeId) this._updateContextFooter(activeId);
514
+
515
+ this.screen.render();
516
+ }
517
+
477
518
  // ---------------------------------------------------------------------------
478
519
  // Private: Render tab bar content string for given active tab
479
520
  // (kept as a pure helper for unit tests; real rendering uses _updateTabBar)
480
521
 
481
522
  _renderTabBarContent(activeTabId) {
523
+ const lang = this._languageService?.getLang() ?? 'en';
482
524
  return TAB_ORDER.map(id => {
483
- const label = TAB_DISPLAY_LABELS[id];
525
+ const label = getTabLabel(id, lang);
484
526
  const shortcutKey = TAB_SHORTCUT_KEYS[id] || label[0];
485
527
  if (id === activeTabId) {
486
528
  return `{bold}{white-fg}[${shortcutKey}] ${label}{/white-fg}{/bold}`;
@@ -614,9 +656,14 @@ export class AgentVibesConsole {
614
656
  const providerService = new ProviderService(configService);
615
657
  this._configService = configService;
616
658
  this._providerService = providerService;
659
+ const languageService = new LanguageService();
660
+ this._languageService = languageService;
661
+ // Refresh UI chrome when language changes
662
+ languageService.onChange(lang => this._refreshChrome(lang));
617
663
  const services = {
618
664
  configService,
619
665
  providerService,
666
+ languageService,
620
667
  navigationService: this.navigationService,
621
668
  updateHeaderStatus: () => this._updateHeaderStatus(),
622
669
  focusMainTabBar: () => {
@@ -21,6 +21,7 @@ import {
21
21
  import { buildAudioEnv, detectWavPlayer } from '../audio-env.js';
22
22
  import { destroyList } from '../widgets/destroy-list.js';
23
23
  import { BRAND_PINK } from '../brand-colors.js';
24
+ import { t } from '../../i18n/strings.js';
24
25
  import crypto from 'node:crypto';
25
26
  import fs from 'node:fs';
26
27
  import os from 'node:os';
@@ -57,8 +58,8 @@ const COLORS = {
57
58
  linkFg: 'bright-cyan',
58
59
  };
59
60
 
60
- const FOOTER_TEXT_BMAD = '[↑↓/jk] Navigate [Space] Preview [Enter] Configure [A] Auto-assign [B] Bulk [X] Reset [Q] Quit';
61
- const FOOTER_TEXT_NOBMAD = '[Tab] Switch Tab [Q] Quit';
61
+ const _FOOTER_BMAD_EN = '[↑↓/jk] Navigate [Space] Preview [Enter] Configure [A] Auto-assign [B] Bulk [X] Reset [Q] Quit';
62
+ const _FOOTER_NOBMAD_EN = '[Tab] Switch Tab [Q] Quit';
62
63
 
63
64
  const _modalTitle = (text) => ` {${BRAND_PINK}-fg}${text}{/${BRAND_PINK}-fg} `;
64
65
 
@@ -85,53 +86,43 @@ function createTestStub() {
85
86
  hide: () => {},
86
87
  onFocus: () => {},
87
88
  onBlur: () => {},
88
- getFooterText: () => FOOTER_TEXT_BMAD,
89
+ getFooterText: () => _FOOTER_BMAD_EN,
89
90
  getFooterColor: () => COLORS.footerBg,
90
91
  };
91
92
  }
92
93
 
93
94
  // ---------------------------------------------------------------------------
94
- // No-BMAD onboarding content
95
95
 
96
- const ONBOARDING_TEXT = `{bold}{#ce93d8-fg}🧙 BMAD Agents{/#ce93d8-fg}{/bold}
96
+ /**
97
+ * Create the Agents tab component.
98
+ */
99
+ export function createAgentsTab(screen, services) {
100
+ if (IS_TEST) return createTestStub();
101
+
102
+ const { configService, providerService, focusMainTabBar, navigationService, languageService } = services;
103
+ const _tl = (key) => languageService ? languageService.t(key) : t('en', key);
97
104
 
98
- {bold}What is BMAD?{/bold}
105
+ function _buildOnboardingText() {
106
+ return `{bold}{#ce93d8-fg}${_tl('bmadTitle')}{/#ce93d8-fg}{/bold}
99
107
 
100
- The BMad Method (Build More Architect Dreams) is an AI-driven development
101
- framework module within the BMad Method Ecosystem that helps you build
102
- software through the whole process from ideation and planning all the way
103
- through agentic implementation. It provides specialized AI agents, guided
104
- workflows, and intelligent planning that adapts to your project's
105
- complexity, whether you're fixing a bug or building an enterprise platform.
108
+ {bold}${_tl('bmadWhatIsHeader')}{/bold}
106
109
 
107
- If you're comfortable working with AI coding assistants like Claude,
108
- Cursor, or GitHub Copilot, you're ready to get started.
110
+ ${_tl('bmadDesc')}
109
111
 
110
112
 
111
- {bold}Install BMAD in your project:{/bold}
113
+ {bold}${_tl('bmadInstallHeader')}{/bold}
112
114
 
113
115
  {bright-cyan-fg}npx bmad-method install{/bright-cyan-fg}
114
116
 
115
117
 
116
- {bold}Learn more:{/bold}
118
+ {bold}${_tl('bmadLearnMoreHeader')}{/bold}
117
119
 
118
120
  {bright-cyan-fg}https://docs.bmad-method.org/{/bright-cyan-fg}
119
121
  {bright-cyan-fg}https://github.com/bmad-code-org/BMAD-METHOD{/bright-cyan-fg}
120
122
 
121
123
 
122
- {#90a4ae-fg}Once BMAD is installed, this tab will show all your agents and let you
123
- customize each agent's voice, pretext, reverb, personality, and background
124
- music independently.{/#90a4ae-fg}`;
125
-
126
- // ---------------------------------------------------------------------------
127
-
128
- /**
129
- * Create the Agents tab component.
130
- */
131
- export function createAgentsTab(screen, services) {
132
- if (IS_TEST) return createTestStub();
133
-
134
- const { configService, providerService, focusMainTabBar, navigationService } = services;
124
+ {#90a4ae-fg}${_tl('bmadInstalledNote')}{/#90a4ae-fg}`;
125
+ }
135
126
  const voiceStore = new AgentVoiceStore();
136
127
 
137
128
  // Capture cwd once at construction (L1 fix)
@@ -183,10 +174,14 @@ export function createAgentsTab(screen, services) {
183
174
  keys: true,
184
175
  vi: true,
185
176
  mouse: true,
186
- content: ONBOARDING_TEXT,
177
+ content: _buildOnboardingText(),
187
178
  style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
188
179
  });
189
180
 
181
+ onboardingBox.key(['escape'], () => {
182
+ if (typeof focusMainTabBar === 'function') { focusMainTabBar(); screen.render(); }
183
+ });
184
+
190
185
  // -------------------------------------------------------------------------
191
186
  // BMAD state — section header
192
187
 
@@ -486,20 +481,6 @@ export function createAgentsTab(screen, services) {
486
481
  }
487
482
  }
488
483
 
489
- // -------------------------------------------------------------------------
490
- // Resolve piper binary — shared helper to avoid duplication (#153)
491
-
492
- function _resolvePiperBin() {
493
- if (process.platform !== 'win32' || process.env.WSL_DISTRO_NAME) return 'piper';
494
- const localAppData = process.env.LOCALAPPDATA ||
495
- (process.env.USERPROFILE ? path.join(process.env.USERPROFILE, 'AppData', 'Local') : null);
496
- if (localAppData) {
497
- const exePath = path.join(localAppData, 'Programs', 'Piper', 'piper.exe');
498
- if (fs.existsSync(exePath)) return exePath;
499
- }
500
- return 'piper';
501
- }
502
-
503
484
  // -------------------------------------------------------------------------
504
485
  // Kill any playing preview
505
486
 
@@ -935,7 +916,16 @@ export function createAgentsTab(screen, services) {
935
916
  const tempWav = _secureTempWav('vp');
936
917
  const phrase = SAMPLE_PHRASES[Math.floor(Math.random() * SAMPLE_PHRASES.length)];
937
918
 
938
- const _piperBin = _resolvePiperBin();
919
+ // Resolve piper binary (on Windows, find piper.exe)
920
+ let _piperBin = 'piper';
921
+ if (_isWin) {
922
+ const _lad = process.env.LOCALAPPDATA ||
923
+ (process.env.USERPROFILE ? path.join(process.env.USERPROFILE, 'AppData', 'Local') : null);
924
+ if (_lad) {
925
+ const _ep = path.join(_lad, 'Programs', 'Piper', 'piper.exe');
926
+ if (fs.existsSync(_ep)) _piperBin = _ep;
927
+ }
928
+ }
939
929
 
940
930
  const args = ['--model', voicePath, '--output_file', tempWav];
941
931
  if (_ms.speakerId != null) args.push('--speaker', String(_ms.speakerId));
@@ -1112,7 +1102,15 @@ export function createAgentsTab(screen, services) {
1112
1102
  /** Windows-native sample: piper.exe → wav → detectWavPlayer */
1113
1103
  function _sampleWithPiperDirect(gen, voiceId, phrase) {
1114
1104
  const _spawnEnv = buildAudioEnv();
1115
- const piperBin = _resolvePiperBin();
1105
+
1106
+ // Resolve piper binary
1107
+ let piperBin = 'piper';
1108
+ const localAppData = process.env.LOCALAPPDATA ||
1109
+ (process.env.USERPROFILE ? path.join(process.env.USERPROFILE, 'AppData', 'Local') : null);
1110
+ if (localAppData) {
1111
+ const exePath = path.join(localAppData, 'Programs', 'Piper', 'piper.exe');
1112
+ if (fs.existsSync(exePath)) piperBin = exePath;
1113
+ }
1116
1114
 
1117
1115
  // Resolve voice model path
1118
1116
  const ms = parseMultiSpeaker(voiceId);
@@ -1138,7 +1136,6 @@ export function createAgentsTab(screen, services) {
1138
1136
  _playingProcess = piper;
1139
1137
 
1140
1138
  piper.on('exit', (code) => {
1141
- // Generation changed — another preview was triggered; clean up silently
1142
1139
  if (gen !== _playGeneration) {
1143
1140
  try { fs.unlinkSync(tempWav); } catch {}
1144
1141
  return;
@@ -1150,12 +1147,6 @@ export function createAgentsTab(screen, services) {
1150
1147
  return;
1151
1148
  }
1152
1149
 
1153
- // Re-check generation after piper exit to close the race window (#154)
1154
- if (gen !== _playGeneration) {
1155
- try { fs.unlinkSync(tempWav); } catch {}
1156
- return;
1157
- }
1158
-
1159
1150
  // Play the synthesized wav
1160
1151
  const wavPlayer = detectWavPlayer(_spawnEnv);
1161
1152
  if (!wavPlayer) {
@@ -1184,7 +1175,6 @@ export function createAgentsTab(screen, services) {
1184
1175
 
1185
1176
  piper.on('error', () => {
1186
1177
  if (gen === _playGeneration) { _playingProcess = null; _stopSpinner(); }
1187
- try { fs.unlinkSync(tempWav); } catch {}
1188
1178
  });
1189
1179
  }
1190
1180
 
@@ -1202,12 +1192,10 @@ export function createAgentsTab(screen, services) {
1202
1192
 
1203
1193
  // Common first-name → gender map for gender-aware auto-assign.
1204
1194
  // Only needs to cover names likely used as BMAD agent display names.
1205
- // Ambiguous names (sam, charlie, dana, max, pat, etc.) are intentionally
1206
- // omitted so they fall through to the gender-neutral 'other' pool (#156).
1207
1195
  const _NAME_GENDER = {
1208
1196
  // Female
1209
1197
  amelia: 'Female', amy: 'Female', anna: 'Female', betty: 'Female',
1210
- claire: 'Female', emma: 'Female', faye: 'Female',
1198
+ claire: 'Female', dana: 'Female', emma: 'Female', faye: 'Female',
1211
1199
  grace: 'Female', heather: 'Female', ivy: 'Female', jane: 'Female',
1212
1200
  jenny: 'Female', julia: 'Female', kate: 'Female', laura: 'Female',
1213
1201
  lily: 'Female', maria: 'Female', mary: 'Female', nina: 'Female',
@@ -1216,12 +1204,12 @@ export function createAgentsTab(screen, services) {
1216
1204
  wendy: 'Female', zoe: 'Female',
1217
1205
  // Male
1218
1206
  alan: 'Male', barry: 'Male', bob: 'Male', carl: 'Male',
1219
- dan: 'Male', david: 'Male', eric: 'Male',
1207
+ charlie: 'Male', dan: 'Male', david: 'Male', eric: 'Male',
1220
1208
  frank: 'Male', george: 'Male', hank: 'Male', jack: 'Male',
1221
1209
  james: 'Male', joe: 'Male', john: 'Male', kevin: 'Male',
1222
- leo: 'Male', mark: 'Male', murat: 'Male',
1210
+ leo: 'Male', mark: 'Male', max: 'Male', murat: 'Male',
1223
1211
  nick: 'Male', oscar: 'Male', paul: 'Male', ray: 'Male',
1224
- ryan: 'Male', saif: 'Male', steve: 'Male',
1212
+ ryan: 'Male', saif: 'Male', sam: 'Male', steve: 'Male',
1225
1213
  tom: 'Male', victor: 'Male', winston: 'Male', zach: 'Male',
1226
1214
  };
1227
1215
 
@@ -1250,17 +1238,15 @@ export function createAgentsTab(screen, services) {
1250
1238
  // Assign matching-gender voices first, then fall back to any available
1251
1239
  function assignGroup(agents, preferredPool, fallbackPools) {
1252
1240
  const allPools = [preferredPool, ...fallbackPools];
1253
- let reuseIdx = 0;
1254
1241
  agents.forEach(agent => {
1255
1242
  let voice = null;
1256
1243
  for (const pool of allPools) {
1257
1244
  voice = pool.find(v => !usedVoices.has(v));
1258
1245
  if (voice) break;
1259
1246
  }
1260
- // If all unique voices exhausted, round-robin reuse from preferred pool
1247
+ // If all unique voices exhausted, reuse from preferred pool
1261
1248
  if (!voice && preferredPool.length > 0) {
1262
- voice = preferredPool[reuseIdx % preferredPool.length];
1263
- reuseIdx++;
1249
+ voice = preferredPool[usedVoices.size % preferredPool.length];
1264
1250
  }
1265
1251
  if (voice) {
1266
1252
  usedVoices.add(voice);
@@ -1642,6 +1628,16 @@ export function createAgentsTab(screen, services) {
1642
1628
  if (typeof focusMainTabBar === 'function') { focusMainTabBar(); screen.render(); }
1643
1629
  });
1644
1630
 
1631
+ // -------------------------------------------------------------------------
1632
+ // Language change handler
1633
+
1634
+ if (languageService) {
1635
+ languageService.onChange(() => {
1636
+ onboardingBox.setContent(_buildOnboardingText());
1637
+ screen.render();
1638
+ });
1639
+ }
1640
+
1645
1641
  // -------------------------------------------------------------------------
1646
1642
  // Tab Component Contract
1647
1643
 
@@ -1674,7 +1670,7 @@ export function createAgentsTab(screen, services) {
1674
1670
  },
1675
1671
 
1676
1672
  getFooterText() {
1677
- return _bmadDetected ? FOOTER_TEXT_BMAD : FOOTER_TEXT_NOBMAD;
1673
+ return _bmadDetected ? _tl('bmadFooterBmad') : _tl('bmadFooterNobmad');
1678
1674
  },
1679
1675
 
1680
1676
  getFooterColor() {
@@ -8,6 +8,8 @@
8
8
  * Features: keyboard shortcuts reference, two sections, [/] search.
9
9
  */
10
10
 
11
+ import { t } from '../../i18n/strings.js';
12
+
11
13
  const IS_TEST = process.env.AGENTVIBES_TEST_MODE === 'true';
12
14
 
13
15
  let blessed;
@@ -28,65 +30,60 @@ const COLORS = {
28
30
  footerBg: '#607d8b', // Gray — Help tab footer
29
31
  };
30
32
 
31
- const FOOTER_TEXT = '[↑↓/jk] Scroll [/] Search [PgUp/PgDn] Page [S/V/M/A/R] Tab [Q] Quit';
32
-
33
33
  // ---------------------------------------------------------------------------
34
- // Keyboard shortcuts data
35
-
36
- const SHORTCUT_SECTIONS = Object.freeze([
37
- {
38
- title: 'Global Shortcuts',
39
- shortcuts: [
40
- { key: 'Q', desc: 'Quit the console' },
41
- { key: 'Ctrl+C', desc: 'Force quit' },
42
- { key: 'S', desc: 'Switch to Settings tab' },
43
- { key: 'V', desc: 'Switch to Voices tab' },
44
- { key: 'M', desc: 'Switch to Music tab' },
45
- { key: 'R', desc: 'Switch to Readme tab' },
46
- { key: 'H', desc: 'Switch to Help tab' },
47
- { key: 'I', desc: 'Switch to Install tab' },
48
- { key: 'Esc', desc: 'Close modal / go back' },
49
- ],
50
- },
51
- {
52
- title: 'Navigation Shortcuts',
53
- shortcuts: [
54
- { key: '↑↓ / j k', desc: 'Navigate lists' },
55
- { key: 'Enter', desc: 'Select / activate' },
56
- { key: 'Space', desc: 'Toggle / preview' },
57
- { key: 'Tab', desc: 'Next button' },
58
- { key: 'Shift+Tab', desc: 'Previous button' },
59
- { key: '/', desc: 'Open search/filter' },
60
- { key: 'F', desc: 'Toggle favorites filter (Voices/Music)' },
61
- { key: '*', desc: 'Toggle favorite (Music tab)' },
62
- { key: 'M', desc: 'Toggle music on/off (Music tab)' },
63
-
64
- ],
65
- },
66
- {
67
- title: 'Tab Color Guide',
68
- shortcuts: [
69
- { key: 'Blue (#2196f3)', desc: 'Settings tab footer' },
70
- { key: 'Teal (#00695c)', desc: 'Voices tab footer' },
71
- { key: 'Orange (#ff9800)', desc: 'Music tab footer' },
72
-
73
- { key: 'Dark (#455a64)', desc: 'Readme tab footer' },
74
- { key: 'Gray (#607d8b)', desc: 'Help tab footer' },
75
- { key: 'Indigo (#3f51b5)', desc: 'Install tab footer' },
76
- ],
77
- },
78
- ]);
79
34
 
80
35
  /**
81
36
  * Return all shortcut sections.
82
37
  * @returns {{ title: string, shortcuts: { key: string, desc: string }[] }[]}
83
38
  */
84
39
  export function getShortcutSections() {
85
- return [...SHORTCUT_SECTIONS];
40
+ return [
41
+ {
42
+ title: 'Global Shortcuts',
43
+ shortcuts: [
44
+ { key: 'Q', desc: 'Quit the console' },
45
+ { key: 'Ctrl+C', desc: 'Force quit' },
46
+ { key: 'S', desc: 'Switch to Settings tab' },
47
+ { key: 'V', desc: 'Switch to Voices tab' },
48
+ { key: 'M', desc: 'Switch to Music tab' },
49
+ { key: 'R', desc: 'Switch to Readme tab' },
50
+ { key: 'H', desc: 'Switch to Help tab' },
51
+ { key: 'I', desc: 'Switch to Install tab' },
52
+ { key: 'Esc', desc: 'Close modal / go back' },
53
+ ],
54
+ },
55
+ {
56
+ title: 'Navigation Shortcuts',
57
+ shortcuts: [
58
+ { key: '↑↓ / j k', desc: 'Navigate lists' },
59
+ { key: 'Enter', desc: 'Select / activate' },
60
+ { key: 'Space', desc: 'Toggle / preview' },
61
+ { key: 'Tab', desc: 'Next button' },
62
+ { key: 'Shift+Tab', desc: 'Previous button' },
63
+ { key: '/', desc: 'Open search/filter' },
64
+ { key: 'F', desc: 'Toggle favorites filter (Voices/Music)' },
65
+ { key: '*', desc: 'Toggle favorite (Music tab)' },
66
+ { key: 'M', desc: 'Toggle music on/off (Music tab)' },
67
+ ],
68
+ },
69
+ {
70
+ title: 'Tab Color Guide',
71
+ shortcuts: [
72
+ { key: 'Blue (#2196f3)', desc: 'Settings tab footer' },
73
+ { key: 'Teal (#00695c)', desc: 'Voices tab footer' },
74
+ { key: 'Orange (#ff9800)', desc: 'Music tab footer' },
75
+ { key: 'Dark (#455a64)', desc: 'Readme tab footer' },
76
+ { key: 'Gray (#607d8b)', desc: 'Help tab footer' },
77
+ { key: 'Indigo (#3f51b5)', desc: 'Install tab footer' },
78
+ ],
79
+ },
80
+ ];
86
81
  }
87
82
 
88
83
  // ---------------------------------------------------------------------------
89
84
 
85
+ const _FOOTER_TEXT_EN = '[↑↓/jk] Scroll [/] Search [PgUp/PgDn] Page [S/V/M/A/R] Tab [Q] Quit';
86
+
90
87
  function createTestStub() {
91
88
  return {
92
89
  box: {},
@@ -94,7 +91,7 @@ function createTestStub() {
94
91
  hide: () => {},
95
92
  onFocus: () => {},
96
93
  onBlur: () => {},
97
- getFooterText: () => FOOTER_TEXT,
94
+ getFooterText: () => _FOOTER_TEXT_EN,
98
95
  getFooterColor: () => COLORS.footerBg,
99
96
  };
100
97
  }
@@ -111,7 +108,52 @@ function createTestStub() {
111
108
  export function createHelpTab(screen, services) {
112
109
  if (IS_TEST) return createTestStub();
113
110
 
114
- const { focusMainTabBar } = services;
111
+ const { focusMainTabBar, languageService } = services;
112
+ const _tl = (key) => languageService ? languageService.t(key) : t('en', key);
113
+
114
+ function _buildSections() {
115
+ return [
116
+ {
117
+ title: _tl('helpSectionGlobal'),
118
+ shortcuts: [
119
+ { key: 'Q', desc: _tl('helpQuit') },
120
+ { key: 'Ctrl+C', desc: _tl('helpForceQuit') },
121
+ { key: 'S', desc: _tl('helpSwitchSettings') },
122
+ { key: 'V', desc: _tl('helpSwitchVoices') },
123
+ { key: 'M', desc: _tl('helpSwitchMusic') },
124
+ { key: 'R', desc: _tl('helpSwitchReadme') },
125
+ { key: 'H', desc: _tl('helpSwitchHelp') },
126
+ { key: 'I', desc: _tl('helpSwitchInstall') },
127
+ { key: 'Esc', desc: _tl('helpCloseModal') },
128
+ ],
129
+ },
130
+ {
131
+ title: _tl('helpSectionNavigation'),
132
+ shortcuts: [
133
+ { key: '↑↓ / j k', desc: _tl('helpNavigateLists') },
134
+ { key: 'Enter', desc: _tl('helpSelectActivate') },
135
+ { key: 'Space', desc: _tl('helpTogglePreview') },
136
+ { key: 'Tab', desc: _tl('helpNextButton') },
137
+ { key: 'Shift+Tab', desc: _tl('helpPrevButton') },
138
+ { key: '/', desc: _tl('helpOpenSearch') },
139
+ { key: 'F', desc: _tl('helpToggleFavFilter') },
140
+ { key: '*', desc: _tl('helpToggleFav') },
141
+ { key: 'M', desc: _tl('helpToggleMusic') },
142
+ ],
143
+ },
144
+ {
145
+ title: _tl('helpSectionColors'),
146
+ shortcuts: [
147
+ { key: 'Blue (#2196f3)', desc: _tl('helpColorSettings') },
148
+ { key: 'Teal (#00695c)', desc: _tl('helpColorVoices') },
149
+ { key: 'Orange (#ff9800)', desc: _tl('helpColorMusic') },
150
+ { key: 'Dark (#455a64)', desc: _tl('helpColorReadme') },
151
+ { key: 'Gray (#607d8b)', desc: _tl('helpColorHelp') },
152
+ { key: 'Indigo (#3f51b5)', desc: _tl('helpColorInstall') },
153
+ ],
154
+ },
155
+ ];
156
+ }
115
157
 
116
158
  // -------------------------------------------------------------------------
117
159
  // Container
@@ -133,7 +175,7 @@ export function createHelpTab(screen, services) {
133
175
 
134
176
  function _buildContent(filterText) {
135
177
  const lines = [];
136
- for (const section of SHORTCUT_SECTIONS) {
178
+ for (const section of _buildSections()) {
137
179
  lines.push(`{bold}{#546e7a-fg}── ${section.title} ${'─'.repeat(Math.max(0, 60 - section.title.length))}{/#546e7a-fg}{/bold}`);
138
180
  for (const { key, desc } of section.shortcuts) {
139
181
  const displayKey = key.padEnd(20);
@@ -183,11 +225,11 @@ export function createHelpTab(screen, services) {
183
225
  style: { fg: COLORS.keyFg, bg: '#1a3a5c', focus: { bg: '#245a80' } },
184
226
  });
185
227
 
186
- blessed.text({
228
+ const searchLabel = blessed.text({
187
229
  parent: box,
188
230
  bottom: 2,
189
231
  left: 2,
190
- content: 'Search:',
232
+ content: _tl('helpSearchLabel'),
191
233
  style: { fg: COLORS.descFg, bg: COLORS.contentBg },
192
234
  });
193
235
 
@@ -226,6 +268,17 @@ export function createHelpTab(screen, services) {
226
268
  if (typeof focusMainTabBar === 'function') { focusMainTabBar(); screen.render(); }
227
269
  });
228
270
 
271
+ // -------------------------------------------------------------------------
272
+ // Language change handler
273
+
274
+ if (languageService) {
275
+ languageService.onChange(() => {
276
+ scrollBox.setContent(_buildContent(''));
277
+ searchLabel.setContent(_tl('helpSearchLabel'));
278
+ screen.render();
279
+ });
280
+ }
281
+
229
282
  // -------------------------------------------------------------------------
230
283
  // Tab Component Contract
231
284
 
@@ -251,7 +304,7 @@ export function createHelpTab(screen, services) {
251
304
  onBlur() {},
252
305
 
253
306
  getFooterText() {
254
- return FOOTER_TEXT;
307
+ return _tl('helpFooter');
255
308
  },
256
309
 
257
310
  getFooterColor() {