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.
- package/.agentvibes/bmad/bmad-voices.md +69 -69
- package/.agentvibes/config.json +12 -0
- package/.claude/activation-instructions +54 -54
- package/.claude/audio/tracks/README.md +52 -52
- package/.claude/commands/agent-vibes/add.md +21 -21
- package/.claude/commands/agent-vibes/agent-vibes.md +101 -101
- package/.claude/commands/agent-vibes/agent.md +79 -79
- package/.claude/commands/agent-vibes/background-music.md +111 -111
- package/.claude/commands/agent-vibes/bmad.md +198 -198
- package/.claude/commands/agent-vibes/clean.md +18 -18
- package/.claude/commands/agent-vibes/cleanup.md +18 -18
- package/.claude/commands/agent-vibes/commands.json +145 -145
- package/.claude/commands/agent-vibes/effects.md +97 -97
- package/.claude/commands/agent-vibes/get.md +9 -9
- package/.claude/commands/agent-vibes/hide.md +91 -91
- package/.claude/commands/agent-vibes/language.md +23 -23
- package/.claude/commands/agent-vibes/learn.md +67 -67
- package/.claude/commands/agent-vibes/list.md +13 -13
- package/.claude/commands/agent-vibes/mute.md +37 -37
- package/.claude/commands/agent-vibes/preview.md +17 -17
- package/.claude/commands/agent-vibes/provider.md +68 -68
- package/.claude/commands/agent-vibes/replay-target.md +14 -14
- package/.claude/commands/agent-vibes/sample.md +12 -12
- package/.claude/commands/agent-vibes/set-favorite-voice.md +84 -84
- package/.claude/commands/agent-vibes/set-pretext.md +65 -65
- package/.claude/commands/agent-vibes/set-speed.md +41 -41
- package/.claude/commands/agent-vibes/show.md +84 -84
- package/.claude/commands/agent-vibes/switch.md +87 -87
- package/.claude/commands/agent-vibes/target-voice.md +26 -26
- package/.claude/commands/agent-vibes/target.md +30 -30
- package/.claude/commands/agent-vibes/translate.md +68 -68
- package/.claude/commands/agent-vibes/unmute.md +45 -45
- package/.claude/commands/agent-vibes/verbosity.md +89 -89
- package/.claude/commands/agent-vibes/whoami.md +7 -7
- package/.claude/commands/agent-vibes-bmad-voices.md +117 -117
- package/.claude/commands/agent-vibes-rdp.md +24 -24
- package/.claude/config/agentvibes.json +1 -0
- package/.claude/config/audio-effects.cfg +3 -2
- package/.claude/config/audio-effects.cfg.sample +52 -52
- package/.claude/config/background-music-volume.txt +1 -0
- package/.claude/config/intro-text.txt +1 -0
- package/.claude/config/piper-speech-rate.txt +4 -0
- package/.claude/config/piper-target-speech-rate.txt +1 -0
- package/.claude/config/reverb-level.txt +1 -0
- package/.claude/config/tts-speech-rate.txt +4 -0
- package/.claude/config/tts-target-speech-rate.txt +1 -0
- package/.claude/docs/TERMUX_SETUP.md +408 -408
- package/.claude/github-star-reminder.txt +1 -1
- package/.claude/hooks/README-TTS-QUEUE.md +135 -135
- package/.claude/hooks/audio-cache-utils.sh +246 -246
- package/.claude/hooks/audio-processor.sh +433 -389
- package/.claude/hooks/background-music-manager.sh +404 -404
- package/.claude/hooks/bmad-speak-enhanced.sh +165 -165
- package/.claude/hooks/bmad-speak.sh +269 -112
- package/.claude/hooks/bmad-tts-injector.sh +568 -568
- package/.claude/hooks/bmad-voice-manager.sh +928 -928
- package/.claude/hooks/clawdbot-receiver-SECURE.sh +129 -129
- package/.claude/hooks/clawdbot-receiver.sh +107 -107
- package/.claude/hooks/clean-audio-cache.sh +22 -22
- package/.claude/hooks/cleanup-cache.sh +106 -106
- package/.claude/hooks/configure-rdp-mode.sh +137 -137
- package/.claude/hooks/download-extra-voices.sh +244 -244
- package/.claude/hooks/effects-manager.sh +268 -268
- package/.claude/hooks/github-star-reminder.sh +154 -154
- package/.claude/hooks/language-manager.sh +362 -362
- package/.claude/hooks/learn-manager.sh +492 -492
- package/.claude/hooks/macos-voice-manager.sh +205 -205
- package/.claude/hooks/migrate-background-music.sh +125 -125
- package/.claude/hooks/migrate-to-agentvibes.sh +161 -161
- package/.claude/hooks/optimize-background-music.sh +87 -87
- package/.claude/hooks/path-resolver.sh +60 -60
- package/.claude/hooks/personality-manager.sh +448 -448
- package/.claude/hooks/piper-download-voices.sh +225 -225
- package/.claude/hooks/piper-installer.sh +292 -292
- package/.claude/hooks/piper-multispeaker-registry.sh +171 -171
- package/.claude/hooks/piper-voice-manager.sh +24 -3
- package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +90 -90
- package/.claude/hooks/play-tts-enhanced.sh +105 -70
- package/.claude/hooks/play-tts-macos.sh +368 -345
- package/.claude/hooks/play-tts-piper.sh +679 -578
- package/.claude/hooks/play-tts-soprano.sh +356 -320
- package/.claude/hooks/play-tts-ssh-remote.sh +167 -88
- package/.claude/hooks/play-tts-termux-ssh.sh +169 -169
- package/.claude/hooks/play-tts.sh +301 -298
- package/.claude/hooks/prepare-release.sh +54 -54
- package/.claude/hooks/provider-commands.sh +617 -617
- package/.claude/hooks/provider-manager.sh +399 -399
- package/.claude/hooks/replay-target-audio.sh +95 -95
- package/.claude/hooks/requirements.txt +6 -6
- package/.claude/hooks/sentiment-manager.sh +201 -201
- package/.claude/hooks/session-start-tts.sh +81 -71
- package/.claude/hooks/soprano-gradio-synth.py +139 -139
- package/.claude/hooks/speed-manager.sh +291 -291
- package/.claude/hooks/stop-tts.sh +84 -0
- package/.claude/hooks/termux-installer.sh +261 -261
- package/.claude/hooks/translate-manager.sh +341 -341
- package/.claude/hooks/translator.py +237 -237
- package/.claude/hooks/tts-queue-worker.sh +145 -114
- package/.claude/hooks/tts-queue.sh +165 -136
- package/.claude/hooks/verbosity-manager.sh +178 -178
- package/.claude/hooks/voice-manager.sh +548 -544
- package/.claude/hooks-windows/audio-cache-utils.ps1 +119 -119
- package/.claude/hooks-windows/background-music-manager.ps1 +348 -0
- package/.claude/hooks-windows/clean-audio-cache.ps1 +53 -0
- package/.claude/hooks-windows/download-extra-voices.ps1 +185 -0
- package/.claude/hooks-windows/effects-manager.ps1 +294 -0
- package/.claude/hooks-windows/language-manager.ps1 +193 -0
- package/.claude/hooks-windows/learn-manager.ps1 +241 -0
- package/.claude/hooks-windows/personality-manager.ps1 +266 -0
- package/.claude/hooks-windows/play-tts-piper.ps1 +209 -0
- package/.claude/hooks-windows/play-tts-sapi.ps1 +108 -0
- package/.claude/hooks-windows/play-tts-soprano.ps1 +159 -158
- package/.claude/hooks-windows/play-tts-windows-piper.ps1 +50 -5
- package/.claude/hooks-windows/play-tts-windows-sapi.ps1 +108 -108
- package/.claude/hooks-windows/play-tts.ps1 +344 -266
- package/.claude/hooks-windows/provider-manager.ps1 +29 -10
- package/.claude/hooks-windows/session-start-tts.ps1 +124 -124
- package/.claude/hooks-windows/soprano-gradio-synth.py +153 -153
- package/.claude/hooks-windows/speed-manager.ps1 +166 -0
- package/.claude/hooks-windows/verbosity-manager.ps1 +119 -0
- package/.claude/hooks-windows/voice-manager-windows.ps1 +92 -8
- package/.claude/output-styles/agent-vibes.md +202 -202
- package/.claude/personalities/angry.md +14 -14
- package/.claude/personalities/annoying.md +14 -14
- package/.claude/personalities/crass.md +14 -14
- package/.claude/personalities/dramatic.md +14 -14
- package/.claude/personalities/dry-humor.md +50 -50
- package/.claude/personalities/flirty.md +20 -20
- package/.claude/personalities/funny.md +14 -14
- package/.claude/personalities/grandpa.md +32 -32
- package/.claude/personalities/millennial.md +14 -14
- package/.claude/personalities/moody.md +14 -14
- package/.claude/personalities/normal.md +16 -16
- package/.claude/personalities/pirate.md +14 -14
- package/.claude/personalities/poetic.md +14 -14
- package/.claude/personalities/professional.md +14 -14
- package/.claude/personalities/rapper.md +55 -55
- package/.claude/personalities/robot.md +14 -14
- package/.claude/personalities/sarcastic.md +38 -38
- package/.claude/personalities/sassy.md +14 -14
- package/.claude/personalities/surfer-dude.md +14 -14
- package/.claude/personalities/zen.md +14 -14
- package/.claude/settings.json +15 -15
- package/.claude/verbosity.txt +1 -1
- package/.clawdbot/README.md +105 -105
- package/.clawdbot/skill/SKILL.md +241 -241
- package/.mcp.json +12 -0
- package/CLAUDE.md +170 -181
- package/README.md +2029 -1909
- package/RELEASE_NOTES.md +1310 -66
- package/WINDOWS-SETUP.md +208 -208
- package/bin/agent-vibes +39 -39
- package/bin/agentvibes-voice-browser.js +1840 -1826
- package/bin/agentvibes.js +48 -2
- package/bin/mcp-server.js +121 -121
- package/bin/mcp-server.sh +206 -206
- package/bin/test-bmad-pr +78 -78
- package/mcp-server/QUICK_START.md +203 -203
- package/mcp-server/README.md +345 -345
- package/mcp-server/WINDOWS_SETUP.md +260 -260
- package/mcp-server/docs/troubleshooting-audio.md +313 -313
- package/mcp-server/examples/claude_desktop_config.json +11 -11
- package/mcp-server/examples/claude_desktop_config_piper.json +9 -9
- package/mcp-server/examples/custom_instructions.md +169 -169
- package/mcp-server/install-deps.js +130 -130
- package/mcp-server/pyproject.toml +52 -52
- package/mcp-server/requirements.txt +2 -2
- package/mcp-server/server.py +1465 -1417
- package/mcp-server/test_server.py +395 -395
- package/mcp-server/test_windows_script_parity.py +336 -0
- package/package.json +110 -112
- package/setup-windows.ps1 +815 -815
- package/src/bmad-detector.js +71 -71
- package/src/cli/list-personalities.js +110 -110
- package/src/cli/list-voices.js +114 -114
- package/src/commands/bmad-voices.js +394 -394
- package/src/commands/install-mcp.js +476 -476
- package/src/console/app.js +824 -806
- package/src/console/audio-env.js +20 -1
- package/src/console/brand-colors.js +13 -13
- package/src/console/constants/personalities.js +44 -0
- package/src/console/footer-config.js +50 -46
- package/src/console/modals/modal-overlay.js +247 -247
- package/src/console/navigation.js +62 -61
- package/src/console/tabs/agents-tab.js +1684 -369
- package/src/console/tabs/help-tab.js +261 -261
- package/src/console/tabs/install-tab.js +1007 -991
- package/src/console/tabs/music-tab.js +22 -8
- package/src/console/tabs/placeholder-tab.js +53 -46
- package/src/console/tabs/readme-tab.js +267 -267
- package/src/console/tabs/receiver-tab.js +1472 -0
- package/src/console/tabs/settings-tab.js +185 -402
- package/src/console/tabs/voices-tab.js +100 -21
- package/src/console/widgets/destroy-list.js +25 -0
- package/src/console/widgets/format-utils.js +89 -0
- package/src/console/widgets/notice.js +55 -0
- package/src/console/widgets/personality-picker.js +185 -0
- package/src/console/widgets/reverb-picker.js +94 -0
- package/src/console/widgets/track-picker.js +285 -0
- package/src/installer/music-file-input.js +304 -304
- package/src/installer.js +5882 -5777
- package/src/services/agent-voice-store.js +423 -163
- package/src/services/config-service.js +264 -264
- package/src/services/navigation-service.js +123 -123
- package/src/services/provider-service.js +132 -132
- package/src/services/verbosity-service.js +157 -157
- package/src/utils/audio-duration-validator.js +298 -298
- package/src/utils/audio-format-validator.js +277 -277
- package/src/utils/dependency-checker.js +469 -466
- package/src/utils/file-ownership-verifier.js +358 -358
- package/src/utils/list-formatter.js +194 -194
- package/src/utils/music-file-validator.js +285 -275
- package/src/utils/preview-list-prompt.js +136 -136
- package/src/utils/provider-validator.js +96 -12
- package/src/utils/secure-music-storage.js +412 -412
- package/templates/agentvibes-receiver.sh +482 -162
- package/templates/audio/welcome-music.mp3 +0 -0
- package/voice-assignments.json +8244 -8244
- package/.claude/config/background-music-position.txt +0 -1
|
@@ -1,369 +1,1684 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AgentVibes TUI Console — Agents Tab
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
// ---------------------------------------------------------------------------
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
}
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
1
|
+
/**
|
|
2
|
+
* AgentVibes TUI Console — Agents Tab (BMAD Integration)
|
|
3
|
+
*
|
|
4
|
+
* Implements the Tab Component Contract:
|
|
5
|
+
* createAgentsTab(screen, services) → { box, show, hide, onFocus, onBlur, getFooterText, getFooterColor }
|
|
6
|
+
*
|
|
7
|
+
* Two states:
|
|
8
|
+
* 1. No BMAD detected → onboarding screen with description, links, install command
|
|
9
|
+
* 2. BMAD detected → agent table with per-agent voice/pretext/reverb/personality/music customization
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { AgentVoiceStore, scanBmadAgents, isBmadDetected, isSingleVoiceProvider } from '../../services/agent-voice-store.js';
|
|
13
|
+
import { openReverbPicker, REVERB_PRESETS } from '../widgets/reverb-picker.js';
|
|
14
|
+
import { openPersonalityPicker, PERSONALITIES, PERSONALITY_EMOJIS } from '../widgets/personality-picker.js';
|
|
15
|
+
import { openTrackPicker, openVolumeInput } from '../widgets/track-picker.js';
|
|
16
|
+
import { formatReverbState, formatTrackName, formatVoiceName } from '../widgets/format-utils.js';
|
|
17
|
+
import {
|
|
18
|
+
PIPER_VOICES_DIR, SAMPLE_PHRASES,
|
|
19
|
+
parseMultiSpeaker, scanInstalledVoices, getVoiceMeta,
|
|
20
|
+
} from './voices-tab.js';
|
|
21
|
+
import { buildAudioEnv, detectWavPlayer } from '../audio-env.js';
|
|
22
|
+
import { destroyList } from '../widgets/destroy-list.js';
|
|
23
|
+
import { BRAND_PINK } from '../brand-colors.js';
|
|
24
|
+
import crypto from 'node:crypto';
|
|
25
|
+
import fs from 'node:fs';
|
|
26
|
+
import os from 'node:os';
|
|
27
|
+
import path from 'node:path';
|
|
28
|
+
import { spawn } from 'node:child_process';
|
|
29
|
+
|
|
30
|
+
// Max pretext length to prevent excessively long TTS utterances
|
|
31
|
+
const MAX_PRETEXT_LENGTH = 200;
|
|
32
|
+
|
|
33
|
+
const IS_TEST = process.env.AGENTVIBES_TEST_MODE === 'true';
|
|
34
|
+
|
|
35
|
+
let blessed;
|
|
36
|
+
if (!IS_TEST) {
|
|
37
|
+
const { default: b } = await import('blessed');
|
|
38
|
+
blessed = b;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
const COLORS = {
|
|
44
|
+
contentBg: '#0a0e1a',
|
|
45
|
+
sectionHdr: '#7b1fa2',
|
|
46
|
+
labelFg: '#e3f2fd',
|
|
47
|
+
valueFg: '#ffff00',
|
|
48
|
+
activeFg: '#ce93d8',
|
|
49
|
+
btnDefault: '#6a1b9a',
|
|
50
|
+
btnFocus: '#2e7d32', // Green — focused/selected
|
|
51
|
+
btnFocusFg: '#ffffff',
|
|
52
|
+
btnPress: '#ff00ff',
|
|
53
|
+
borderFg: '#9c27b0',
|
|
54
|
+
footerBg: '#9c27b0',
|
|
55
|
+
noticeFg: '#90a4ae',
|
|
56
|
+
warnFg: '#ff9800',
|
|
57
|
+
linkFg: 'bright-cyan',
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const FOOTER_TEXT_BMAD = '[↑↓/jk] Navigate [Space] Preview [Enter] Configure [A] Auto-assign [B] Bulk [X] Reset [Q] Quit';
|
|
61
|
+
const FOOTER_TEXT_NOBMAD = '[Tab] Switch Tab [Q] Quit';
|
|
62
|
+
|
|
63
|
+
const _modalTitle = (text) => ` {${BRAND_PINK}-fg}${text}{/${BRAND_PINK}-fg} `;
|
|
64
|
+
|
|
65
|
+
// Column widths for agent table
|
|
66
|
+
const COL_ICON = 4;
|
|
67
|
+
const COL_NAME = 16;
|
|
68
|
+
const COL_VOICE = 12; // beautified names avg 5-11 chars
|
|
69
|
+
const COL_GENDER = 8;
|
|
70
|
+
const COL_PROVIDER = 12;
|
|
71
|
+
const COL_PRETEXT = 14;
|
|
72
|
+
const COL_REVERB = 10;
|
|
73
|
+
const COL_MUSIC = 11;
|
|
74
|
+
const COL_VOL = 5; // e.g. "70%" or "100%"
|
|
75
|
+
|
|
76
|
+
// Inline hint appended to the selected row when list is focused
|
|
77
|
+
const _ROW_HINT_BMAD = ` {bright-black-fg}[Space] Preview [Enter] Configure{/bright-black-fg}`;
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
function createTestStub() {
|
|
82
|
+
return {
|
|
83
|
+
box: {},
|
|
84
|
+
show: () => {},
|
|
85
|
+
hide: () => {},
|
|
86
|
+
onFocus: () => {},
|
|
87
|
+
onBlur: () => {},
|
|
88
|
+
getFooterText: () => FOOTER_TEXT_BMAD,
|
|
89
|
+
getFooterColor: () => COLORS.footerBg,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// No-BMAD onboarding content
|
|
95
|
+
|
|
96
|
+
const ONBOARDING_TEXT = `{bold}{#ce93d8-fg}🧙 BMAD Agents{/#ce93d8-fg}{/bold}
|
|
97
|
+
|
|
98
|
+
{bold}What is BMAD?{/bold}
|
|
99
|
+
|
|
100
|
+
The BMad Method (Build More Architect Dreams) is an AI-driven development
|
|
101
|
+
framework module within the BMad Method Ecosystem that helps you build
|
|
102
|
+
software through the whole process from ideation and planning all the way
|
|
103
|
+
through agentic implementation. It provides specialized AI agents, guided
|
|
104
|
+
workflows, and intelligent planning that adapts to your project's
|
|
105
|
+
complexity, whether you're fixing a bug or building an enterprise platform.
|
|
106
|
+
|
|
107
|
+
If you're comfortable working with AI coding assistants like Claude,
|
|
108
|
+
Cursor, or GitHub Copilot, you're ready to get started.
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
{bold}Install BMAD in your project:{/bold}
|
|
112
|
+
|
|
113
|
+
{bright-cyan-fg}npx bmad-method install{/bright-cyan-fg}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
{bold}Learn more:{/bold}
|
|
117
|
+
|
|
118
|
+
{bright-cyan-fg}https://docs.bmad-method.org/{/bright-cyan-fg}
|
|
119
|
+
{bright-cyan-fg}https://github.com/bmad-code-org/BMAD-METHOD{/bright-cyan-fg}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
{#90a4ae-fg}Once BMAD is installed, this tab will show all your agents and let you
|
|
123
|
+
customize each agent's voice, pretext, reverb, personality, and background
|
|
124
|
+
music independently.{/#90a4ae-fg}`;
|
|
125
|
+
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Create the Agents tab component.
|
|
130
|
+
*/
|
|
131
|
+
export function createAgentsTab(screen, services) {
|
|
132
|
+
if (IS_TEST) return createTestStub();
|
|
133
|
+
|
|
134
|
+
const { configService, providerService, focusMainTabBar, navigationService } = services;
|
|
135
|
+
const voiceStore = new AgentVoiceStore();
|
|
136
|
+
|
|
137
|
+
// Capture cwd once at construction (L1 fix)
|
|
138
|
+
const _projectRoot = process.cwd();
|
|
139
|
+
|
|
140
|
+
let _bmadDetected = false;
|
|
141
|
+
let _agents = [];
|
|
142
|
+
let _playingProcess = null;
|
|
143
|
+
let _playGeneration = 0; // H4: generation counter to prevent orphaned processes
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Create a secure temp file path using XDG_RUNTIME_DIR or user-specific dir (H3 fix).
|
|
147
|
+
*/
|
|
148
|
+
function _secureTempWav(prefix) {
|
|
149
|
+
const baseDir = process.env.XDG_RUNTIME_DIR || os.tmpdir();
|
|
150
|
+
const dir = path.join(baseDir, `agentvibes-${process.getuid?.() ?? 'u'}`);
|
|
151
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
152
|
+
try { fs.chmodSync(dir, 0o700); } catch {}
|
|
153
|
+
return path.join(dir, `${prefix}-${crypto.randomUUID()}.wav`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// -------------------------------------------------------------------------
|
|
157
|
+
// Container
|
|
158
|
+
|
|
159
|
+
const box = blessed.box({
|
|
160
|
+
parent: screen,
|
|
161
|
+
top: 4,
|
|
162
|
+
left: 0,
|
|
163
|
+
width: '100%',
|
|
164
|
+
bottom: 2,
|
|
165
|
+
hidden: true,
|
|
166
|
+
style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
|
|
167
|
+
border: { type: 'line' },
|
|
168
|
+
borderStyle: { fg: COLORS.borderFg },
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// -------------------------------------------------------------------------
|
|
172
|
+
// Onboarding content (no-BMAD state)
|
|
173
|
+
|
|
174
|
+
const onboardingBox = blessed.box({
|
|
175
|
+
parent: box,
|
|
176
|
+
top: 1,
|
|
177
|
+
left: 3,
|
|
178
|
+
right: 3,
|
|
179
|
+
bottom: 1,
|
|
180
|
+
hidden: true,
|
|
181
|
+
tags: true,
|
|
182
|
+
scrollable: true,
|
|
183
|
+
keys: true,
|
|
184
|
+
vi: true,
|
|
185
|
+
mouse: true,
|
|
186
|
+
content: ONBOARDING_TEXT,
|
|
187
|
+
style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// -------------------------------------------------------------------------
|
|
191
|
+
// BMAD state — section header
|
|
192
|
+
|
|
193
|
+
const sectionHeader = blessed.text({
|
|
194
|
+
parent: box,
|
|
195
|
+
top: 1,
|
|
196
|
+
left: 2,
|
|
197
|
+
hidden: true,
|
|
198
|
+
content: `{#7b1fa2-fg}── BMAD Agents ${'─'.repeat(53)}{/#7b1fa2-fg}`,
|
|
199
|
+
tags: true,
|
|
200
|
+
style: { bg: COLORS.contentBg },
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Column header
|
|
204
|
+
const columnHeader = blessed.text({
|
|
205
|
+
parent: box,
|
|
206
|
+
top: 2,
|
|
207
|
+
left: 4,
|
|
208
|
+
hidden: true,
|
|
209
|
+
tags: true,
|
|
210
|
+
content: `{#90a4ae-fg}${''.padEnd(COL_ICON)}${' Agent'.padEnd(COL_NAME)}${' Voice'.padEnd(COL_VOICE)}${' Gender'.padEnd(COL_GENDER)}${' Provider'.padEnd(COL_PROVIDER)}${' Reverb'.padEnd(COL_REVERB)}${' Music'.padEnd(COL_MUSIC)}${' Vol'.padEnd(COL_VOL)} Pretext{/#90a4ae-fg}`,
|
|
211
|
+
style: { bg: COLORS.contentBg },
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// -------------------------------------------------------------------------
|
|
215
|
+
// Agent list
|
|
216
|
+
|
|
217
|
+
const agentList = blessed.list({
|
|
218
|
+
parent: box,
|
|
219
|
+
top: 3,
|
|
220
|
+
left: 2,
|
|
221
|
+
width: '96%',
|
|
222
|
+
height: '55%',
|
|
223
|
+
hidden: true,
|
|
224
|
+
keys: true,
|
|
225
|
+
vi: true,
|
|
226
|
+
mouse: true,
|
|
227
|
+
tags: true,
|
|
228
|
+
border: { type: 'line' },
|
|
229
|
+
scrollbar: { ch: '│', style: { fg: COLORS.sectionHdr } },
|
|
230
|
+
style: {
|
|
231
|
+
fg: COLORS.labelFg,
|
|
232
|
+
bg: COLORS.contentBg,
|
|
233
|
+
border: { fg: COLORS.borderFg },
|
|
234
|
+
selected: { bg: '#4a148c', fg: COLORS.activeFg, bold: true },
|
|
235
|
+
item: { fg: COLORS.labelFg },
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// -------------------------------------------------------------------------
|
|
240
|
+
// Status panel
|
|
241
|
+
|
|
242
|
+
const statusDivider = blessed.text({
|
|
243
|
+
parent: box,
|
|
244
|
+
top: '64%',
|
|
245
|
+
left: 2,
|
|
246
|
+
hidden: true,
|
|
247
|
+
content: `{#7b1fa2-fg}── Status ${'─'.repeat(58)}{/#7b1fa2-fg}`,
|
|
248
|
+
tags: true,
|
|
249
|
+
style: { bg: COLORS.contentBg },
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const statusLine = blessed.text({
|
|
253
|
+
parent: box,
|
|
254
|
+
top: '69%',
|
|
255
|
+
left: 2,
|
|
256
|
+
hidden: true,
|
|
257
|
+
tags: true,
|
|
258
|
+
content: '',
|
|
259
|
+
style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const warningLine = blessed.text({
|
|
263
|
+
parent: box,
|
|
264
|
+
top: '74%',
|
|
265
|
+
left: 2,
|
|
266
|
+
hidden: true,
|
|
267
|
+
tags: true,
|
|
268
|
+
content: '',
|
|
269
|
+
style: { fg: COLORS.warnFg, bg: COLORS.contentBg },
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// Hint shown inline next to the action buttons at bottom of list
|
|
273
|
+
const hintLine = blessed.text({
|
|
274
|
+
parent: box,
|
|
275
|
+
bottom: 5,
|
|
276
|
+
left: 4,
|
|
277
|
+
hidden: true,
|
|
278
|
+
tags: true,
|
|
279
|
+
content: '{#546e7a-fg}[Space] Preview [Enter] Configure [X] Reset [A] Auto-assign [B] Bulk Edit{/#546e7a-fg}',
|
|
280
|
+
style: { bg: COLORS.contentBg },
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// -------------------------------------------------------------------------
|
|
284
|
+
// Buttons
|
|
285
|
+
|
|
286
|
+
function _createBtn(label, onClick) {
|
|
287
|
+
const btn = blessed.button({
|
|
288
|
+
parent: box,
|
|
289
|
+
content: label,
|
|
290
|
+
mouse: true,
|
|
291
|
+
keys: true,
|
|
292
|
+
shrink: true,
|
|
293
|
+
hidden: true,
|
|
294
|
+
padding: { left: 1, right: 1 },
|
|
295
|
+
style: {
|
|
296
|
+
bg: COLORS.btnDefault,
|
|
297
|
+
fg: 'white',
|
|
298
|
+
focus: { bg: COLORS.btnFocus, fg: COLORS.btnFocusFg, bold: true },
|
|
299
|
+
hover: { bg: COLORS.btnFocus, fg: COLORS.btnFocusFg, bold: true },
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
btn.on('focus', () => {
|
|
303
|
+
const raw = btn.content.replace(/[►◄]/g, '').trim();
|
|
304
|
+
btn.setContent(`►${raw}◄`);
|
|
305
|
+
screen.render();
|
|
306
|
+
});
|
|
307
|
+
btn.on('blur', () => {
|
|
308
|
+
const raw = btn.content.replace(/[►◄]/g, '').trim();
|
|
309
|
+
btn.setContent(raw);
|
|
310
|
+
screen.render();
|
|
311
|
+
});
|
|
312
|
+
btn.key(['enter', 'space'], () => {
|
|
313
|
+
btn.style.bg = COLORS.btnPress;
|
|
314
|
+
screen.render();
|
|
315
|
+
setTimeout(() => {
|
|
316
|
+
btn.style.bg = COLORS.btnDefault;
|
|
317
|
+
screen.render();
|
|
318
|
+
onClick();
|
|
319
|
+
}, 150);
|
|
320
|
+
});
|
|
321
|
+
btn.on('click', () => btn.press());
|
|
322
|
+
btn.on('mouseover', () => btn.focus());
|
|
323
|
+
return btn;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const resetBtn = _createBtn('[X] Reset', () => {
|
|
327
|
+
const agent = _agents[agentList.selected];
|
|
328
|
+
if (agent) {
|
|
329
|
+
voiceStore.resetAgentProfile(agent.id);
|
|
330
|
+
refreshDisplay();
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
resetBtn.bottom = 4;
|
|
334
|
+
resetBtn.left = 4;
|
|
335
|
+
|
|
336
|
+
const autoAssignBtn = _createBtn('[A] Auto-assign', () => _autoAssignAll());
|
|
337
|
+
autoAssignBtn.bottom = 4;
|
|
338
|
+
autoAssignBtn.left = 18;
|
|
339
|
+
|
|
340
|
+
const bulkEditBtn = _createBtn('[B] Bulk Edit', () => _openBulkEditMenu());
|
|
341
|
+
bulkEditBtn.bottom = 4;
|
|
342
|
+
bulkEditBtn.left = 36;
|
|
343
|
+
|
|
344
|
+
// -------------------------------------------------------------------------
|
|
345
|
+
// Show/hide helpers for the two states
|
|
346
|
+
|
|
347
|
+
const _bmadWidgets = [sectionHeader, columnHeader, agentList, hintLine, resetBtn, autoAssignBtn, bulkEditBtn];
|
|
348
|
+
|
|
349
|
+
function _showBmadState() {
|
|
350
|
+
onboardingBox.hide();
|
|
351
|
+
for (const w of _bmadWidgets) w.show();
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function _showOnboardingState() {
|
|
355
|
+
for (const w of _bmadWidgets) w.hide();
|
|
356
|
+
onboardingBox.show();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// -------------------------------------------------------------------------
|
|
360
|
+
// Build table row items
|
|
361
|
+
|
|
362
|
+
function _buildListItems(agents) {
|
|
363
|
+
if (agents.length === 0) {
|
|
364
|
+
return [' (no BMAD agents detected)'];
|
|
365
|
+
}
|
|
366
|
+
return agents.map(a => {
|
|
367
|
+
const profile = voiceStore.getAgentProfile(a.id);
|
|
368
|
+
// Strip variation selectors (e.g. U+FE0F on 🏗️) so padEnd uses visual width
|
|
369
|
+
const rawIcon = (a.icon || '').replace(/\uFE0F/g, '');
|
|
370
|
+
const icon = (rawIcon ? `${rawIcon} ` : ' ').padEnd(COL_ICON);
|
|
371
|
+
const name = ` ${a.displayName}`.padEnd(COL_NAME).slice(0, COL_NAME);
|
|
372
|
+
const voiceRaw = formatVoiceName(profile.voice);
|
|
373
|
+
const voice = (' ' + voiceRaw).padEnd(COL_VOICE).slice(0, COL_VOICE);
|
|
374
|
+
const meta = profile.voice ? getVoiceMeta(profile.voice) : { gender: '—', provider: '—' };
|
|
375
|
+
const gender = (' ' + meta.gender).padEnd(COL_GENDER).slice(0, COL_GENDER);
|
|
376
|
+
const provider = (' ' + meta.provider).padEnd(COL_PROVIDER).slice(0, COL_PROVIDER);
|
|
377
|
+
const reverb = (' ' + (profile.reverbPreset || '(global)')).padEnd(COL_REVERB).slice(0, COL_REVERB);
|
|
378
|
+
const music = (' ' + (profile.backgroundMusic?.track
|
|
379
|
+
? formatTrackName(profile.backgroundMusic.track)
|
|
380
|
+
: '(global)')).padEnd(COL_MUSIC).slice(0, COL_MUSIC);
|
|
381
|
+
const vol = profile.backgroundMusic?.enabled
|
|
382
|
+
? ` ${profile.backgroundMusic.volume ?? 70}%`.padEnd(COL_VOL)
|
|
383
|
+
: ' — ';
|
|
384
|
+
const pretext = ' ' + (profile.pretext || '(default)').slice(0, COL_PRETEXT - 1);
|
|
385
|
+
return ` ${icon}${name}${voice}${gender}${provider}${reverb}${music}${vol} ${pretext}`;
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// -------------------------------------------------------------------------
|
|
390
|
+
// Refresh display
|
|
391
|
+
|
|
392
|
+
function refreshDisplay() {
|
|
393
|
+
_bmadDetected = isBmadDetected(_projectRoot);
|
|
394
|
+
_agents = scanBmadAgents(_projectRoot);
|
|
395
|
+
|
|
396
|
+
if (!_bmadDetected) {
|
|
397
|
+
_showOnboardingState();
|
|
398
|
+
screen.render();
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
_showBmadState();
|
|
403
|
+
|
|
404
|
+
const items = _buildListItems(_agents);
|
|
405
|
+
agentList.setItems(items);
|
|
406
|
+
|
|
407
|
+
if (_listFocused) {
|
|
408
|
+
_hintIdx = -1;
|
|
409
|
+
_hintBase = '';
|
|
410
|
+
_updateHint(agentList.selected ?? 0);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
screen.render();
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// -------------------------------------------------------------------------
|
|
417
|
+
// Temporary "Saved!" toast notification
|
|
418
|
+
|
|
419
|
+
function _showSavedToast(agentName) {
|
|
420
|
+
const toast = blessed.box({
|
|
421
|
+
parent: screen,
|
|
422
|
+
top: 'center',
|
|
423
|
+
left: 'center',
|
|
424
|
+
width: 34,
|
|
425
|
+
height: 3,
|
|
426
|
+
border: { type: 'line' },
|
|
427
|
+
tags: true,
|
|
428
|
+
content: ` {green-fg}{bold}✓ ${agentName} saved!{/bold}{/green-fg}`,
|
|
429
|
+
style: { fg: '#e3f2fd', bg: '#1b5e20', border: { fg: '#4caf50' } },
|
|
430
|
+
});
|
|
431
|
+
toast.setFront();
|
|
432
|
+
screen.render();
|
|
433
|
+
setTimeout(() => {
|
|
434
|
+
toast.destroy();
|
|
435
|
+
try {
|
|
436
|
+
for (let r = 0; r < screen.height; r++)
|
|
437
|
+
for (let c = 0; c < screen.width; c++)
|
|
438
|
+
if (screen.olines[r]?.[c]) screen.olines[r][c][0] = -1;
|
|
439
|
+
} catch {}
|
|
440
|
+
screen.render();
|
|
441
|
+
}, 1500);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// -------------------------------------------------------------------------
|
|
445
|
+
// Row spinner (animated braille while preview is playing)
|
|
446
|
+
|
|
447
|
+
const _SPIN_PFX = '{bright-cyan-fg}';
|
|
448
|
+
const _SPIN_SFX = '{/bright-cyan-fg}';
|
|
449
|
+
const _SPIN_PFX_TOTAL_LEN = _SPIN_PFX.length + 1 + _SPIN_SFX.length; // tag + 1 frame char + close tag
|
|
450
|
+
const _SPIN_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
451
|
+
let _spinnerInterval = null;
|
|
452
|
+
let _spinnerFrameIdx = 0;
|
|
453
|
+
let _spinnerAgentIdx = -1;
|
|
454
|
+
|
|
455
|
+
// Strip the spinner prefix (tag+frame+close or plain first char) to get the row tail.
|
|
456
|
+
function _stripSpinnerPfx(c) {
|
|
457
|
+
return c.startsWith(_SPIN_PFX) ? c.slice(_SPIN_PFX_TOTAL_LEN) : c.slice(1);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function _startSpinner(agentIdx) {
|
|
461
|
+
_stopSpinner();
|
|
462
|
+
_spinnerAgentIdx = agentIdx;
|
|
463
|
+
_spinnerFrameIdx = 0;
|
|
464
|
+
const items = agentList.items;
|
|
465
|
+
const item = items[_spinnerAgentIdx];
|
|
466
|
+
if (item) {
|
|
467
|
+
item.setContent(`${_SPIN_PFX}${_SPIN_FRAMES[0]}${_SPIN_SFX}${_stripSpinnerPfx(item.content ?? ' ')}`);
|
|
468
|
+
screen.render();
|
|
469
|
+
}
|
|
470
|
+
_spinnerInterval = setInterval(() => {
|
|
471
|
+
_spinnerFrameIdx = (_spinnerFrameIdx + 1) % _SPIN_FRAMES.length;
|
|
472
|
+
const it = agentList.items[_spinnerAgentIdx];
|
|
473
|
+
if (!it) return;
|
|
474
|
+
it.setContent(`${_SPIN_PFX}${_SPIN_FRAMES[_spinnerFrameIdx]}${_SPIN_SFX}${_stripSpinnerPfx(it.content ?? ' ')}`);
|
|
475
|
+
screen.render();
|
|
476
|
+
}, 80);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function _stopSpinner() {
|
|
480
|
+
if (_spinnerInterval) { clearInterval(_spinnerInterval); _spinnerInterval = null; }
|
|
481
|
+
if (_spinnerAgentIdx >= 0) {
|
|
482
|
+
const item = agentList.items[_spinnerAgentIdx];
|
|
483
|
+
if (item) item.setContent(' ' + _stripSpinnerPfx(item.content ?? ' '));
|
|
484
|
+
_spinnerAgentIdx = -1;
|
|
485
|
+
screen.render();
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// -------------------------------------------------------------------------
|
|
490
|
+
// Resolve piper binary — shared helper to avoid duplication (#153)
|
|
491
|
+
|
|
492
|
+
function _resolvePiperBin() {
|
|
493
|
+
if (process.platform !== 'win32' || process.env.WSL_DISTRO_NAME) return 'piper';
|
|
494
|
+
const localAppData = process.env.LOCALAPPDATA ||
|
|
495
|
+
(process.env.USERPROFILE ? path.join(process.env.USERPROFILE, 'AppData', 'Local') : null);
|
|
496
|
+
if (localAppData) {
|
|
497
|
+
const exePath = path.join(localAppData, 'Programs', 'Piper', 'piper.exe');
|
|
498
|
+
if (fs.existsSync(exePath)) return exePath;
|
|
499
|
+
}
|
|
500
|
+
return 'piper';
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// -------------------------------------------------------------------------
|
|
504
|
+
// Kill any playing preview
|
|
505
|
+
|
|
506
|
+
function _killPreview() {
|
|
507
|
+
_stopSpinner();
|
|
508
|
+
if (_playingProcess) {
|
|
509
|
+
try {
|
|
510
|
+
// On Windows, negative-PID process group kill is unsupported
|
|
511
|
+
if (process.platform === 'win32') {
|
|
512
|
+
_playingProcess.kill();
|
|
513
|
+
} else {
|
|
514
|
+
process.kill(-_playingProcess.pid, 'SIGTERM');
|
|
515
|
+
}
|
|
516
|
+
} catch {}
|
|
517
|
+
_playingProcess = null;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// -------------------------------------------------------------------------
|
|
522
|
+
// Sample an agent with their full profile (voice + pretext + reverb + music)
|
|
523
|
+
// Uses play-tts-enhanced.sh for the complete effects pipeline.
|
|
524
|
+
|
|
525
|
+
function _sampleAgent(agent) {
|
|
526
|
+
const profile = voiceStore.getAgentProfile(agent.id);
|
|
527
|
+
const globalCfg = configService.getConfig();
|
|
528
|
+
_sampleWithFullProfile(agent, {
|
|
529
|
+
voice: profile.voice || globalCfg.voice || '',
|
|
530
|
+
pretext: profile.pretext || AgentVoiceStore.getDefaultPretext(agent.displayName, agent.title),
|
|
531
|
+
reverbPreset: profile.reverbPreset || globalCfg.effects?.reverbPreset || 'light',
|
|
532
|
+
personality: profile.personality || globalCfg.personality || 'none',
|
|
533
|
+
backgroundMusic: {
|
|
534
|
+
track: profile.backgroundMusic?.track || globalCfg.backgroundMusic?.track || '',
|
|
535
|
+
volume: profile.backgroundMusic?.volume ?? globalCfg.backgroundMusic?.volume ?? 70,
|
|
536
|
+
enabled: profile.backgroundMusic?.enabled ?? globalCfg.backgroundMusic?.enabled ?? false,
|
|
537
|
+
},
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// -------------------------------------------------------------------------
|
|
542
|
+
// Agent detail panel (modal overlay)
|
|
543
|
+
|
|
544
|
+
function _openAgentDetailPanel(agent) {
|
|
545
|
+
const profile = voiceStore.getAgentProfile(agent.id);
|
|
546
|
+
const globalCfg = configService.getConfig();
|
|
547
|
+
|
|
548
|
+
// Working copy of the profile being edited
|
|
549
|
+
const draft = {
|
|
550
|
+
voice: profile.voice || globalCfg.voice || '',
|
|
551
|
+
pretext: profile.pretext || AgentVoiceStore.getDefaultPretext(agent.displayName, agent.title),
|
|
552
|
+
reverbPreset: profile.reverbPreset || globalCfg.effects?.reverbPreset || 'light',
|
|
553
|
+
personality: profile.personality || globalCfg.personality || 'none',
|
|
554
|
+
backgroundMusic: {
|
|
555
|
+
track: profile.backgroundMusic?.track || globalCfg.backgroundMusic?.track || '',
|
|
556
|
+
volume: profile.backgroundMusic?.volume ?? globalCfg.backgroundMusic?.volume ?? 70,
|
|
557
|
+
enabled: profile.backgroundMusic?.enabled ?? globalCfg.backgroundMusic?.enabled ?? false,
|
|
558
|
+
},
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
let _closed = false;
|
|
562
|
+
navigationService?.openModal();
|
|
563
|
+
|
|
564
|
+
const modal = blessed.box({
|
|
565
|
+
parent: screen,
|
|
566
|
+
top: 'center',
|
|
567
|
+
left: 'center',
|
|
568
|
+
width: 72,
|
|
569
|
+
height: 18,
|
|
570
|
+
border: { type: 'line' },
|
|
571
|
+
tags: true,
|
|
572
|
+
label: _modalTitle(`${agent.icon || '🧙'} ${agent.displayName} (${agent.title || 'Agent'})`),
|
|
573
|
+
style: {
|
|
574
|
+
fg: COLORS.labelFg,
|
|
575
|
+
bg: COLORS.contentBg,
|
|
576
|
+
border: { fg: COLORS.btnFocus },
|
|
577
|
+
},
|
|
578
|
+
});
|
|
579
|
+
modal.setFront();
|
|
580
|
+
|
|
581
|
+
// Field definitions
|
|
582
|
+
const FIELDS = [
|
|
583
|
+
{ key: 'voice', label: 'Voice', getValue: () => draft.voice || '(global default)' },
|
|
584
|
+
{ key: 'pretext', label: 'Pretext', getValue: () => draft.pretext || '(default)' },
|
|
585
|
+
{ key: 'reverbPreset', label: 'Reverb', getValue: () => formatReverbState(draft.reverbPreset) },
|
|
586
|
+
{ key: 'personality', label: 'Personality', getValue: () => {
|
|
587
|
+
const p = draft.personality;
|
|
588
|
+
const emoji = PERSONALITY_EMOJIS[p] || '';
|
|
589
|
+
return `${emoji} ${p === 'none' ? 'None' : p.charAt(0).toUpperCase() + p.slice(1)}`;
|
|
590
|
+
}},
|
|
591
|
+
{ key: 'music', label: 'Music', getValue: () => {
|
|
592
|
+
if (!draft.backgroundMusic.enabled) return '(disabled)';
|
|
593
|
+
return `${formatTrackName(draft.backgroundMusic.track)} Vol:${draft.backgroundMusic.volume}%`;
|
|
594
|
+
}},
|
|
595
|
+
];
|
|
596
|
+
|
|
597
|
+
// Build field list items
|
|
598
|
+
function _fieldItems() {
|
|
599
|
+
return FIELDS.map(f => {
|
|
600
|
+
const label = f.label.padEnd(14);
|
|
601
|
+
const val = f.getValue();
|
|
602
|
+
return ` ${label} ${val}`;
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const fieldList = blessed.list({
|
|
607
|
+
parent: modal,
|
|
608
|
+
top: 1,
|
|
609
|
+
left: 2,
|
|
610
|
+
right: 2,
|
|
611
|
+
height: FIELDS.length + 2,
|
|
612
|
+
keys: true,
|
|
613
|
+
vi: true,
|
|
614
|
+
mouse: true,
|
|
615
|
+
border: { type: 'line' },
|
|
616
|
+
tags: true,
|
|
617
|
+
style: {
|
|
618
|
+
fg: COLORS.labelFg,
|
|
619
|
+
bg: COLORS.contentBg,
|
|
620
|
+
border: { fg: '#4a148c' },
|
|
621
|
+
selected: { bg: '#4a148c', fg: COLORS.activeFg, bold: true },
|
|
622
|
+
item: { fg: COLORS.labelFg },
|
|
623
|
+
},
|
|
624
|
+
});
|
|
625
|
+
fieldList.setItems(_fieldItems());
|
|
626
|
+
|
|
627
|
+
// Key hint
|
|
628
|
+
blessed.text({
|
|
629
|
+
parent: modal,
|
|
630
|
+
bottom: 4,
|
|
631
|
+
left: 2,
|
|
632
|
+
right: 2,
|
|
633
|
+
tags: true,
|
|
634
|
+
content: '{#455a64-fg}[↑↓] Navigate fields [Enter] Edit field [Space] Sample [Esc] Cancel{/#455a64-fg}',
|
|
635
|
+
style: { bg: COLORS.contentBg },
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
// Buttons
|
|
639
|
+
function _modalBtn(label, leftPos, onClick) {
|
|
640
|
+
const btn = blessed.button({
|
|
641
|
+
parent: modal,
|
|
642
|
+
content: label,
|
|
643
|
+
bottom: 2,
|
|
644
|
+
left: leftPos,
|
|
645
|
+
mouse: true,
|
|
646
|
+
keys: true,
|
|
647
|
+
shrink: true,
|
|
648
|
+
padding: { left: 1, right: 1 },
|
|
649
|
+
style: {
|
|
650
|
+
bg: COLORS.btnDefault,
|
|
651
|
+
fg: 'white',
|
|
652
|
+
focus: { bg: COLORS.btnFocus, fg: COLORS.btnFocusFg, bold: true },
|
|
653
|
+
hover: { bg: COLORS.btnFocus, fg: COLORS.btnFocusFg, bold: true },
|
|
654
|
+
},
|
|
655
|
+
});
|
|
656
|
+
btn.on('focus', () => {
|
|
657
|
+
const raw = btn.content.replace(/[►◄]/g, '').trim();
|
|
658
|
+
btn.setContent(`►${raw}◄`);
|
|
659
|
+
screen.render();
|
|
660
|
+
});
|
|
661
|
+
btn.on('blur', () => {
|
|
662
|
+
const raw = btn.content.replace(/[►◄]/g, '').trim();
|
|
663
|
+
btn.setContent(raw);
|
|
664
|
+
screen.render();
|
|
665
|
+
});
|
|
666
|
+
btn.key(['enter', 'space'], () => onClick());
|
|
667
|
+
btn.on('click', () => onClick());
|
|
668
|
+
return btn;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const saveBtn = _modalBtn('Save', 4, () => {
|
|
672
|
+
// Only save fields that differ from global
|
|
673
|
+
const toSave = {};
|
|
674
|
+
if (draft.voice && draft.voice !== globalCfg.voice) toSave.voice = draft.voice;
|
|
675
|
+
if (draft.pretext !== AgentVoiceStore.getDefaultPretext(agent.displayName, agent.title)) toSave.pretext = draft.pretext;
|
|
676
|
+
if (draft.reverbPreset !== (globalCfg.effects?.reverbPreset || 'light')) toSave.reverbPreset = draft.reverbPreset;
|
|
677
|
+
if (draft.personality !== (globalCfg.personality || 'none')) toSave.personality = draft.personality;
|
|
678
|
+
if (draft.backgroundMusic.track !== (globalCfg.backgroundMusic?.track || '') ||
|
|
679
|
+
draft.backgroundMusic.volume !== (globalCfg.backgroundMusic?.volume ?? 70) ||
|
|
680
|
+
draft.backgroundMusic.enabled !== (globalCfg.backgroundMusic?.enabled ?? false)) {
|
|
681
|
+
toSave.backgroundMusic = draft.backgroundMusic;
|
|
682
|
+
}
|
|
683
|
+
voiceStore.setAgentProfile(agent.id, toSave);
|
|
684
|
+
_closeModal();
|
|
685
|
+
refreshDisplay();
|
|
686
|
+
// Show temporary "Saved!" toast
|
|
687
|
+
_showSavedToast(agent.displayName);
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
const resetAllBtn = _modalBtn('Reset to Defaults', 14, () => {
|
|
691
|
+
voiceStore.resetAgentProfile(agent.id);
|
|
692
|
+
_closeModal();
|
|
693
|
+
refreshDisplay();
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
const cancelBtn = _modalBtn('Cancel', 38, _closeModal);
|
|
697
|
+
|
|
698
|
+
function _closeModal() {
|
|
699
|
+
if (_closed) return;
|
|
700
|
+
_closed = true;
|
|
701
|
+
_killPreview();
|
|
702
|
+
navigationService?.closeModal();
|
|
703
|
+
destroyList(modal, screen);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Field editing via Enter
|
|
707
|
+
fieldList.key(['enter'], () => {
|
|
708
|
+
const idx = fieldList.selected;
|
|
709
|
+
const field = FIELDS[idx];
|
|
710
|
+
if (!field) return;
|
|
711
|
+
|
|
712
|
+
switch (field.key) {
|
|
713
|
+
case 'voice':
|
|
714
|
+
_openVoicePickerForAgent(agent, draft, () => {
|
|
715
|
+
fieldList.setItems(_fieldItems());
|
|
716
|
+
fieldList.select(idx);
|
|
717
|
+
fieldList.focus();
|
|
718
|
+
screen.render();
|
|
719
|
+
});
|
|
720
|
+
break;
|
|
721
|
+
|
|
722
|
+
case 'pretext':
|
|
723
|
+
_openPretextEditor(modal, draft, () => {
|
|
724
|
+
fieldList.setItems(_fieldItems());
|
|
725
|
+
fieldList.select(idx);
|
|
726
|
+
fieldList.focus();
|
|
727
|
+
screen.render();
|
|
728
|
+
});
|
|
729
|
+
break;
|
|
730
|
+
|
|
731
|
+
case 'reverbPreset':
|
|
732
|
+
openReverbPicker(screen, draft.reverbPreset, (val) => {
|
|
733
|
+
draft.reverbPreset = val;
|
|
734
|
+
fieldList.setItems(_fieldItems());
|
|
735
|
+
fieldList.select(idx);
|
|
736
|
+
fieldList.focus();
|
|
737
|
+
screen.render();
|
|
738
|
+
}, () => {
|
|
739
|
+
fieldList.focus();
|
|
740
|
+
screen.render();
|
|
741
|
+
}, { applyToEffectsManager: false });
|
|
742
|
+
break;
|
|
743
|
+
|
|
744
|
+
case 'personality':
|
|
745
|
+
openPersonalityPicker(screen, draft.personality, (val) => {
|
|
746
|
+
draft.personality = val;
|
|
747
|
+
fieldList.setItems(_fieldItems());
|
|
748
|
+
fieldList.select(idx);
|
|
749
|
+
fieldList.focus();
|
|
750
|
+
screen.render();
|
|
751
|
+
}, () => {
|
|
752
|
+
fieldList.focus();
|
|
753
|
+
screen.render();
|
|
754
|
+
});
|
|
755
|
+
break;
|
|
756
|
+
|
|
757
|
+
case 'music':
|
|
758
|
+
openTrackPicker(screen, draft.backgroundMusic.track, draft.backgroundMusic.volume, (track, volume) => {
|
|
759
|
+
draft.backgroundMusic.track = track;
|
|
760
|
+
draft.backgroundMusic.volume = volume;
|
|
761
|
+
draft.backgroundMusic.enabled = true;
|
|
762
|
+
fieldList.setItems(_fieldItems());
|
|
763
|
+
fieldList.select(idx);
|
|
764
|
+
fieldList.focus();
|
|
765
|
+
screen.render();
|
|
766
|
+
}, () => {
|
|
767
|
+
fieldList.focus();
|
|
768
|
+
screen.render();
|
|
769
|
+
});
|
|
770
|
+
break;
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
// Space = sample with current draft
|
|
775
|
+
fieldList.key(['space'], () => {
|
|
776
|
+
const draftAgent = { ...agent };
|
|
777
|
+
// Temporarily set profile for sampling
|
|
778
|
+
_sampleAgentWithDraft(draftAgent, draft);
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
// Escape = close
|
|
782
|
+
fieldList.key(['escape', 'q'], _closeModal);
|
|
783
|
+
saveBtn.key(['escape'], _closeModal);
|
|
784
|
+
resetAllBtn.key(['escape'], _closeModal);
|
|
785
|
+
cancelBtn.key(['escape'], _closeModal);
|
|
786
|
+
|
|
787
|
+
// Tab navigation within modal
|
|
788
|
+
fieldList.key(['tab'], () => { saveBtn.focus(); screen.render(); });
|
|
789
|
+
saveBtn.key(['tab'], () => { resetAllBtn.focus(); screen.render(); });
|
|
790
|
+
resetAllBtn.key(['tab'], () => { cancelBtn.focus(); screen.render(); });
|
|
791
|
+
cancelBtn.key(['tab'], () => { fieldList.focus(); screen.render(); });
|
|
792
|
+
|
|
793
|
+
fieldList.focus();
|
|
794
|
+
screen.render();
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// -------------------------------------------------------------------------
|
|
798
|
+
// Voice picker for agent detail panel
|
|
799
|
+
|
|
800
|
+
function _openVoicePickerForAgent(agent, draft, onDone) {
|
|
801
|
+
let _allVoices = [];
|
|
802
|
+
let _filterText = '';
|
|
803
|
+
let _previewProc = null;
|
|
804
|
+
let _previewVoiceId = null;
|
|
805
|
+
let _vpClosed = false;
|
|
806
|
+
|
|
807
|
+
const _spawnEnv = buildAudioEnv();
|
|
808
|
+
|
|
809
|
+
function _killVP() {
|
|
810
|
+
if (_previewProc) {
|
|
811
|
+
try {
|
|
812
|
+
if (process.platform === 'win32') {
|
|
813
|
+
_previewProc.kill();
|
|
814
|
+
} else {
|
|
815
|
+
process.kill(-_previewProc.pid, 'SIGTERM');
|
|
816
|
+
}
|
|
817
|
+
} catch {}
|
|
818
|
+
_previewProc = null;
|
|
819
|
+
}
|
|
820
|
+
_previewVoiceId = null;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function _closeVP() {
|
|
824
|
+
if (_vpClosed) return;
|
|
825
|
+
_vpClosed = true;
|
|
826
|
+
_killVP();
|
|
827
|
+
destroyList(vpModal, screen, onDone);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const vpModal = blessed.box({
|
|
831
|
+
parent: screen,
|
|
832
|
+
top: '6%',
|
|
833
|
+
left: '3%',
|
|
834
|
+
width: '94%',
|
|
835
|
+
height: '88%',
|
|
836
|
+
border: { type: 'line' },
|
|
837
|
+
tags: true,
|
|
838
|
+
label: _modalTitle(`Select Voice for ${agent.icon || ''} ${agent.displayName}`),
|
|
839
|
+
style: {
|
|
840
|
+
fg: COLORS.labelFg,
|
|
841
|
+
bg: COLORS.contentBg,
|
|
842
|
+
border: { fg: 'bright-cyan' },
|
|
843
|
+
},
|
|
844
|
+
});
|
|
845
|
+
vpModal.setFront();
|
|
846
|
+
|
|
847
|
+
// Search
|
|
848
|
+
blessed.text({
|
|
849
|
+
parent: vpModal, top: 1, left: 2,
|
|
850
|
+
content: 'Search:', style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
|
|
851
|
+
});
|
|
852
|
+
const vpSearch = blessed.textbox({
|
|
853
|
+
parent: vpModal, top: 1, left: 11, width: 40, height: 1,
|
|
854
|
+
inputOnFocus: true, keys: true,
|
|
855
|
+
style: { fg: COLORS.valueFg, bg: '#1a3a5c', focus: { bg: '#245a80' } },
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
// Column header
|
|
859
|
+
const COL_N = 28;
|
|
860
|
+
const COL_G = 10;
|
|
861
|
+
blessed.text({
|
|
862
|
+
parent: vpModal, top: 2, left: 6, tags: true,
|
|
863
|
+
content: `{bright-cyan-fg}${'Name'.padEnd(COL_N)}${'Gender'.padEnd(COL_G)}Provider{/bright-cyan-fg}`,
|
|
864
|
+
style: { bg: COLORS.contentBg },
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
const vpList = blessed.list({
|
|
868
|
+
parent: vpModal, top: 3, left: 2, right: 2, bottom: 5,
|
|
869
|
+
keys: true, vi: true, mouse: true,
|
|
870
|
+
border: { type: 'line' },
|
|
871
|
+
scrollbar: { ch: '│', style: { fg: COLORS.borderFg } },
|
|
872
|
+
style: {
|
|
873
|
+
fg: COLORS.labelFg, bg: COLORS.contentBg,
|
|
874
|
+
border: { fg: COLORS.borderFg },
|
|
875
|
+
selected: { bg: '#2e7d32', fg: '#ffffff', bold: true },
|
|
876
|
+
item: { fg: COLORS.labelFg },
|
|
877
|
+
},
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
const vpInfoLine = blessed.text({
|
|
881
|
+
parent: vpModal, bottom: 4, left: 2, right: 2, tags: true,
|
|
882
|
+
content: '', style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
const vpPreviewLine = blessed.text({
|
|
886
|
+
parent: vpModal, bottom: 3, left: 2, right: 2, tags: true,
|
|
887
|
+
content: '', style: { fg: 'bright-cyan', bg: COLORS.contentBg },
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
blessed.text({
|
|
891
|
+
parent: vpModal, bottom: 2, left: 2, right: 2, tags: true,
|
|
892
|
+
content: '{#455a64-fg}[↑↓/jk] Navigate [Enter] Select [Space] Preview [/] Search [Esc] Cancel{/#455a64-fg}',
|
|
893
|
+
style: { bg: COLORS.contentBg },
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
function _getFiltered() {
|
|
897
|
+
if (!_filterText) return _allVoices;
|
|
898
|
+
const f = _filterText.toLowerCase();
|
|
899
|
+
return _allVoices.filter(v => v.toLowerCase().includes(f));
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function _buildVoiceItems(voices) {
|
|
903
|
+
return voices.map(v => {
|
|
904
|
+
const isActive = v === draft.voice;
|
|
905
|
+
const isPrev = v === _previewVoiceId;
|
|
906
|
+
const dot = isPrev ? '♪' : (isActive ? '●' : ' ');
|
|
907
|
+
const meta = getVoiceMeta(v);
|
|
908
|
+
const name = meta.displayName.length > COL_N
|
|
909
|
+
? meta.displayName.slice(0, COL_N - 1) + '…'
|
|
910
|
+
: meta.displayName.padEnd(COL_N);
|
|
911
|
+
return ` ${dot} ${name}${meta.gender.padEnd(COL_G)}${meta.provider}`;
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function _refreshVP() {
|
|
916
|
+
if (_vpClosed) return;
|
|
917
|
+
_allVoices = scanInstalledVoices();
|
|
918
|
+
const filtered = _getFiltered();
|
|
919
|
+
const items = _buildVoiceItems(filtered);
|
|
920
|
+
vpList.setItems(items.length > 0 ? items : [' (no voices found)']);
|
|
921
|
+
screen.render();
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function _previewVoice(voiceId) {
|
|
925
|
+
if (_previewVoiceId === voiceId) { _killVP(); vpPreviewLine.setContent(''); screen.render(); return; }
|
|
926
|
+
_killVP();
|
|
927
|
+
|
|
928
|
+
const _isWin = process.platform === 'win32' && !process.env.WSL_DISTRO_NAME;
|
|
929
|
+
|
|
930
|
+
const _ms = parseMultiSpeaker(voiceId);
|
|
931
|
+
const voicePath = path.resolve(PIPER_VOICES_DIR, _ms.model + '.onnx');
|
|
932
|
+
const safeBase = path.resolve(PIPER_VOICES_DIR);
|
|
933
|
+
if (!voicePath.startsWith(safeBase + path.sep) && voicePath !== safeBase) return;
|
|
934
|
+
|
|
935
|
+
const tempWav = _secureTempWav('vp');
|
|
936
|
+
const phrase = SAMPLE_PHRASES[Math.floor(Math.random() * SAMPLE_PHRASES.length)];
|
|
937
|
+
|
|
938
|
+
const _piperBin = _resolvePiperBin();
|
|
939
|
+
|
|
940
|
+
const args = ['--model', voicePath, '--output_file', tempWav];
|
|
941
|
+
if (_ms.speakerId != null) args.push('--speaker', String(_ms.speakerId));
|
|
942
|
+
const piper = spawn(_piperBin, args, {
|
|
943
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
944
|
+
detached: !_isWin,
|
|
945
|
+
windowsHide: true,
|
|
946
|
+
env: _spawnEnv,
|
|
947
|
+
});
|
|
948
|
+
piper.stdin.write(phrase + '\n');
|
|
949
|
+
piper.stdin.end();
|
|
950
|
+
_previewProc = piper;
|
|
951
|
+
_previewVoiceId = voiceId;
|
|
952
|
+
|
|
953
|
+
if (!_vpClosed) {
|
|
954
|
+
vpPreviewLine.setContent(`{bright-cyan-fg}♪ Synthesizing: ${voiceId}...{/bright-cyan-fg}`);
|
|
955
|
+
screen.render();
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
piper.on('exit', (code) => {
|
|
959
|
+
if (_previewVoiceId !== voiceId) { try { fs.unlinkSync(tempWav); } catch {} return; }
|
|
960
|
+
if (code !== 0) { _previewProc = null; _previewVoiceId = null; return; }
|
|
961
|
+
const wp = detectWavPlayer(_spawnEnv);
|
|
962
|
+
if (!wp) return;
|
|
963
|
+
const pp = spawn(wp.bin, wp.args(tempWav), {
|
|
964
|
+
stdio: 'ignore',
|
|
965
|
+
detached: !_isWin,
|
|
966
|
+
windowsHide: true,
|
|
967
|
+
env: _spawnEnv,
|
|
968
|
+
});
|
|
969
|
+
_previewProc = pp;
|
|
970
|
+
if (!_vpClosed) { vpPreviewLine.setContent(`{bright-cyan-fg}♪ Playing: ${voiceId}{/bright-cyan-fg}`); screen.render(); }
|
|
971
|
+
pp.on('exit', () => {
|
|
972
|
+
if (_previewVoiceId === voiceId) { _previewVoiceId = null; _previewProc = null; if (!_vpClosed) { vpPreviewLine.setContent(''); screen.render(); } }
|
|
973
|
+
try { fs.unlinkSync(tempWav); } catch {}
|
|
974
|
+
});
|
|
975
|
+
});
|
|
976
|
+
piper.on('error', () => { _previewProc = null; _previewVoiceId = null; });
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
vpSearch.on('keypress', () => {
|
|
980
|
+
setTimeout(() => { _filterText = vpSearch.getValue().trim(); _refreshVP(); }, 0);
|
|
981
|
+
});
|
|
982
|
+
vpSearch.key(['escape'], () => { vpList.focus(); screen.render(); });
|
|
983
|
+
vpList.key(['/'], () => { vpSearch.clearValue(); vpSearch.focus(); screen.render(); });
|
|
984
|
+
vpList.key(['enter'], () => {
|
|
985
|
+
const filtered = _getFiltered();
|
|
986
|
+
const sel = filtered[vpList.selected];
|
|
987
|
+
if (sel) { draft.voice = sel; _closeVP(); }
|
|
988
|
+
});
|
|
989
|
+
vpList.key(['space'], () => {
|
|
990
|
+
const filtered = _getFiltered();
|
|
991
|
+
const sel = filtered[vpList.selected];
|
|
992
|
+
if (sel) _previewVoice(sel);
|
|
993
|
+
});
|
|
994
|
+
vpList.key(['escape', 'q'], _closeVP);
|
|
995
|
+
|
|
996
|
+
_refreshVP();
|
|
997
|
+
const activeIdx = _getFiltered().indexOf(draft.voice);
|
|
998
|
+
if (activeIdx >= 0) vpList.select(activeIdx);
|
|
999
|
+
vpList.focus();
|
|
1000
|
+
screen.render();
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// -------------------------------------------------------------------------
|
|
1004
|
+
// Pretext inline editor
|
|
1005
|
+
|
|
1006
|
+
function _openPretextEditor(parentModal, draft, onDone) {
|
|
1007
|
+
const editModal = blessed.box({
|
|
1008
|
+
parent: screen,
|
|
1009
|
+
top: 'center',
|
|
1010
|
+
left: 'center',
|
|
1011
|
+
width: 60,
|
|
1012
|
+
height: 8,
|
|
1013
|
+
border: { type: 'line' },
|
|
1014
|
+
tags: true,
|
|
1015
|
+
label: _modalTitle('Edit Pretext'),
|
|
1016
|
+
style: { fg: COLORS.labelFg, bg: COLORS.contentBg, border: { fg: 'bright-cyan' } },
|
|
1017
|
+
});
|
|
1018
|
+
editModal.setFront();
|
|
1019
|
+
|
|
1020
|
+
blessed.text({
|
|
1021
|
+
parent: editModal, top: 1, left: 2,
|
|
1022
|
+
content: 'Agent pretext (spoken before each TTS message):',
|
|
1023
|
+
style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
const inputBox = blessed.textbox({
|
|
1027
|
+
parent: editModal, top: 3, left: 2, right: 2, height: 3,
|
|
1028
|
+
border: { type: 'line' },
|
|
1029
|
+
inputOnFocus: true,
|
|
1030
|
+
value: draft.pretext,
|
|
1031
|
+
style: {
|
|
1032
|
+
fg: COLORS.valueFg, bg: '#0d1b35',
|
|
1033
|
+
border: { fg: COLORS.borderFg },
|
|
1034
|
+
focus: { border: { fg: 'bright-cyan' } },
|
|
1035
|
+
},
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
let _editClosed = false;
|
|
1039
|
+
function _closeEdit(save) {
|
|
1040
|
+
if (_editClosed) return;
|
|
1041
|
+
_editClosed = true;
|
|
1042
|
+
if (save) {
|
|
1043
|
+
const raw = inputBox.getValue().trim();
|
|
1044
|
+
// M7: enforce max pretext length
|
|
1045
|
+
draft.pretext = (raw || draft.pretext).slice(0, MAX_PRETEXT_LENGTH);
|
|
1046
|
+
}
|
|
1047
|
+
destroyList(editModal, screen, onDone);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
inputBox.key(['enter'], () => _closeEdit(true));
|
|
1051
|
+
inputBox.key(['escape'], () => _closeEdit(false));
|
|
1052
|
+
|
|
1053
|
+
inputBox.focus();
|
|
1054
|
+
screen.render();
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// -------------------------------------------------------------------------
|
|
1058
|
+
// Sample agent with a draft profile (no save) — same full pipeline
|
|
1059
|
+
|
|
1060
|
+
function _sampleAgentWithDraft(agent, draft) {
|
|
1061
|
+
_sampleWithFullProfile(agent, draft);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// -------------------------------------------------------------------------
|
|
1065
|
+
// Shared: sample with full profile via play-tts-enhanced.sh
|
|
1066
|
+
// Writes a temp agent profile JSON, then calls the enhanced TTS pipeline
|
|
1067
|
+
// which applies voice + reverb + background music.
|
|
1068
|
+
|
|
1069
|
+
function _sampleWithFullProfile(agent, profile) {
|
|
1070
|
+
_killPreview();
|
|
1071
|
+
const gen = ++_playGeneration;
|
|
1072
|
+
|
|
1073
|
+
// Start spinner on the agent's row in the list
|
|
1074
|
+
const agentIdx = _agents.findIndex(a => a.id === agent.id);
|
|
1075
|
+
if (agentIdx >= 0) _startSpinner(agentIdx);
|
|
1076
|
+
|
|
1077
|
+
const voiceId = profile.voice || '';
|
|
1078
|
+
const pretext = profile.pretext || AgentVoiceStore.getDefaultPretext(agent.displayName, agent.title);
|
|
1079
|
+
const phrase = `${pretext} ${SAMPLE_PHRASES[Math.floor(Math.random() * SAMPLE_PHRASES.length)]}`;
|
|
1080
|
+
|
|
1081
|
+
const isWindows = process.platform === 'win32' && !process.env.WSL_DISTRO_NAME;
|
|
1082
|
+
|
|
1083
|
+
if (isWindows) {
|
|
1084
|
+
// On Windows, synthesize with piper.exe directly then play the wav,
|
|
1085
|
+
// avoiding bash/wsl.exe which opens a visible console window.
|
|
1086
|
+
_sampleWithPiperDirect(gen, voiceId, phrase);
|
|
1087
|
+
} else {
|
|
1088
|
+
// On Linux/macOS/WSL, use play-tts.sh
|
|
1089
|
+
const _spawnEnv = buildAudioEnv();
|
|
1090
|
+
const scriptDir = path.join(_projectRoot, '.claude', 'hooks');
|
|
1091
|
+
const plainScript = path.join(scriptDir, 'play-tts.sh');
|
|
1092
|
+
const args = [plainScript, phrase];
|
|
1093
|
+
if (voiceId) args.push(voiceId);
|
|
1094
|
+
|
|
1095
|
+
const proc = spawn('bash', args, {
|
|
1096
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
1097
|
+
detached: true,
|
|
1098
|
+
env: { ..._spawnEnv },
|
|
1099
|
+
cwd: _projectRoot,
|
|
1100
|
+
});
|
|
1101
|
+
_playingProcess = proc;
|
|
1102
|
+
|
|
1103
|
+
proc.on('exit', () => {
|
|
1104
|
+
if (gen === _playGeneration) { _playingProcess = null; _stopSpinner(); }
|
|
1105
|
+
});
|
|
1106
|
+
proc.on('error', () => {
|
|
1107
|
+
if (gen === _playGeneration) { _playingProcess = null; _stopSpinner(); }
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
/** Windows-native sample: piper.exe → wav → detectWavPlayer */
|
|
1113
|
+
function _sampleWithPiperDirect(gen, voiceId, phrase) {
|
|
1114
|
+
const _spawnEnv = buildAudioEnv();
|
|
1115
|
+
const piperBin = _resolvePiperBin();
|
|
1116
|
+
|
|
1117
|
+
// Resolve voice model path
|
|
1118
|
+
const ms = parseMultiSpeaker(voiceId);
|
|
1119
|
+
const voicePath = path.resolve(PIPER_VOICES_DIR, ms.model + '.onnx');
|
|
1120
|
+
const safeBase = path.resolve(PIPER_VOICES_DIR);
|
|
1121
|
+
if (!voicePath.startsWith(safeBase + path.sep) && voicePath !== safeBase) {
|
|
1122
|
+
_stopSpinner();
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
const tempWav = path.join(os.tmpdir(), `agentvibes-agent-preview-${Date.now()}.wav`);
|
|
1127
|
+
const piperArgs = ['--model', voicePath, '--output_file', tempWav];
|
|
1128
|
+
if (ms.speakerId != null) piperArgs.push('--speaker', String(ms.speakerId));
|
|
1129
|
+
|
|
1130
|
+
const piper = spawn(piperBin, piperArgs, {
|
|
1131
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
1132
|
+
detached: false,
|
|
1133
|
+
windowsHide: true,
|
|
1134
|
+
env: _spawnEnv,
|
|
1135
|
+
});
|
|
1136
|
+
piper.stdin.write(phrase + '\n');
|
|
1137
|
+
piper.stdin.end();
|
|
1138
|
+
_playingProcess = piper;
|
|
1139
|
+
|
|
1140
|
+
piper.on('exit', (code) => {
|
|
1141
|
+
// Generation changed — another preview was triggered; clean up silently
|
|
1142
|
+
if (gen !== _playGeneration) {
|
|
1143
|
+
try { fs.unlinkSync(tempWav); } catch {}
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
if (code !== 0) {
|
|
1147
|
+
_playingProcess = null;
|
|
1148
|
+
_stopSpinner();
|
|
1149
|
+
try { fs.unlinkSync(tempWav); } catch {}
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// Re-check generation after piper exit to close the race window (#154)
|
|
1154
|
+
if (gen !== _playGeneration) {
|
|
1155
|
+
try { fs.unlinkSync(tempWav); } catch {}
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// Play the synthesized wav
|
|
1160
|
+
const wavPlayer = detectWavPlayer(_spawnEnv);
|
|
1161
|
+
if (!wavPlayer) {
|
|
1162
|
+
_playingProcess = null;
|
|
1163
|
+
_stopSpinner();
|
|
1164
|
+
try { fs.unlinkSync(tempWav); } catch {}
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
const playProc = spawn(wavPlayer.bin, wavPlayer.args(tempWav), {
|
|
1168
|
+
stdio: 'ignore',
|
|
1169
|
+
detached: false,
|
|
1170
|
+
windowsHide: true,
|
|
1171
|
+
env: _spawnEnv,
|
|
1172
|
+
});
|
|
1173
|
+
_playingProcess = playProc;
|
|
1174
|
+
|
|
1175
|
+
playProc.on('exit', () => {
|
|
1176
|
+
if (gen === _playGeneration) { _playingProcess = null; _stopSpinner(); }
|
|
1177
|
+
try { fs.unlinkSync(tempWav); } catch {}
|
|
1178
|
+
});
|
|
1179
|
+
playProc.on('error', () => {
|
|
1180
|
+
if (gen === _playGeneration) { _playingProcess = null; _stopSpinner(); }
|
|
1181
|
+
try { fs.unlinkSync(tempWav); } catch {}
|
|
1182
|
+
});
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
piper.on('error', () => {
|
|
1186
|
+
if (gen === _playGeneration) { _playingProcess = null; _stopSpinner(); }
|
|
1187
|
+
try { fs.unlinkSync(tempWav); } catch {}
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// -------------------------------------------------------------------------
|
|
1192
|
+
// Auto-assign helpers
|
|
1193
|
+
|
|
1194
|
+
function _shuffleArray(arr) {
|
|
1195
|
+
const a = [...arr];
|
|
1196
|
+
for (let i = a.length - 1; i > 0; i--) {
|
|
1197
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
1198
|
+
[a[i], a[j]] = [a[j], a[i]];
|
|
1199
|
+
}
|
|
1200
|
+
return a;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// Common first-name → gender map for gender-aware auto-assign.
|
|
1204
|
+
// Only needs to cover names likely used as BMAD agent display names.
|
|
1205
|
+
// Ambiguous names (sam, charlie, dana, max, pat, etc.) are intentionally
|
|
1206
|
+
// omitted so they fall through to the gender-neutral 'other' pool (#156).
|
|
1207
|
+
const _NAME_GENDER = {
|
|
1208
|
+
// Female
|
|
1209
|
+
amelia: 'Female', amy: 'Female', anna: 'Female', betty: 'Female',
|
|
1210
|
+
claire: 'Female', emma: 'Female', faye: 'Female',
|
|
1211
|
+
grace: 'Female', heather: 'Female', ivy: 'Female', jane: 'Female',
|
|
1212
|
+
jenny: 'Female', julia: 'Female', kate: 'Female', laura: 'Female',
|
|
1213
|
+
lily: 'Female', maria: 'Female', mary: 'Female', nina: 'Female',
|
|
1214
|
+
olivia: 'Female', paige: 'Female', rachel: 'Female', sally: 'Female',
|
|
1215
|
+
sara: 'Female', sarah: 'Female', sophie: 'Female', tina: 'Female',
|
|
1216
|
+
wendy: 'Female', zoe: 'Female',
|
|
1217
|
+
// Male
|
|
1218
|
+
alan: 'Male', barry: 'Male', bob: 'Male', carl: 'Male',
|
|
1219
|
+
dan: 'Male', david: 'Male', eric: 'Male',
|
|
1220
|
+
frank: 'Male', george: 'Male', hank: 'Male', jack: 'Male',
|
|
1221
|
+
james: 'Male', joe: 'Male', john: 'Male', kevin: 'Male',
|
|
1222
|
+
leo: 'Male', mark: 'Male', murat: 'Male',
|
|
1223
|
+
nick: 'Male', oscar: 'Male', paul: 'Male', ray: 'Male',
|
|
1224
|
+
ryan: 'Male', saif: 'Male', steve: 'Male',
|
|
1225
|
+
tom: 'Male', victor: 'Male', winston: 'Male', zach: 'Male',
|
|
1226
|
+
};
|
|
1227
|
+
|
|
1228
|
+
/** Infer agent gender from display name (first word). */
|
|
1229
|
+
function _inferAgentGender(agent) {
|
|
1230
|
+
const firstName = (agent.displayName || '').split(/[\s(]/)[0].toLowerCase();
|
|
1231
|
+
return _NAME_GENDER[firstName] || null;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
function _autoAssignVoices() {
|
|
1235
|
+
const installed = scanInstalledVoices();
|
|
1236
|
+
if (installed.length === 0) return false;
|
|
1237
|
+
|
|
1238
|
+
// Separate voices by gender
|
|
1239
|
+
const femaleVoices = _shuffleArray(installed.filter(v => getVoiceMeta(v).gender === 'Female'));
|
|
1240
|
+
const maleVoices = _shuffleArray(installed.filter(v => getVoiceMeta(v).gender === 'Male'));
|
|
1241
|
+
const otherVoices = _shuffleArray(installed.filter(v => !['Male', 'Female'].includes(getVoiceMeta(v).gender)));
|
|
1242
|
+
|
|
1243
|
+
// Separate agents by gender
|
|
1244
|
+
const femaleAgents = _agents.filter(a => _inferAgentGender(a) === 'Female');
|
|
1245
|
+
const maleAgents = _agents.filter(a => _inferAgentGender(a) === 'Male');
|
|
1246
|
+
const otherAgents = _agents.filter(a => !_inferAgentGender(a));
|
|
1247
|
+
|
|
1248
|
+
const usedVoices = new Set();
|
|
1249
|
+
|
|
1250
|
+
// Assign matching-gender voices first, then fall back to any available
|
|
1251
|
+
function assignGroup(agents, preferredPool, fallbackPools) {
|
|
1252
|
+
const allPools = [preferredPool, ...fallbackPools];
|
|
1253
|
+
let reuseIdx = 0;
|
|
1254
|
+
agents.forEach(agent => {
|
|
1255
|
+
let voice = null;
|
|
1256
|
+
for (const pool of allPools) {
|
|
1257
|
+
voice = pool.find(v => !usedVoices.has(v));
|
|
1258
|
+
if (voice) break;
|
|
1259
|
+
}
|
|
1260
|
+
// If all unique voices exhausted, round-robin reuse from preferred pool
|
|
1261
|
+
if (!voice && preferredPool.length > 0) {
|
|
1262
|
+
voice = preferredPool[reuseIdx % preferredPool.length];
|
|
1263
|
+
reuseIdx++;
|
|
1264
|
+
}
|
|
1265
|
+
if (voice) {
|
|
1266
|
+
usedVoices.add(voice);
|
|
1267
|
+
voiceStore.setAgentProfile(agent.id, { voice });
|
|
1268
|
+
}
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
assignGroup(femaleAgents, femaleVoices, [otherVoices, maleVoices]);
|
|
1273
|
+
assignGroup(maleAgents, maleVoices, [otherVoices, femaleVoices]);
|
|
1274
|
+
assignGroup(otherAgents, otherVoices, [maleVoices, femaleVoices]);
|
|
1275
|
+
|
|
1276
|
+
return true;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
function _autoAssignMusic() {
|
|
1280
|
+
const tracksDir = path.join(_projectRoot, '.claude', 'audio', 'tracks');
|
|
1281
|
+
let tracks = [];
|
|
1282
|
+
try {
|
|
1283
|
+
tracks = fs.readdirSync(tracksDir).filter(f => /\.mp3$/i.test(f));
|
|
1284
|
+
} catch { /* no tracks dir */ }
|
|
1285
|
+
if (tracks.length === 0) return false;
|
|
1286
|
+
|
|
1287
|
+
const shuffled = _shuffleArray(tracks);
|
|
1288
|
+
_agents.forEach((agent, i) => {
|
|
1289
|
+
const track = shuffled[i % shuffled.length];
|
|
1290
|
+
const existing = voiceStore.getAgentProfile(agent.id);
|
|
1291
|
+
voiceStore.setAgentProfile(agent.id, {
|
|
1292
|
+
backgroundMusic: { track, volume: existing.backgroundMusic?.volume ?? 70, enabled: true },
|
|
1293
|
+
});
|
|
1294
|
+
});
|
|
1295
|
+
return true;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
function _autoAssignAll() {
|
|
1299
|
+
if (_agents.length === 0) return;
|
|
1300
|
+
const voiceOk = _autoAssignVoices();
|
|
1301
|
+
const musicOk = _autoAssignMusic();
|
|
1302
|
+
refreshDisplay();
|
|
1303
|
+
const msg = voiceOk && musicOk ? 'Voices and music auto-assigned'
|
|
1304
|
+
: voiceOk ? 'Voices auto-assigned' : 'Auto-assign: no voices found';
|
|
1305
|
+
_showSavedToast(msg);
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
// -------------------------------------------------------------------------
|
|
1309
|
+
// Bulk edit menu
|
|
1310
|
+
|
|
1311
|
+
function _openBulkEditMenu() {
|
|
1312
|
+
const BULK_ACTIONS = [
|
|
1313
|
+
{ label: ' Randomize Voices (gender-aware)', key: 'voices' },
|
|
1314
|
+
{ label: ' Randomize Music (unique per agent)', key: 'music' },
|
|
1315
|
+
{ label: ' Randomize Both', key: 'both' },
|
|
1316
|
+
{ label: ' Set Same Music for All Agents...', key: 'setMusic' },
|
|
1317
|
+
{ label: ' Set Same Volume for All Agents...', key: 'setVolume' },
|
|
1318
|
+
{ label: ' Set Same Pretext for All Agents...', key: 'setPretext' },
|
|
1319
|
+
{ label: ' Set Same Reverb for All Agents...', key: 'setReverb' },
|
|
1320
|
+
{ label: ' Reset All Agent Profiles', key: 'resetAll' },
|
|
1321
|
+
];
|
|
1322
|
+
|
|
1323
|
+
const menuList = blessed.list({
|
|
1324
|
+
parent: screen,
|
|
1325
|
+
top: 'center',
|
|
1326
|
+
left: 'center',
|
|
1327
|
+
width: 52,
|
|
1328
|
+
height: BULK_ACTIONS.length + 4,
|
|
1329
|
+
border: { type: 'line' },
|
|
1330
|
+
tags: true,
|
|
1331
|
+
label: _modalTitle('Bulk Edit'),
|
|
1332
|
+
keys: true,
|
|
1333
|
+
vi: true,
|
|
1334
|
+
mouse: true,
|
|
1335
|
+
items: BULK_ACTIONS.map(a => a.label),
|
|
1336
|
+
style: {
|
|
1337
|
+
fg: COLORS.labelFg,
|
|
1338
|
+
bg: COLORS.contentBg,
|
|
1339
|
+
border: { fg: COLORS.btnFocus },
|
|
1340
|
+
selected: { bg: '#4a148c', fg: COLORS.activeFg, bold: true },
|
|
1341
|
+
item: { fg: COLORS.labelFg },
|
|
1342
|
+
},
|
|
1343
|
+
});
|
|
1344
|
+
|
|
1345
|
+
blessed.text({
|
|
1346
|
+
parent: menuList,
|
|
1347
|
+
bottom: -1,
|
|
1348
|
+
left: 1,
|
|
1349
|
+
width: 48,
|
|
1350
|
+
height: 1,
|
|
1351
|
+
tags: true,
|
|
1352
|
+
content: '{#455a64-fg}[Enter] Select [Esc] Cancel{/#455a64-fg}',
|
|
1353
|
+
style: { bg: COLORS.contentBg },
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1356
|
+
menuList.setFront();
|
|
1357
|
+
menuList.focus();
|
|
1358
|
+
screen.render();
|
|
1359
|
+
|
|
1360
|
+
let _menuClosed = false;
|
|
1361
|
+
function _closeMenu(callback) {
|
|
1362
|
+
if (_menuClosed) return;
|
|
1363
|
+
_menuClosed = true;
|
|
1364
|
+
destroyList(menuList, screen, callback);
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
menuList.key(['enter'], () => {
|
|
1368
|
+
const action = BULK_ACTIONS[menuList.selected];
|
|
1369
|
+
if (!action) return;
|
|
1370
|
+
|
|
1371
|
+
switch (action.key) {
|
|
1372
|
+
case 'voices':
|
|
1373
|
+
_closeMenu(() => {
|
|
1374
|
+
if (_autoAssignVoices()) { refreshDisplay(); _showSavedToast('Voices randomized'); }
|
|
1375
|
+
});
|
|
1376
|
+
break;
|
|
1377
|
+
|
|
1378
|
+
case 'music':
|
|
1379
|
+
_closeMenu(() => {
|
|
1380
|
+
if (_autoAssignMusic()) { refreshDisplay(); _showSavedToast('Music randomized'); }
|
|
1381
|
+
});
|
|
1382
|
+
break;
|
|
1383
|
+
|
|
1384
|
+
case 'both':
|
|
1385
|
+
_closeMenu(() => { _autoAssignAll(); });
|
|
1386
|
+
break;
|
|
1387
|
+
|
|
1388
|
+
case 'setMusic':
|
|
1389
|
+
_closeMenu(() => {
|
|
1390
|
+
openTrackPicker(screen, '', 70, (track, volume) => {
|
|
1391
|
+
_agents.forEach(agent => {
|
|
1392
|
+
const p = voiceStore.getAgentProfile(agent.id);
|
|
1393
|
+
voiceStore.setAgentProfile(agent.id, {
|
|
1394
|
+
backgroundMusic: { track, volume, enabled: true },
|
|
1395
|
+
});
|
|
1396
|
+
});
|
|
1397
|
+
refreshDisplay();
|
|
1398
|
+
_showSavedToast('Music set for all agents');
|
|
1399
|
+
agentList.focus();
|
|
1400
|
+
}, () => { agentList.focus(); screen.render(); });
|
|
1401
|
+
});
|
|
1402
|
+
break;
|
|
1403
|
+
|
|
1404
|
+
case 'setVolume':
|
|
1405
|
+
_closeMenu(() => {
|
|
1406
|
+
const curVol = voiceStore.getAgentProfile(_agents[0]?.id)?.backgroundMusic?.volume ?? 70;
|
|
1407
|
+
openVolumeInput(screen, curVol, (volume) => {
|
|
1408
|
+
_agents.forEach(agent => {
|
|
1409
|
+
const p = voiceStore.getAgentProfile(agent.id);
|
|
1410
|
+
const bm = p.backgroundMusic || {};
|
|
1411
|
+
voiceStore.setAgentProfile(agent.id, {
|
|
1412
|
+
backgroundMusic: { ...bm, volume },
|
|
1413
|
+
});
|
|
1414
|
+
});
|
|
1415
|
+
refreshDisplay();
|
|
1416
|
+
_showSavedToast(`Volume set to ${volume}% for all agents`);
|
|
1417
|
+
agentList.focus();
|
|
1418
|
+
}, () => { agentList.focus(); screen.render(); });
|
|
1419
|
+
});
|
|
1420
|
+
break;
|
|
1421
|
+
|
|
1422
|
+
case 'setPretext':
|
|
1423
|
+
_closeMenu(() => { _openBulkPretextEditor(); });
|
|
1424
|
+
break;
|
|
1425
|
+
|
|
1426
|
+
case 'setReverb':
|
|
1427
|
+
_closeMenu(() => {
|
|
1428
|
+
openReverbPicker(screen, '', (val) => {
|
|
1429
|
+
_agents.forEach(agent => voiceStore.setAgentProfile(agent.id, { reverbPreset: val }));
|
|
1430
|
+
refreshDisplay();
|
|
1431
|
+
_showSavedToast('Reverb set for all agents');
|
|
1432
|
+
agentList.focus();
|
|
1433
|
+
}, () => { agentList.focus(); screen.render(); }, { applyToEffectsManager: false });
|
|
1434
|
+
});
|
|
1435
|
+
break;
|
|
1436
|
+
|
|
1437
|
+
case 'resetAll':
|
|
1438
|
+
_closeMenu(() => {
|
|
1439
|
+
_agents.forEach(agent => voiceStore.resetAgentProfile(agent.id));
|
|
1440
|
+
refreshDisplay();
|
|
1441
|
+
_showSavedToast('All profiles reset');
|
|
1442
|
+
});
|
|
1443
|
+
break;
|
|
1444
|
+
}
|
|
1445
|
+
});
|
|
1446
|
+
|
|
1447
|
+
menuList.key(['escape', 'q'], () => {
|
|
1448
|
+
_closeMenu(() => { agentList.focus(); screen.render(); });
|
|
1449
|
+
});
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
function _openBulkPretextEditor() {
|
|
1453
|
+
const editModal = blessed.box({
|
|
1454
|
+
parent: screen,
|
|
1455
|
+
top: 'center',
|
|
1456
|
+
left: 'center',
|
|
1457
|
+
width: 60,
|
|
1458
|
+
height: 9,
|
|
1459
|
+
border: { type: 'line' },
|
|
1460
|
+
tags: true,
|
|
1461
|
+
label: _modalTitle('Set Pretext for All Agents'),
|
|
1462
|
+
style: { fg: COLORS.labelFg, bg: COLORS.contentBg, border: { fg: 'bright-cyan' } },
|
|
1463
|
+
});
|
|
1464
|
+
editModal.setFront();
|
|
1465
|
+
|
|
1466
|
+
blessed.text({
|
|
1467
|
+
parent: editModal, top: 1, left: 2,
|
|
1468
|
+
content: 'Pretext to apply to all agents (leave empty to clear):',
|
|
1469
|
+
style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
|
|
1470
|
+
});
|
|
1471
|
+
|
|
1472
|
+
const inputBox = blessed.textbox({
|
|
1473
|
+
parent: editModal, top: 3, left: 2, right: 2, height: 3,
|
|
1474
|
+
border: { type: 'line' },
|
|
1475
|
+
inputOnFocus: true,
|
|
1476
|
+
style: {
|
|
1477
|
+
fg: COLORS.valueFg, bg: '#0d1b35',
|
|
1478
|
+
border: { fg: COLORS.borderFg },
|
|
1479
|
+
focus: { border: { fg: 'bright-cyan' } },
|
|
1480
|
+
},
|
|
1481
|
+
});
|
|
1482
|
+
|
|
1483
|
+
blessed.text({
|
|
1484
|
+
parent: editModal, bottom: 1, left: 2, tags: true,
|
|
1485
|
+
content: '{#455a64-fg}[Enter] Apply to all [Esc] Cancel{/#455a64-fg}',
|
|
1486
|
+
style: { bg: COLORS.contentBg },
|
|
1487
|
+
});
|
|
1488
|
+
|
|
1489
|
+
let _closed = false;
|
|
1490
|
+
function _close(save) {
|
|
1491
|
+
if (_closed) return;
|
|
1492
|
+
_closed = true;
|
|
1493
|
+
if (save) {
|
|
1494
|
+
const raw = inputBox.getValue().trim().slice(0, MAX_PRETEXT_LENGTH);
|
|
1495
|
+
_agents.forEach(agent => {
|
|
1496
|
+
if (raw) {
|
|
1497
|
+
voiceStore.setAgentProfile(agent.id, { pretext: raw });
|
|
1498
|
+
} else {
|
|
1499
|
+
const p = voiceStore.getAgentProfile(agent.id);
|
|
1500
|
+
const { pretext: _removed, ...rest } = p;
|
|
1501
|
+
voiceStore.resetAgentProfile(agent.id);
|
|
1502
|
+
if (Object.keys(rest).length > 0) voiceStore.setAgentProfile(agent.id, rest);
|
|
1503
|
+
}
|
|
1504
|
+
});
|
|
1505
|
+
refreshDisplay();
|
|
1506
|
+
_showSavedToast('Pretext set for all agents');
|
|
1507
|
+
}
|
|
1508
|
+
destroyList(editModal, screen, () => { agentList.focus(); screen.render(); });
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
inputBox.key(['enter'], () => _close(true));
|
|
1512
|
+
inputBox.key(['escape'], () => _close(false));
|
|
1513
|
+
inputBox.focus();
|
|
1514
|
+
screen.render();
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
// -------------------------------------------------------------------------
|
|
1518
|
+
// Key bindings
|
|
1519
|
+
|
|
1520
|
+
agentList.key(['x', 'X'], () => {
|
|
1521
|
+
const agent = _agents[agentList.selected];
|
|
1522
|
+
if (agent) {
|
|
1523
|
+
voiceStore.resetAgentProfile(agent.id);
|
|
1524
|
+
refreshDisplay();
|
|
1525
|
+
}
|
|
1526
|
+
});
|
|
1527
|
+
|
|
1528
|
+
|
|
1529
|
+
agentList.key(['enter'], () => {
|
|
1530
|
+
const agent = _agents[agentList.selected];
|
|
1531
|
+
if (agent) _openAgentDetailPanel(agent);
|
|
1532
|
+
});
|
|
1533
|
+
|
|
1534
|
+
agentList.key(['space'], () => {
|
|
1535
|
+
const agent = _agents[agentList.selected];
|
|
1536
|
+
if (agent) _sampleAgent(agent);
|
|
1537
|
+
});
|
|
1538
|
+
|
|
1539
|
+
agentList.key(['a', 'A'], () => { _autoAssignAll(); });
|
|
1540
|
+
agentList.key(['b', 'B'], () => { _openBulkEditMenu(); });
|
|
1541
|
+
|
|
1542
|
+
// Type-to-jump
|
|
1543
|
+
const _agentJumpBlocked = new Set(['j', 'k', 'g', 'h', 'l', 'd', 'u', 'x', 'a', 'b']);
|
|
1544
|
+
agentList.on('keypress', (ch, key) => {
|
|
1545
|
+
if (!ch || key.ctrl || key.meta) return;
|
|
1546
|
+
const lower = ch.toLowerCase();
|
|
1547
|
+
if (!/^[a-z]$/.test(lower)) return;
|
|
1548
|
+
if (_agentJumpBlocked.has(lower)) return;
|
|
1549
|
+
const count = _agents.length;
|
|
1550
|
+
if (count === 0) return;
|
|
1551
|
+
const start = agentList.selected ?? 0;
|
|
1552
|
+
for (let i = 1; i <= count; i++) {
|
|
1553
|
+
const idx = (start + i) % count;
|
|
1554
|
+
const name = (_agents[idx]?.displayName ?? '').toLowerCase();
|
|
1555
|
+
if (name.startsWith(lower)) {
|
|
1556
|
+
agentList.select(idx);
|
|
1557
|
+
screen.render();
|
|
1558
|
+
break;
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
});
|
|
1562
|
+
|
|
1563
|
+
// Inline row hint (appended to selected row while list is focused)
|
|
1564
|
+
let _listFocused = false;
|
|
1565
|
+
let _hintIdx = -1;
|
|
1566
|
+
let _hintBase = ''; // row content before hint was appended (no hint, no █)
|
|
1567
|
+
|
|
1568
|
+
function _updateHint(idx) {
|
|
1569
|
+
const items = agentList.items;
|
|
1570
|
+
if (_hintIdx >= 0 && _hintIdx !== idx && items[_hintIdx]) {
|
|
1571
|
+
const hadBlink = (items[_hintIdx].content ?? '').endsWith(' █');
|
|
1572
|
+
items[_hintIdx].setContent(hadBlink ? _hintBase + ' █' : _hintBase);
|
|
1573
|
+
}
|
|
1574
|
+
if (idx >= 0 && items[idx]) {
|
|
1575
|
+
let c = items[idx].content ?? '';
|
|
1576
|
+
const hasBlink = c.endsWith(' █');
|
|
1577
|
+
if (hasBlink) c = c.slice(0, -3);
|
|
1578
|
+
_hintBase = c;
|
|
1579
|
+
items[idx].setContent(c + _ROW_HINT_BMAD + (hasBlink ? ' █' : ''));
|
|
1580
|
+
} else {
|
|
1581
|
+
_hintBase = '';
|
|
1582
|
+
}
|
|
1583
|
+
_hintIdx = idx;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
// Blinking cursor
|
|
1587
|
+
let _alBlink = { interval: null, on: false, sel: -1 };
|
|
1588
|
+
function _alTick() {
|
|
1589
|
+
_alBlink.on = !_alBlink.on;
|
|
1590
|
+
const items = agentList.items;
|
|
1591
|
+
const cur = agentList.selected ?? 0;
|
|
1592
|
+
if (_alBlink.sel !== cur && _alBlink.sel >= 0 && items[_alBlink.sel]) {
|
|
1593
|
+
items[_alBlink.sel].setContent((items[_alBlink.sel].content ?? '').replace(/ █$/, ''));
|
|
1594
|
+
}
|
|
1595
|
+
_alBlink.sel = cur;
|
|
1596
|
+
if (items[cur]) {
|
|
1597
|
+
const base = (items[cur].content ?? '').replace(/ █$/, '');
|
|
1598
|
+
items[cur].setContent(_alBlink.on ? `${base} █` : base);
|
|
1599
|
+
}
|
|
1600
|
+
screen.render();
|
|
1601
|
+
}
|
|
1602
|
+
agentList.on('focus', () => {
|
|
1603
|
+
_listFocused = true;
|
|
1604
|
+
_alBlink.on = true;
|
|
1605
|
+
_alBlink.sel = agentList.selected ?? 0;
|
|
1606
|
+
_hintIdx = -1;
|
|
1607
|
+
_hintBase = '';
|
|
1608
|
+
_updateHint(_alBlink.sel);
|
|
1609
|
+
const items = agentList.items;
|
|
1610
|
+
if (items[_alBlink.sel]) items[_alBlink.sel].setContent((items[_alBlink.sel].content ?? '') + ' █');
|
|
1611
|
+
screen.render();
|
|
1612
|
+
_alBlink.interval = setInterval(_alTick, 500);
|
|
1613
|
+
});
|
|
1614
|
+
agentList.on('blur', () => {
|
|
1615
|
+
_listFocused = false;
|
|
1616
|
+
if (_alBlink.interval) { clearInterval(_alBlink.interval); _alBlink.interval = null; }
|
|
1617
|
+
const items = agentList.items;
|
|
1618
|
+
const sel = agentList.selected ?? 0;
|
|
1619
|
+
if (items[sel]) {
|
|
1620
|
+
items[sel].setContent(sel === _hintIdx ? _hintBase : (items[sel].content ?? '').replace(/ █$/, ''));
|
|
1621
|
+
}
|
|
1622
|
+
if (_hintIdx >= 0 && _hintIdx !== sel && items[_hintIdx]) {
|
|
1623
|
+
items[_hintIdx].setContent(_hintBase);
|
|
1624
|
+
}
|
|
1625
|
+
_hintIdx = -1;
|
|
1626
|
+
_hintBase = '';
|
|
1627
|
+
screen.render();
|
|
1628
|
+
});
|
|
1629
|
+
agentList.on('select item', () => {
|
|
1630
|
+
_updateHint(agentList.selected ?? 0);
|
|
1631
|
+
if (_alBlink.interval) _alTick();
|
|
1632
|
+
});
|
|
1633
|
+
|
|
1634
|
+
// Navigation: up at top → tab bar, escape → tab bar
|
|
1635
|
+
agentList.key(['up'], () => {
|
|
1636
|
+
if (agentList.selected === 0 && typeof focusMainTabBar === 'function') {
|
|
1637
|
+
focusMainTabBar();
|
|
1638
|
+
setTimeout(() => { agentList.select(0); screen.render(); }, 0);
|
|
1639
|
+
}
|
|
1640
|
+
});
|
|
1641
|
+
agentList.key(['escape'], () => {
|
|
1642
|
+
if (typeof focusMainTabBar === 'function') { focusMainTabBar(); screen.render(); }
|
|
1643
|
+
});
|
|
1644
|
+
|
|
1645
|
+
// -------------------------------------------------------------------------
|
|
1646
|
+
// Tab Component Contract
|
|
1647
|
+
|
|
1648
|
+
return {
|
|
1649
|
+
box,
|
|
1650
|
+
|
|
1651
|
+
show() {
|
|
1652
|
+
box.show();
|
|
1653
|
+
refreshDisplay();
|
|
1654
|
+
screen.render();
|
|
1655
|
+
},
|
|
1656
|
+
|
|
1657
|
+
hide() {
|
|
1658
|
+
_killPreview();
|
|
1659
|
+
box.hide();
|
|
1660
|
+
screen.render();
|
|
1661
|
+
},
|
|
1662
|
+
|
|
1663
|
+
onFocus() {
|
|
1664
|
+
if (_bmadDetected) {
|
|
1665
|
+
agentList.focus();
|
|
1666
|
+
} else {
|
|
1667
|
+
onboardingBox.focus();
|
|
1668
|
+
}
|
|
1669
|
+
screen.render();
|
|
1670
|
+
},
|
|
1671
|
+
|
|
1672
|
+
onBlur() {
|
|
1673
|
+
_killPreview();
|
|
1674
|
+
},
|
|
1675
|
+
|
|
1676
|
+
getFooterText() {
|
|
1677
|
+
return _bmadDetected ? FOOTER_TEXT_BMAD : FOOTER_TEXT_NOBMAD;
|
|
1678
|
+
},
|
|
1679
|
+
|
|
1680
|
+
getFooterColor() {
|
|
1681
|
+
return COLORS.footerBg;
|
|
1682
|
+
},
|
|
1683
|
+
};
|
|
1684
|
+
}
|