agentvibes 4.4.1 → 4.5.7

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 (55) hide show
  1. package/.agentvibes/config.json +4 -4
  2. package/.claude/config/audio-effects.cfg +1 -0
  3. package/.claude/config/background-music-enabled.txt +1 -0
  4. package/.claude/config/reverb-level.txt +1 -1
  5. package/.claude/github-star-reminder.txt +1 -1
  6. package/.claude/hooks/audio-processor.sh +1 -1
  7. package/.claude/hooks/bmad-speak.sh +16 -2
  8. package/.claude/hooks-windows/bmad-speak.ps1 +200 -0
  9. package/.claude/hooks-windows/play-tts-piper.ps1 +3 -4
  10. package/.claude/hooks-windows/play-tts-sapi.ps1 +3 -4
  11. package/.claude/hooks-windows/play-tts-soprano.ps1 +2 -3
  12. package/.claude/hooks-windows/play-tts-termux-ssh.ps1 +138 -0
  13. package/.claude/hooks-windows/play-tts.ps1 +14 -6
  14. package/.claude/hooks-windows/provider-manager.ps1 +16 -1
  15. package/CLAUDE.md +4 -0
  16. package/README.md +39 -9
  17. package/RELEASE_NOTES.md +78 -0
  18. package/bin/agent-vibes +1 -1
  19. package/bin/agentvibes-voice-browser.js +1 -1
  20. package/bin/bmad-speak.js +52 -0
  21. package/bin/mcp-server.js +1 -1
  22. package/bin/test-bmad-pr +1 -1
  23. package/package.json +1 -1
  24. package/setup-windows.ps1 +4 -4
  25. package/src/console/app.js +63 -12
  26. package/src/console/navigation.js +5 -2
  27. package/src/console/tabs/agents-tab.js +72 -76
  28. package/src/console/tabs/help-tab.js +107 -54
  29. package/src/console/tabs/install-tab.js +132 -56
  30. package/src/console/tabs/music-tab.js +1039 -1011
  31. package/src/console/tabs/placeholder-tab.js +27 -0
  32. package/src/console/tabs/readme-tab.js +9 -7
  33. package/src/console/tabs/receiver-tab.js +23 -12
  34. package/src/console/tabs/settings-tab.js +4001 -3783
  35. package/src/console/tabs/voices-tab.js +1680 -1653
  36. package/src/console/widgets/personality-picker.js +35 -7
  37. package/src/console/widgets/reverb-picker.js +9 -6
  38. package/src/console/widgets/track-picker.js +7 -2
  39. package/src/i18n/de.js +203 -0
  40. package/src/i18n/en.js +203 -0
  41. package/src/i18n/es.js +203 -0
  42. package/src/i18n/fr.js +203 -0
  43. package/src/i18n/hi.js +203 -0
  44. package/src/i18n/ja.js +203 -0
  45. package/src/i18n/ko.js +203 -0
  46. package/src/i18n/pt.js +203 -0
  47. package/src/i18n/strings.js +54 -0
  48. package/src/i18n/zh-CN.js +203 -0
  49. package/src/installer/language-screen.js +31 -0
  50. package/src/installer.js +79 -25
  51. package/src/services/language-service.js +47 -0
  52. package/src/utils/file-ownership-verifier.js +2 -2
  53. package/src/utils/provider-validator.js +9 -13
  54. package/.claude/hooks-windows/play-tts-windows-piper.ps1 +0 -209
  55. package/.claude/hooks-windows/play-tts-windows-sapi.ps1 +0 -108
@@ -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
 
@@ -324,7 +319,7 @@ export function createAgentsTab(screen, services) {
324
319
  }
325
320
 
