agentvibes 4.0.1 → 4.4.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 (219) hide show
  1. package/.agentvibes/bmad/bmad-voices.md +69 -69
  2. package/.agentvibes/config.json +12 -0
  3. package/.claude/activation-instructions +54 -54
  4. package/.claude/audio/tracks/README.md +52 -52
  5. package/.claude/commands/agent-vibes/add.md +21 -21
  6. package/.claude/commands/agent-vibes/agent-vibes.md +101 -101
  7. package/.claude/commands/agent-vibes/agent.md +79 -79
  8. package/.claude/commands/agent-vibes/background-music.md +111 -111
  9. package/.claude/commands/agent-vibes/bmad.md +198 -198
  10. package/.claude/commands/agent-vibes/clean.md +18 -18
  11. package/.claude/commands/agent-vibes/cleanup.md +18 -18
  12. package/.claude/commands/agent-vibes/commands.json +145 -145
  13. package/.claude/commands/agent-vibes/effects.md +97 -97
  14. package/.claude/commands/agent-vibes/get.md +9 -9
  15. package/.claude/commands/agent-vibes/hide.md +91 -91
  16. package/.claude/commands/agent-vibes/language.md +23 -23
  17. package/.claude/commands/agent-vibes/learn.md +67 -67
  18. package/.claude/commands/agent-vibes/list.md +13 -13
  19. package/.claude/commands/agent-vibes/mute.md +37 -37
  20. package/.claude/commands/agent-vibes/preview.md +17 -17
  21. package/.claude/commands/agent-vibes/provider.md +68 -68
  22. package/.claude/commands/agent-vibes/replay-target.md +14 -14
  23. package/.claude/commands/agent-vibes/sample.md +12 -12
  24. package/.claude/commands/agent-vibes/set-favorite-voice.md +84 -84
  25. package/.claude/commands/agent-vibes/set-pretext.md +65 -65
  26. package/.claude/commands/agent-vibes/set-speed.md +41 -41
  27. package/.claude/commands/agent-vibes/show.md +84 -84
  28. package/.claude/commands/agent-vibes/switch.md +87 -87
  29. package/.claude/commands/agent-vibes/target-voice.md +26 -26
  30. package/.claude/commands/agent-vibes/target.md +30 -30
  31. package/.claude/commands/agent-vibes/translate.md +68 -68
  32. package/.claude/commands/agent-vibes/unmute.md +45 -45
  33. package/.claude/commands/agent-vibes/verbosity.md +89 -89
  34. package/.claude/commands/agent-vibes/whoami.md +7 -7
  35. package/.claude/commands/agent-vibes-bmad-voices.md +117 -117
  36. package/.claude/commands/agent-vibes-rdp.md +24 -24
  37. package/.claude/config/agentvibes.json +1 -0
  38. package/.claude/config/audio-effects.cfg +3 -2
  39. package/.claude/config/audio-effects.cfg.sample +52 -52
  40. package/.claude/config/background-music-volume.txt +1 -0
  41. package/.claude/config/intro-text.txt +1 -0
  42. package/.claude/config/piper-speech-rate.txt +4 -0
  43. package/.claude/config/piper-target-speech-rate.txt +1 -0
  44. package/.claude/config/reverb-level.txt +1 -0
  45. package/.claude/config/tts-speech-rate.txt +4 -0
  46. package/.claude/config/tts-target-speech-rate.txt +1 -0
  47. package/.claude/docs/TERMUX_SETUP.md +408 -408
  48. package/.claude/github-star-reminder.txt +1 -1
  49. package/.claude/hooks/README-TTS-QUEUE.md +135 -135
  50. package/.claude/hooks/audio-cache-utils.sh +246 -246
  51. package/.claude/hooks/audio-processor.sh +433 -389
  52. package/.claude/hooks/background-music-manager.sh +404 -404
  53. package/.claude/hooks/bmad-speak-enhanced.sh +165 -165
  54. package/.claude/hooks/bmad-speak.sh +269 -112
  55. package/.claude/hooks/bmad-tts-injector.sh +568 -568
  56. package/.claude/hooks/bmad-voice-manager.sh +928 -928
  57. package/.claude/hooks/clawdbot-receiver-SECURE.sh +129 -129
  58. package/.claude/hooks/clawdbot-receiver.sh +107 -107
  59. package/.claude/hooks/clean-audio-cache.sh +22 -22
  60. package/.claude/hooks/cleanup-cache.sh +106 -106
  61. package/.claude/hooks/configure-rdp-mode.sh +137 -137
  62. package/.claude/hooks/download-extra-voices.sh +244 -244
  63. package/.claude/hooks/effects-manager.sh +268 -268
  64. package/.claude/hooks/github-star-reminder.sh +154 -154
  65. package/.claude/hooks/language-manager.sh +362 -362
  66. package/.claude/hooks/learn-manager.sh +492 -492
  67. package/.claude/hooks/macos-voice-manager.sh +205 -205
  68. package/.claude/hooks/migrate-background-music.sh +125 -125
  69. package/.claude/hooks/migrate-to-agentvibes.sh +161 -161
  70. package/.claude/hooks/optimize-background-music.sh +87 -87
  71. package/.claude/hooks/path-resolver.sh +60 -60
  72. package/.claude/hooks/personality-manager.sh +448 -448
  73. package/.claude/hooks/piper-download-voices.sh +225 -225
  74. package/.claude/hooks/piper-installer.sh +292 -292
  75. package/.claude/hooks/piper-multispeaker-registry.sh +171 -171
  76. package/.claude/hooks/piper-voice-manager.sh +24 -3
  77. package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +90 -90
  78. package/.claude/hooks/play-tts-enhanced.sh +105 -70
  79. package/.claude/hooks/play-tts-macos.sh +368 -345
  80. package/.claude/hooks/play-tts-piper.sh +679 -578
  81. package/.claude/hooks/play-tts-soprano.sh +356 -320
  82. package/.claude/hooks/play-tts-ssh-remote.sh +167 -88
  83. package/.claude/hooks/play-tts-termux-ssh.sh +169 -169
  84. package/.claude/hooks/play-tts.sh +301 -298
  85. package/.claude/hooks/prepare-release.sh +54 -54
  86. package/.claude/hooks/provider-commands.sh +617 -617
  87. package/.claude/hooks/provider-manager.sh +399 -399
  88. package/.claude/hooks/replay-target-audio.sh +95 -95
  89. package/.claude/hooks/requirements.txt +6 -6
  90. package/.claude/hooks/sentiment-manager.sh +201 -201
  91. package/.claude/hooks/session-start-tts.sh +81 -71
  92. package/.claude/hooks/soprano-gradio-synth.py +139 -139
  93. package/.claude/hooks/speed-manager.sh +291 -291
  94. package/.claude/hooks/stop-tts.sh +84 -0
  95. package/.claude/hooks/termux-installer.sh +261 -261
  96. package/.claude/hooks/translate-manager.sh +341 -341
  97. package/.claude/hooks/translator.py +237 -237
  98. package/.claude/hooks/tts-queue-worker.sh +145 -114
  99. package/.claude/hooks/tts-queue.sh +165 -136
  100. package/.claude/hooks/verbosity-manager.sh +178 -178
  101. package/.claude/hooks/voice-manager.sh +548 -544
  102. package/.claude/hooks-windows/audio-cache-utils.ps1 +119 -119
  103. package/.claude/hooks-windows/background-music-manager.ps1 +348 -0
  104. package/.claude/hooks-windows/clean-audio-cache.ps1 +53 -0
  105. package/.claude/hooks-windows/download-extra-voices.ps1 +185 -0
  106. package/.claude/hooks-windows/effects-manager.ps1 +294 -0
  107. package/.claude/hooks-windows/language-manager.ps1 +193 -0
  108. package/.claude/hooks-windows/learn-manager.ps1 +241 -0
  109. package/.claude/hooks-windows/personality-manager.ps1 +266 -0
  110. package/.claude/hooks-windows/play-tts-piper.ps1 +209 -0
  111. package/.claude/hooks-windows/play-tts-sapi.ps1 +108 -0
  112. package/.claude/hooks-windows/play-tts-soprano.ps1 +159 -158
  113. package/.claude/hooks-windows/play-tts-windows-piper.ps1 +50 -5
  114. package/.claude/hooks-windows/play-tts-windows-sapi.ps1 +108 -108
  115. package/.claude/hooks-windows/play-tts.ps1 +344 -266
  116. package/.claude/hooks-windows/provider-manager.ps1 +29 -10
  117. package/.claude/hooks-windows/session-start-tts.ps1 +124 -124
  118. package/.claude/hooks-windows/soprano-gradio-synth.py +153 -153
  119. package/.claude/hooks-windows/speed-manager.ps1 +166 -0
  120. package/.claude/hooks-windows/verbosity-manager.ps1 +119 -0
  121. package/.claude/hooks-windows/voice-manager-windows.ps1 +92 -8
  122. package/.claude/output-styles/agent-vibes.md +202 -202
  123. package/.claude/personalities/angry.md +14 -14
  124. package/.claude/personalities/annoying.md +14 -14
  125. package/.claude/personalities/crass.md +14 -14
  126. package/.claude/personalities/dramatic.md +14 -14
  127. package/.claude/personalities/dry-humor.md +50 -50
  128. package/.claude/personalities/flirty.md +20 -20
  129. package/.claude/personalities/funny.md +14 -14
  130. package/.claude/personalities/grandpa.md +32 -32
  131. package/.claude/personalities/millennial.md +14 -14
  132. package/.claude/personalities/moody.md +14 -14
  133. package/.claude/personalities/normal.md +16 -16
  134. package/.claude/personalities/pirate.md +14 -14
  135. package/.claude/personalities/poetic.md +14 -14
  136. package/.claude/personalities/professional.md +14 -14
  137. package/.claude/personalities/rapper.md +55 -55
  138. package/.claude/personalities/robot.md +14 -14
  139. package/.claude/personalities/sarcastic.md +38 -38
  140. package/.claude/personalities/sassy.md +14 -14
  141. package/.claude/personalities/surfer-dude.md +14 -14
  142. package/.claude/personalities/zen.md +14 -14
  143. package/.claude/settings.json +15 -15
  144. package/.claude/verbosity.txt +1 -1
  145. package/.clawdbot/README.md +105 -105
  146. package/.clawdbot/skill/SKILL.md +241 -241
  147. package/.mcp.json +12 -0
  148. package/CLAUDE.md +170 -181
  149. package/README.md +2029 -1909
  150. package/RELEASE_NOTES.md +1310 -66
  151. package/WINDOWS-SETUP.md +208 -208
  152. package/bin/agent-vibes +39 -39
  153. package/bin/agentvibes-voice-browser.js +1840 -1826
  154. package/bin/agentvibes.js +48 -2
  155. package/bin/mcp-server.js +121 -121
  156. package/bin/mcp-server.sh +206 -206
  157. package/bin/test-bmad-pr +78 -78
  158. package/mcp-server/QUICK_START.md +203 -203
  159. package/mcp-server/README.md +345 -345
  160. package/mcp-server/WINDOWS_SETUP.md +260 -260
  161. package/mcp-server/docs/troubleshooting-audio.md +313 -313
  162. package/mcp-server/examples/claude_desktop_config.json +11 -11
  163. package/mcp-server/examples/claude_desktop_config_piper.json +9 -9
  164. package/mcp-server/examples/custom_instructions.md +169 -169
  165. package/mcp-server/install-deps.js +130 -130
  166. package/mcp-server/pyproject.toml +52 -52
  167. package/mcp-server/requirements.txt +2 -2
  168. package/mcp-server/server.py +1465 -1417
  169. package/mcp-server/test_server.py +395 -395
  170. package/mcp-server/test_windows_script_parity.py +336 -0
  171. package/package.json +110 -112
  172. package/setup-windows.ps1 +815 -815
  173. package/src/bmad-detector.js +71 -71
  174. package/src/cli/list-personalities.js +110 -110
  175. package/src/cli/list-voices.js +114 -114
  176. package/src/commands/bmad-voices.js +394 -394
  177. package/src/commands/install-mcp.js +476 -476
  178. package/src/console/app.js +824 -806
  179. package/src/console/audio-env.js +20 -1
  180. package/src/console/brand-colors.js +13 -13
  181. package/src/console/constants/personalities.js +44 -0
  182. package/src/console/footer-config.js +50 -46
  183. package/src/console/modals/modal-overlay.js +247 -247
  184. package/src/console/navigation.js +62 -61
  185. package/src/console/tabs/agents-tab.js +1684 -369
  186. package/src/console/tabs/help-tab.js +261 -261
  187. package/src/console/tabs/install-tab.js +1007 -991
  188. package/src/console/tabs/music-tab.js +22 -8
  189. package/src/console/tabs/placeholder-tab.js +53 -46
  190. package/src/console/tabs/readme-tab.js +267 -267
  191. package/src/console/tabs/receiver-tab.js +1472 -0
  192. package/src/console/tabs/settings-tab.js +185 -402
  193. package/src/console/tabs/voices-tab.js +100 -21
  194. package/src/console/widgets/destroy-list.js +25 -0
  195. package/src/console/widgets/format-utils.js +89 -0
  196. package/src/console/widgets/notice.js +55 -0
  197. package/src/console/widgets/personality-picker.js +185 -0
  198. package/src/console/widgets/reverb-picker.js +94 -0
  199. package/src/console/widgets/track-picker.js +285 -0
  200. package/src/installer/music-file-input.js +304 -304
  201. package/src/installer.js +5882 -5777
  202. package/src/services/agent-voice-store.js +423 -163
  203. package/src/services/config-service.js +264 -264
  204. package/src/services/navigation-service.js +123 -123
  205. package/src/services/provider-service.js +132 -132
  206. package/src/services/verbosity-service.js +157 -157
  207. package/src/utils/audio-duration-validator.js +298 -298
  208. package/src/utils/audio-format-validator.js +277 -277
  209. package/src/utils/dependency-checker.js +469 -466
  210. package/src/utils/file-ownership-verifier.js +358 -358
  211. package/src/utils/list-formatter.js +194 -194
  212. package/src/utils/music-file-validator.js +285 -275
  213. package/src/utils/preview-list-prompt.js +136 -136
  214. package/src/utils/provider-validator.js +96 -12
  215. package/src/utils/secure-music-storage.js +412 -412
  216. package/templates/agentvibes-receiver.sh +482 -162
  217. package/templates/audio/welcome-music.mp3 +0 -0
  218. package/voice-assignments.json +8244 -8244
  219. package/.claude/config/background-music-position.txt +0 -1
