agentvibes 5.10.1 → 5.11.1
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/config.json +15 -1
- package/.agentvibes/install-manifest.json +342 -0
- package/.claude/audio/ui/CREDITS.txt +16 -0
- package/.claude/audio/ui/bling-success.wav +0 -0
- package/.claude/commands/agent-vibes/receiver.md +64 -0
- package/.claude/commands/agent-vibes-bmad-voices.md +117 -117
- package/.claude/commands/agent-vibes-rdp.md +24 -24
- package/.claude/config/audio-effects.cfg +5 -4
- package/.claude/config/audio-effects.cfg.sample +52 -52
- package/.claude/config/background-music-enabled.txt +1 -1
- package/.claude/config/background-music-position.txt +27 -0
- package/.claude/config/background-music-volume.txt +1 -0
- package/.claude/config/background-music.cfg +1 -0
- package/.claude/config/background-music.txt +1 -0
- package/.claude/config/language.txt +1 -0
- package/.claude/config/personality.txt +1 -0
- package/.claude/config/reverb-level.txt +1 -0
- package/.claude/config/tts-speech-rate.txt +1 -0
- package/.claude/config/tts-verbosity.txt +1 -0
- package/.claude/docs/TERMUX_SETUP.md +408 -408
- package/.claude/github-star-reminder.txt +1 -1
- package/.claude/hooks/audio-cache-utils.sh +0 -0
- package/.claude/hooks/audio-processor.sh +57 -18
- package/.claude/hooks/background-music-manager.sh +0 -0
- package/.claude/hooks/bmad-party-speak.sh +0 -0
- package/.claude/hooks/bmad-speak-enhanced.sh +0 -0
- package/.claude/hooks/bmad-speak.sh +0 -0
- package/.claude/hooks/bmad-tts-injector.sh +0 -0
- package/.claude/hooks/bmad-voice-manager.sh +0 -0
- package/.claude/hooks/clawdbot-receiver-SECURE.sh +0 -0
- package/.claude/hooks/clawdbot-receiver.sh +0 -0
- package/.claude/hooks/clean-audio-cache.sh +0 -0
- package/.claude/hooks/cleanup-cache.sh +0 -0
- package/.claude/hooks/configure-rdp-mode.sh +0 -0
- package/.claude/hooks/download-extra-voices.sh +0 -0
- package/.claude/hooks/effects-manager.sh +0 -0
- package/.claude/hooks/github-star-reminder.sh +0 -0
- package/.claude/hooks/kokoro-installer.sh +117 -0
- package/.claude/hooks/kokoro-server.py +219 -0
- package/.claude/hooks/kokoro-tts.py +141 -0
- package/.claude/hooks/language-manager.sh +0 -0
- package/.claude/hooks/learn-manager.sh +0 -0
- package/.claude/hooks/macos-voice-manager.sh +0 -0
- package/.claude/hooks/migrate-background-music.sh +0 -0
- package/.claude/hooks/migrate-to-agentvibes.sh +0 -0
- package/.claude/hooks/optimize-background-music.sh +0 -0
- package/.claude/hooks/path-resolver.sh +0 -0
- package/.claude/hooks/personality-manager.sh +0 -0
- package/.claude/hooks/piper-download-voices.sh +39 -16
- package/.claude/hooks/piper-installer.sh +11 -6
- package/.claude/hooks/piper-multispeaker-registry.sh +0 -0
- package/.claude/hooks/piper-voice-manager.sh +6 -6
- package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +3 -1
- package/.claude/hooks/play-tts-agentvibes-receiver.sh +1 -0
- package/.claude/hooks/play-tts-elevenlabs.sh +360 -0
- package/.claude/hooks/play-tts-enhanced.sh +0 -0
- package/.claude/hooks/play-tts-kokoro.sh +127 -0
- package/.claude/hooks/play-tts-macos.sh +0 -0
- package/.claude/hooks/play-tts-piper.sh +0 -0
- package/.claude/hooks/play-tts-soprano.sh +0 -0
- package/.claude/hooks/play-tts-ssh-remote.sh +9 -2
- package/.claude/hooks/play-tts-termux-ssh.sh +3 -1
- package/.claude/hooks/play-tts-windows-receiver.sh +0 -0
- package/.claude/hooks/play-tts.sh +78 -2
- package/.claude/hooks/prepare-release.sh +0 -0
- package/.claude/hooks/provider-commands.sh +71 -22
- package/.claude/hooks/provider-manager.sh +15 -7
- package/.claude/hooks/replay-target-audio.sh +0 -0
- package/.claude/hooks/requirements.txt +6 -6
- package/.claude/hooks/sentiment-manager.sh +0 -0
- package/.claude/hooks/session-start-tts.sh +0 -0
- package/.claude/hooks/soprano-gradio-synth.py +139 -139
- package/.claude/hooks/speed-manager.sh +0 -0
- package/.claude/hooks/stop-tts.sh +0 -0
- package/.claude/hooks/termux-installer.sh +0 -0
- package/.claude/hooks/translate-manager.sh +0 -0
- package/.claude/hooks/translator.py +237 -237
- package/.claude/hooks/tts-queue-worker.sh +0 -0
- package/.claude/hooks/tts-queue.sh +0 -0
- package/.claude/hooks/verbosity-manager.sh +0 -0
- package/.claude/hooks/voice-manager.sh +0 -0
- package/.claude/hooks-windows/audio-cache-utils.ps1.user.bak +119 -0
- package/.claude/hooks-windows/kokoro-server.py +219 -0
- package/.claude/hooks-windows/kokoro-tts.py +107 -0
- package/.claude/hooks-windows/play-tts-kokoro.ps1 +191 -0
- package/.claude/hooks-windows/play-tts.ps1 +29 -1
- package/.claude/hooks-windows/soprano-gradio-synth.py +153 -153
- package/.claude/hooks-windows/soprano-gradio-synth.py.user.bak +153 -0
- package/.claude/piper-voices-dir.txt +1 -0
- package/.claude/verbosity.txt +1 -1
- package/.clawdbot/README.md +105 -105
- package/.mcp.json +6 -19
- package/README.md +127 -2116
- package/RELEASE_NOTES.md +73 -0
- package/WINDOWS-SETUP.md +208 -208
- package/bin/agent-vibes +39 -39
- package/bin/agentvibes-voice-browser.js +0 -0
- package/bin/agentvibes.js +0 -0
- package/bin/mcp-server.js +121 -121
- package/bin/mcp-server.sh +0 -0
- 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 +0 -0
- 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 +0 -0
- package/mcp-server/server.py +1807 -1807
- package/mcp-server/test_server.py +0 -0
- package/package.json +4 -3
- 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 +3 -3
- package/src/console/app.js +25 -9
- package/src/console/audio-env.js +85 -1
- package/src/console/brand-colors.js +13 -13
- package/src/console/constants/personalities.js +44 -44
- package/src/console/navigation.js +4 -0
- package/src/console/tabs/agents-tab.js +12 -6
- package/src/console/tabs/help-tab.js +314 -314
- package/src/console/tabs/music-tab.js +7 -25
- package/src/console/tabs/readme-tab.js +272 -272
- package/src/console/tabs/setup-tab.js +1704 -120
- package/src/console/tabs/voices-tab.js +72 -88
- package/src/console/widgets/destroy-list.js +25 -25
- package/src/console/widgets/format-utils.js +14 -2
- package/src/console/widgets/help-bar.js +55 -0
- package/src/console/widgets/notice.js +55 -55
- package/src/console/widgets/reverb-picker.js +429 -41
- package/src/console/widgets/track-picker.js +60 -51
- package/src/i18n/de.js +2 -1
- package/src/i18n/en.js +1 -0
- package/src/i18n/es.js +2 -1
- package/src/i18n/fr.js +2 -1
- package/src/i18n/hi.js +2 -1
- package/src/i18n/ja.js +2 -1
- package/src/i18n/ko.js +2 -1
- package/src/i18n/pt.js +2 -1
- package/src/i18n/strings.js +9 -9
- package/src/i18n/zh-CN.js +2 -1
- package/src/installer/language-screen.js +31 -31
- package/src/installer/music-file-input.js +304 -304
- package/src/installer.js +95 -7
- package/src/services/config-service.js +264 -264
- package/src/services/language-service.js +47 -47
- package/src/services/provider-service.js +35 -0
- package/src/services/provider-voice-catalog.js +126 -0
- package/src/services/tts-engine-service.js +51 -2
- package/src/utils/audio-duration-validator.js +53 -10
- package/src/utils/audio-format-validator.js +277 -277
- package/src/utils/dependency-checker.js +469 -469
- package/src/utils/file-ownership-verifier.js +358 -358
- package/src/utils/music-file-validator.js +285 -285
- package/src/utils/preview-list-prompt.js +144 -136
- package/src/utils/provider-validator.js +70 -0
- package/src/utils/secure-music-storage.js +412 -412
- package/templates/agentvibes-receiver.sh +235 -231
- package/templates/audio/welcome-music.mp3 +0 -0
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* AgentVibes TUI — Shared Widget:
|
|
2
|
+
* AgentVibes TUI — Shared Widget: Audio Effects Picker
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Multi-select modal: pick reverb, echo, and chorus independently (combinable).
|
|
5
|
+
* Space = preview highlighted effect on a test tone (sox required).
|
|
6
|
+
* Enter = toggle selection for that category, c = apply, Esc = cancel.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import path from 'node:path';
|
|
9
|
-
import
|
|
10
|
+
import os from 'node:os';
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import crypto from 'node:crypto';
|
|
13
|
+
import { spawnSync, spawn } from 'node:child_process';
|
|
10
14
|
import { destroyList } from './destroy-list.js';
|
|
11
|
-
import {
|
|
15
|
+
import { renderHelpBar, selectorTitle } from './help-bar.js';
|
|
12
16
|
|
|
13
17
|
const IS_TEST = process.env.AGENTVIBES_TEST_MODE === 'true';
|
|
14
18
|
let blessed;
|
|
@@ -17,81 +21,465 @@ if (!IS_TEST) {
|
|
|
17
21
|
blessed = b;
|
|
18
22
|
}
|
|
19
23
|
|
|
20
|
-
|
|
24
|
+
// ── Effect category definitions ───────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
const EFFECT_CATEGORIES = [
|
|
27
|
+
{
|
|
28
|
+
id: 'reverb',
|
|
29
|
+
label: 'Reverb',
|
|
30
|
+
defaultValue: 'off-reverb',
|
|
31
|
+
items: [
|
|
32
|
+
{ label: 'Off (no reverb)', value: 'off-reverb' },
|
|
33
|
+
{ label: 'Light Reverb (small room)', value: 'light' },
|
|
34
|
+
{ label: 'Medium Reverb (conference)', value: 'medium' },
|
|
35
|
+
{ label: 'Heavy Reverb (large hall)', value: 'heavy' },
|
|
36
|
+
{ label: 'Cathedral (epic space)', value: 'cathedral' },
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: 'echo',
|
|
41
|
+
label: 'Echo',
|
|
42
|
+
defaultValue: 'off-echo',
|
|
43
|
+
items: [
|
|
44
|
+
{ label: 'Off (no echo)', value: 'off-echo' },
|
|
45
|
+
{ label: 'Echo (short delay)', value: 'echo-short' },
|
|
46
|
+
{ label: 'Cave Echo (long)', value: 'echo-long' },
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 'chorus',
|
|
51
|
+
label: 'Chorus',
|
|
52
|
+
defaultValue: 'off-chorus',
|
|
53
|
+
items: [
|
|
54
|
+
{ label: 'Off (no chorus)', value: 'off-chorus' },
|
|
55
|
+
{ label: 'Light Chorus', value: 'chorus-light' },
|
|
56
|
+
{ label: 'Deep Chorus', value: 'chorus-deep' },
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
// Character presets are compound — selecting one replaces all category selections
|
|
62
|
+
const CHARACTER_PRESETS = [
|
|
63
|
+
{ label: 'Warm (reverb + bass)', value: 'warm' },
|
|
64
|
+
{ label: 'Radio (EQ + overdrive)', value: 'radio' },
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
// ── REVERB_PRESETS (backward-compat flat export) ──────────────────────────────
|
|
21
68
|
|
|
22
69
|
export const REVERB_PRESETS = Object.freeze([
|
|
23
|
-
{ label: 'Off
|
|
24
|
-
{ label: '
|
|
25
|
-
{ label: '
|
|
26
|
-
{ label: '
|
|
27
|
-
|
|
70
|
+
{ label: 'Off (no effects)', value: 'off' },
|
|
71
|
+
{ label: 'Off (no reverb)', value: 'off-reverb' },
|
|
72
|
+
{ label: 'Off (no echo)', value: 'off-echo' },
|
|
73
|
+
{ label: 'Off (no chorus)', value: 'off-chorus' },
|
|
74
|
+
...EFFECT_CATEGORIES.flatMap(cat => cat.items.filter(i => !i.value.startsWith('off-'))),
|
|
75
|
+
...CHARACTER_PRESETS,
|
|
28
76
|
]);
|
|
29
77
|
|
|
78
|
+
export const AUDIO_EFFECT_PRESETS = REVERB_PRESETS;
|
|
79
|
+
|
|
80
|
+
// ── Label helper ──────────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Format a stored effect value for display.
|
|
84
|
+
* Handles single values ('light') and combined values ('light+echo-short').
|
|
85
|
+
*/
|
|
86
|
+
export function formatEffectLabel(value) {
|
|
87
|
+
if (!value || value === 'off') return 'Off';
|
|
88
|
+
return value.split('+')
|
|
89
|
+
.map(v => v.trim())
|
|
90
|
+
.filter(Boolean)
|
|
91
|
+
.map(v => {
|
|
92
|
+
const p = REVERB_PRESETS.find(r => r.value === v);
|
|
93
|
+
return p ? p.label.replace(/\s{2,}/g, ' ').trim() : v;
|
|
94
|
+
})
|
|
95
|
+
.join(' + ');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ── Parse stored value → selections Map ──────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
function parseEffectValue(value) {
|
|
101
|
+
const sel = new Map();
|
|
102
|
+
for (const cat of EFFECT_CATEGORIES) sel.set(cat.id, cat.defaultValue);
|
|
103
|
+
sel.set('character', null);
|
|
104
|
+
|
|
105
|
+
if (!value || value === 'off') return sel;
|
|
106
|
+
|
|
107
|
+
for (const part of value.split('+').map(s => s.trim()).filter(Boolean)) {
|
|
108
|
+
if (CHARACTER_PRESETS.find(p => p.value === part)) {
|
|
109
|
+
for (const cat of EFFECT_CATEGORIES) sel.set(cat.id, cat.defaultValue);
|
|
110
|
+
sel.set('character', part);
|
|
111
|
+
return sel;
|
|
112
|
+
}
|
|
113
|
+
for (const cat of EFFECT_CATEGORIES) {
|
|
114
|
+
if (cat.items.find(item => item.value === part)) {
|
|
115
|
+
sel.set(cat.id, part);
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return sel;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── Serialize selections Map → stored value ───────────────────────────────────
|
|
124
|
+
|
|
125
|
+
function serializeSelections(sel) {
|
|
126
|
+
const charVal = sel.get('character');
|
|
127
|
+
if (charVal) return charVal;
|
|
128
|
+
const parts = [];
|
|
129
|
+
for (const cat of EFFECT_CATEGORIES) {
|
|
130
|
+
const v = sel.get(cat.id);
|
|
131
|
+
if (v && !v.startsWith('off-')) parts.push(v);
|
|
132
|
+
}
|
|
133
|
+
return parts.length ? parts.join('+') : 'off';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ── TTS + effect preview ──────────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
function previewEffectWithVoice(effectValue, phrase, opts, onDone) {
|
|
139
|
+
if (IS_TEST) return;
|
|
140
|
+
stopPreview();
|
|
141
|
+
const playTts = path.join(opts.previewHooksDir, 'play-tts.sh');
|
|
142
|
+
if (!fs.existsSync(playTts)) { previewEffect(effectValue); onDone?.(); return; }
|
|
143
|
+
|
|
144
|
+
const env = { ...process.env, AGENTVIBES_REVERB_OVERRIDE: effectValue, AGENTVIBES_EFFECTS_PREVIEW: '1' };
|
|
145
|
+
if (opts.previewTargetDir) env.CLAUDE_PROJECT_DIR = opts.previewTargetDir;
|
|
146
|
+
// Force a specific synth provider when the caller knows it (e.g. ElevenLabs),
|
|
147
|
+
// so the orchestrator doesn't fall back to the global provider.
|
|
148
|
+
if (opts.previewForceProvider) env.AGENTVIBES_FORCE_PROVIDER = opts.previewForceProvider;
|
|
149
|
+
|
|
150
|
+
// On Windows, a bare 'bash' resolves to WSL (no audio/key access); the caller
|
|
151
|
+
// passes the real git-bash path via previewBashBin.
|
|
152
|
+
const bashBin = opts.previewBashBin || 'bash';
|
|
153
|
+
const args = [bashBin, playTts, phrase];
|
|
154
|
+
if (opts.previewLlmKey) args.push('--llm', opts.previewLlmKey, '--project-dir', opts.previewTargetDir || '');
|
|
155
|
+
// Pass voice explicitly so the preview uses the current voice, not the LLM row's default
|
|
156
|
+
if (opts.previewVoice) args.push(opts.previewVoice);
|
|
157
|
+
|
|
158
|
+
_previewProc = spawn(args[0], args.slice(1), { stdio: 'ignore', env });
|
|
159
|
+
if (_previewProc) {
|
|
160
|
+
_previewProc.on('close', () => { _previewProc = null; onDone?.(); });
|
|
161
|
+
_previewProc.on('error', () => { _previewProc = null; onDone?.(); });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ── Sox effect preview ────────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
const SOX_EFFECT_MAP = {
|
|
168
|
+
'off': '', 'off-reverb': '', 'off-echo': '', 'off-chorus': '',
|
|
169
|
+
'light': 'reverb 20 50 50',
|
|
170
|
+
'medium': 'reverb 40 50 70',
|
|
171
|
+
'heavy': 'reverb 70 50 100',
|
|
172
|
+
'cathedral': 'reverb 90 30 100',
|
|
173
|
+
'chorus-light': 'chorus 0.7 0.9 55 0.4 0.25 2 -t',
|
|
174
|
+
'chorus-deep': 'chorus 0.8 0.9 55 0.4 0.25 2 -t chorus 0.8 0.9 44 0.4 0.2 2.3 -t',
|
|
175
|
+
'echo-short': 'echo 0.8 0.6 60 0.4',
|
|
176
|
+
'echo-long': 'echo 0.8 0.7 100 0.5 200 0.3',
|
|
177
|
+
'warm': 'bass 5 reverb 30 50 60',
|
|
178
|
+
'radio': 'highpass 300 treble 5 gain -3 overdrive 10 gain -3',
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
let _previewProc = null;
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Create a secure temp .wav path: per-uid subdir under XDG_RUNTIME_DIR (fallback
|
|
185
|
+
* os.tmpdir()) with an unpredictable crypto.randomUUID() filename. Mirrors the
|
|
186
|
+
* private _secureTempWav helper used in the tabs (not importable without a larger
|
|
187
|
+
* refactor), so it is replicated here.
|
|
188
|
+
*/
|
|
189
|
+
function _secureTempWav(prefix) {
|
|
190
|
+
const baseDir = process.env.XDG_RUNTIME_DIR || os.tmpdir();
|
|
191
|
+
const dir = path.join(baseDir, `agentvibes-${process.getuid?.() ?? 'u'}`);
|
|
192
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
193
|
+
try { fs.chmodSync(dir, 0o700); } catch {}
|
|
194
|
+
return path.join(dir, `${prefix}-${crypto.randomUUID()}.wav`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function stopPreview() {
|
|
198
|
+
if (_previewProc && !_previewProc.killed) {
|
|
199
|
+
try { _previewProc.kill(); } catch {}
|
|
200
|
+
}
|
|
201
|
+
_previewProc = null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function previewEffect(effectValue) {
|
|
205
|
+
if (IS_TEST || (process.platform === 'win32' && !process.env.WSL_DISTRO_NAME)) return;
|
|
206
|
+
|
|
207
|
+
stopPreview();
|
|
208
|
+
|
|
209
|
+
const soxFx = SOX_EFFECT_MAP[effectValue] ?? '';
|
|
210
|
+
const tonePath = _secureTempWav('av-tone');
|
|
211
|
+
const prevPath = _secureTempWav('av-prev');
|
|
212
|
+
|
|
213
|
+
const cleanup = () => {
|
|
214
|
+
try { if (fs.existsSync(tonePath)) fs.unlinkSync(tonePath); } catch {}
|
|
215
|
+
try { if (fs.existsSync(prevPath)) fs.unlinkSync(prevPath); } catch {}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const doPlay = (file) => {
|
|
219
|
+
// S4036: 'play'/'sox' are standard SoX tools on the user's local PATH; absolute
|
|
220
|
+
// paths aren't portable across distros/Homebrew. Risk accepted.
|
|
221
|
+
_previewProc = spawn('play', [file], { stdio: 'ignore' }); // NOSONAR
|
|
222
|
+
_previewProc.on('error', cleanup);
|
|
223
|
+
_previewProc.on('close', () => { _previewProc = null; cleanup(); });
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// Generate a two-octave harmonic tone (~1s) — richer than pure sine for hearing effects
|
|
227
|
+
const gen = spawn('sox', [ // NOSONAR — see S4036 note above
|
|
228
|
+
'-n', '-r', '44100', '-c', '1', tonePath,
|
|
229
|
+
'synth', '1.0', 'sine', '220', 'sine', '440',
|
|
230
|
+
'gain', '-n', '-6',
|
|
231
|
+
], { stdio: 'ignore' });
|
|
232
|
+
|
|
233
|
+
_previewProc = gen;
|
|
234
|
+
gen.on('error', cleanup);
|
|
235
|
+
gen.on('close', (code) => {
|
|
236
|
+
if (code !== 0 || !fs.existsSync(tonePath)) { cleanup(); return; }
|
|
237
|
+
if (!soxFx) { doPlay(tonePath); return; }
|
|
238
|
+
|
|
239
|
+
const fxProc = spawn('sox', [tonePath, prevPath, ...soxFx.split(/\s+/).filter(Boolean)], { stdio: 'ignore' }); // NOSONAR — see S4036 note above
|
|
240
|
+
_previewProc = fxProc;
|
|
241
|
+
fxProc.on('error', () => doPlay(tonePath));
|
|
242
|
+
fxProc.on('close', (c) => {
|
|
243
|
+
if (c === 0 && fs.existsSync(prevPath)) doPlay(prevPath);
|
|
244
|
+
else doPlay(tonePath);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ── Build flat items list for the blessed list widget ─────────────────────────
|
|
250
|
+
|
|
251
|
+
function buildFlatItems() {
|
|
252
|
+
const items = [];
|
|
253
|
+
items.push({ isHelp: true, label: renderHelpBar([
|
|
254
|
+
{ key: 'Space', label: 'test' },
|
|
255
|
+
{ key: 'Enter', label: 'toggle' },
|
|
256
|
+
{ key: 'A', label: 'apply' },
|
|
257
|
+
{ key: 'Esc', label: 'cancel' },
|
|
258
|
+
]) });
|
|
259
|
+
items.push({ isSpacer: true, label: '' });
|
|
260
|
+
for (const cat of EFFECT_CATEGORIES) {
|
|
261
|
+
items.push({ isHeader: true, label: `── ${cat.label} ` });
|
|
262
|
+
for (const item of cat.items) items.push({ category: cat.id, ...item });
|
|
263
|
+
}
|
|
264
|
+
items.push({ isHeader: true, label: '── Character (compound presets) ' });
|
|
265
|
+
for (const item of CHARACTER_PRESETS) items.push({ category: 'character', ...item });
|
|
266
|
+
// Top padding so the Apply button isn't hunched against the last option. The
|
|
267
|
+
// button is the LAST row so pressing Down to the bottom always lands on it.
|
|
268
|
+
items.push({ isSpacer: true, label: '' });
|
|
269
|
+
items.push({ isApply: true, label: '✓ Apply effects' });
|
|
270
|
+
return items;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const FLAT_ITEMS = buildFlatItems();
|
|
274
|
+
|
|
275
|
+
// ── Main export ───────────────────────────────────────────────────────────────
|
|
276
|
+
|
|
30
277
|
/**
|
|
31
|
-
* Open the
|
|
278
|
+
* Open the multi-select audio effects picker.
|
|
279
|
+
* API-compatible with the old single-select openReverbPicker.
|
|
280
|
+
* currentPreset and onSelect(value) now support combined values like 'light+echo-short'.
|
|
32
281
|
*
|
|
33
282
|
* @param {object} screen - blessed screen
|
|
34
|
-
* @param {string} currentPreset - current
|
|
35
|
-
* @param {Function} onSelect - called with
|
|
283
|
+
* @param {string} currentPreset - current effect value (single or '+'-combined)
|
|
284
|
+
* @param {Function} onSelect - called with the new combined effect value
|
|
36
285
|
* @param {Function} [onClose] - called after modal closes
|
|
37
|
-
* @param {object} [opts]
|
|
38
|
-
* @param {boolean} [opts.applyToEffectsManager=true]
|
|
286
|
+
* @param {object} [opts]
|
|
287
|
+
* @param {boolean} [opts.applyToEffectsManager=true]
|
|
39
288
|
*/
|
|
40
289
|
export function openReverbPicker(screen, currentPreset, onSelect, onClose, opts = {}) {
|
|
41
290
|
const applyToEffectsManager = opts.applyToEffectsManager !== false;
|
|
42
|
-
const
|
|
291
|
+
const selections = parseEffectValue(currentPreset);
|
|
292
|
+
const WIDTH = 52;
|
|
293
|
+
const HEIGHT = Math.min(FLAT_ITEMS.length + 4, Math.max(20, (screen.height || 24) - 2));
|
|
43
294
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
295
|
+
let _previewIdx = -1;
|
|
296
|
+
let _spinFrame = 0;
|
|
297
|
+
const _SPIN = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
|
|
298
|
+
// Tracked selection index so the Apply button (whose solid bg overrides
|
|
299
|
+
// blessed's row highlight) can render a distinct focused state.
|
|
300
|
+
let _selIdx = 0;
|
|
301
|
+
const _applyIdx = FLAT_ITEMS.findIndex(it => it.isApply);
|
|
302
|
+
|
|
303
|
+
const renderItems = () => FLAT_ITEMS.map((item, idx) => {
|
|
304
|
+
if (item.isSpacer) return '';
|
|
305
|
+
if (item.isHelp) return ` ${item.label}`;
|
|
306
|
+
if (item.isHeader) {
|
|
307
|
+
const fill = Math.max(0, WIDTH - 6 - item.label.length);
|
|
308
|
+
return `{#90a4ae-fg}${item.label}${'─'.repeat(fill)}{/#90a4ae-fg}`;
|
|
309
|
+
}
|
|
310
|
+
if (item.isLegend) return `{#90a4ae-fg}${item.label}{/#90a4ae-fg}`;
|
|
311
|
+
if (item.isApply) {
|
|
312
|
+
const btn = ' ✓ Apply effects ';
|
|
313
|
+
const pad = ' '.repeat(Math.max(0, Math.floor((WIDTH - 4 - btn.length) / 2)));
|
|
314
|
+
// Bright + arrows when focused, dim when not — so navigating to it is obvious.
|
|
315
|
+
return idx === _selIdx
|
|
316
|
+
? `${pad}{#43a047-bg}{white-fg}{bold}▸${btn}◂{/bold}{/white-fg}{/#43a047-bg}`
|
|
317
|
+
: `${pad}{#1b3a1f-bg}{#81c784-fg} ${btn} {/#81c784-fg}{/#1b3a1f-bg}`;
|
|
318
|
+
}
|
|
319
|
+
const isSel = item.category === 'character'
|
|
320
|
+
? selections.get('character') === item.value
|
|
321
|
+
: selections.get(item.category) === item.value;
|
|
322
|
+
const dot = isSel ? '{green-fg}●{/green-fg}' : '{#546e7a-fg}○{/#546e7a-fg}';
|
|
323
|
+
const spin = idx === _previewIdx ? ` {cyan-fg}${_SPIN[_spinFrame % _SPIN.length]}{/cyan-fg}` : '';
|
|
324
|
+
return ` ${dot} ${item.label}${spin}`;
|
|
325
|
+
});
|
|
48
326
|
|
|
49
327
|
const list = blessed.list({
|
|
50
328
|
parent: screen,
|
|
51
329
|
top: 'center',
|
|
52
330
|
left: 'center',
|
|
53
|
-
width:
|
|
54
|
-
height:
|
|
331
|
+
width: WIDTH,
|
|
332
|
+
height: HEIGHT,
|
|
55
333
|
border: { type: 'line' },
|
|
56
334
|
tags: true,
|
|
57
|
-
label:
|
|
58
|
-
items:
|
|
59
|
-
keys:
|
|
335
|
+
label: selectorTitle('Audio Effects'),
|
|
336
|
+
items: renderItems(),
|
|
337
|
+
keys: false,
|
|
60
338
|
vi: false,
|
|
61
339
|
mouse: true,
|
|
340
|
+
scrollable: true,
|
|
341
|
+
scrollbar: { ch: '▐', style: { fg: '#2e7d32' } },
|
|
62
342
|
style: {
|
|
63
|
-
border: { fg:
|
|
64
|
-
selected: { bg:
|
|
343
|
+
border: { fg: '#2e7d32' },
|
|
344
|
+
selected: { bg: '#1b3a1f', fg: '#e3f2fd' },
|
|
65
345
|
item: { fg: '#e3f2fd' },
|
|
66
346
|
},
|
|
67
347
|
});
|
|
68
348
|
|
|
69
|
-
|
|
349
|
+
// Position cursor at first active (non-off) selection, or first selectable item
|
|
350
|
+
let initialIdx = FLAT_ITEMS.findIndex(item =>
|
|
351
|
+
item.category &&
|
|
352
|
+
(item.category === 'character'
|
|
353
|
+
? selections.get('character') === item.value
|
|
354
|
+
: selections.get(item.category) === item.value && !item.value.startsWith('off-'))
|
|
355
|
+
);
|
|
356
|
+
if (initialIdx < 0) initialIdx = FLAT_ITEMS.findIndex(item => item.category);
|
|
357
|
+
|
|
358
|
+
_selIdx = Math.max(0, initialIdx);
|
|
359
|
+
list.select(_selIdx);
|
|
70
360
|
list.focus();
|
|
71
361
|
screen.render();
|
|
72
362
|
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
363
|
+
const _refresh = () => {
|
|
364
|
+
const cur = list.selected;
|
|
365
|
+
_selIdx = cur;
|
|
366
|
+
list.setItems(renderItems());
|
|
367
|
+
list.select(cur);
|
|
368
|
+
screen.render();
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
let _spinInterval = null;
|
|
76
372
|
|
|
373
|
+
const _stopSpinner = () => {
|
|
374
|
+
if (_spinInterval) { clearInterval(_spinInterval); _spinInterval = null; }
|
|
375
|
+
if (_previewIdx >= 0) {
|
|
376
|
+
const prevIdx = _previewIdx;
|
|
377
|
+
_previewIdx = -1;
|
|
378
|
+
list.setItem(prevIdx, renderItems()[prevIdx]);
|
|
379
|
+
screen.render();
|
|
380
|
+
}
|
|
381
|
+
_previewIdx = -1;
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
const _startSpinner = (idx) => {
|
|
385
|
+
_stopSpinner();
|
|
386
|
+
_previewIdx = idx;
|
|
387
|
+
_spinFrame = 0;
|
|
388
|
+
_spinInterval = setInterval(() => {
|
|
389
|
+
_spinFrame++;
|
|
390
|
+
// Update only the spinning item — avoids full-list redraw flashing
|
|
391
|
+
list.setItem(_previewIdx, renderItems()[_previewIdx]);
|
|
392
|
+
screen.render();
|
|
393
|
+
}, 80);
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
const _confirm = () => {
|
|
397
|
+
_stopSpinner();
|
|
398
|
+
const val = serializeSelections(selections);
|
|
77
399
|
if (applyToEffectsManager) {
|
|
78
|
-
const
|
|
79
|
-
if (!
|
|
400
|
+
const isWin = process.platform === 'win32' && !process.env.WSL_DISTRO_NAME;
|
|
401
|
+
if (!isWin) {
|
|
80
402
|
const effectsScript = path.join(process.cwd(), '.claude', 'hooks', 'effects-manager.sh');
|
|
81
|
-
spawnSync('bash', [effectsScript, 'set-reverb',
|
|
82
|
-
stdio: 'ignore',
|
|
83
|
-
timeout: 5000,
|
|
84
|
-
env: { ...process.env },
|
|
403
|
+
spawnSync('bash', [effectsScript, 'set-reverb', val, 'default'], { // NOSONAR
|
|
404
|
+
stdio: 'ignore', timeout: 5000, env: { ...process.env },
|
|
85
405
|
});
|
|
86
406
|
}
|
|
87
407
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
onSelect(selected.value);
|
|
408
|
+
stopPreview();
|
|
409
|
+
onSelect(val);
|
|
91
410
|
destroyList(list, screen, onClose);
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
list.key('enter', () => {
|
|
414
|
+
const item = FLAT_ITEMS[list.selected];
|
|
415
|
+
if (!item || item.isHeader || item.isLegend || item.isHelp || item.isSpacer) return;
|
|
416
|
+
if (item.isApply) { _confirm(); return; }
|
|
417
|
+
|
|
418
|
+
if (item.category === 'character') {
|
|
419
|
+
// Toggle: selecting same character preset again turns it off
|
|
420
|
+
if (selections.get('character') === item.value) {
|
|
421
|
+
selections.set('character', null);
|
|
422
|
+
} else {
|
|
423
|
+
// Character presets clear individual category selections
|
|
424
|
+
for (const cat of EFFECT_CATEGORIES) selections.set(cat.id, cat.defaultValue);
|
|
425
|
+
selections.set('character', item.value);
|
|
426
|
+
}
|
|
427
|
+
} else {
|
|
428
|
+
// Selecting a category item clears any active character preset
|
|
429
|
+
selections.set('character', null);
|
|
430
|
+
selections.set(item.category, item.value);
|
|
431
|
+
}
|
|
432
|
+
_refresh();
|
|
92
433
|
});
|
|
93
434
|
|
|
435
|
+
list.key('space', () => {
|
|
436
|
+
const idx = list.selected;
|
|
437
|
+
const item = FLAT_ITEMS[idx];
|
|
438
|
+
if (!item || item.isHeader || item.isLegend || item.isApply || item.isHelp || item.isSpacer) return;
|
|
439
|
+
if (opts.previewHooksDir) {
|
|
440
|
+
const name = item.label.replace(/\s{2,}\(.*?\)\s*$/, '').trim();
|
|
441
|
+
_startSpinner(idx);
|
|
442
|
+
previewEffectWithVoice(item.value, `Testing ${name}.`, opts, _stopSpinner);
|
|
443
|
+
} else {
|
|
444
|
+
previewEffect(item.value);
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
list.key(['a', 'A', 'c', 'C'], _confirm);
|
|
449
|
+
|
|
450
|
+
// Navigation skips decoration (headers, spacers, help) so arrow keys move only
|
|
451
|
+
// between real options and the Apply button — Down to the bottom always lands
|
|
452
|
+
// on Apply. (keys:false above disables blessed's built-in nav so we own it.)
|
|
453
|
+
const _isSelectable = (i) => {
|
|
454
|
+
const it = FLAT_ITEMS[i];
|
|
455
|
+
return Boolean(it && (it.category || it.isApply));
|
|
456
|
+
};
|
|
457
|
+
// Select row i and repaint the Apply button so its focused/dim state follows.
|
|
458
|
+
const _selectRow = (i) => {
|
|
459
|
+
_selIdx = i;
|
|
460
|
+
list.select(i);
|
|
461
|
+
if (_applyIdx >= 0) list.setItem(_applyIdx, renderItems()[_applyIdx]);
|
|
462
|
+
screen.render();
|
|
463
|
+
};
|
|
464
|
+
const _move = (dir) => {
|
|
465
|
+
let i = list.selected;
|
|
466
|
+
do { i += dir; } while (i >= 0 && i < FLAT_ITEMS.length && !_isSelectable(i));
|
|
467
|
+
if (i >= 0 && i < FLAT_ITEMS.length && _isSelectable(i)) _selectRow(i);
|
|
468
|
+
};
|
|
469
|
+
const _jump = (dir) => {
|
|
470
|
+
const idx = dir > 0
|
|
471
|
+
? FLAT_ITEMS.map((_, i) => i).reverse().find(_isSelectable)
|
|
472
|
+
: FLAT_ITEMS.findIndex((_, i) => _isSelectable(i));
|
|
473
|
+
if (idx >= 0) _selectRow(idx);
|
|
474
|
+
};
|
|
475
|
+
list.key(['down', 'j'], () => _move(1));
|
|
476
|
+
list.key(['up', 'k'], () => _move(-1));
|
|
477
|
+
list.key(['home'], () => _jump(-1));
|
|
478
|
+
list.key(['end'], () => _jump(1)); // jump straight to Apply
|
|
479
|
+
|
|
94
480
|
list.key(['escape', 'q', 'Q'], () => {
|
|
481
|
+
_stopSpinner();
|
|
482
|
+
stopPreview();
|
|
95
483
|
destroyList(list, screen, onClose);
|
|
96
484
|
});
|
|
97
485
|
}
|