326
321
  const resetBtn = _createBtn('[X] Reset', () => {
327
- const agent = _agents[agentList.selected];
322
+ const agent = _agents[agentList.selected ?? 0];
328
323
  if (agent) {
329
324
  voiceStore.resetAgentProfile(agent.id);
330
325
  refreshDisplay();
@@ -379,7 +374,7 @@ export function createAgentsTab(screen, services) {
379
374
  ? formatTrackName(profile.backgroundMusic.track)
380
375
  : '(global)')).padEnd(COL_MUSIC).slice(0, COL_MUSIC);
381
376
  const vol = profile.backgroundMusic?.enabled
382
- ? ` ${profile.backgroundMusic.volume ?? 70}%`.padEnd(COL_VOL)
377
+ ? ` ${profile.backgroundMusic.volume ?? 20}%`.padEnd(COL_VOL)
383
378
  : ' — ';
384
379
  const pretext = ' ' + (profile.pretext || '(default)').slice(0, COL_PRETEXT - 1);
385
380
  return ` ${icon}${name}${voice}${gender}${provider}${reverb}${music}${vol} ${pretext}`;
@@ -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
 
@@ -532,7 +513,7 @@ export function createAgentsTab(screen, services) {
532
513
  personality: profile.personality || globalCfg.personality || 'none',
533
514
  backgroundMusic: {
534
515
  track: profile.backgroundMusic?.track || globalCfg.backgroundMusic?.track || '',
535
- volume: profile.backgroundMusic?.volume ?? globalCfg.backgroundMusic?.volume ?? 70,
516
+ volume: profile.backgroundMusic?.volume ?? globalCfg.backgroundMusic?.volume ?? 20,
536
517
  enabled: profile.backgroundMusic?.enabled ?? globalCfg.backgroundMusic?.enabled ?? false,
537
518
  },
538
519
  });
@@ -553,7 +534,7 @@ export function createAgentsTab(screen, services) {
553
534
  personality: profile.personality || globalCfg.personality || 'none',
554
535
  backgroundMusic: {
555
536
  track: profile.backgroundMusic?.track || globalCfg.backgroundMusic?.track || '',
556
- volume: profile.backgroundMusic?.volume ?? globalCfg.backgroundMusic?.volume ?? 70,
537
+ volume: profile.backgroundMusic?.volume ?? globalCfg.backgroundMusic?.volume ?? 20,
557
538
  enabled: profile.backgroundMusic?.enabled ?? globalCfg.backgroundMusic?.enabled ?? false,
558
539
  },
559
540
  };
@@ -676,7 +657,7 @@ export function createAgentsTab(screen, services) {
676
657
  if (draft.reverbPreset !== (globalCfg.effects?.reverbPreset || 'light')) toSave.reverbPreset = draft.reverbPreset;
677
658
  if (draft.personality !== (globalCfg.personality || 'none')) toSave.personality = draft.personality;
678
659
  if (draft.backgroundMusic.track !== (globalCfg.backgroundMusic?.track || '') ||
679
- draft.backgroundMusic.volume !== (globalCfg.backgroundMusic?.volume ?? 70) ||
660
+ draft.backgroundMusic.volume !== (globalCfg.backgroundMusic?.volume ?? 20) ||
680
661
  draft.backgroundMusic.enabled !== (globalCfg.backgroundMusic?.enabled ?? false)) {
681
662
  toSave.backgroundMusic = draft.backgroundMusic;
682
663
  }
@@ -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);
@@ -1289,7 +1275,7 @@ export function createAgentsTab(screen, services) {
1289
1275
  const track = shuffled[i % shuffled.length];
1290
1276
  const existing = voiceStore.getAgentProfile(agent.id);
1291
1277
  voiceStore.setAgentProfile(agent.id, {
1292
- backgroundMusic: { track, volume: existing.backgroundMusic?.volume ?? 70, enabled: true },
1278
+ backgroundMusic: { track, volume: existing.backgroundMusic?.volume ?? 20, enabled: true },
1293
1279
  });
1294
1280
  });
1295
1281
  return true;
@@ -1387,7 +1373,7 @@ export function createAgentsTab(screen, services) {
1387
1373
 
1388
1374
  case 'setMusic':
1389
1375
  _closeMenu(() => {
1390
- openTrackPicker(screen, '', 70, (track, volume) => {
1376
+ openTrackPicker(screen, '', 20, (track, volume) => {
1391
1377
  _agents.forEach(agent => {
1392
1378
  const p = voiceStore.getAgentProfile(agent.id);
1393
1379
  voiceStore.setAgentProfile(agent.id, {
@@ -1403,7 +1389,7 @@ export function createAgentsTab(screen, services) {
1403
1389
 
1404
1390
  case 'setVolume':
1405
1391
  _closeMenu(() => {
1406
- const curVol = voiceStore.getAgentProfile(_agents[0]?.id)?.backgroundMusic?.volume ?? 70;
1392
+ const curVol = voiceStore.getAgentProfile(_agents[0]?.id)?.backgroundMusic?.volume ?? 20;
1407
1393
  openVolumeInput(screen, curVol, (volume) => {
1408
1394
  _agents.forEach(agent => {
1409
1395
  const p = voiceStore.getAgentProfile(agent.id);
@@ -1518,7 +1504,7 @@ export function createAgentsTab(screen, services) {
1518
1504
  // Key bindings
1519
1505
 
1520
1506
  agentList.key(['x', 'X'], () => {
1521
- const agent = _agents[agentList.selected];
1507
+ const agent = _agents[agentList.selected ?? 0];
1522
1508
  if (agent) {
1523
1509
  voiceStore.resetAgentProfile(agent.id);
1524
1510
  refreshDisplay();
@@ -1527,12 +1513,12 @@ export function createAgentsTab(screen, services) {
1527
1513
 
1528
1514
 
1529
1515
  agentList.key(['enter'], () => {
1530
- const agent = _agents[agentList.selected];
1516
+ const agent = _agents[agentList.selected ?? 0];
1531
1517
  if (agent) _openAgentDetailPanel(agent);
1532
1518
  });
1533
1519
 
1534
1520
  agentList.key(['space'], () => {
1535
- const agent = _agents[agentList.selected];
1521
+ const agent = _agents[agentList.selected ?? 0];
1536
1522
  if (agent) _sampleAgent(agent);
1537
1523
  });
1538
1524
 
@@ -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() {