@@ -21,8 +21,33 @@ import {
21
21
  import { formatTrackLabel, scanTracks, getMusicFavorites, toggleMusicFavorite, applyTrackToAudioEffects } from './music-tab.js';
22
22
  import { BRAND_PINK, BRAND_BLUE } from '../brand-colors.js';
23
23
  import { buildAudioEnv, detectMp3Player, detectWavPlayer } from '../audio-env.js';
24
+ import { destroyList } from '../widgets/destroy-list.js';
25
+ import { openReverbPicker } from '../widgets/reverb-picker.js';
26
+ import { openPersonalityPicker } from '../widgets/personality-picker.js';
27
+ import { PERSONALITY_EMOJIS } from '../constants/personalities.js';
28
+ import { formatTrackName as _sharedFormatTrackName, formatReverbState as _sharedFormatReverbState } from '../widgets/format-utils.js';
29
+ import { showNotice as _showNoticeWidget } from '../widgets/notice.js';
24
30
 
25
31
  const IS_TEST = process.env.AGENTVIBES_TEST_MODE === 'true';
32
+ const _IS_WINDOWS = process.platform === 'win32' && !process.env.WSL_DISTRO_NAME;
33
+
34
+ /** Resolve piper binary path — uses exe on Windows, 'piper' in PATH on Unix */
35
+ function _resolvePiperBin() {
36
+ if (_IS_WINDOWS) {
37
+ const lad = process.env.LOCALAPPDATA ||
38
+ (process.env.USERPROFILE ? path.join(process.env.USERPROFILE, 'AppData', 'Local') : null);
39
+ if (lad) {
40
+ const exe = path.join(lad, 'Programs', 'Piper', 'piper.exe');
41
+ if (fs.existsSync(exe)) return exe;
42
+ }
43
+ }
44
+ return 'piper';
45
+ }
46
+
47
+ /** Build spawn options with Windows-safe defaults (no visible console, no detached) */
48
+ function _spawnOpts(env, extraOpts = {}) {
49
+ return { stdio: 'ignore', detached: !_IS_WINDOWS, windowsHide: true, env, ...extraOpts };
50
+ }
26
51
 
27
52
  // Sanitize strings before passing as env vars to shell commands.
28
53
  // Removes characters that could cause shell injection when expanded inside sh -c.
@@ -45,19 +70,19 @@ const _modalTitle = (text) => ` {${BRAND_PINK}-fg}${text}{/${BRAND_PINK}-fg} `;
45
70
 
46
71
  const COLORS = {
47
72
  contentBg: '#0a0e1a', // Near-black content background
48
- sectionHdr: '#7986cb', // Light blue section dividers
73
+ sectionHdr: 'bright-cyan', // Matches header "Agent" color
49
74
  labelFg: '#e3f2fd', // Light blue text — labels
50
75
  valueFg: '#ffff00', // Yellow — current values
51
76
  btnDefault: '#37474f', // Dark slate — default button bg
52
- btnFocus: '#00e5ff', // Cyan — focused button bg
53
- btnFocusFg: '#000000', // Black — focused button text
77
+ btnFocus: '#2e7d32', // Green — focused/selected button bg
78
+ btnFocusFg: '#ffffff', // White — focused button text
54
79
  btnPress: '#ff00ff', // Magenta — pressed button bg
55
80
  btnChange: '#37474f', // Dark slate — Change buttons
56
81
  btnTest: '#37474f', // Dark slate — Test buttons
57
82
  btnEdit: '#37474f', // Dark slate — Edit buttons
58
83
  btnEnableOn: '#37474f', // Dark slate — Enabled toggle
59
84
  btnEnableOff: '#37474f', // Dark slate — Disabled toggle
60
- borderFg: '#7986cb', // Light blue — borders
85
+ borderFg: 'bright-cyan', // Matches section headers
61
86
  footerBg: '#2196f3', // Blue — settings footer
62
87
  noticeFg: '#90a4ae', // Gray — stub notice text
63
88
  };
@@ -74,82 +99,11 @@ const MUSIC_DEFAULTS = Object.freeze({ enabled: false, track: 'agentvibes_soft_f
74
99
  // Verbosity display labels
75
100
  const VERBOSITY_LABELS = Object.freeze({ high: 'High', medium: 'Medium', low: 'Low', minimal: 'Minimal', custom: 'Custom' });
76
101
 
77
- // Personality emojis mirrors installer.js personalityEmojis (src/installer.js:84)
78
- const PERSONALITY_EMOJIS = Object.freeze({
79
- angry: '😠',
80
- annoying: '😤',
81
- crass: '🤬',
82
- dramatic: '🎭',
83
- 'dry-humor': '😐',
84
- flirty: '😘',
85
- funny: '😂',
86
- grandpa: '👴',
87
- millennial: '🙄',
88
- moody: '😒',
89
- none: '😊',
90
- normal: '😊',
91
- pirate: '⚓',
92
- poetic: '📜',
93
- professional: '👔',
94
- rapper: '🎤',
95
- robot: '🤖',
96
- sarcastic: '😏',
97
- sassy: '💁',
98
- 'surfer-dude':'🏄',
99
- zen: '🧘',
100
- });
101
-
102
- // Known personalities (matches .claude/personalities/ directory)
103
- const PERSONALITIES = Object.freeze([
104
- 'none', 'angry', 'annoying', 'crass', 'dramatic', 'dry-humor',
105
- 'flirty', 'funny', 'grandpa', 'millennial', 'moody', 'normal',
106
- 'pirate', 'poetic', 'professional', 'rapper', 'robot', 'sarcastic',
107
- 'sassy', 'surfer-dude', 'zen',
108
- ]);
109
-
110
- // Preview phrases — one short, exemplary, in-character line per personality.
111
- // Spoken automatically when the cursor lands on a personality in the picker.
112
- const PERSONALITY_PREVIEW_PHRASES = Object.freeze({
113
- angry: "UNACCEPTABLE! This build time is a DISASTER! Fix it NOW or so help me!",
114
- annoying: "Oh oh oh! Can I tell you something? Can I? Can I? PLEASE? It is so important!",
115
- crass: "Well damn, that code runs like my uncle's truck. Barely, and it smells funny.",
116
- dramatic: "The tests... have failed. I don't know how much longer I can do this.",
117
- 'dry-humor': "Your code worked. I too am surprised.",
118
- flirty: "Ooh, a clean merge? You know exactly how to make my heart race.",
119
- funny: "Why do programmers hate nature? Too many bugs. I will show myself out.",
120
- grandpa: "Back in my day, we compiled by hand. Uphill. In the snow. Both ways.",
121
- millennial: "I literally cannot even with this error. I am so done. Like, actually deceased.",
122
- moody: "...It works. Whatever. Do not get used to it.",
123
- pirate: "Arrr! The build be sailin' smooth today, matey! No barnacles in sight!",
124
- poetic: "Like rivers to the sea, your code flows toward eventual compilation.",
125
- professional: "I have completed the requested task and am prepared to document outcomes.",
126
- rapper: "Yo! Clean code flowin', tests are glowin', no bugs showin'!",
127
- robot: "TASK COMPLETE. EFFICIENCY: OPTIMAL. PROBABILITY OF SUCCESS: 97.3 PERCENT. BEEP.",
128
- sarcastic: "Oh wow, another bug. What a completely unexpected surprise. Truly shocking.",
129
- sassy: "Honey, whoever told you that was good code was not your friend.",
130
- 'surfer-dude':"Duuude! That commit totally shredded! Gnarly clean code, bro!",
131
- zen: "The bug is not the enemy. The bug is the teacher. Breathe. Commit.",
132
- random: "Who will I be today? Even I do not know. Expect the unexpected.",
133
- });
134
-
135
- // Human-readable track display names — matches installer track picker (src/installer.js:2280)
136
- const TRACK_NAMES = Object.freeze({
137
- 'agentvibes_soft_flamenco_loop.mp3': '🎻 Soft Flamenco',
138
- 'agent_vibes_bachata_v1_loop.mp3': '🎺 Bachata',
139
- 'agent_vibes_salsa_v2_loop.mp3': '💃 Salsa',
140
- 'agent_vibes_cumbia_v1_loop.mp3': '🎸 Cumbia',
141
- 'agent_vibes_bossa_nova_v2_loop.mp3': '🌸 Bossa Nova',
142
- 'agent_vibes_japanese_city_pop_v1_loop.mp3': '🌆 Japanese City Pop',
143
- 'agent_vibes_chillwave_v2_loop.mp3': '🌊 Chillwave',
144
- 'agent_vibes_dark_chill_step_loop.mp3': '🌙 Dark Chill Step',
145
- 'agent_vibes_goa_trance_v2_loop.mp3': '🌀 Goa Trance',
146
- 'agent_vibes_harpsichord_v2_loop.mp3': '🎼 Harpsichord',
147
- 'agent_vibes_celtic_harp_v1_loop.mp3': '🎻 Celtic Harp',
148
- 'agent_vibes_hawaiian_slack_key_guitar_v2_loop.mp3': '🌺 Hawaiian Slack Key Guitar',
149
- 'agent_vibes_arabic_v2_loop.mp3': '🎵 Arabic Oud',
150
- 'agent_vibes_ganawa_ambient_v2_loop.mp3': '🪘 Gnawa Ambient',
151
- 'agent_vibes_tabla_dream_pop_v1_loop.mp3': '🥁 Tabla Dream Pop',
152
- });
102
+ // Personality emojis and names imported from src/console/constants/personalities.js
103
+ // (via the import at the top of this file)
104
+
105
+ // Human-readable track display names — moved to shared widgets/format-utils.js
106
+ // TRACK_NAMES constant removed (M1 dedup). Use formatTrackName() instead.
153
107
 
154
108
  // Built-in track list for the picker (fallback when tracks dir is missing)
155
109
  const BUILT_IN_TRACKS = [
@@ -166,10 +120,7 @@ const BUILT_IN_TRACKS = [
166
120
  * @param {string} preset - 'off' | 'light' | 'medium' | 'heavy' | 'cathedral'
167
121
  * @returns {string}
168
122
  */
169
- export function formatReverbState(preset) {
170
- const LABELS = { off: 'Off', light: 'Light (Small room)', medium: 'Medium (Conference room)', heavy: 'Heavy (Large hall)', cathedral: 'Cathedral (Epic space)' };
171
- return LABELS[preset] ?? LABELS.light;
172
- }
123
+ export const formatReverbState = _sharedFormatReverbState;
173
124
 
174
125
  /**
175
126
  * @param {boolean} enabled
@@ -192,18 +143,7 @@ export function formatVolume(volume) {
192
143
  * @param {string} track - filename (e.g. 'agentvibes_soft_flamenco_loop.mp3')
193
144
  * @returns {string}
194
145
  */
195
- export function formatTrackName(track) {
196
- if (!track) return 'None';
197
- if (TRACK_NAMES[track]) return TRACK_NAMES[track];
198
- // Custom/unknown track: strip extension, agentvibes_/agent_vibes_ prefix,
199
- // _v1/_v2/_loop/_v1_loop/_v2_loop suffixes, then title-case each word
200
- return track
201
- .replace(/\.[^.]+$/, '')
202
- .replace(/^agentvibes_|^agent_vibes_/, '')
203
- .replace(/_v\d+_loop$|_loop$|_v\d+$/, '')
204
- .replace(/_/g, ' ')
205
- .replace(/\b\w/g, c => c.toUpperCase());
206
- }
146
+ export const formatTrackName = _sharedFormatTrackName;
207
147
 
208
148
  /**
209
149
  * @param {string} verbosity - 'high' | 'medium' | 'low'
@@ -492,9 +432,14 @@ export function createSettingsTab(screen, services) {
492
432
  `play "${trackPath}" repeat 9999 vol ${volFraction}`,
493
433
  `mpg123 -q --loop -1 "${trackPath}"`,
494
434
  ].join(' 2>/dev/null || ') + ' 2>/dev/null';
495
- _testMusicProc = spawn('sh', ['-c', musicCmd], {
496
- stdio: 'ignore', detached: true, env: _testEnv,
497
- });
435
+ if (_IS_WINDOWS) {
436
+ const _mp3P = detectMp3Player(_testEnv);
437
+ _testMusicProc = _mp3P
438
+ ? spawn(_mp3P.bin, _mp3P.args(trackPath), _spawnOpts(_testEnv))
439
+ : null;
440
+ } else {
441
+ _testMusicProc = spawn('sh', ['-c', musicCmd], _spawnOpts(_testEnv));
442
+ }
498
443
  _testMusicProc.unref();
499
444
  }
500
445
  }
@@ -542,7 +487,7 @@ export function createSettingsTab(screen, services) {
542
487
  `soprano "$_AV_PHRASE" -o "$_AV_WAV"`,
543
488
  ].join(' || ');
544
489
  synthProc = spawn('sh', ['-c', cmd], {
545
- stdio: 'ignore', detached: true, env: sopranoEnv,
490
+ stdio: 'ignore', detached: !_IS_WINDOWS, windowsHide: true, env: sopranoEnv,
546
491
  });
547
492
  } else {
548
493
  const voiceId = providerService.getActiveVoiceId();
@@ -555,8 +500,8 @@ export function createSettingsTab(screen, services) {
555
500
  }
556
501
  const _piperArgs = ['--model', voicePath, '--output_file', tempWav];
557
502
  if (_ms.speakerId != null) _piperArgs.push('--speaker', String(_ms.speakerId));
558
- synthProc = spawn('piper', _piperArgs, {
559
- stdio: ['pipe', 'ignore', 'ignore'], detached: true, env: _testEnv,
503
+ synthProc = spawn(_resolvePiperBin(), _piperArgs, {
504
+ stdio: ['pipe', 'ignore', 'ignore'], detached: !_IS_WINDOWS, windowsHide: true, env: _testEnv,
560
505
  });
561
506
  synthProc.stdin.write(ttsInput + '\n');
562
507
  synthProc.stdin.end();
@@ -607,7 +552,7 @@ export function createSettingsTab(screen, services) {
607
552
  _setTestBtnsLabel('■ Stop');
608
553
  const _wavPlayer1 = detectWavPlayer(_testEnv);
609
554
  const playProc = _wavPlayer1
610
- ? spawn(_wavPlayer1.bin, _wavPlayer1.args(wavToPlay), { stdio: 'ignore', detached: true, env: _testEnv })
555
+ ? spawn(_wavPlayer1.bin, _wavPlayer1.args(wavToPlay), _spawnOpts(_testEnv))
611
556
  : null;
612
557
  if (!playProc) { _killTest(); _restoreTestBtnsLabels(); return; }
613
558
  _testVoiceProc = playProc;
@@ -712,9 +657,14 @@ export function createSettingsTab(screen, services) {
712
657
  `mpg123 -q "${trackPath}"`,
713
658
  ].join(' 2>/dev/null || ') + ' 2>/dev/null';
714
659
 
715
- _musicTestProc = spawn('sh', ['-c', cmd], {
716
- stdio: 'ignore', detached: true, env: _testEnv,
717
- });
660
+ if (_IS_WINDOWS) {
661
+ const _mp3P2 = detectMp3Player(_testEnv);
662
+ _musicTestProc = _mp3P2
663
+ ? spawn(_mp3P2.bin, _mp3P2.args(trackPath), _spawnOpts(_testEnv))
664
+ : null;
665
+ } else {
666
+ _musicTestProc = spawn('sh', ['-c', cmd], _spawnOpts(_testEnv));
667
+ }
718
668
  _musicTestProc.unref();
719
669
  musicTestBtn.setContent('■ Stop');
720
670
  screen.render();
@@ -807,7 +757,7 @@ export function createSettingsTab(screen, services) {
807
757
  content: lbl, width: lbl.length, height: 1,
808
758
  top: 0, left: _xOff,
809
759
  keys: true, focusable: true,
810
- style: { fg: '#00e5ff', bg: '#263238' },
760
+ style: { fg: 'bright-cyan', bg: '#263238' },
811
761
  });
812
762
  _subTabItemsMap[id] = item;
813
763
  _xOff += lbl.length;
@@ -821,7 +771,7 @@ export function createSettingsTab(screen, services) {
821
771
  item.style.bg = '#0288d1'; // light blue — active tab
822
772
  item.style.bold = true;
823
773
  } else {
824
- item.style.fg = '#00e5ff';
774
+ item.style.fg = 'bright-cyan';
825
775
  item.style.bg = '#263238';
826
776
  item.style.bold = false;
827
777
  }
@@ -855,12 +805,24 @@ export function createSettingsTab(screen, services) {
855
805
  });
856
806
  }
857
807
 
808
+ // -------------------------------------------------------------------------
809
+ // Section header: ── Provider & Voice ──
810
+
811
+ const providerVoiceHeader = blessed.text({
812
+ parent: box,
813
+ top: 3,
814
+ left: 1,
815
+ content: '{bright-cyan-fg} 🎤 Provider & Voice {/bright-cyan-fg}',
816
+ tags: true,
817
+ style: { bg: COLORS.contentBg },
818
+ });
819
+
858
820
  // -------------------------------------------------------------------------
859
821
  // Provider row: label + value + [Switch] button
860
822
 
861
823
  const providerLabel = blessed.text({
862
824
  parent: box,
863
- top: 3,
825
+ top: 5,
864
826
  left: 6,
865
827
  content: 'Provider:',
866
828
  style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
@@ -868,7 +830,7 @@ export function createSettingsTab(screen, services) {
868
830
 
869
831
  const providerValue = blessed.text({
870
832
  parent: box,
871
- top: 3,
833
+ top: 5,
872
834
  left: 22,
873
835
  width: 26, // truncate before [Switch] at left:40
874
836
  wrap: false,
@@ -884,7 +846,7 @@ export function createSettingsTab(screen, services) {
884
846
  screen.render();
885
847
  }, _restoreFocus);
886
848
  });
887
- switchBtn.top = 3;
849
+ switchBtn.top = 5;
888
850
  switchBtn.left = 52;
889
851
 
890
852
  // -------------------------------------------------------------------------
@@ -892,7 +854,7 @@ export function createSettingsTab(screen, services) {
892
854
 
893
855
  const voiceLabel = blessed.text({
894
856
  parent: box,
895
- top: 5,
857
+ top: 7,
896
858
  left: 6,
897
859
  content: 'Current Voice:',
898
860
  style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
@@ -900,7 +862,7 @@ export function createSettingsTab(screen, services) {
900
862
 
901
863
  const voiceValue = blessed.text({
902
864
  parent: box,
903
- top: 5,
865
+ top: 7,
904
866
  left: 22,
905
867
  width: 26, // truncate before [Change] at left:40
906
868
  wrap: false,
@@ -916,7 +878,7 @@ export function createSettingsTab(screen, services) {
916
878
  screen.render();
917
879
  }, _restoreFocus);
918
880
  }, { bg: COLORS.btnChange });
919
- changeBtn.top = 5;
881
+ changeBtn.top = 7;
920
882
  changeBtn.left = 52;
921
883
 
922
884
  const playBtn = _createButton(box, screen, '▶ Play', COLORS, () => {
@@ -951,7 +913,7 @@ export function createSettingsTab(screen, services) {
951
913
  screen.render();
952
914
  const _wavPlayer2 = detectWavPlayer(_sampleEnv);
953
915
  if (!_wavPlayer2) { _stopSpinner(); _killSample(); playBtn.setContent('▶ Play'); screen.render(); return; }
954
- const playProc = spawn(_wavPlayer2.bin, _wavPlayer2.args(tempWav), { stdio: 'ignore', detached: true, env: _sampleEnv });
916
+ const playProc = spawn(_wavPlayer2.bin, _wavPlayer2.args(tempWav), _spawnOpts(_sampleEnv));
955
917
  _sampleProcess = playProc;
956
918
  const _done = () => { _killSample(); playBtn.setContent('▶ Play'); _focusButton(playBtn); try { fs.unlinkSync(tempWav); } catch {} };
957
919
  playProc.on('exit', _done);
@@ -1052,7 +1014,7 @@ export function createSettingsTab(screen, services) {
1052
1014
  ].join(' || ');
1053
1015
 
1054
1016
  const soprano = spawn('sh', ['-c', cmd], {
1055
- stdio: 'ignore', detached: true, env: sopranoEnv,
1017
+ stdio: 'ignore', detached: !_IS_WINDOWS, windowsHide: true, env: sopranoEnv,
1056
1018
  });
1057
1019
  _sampleProcess = soprano;
1058
1020
  soprano.on('exit', (code) => {
@@ -1074,8 +1036,8 @@ export function createSettingsTab(screen, services) {
1074
1036
  }
1075
1037
  const _piperArgs2 = ['--model', voicePath, '--output_file', tempWav];
1076
1038
  if (_ms2.speakerId != null) _piperArgs2.push('--speaker', String(_ms2.speakerId));
1077
- const piper = spawn('piper', _piperArgs2, {
1078
- stdio: ['pipe', 'ignore', 'ignore'], detached: true, env: _sampleEnv,
1039
+ const piper = spawn(_resolvePiperBin(), _piperArgs2, {
1040
+ stdio: ['pipe', 'ignore', 'ignore'], detached: !_IS_WINDOWS, windowsHide: true, env: _sampleEnv,
1079
1041
  });
1080
1042
  piper.stdin.write(phrase + '\n');
1081
1043
  piper.stdin.end();
@@ -1084,12 +1046,12 @@ export function createSettingsTab(screen, services) {
1084
1046
  piper.on('error', () => { _stopSpinner(); _killSample(); playBtn.setContent('▶ Play'); _focusButton(playBtn); });
1085
1047
  }
1086
1048
  });
1087
- playBtn.top = 5;
1049
+ playBtn.top = 7;
1088
1050
  playBtn.left = 64;
1089
1051
 
1090
1052
  const voiceFileText = blessed.text({
1091
1053
  parent: box,
1092
- top: 6,
1054
+ top: 8,
1093
1055
  left: 22,
1094
1056
  right: 2,
1095
1057
  wrap: false,
@@ -1104,7 +1066,7 @@ export function createSettingsTab(screen, services) {
1104
1066
  parent: box,
1105
1067
  top: 3,
1106
1068
  left: 1,
1107
- content: '{#7986cb-fg} ⚡ Audio Effects {/#7986cb-fg}',
1069
+ content: '{bright-cyan-fg} ⚡ Audio Effects {/bright-cyan-fg}',
1108
1070
  tags: true,
1109
1071
  style: { bg: COLORS.contentBg },
1110
1072
  });
@@ -1131,7 +1093,7 @@ export function createSettingsTab(screen, services) {
1131
1093
  });
1132
1094
 
1133
1095
  const reverbChangeBtn = _createButton(box, screen, 'Change', COLORS, () => {
1134
- _openReverbPicker(screen, configService, (preset) => {
1096
+ openReverbPicker(screen, configService.getConfig().effects?.reverbPreset ?? 'light', (preset) => {
1135
1097
  _setEffects(configService, { reverbPreset: preset });
1136
1098
  refreshDisplay();
1137
1099
  }, _restoreFocus);
@@ -1160,7 +1122,7 @@ export function createSettingsTab(screen, services) {
1160
1122
  parent: box,
1161
1123
  top: 7,
1162
1124
  left: 1,
1163
- content: '{#7986cb-fg} 🎸 Background Music {/#7986cb-fg}',
1125
+ content: '{bright-cyan-fg} 🎸 Background Music {/bright-cyan-fg}',
1164
1126
  tags: true,
1165
1127
  style: { bg: COLORS.contentBg },
1166
1128
  });
@@ -1256,12 +1218,24 @@ export function createSettingsTab(screen, services) {
1256
1218
  volumeChangeBtn.top = 11;
1257
1219
  volumeChangeBtn.left = 52;
1258
1220
 
1221
+ // -------------------------------------------------------------------------
1222
+ // Section header: ── Style ──
1223
+
1224
+ const styleHeader = blessed.text({
1225
+ parent: box,
1226
+ top: 3,
1227
+ left: 1,
1228
+ content: '{bright-cyan-fg} 🎭 Style {/bright-cyan-fg}',
1229
+ tags: true,
1230
+ style: { bg: COLORS.contentBg },
1231
+ });
1232
+
1259
1233
  // -------------------------------------------------------------------------
1260
1234
  // Verbosity row: label + value + [Change] button
1261
1235
 
1262
1236
  const verbosityLabel = blessed.text({
1263
1237
  parent: box,
1264
- top: 3,
1238
+ top: 5,
1265
1239
  left: 6,
1266
1240
  content: 'Verbosity:',
1267
1241
  style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
@@ -1269,7 +1243,7 @@ export function createSettingsTab(screen, services) {
1269
1243
 
1270
1244
  const verbosityValue = blessed.text({
1271
1245
  parent: box,
1272
- top: 3,
1246
+ top: 5,
1273
1247
  left: 22,
1274
1248
  width: 26, // truncate before [Change] at left:40
1275
1249
  wrap: false,
@@ -1280,12 +1254,12 @@ export function createSettingsTab(screen, services) {
1280
1254
  const verbosityChangeBtn = _createButton(box, screen, 'Change', COLORS, () => {
1281
1255
  _openVerbosityPicker(screen, configService, () => refreshDisplay(), _restoreFocus);
1282
1256
  }, { bg: COLORS.btnChange });
1283
- verbosityChangeBtn.top = 3;
1257
+ verbosityChangeBtn.top = 5;
1284
1258
  verbosityChangeBtn.left = 52;
1285
1259
 
1286
1260
  const verbosityPathText = blessed.text({
1287
1261
  parent: box,
1288
- top: 4,
1262
+ top: 6,
1289
1263
  left: 22,
1290
1264
  right: 2,
1291
1265
  wrap: false,
@@ -1298,7 +1272,7 @@ export function createSettingsTab(screen, services) {
1298
1272
 
1299
1273
  const personalityLabel = blessed.text({
1300
1274
  parent: box,
1301
- top: 5,
1275
+ top: 7,
1302
1276
  left: 6,
1303
1277
  content: 'Personality:',
1304
1278
  style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
@@ -1306,7 +1280,7 @@ export function createSettingsTab(screen, services) {
1306
1280
 
1307
1281
  const personalityValue = blessed.text({
1308
1282
  parent: box,
1309
- top: 5,
1283
+ top: 7,
1310
1284
  left: 22,
1311
1285
  width: 26, // truncate before [Change] at left:40
1312
1286
  wrap: false,
@@ -1315,12 +1289,12 @@ export function createSettingsTab(screen, services) {
1315
1289
  });
1316
1290
 
1317
1291
  const personalityChangeBtn = _createButton(box, screen, 'Change', COLORS, () => {
1318
- _openPersonalityPicker(screen, configService, (name) => {
1292
+ openPersonalityPicker(screen, configService.getConfig().personality ?? 'none', (name) => {
1319
1293
  configService.set('personality', name);
1320
1294
  refreshDisplay();
1321
1295
  }, _restoreFocus);
1322
1296
  }, { bg: COLORS.btnChange });
1323
- personalityChangeBtn.top = 5;
1297
+ personalityChangeBtn.top = 7;
1324
1298
  personalityChangeBtn.left = 52;
1325
1299
 
1326
1300
  const personalityTestBtn = _createButton(box, screen, '▶ Preview', COLORS, () => {
@@ -1331,12 +1305,12 @@ export function createSettingsTab(screen, services) {
1331
1305
  : _buildPreviewPhrase();
1332
1306
  _runTest(false, phrase);
1333
1307
  }, { bg: COLORS.btnTest });
1334
- personalityTestBtn.top = 5;
1308
+ personalityTestBtn.top = 7;
1335
1309
  personalityTestBtn.left = 64;
1336
1310
 
1337
1311
  const personalityFileText = blessed.text({
1338
1312
  parent: box,
1339
- top: 6,
1313
+ top: 8,
1340
1314
  left: 22,
1341
1315
  right: 2,
1342
1316
  wrap: false,
@@ -1350,9 +1324,9 @@ export function createSettingsTab(screen, services) {
1350
1324
 
1351
1325
  const introTextHeader = blessed.text({
1352
1326
  parent: box,
1353
- top: 8,
1327
+ top: 10,
1354
1328
  left: 1,
1355
- content: '{#7986cb-fg} ✍️ Intro Text {/#7986cb-fg}',
1329
+ content: '{bright-cyan-fg} ✍️ Intro Text {/bright-cyan-fg}',
1356
1330
  tags: true,
1357
1331
  style: { bg: COLORS.contentBg },
1358
1332
  });
@@ -1362,7 +1336,7 @@ export function createSettingsTab(screen, services) {
1362
1336
 
1363
1337
  const introTextLabel = blessed.text({
1364
1338
  parent: box,
1365
- top: 10,
1339
+ top: 12,
1366
1340
  left: 6,
1367
1341
  content: 'Intro Text:',
1368
1342
  style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
@@ -1370,7 +1344,7 @@ export function createSettingsTab(screen, services) {
1370
1344
 
1371
1345
  const introTextValue = blessed.text({
1372
1346
  parent: box,
1373
- top: 10,
1347
+ top: 12,
1374
1348
  left: 22,
1375
1349
  width: 26, // truncate before [Edit] at left:40
1376
1350
  wrap: false,
@@ -1381,19 +1355,19 @@ export function createSettingsTab(screen, services) {
1381
1355
  const introEditBtn = _createButton(box, screen, 'Edit', COLORS, () => {
1382
1356
  _openIntroTextEditor(screen, configService, () => { refreshDisplay(); }, _restoreFocus);
1383
1357
  }, { bg: COLORS.btnEdit });
1384
- introEditBtn.top = 10;
1358
+ introEditBtn.top = 12;
1385
1359
  introEditBtn.left = 52;
1386
1360
 
1387
1361
  const introClearBtn = _createButton(box, screen, 'Clear', COLORS, () => {
1388
1362
  configService.set('pretext', '');
1389
1363
  refreshDisplay();
1390
1364
  }, { bg: '#c62828' });
1391
- introClearBtn.top = 10;
1365
+ introClearBtn.top = 12;
1392
1366
  introClearBtn.left = 64;
1393
1367
 
1394
1368
  const introPathText = blessed.text({
1395
1369
  parent: box,
1396
- top: 11,
1370
+ top: 13,
1397
1371
  left: 22,
1398
1372
  right: 2,
1399
1373
  wrap: false,
@@ -1413,7 +1387,7 @@ export function createSettingsTab(screen, services) {
1413
1387
  parent: box,
1414
1388
  top: 3,
1415
1389
  left: 2,
1416
- content: '{#7986cb-fg} 📡 Audio Destination {/#7986cb-fg}',
1390
+ content: '{bright-cyan-fg} 📡 Audio Destination {/bright-cyan-fg}',
1417
1391
  tags: true,
1418
1392
  style: { bg: COLORS.contentBg },
1419
1393
  });
@@ -1542,7 +1516,7 @@ export function createSettingsTab(screen, services) {
1542
1516
  const current = configService.getConfig().audio_stream_mode ?? 'text';
1543
1517
  configService.set('audio_stream_mode', current === 'text' ? 'pulse' : 'text');
1544
1518
  refreshDisplay();
1545
- }, { bg: '#2e7d32' }); // green = recommended
1519
+ }, { bg: '#1565c0' }); // blue — distinct from green focus
1546
1520
  audioStreamModeBtn.top = 7;
1547
1521
  audioStreamModeBtn.left = 64;
1548
1522
  audioStreamModeBtn.hide();
@@ -1566,7 +1540,7 @@ export function createSettingsTab(screen, services) {
1566
1540
  parent: box,
1567
1541
  top: 11,
1568
1542
  left: 2,
1569
- content: '{#7986cb-fg} 💾 Config Storage {/#7986cb-fg}',
1543
+ content: '{bright-cyan-fg} 💾 Config Storage {/bright-cyan-fg}',
1570
1544
  tags: true,
1571
1545
  style: { bg: COLORS.contentBg },
1572
1546
  });
@@ -1632,7 +1606,7 @@ export function createSettingsTab(screen, services) {
1632
1606
  refreshConfigDisplay();
1633
1607
  _showNotice(screen, 'Settings Saved');
1634
1608
  }, () => { _currentIdx = _buttons.indexOf(saveLocallyBtn); _focusButton(saveLocallyBtn); });
1635
- }, { bg: '#2e7d32' }); // green
1609
+ }, { bg: '#1565c0' }); // blue — distinct from green focus
1636
1610
  saveLocallyBtn.bottom = 0;
1637
1611
  saveLocallyBtn.left = 46;
1638
1612
 
@@ -1660,6 +1634,7 @@ export function createSettingsTab(screen, services) {
1660
1634
  // Widget groups for each sub-tab (used by _showSubTab to show/hide)
1661
1635
  const _subTabWidgets = {
1662
1636
  voice: [
1637
+ providerVoiceHeader,
1663
1638
  providerLabel, providerValue, switchBtn,
1664
1639
  voiceLabel, voiceValue, changeBtn, playBtn, voiceFileText,
1665
1640
  ],
@@ -1671,6 +1646,7 @@ export function createSettingsTab(screen, services) {
1671
1646
  volumeLabel, volumeValue, volumeChangeBtn,
1672
1647
  ],
1673
1648
  personality: [
1649
+ styleHeader,
1674
1650
  verbosityLabel, verbosityValue, verbosityChangeBtn, verbosityPathText,
1675
1651
  personalityLabel, personalityValue, personalityChangeBtn, personalityTestBtn, personalityFileText,
1676
1652
  introTextHeader,
@@ -2131,7 +2107,7 @@ export function createSettingsTab(screen, services) {
2131
2107
  audioSshValue.setContent(audioAlias || '(none)');
2132
2108
  const streamMode = cfg.audio_stream_mode ?? 'text';
2133
2109
  audioStreamModeBtn.setContent(streamMode === 'pulse' ? 'Streaming Pulse Audio' : 'Streaming Text Only ✓');
2134
- audioStreamModeBtn.style.bg = streamMode === 'text' ? '#2e7d32' : COLORS.btnChange;
2110
+ audioStreamModeBtn.style.bg = streamMode === 'text' ? '#1565c0' : COLORS.btnChange;
2135
2111
  } else {
2136
2112
  audioSshLabel.hide();
2137
2113
  audioSshValue.hide();
@@ -2302,14 +2278,14 @@ function _createButton(parent, screen, label, COLORS, onClick, opts = {}) {
2302
2278
  const _ALL_PROVIDERS = [
2303
2279
  { id: 'piper', name: 'Piper TTS', platforms: ['linux', 'darwin', 'win32'], desc: 'High-quality local neural TTS' },
2304
2280
  { id: 'soprano', name: 'Soprano', platforms: ['linux', 'darwin'], desc: 'Ultra-fast neural TTS (single voice)' },
2305
- { id: 'windows-sapi', name: 'Windows SAPI', platforms: ['win32'], desc: 'Windows built-in text-to-speech' },
2281
+ { id: 'sapi', name: 'Windows SAPI', platforms: ['win32'], desc: 'Windows built-in text-to-speech' },
2306
2282
  { id: 'macos', name: 'Mac Say', platforms: ['darwin'], desc: 'macOS built-in text-to-speech' },
2307
2283
  ];
2308
2284
 
2309
2285
  const _INSTALL_CMDS = {
2310
2286
  piper: ['pip install piper-tts', 'OR: pipx install piper-tts', '', 'Voices are downloaded separately:', 'Run: agentvibes install (then choose Piper)'],
2311
2287
  soprano: ['pip install soprano-tts', 'OR: pipx install soprano-tts', '', 'Keep model loaded for fast synthesis:', 'soprano-webui'],
2312
- 'windows-sapi': ['Built-in on Windows — no install required.', 'Only works in a native Windows shell,', 'not inside WSL. Use piper or soprano in WSL.'],
2288
+ sapi: ['Built-in on Windows — no install required.', 'Only works in a native Windows shell,', 'not inside WSL. Use piper or soprano in WSL.'],
2313
2289
  macos: ['Built-in on macOS — no install required.', 'The say command ships with every Mac.'],
2314
2290
  };
2315
2291
 
@@ -2354,7 +2330,7 @@ function _openProviderPicker(screen, providerService, onSelect, onClose) {
2354
2330
  // Environment header
2355
2331
  blessed.text({
2356
2332
  parent: modal, top: 0, left: 1, tags: true,
2357
- content: `{#00e5ff-fg}🖥 Environment:{/#00e5ff-fg} {bold}${envLabel}{/bold}`,
2333
+ content: `{bright-cyan-fg}🖥 Environment:{/bright-cyan-fg} {bold}${envLabel}{/bold}`,
2358
2334
  style: { bg: COLORS.contentBg },
2359
2335
  });
2360
2336
  blessed.text({
@@ -2404,8 +2380,8 @@ function _openProviderPicker(screen, providerService, onSelect, onClose) {
2404
2380
  _close(); onSelect(prov.id);
2405
2381
  } else {
2406
2382
  const lines = _INSTALL_CMDS[prov.id] ?? ['No instructions available.'];
2407
- instrTitle.setContent(`{#7986cb-fg}Install — ${prov.name}:{/#7986cb-fg}`);
2408
- instrContent.setContent(lines.map(l => l ? `{#00e5ff-fg}${l}{/#00e5ff-fg}` : '').join('\n'));
2383
+ instrTitle.setContent(`{bright-cyan-fg}Install — ${prov.name}:{/bright-cyan-fg}`);
2384
+ instrContent.setContent(lines.map(l => l ? `{bright-cyan-fg}${l}{/bright-cyan-fg}` : '').join('\n'));
2409
2385
  screen.render();
2410
2386
  }
2411
2387
  });
@@ -2419,7 +2395,7 @@ function _openProviderPicker(screen, providerService, onSelect, onClose) {
2419
2395
 
2420
2396
  const instrTitle = blessed.text({
2421
2397
  parent: modal, top: 11, left: 1, width: 66, tags: true,
2422
- content: '{#7986cb-fg}Install instructions — click Install beside a provider:{/#7986cb-fg}',
2398
+ content: '{bright-cyan-fg}Install instructions — click Install beside a provider:{/bright-cyan-fg}',
2423
2399
  style: { bg: COLORS.contentBg },
2424
2400
  });
2425
2401
  const instrContent = blessed.text({
@@ -2453,19 +2429,14 @@ function _openProviderPicker(screen, providerService, onSelect, onClose) {
2453
2429
  }
2454
2430
 
2455
2431
  // ---------------------------------------------------------------------------
2456
- // Private: Destroy a list/modal widget and force-invalidate olines so blessed
2457
- // physically redraws every cell the widget covered (avoids ghost rendering).
2432
+ // Private: Destroy helper now imported from shared widgets/destroy-list.js
2433
+ // (kept as comment for git blame traceability)
2458
2434
 
2459
- function _destroyList(list, screen, onClose) {
2460
- list.destroy();
2461
- try {
2462
- for (let r = 0; r < screen.height; r++)
2463
- for (let c = 0; c < screen.width; c++)
2464
- if (screen.olines[r]?.[c]) screen.olines[r][c][0] = -1;
2465
- } catch {}
2466
- onClose?.();
2467
- screen.render();
2468
- }
2435
+ // NOTE: The following line was the old _destroyList definition, now using shared import:
2436
+ // import { destroyList } from '../widgets/destroy-list.js';
2437
+ //
2438
+ // Old code removed to eliminate duplication (M1 fix).
2439
+ // The shared destroyList has identical behavior.
2469
2440
 
2470
2441
  // ---------------------------------------------------------------------------
2471
2442
  // Private: Show a temporary stub notice text
@@ -2536,11 +2507,11 @@ function _showSavePreview(screen, filePath, data, onConfirm, onClose) {
2536
2507
  style: {
2537
2508
  fg: '#e3f2fd',
2538
2509
  bg: COLORS.contentBg,
2539
- border: { fg: '#00e5ff' },
2510
+ border: { fg: 'bright-cyan' },
2540
2511
  },
2541
2512
  });
2542
2513
 
2543
- function _close() { _destroyList(modal, screen, onClose); }
2514
+ function _close() { destroyList(modal, screen, onClose); }
2544
2515
 
2545
2516
  modal.key(['escape'], _close);
2546
2517
 
@@ -2555,7 +2526,7 @@ function _showSavePreview(screen, filePath, data, onConfirm, onClose) {
2555
2526
  const okBtn = _createButton(modal, screen, 'OK — Save', COLORS, () => {
2556
2527
  _close();
2557
2528
  onConfirm();
2558
- }, { bg: '#2e7d32' });
2529
+ }, { bg: '#1565c0' });
2559
2530
  okBtn.top = btnRow;
2560
2531
  okBtn.left = midX + 2;
2561
2532
 
@@ -2568,27 +2539,7 @@ function _showSavePreview(screen, filePath, data, onConfirm, onClose) {
2568
2539
  }
2569
2540
 
2570
2541
  function _showNotice(screen, message) {
2571
- const width = Math.max(28, message.length + 6);
2572
- const modal = blessed.box({
2573
- parent: screen,
2574
- top: 'center',
2575
- left: 'center',
2576
- width,
2577
- height: 3,
2578
- border: { type: 'line' },
2579
- tags: true,
2580
- content: `{center}${message}{/center}`,
2581
- style: {
2582
- fg: '#e3f2fd',
2583
- bg: COLORS.contentBg,
2584
- border: { fg: '#00e5ff' },
2585
- },
2586
- });
2587
- screen.render();
2588
-
2589
- setTimeout(() => {
2590
- _destroyList(modal, screen);
2591
- }, 2500);
2542
+ _showNoticeWidget(screen, message, { bg: COLORS.contentBg });
2592
2543
  }
2593
2544
 
2594
2545
  // ---------------------------------------------------------------------------
@@ -2605,64 +2556,8 @@ function _setEffects(configService, partial) {
2605
2556
  }
2606
2557
 
2607
2558
  // ---------------------------------------------------------------------------
2608
- // Private: Inline reverb preset picker
2609
-
2610
- function _openReverbPicker(screen, configService, onSelect, onClose) {
2611
- const PRESETS = [
2612
- { label: 'Off (Dry, no reverb)', value: 'off' },
2613
- { label: 'Light (Small room)', value: 'light' },
2614
- { label: 'Medium (Conference room)', value: 'medium' },
2615
- { label: 'Heavy (Large hall)', value: 'heavy' },
2616
- { label: 'Cathedral (Epic space)', value: 'cathedral' },
2617
- ];
2618
-
2619
- const currentPreset = configService.getConfig().effects?.reverbPreset ?? 'light';
2620
- const currentIdx = Math.max(0, PRESETS.findIndex(p => p.value === currentPreset));
2621
-
2622
- const list = blessed.list({
2623
- parent: screen,
2624
- top: 'center',
2625
- left: 'center',
2626
- width: 40,
2627
- height: PRESETS.length + 4,
2628
- border: { type: 'line' },
2629
- tags: true,
2630
- label: _modalTitle('Select Reverb Preset'),
2631
- items: PRESETS.map((p, i) => (i === currentIdx ? `● ${p.label}` : ` ${p.label}`)),
2632
- keys: true,
2633
- vi: false,
2634
- mouse: true,
2635
- style: {
2636
- border: { fg: COLORS.btnFocus },
2637
- selected: { bg: COLORS.btnFocus, fg: COLORS.btnFocusFg, bold: true },
2638
- item: { fg: '#e3f2fd' },
2639
- },
2640
- });
2641
-
2642
- list.select(currentIdx);
2643
- list.focus();
2644
- screen.render();
2645
-
2646
- list.key(['enter', 'space'], () => {
2647
- const selected = PRESETS[list.selected];
2648
- if (!selected) return;
2649
- _destroyList(list, screen, onClose);
2650
-
2651
- // Apply to audio config via effects-manager.sh
2652
- const effectsScript = path.join(process.cwd(), '.claude', 'hooks', 'effects-manager.sh');
2653
- spawnSync('bash', [effectsScript, 'set-reverb', selected.value, 'default'], {
2654
- stdio: 'ignore',
2655
- timeout: 5000,
2656
- env: { ...process.env },
2657
- });
2658
-
2659
- onSelect(selected.value);
2660
- });
2661
-
2662
- list.key(['escape', 'q'], () => {
2663
- _destroyList(list, screen, onClose);
2664
- });
2665
- }
2559
+ // Private: _openReverbPicker removed now using shared import:
2560
+ // import { openReverbPicker } from '../widgets/reverb-picker.js';
2666
2561
 
2667
2562
  // ---------------------------------------------------------------------------
2668
2563
  // Private: Background music config read/write helpers
@@ -2700,7 +2595,7 @@ function _openTrackPicker(screen, configService, onSelect, onClose) {
2700
2595
  const currentTrack = (configService.getConfig().backgroundMusic?.track ?? MUSIC_DEFAULTS.track);
2701
2596
  const items = allItems.map(t =>
2702
2597
  t.file === ADD_SENTINEL
2703
- ? ` {#00e5ff-fg}+ Add Custom Track{/#00e5ff-fg}`
2598
+ ? ` {bright-cyan-fg}+ Add Custom Track{/bright-cyan-fg}`
2704
2599
  : (t.file === currentTrack ? `● ${t.label}` : ` ${t.label}`)
2705
2600
  );
2706
2601
  const currentIdx = tracks.findIndex(t => t.file === currentTrack);
@@ -2737,18 +2632,18 @@ function _openTrackPicker(screen, configService, onSelect, onClose) {
2737
2632
  if (!selected) return;
2738
2633
  if (selected.file === ADD_SENTINEL) {
2739
2634
  // Destroy list first, then open path-input dialog
2740
- _destroyList(list, screen);
2635
+ destroyList(list, screen);
2741
2636
  _openCustomTrackInput(screen, tracksDir, (newFile) => {
2742
2637
  onSelect(newFile);
2743
2638
  }, onClose);
2744
2639
  return;
2745
2640
  }
2746
- _destroyList(list, screen, onClose);
2641
+ destroyList(list, screen, onClose);
2747
2642
  onSelect(selected.file);
2748
2643
  });
2749
2644
 
2750
2645
  list.key(['escape', 'q'], () => {
2751
- _destroyList(list, screen, onClose);
2646
+ destroyList(list, screen, onClose);
2752
2647
  });
2753
2648
  }
2754
2649
 
@@ -2939,9 +2834,14 @@ function _openVolumePicker(screen, configService, onSelect, onClose) {
2939
2834
  `mpg123 -q "${trackPath}"`,
2940
2835
  ].join(' 2>/dev/null || ') + ' 2>/dev/null';
2941
2836
 
2942
- _previewProcess = spawn('sh', ['-c', cmd], {
2943
- stdio: 'ignore', detached: true, env: _previewEnv,
2944
- });
2837
+ if (_IS_WINDOWS) {
2838
+ const _mp3P3 = detectMp3Player(_previewEnv);
2839
+ _previewProcess = _mp3P3
2840
+ ? spawn(_mp3P3.bin, _mp3P3.args(trackPath), _spawnOpts(_previewEnv))
2841
+ : null;
2842
+ } else {
2843
+ _previewProcess = spawn('sh', ['-c', cmd], _spawnOpts(_previewEnv));
2844
+ }
2945
2845
  _previewProcess.unref();
2946
2846
  _refreshList();
2947
2847
 
@@ -3077,7 +2977,7 @@ function _openMusicBrowserModal(screen, configService, navigationService, onDone
3077
2977
  fg: COLORS.labelFg,
3078
2978
  bg: COLORS.contentBg,
3079
2979
  border: { fg: COLORS.borderFg },
3080
- selected: { bg: '#1a237e', fg: '#00e5ff', bold: true },
2980
+ selected: { bg: '#2e7d32', fg: '#ffffff', bold: true },
3081
2981
  item: { fg: COLORS.labelFg },
3082
2982
  },
3083
2983
  });
@@ -3090,7 +2990,7 @@ function _openMusicBrowserModal(screen, configService, navigationService, onDone
3090
2990
  right: 2,
3091
2991
  tags: true,
3092
2992
  content: '',
3093
- style: { fg: '#00e5ff', bg: COLORS.contentBg },
2993
+ style: { fg: 'bright-cyan', bg: COLORS.contentBg },
3094
2994
  });
3095
2995
 
3096
2996
  // ---- File location hint ----
@@ -3184,15 +3084,16 @@ function _openMusicBrowserModal(screen, configService, navigationService, onDone
3184
3084
 
3185
3085
  const _mp3Player = detectMp3Player(_modalEnv);
3186
3086
  if (!_mp3Player) return;
3087
+ const _isWin = process.platform === 'win32' && !process.env.WSL_DISTRO_NAME;
3187
3088
  _previewProcess = spawn(_mp3Player.bin, _mp3Player.args(trackPath), {
3188
- stdio: 'ignore', detached: true, env: _modalEnv,
3089
+ stdio: 'ignore', detached: !_isWin, windowsHide: true, env: _modalEnv,
3189
3090
  });
3190
3091
  _previewProcess.unref();
3191
3092
  _previewTrackId = trackId;
3192
3093
 
3193
3094
  const label = _allTracks.find(t => t.id === trackId)?.label ?? formatTrackLabel(trackId);
3194
3095
  if (!_closed) {
3195
- modalPreviewLine.setContent(`{#00e5ff-fg}\u266A Previewing: ${label} (Space to stop){/#00e5ff-fg}`);
3096
+ modalPreviewLine.setContent(`{bright-cyan-fg}\u266A Previewing: ${label} (Space to stop){/bright-cyan-fg}`);
3196
3097
  screen.render();
3197
3098
  }
3198
3099
 
@@ -3301,13 +3202,13 @@ function _openVerbosityPicker(screen, configService, onDone, onClose) {
3301
3202
  list.key(['enter', 'space'], () => {
3302
3203
  const selected = levels[list.selected];
3303
3204
  if (!selected) return;
3304
- _destroyList(list, screen, onClose);
3205
+ destroyList(list, screen, onClose);
3305
3206
  configService.set('verbosity', selected.toLowerCase());
3306
3207
  onDone();
3307
3208
  });
3308
3209
 
3309
3210
  list.key(['escape', 'q'], () => {
3310
- _destroyList(list, screen, onClose);
3211
+ destroyList(list, screen, onClose);
3311
3212
  });
3312
3213
  }
3313
3214
 
@@ -3487,7 +3388,7 @@ function _openVoiceBrowserModal(screen, providerService, configService, navigati
3487
3388
  keys: true,
3488
3389
  style: {
3489
3390
  fg: COLORS.valueFg,
3490
- bg: '#1a237e',
3391
+ bg: '#1a3a5c',
3491
3392
  focus: { bg: '#283593' },
3492
3393
  },
3493
3394
  });
@@ -3497,7 +3398,7 @@ function _openVoiceBrowserModal(screen, providerService, configService, navigati
3497
3398
  parent: modal,
3498
3399
  top: 2,
3499
3400
  left: 6,
3500
- content: `{#7986cb-fg}${'Name'.padEnd(COL_NAME_W)}${'Gender'.padEnd(COL_GENDER_W)}Provider{/#7986cb-fg}`,
3401
+ content: `{bright-cyan-fg}${'Name'.padEnd(COL_NAME_W)}${'Gender'.padEnd(COL_GENDER_W)}Provider{/bright-cyan-fg}`,
3501
3402
  tags: true,
3502
3403
  style: { bg: COLORS.contentBg },
3503
3404
  });
@@ -3518,7 +3419,7 @@ function _openVoiceBrowserModal(screen, providerService, configService, navigati
3518
3419
  fg: COLORS.labelFg,
3519
3420
  bg: COLORS.contentBg,
3520
3421
  border: { fg: COLORS.borderFg },
3521
- selected: { bg: '#1a237e', fg: '#00e5ff', bold: true },
3422
+ selected: { bg: '#2e7d32', fg: '#ffffff', bold: true },
3522
3423
  item: { fg: COLORS.labelFg },
3523
3424
  },
3524
3425
  });
@@ -3528,7 +3429,7 @@ function _openVoiceBrowserModal(screen, providerService, configService, navigati
3528
3429
  parent: modal,
3529
3430
  bottom: 5,
3530
3431
  left: 2,
3531
- content: `{#7986cb-fg}── Voice Info ${'─'.repeat(50)}{/#7986cb-fg}`,
3432
+ content: `{bright-cyan-fg}── Voice Info ${'─'.repeat(50)}{/bright-cyan-fg}`,
3532
3433
  tags: true,
3533
3434
  style: { bg: COLORS.contentBg },
3534
3435
  });
@@ -3550,7 +3451,7 @@ function _openVoiceBrowserModal(screen, providerService, configService, navigati
3550
3451
  right: 2,
3551
3452
  tags: true,
3552
3453
  content: '',
3553
- style: { fg: '#00e5ff', bg: COLORS.contentBg },
3454
+ style: { fg: 'bright-cyan', bg: COLORS.contentBg },
3554
3455
  });
3555
3456
 
3556
3457
  // ---- Key hint bar ----
@@ -3655,11 +3556,22 @@ function _openVoiceBrowserModal(screen, providerService, configService, navigati
3655
3556
  const tempWav = path.join(os.tmpdir(), `agentvibes-preview-${Date.now()}.wav`);
3656
3557
  const phrase = SAMPLE_PHRASES[Math.floor(Math.random() * SAMPLE_PHRASES.length)];
3657
3558
 
3559
+ const _isWinPreview = process.platform === 'win32' && !process.env.WSL_DISTRO_NAME;
3560
+ let _piperBin3 = 'piper';
3561
+ if (_isWinPreview) {
3562
+ const _lad = process.env.LOCALAPPDATA ||
3563
+ (process.env.USERPROFILE ? path.join(process.env.USERPROFILE, 'AppData', 'Local') : null);
3564
+ if (_lad) {
3565
+ const _exe = path.join(_lad, 'Programs', 'Piper', 'piper.exe');
3566
+ if (fs.existsSync(_exe)) _piperBin3 = _exe;
3567
+ }
3568
+ }
3658
3569
  const _piperArgs3 = ['--model', voicePath, '--output_file', tempWav];
3659
3570
  if (_ms3.speakerId != null) _piperArgs3.push('--speaker', String(_ms3.speakerId));
3660
- const piper = spawn('piper', _piperArgs3, {
3571
+ const piper = spawn(_piperBin3, _piperArgs3, {
3661
3572
  stdio: ['pipe', 'ignore', 'ignore'],
3662
- detached: true,
3573
+ detached: !_isWinPreview,
3574
+ windowsHide: true,
3663
3575
  env: _spawnEnv,
3664
3576
  });
3665
3577
  piper.stdin.write(phrase + '\n');
@@ -3668,7 +3580,7 @@ function _openVoiceBrowserModal(screen, providerService, configService, navigati
3668
3580
  _playingProcess = piper;
3669
3581
  _playingVoiceId = voiceId;
3670
3582
  if (!_closed) {
3671
- modalPreviewLine.setContent(`{#00e5ff-fg}♪ Synthesizing: ${voiceId}…{/#00e5ff-fg}`);
3583
+ modalPreviewLine.setContent(`{bright-cyan-fg}♪ Synthesizing: ${voiceId}…{/bright-cyan-fg}`);
3672
3584
  screen.render();
3673
3585
  }
3674
3586
 
@@ -3681,7 +3593,7 @@ function _openVoiceBrowserModal(screen, providerService, configService, navigati
3681
3593
  _playingVoiceId = null;
3682
3594
  _playingProcess = null;
3683
3595
  if (!_closed) {
3684
- modalPreviewLine.setContent('{#00e5ff-fg}♪ Preview failed (piper error — is piper installed?){/#00e5ff-fg}');
3596
+ modalPreviewLine.setContent('{bright-cyan-fg}♪ Preview failed (piper error — is piper installed?){/bright-cyan-fg}');
3685
3597
  screen.render();
3686
3598
  setTimeout(() => { if (!_closed) { modalPreviewLine.setContent(''); screen.render(); } }, 4000);
3687
3599
  }
@@ -3692,13 +3604,14 @@ function _openVoiceBrowserModal(screen, providerService, configService, navigati
3692
3604
  if (!_wavPlayer3) return;
3693
3605
  const playProc = spawn(_wavPlayer3.bin, _wavPlayer3.args(tempWav), {
3694
3606
  stdio: 'ignore',
3695
- detached: true,
3607
+ detached: !_isWinPreview,
3608
+ windowsHide: true,
3696
3609
  env: _spawnEnv,
3697
3610
  });
3698
3611
  _playingProcess = playProc;
3699
3612
 
3700
3613
  if (!_closed) {
3701
- modalPreviewLine.setContent(`{#00e5ff-fg}♪ Playing: ${voiceId} (Space to stop){/#00e5ff-fg}`);
3614
+ modalPreviewLine.setContent(`{bright-cyan-fg}♪ Playing: ${voiceId} (Space to stop){/bright-cyan-fg}`);
3702
3615
  screen.render();
3703
3616
  }
3704
3617
 
@@ -3723,7 +3636,7 @@ function _openVoiceBrowserModal(screen, providerService, configService, navigati
3723
3636
  _playingVoiceId = null;
3724
3637
  _playingProcess = null;
3725
3638
  if (!_closed) {
3726
- modalPreviewLine.setContent('{#00e5ff-fg}♪ Cannot find piper — install with: pipx install piper-tts{/#00e5ff-fg}');
3639
+ modalPreviewLine.setContent('{bright-cyan-fg}♪ Cannot find piper — install with: pipx install piper-tts{/bright-cyan-fg}');
3727
3640
  screen.render();
3728
3641
  setTimeout(() => { if (!_closed) { modalPreviewLine.setContent(''); screen.render(); } }, 4000);
3729
3642
  }
@@ -3815,135 +3728,5 @@ function _openVoiceBrowserModal(screen, providerService, configService, navigati
3815
3728
  }
3816
3729
 
3817
3730
  // ---------------------------------------------------------------------------
3818
- // Private: Inline personality picker
3819
-
3820
- function _openPersonalityPicker(screen, configService, onSelect, onClose) {
3821
- const current = configService.getConfig().personality ?? 'none';
3822
- const currentIdx = Math.max(0, PERSONALITIES.indexOf(current));
3823
-
3824
- const list = blessed.list({
3825
- parent: screen,
3826
- top: 'center',
3827
- left: 'center',
3828
- width: 44,
3829
- height: Math.min(PERSONALITIES.length + 4, 22),
3830
- border: { type: 'line' },
3831
- tags: true,
3832
- label: _modalTitle('Select Personality'),
3833
- items: PERSONALITIES.map((p, i) => {
3834
- const emoji = PERSONALITY_EMOJIS[p] ?? '✨';
3835
- const label = p === 'none' ? 'None' : p.charAt(0).toUpperCase() + p.slice(1);
3836
- const mark = i === currentIdx ? '✅' : ' ';
3837
- return `${mark} ${emoji} ${label}`;
3838
- }),
3839
- keys: true,
3840
- vi: true,
3841
- mouse: true,
3842
- style: {
3843
- border: { fg: COLORS.btnFocus },
3844
- selected: { bg: COLORS.btnFocus, fg: COLORS.btnFocusFg, bold: true },
3845
- item: { fg: '#e3f2fd' },
3846
- },
3847
- });
3848
-
3849
- list.select(currentIdx);
3850
- list.focus();
3851
- screen.render();
3852
-
3853
- // ---------- Hover TTS preview ----------
3854
-
3855
- let _pickerTtsProc = null;
3856
- let _playingItemIdx = -1;
3857
-
3858
- // Add or remove " (playing)" from a list item (strips any trailing █ first)
3859
- function _setItemPlaying(idx, playing) {
3860
- const item = list.items?.[idx];
3861
- if (!item) return;
3862
- const base = (item.content ?? '').replace(/ █$/, '').replace(/ \(playing\)$/, '');
3863
- item.setContent(playing ? `${base} (playing)` : base);
3864
- }
3865
-
3866
- function _killPickerTts() {
3867
- if (_pickerTtsProc) {
3868
- try { process.kill(-_pickerTtsProc.pid, 'SIGTERM'); } catch {}
3869
- _pickerTtsProc = null;
3870
- }
3871
- if (_playingItemIdx >= 0) {
3872
- _setItemPlaying(_playingItemIdx, false);
3873
- _playingItemIdx = -1;
3874
- }
3875
- }
3876
-
3877
- function _speakPersonalityPreview(personality) {
3878
- _killPickerTts();
3879
- const phrase = PERSONALITY_PREVIEW_PHRASES[personality];
3880
- if (!phrase) return;
3881
- const ttsScript = path.join(process.cwd(), '.claude', 'hooks', 'play-tts.sh');
3882
- _pickerTtsProc = spawn('bash', [ttsScript, phrase], {
3883
- stdio: 'ignore',
3884
- detached: true,
3885
- env: buildAudioEnv(),
3886
- });
3887
- _playingItemIdx = list.selected;
3888
- _setItemPlaying(_playingItemIdx, true);
3889
- screen.render();
3890
- // Clear indicator when audio finishes naturally
3891
- _pickerTtsProc.on('exit', () => {
3892
- if (_playingItemIdx >= 0) {
3893
- _setItemPlaying(_playingItemIdx, false);
3894
- _playingItemIdx = -1;
3895
- screen.render();
3896
- }
3897
- _pickerTtsProc = null;
3898
- });
3899
- _pickerTtsProc.unref();
3900
- }
3901
-
3902
- // Hover: auto-speaks preview phrase when cursor moves
3903
- list.on('select item', () => {
3904
- _speakPersonalityPreview(PERSONALITIES[list.selected]);
3905
- });
3906
-
3907
- // [Space] plays the selected personality, or stops if the same item is already playing.
3908
- // Uses item-aware toggle so navigating with ↓ (which auto-plays) doesn't prevent Space from working.
3909
- list.key(['space'], () => {
3910
- if (_pickerTtsProc && _playingItemIdx === list.selected) {
3911
- _killPickerTts(); // true toggle: stop only if this exact item is playing
3912
- } else {
3913
- _speakPersonalityPreview(PERSONALITIES[list.selected]); // play or switch
3914
- }
3915
- });
3916
-
3917
- // Type-to-jump: press a letter to jump to the first matching personality (cycles on repeat)
3918
- const _jumpBlocked = new Set(['j', 'k', 'g', 'h', 'l', 'd', 'u', 'q']);
3919
- list.on('keypress', (ch, key) => {
3920
- if (!ch || key.ctrl || key.meta) return;
3921
- const lower = ch.toLowerCase();
3922
- if (!/^[a-z]$/.test(lower)) return;
3923
- if (_jumpBlocked.has(lower)) return;
3924
- const count = PERSONALITIES.length;
3925
- const start = list.selected ?? 0;
3926
- for (let i = 1; i <= count; i++) {
3927
- const idx = (start + i) % count;
3928
- if (PERSONALITIES[idx].startsWith(lower)) {
3929
- list.select(idx);
3930
- screen.render();
3931
- break;
3932
- }
3933
- }
3934
- });
3935
-
3936
- // [Enter] confirms selection
3937
- list.key(['enter'], () => {
3938
- const selected = PERSONALITIES[list.selected];
3939
- if (!selected) return;
3940
- _killPickerTts();
3941
- _destroyList(list, screen, onClose);
3942
- onSelect(selected);
3943
- });
3944
-
3945
- list.key(['escape', 'q'], () => {
3946
- _killPickerTts();
3947
- _destroyList(list, screen, onClose);
3948
- });
3949
- }
3731
+ // Private: _openPersonalityPicker removed — now using shared import:
3732
+ // import { openPersonalityPicker } from '../widgets/personality-picker.js';