agentvibes 5.10.1 → 5.11.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 (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 +12 -86
  94. package/RELEASE_NOTES.md +60 -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
@@ -45,7 +45,7 @@ const COLORS = {
45
45
  dimFg: '#455a64',
46
46
  };
47
47
 
48
- const FOOTER_TEXT = '[↑↓/jk] Navigate [Space] Preview [Enter] Select/Install [*] Favorite [/] Search';
48
+ const FOOTER_TEXT = '[↑↓/jk] Navigate [Space] Preview [Enter] Select/Install [f] Favorite [/] Search';
49
49
  /**
50
50
  * Resolve the Piper voice storage directory using the same precedence as the
51
51
  * shell-side get_voice_storage_dir() in piper-voice-manager.sh:
@@ -229,6 +229,64 @@ export function genderIconTag(gender) {
229
229
  return '—';
230
230
  }
231
231
 
232
+ // Fixed column widths shared by every provider's voice picker so the rows line up
233
+ // identically regardless of engine. `status` is in terminal cells (emoji count 2).
234
+ export const VOICE_ROW_COLS = Object.freeze({ status: 3, name: 14, lang: 6 });
235
+
236
+ const _TAG_RE = /\{[^}]*\}/g;
237
+
238
+ /**
239
+ * Visible terminal-cell width of a blessed-tagged string. Color tags ({...}) are
240
+ * zero-width; astral-plane emoji (👍/👎, cp ≥ U+1F000) render two cells; the
241
+ * VS16 emoji-presentation selector (U+FE0F) adds none. BMP symbols (★ ♀ ♂ ♪ ✓ ☁)
242
+ * stay one cell. Used to pad the status column so emoji-bearing rows (Piper's
243
+ * thumbs) align with single-glyph rows (Kokoro/ElevenLabs).
244
+ * @param {string} str
245
+ * @returns {number}
246
+ */
247
+ export function cellWidth(str) {
248
+ const plain = String(str).replace(_TAG_RE, '');
249
+ let w = 0;
250
+ for (const ch of plain) {
251
+ const cp = ch.codePointAt(0);
252
+ if (cp === 0xFE0F) continue; // variation selector — zero width
253
+ w += cp >= 0x1F000 ? 2 : 1; // astral emoji = 2 cells, everything else = 1
254
+ }
255
+ return w;
256
+ }
257
+
258
+ /**
259
+ * Canonical voice-row renderer used by ALL voice pickers (Piper, Kokoro,
260
+ * ElevenLabs) so their columns and per-column styling are identical:
261
+ * [status] [Name] [gender ♀/♂ colored] [language grey] [detail grey]
262
+ *
263
+ * @param {object} o
264
+ * @param {string} [o.status=' '] provider indicator slot (e.g. fav/cache marks); may contain tags
265
+ * @param {string} [o.name=''] voice display name
266
+ * @param {string} [o.gender=''] 'Female' | 'Male' | '' (drives the colored icon)
267
+ * @param {string} [o.lang=''] language tag (e.g. 'en-US', 'ja')
268
+ * @param {string} [o.detail=''] trailing description / id / provider (rendered grey)
269
+ * @returns {string} blessed tag-formatted row (host list must set tags:true)
270
+ */
271
+ export function formatVoiceRow({ status = ' ', name = '', gender = '', lang = '', detail = '' } = {}) {
272
+ const { status: STATUS_W, name: NAME_W, lang: LANG_W } = VOICE_ROW_COLS;
273
+ // Pad the status to a fixed cell width so every provider's name column starts
274
+ // at the same offset, even when the status holds a 2-cell emoji (Piper thumbs).
275
+ const st = status + ' '.repeat(Math.max(0, STATUS_W - cellWidth(status)));
276
+ const nm = name.length > NAME_W ? name.slice(0, NAME_W - 1) + '…' : name.padEnd(NAME_W);
277
+ const lg = String(lang || '').padEnd(LANG_W);
278
+ const det = detail ? `{#9e9e9e-fg}${detail}{/#9e9e9e-fg}` : '';
279
+ return ` ${st} ${nm} ${genderIconTag(gender)} {#9e9e9e-fg}${lg}{/#9e9e9e-fg} ${det}`;
280
+ }
281
+
282
+ /** Standardized column header matching formatVoiceRow's layout. */
283
+ export function voiceRowHeader() {
284
+ const { status: STATUS_W, name: NAME_W, lang: LANG_W } = VOICE_ROW_COLS;
285
+ // Lead = the row's leading space + status column + the space after it.
286
+ const lead = ' '.repeat(1 + STATUS_W + 1);
287
+ return `${lead}{cyan-fg}${'Name'.padEnd(NAME_W)}{/cyan-fg} {cyan-fg}⚥{/cyan-fg} {cyan-fg}${'Lang'.padEnd(LANG_W)}{/cyan-fg} {cyan-fg}Detail{/cyan-fg}`;
288
+ }
289
+
232
290
  // ---------------------------------------------------------------------------
233
291
  // Pure helpers — exported for testability
234
292
 
@@ -776,7 +834,7 @@ export function createVoicesTab(screen, services) {
776
834
 
777
835
  // -------------------------------------------------------------------------
778
836
  // Hint text shown in previewLine when the list has focus and nothing is playing
779
- const HINT_TEXT = '{white-fg}[Space] preview [Enter] select [+] thumbs up [-] thumbs down{/white-fg}';
837
+ const HINT_TEXT = '{white-fg}[Space] preview [Enter] select [f] favorite [/] search{/white-fg}';
780
838
  let _listFocused = false;
781
839
 
782
840
  // Inline selection hint appended to the currently highlighted voice row.
@@ -1185,49 +1243,10 @@ export function createVoicesTab(screen, services) {
1185
1243
  return btn;
1186
1244
  }
1187
1245
 
1188
- const switchBtn = _createBtn(_tl('voicesSwitchBtn'), () => {
1189
- const voices = _getFilteredVoices();
1190
- const selected = voices[voiceList.selected];
1191
- if (selected) {
1192
- _activateVoice(selected);
1193
- refreshDisplay();
1194
- }
1195
- });
1196
- switchBtn.bottom = 4;
1197
- switchBtn.left = 4;
1198
-
1199
- const favoriteBtn = _createBtn(_tl('voicesFavoriteBtn'), () => {
1200
- const voices = _getFilteredVoices();
1201
- const selected = voices[voiceList.selected];
1202
- if (selected) {
1203
- toggleFavorite(configService, selected);
1204
- refreshDisplay();
1205
- }
1206
- });
1207
- favoriteBtn.bottom = 4;
1208
- favoriteBtn.left = 22;
1209
-
1210
- const installBtn = _createBtn(_tl('voicesDownloadBtn'), () => {
1211
- const voices = _getFilteredVoices();
1212
- const selected = voices[voiceList.selected];
1213
- if (!selected) return;
1214
- if (_isInstalled(selected)) {
1215
- const notice = blessed.text({
1216
- parent: box,
1217
- top: 'center',
1218
- left: 'center',
1219
- content: 'Voice already installed. Scroll down to find uninstalled voices (greyed out).',
1220
- tags: true,
1221
- style: { fg: COLORS.noticeFg, bg: COLORS.contentBg },
1222
- });
1223
- screen.render();
1224
- setTimeout(() => { notice.destroy(); screen.render(); }, 3000);
1225
- } else {
1226
- _openDownloadModal(selected);
1227
- }
1228
- });
1229
- installBtn.bottom = 4;
1230
- installBtn.left = 38;
1246
+ // The voice list is button-free: Enter selects (or downloads an uninstalled
1247
+ // Piper voice then selects), Space previews, and 'f'/'*' toggles the favorite
1248
+ // star. The old Switch/Favorite/Download button row was redundant with these
1249
+ // keys and has been removed.
1231
1250
 
1232
1251
  // -------------------------------------------------------------------------
1233
1252
  // "Voice Changed" notice — auto-dismisses after 2 s
@@ -1725,11 +1744,13 @@ export function createVoicesTab(screen, services) {
1725
1744
 
1726
1745
  const active = providerService.getActiveVoiceId();
1727
1746
  const favorites = getFavorites(configService);
1728
- const thumbsDown = getThumbsDown(configService);
1747
+ // Thumbs-down was removed — only the favorite star remains. Pass an empty
1748
+ // set so any pre-existing downvotes in config no longer render.
1749
+ const thumbsDown = [];
1729
1750
  const filtered = _getFilteredVoices();
1730
1751
  const items = _buildListItems(filtered, active, favorites, thumbsDown);
1731
1752
 
1732
- voiceList.setItems(items.length > 0 ? items : [' (no voices foundinstall piper first)']);
1753
+ voiceList.setItems(items.length > 0 ? items : [' No voices installedrun: npx agentvibes voices download']);
1733
1754
  const maxIdx = Math.max(0, (items.length > 0 ? items.length : 1) - 1);
1734
1755
  voiceList.select(Math.min(savedIdx, maxIdx));
1735
1756
  voiceList.childBase = Math.min(savedScroll, Math.max(0, (items.length > 0 ? items.length : 1) - (voiceList.height - 2)));
@@ -1816,21 +1837,13 @@ export function createVoicesTab(screen, services) {
1816
1837
  });
1817
1838
 
1818
1839
  // '*' or '+' in voiceList toggles thumbs-up
1819
- voiceList.key(['*', '+'], () => {
1840
+ // 'f' / '*' / '+' toggle the favorite star. There is a single favorite
1841
+ // mechanism now (the star) — thumbs-up/thumbs-down was removed.
1842
+ voiceList.key(['f', '*', '+'], () => {
1820
1843
  const voices = _getFilteredVoices();
1821
1844
  const selected = voices[voiceList.selected];
1822
1845
  if (selected) {
1823
- toggleThumbsUp(configService, selected);
1824
- refreshDisplay();
1825
- }
1826
- });
1827
-
1828
- // '-' in voiceList toggles thumbs-down
1829
- voiceList.key(['-'], () => {
1830
- const voices = _getFilteredVoices();
1831
- const selected = voices[voiceList.selected];
1832
- if (selected) {
1833
- toggleThumbsDown(configService, selected);
1846
+ toggleFavorite(configService, selected);
1834
1847
  refreshDisplay();
1835
1848
  }
1836
1849
  });
@@ -1961,32 +1974,6 @@ export function createVoicesTab(screen, services) {
1961
1974
  }
1962
1975
  });
1963
1976
 
1964
- // -------------------------------------------------------------------------
1965
- // Button-row keyboard navigation
1966
- // ↓ at the last list item → descend into the button row (Switch Voice gets focus first)
1967
- // Note: Tab is NOT used — navigation.js registers screen.key(['tab']) to cycle tabs,
1968
- // so element.key(['tab']) + screen.key(['tab']) both fire simultaneously.
1969
- voiceList.key(['down'], () => {
1970
- const voices = _getFilteredVoices();
1971
- if (voiceList.selected >= voices.length - 1) {
1972
- switchBtn.focus();
1973
- screen.render();
1974
- }
1975
- });
1976
-
1977
- // ←/→ navigate between the three buttons
1978
- switchBtn.key(['right'], () => { favoriteBtn.focus(); screen.render(); });
1979
- favoriteBtn.key(['right'], () => { installBtn.focus(); screen.render(); });
1980
- installBtn.key(['right'], () => { switchBtn.focus(); screen.render(); });
1981
- switchBtn.key(['left'], () => { installBtn.focus(); screen.render(); });
1982
- favoriteBtn.key(['left'], () => { switchBtn.focus(); screen.render(); });
1983
- installBtn.key(['left'], () => { favoriteBtn.focus(); screen.render(); });
1984
-
1985
- // ↑ or Escape from any button → back to voice list
1986
- switchBtn.key(['up', 'escape'], () => { voiceList.focus(); screen.render(); });
1987
- favoriteBtn.key(['up', 'escape'], () => { voiceList.focus(); screen.render(); });
1988
- installBtn.key(['up', 'escape'], () => { voiceList.focus(); screen.render(); });
1989
-
1990
1977
  // -------------------------------------------------------------------------
1991
1978
  // Language refresh
1992
1979
 
@@ -1995,9 +1982,6 @@ export function createVoicesTab(screen, services) {
1995
1982
  searchLabelText.setContent(_tl('searchLabel'));
1996
1983
  colHeaderText.setContent(`{#00897b-fg}${_tl('voicesColName').padEnd(COL_NAME_W)}{/#00897b-fg}{magenta-fg}♀{/magenta-fg}/{bright-cyan-fg}♂{/bright-cyan-fg} {#00897b-fg}${_tl('voicesColProvider')}{/#00897b-fg}`);
1997
1984
  voiceInfoHdr.setContent(`{#00897b-fg}${_tl('voicesInfoHeader')}${'─'.repeat(54)}{/#00897b-fg}`);
1998
- switchBtn.setContent(_tl('voicesSwitchBtn'));
1999
- favoriteBtn.setContent(_tl('voicesFavoriteBtn'));
2000
- installBtn.setContent(_tl('voicesDownloadBtn'));
2001
1985
  screen.render();
2002
1986
  }
2003
1987
 
@@ -1,25 +1,25 @@
1
- /**
2
- * AgentVibes TUI — Shared Widget: Modal Destroy Helper
3
- *
4
- * Force-invalidates blessed's olines buffer after destroying a modal widget.
5
- * Without this, blessed skips repainting cells where lines==olines and the
6
- * terminal retains stale modal content as ghost artifacts.
7
- */
8
-
9
- /**
10
- * Destroy a blessed list/box widget and force full screen repaint.
11
- *
12
- * @param {object} widget - blessed widget to destroy
13
- * @param {object} screen - blessed screen instance
14
- * @param {Function} [onClose] - optional callback after destruction
15
- */
16
- export function destroyList(widget, screen, onClose) {
17
- widget.destroy();
18
- try {
19
- for (let r = 0; r < screen.height; r++)
20
- for (let c = 0; c < screen.width; c++)
21
- if (screen.olines[r]?.[c]) screen.olines[r][c][0] = -1;
22
- } catch {}
23
- onClose?.();
24
- screen.render();
25
- }
1
+ /**
2
+ * AgentVibes TUI — Shared Widget: Modal Destroy Helper
3
+ *
4
+ * Force-invalidates blessed's olines buffer after destroying a modal widget.
5
+ * Without this, blessed skips repainting cells where lines==olines and the
6
+ * terminal retains stale modal content as ghost artifacts.
7
+ */
8
+
9
+ /**
10
+ * Destroy a blessed list/box widget and force full screen repaint.
11
+ *
12
+ * @param {object} widget - blessed widget to destroy
13
+ * @param {object} screen - blessed screen instance
14
+ * @param {Function} [onClose] - optional callback after destruction
15
+ */
16
+ export function destroyList(widget, screen, onClose) {
17
+ widget.destroy();
18
+ try {
19
+ for (let r = 0; r < screen.height; r++)
20
+ for (let c = 0; c < screen.width; c++)
21
+ if (screen.olines[r]?.[c]) screen.olines[r][c][0] = -1;
22
+ } catch {}
23
+ onClose?.();
24
+ screen.render();
25
+ }
@@ -96,6 +96,18 @@ export function formatVoiceName(voice) {
96
96
  * @returns {string}
97
97
  */
98
98
  export function formatReverbState(preset) {
99
- const LABELS = { off: 'Off', light: 'Light (Small room)', medium: 'Medium (Conference room)', heavy: 'Heavy (Large hall)', cathedral: 'Cathedral (Epic space)' };
100
- return LABELS[preset] ?? LABELS.light;
99
+ const LABELS = {
100
+ off: 'Off',
101
+ light: 'Light Reverb',
102
+ medium: 'Medium Reverb',
103
+ heavy: 'Heavy Reverb',
104
+ cathedral: 'Cathedral',
105
+ 'chorus-light': 'Light Chorus',
106
+ 'chorus-deep': 'Deep Chorus',
107
+ 'echo-short': 'Echo (short)',
108
+ 'echo-long': 'Cave Echo',
109
+ warm: 'Warm',
110
+ radio: 'Radio',
111
+ };
112
+ return LABELS[preset] ?? 'Light Reverb';
101
113
  }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * AgentVibes TUI — Shared Widget: Picker Chrome Helpers
3
+ *
4
+ * Pure, blessed-free string builders shared by every selector modal so the
5
+ * control hints and titles look identical across the whole app. Keeping these
6
+ * free of any blessed import makes them trivially unit-testable (no
7
+ * AGENTVIBES_TEST_MODE gate needed).
8
+ *
9
+ * renderHelpBar([{ key, label }, ...]) -> a tag-formatted hint row
10
+ * selectorTitle('Voice') -> a standardized box label
11
+ *
12
+ * Color grammar (Paul's spec): square brackets grey, key word magenta,
13
+ * the '=' grey, the action label white.
14
+ */
15
+
16
+ const C_BRACKET = '#9e9e9e'; // grey — the [ ] around the key
17
+ const C_KEY = 'magenta'; // — the key word (Space, Enter, Esc…)
18
+ const C_EQ = '#9e9e9e'; // grey — the '=' sign
19
+ const C_LABEL = 'white'; // — the action label (preview, select…)
20
+
21
+ /**
22
+ * Build one standardized help-bar row from a list of {key, label} hints.
23
+ * Only pass hints whose keys actually do something in this picker — the bar
24
+ * must never advertise a control that does not fire.
25
+ *
26
+ * @param {Array<{key: string, label: string}>} items
27
+ * @param {object} [opts]
28
+ * @param {string} [opts.sep=' '] separator between hints
29
+ * @returns {string} blessed tag-formatted string (host must set tags:true)
30
+ */
31
+ export function renderHelpBar(items, opts = {}) {
32
+ const sep = opts.sep ?? ' ';
33
+ if (!Array.isArray(items)) return '';
34
+ return items
35
+ .filter((it) => it && it.key && it.label)
36
+ .map(({ key, label }) =>
37
+ `{${C_BRACKET}-fg}[{/${C_BRACKET}-fg}`
38
+ + `{${C_KEY}-fg}${key}{/${C_KEY}-fg}`
39
+ + `{${C_BRACKET}-fg}]{/${C_BRACKET}-fg} `
40
+ + `{${C_EQ}-fg}={/${C_EQ}-fg} `
41
+ + `{${C_LABEL}-fg}${label}{/${C_LABEL}-fg}`)
42
+ .join(sep);
43
+ }
44
+
45
+ /**
46
+ * Standardized box label for a selector modal:
47
+ * ' {bold}{cyan-fg}<name> Selector{/cyan-fg}{/bold} '
48
+ * Pass the noun only — e.g. selectorTitle('Voice') -> ' Voice Selector '.
49
+ *
50
+ * @param {string} name
51
+ * @returns {string}
52
+ */
53
+ export function selectorTitle(name) {
54
+ return ` {bold}{cyan-fg}${name} Selector{/cyan-fg}{/bold} `;
55
+ }
@@ -1,55 +1,55 @@
1
- /**
2
- * AgentVibes TUI — Shared Widget: Notice Toast
3
- *
4
- * Displays a short auto-dismissing notice modal centred on screen.
5
- * Usable from any tab; no settings-specific state required.
6
- */
7
-
8
- import { destroyList } from './destroy-list.js';
9
-
10
- const IS_TEST = process.env.AGENTVIBES_TEST_MODE === 'true';
11
- let blessed;
12
- if (!IS_TEST) {
13
- const { default: b } = await import('blessed');
14
- blessed = b;
15
- }
16
-
17
- /**
18
- * Show a temporary notice that auto-dismisses after 2.5 seconds.
19
- *
20
- * @param {object} screen - blessed screen instance
21
- * @param {string} message - text to display
22
- * @param {object} [opts]
23
- * @param {string} [opts.bg='#0a0e1a'] - background colour
24
- * @param {string} [opts.fg='#e3f2fd'] - foreground colour
25
- * @param {string} [opts.borderFg='bright-cyan'] - border colour
26
- * @param {number} [opts.durationMs=2500] - auto-dismiss delay in ms
27
- */
28
- export function showNotice(screen, message, opts = {}) {
29
- const bg = opts.bg ?? '#0a0e1a';
30
- const fg = opts.fg ?? '#e3f2fd';
31
- const borderFg = opts.borderFg ?? 'bright-cyan';
32
- const durationMs = opts.durationMs ?? 2500;
33
-
34
- const width = Math.max(28, message.length + 6);
35
- const modal = blessed.box({
36
- parent: screen,
37
- top: 'center',
38
- left: 'center',
39
- width,
40
- height: 3,
41
- border: { type: 'line' },
42
- tags: true,
43
- content: `{center}${message}{/center}`,
44
- style: {
45
- fg,
46
- bg,
47
- border: { fg: borderFg },
48
- },
49
- });
50
- screen.render();
51
-
52
- setTimeout(() => {
53
- destroyList(modal, screen);
54
- }, durationMs);
55
- }
1
+ /**
2
+ * AgentVibes TUI — Shared Widget: Notice Toast
3
+ *
4
+ * Displays a short auto-dismissing notice modal centred on screen.
5
+ * Usable from any tab; no settings-specific state required.
6
+ */
7
+
8
+ import { destroyList } from './destroy-list.js';
9
+
10
+ const IS_TEST = process.env.AGENTVIBES_TEST_MODE === 'true';
11
+ let blessed;
12
+ if (!IS_TEST) {
13
+ const { default: b } = await import('blessed');
14
+ blessed = b;
15
+ }
16
+
17
+ /**
18
+ * Show a temporary notice that auto-dismisses after 2.5 seconds.
19
+ *
20
+ * @param {object} screen - blessed screen instance
21
+ * @param {string} message - text to display
22
+ * @param {object} [opts]
23
+ * @param {string} [opts.bg='#0a0e1a'] - background colour
24
+ * @param {string} [opts.fg='#e3f2fd'] - foreground colour
25
+ * @param {string} [opts.borderFg='bright-cyan'] - border colour
26
+ * @param {number} [opts.durationMs=2500] - auto-dismiss delay in ms
27
+ */
28
+ export function showNotice(screen, message, opts = {}) {
29
+ const bg = opts.bg ?? '#0a0e1a';
30
+ const fg = opts.fg ?? '#e3f2fd';
31
+ const borderFg = opts.borderFg ?? 'bright-cyan';
32
+ const durationMs = opts.durationMs ?? 2500;
33
+
34
+ const width = Math.max(28, message.length + 6);
35
+ const modal = blessed.box({
36
+ parent: screen,
37
+ top: 'center',
38
+ left: 'center',
39
+ width,
40
+ height: 3,
41
+ border: { type: 'line' },
42
+ tags: true,
43
+ content: `{center}${message}{/center}`,
44
+ style: {
45
+ fg,
46
+ bg,
47
+ border: { fg: borderFg },
48
+ },
49
+ });
50
+ screen.render();
51
+
52
+ setTimeout(() => {
53
+ destroyList(modal, screen);
54
+ }, durationMs);
55
+ }