agentvibes 4.0.0 → 4.2.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/.claude/config/audio-effects.cfg +3 -2
- package/.claude/config/background-music-position.txt +1 -1
- package/.claude/hooks/audio-processor.sh +87 -43
- package/.claude/hooks/bmad-speak.sh +184 -27
- package/.claude/hooks/play-tts-enhanced.sh +40 -5
- package/.claude/hooks/play-tts-macos.sh +29 -6
- package/.claude/hooks/play-tts-piper.sh +174 -67
- package/.claude/hooks/play-tts-soprano.sh +42 -6
- package/.claude/hooks/play-tts-ssh-remote.sh +117 -38
- package/.claude/hooks/play-tts.sh +12 -9
- package/.claude/hooks/session-start-tts.sh +10 -0
- package/.claude/hooks/stop-tts.sh +84 -0
- package/.claude/hooks/tts-queue-worker.sh +51 -20
- package/.claude/hooks/tts-queue.sh +37 -8
- package/.claude/hooks/voice-manager.sh +5 -1
- package/CLAUDE.md +0 -11
- package/README.md +176 -78
- package/RELEASE_NOTES.md +1197 -60
- package/bin/agentvibes-voice-browser.js +35 -21
- package/mcp-server/server.py +36 -0
- package/package.json +1 -3
- package/src/console/app.js +23 -5
- package/src/console/constants/personalities.js +44 -0
- package/src/console/footer-config.js +8 -0
- package/src/console/navigation.js +3 -1
- package/src/console/tabs/agents-tab.js +1219 -72
- package/src/console/tabs/install-tab.js +2 -1
- package/src/console/tabs/placeholder-tab.js +9 -1
- package/src/console/tabs/receiver-tab.js +1212 -0
- package/src/console/tabs/settings-tab.js +33 -323
- 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.js +54 -2
- package/src/services/agent-voice-store.js +282 -22
- package/src/services/config-service.js +24 -0
- package/src/services/navigation-service.js +1 -1
- package/src/utils/music-file-validator.js +41 -31
- package/templates/agentvibes-receiver.sh +431 -111
|
@@ -1,14 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* AgentVibes TUI Console — Agents Tab
|
|
3
|
-
* Epic 11: Stories 11.1-11.5
|
|
2
|
+
* AgentVibes TUI Console — Agents Tab (BMAD Integration)
|
|
4
3
|
*
|
|
5
4
|
* Implements the Tab Component Contract:
|
|
6
5
|
* createAgentsTab(screen, services) → { box, show, hide, onFocus, onBlur, getFooterText, getFooterColor }
|
|
7
6
|
*
|
|
8
|
-
*
|
|
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
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
|
-
import { AgentVoiceStore, scanBmadAgents, isSingleVoiceProvider } from '../../services/agent-voice-store.js';
|
|
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;
|
|
12
32
|
|
|
13
33
|
const IS_TEST = process.env.AGENTVIBES_TEST_MODE === 'true';
|
|
14
34
|
|
|
@@ -22,21 +42,39 @@ if (!IS_TEST) {
|
|
|
22
42
|
|
|
23
43
|
const COLORS = {
|
|
24
44
|
contentBg: '#0a0e1a',
|
|
25
|
-
sectionHdr: '#7b1fa2',
|
|
45
|
+
sectionHdr: '#7b1fa2',
|
|
26
46
|
labelFg: '#e3f2fd',
|
|
27
|
-
valueFg: '#ffff00',
|
|
28
|
-
activeFg: '#ce93d8',
|
|
29
|
-
btnDefault: '#6a1b9a',
|
|
47
|
+
valueFg: '#ffff00',
|
|
48
|
+
activeFg: '#ce93d8',
|
|
49
|
+
btnDefault: '#6a1b9a',
|
|
30
50
|
btnFocus: '#9c27b0',
|
|
31
51
|
btnFocusFg: '#ffffff',
|
|
32
52
|
btnPress: '#ff00ff',
|
|
33
53
|
borderFg: '#9c27b0',
|
|
34
|
-
footerBg: '#9c27b0',
|
|
54
|
+
footerBg: '#9c27b0',
|
|
35
55
|
noticeFg: '#90a4ae',
|
|
36
56
|
warnFg: '#ff9800',
|
|
57
|
+
linkFg: '#00e5ff',
|
|
37
58
|
};
|
|
38
59
|
|
|
39
|
-
const
|
|
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}`;
|
|
40
78
|
|
|
41
79
|
// ---------------------------------------------------------------------------
|
|
42
80
|
|
|
@@ -47,28 +85,74 @@ function createTestStub() {
|
|
|
47
85
|
hide: () => {},
|
|
48
86
|
onFocus: () => {},
|
|
49
87
|
onBlur: () => {},
|
|
50
|
-
getFooterText: () =>
|
|
88
|
+
getFooterText: () => FOOTER_TEXT_BMAD,
|
|
51
89
|
getFooterColor: () => COLORS.footerBg,
|
|
52
90
|
};
|
|
53
91
|
}
|
|
54
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
|
+
{#00e5ff-fg}npx bmad-method install{/#00e5ff-fg}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
{bold}Learn more:{/bold}
|
|
117
|
+
|
|
118
|
+
{#00e5ff-fg}https://docs.bmad-method.org/{/#00e5ff-fg}
|
|
119
|
+
{#00e5ff-fg}https://github.com/bmad-code-org/BMAD-METHOD{/#00e5ff-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
|
+
|
|
55
126
|
// ---------------------------------------------------------------------------
|
|
56
127
|
|
|
57
128
|
/**
|
|
58
129
|
* Create the Agents tab component.
|
|
59
|
-
*
|
|
60
|
-
* @param {object} screen - Blessed screen instance (or test stub)
|
|
61
|
-
* @param {object} services
|
|
62
|
-
* @param {import('../../services/config-service.js').ConfigService} services.configService
|
|
63
|
-
* @param {import('../../services/provider-service.js').ProviderService} services.providerService
|
|
64
|
-
* @returns {{ box, show, hide, onFocus, onBlur, getFooterText, getFooterColor }}
|
|
65
130
|
*/
|
|
66
131
|
export function createAgentsTab(screen, services) {
|
|
67
132
|
if (IS_TEST) return createTestStub();
|
|
68
133
|
|
|
69
|
-
const { configService, providerService, focusMainTabBar } = services;
|
|
134
|
+
const { configService, providerService, focusMainTabBar, navigationService } = services;
|
|
70
135
|
const voiceStore = new AgentVoiceStore();
|
|
71
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
|
+
|
|
72
156
|
// -------------------------------------------------------------------------
|
|
73
157
|
// Container
|
|
74
158
|
|
|
@@ -85,17 +169,48 @@ export function createAgentsTab(screen, services) {
|
|
|
85
169
|
});
|
|
86
170
|
|
|
87
171
|
// -------------------------------------------------------------------------
|
|
88
|
-
//
|
|
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
|
|
89
192
|
|
|
90
|
-
blessed.text({
|
|
193
|
+
const sectionHeader = blessed.text({
|
|
91
194
|
parent: box,
|
|
92
195
|
top: 1,
|
|
93
196
|
left: 2,
|
|
197
|
+
hidden: true,
|
|
94
198
|
content: `{#7b1fa2-fg}── BMAD Agents ${'─'.repeat(53)}{/#7b1fa2-fg}`,
|
|
95
199
|
tags: true,
|
|
96
200
|
style: { bg: COLORS.contentBg },
|
|
97
201
|
});
|
|
98
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
|
+
|
|
99
214
|
// -------------------------------------------------------------------------
|
|
100
215
|
// Agent list
|
|
101
216
|
|
|
@@ -105,9 +220,11 @@ export function createAgentsTab(screen, services) {
|
|
|
105
220
|
left: 2,
|
|
106
221
|
width: '96%',
|
|
107
222
|
height: '55%',
|
|
223
|
+
hidden: true,
|
|
108
224
|
keys: true,
|
|
109
225
|
vi: true,
|
|
110
226
|
mouse: true,
|
|
227
|
+
tags: true,
|
|
111
228
|
border: { type: 'line' },
|
|
112
229
|
scrollbar: { ch: '│', style: { fg: COLORS.sectionHdr } },
|
|
113
230
|
style: {
|
|
@@ -122,10 +239,11 @@ export function createAgentsTab(screen, services) {
|
|
|
122
239
|
// -------------------------------------------------------------------------
|
|
123
240
|
// Status panel
|
|
124
241
|
|
|
125
|
-
blessed.text({
|
|
242
|
+
const statusDivider = blessed.text({
|
|
126
243
|
parent: box,
|
|
127
244
|
top: '64%',
|
|
128
245
|
left: 2,
|
|
246
|
+
hidden: true,
|
|
129
247
|
content: `{#7b1fa2-fg}── Status ${'─'.repeat(58)}{/#7b1fa2-fg}`,
|
|
130
248
|
tags: true,
|
|
131
249
|
style: { bg: COLORS.contentBg },
|
|
@@ -135,6 +253,7 @@ export function createAgentsTab(screen, services) {
|
|
|
135
253
|
parent: box,
|
|
136
254
|
top: '69%',
|
|
137
255
|
left: 2,
|
|
256
|
+
hidden: true,
|
|
138
257
|
tags: true,
|
|
139
258
|
content: '',
|
|
140
259
|
style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
|
|
@@ -144,11 +263,23 @@ export function createAgentsTab(screen, services) {
|
|
|
144
263
|
parent: box,
|
|
145
264
|
top: '74%',
|
|
146
265
|
left: 2,
|
|
266
|
+
hidden: true,
|
|
147
267
|
tags: true,
|
|
148
268
|
content: '',
|
|
149
269
|
style: { fg: COLORS.warnFg, bg: COLORS.contentBg },
|
|
150
270
|
});
|
|
151
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
|
+
|
|
152
283
|
// -------------------------------------------------------------------------
|
|
153
284
|
// Buttons
|
|
154
285
|
|
|
@@ -159,6 +290,7 @@ export function createAgentsTab(screen, services) {
|
|
|
159
290
|
mouse: true,
|
|
160
291
|
keys: true,
|
|
161
292
|
shrink: true,
|
|
293
|
+
hidden: true,
|
|
162
294
|
padding: { left: 1, right: 1 },
|
|
163
295
|
style: {
|
|
164
296
|
bg: COLORS.btnDefault,
|
|
@@ -191,82 +323,1056 @@ export function createAgentsTab(screen, services) {
|
|
|
191
323
|
return btn;
|
|
192
324
|
}
|
|
193
325
|
|
|
194
|
-
const resetBtn = _createBtn('[
|
|
195
|
-
const
|
|
196
|
-
const agent = agents[agentList.selected];
|
|
326
|
+
const resetBtn = _createBtn('[X] Reset', () => {
|
|
327
|
+
const agent = _agents[agentList.selected];
|
|
197
328
|
if (agent) {
|
|
198
|
-
voiceStore.
|
|
329
|
+
voiceStore.resetAgentProfile(agent.id);
|
|
199
330
|
refreshDisplay();
|
|
200
331
|
}
|
|
201
332
|
});
|
|
202
333
|
resetBtn.bottom = 4;
|
|
203
334
|
resetBtn.left = 4;
|
|
204
335
|
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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;
|
|
212
343
|
|
|
213
344
|
// -------------------------------------------------------------------------
|
|
214
|
-
//
|
|
345
|
+
// Show/hide helpers for the two states
|
|
215
346
|
|
|
216
|
-
|
|
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
|
+
}
|
|
217
353
|
|
|
218
|
-
function
|
|
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) {
|
|
219
363
|
if (agents.length === 0) {
|
|
220
|
-
return [' (no BMAD agents detected
|
|
364
|
+
return [' (no BMAD agents detected)'];
|
|
221
365
|
}
|
|
222
366
|
return agents.map(a => {
|
|
223
|
-
const
|
|
224
|
-
|
|
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}`;
|
|
225
386
|
});
|
|
226
387
|
}
|
|
227
388
|
|
|
389
|
+
// -------------------------------------------------------------------------
|
|
390
|
+
// Refresh display
|
|
391
|
+
|
|
228
392
|
function refreshDisplay() {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
393
|
+
_bmadDetected = isBmadDetected(_projectRoot);
|
|
394
|
+
_agents = scanBmadAgents(_projectRoot);
|
|
395
|
+
|
|
396
|
+
if (!_bmadDetected) {
|
|
397
|
+
_showOnboardingState();
|
|
398
|
+
screen.render();
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
_showBmadState();
|
|
234
403
|
|
|
235
|
-
const items = _buildListItems(_agents
|
|
404
|
+
const items = _buildListItems(_agents);
|
|
236
405
|
agentList.setItems(items);
|
|
237
406
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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 = '{#00e5ff-fg}';
|
|
448
|
+
const _SPIN_SFX = '{/#00e5ff-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
|
+
// Kill any playing preview
|
|
491
|
+
|
|
492
|
+
function _killPreview() {
|
|
493
|
+
_stopSpinner();
|
|
494
|
+
if (_playingProcess) {
|
|
495
|
+
try { process.kill(-_playingProcess.pid, 'SIGTERM'); } catch {}
|
|
496
|
+
_playingProcess = null;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// -------------------------------------------------------------------------
|
|
501
|
+
// Sample an agent with their full profile (voice + pretext + reverb + music)
|
|
502
|
+
// Uses play-tts-enhanced.sh for the complete effects pipeline.
|
|
503
|
+
|
|
504
|
+
function _sampleAgent(agent) {
|
|
505
|
+
const profile = voiceStore.getAgentProfile(agent.id);
|
|
506
|
+
const globalCfg = configService.getConfig();
|
|
507
|
+
_sampleWithFullProfile(agent, {
|
|
508
|
+
voice: profile.voice || globalCfg.voice || '',
|
|
509
|
+
pretext: profile.pretext || AgentVoiceStore.getDefaultPretext(agent.displayName, agent.title),
|
|
510
|
+
reverbPreset: profile.reverbPreset || globalCfg.effects?.reverbPreset || 'light',
|
|
511
|
+
personality: profile.personality || globalCfg.personality || 'none',
|
|
512
|
+
backgroundMusic: {
|
|
513
|
+
track: profile.backgroundMusic?.track || globalCfg.backgroundMusic?.track || '',
|
|
514
|
+
volume: profile.backgroundMusic?.volume ?? globalCfg.backgroundMusic?.volume ?? 70,
|
|
515
|
+
enabled: profile.backgroundMusic?.enabled ?? globalCfg.backgroundMusic?.enabled ?? false,
|
|
516
|
+
},
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// -------------------------------------------------------------------------
|
|
521
|
+
// Agent detail panel (modal overlay)
|
|
522
|
+
|
|
523
|
+
function _openAgentDetailPanel(agent) {
|
|
524
|
+
const profile = voiceStore.getAgentProfile(agent.id);
|
|
525
|
+
const globalCfg = configService.getConfig();
|
|
526
|
+
|
|
527
|
+
// Working copy of the profile being edited
|
|
528
|
+
const draft = {
|
|
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
|
+
let _closed = false;
|
|
541
|
+
navigationService?.openModal();
|
|
542
|
+
|
|
543
|
+
const modal = blessed.box({
|
|
544
|
+
parent: screen,
|
|
545
|
+
top: 'center',
|
|
546
|
+
left: 'center',
|
|
547
|
+
width: 72,
|
|
548
|
+
height: 18,
|
|
549
|
+
border: { type: 'line' },
|
|
550
|
+
tags: true,
|
|
551
|
+
label: _modalTitle(`${agent.icon || '🧙'} ${agent.displayName} (${agent.title || 'Agent'})`),
|
|
552
|
+
style: {
|
|
553
|
+
fg: COLORS.labelFg,
|
|
554
|
+
bg: COLORS.contentBg,
|
|
555
|
+
border: { fg: COLORS.btnFocus },
|
|
556
|
+
},
|
|
557
|
+
});
|
|
558
|
+
modal.setFront();
|
|
559
|
+
|
|
560
|
+
// Field definitions
|
|
561
|
+
const FIELDS = [
|
|
562
|
+
{ key: 'voice', label: 'Voice', getValue: () => draft.voice || '(global default)' },
|
|
563
|
+
{ key: 'pretext', label: 'Pretext', getValue: () => draft.pretext || '(default)' },
|
|
564
|
+
{ key: 'reverbPreset', label: 'Reverb', getValue: () => formatReverbState(draft.reverbPreset) },
|
|
565
|
+
{ key: 'personality', label: 'Personality', getValue: () => {
|
|
566
|
+
const p = draft.personality;
|
|
567
|
+
const emoji = PERSONALITY_EMOJIS[p] || '';
|
|
568
|
+
return `${emoji} ${p === 'none' ? 'None' : p.charAt(0).toUpperCase() + p.slice(1)}`;
|
|
569
|
+
}},
|
|
570
|
+
{ key: 'music', label: 'Music', getValue: () => {
|
|
571
|
+
if (!draft.backgroundMusic.enabled) return '(disabled)';
|
|
572
|
+
return `${formatTrackName(draft.backgroundMusic.track)} Vol:${draft.backgroundMusic.volume}%`;
|
|
573
|
+
}},
|
|
574
|
+
];
|
|
575
|
+
|
|
576
|
+
// Build field list items
|
|
577
|
+
function _fieldItems() {
|
|
578
|
+
return FIELDS.map(f => {
|
|
579
|
+
const label = f.label.padEnd(14);
|
|
580
|
+
const val = f.getValue();
|
|
581
|
+
return ` ${label} ${val}`;
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const fieldList = blessed.list({
|
|
586
|
+
parent: modal,
|
|
587
|
+
top: 1,
|
|
588
|
+
left: 2,
|
|
589
|
+
right: 2,
|
|
590
|
+
height: FIELDS.length + 2,
|
|
591
|
+
keys: true,
|
|
592
|
+
vi: true,
|
|
593
|
+
mouse: true,
|
|
594
|
+
border: { type: 'line' },
|
|
595
|
+
tags: true,
|
|
596
|
+
style: {
|
|
597
|
+
fg: COLORS.labelFg,
|
|
598
|
+
bg: COLORS.contentBg,
|
|
599
|
+
border: { fg: '#4a148c' },
|
|
600
|
+
selected: { bg: '#4a148c', fg: COLORS.activeFg, bold: true },
|
|
601
|
+
item: { fg: COLORS.labelFg },
|
|
602
|
+
},
|
|
603
|
+
});
|
|
604
|
+
fieldList.setItems(_fieldItems());
|
|
605
|
+
|
|
606
|
+
// Key hint
|
|
607
|
+
blessed.text({
|
|
608
|
+
parent: modal,
|
|
609
|
+
bottom: 4,
|
|
610
|
+
left: 2,
|
|
611
|
+
right: 2,
|
|
612
|
+
tags: true,
|
|
613
|
+
content: '{#455a64-fg}[↑↓] Navigate fields [Enter] Edit field [Space] Sample [Esc] Cancel{/#455a64-fg}',
|
|
614
|
+
style: { bg: COLORS.contentBg },
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
// Buttons
|
|
618
|
+
function _modalBtn(label, leftPos, onClick) {
|
|
619
|
+
const btn = blessed.button({
|
|
620
|
+
parent: modal,
|
|
621
|
+
content: label,
|
|
622
|
+
bottom: 2,
|
|
623
|
+
left: leftPos,
|
|
624
|
+
mouse: true,
|
|
625
|
+
keys: true,
|
|
626
|
+
shrink: true,
|
|
627
|
+
padding: { left: 1, right: 1 },
|
|
628
|
+
style: {
|
|
629
|
+
bg: COLORS.btnDefault,
|
|
630
|
+
fg: 'white',
|
|
631
|
+
focus: { bg: '#00e5ff', fg: '#000000', bold: true },
|
|
632
|
+
hover: { bg: '#00e5ff', fg: '#000000', bold: true },
|
|
633
|
+
},
|
|
634
|
+
});
|
|
635
|
+
btn.on('focus', () => {
|
|
636
|
+
const raw = btn.content.replace(/[►◄]/g, '').trim();
|
|
637
|
+
btn.setContent(`►${raw}◄`);
|
|
638
|
+
screen.render();
|
|
639
|
+
});
|
|
640
|
+
btn.on('blur', () => {
|
|
641
|
+
const raw = btn.content.replace(/[►◄]/g, '').trim();
|
|
642
|
+
btn.setContent(raw);
|
|
643
|
+
screen.render();
|
|
644
|
+
});
|
|
645
|
+
btn.key(['enter', 'space'], () => onClick());
|
|
646
|
+
btn.on('click', () => onClick());
|
|
647
|
+
return btn;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const saveBtn = _modalBtn('Save', 4, () => {
|
|
651
|
+
// Only save fields that differ from global
|
|
652
|
+
const toSave = {};
|
|
653
|
+
if (draft.voice && draft.voice !== globalCfg.voice) toSave.voice = draft.voice;
|
|
654
|
+
if (draft.pretext !== AgentVoiceStore.getDefaultPretext(agent.displayName, agent.title)) toSave.pretext = draft.pretext;
|
|
655
|
+
if (draft.reverbPreset !== (globalCfg.effects?.reverbPreset || 'light')) toSave.reverbPreset = draft.reverbPreset;
|
|
656
|
+
if (draft.personality !== (globalCfg.personality || 'none')) toSave.personality = draft.personality;
|
|
657
|
+
if (draft.backgroundMusic.track !== (globalCfg.backgroundMusic?.track || '') ||
|
|
658
|
+
draft.backgroundMusic.volume !== (globalCfg.backgroundMusic?.volume ?? 70) ||
|
|
659
|
+
draft.backgroundMusic.enabled !== (globalCfg.backgroundMusic?.enabled ?? false)) {
|
|
660
|
+
toSave.backgroundMusic = draft.backgroundMusic;
|
|
661
|
+
}
|
|
662
|
+
voiceStore.setAgentProfile(agent.id, toSave);
|
|
663
|
+
_closeModal();
|
|
664
|
+
refreshDisplay();
|
|
665
|
+
// Show temporary "Saved!" toast
|
|
666
|
+
_showSavedToast(agent.displayName);
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
const resetAllBtn = _modalBtn('Reset to Defaults', 14, () => {
|
|
670
|
+
voiceStore.resetAgentProfile(agent.id);
|
|
671
|
+
_closeModal();
|
|
672
|
+
refreshDisplay();
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
const cancelBtn = _modalBtn('Cancel', 38, _closeModal);
|
|
676
|
+
|
|
677
|
+
function _closeModal() {
|
|
678
|
+
if (_closed) return;
|
|
679
|
+
_closed = true;
|
|
680
|
+
_killPreview();
|
|
681
|
+
navigationService?.closeModal();
|
|
682
|
+
destroyList(modal, screen);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Field editing via Enter
|
|
686
|
+
fieldList.key(['enter'], () => {
|
|
687
|
+
const idx = fieldList.selected;
|
|
688
|
+
const field = FIELDS[idx];
|
|
689
|
+
if (!field) return;
|
|
690
|
+
|
|
691
|
+
switch (field.key) {
|
|
692
|
+
case 'voice':
|
|
693
|
+
_openVoicePickerForAgent(agent, draft, () => {
|
|
694
|
+
fieldList.setItems(_fieldItems());
|
|
695
|
+
fieldList.select(idx);
|
|
696
|
+
fieldList.focus();
|
|
697
|
+
screen.render();
|
|
698
|
+
});
|
|
699
|
+
break;
|
|
700
|
+
|
|
701
|
+
case 'pretext':
|
|
702
|
+
_openPretextEditor(modal, draft, () => {
|
|
703
|
+
fieldList.setItems(_fieldItems());
|
|
704
|
+
fieldList.select(idx);
|
|
705
|
+
fieldList.focus();
|
|
706
|
+
screen.render();
|
|
707
|
+
});
|
|
708
|
+
break;
|
|
709
|
+
|
|
710
|
+
case 'reverbPreset':
|
|
711
|
+
openReverbPicker(screen, draft.reverbPreset, (val) => {
|
|
712
|
+
draft.reverbPreset = val;
|
|
713
|
+
fieldList.setItems(_fieldItems());
|
|
714
|
+
fieldList.select(idx);
|
|
715
|
+
fieldList.focus();
|
|
716
|
+
screen.render();
|
|
717
|
+
}, () => {
|
|
718
|
+
fieldList.focus();
|
|
719
|
+
screen.render();
|
|
720
|
+
}, { applyToEffectsManager: false });
|
|
721
|
+
break;
|
|
722
|
+
|
|
723
|
+
case 'personality':
|
|
724
|
+
openPersonalityPicker(screen, draft.personality, (val) => {
|
|
725
|
+
draft.personality = val;
|
|
726
|
+
fieldList.setItems(_fieldItems());
|
|
727
|
+
fieldList.select(idx);
|
|
728
|
+
fieldList.focus();
|
|
729
|
+
screen.render();
|
|
730
|
+
}, () => {
|
|
731
|
+
fieldList.focus();
|
|
732
|
+
screen.render();
|
|
733
|
+
});
|
|
734
|
+
break;
|
|
735
|
+
|
|
736
|
+
case 'music':
|
|
737
|
+
openTrackPicker(screen, draft.backgroundMusic.track, draft.backgroundMusic.volume, (track, volume) => {
|
|
738
|
+
draft.backgroundMusic.track = track;
|
|
739
|
+
draft.backgroundMusic.volume = volume;
|
|
740
|
+
draft.backgroundMusic.enabled = true;
|
|
741
|
+
fieldList.setItems(_fieldItems());
|
|
742
|
+
fieldList.select(idx);
|
|
743
|
+
fieldList.focus();
|
|
744
|
+
screen.render();
|
|
745
|
+
}, () => {
|
|
746
|
+
fieldList.focus();
|
|
747
|
+
screen.render();
|
|
748
|
+
});
|
|
749
|
+
break;
|
|
750
|
+
}
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
// Space = sample with current draft
|
|
754
|
+
fieldList.key(['space'], () => {
|
|
755
|
+
const draftAgent = { ...agent };
|
|
756
|
+
// Temporarily set profile for sampling
|
|
757
|
+
_sampleAgentWithDraft(draftAgent, draft);
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
// Escape = close
|
|
761
|
+
fieldList.key(['escape', 'q'], _closeModal);
|
|
762
|
+
saveBtn.key(['escape'], _closeModal);
|
|
763
|
+
resetAllBtn.key(['escape'], _closeModal);
|
|
764
|
+
cancelBtn.key(['escape'], _closeModal);
|
|
765
|
+
|
|
766
|
+
// Tab navigation within modal
|
|
767
|
+
fieldList.key(['tab'], () => { saveBtn.focus(); screen.render(); });
|
|
768
|
+
saveBtn.key(['tab'], () => { resetAllBtn.focus(); screen.render(); });
|
|
769
|
+
resetAllBtn.key(['tab'], () => { cancelBtn.focus(); screen.render(); });
|
|
770
|
+
cancelBtn.key(['tab'], () => { fieldList.focus(); screen.render(); });
|
|
771
|
+
|
|
772
|
+
fieldList.focus();
|
|
773
|
+
screen.render();
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// -------------------------------------------------------------------------
|
|
777
|
+
// Voice picker for agent detail panel
|
|
778
|
+
|
|
779
|
+
function _openVoicePickerForAgent(agent, draft, onDone) {
|
|
780
|
+
let _allVoices = [];
|
|
781
|
+
let _filterText = '';
|
|
782
|
+
let _previewProc = null;
|
|
783
|
+
let _previewVoiceId = null;
|
|
784
|
+
let _vpClosed = false;
|
|
785
|
+
|
|
786
|
+
const _spawnEnv = buildAudioEnv();
|
|
241
787
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
788
|
+
function _killVP() {
|
|
789
|
+
if (_previewProc) {
|
|
790
|
+
try { process.kill(-_previewProc.pid, 'SIGTERM'); } catch {}
|
|
791
|
+
_previewProc = null;
|
|
792
|
+
}
|
|
793
|
+
_previewVoiceId = null;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function _closeVP() {
|
|
797
|
+
if (_vpClosed) return;
|
|
798
|
+
_vpClosed = true;
|
|
799
|
+
_killVP();
|
|
800
|
+
destroyList(vpModal, screen, onDone);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const vpModal = blessed.box({
|
|
804
|
+
parent: screen,
|
|
805
|
+
top: '6%',
|
|
806
|
+
left: '3%',
|
|
807
|
+
width: '94%',
|
|
808
|
+
height: '88%',
|
|
809
|
+
border: { type: 'line' },
|
|
810
|
+
tags: true,
|
|
811
|
+
label: _modalTitle(`Select Voice for ${agent.icon || ''} ${agent.displayName}`),
|
|
812
|
+
style: {
|
|
813
|
+
fg: COLORS.labelFg,
|
|
814
|
+
bg: COLORS.contentBg,
|
|
815
|
+
border: { fg: '#00e5ff' },
|
|
816
|
+
},
|
|
817
|
+
});
|
|
818
|
+
vpModal.setFront();
|
|
819
|
+
|
|
820
|
+
// Search
|
|
821
|
+
blessed.text({
|
|
822
|
+
parent: vpModal, top: 1, left: 2,
|
|
823
|
+
content: 'Search:', style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
|
|
824
|
+
});
|
|
825
|
+
const vpSearch = blessed.textbox({
|
|
826
|
+
parent: vpModal, top: 1, left: 11, width: 40, height: 1,
|
|
827
|
+
inputOnFocus: true, keys: true,
|
|
828
|
+
style: { fg: COLORS.valueFg, bg: '#1a237e', focus: { bg: '#283593' } },
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
// Column header
|
|
832
|
+
const COL_N = 28;
|
|
833
|
+
const COL_G = 10;
|
|
834
|
+
blessed.text({
|
|
835
|
+
parent: vpModal, top: 2, left: 6, tags: true,
|
|
836
|
+
content: `{#7986cb-fg}${'Name'.padEnd(COL_N)}${'Gender'.padEnd(COL_G)}Provider{/#7986cb-fg}`,
|
|
837
|
+
style: { bg: COLORS.contentBg },
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
const vpList = blessed.list({
|
|
841
|
+
parent: vpModal, top: 3, left: 2, right: 2, bottom: 5,
|
|
842
|
+
keys: true, vi: true, mouse: true,
|
|
843
|
+
border: { type: 'line' },
|
|
844
|
+
scrollbar: { ch: '│', style: { fg: COLORS.borderFg } },
|
|
845
|
+
style: {
|
|
846
|
+
fg: COLORS.labelFg, bg: COLORS.contentBg,
|
|
847
|
+
border: { fg: COLORS.borderFg },
|
|
848
|
+
selected: { bg: '#1a237e', fg: '#00e5ff', bold: true },
|
|
849
|
+
item: { fg: COLORS.labelFg },
|
|
850
|
+
},
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
const vpInfoLine = blessed.text({
|
|
854
|
+
parent: vpModal, bottom: 4, left: 2, right: 2, tags: true,
|
|
855
|
+
content: '', style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
const vpPreviewLine = blessed.text({
|
|
859
|
+
parent: vpModal, bottom: 3, left: 2, right: 2, tags: true,
|
|
860
|
+
content: '', style: { fg: '#00e5ff', bg: COLORS.contentBg },
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
blessed.text({
|
|
864
|
+
parent: vpModal, bottom: 2, left: 2, right: 2, tags: true,
|
|
865
|
+
content: '{#455a64-fg}[↑↓/jk] Navigate [Enter] Select [Space] Preview [/] Search [Esc] Cancel{/#455a64-fg}',
|
|
866
|
+
style: { bg: COLORS.contentBg },
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
function _getFiltered() {
|
|
870
|
+
if (!_filterText) return _allVoices;
|
|
871
|
+
const f = _filterText.toLowerCase();
|
|
872
|
+
return _allVoices.filter(v => v.toLowerCase().includes(f));
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
function _buildVoiceItems(voices) {
|
|
876
|
+
return voices.map(v => {
|
|
877
|
+
const isActive = v === draft.voice;
|
|
878
|
+
const isPrev = v === _previewVoiceId;
|
|
879
|
+
const dot = isPrev ? '♪' : (isActive ? '●' : ' ');
|
|
880
|
+
const meta = getVoiceMeta(v);
|
|
881
|
+
const name = meta.displayName.length > COL_N
|
|
882
|
+
? meta.displayName.slice(0, COL_N - 1) + '…'
|
|
883
|
+
: meta.displayName.padEnd(COL_N);
|
|
884
|
+
return ` ${dot} ${name}${meta.gender.padEnd(COL_G)}${meta.provider}`;
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
function _refreshVP() {
|
|
889
|
+
if (_vpClosed) return;
|
|
890
|
+
_allVoices = scanInstalledVoices();
|
|
891
|
+
const filtered = _getFiltered();
|
|
892
|
+
const items = _buildVoiceItems(filtered);
|
|
893
|
+
vpList.setItems(items.length > 0 ? items : [' (no voices found)']);
|
|
894
|
+
screen.render();
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function _previewVoice(voiceId) {
|
|
898
|
+
if (_previewVoiceId === voiceId) { _killVP(); vpPreviewLine.setContent(''); screen.render(); return; }
|
|
899
|
+
_killVP();
|
|
900
|
+
|
|
901
|
+
const _ms = parseMultiSpeaker(voiceId);
|
|
902
|
+
const voicePath = path.resolve(PIPER_VOICES_DIR, _ms.model + '.onnx');
|
|
903
|
+
const safeBase = path.resolve(PIPER_VOICES_DIR);
|
|
904
|
+
if (!voicePath.startsWith(safeBase + path.sep) && voicePath !== safeBase) return;
|
|
905
|
+
|
|
906
|
+
const tempWav = _secureTempWav('vp');
|
|
907
|
+
const phrase = SAMPLE_PHRASES[Math.floor(Math.random() * SAMPLE_PHRASES.length)];
|
|
908
|
+
|
|
909
|
+
const args = ['--model', voicePath, '--output_file', tempWav];
|
|
910
|
+
if (_ms.speakerId != null) args.push('--speaker', String(_ms.speakerId));
|
|
911
|
+
const piper = spawn('piper', args, {
|
|
912
|
+
stdio: ['pipe', 'ignore', 'ignore'], detached: true, env: _spawnEnv,
|
|
913
|
+
});
|
|
914
|
+
piper.stdin.write(phrase + '\n');
|
|
915
|
+
piper.stdin.end();
|
|
916
|
+
_previewProc = piper;
|
|
917
|
+
_previewVoiceId = voiceId;
|
|
918
|
+
|
|
919
|
+
if (!_vpClosed) {
|
|
920
|
+
vpPreviewLine.setContent(`{#00e5ff-fg}♪ Synthesizing: ${voiceId}...{/#00e5ff-fg}`);
|
|
921
|
+
screen.render();
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
piper.on('exit', (code) => {
|
|
925
|
+
if (_previewVoiceId !== voiceId) { try { fs.unlinkSync(tempWav); } catch {} return; }
|
|
926
|
+
if (code !== 0) { _previewProc = null; _previewVoiceId = null; return; }
|
|
927
|
+
const wp = detectWavPlayer(_spawnEnv);
|
|
928
|
+
if (!wp) return;
|
|
929
|
+
const pp = spawn(wp.bin, wp.args(tempWav), { stdio: 'ignore', detached: true, env: _spawnEnv });
|
|
930
|
+
_previewProc = pp;
|
|
931
|
+
if (!_vpClosed) { vpPreviewLine.setContent(`{#00e5ff-fg}♪ Playing: ${voiceId}{/#00e5ff-fg}`); screen.render(); }
|
|
932
|
+
pp.on('exit', () => {
|
|
933
|
+
if (_previewVoiceId === voiceId) { _previewVoiceId = null; _previewProc = null; if (!_vpClosed) { vpPreviewLine.setContent(''); screen.render(); } }
|
|
934
|
+
try { fs.unlinkSync(tempWav); } catch {}
|
|
935
|
+
});
|
|
936
|
+
});
|
|
937
|
+
piper.on('error', () => { _previewProc = null; _previewVoiceId = null; });
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
vpSearch.on('keypress', () => {
|
|
941
|
+
setTimeout(() => { _filterText = vpSearch.getValue().trim(); _refreshVP(); }, 0);
|
|
942
|
+
});
|
|
943
|
+
vpSearch.key(['escape'], () => { vpList.focus(); screen.render(); });
|
|
944
|
+
vpList.key(['/'], () => { vpSearch.clearValue(); vpSearch.focus(); screen.render(); });
|
|
945
|
+
vpList.key(['enter'], () => {
|
|
946
|
+
const filtered = _getFiltered();
|
|
947
|
+
const sel = filtered[vpList.selected];
|
|
948
|
+
if (sel) { draft.voice = sel; _closeVP(); }
|
|
949
|
+
});
|
|
950
|
+
vpList.key(['space'], () => {
|
|
951
|
+
const filtered = _getFiltered();
|
|
952
|
+
const sel = filtered[vpList.selected];
|
|
953
|
+
if (sel) _previewVoice(sel);
|
|
954
|
+
});
|
|
955
|
+
vpList.key(['escape', 'q'], _closeVP);
|
|
956
|
+
|
|
957
|
+
_refreshVP();
|
|
958
|
+
const activeIdx = _getFiltered().indexOf(draft.voice);
|
|
959
|
+
if (activeIdx >= 0) vpList.select(activeIdx);
|
|
960
|
+
vpList.focus();
|
|
961
|
+
screen.render();
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// -------------------------------------------------------------------------
|
|
965
|
+
// Pretext inline editor
|
|
966
|
+
|
|
967
|
+
function _openPretextEditor(parentModal, draft, onDone) {
|
|
968
|
+
const editModal = blessed.box({
|
|
969
|
+
parent: screen,
|
|
970
|
+
top: 'center',
|
|
971
|
+
left: 'center',
|
|
972
|
+
width: 60,
|
|
973
|
+
height: 8,
|
|
974
|
+
border: { type: 'line' },
|
|
975
|
+
tags: true,
|
|
976
|
+
label: _modalTitle('Edit Pretext'),
|
|
977
|
+
style: { fg: COLORS.labelFg, bg: COLORS.contentBg, border: { fg: '#00e5ff' } },
|
|
978
|
+
});
|
|
979
|
+
editModal.setFront();
|
|
980
|
+
|
|
981
|
+
blessed.text({
|
|
982
|
+
parent: editModal, top: 1, left: 2,
|
|
983
|
+
content: 'Agent pretext (spoken before each TTS message):',
|
|
984
|
+
style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
const inputBox = blessed.textbox({
|
|
988
|
+
parent: editModal, top: 3, left: 2, right: 2, height: 3,
|
|
989
|
+
border: { type: 'line' },
|
|
990
|
+
inputOnFocus: true,
|
|
991
|
+
value: draft.pretext,
|
|
992
|
+
style: {
|
|
993
|
+
fg: COLORS.valueFg, bg: '#0d1b35',
|
|
994
|
+
border: { fg: COLORS.borderFg },
|
|
995
|
+
focus: { border: { fg: '#00e5ff' } },
|
|
996
|
+
},
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
let _editClosed = false;
|
|
1000
|
+
function _closeEdit(save) {
|
|
1001
|
+
if (_editClosed) return;
|
|
1002
|
+
_editClosed = true;
|
|
1003
|
+
if (save) {
|
|
1004
|
+
const raw = inputBox.getValue().trim();
|
|
1005
|
+
// M7: enforce max pretext length
|
|
1006
|
+
draft.pretext = (raw || draft.pretext).slice(0, MAX_PRETEXT_LENGTH);
|
|
1007
|
+
}
|
|
1008
|
+
destroyList(editModal, screen, onDone);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
inputBox.key(['enter'], () => _closeEdit(true));
|
|
1012
|
+
inputBox.key(['escape'], () => _closeEdit(false));
|
|
1013
|
+
|
|
1014
|
+
inputBox.focus();
|
|
1015
|
+
screen.render();
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// -------------------------------------------------------------------------
|
|
1019
|
+
// Sample agent with a draft profile (no save) — same full pipeline
|
|
1020
|
+
|
|
1021
|
+
function _sampleAgentWithDraft(agent, draft) {
|
|
1022
|
+
_sampleWithFullProfile(agent, draft);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// -------------------------------------------------------------------------
|
|
1026
|
+
// Shared: sample with full profile via play-tts-enhanced.sh
|
|
1027
|
+
// Writes a temp agent profile JSON, then calls the enhanced TTS pipeline
|
|
1028
|
+
// which applies voice + reverb + background music.
|
|
1029
|
+
|
|
1030
|
+
function _sampleWithFullProfile(agent, profile) {
|
|
1031
|
+
_killPreview();
|
|
1032
|
+
const gen = ++_playGeneration;
|
|
1033
|
+
|
|
1034
|
+
// Start spinner on the agent's row in the list
|
|
1035
|
+
const agentIdx = _agents.findIndex(a => a.id === agent.id);
|
|
1036
|
+
if (agentIdx >= 0) _startSpinner(agentIdx);
|
|
1037
|
+
|
|
1038
|
+
const voiceId = profile.voice || '';
|
|
1039
|
+
const pretext = profile.pretext || AgentVoiceStore.getDefaultPretext(agent.displayName, agent.title);
|
|
1040
|
+
const phrase = `${pretext} ${SAMPLE_PHRASES[Math.floor(Math.random() * SAMPLE_PHRASES.length)]}`;
|
|
1041
|
+
|
|
1042
|
+
const _spawnEnv = buildAudioEnv();
|
|
1043
|
+
const scriptDir = path.join(_projectRoot, '.claude', 'hooks');
|
|
1044
|
+
const plainScript = path.join(scriptDir, 'play-tts.sh');
|
|
1045
|
+
|
|
1046
|
+
// Use play-tts.sh directly for reliable sample playback.
|
|
1047
|
+
// Voice is passed as CLI arg, pretext is prepended to text.
|
|
1048
|
+
const args = [plainScript, phrase];
|
|
1049
|
+
if (voiceId) args.push(voiceId);
|
|
1050
|
+
|
|
1051
|
+
const env = { ..._spawnEnv };
|
|
1052
|
+
|
|
1053
|
+
const proc = spawn('bash', args, {
|
|
1054
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
1055
|
+
detached: true,
|
|
1056
|
+
env,
|
|
1057
|
+
cwd: _projectRoot,
|
|
1058
|
+
});
|
|
1059
|
+
_playingProcess = proc;
|
|
1060
|
+
|
|
1061
|
+
proc.on('exit', () => {
|
|
1062
|
+
if (gen === _playGeneration) { _playingProcess = null; _stopSpinner(); }
|
|
1063
|
+
});
|
|
1064
|
+
|
|
1065
|
+
proc.on('error', () => {
|
|
1066
|
+
if (gen === _playGeneration) { _playingProcess = null; _stopSpinner(); }
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// -------------------------------------------------------------------------
|
|
1071
|
+
// Auto-assign helpers
|
|
1072
|
+
|
|
1073
|
+
function _shuffleArray(arr) {
|
|
1074
|
+
const a = [...arr];
|
|
1075
|
+
for (let i = a.length - 1; i > 0; i--) {
|
|
1076
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
1077
|
+
[a[i], a[j]] = [a[j], a[i]];
|
|
1078
|
+
}
|
|
1079
|
+
return a;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function _autoAssignVoices() {
|
|
1083
|
+
const installed = scanInstalledVoices();
|
|
1084
|
+
if (installed.length === 0) return false;
|
|
1085
|
+
|
|
1086
|
+
// Separate by gender for variety
|
|
1087
|
+
const females = _shuffleArray(installed.filter(v => getVoiceMeta(v).gender === 'Female'));
|
|
1088
|
+
const males = _shuffleArray(installed.filter(v => getVoiceMeta(v).gender === 'Male'));
|
|
1089
|
+
const others = _shuffleArray(installed.filter(v => !['Male', 'Female'].includes(getVoiceMeta(v).gender)));
|
|
1090
|
+
|
|
1091
|
+
// Interleave female/male for natural variety, then others
|
|
1092
|
+
const pool = [];
|
|
1093
|
+
const maxLen = Math.max(females.length, males.length);
|
|
1094
|
+
for (let i = 0; i < maxLen; i++) {
|
|
1095
|
+
if (i < females.length) pool.push(females[i]);
|
|
1096
|
+
if (i < males.length) pool.push(males[i]);
|
|
1097
|
+
}
|
|
1098
|
+
pool.push(...others);
|
|
1099
|
+
|
|
1100
|
+
const used = new Set();
|
|
1101
|
+
_agents.forEach((agent, i) => {
|
|
1102
|
+
const voice = pool.find(v => !used.has(v)) ?? pool[i % pool.length];
|
|
1103
|
+
if (voice) {
|
|
1104
|
+
used.add(voice);
|
|
1105
|
+
voiceStore.setAgentProfile(agent.id, { voice });
|
|
1106
|
+
}
|
|
1107
|
+
});
|
|
1108
|
+
return true;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
function _autoAssignMusic() {
|
|
1112
|
+
const tracksDir = path.join(_projectRoot, '.claude', 'audio', 'tracks');
|
|
1113
|
+
let tracks = [];
|
|
1114
|
+
try {
|
|
1115
|
+
tracks = fs.readdirSync(tracksDir).filter(f => /\.mp3$/i.test(f));
|
|
1116
|
+
} catch { /* no tracks dir */ }
|
|
1117
|
+
if (tracks.length === 0) return false;
|
|
1118
|
+
|
|
1119
|
+
const shuffled = _shuffleArray(tracks);
|
|
1120
|
+
_agents.forEach((agent, i) => {
|
|
1121
|
+
const track = shuffled[i % shuffled.length];
|
|
1122
|
+
const existing = voiceStore.getAgentProfile(agent.id);
|
|
1123
|
+
voiceStore.setAgentProfile(agent.id, {
|
|
1124
|
+
backgroundMusic: { track, volume: existing.backgroundMusic?.volume ?? 70, enabled: true },
|
|
1125
|
+
});
|
|
1126
|
+
});
|
|
1127
|
+
return true;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
function _autoAssignAll() {
|
|
1131
|
+
if (_agents.length === 0) return;
|
|
1132
|
+
const voiceOk = _autoAssignVoices();
|
|
1133
|
+
const musicOk = _autoAssignMusic();
|
|
1134
|
+
refreshDisplay();
|
|
1135
|
+
const msg = voiceOk && musicOk ? 'Voices and music auto-assigned'
|
|
1136
|
+
: voiceOk ? 'Voices auto-assigned' : 'Auto-assign: no voices found';
|
|
1137
|
+
_showSavedToast(msg);
|
|
1138
|
+
}
|
|
247
1139
|
|
|
1140
|
+
// -------------------------------------------------------------------------
|
|
1141
|
+
// Bulk edit menu
|
|
1142
|
+
|
|
1143
|
+
function _openBulkEditMenu() {
|
|
1144
|
+
const BULK_ACTIONS = [
|
|
1145
|
+
{ label: ' Randomize Voices (gender-aware)', key: 'voices' },
|
|
1146
|
+
{ label: ' Randomize Music (unique per agent)', key: 'music' },
|
|
1147
|
+
{ label: ' Randomize Both', key: 'both' },
|
|
1148
|
+
{ label: ' Set Same Music for All Agents...', key: 'setMusic' },
|
|
1149
|
+
{ label: ' Set Same Volume for All Agents...', key: 'setVolume' },
|
|
1150
|
+
{ label: ' Set Same Pretext for All Agents...', key: 'setPretext' },
|
|
1151
|
+
{ label: ' Set Same Reverb for All Agents...', key: 'setReverb' },
|
|
1152
|
+
{ label: ' Reset All Agent Profiles', key: 'resetAll' },
|
|
1153
|
+
];
|
|
1154
|
+
|
|
1155
|
+
const menuList = blessed.list({
|
|
1156
|
+
parent: screen,
|
|
1157
|
+
top: 'center',
|
|
1158
|
+
left: 'center',
|
|
1159
|
+
width: 52,
|
|
1160
|
+
height: BULK_ACTIONS.length + 4,
|
|
1161
|
+
border: { type: 'line' },
|
|
1162
|
+
tags: true,
|
|
1163
|
+
label: _modalTitle('Bulk Edit'),
|
|
1164
|
+
keys: true,
|
|
1165
|
+
vi: true,
|
|
1166
|
+
mouse: true,
|
|
1167
|
+
items: BULK_ACTIONS.map(a => a.label),
|
|
1168
|
+
style: {
|
|
1169
|
+
fg: COLORS.labelFg,
|
|
1170
|
+
bg: COLORS.contentBg,
|
|
1171
|
+
border: { fg: COLORS.btnFocus },
|
|
1172
|
+
selected: { bg: '#4a148c', fg: COLORS.activeFg, bold: true },
|
|
1173
|
+
item: { fg: COLORS.labelFg },
|
|
1174
|
+
},
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
blessed.text({
|
|
1178
|
+
parent: menuList,
|
|
1179
|
+
bottom: -1,
|
|
1180
|
+
left: 1,
|
|
1181
|
+
width: 48,
|
|
1182
|
+
height: 1,
|
|
1183
|
+
tags: true,
|
|
1184
|
+
content: '{#455a64-fg}[Enter] Select [Esc] Cancel{/#455a64-fg}',
|
|
1185
|
+
style: { bg: COLORS.contentBg },
|
|
1186
|
+
});
|
|
1187
|
+
|
|
1188
|
+
menuList.setFront();
|
|
1189
|
+
menuList.focus();
|
|
1190
|
+
screen.render();
|
|
1191
|
+
|
|
1192
|
+
let _menuClosed = false;
|
|
1193
|
+
function _closeMenu(callback) {
|
|
1194
|
+
if (_menuClosed) return;
|
|
1195
|
+
_menuClosed = true;
|
|
1196
|
+
destroyList(menuList, screen, callback);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
menuList.key(['enter'], () => {
|
|
1200
|
+
const action = BULK_ACTIONS[menuList.selected];
|
|
1201
|
+
if (!action) return;
|
|
1202
|
+
|
|
1203
|
+
switch (action.key) {
|
|
1204
|
+
case 'voices':
|
|
1205
|
+
_closeMenu(() => {
|
|
1206
|
+
if (_autoAssignVoices()) { refreshDisplay(); _showSavedToast('Voices randomized'); }
|
|
1207
|
+
});
|
|
1208
|
+
break;
|
|
1209
|
+
|
|
1210
|
+
case 'music':
|
|
1211
|
+
_closeMenu(() => {
|
|
1212
|
+
if (_autoAssignMusic()) { refreshDisplay(); _showSavedToast('Music randomized'); }
|
|
1213
|
+
});
|
|
1214
|
+
break;
|
|
1215
|
+
|
|
1216
|
+
case 'both':
|
|
1217
|
+
_closeMenu(() => { _autoAssignAll(); });
|
|
1218
|
+
break;
|
|
1219
|
+
|
|
1220
|
+
case 'setMusic':
|
|
1221
|
+
_closeMenu(() => {
|
|
1222
|
+
openTrackPicker(screen, '', 70, (track, volume) => {
|
|
1223
|
+
_agents.forEach(agent => {
|
|
1224
|
+
const p = voiceStore.getAgentProfile(agent.id);
|
|
1225
|
+
voiceStore.setAgentProfile(agent.id, {
|
|
1226
|
+
backgroundMusic: { track, volume, enabled: true },
|
|
1227
|
+
});
|
|
1228
|
+
});
|
|
1229
|
+
refreshDisplay();
|
|
1230
|
+
_showSavedToast('Music set for all agents');
|
|
1231
|
+
agentList.focus();
|
|
1232
|
+
}, () => { agentList.focus(); screen.render(); });
|
|
1233
|
+
});
|
|
1234
|
+
break;
|
|
1235
|
+
|
|
1236
|
+
case 'setVolume':
|
|
1237
|
+
_closeMenu(() => {
|
|
1238
|
+
const curVol = voiceStore.getAgentProfile(_agents[0]?.id)?.backgroundMusic?.volume ?? 70;
|
|
1239
|
+
openVolumeInput(screen, curVol, (volume) => {
|
|
1240
|
+
_agents.forEach(agent => {
|
|
1241
|
+
const p = voiceStore.getAgentProfile(agent.id);
|
|
1242
|
+
const bm = p.backgroundMusic || {};
|
|
1243
|
+
voiceStore.setAgentProfile(agent.id, {
|
|
1244
|
+
backgroundMusic: { ...bm, volume },
|
|
1245
|
+
});
|
|
1246
|
+
});
|
|
1247
|
+
refreshDisplay();
|
|
1248
|
+
_showSavedToast(`Volume set to ${volume}% for all agents`);
|
|
1249
|
+
agentList.focus();
|
|
1250
|
+
}, () => { agentList.focus(); screen.render(); });
|
|
1251
|
+
});
|
|
1252
|
+
break;
|
|
1253
|
+
|
|
1254
|
+
case 'setPretext':
|
|
1255
|
+
_closeMenu(() => { _openBulkPretextEditor(); });
|
|
1256
|
+
break;
|
|
1257
|
+
|
|
1258
|
+
case 'setReverb':
|
|
1259
|
+
_closeMenu(() => {
|
|
1260
|
+
openReverbPicker(screen, '', (val) => {
|
|
1261
|
+
_agents.forEach(agent => voiceStore.setAgentProfile(agent.id, { reverbPreset: val }));
|
|
1262
|
+
refreshDisplay();
|
|
1263
|
+
_showSavedToast('Reverb set for all agents');
|
|
1264
|
+
agentList.focus();
|
|
1265
|
+
}, () => { agentList.focus(); screen.render(); }, { applyToEffectsManager: false });
|
|
1266
|
+
});
|
|
1267
|
+
break;
|
|
1268
|
+
|
|
1269
|
+
case 'resetAll':
|
|
1270
|
+
_closeMenu(() => {
|
|
1271
|
+
_agents.forEach(agent => voiceStore.resetAgentProfile(agent.id));
|
|
1272
|
+
refreshDisplay();
|
|
1273
|
+
_showSavedToast('All profiles reset');
|
|
1274
|
+
});
|
|
1275
|
+
break;
|
|
1276
|
+
}
|
|
1277
|
+
});
|
|
1278
|
+
|
|
1279
|
+
menuList.key(['escape', 'q'], () => {
|
|
1280
|
+
_closeMenu(() => { agentList.focus(); screen.render(); });
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
function _openBulkPretextEditor() {
|
|
1285
|
+
const editModal = blessed.box({
|
|
1286
|
+
parent: screen,
|
|
1287
|
+
top: 'center',
|
|
1288
|
+
left: 'center',
|
|
1289
|
+
width: 60,
|
|
1290
|
+
height: 9,
|
|
1291
|
+
border: { type: 'line' },
|
|
1292
|
+
tags: true,
|
|
1293
|
+
label: _modalTitle('Set Pretext for All Agents'),
|
|
1294
|
+
style: { fg: COLORS.labelFg, bg: COLORS.contentBg, border: { fg: '#00e5ff' } },
|
|
1295
|
+
});
|
|
1296
|
+
editModal.setFront();
|
|
1297
|
+
|
|
1298
|
+
blessed.text({
|
|
1299
|
+
parent: editModal, top: 1, left: 2,
|
|
1300
|
+
content: 'Pretext to apply to all agents (leave empty to clear):',
|
|
1301
|
+
style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
|
|
1302
|
+
});
|
|
1303
|
+
|
|
1304
|
+
const inputBox = blessed.textbox({
|
|
1305
|
+
parent: editModal, top: 3, left: 2, right: 2, height: 3,
|
|
1306
|
+
border: { type: 'line' },
|
|
1307
|
+
inputOnFocus: true,
|
|
1308
|
+
style: {
|
|
1309
|
+
fg: COLORS.valueFg, bg: '#0d1b35',
|
|
1310
|
+
border: { fg: COLORS.borderFg },
|
|
1311
|
+
focus: { border: { fg: '#00e5ff' } },
|
|
1312
|
+
},
|
|
1313
|
+
});
|
|
1314
|
+
|
|
1315
|
+
blessed.text({
|
|
1316
|
+
parent: editModal, bottom: 1, left: 2, tags: true,
|
|
1317
|
+
content: '{#455a64-fg}[Enter] Apply to all [Esc] Cancel{/#455a64-fg}',
|
|
1318
|
+
style: { bg: COLORS.contentBg },
|
|
1319
|
+
});
|
|
1320
|
+
|
|
1321
|
+
let _closed = false;
|
|
1322
|
+
function _close(save) {
|
|
1323
|
+
if (_closed) return;
|
|
1324
|
+
_closed = true;
|
|
1325
|
+
if (save) {
|
|
1326
|
+
const raw = inputBox.getValue().trim().slice(0, MAX_PRETEXT_LENGTH);
|
|
1327
|
+
_agents.forEach(agent => {
|
|
1328
|
+
if (raw) {
|
|
1329
|
+
voiceStore.setAgentProfile(agent.id, { pretext: raw });
|
|
1330
|
+
} else {
|
|
1331
|
+
const p = voiceStore.getAgentProfile(agent.id);
|
|
1332
|
+
const { pretext: _removed, ...rest } = p;
|
|
1333
|
+
voiceStore.resetAgentProfile(agent.id);
|
|
1334
|
+
if (Object.keys(rest).length > 0) voiceStore.setAgentProfile(agent.id, rest);
|
|
1335
|
+
}
|
|
1336
|
+
});
|
|
1337
|
+
refreshDisplay();
|
|
1338
|
+
_showSavedToast('Pretext set for all agents');
|
|
1339
|
+
}
|
|
1340
|
+
destroyList(editModal, screen, () => { agentList.focus(); screen.render(); });
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
inputBox.key(['enter'], () => _close(true));
|
|
1344
|
+
inputBox.key(['escape'], () => _close(false));
|
|
1345
|
+
inputBox.focus();
|
|
248
1346
|
screen.render();
|
|
249
1347
|
}
|
|
250
1348
|
|
|
251
1349
|
// -------------------------------------------------------------------------
|
|
252
1350
|
// Key bindings
|
|
253
1351
|
|
|
254
|
-
agentList.key(['
|
|
1352
|
+
agentList.key(['x', 'X'], () => {
|
|
255
1353
|
const agent = _agents[agentList.selected];
|
|
256
1354
|
if (agent) {
|
|
257
|
-
voiceStore.
|
|
1355
|
+
voiceStore.resetAgentProfile(agent.id);
|
|
258
1356
|
refreshDisplay();
|
|
259
1357
|
}
|
|
260
1358
|
});
|
|
261
1359
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
1360
|
+
|
|
1361
|
+
agentList.key(['enter'], () => {
|
|
1362
|
+
const agent = _agents[agentList.selected];
|
|
1363
|
+
if (agent) _openAgentDetailPanel(agent);
|
|
266
1364
|
});
|
|
267
1365
|
|
|
268
|
-
|
|
269
|
-
|
|
1366
|
+
agentList.key(['space'], () => {
|
|
1367
|
+
const agent = _agents[agentList.selected];
|
|
1368
|
+
if (agent) _sampleAgent(agent);
|
|
1369
|
+
});
|
|
1370
|
+
|
|
1371
|
+
agentList.key(['a', 'A'], () => { _autoAssignAll(); });
|
|
1372
|
+
agentList.key(['b', 'B'], () => { _openBulkEditMenu(); });
|
|
1373
|
+
|
|
1374
|
+
// Type-to-jump
|
|
1375
|
+
const _agentJumpBlocked = new Set(['j', 'k', 'g', 'h', 'l', 'd', 'u', 'x', 'a', 'b']);
|
|
270
1376
|
agentList.on('keypress', (ch, key) => {
|
|
271
1377
|
if (!ch || key.ctrl || key.meta) return;
|
|
272
1378
|
const lower = ch.toLowerCase();
|
|
@@ -286,50 +1392,84 @@ export function createAgentsTab(screen, services) {
|
|
|
286
1392
|
}
|
|
287
1393
|
});
|
|
288
1394
|
|
|
289
|
-
//
|
|
1395
|
+
// Inline row hint (appended to selected row while list is focused)
|
|
1396
|
+
let _listFocused = false;
|
|
1397
|
+
let _hintIdx = -1;
|
|
1398
|
+
let _hintBase = ''; // row content before hint was appended (no hint, no █)
|
|
1399
|
+
|
|
1400
|
+
function _updateHint(idx) {
|
|
1401
|
+
const items = agentList.items;
|
|
1402
|
+
if (_hintIdx >= 0 && _hintIdx !== idx && items[_hintIdx]) {
|
|
1403
|
+
const hadBlink = (items[_hintIdx].content ?? '').endsWith(' █');
|
|
1404
|
+
items[_hintIdx].setContent(hadBlink ? _hintBase + ' █' : _hintBase);
|
|
1405
|
+
}
|
|
1406
|
+
if (idx >= 0 && items[idx]) {
|
|
1407
|
+
let c = items[idx].content ?? '';
|
|
1408
|
+
const hasBlink = c.endsWith(' █');
|
|
1409
|
+
if (hasBlink) c = c.slice(0, -3);
|
|
1410
|
+
_hintBase = c;
|
|
1411
|
+
items[idx].setContent(c + _ROW_HINT_BMAD + (hasBlink ? ' █' : ''));
|
|
1412
|
+
} else {
|
|
1413
|
+
_hintBase = '';
|
|
1414
|
+
}
|
|
1415
|
+
_hintIdx = idx;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
// Blinking cursor
|
|
290
1419
|
let _alBlink = { interval: null, on: false, sel: -1 };
|
|
291
1420
|
function _alTick() {
|
|
292
1421
|
_alBlink.on = !_alBlink.on;
|
|
293
1422
|
const items = agentList.items;
|
|
294
1423
|
const cur = agentList.selected ?? 0;
|
|
295
1424
|
if (_alBlink.sel !== cur && _alBlink.sel >= 0 && items[_alBlink.sel]) {
|
|
296
|
-
items[_alBlink.sel].setContent((items[_alBlink.sel].content ?? '').replace(/
|
|
1425
|
+
items[_alBlink.sel].setContent((items[_alBlink.sel].content ?? '').replace(/ █$/, ''));
|
|
297
1426
|
}
|
|
298
1427
|
_alBlink.sel = cur;
|
|
299
1428
|
if (items[cur]) {
|
|
300
|
-
const base = (items[cur].content ?? '').replace(/
|
|
301
|
-
items[cur].setContent(_alBlink.on ? `${base}
|
|
1429
|
+
const base = (items[cur].content ?? '').replace(/ █$/, '');
|
|
1430
|
+
items[cur].setContent(_alBlink.on ? `${base} █` : base);
|
|
302
1431
|
}
|
|
303
1432
|
screen.render();
|
|
304
1433
|
}
|
|
305
1434
|
agentList.on('focus', () => {
|
|
1435
|
+
_listFocused = true;
|
|
306
1436
|
_alBlink.on = true;
|
|
307
1437
|
_alBlink.sel = agentList.selected ?? 0;
|
|
1438
|
+
_hintIdx = -1;
|
|
1439
|
+
_hintBase = '';
|
|
1440
|
+
_updateHint(_alBlink.sel);
|
|
308
1441
|
const items = agentList.items;
|
|
309
|
-
if (items[_alBlink.sel]) items[_alBlink.sel].setContent((items[_alBlink.sel].content ?? '') + '
|
|
1442
|
+
if (items[_alBlink.sel]) items[_alBlink.sel].setContent((items[_alBlink.sel].content ?? '') + ' █');
|
|
310
1443
|
screen.render();
|
|
311
1444
|
_alBlink.interval = setInterval(_alTick, 500);
|
|
312
1445
|
});
|
|
313
1446
|
agentList.on('blur', () => {
|
|
1447
|
+
_listFocused = false;
|
|
314
1448
|
if (_alBlink.interval) { clearInterval(_alBlink.interval); _alBlink.interval = null; }
|
|
315
1449
|
const items = agentList.items;
|
|
316
1450
|
const sel = agentList.selected ?? 0;
|
|
317
|
-
if (items[sel])
|
|
1451
|
+
if (items[sel]) {
|
|
1452
|
+
items[sel].setContent(sel === _hintIdx ? _hintBase : (items[sel].content ?? '').replace(/ █$/, ''));
|
|
1453
|
+
}
|
|
1454
|
+
if (_hintIdx >= 0 && _hintIdx !== sel && items[_hintIdx]) {
|
|
1455
|
+
items[_hintIdx].setContent(_hintBase);
|
|
1456
|
+
}
|
|
1457
|
+
_hintIdx = -1;
|
|
1458
|
+
_hintBase = '';
|
|
318
1459
|
screen.render();
|
|
319
1460
|
});
|
|
320
1461
|
agentList.on('select item', () => {
|
|
1462
|
+
_updateHint(agentList.selected ?? 0);
|
|
321
1463
|
if (_alBlink.interval) _alTick();
|
|
322
1464
|
});
|
|
323
1465
|
|
|
324
|
-
//
|
|
1466
|
+
// Navigation: up at top → tab bar, escape → tab bar
|
|
325
1467
|
agentList.key(['up'], () => {
|
|
326
1468
|
if (agentList.selected === 0 && typeof focusMainTabBar === 'function') {
|
|
327
1469
|
focusMainTabBar();
|
|
328
1470
|
setTimeout(() => { agentList.select(0); screen.render(); }, 0);
|
|
329
1471
|
}
|
|
330
1472
|
});
|
|
331
|
-
|
|
332
|
-
// Escape at the list level → return to header tab bar
|
|
333
1473
|
agentList.key(['escape'], () => {
|
|
334
1474
|
if (typeof focusMainTabBar === 'function') { focusMainTabBar(); screen.render(); }
|
|
335
1475
|
});
|
|
@@ -347,19 +1487,26 @@ export function createAgentsTab(screen, services) {
|
|
|
347
1487
|
},
|
|
348
1488
|
|
|
349
1489
|
hide() {
|
|
1490
|
+
_killPreview();
|
|
350
1491
|
box.hide();
|
|
351
1492
|
screen.render();
|
|
352
1493
|
},
|
|
353
1494
|
|
|
354
1495
|
onFocus() {
|
|
355
|
-
|
|
1496
|
+
if (_bmadDetected) {
|
|
1497
|
+
agentList.focus();
|
|
1498
|
+
} else {
|
|
1499
|
+
onboardingBox.focus();
|
|
1500
|
+
}
|
|
356
1501
|
screen.render();
|
|
357
1502
|
},
|
|
358
1503
|
|
|
359
|
-
onBlur() {
|
|
1504
|
+
onBlur() {
|
|
1505
|
+
_killPreview();
|
|
1506
|
+
},
|
|
360
1507
|
|
|
361
1508
|
getFooterText() {
|
|
362
|
-
return
|
|
1509
|
+
return _bmadDetected ? FOOTER_TEXT_BMAD : FOOTER_TEXT_NOBMAD;
|
|
363
1510
|
},
|
|
364
1511
|
|
|
365
1512
|
getFooterColor() {
|