agentvibes 4.0.1 → 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agentvibes/bmad/bmad-voices.md +69 -69
- package/.agentvibes/config.json +12 -0
- package/.claude/activation-instructions +54 -54
- package/.claude/audio/tracks/README.md +52 -52
- package/.claude/commands/agent-vibes/add.md +21 -21
- package/.claude/commands/agent-vibes/agent-vibes.md +101 -101
- package/.claude/commands/agent-vibes/agent.md +79 -79
- package/.claude/commands/agent-vibes/background-music.md +111 -111
- package/.claude/commands/agent-vibes/bmad.md +198 -198
- package/.claude/commands/agent-vibes/clean.md +18 -18
- package/.claude/commands/agent-vibes/cleanup.md +18 -18
- package/.claude/commands/agent-vibes/commands.json +145 -145
- package/.claude/commands/agent-vibes/effects.md +97 -97
- package/.claude/commands/agent-vibes/get.md +9 -9
- package/.claude/commands/agent-vibes/hide.md +91 -91
- package/.claude/commands/agent-vibes/language.md +23 -23
- package/.claude/commands/agent-vibes/learn.md +67 -67
- package/.claude/commands/agent-vibes/list.md +13 -13
- package/.claude/commands/agent-vibes/mute.md +37 -37
- package/.claude/commands/agent-vibes/preview.md +17 -17
- package/.claude/commands/agent-vibes/provider.md +68 -68
- package/.claude/commands/agent-vibes/replay-target.md +14 -14
- package/.claude/commands/agent-vibes/sample.md +12 -12
- package/.claude/commands/agent-vibes/set-favorite-voice.md +84 -84
- package/.claude/commands/agent-vibes/set-pretext.md +65 -65
- package/.claude/commands/agent-vibes/set-speed.md +41 -41
- package/.claude/commands/agent-vibes/show.md +84 -84
- package/.claude/commands/agent-vibes/switch.md +87 -87
- package/.claude/commands/agent-vibes/target-voice.md +26 -26
- package/.claude/commands/agent-vibes/target.md +30 -30
- package/.claude/commands/agent-vibes/translate.md +68 -68
- package/.claude/commands/agent-vibes/unmute.md +45 -45
- package/.claude/commands/agent-vibes/verbosity.md +89 -89
- package/.claude/commands/agent-vibes/whoami.md +7 -7
- package/.claude/commands/agent-vibes-bmad-voices.md +117 -117
- package/.claude/commands/agent-vibes-rdp.md +24 -24
- package/.claude/config/agentvibes.json +1 -0
- package/.claude/config/audio-effects.cfg +3 -2
- package/.claude/config/audio-effects.cfg.sample +52 -52
- package/.claude/config/background-music-volume.txt +1 -0
- package/.claude/config/intro-text.txt +1 -0
- package/.claude/config/piper-speech-rate.txt +4 -0
- package/.claude/config/piper-target-speech-rate.txt +1 -0
- package/.claude/config/reverb-level.txt +1 -0
- package/.claude/config/tts-speech-rate.txt +4 -0
- package/.claude/config/tts-target-speech-rate.txt +1 -0
- package/.claude/docs/TERMUX_SETUP.md +408 -408
- package/.claude/github-star-reminder.txt +1 -1
- package/.claude/hooks/README-TTS-QUEUE.md +135 -135
- package/.claude/hooks/audio-cache-utils.sh +246 -246
- package/.claude/hooks/audio-processor.sh +433 -389
- package/.claude/hooks/background-music-manager.sh +404 -404
- package/.claude/hooks/bmad-speak-enhanced.sh +165 -165
- package/.claude/hooks/bmad-speak.sh +269 -112
- package/.claude/hooks/bmad-tts-injector.sh +568 -568
- package/.claude/hooks/bmad-voice-manager.sh +928 -928
- package/.claude/hooks/clawdbot-receiver-SECURE.sh +129 -129
- package/.claude/hooks/clawdbot-receiver.sh +107 -107
- package/.claude/hooks/clean-audio-cache.sh +22 -22
- package/.claude/hooks/cleanup-cache.sh +106 -106
- package/.claude/hooks/configure-rdp-mode.sh +137 -137
- package/.claude/hooks/download-extra-voices.sh +244 -244
- package/.claude/hooks/effects-manager.sh +268 -268
- package/.claude/hooks/github-star-reminder.sh +154 -154
- package/.claude/hooks/language-manager.sh +362 -362
- package/.claude/hooks/learn-manager.sh +492 -492
- package/.claude/hooks/macos-voice-manager.sh +205 -205
- package/.claude/hooks/migrate-background-music.sh +125 -125
- package/.claude/hooks/migrate-to-agentvibes.sh +161 -161
- package/.claude/hooks/optimize-background-music.sh +87 -87
- package/.claude/hooks/path-resolver.sh +60 -60
- package/.claude/hooks/personality-manager.sh +448 -448
- package/.claude/hooks/piper-download-voices.sh +225 -225
- package/.claude/hooks/piper-installer.sh +292 -292
- package/.claude/hooks/piper-multispeaker-registry.sh +171 -171
- package/.claude/hooks/piper-voice-manager.sh +24 -3
- package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +90 -90
- package/.claude/hooks/play-tts-enhanced.sh +105 -70
- package/.claude/hooks/play-tts-macos.sh +368 -345
- package/.claude/hooks/play-tts-piper.sh +679 -578
- package/.claude/hooks/play-tts-soprano.sh +356 -320
- package/.claude/hooks/play-tts-ssh-remote.sh +167 -88
- package/.claude/hooks/play-tts-termux-ssh.sh +169 -169
- package/.claude/hooks/play-tts.sh +301 -298
- package/.claude/hooks/prepare-release.sh +54 -54
- package/.claude/hooks/provider-commands.sh +617 -617
- package/.claude/hooks/provider-manager.sh +399 -399
- package/.claude/hooks/replay-target-audio.sh +95 -95
- package/.claude/hooks/requirements.txt +6 -6
- package/.claude/hooks/sentiment-manager.sh +201 -201
- package/.claude/hooks/session-start-tts.sh +81 -71
- package/.claude/hooks/soprano-gradio-synth.py +139 -139
- package/.claude/hooks/speed-manager.sh +291 -291
- package/.claude/hooks/stop-tts.sh +84 -0
- package/.claude/hooks/termux-installer.sh +261 -261
- package/.claude/hooks/translate-manager.sh +341 -341
- package/.claude/hooks/translator.py +237 -237
- package/.claude/hooks/tts-queue-worker.sh +145 -114
- package/.claude/hooks/tts-queue.sh +165 -136
- package/.claude/hooks/verbosity-manager.sh +178 -178
- package/.claude/hooks/voice-manager.sh +548 -544
- package/.claude/hooks-windows/audio-cache-utils.ps1 +119 -119
- package/.claude/hooks-windows/background-music-manager.ps1 +348 -0
- package/.claude/hooks-windows/clean-audio-cache.ps1 +53 -0
- package/.claude/hooks-windows/download-extra-voices.ps1 +185 -0
- package/.claude/hooks-windows/effects-manager.ps1 +294 -0
- package/.claude/hooks-windows/language-manager.ps1 +193 -0
- package/.claude/hooks-windows/learn-manager.ps1 +241 -0
- package/.claude/hooks-windows/personality-manager.ps1 +266 -0
- package/.claude/hooks-windows/play-tts-piper.ps1 +209 -0
- package/.claude/hooks-windows/play-tts-sapi.ps1 +108 -0
- package/.claude/hooks-windows/play-tts-soprano.ps1 +159 -158
- package/.claude/hooks-windows/play-tts-windows-piper.ps1 +50 -5
- package/.claude/hooks-windows/play-tts-windows-sapi.ps1 +108 -108
- package/.claude/hooks-windows/play-tts.ps1 +344 -266
- package/.claude/hooks-windows/provider-manager.ps1 +29 -10
- package/.claude/hooks-windows/session-start-tts.ps1 +124 -124
- package/.claude/hooks-windows/soprano-gradio-synth.py +153 -153
- package/.claude/hooks-windows/speed-manager.ps1 +166 -0
- package/.claude/hooks-windows/verbosity-manager.ps1 +119 -0
- package/.claude/hooks-windows/voice-manager-windows.ps1 +92 -8
- package/.claude/output-styles/agent-vibes.md +202 -202
- package/.claude/personalities/angry.md +14 -14
- package/.claude/personalities/annoying.md +14 -14
- package/.claude/personalities/crass.md +14 -14
- package/.claude/personalities/dramatic.md +14 -14
- package/.claude/personalities/dry-humor.md +50 -50
- package/.claude/personalities/flirty.md +20 -20
- package/.claude/personalities/funny.md +14 -14
- package/.claude/personalities/grandpa.md +32 -32
- package/.claude/personalities/millennial.md +14 -14
- package/.claude/personalities/moody.md +14 -14
- package/.claude/personalities/normal.md +16 -16
- package/.claude/personalities/pirate.md +14 -14
- package/.claude/personalities/poetic.md +14 -14
- package/.claude/personalities/professional.md +14 -14
- package/.claude/personalities/rapper.md +55 -55
- package/.claude/personalities/robot.md +14 -14
- package/.claude/personalities/sarcastic.md +38 -38
- package/.claude/personalities/sassy.md +14 -14
- package/.claude/personalities/surfer-dude.md +14 -14
- package/.claude/personalities/zen.md +14 -14
- package/.claude/settings.json +15 -15
- package/.claude/verbosity.txt +1 -1
- package/.clawdbot/README.md +105 -105
- package/.clawdbot/skill/SKILL.md +241 -241
- package/.mcp.json +12 -0
- package/CLAUDE.md +170 -181
- package/README.md +2029 -1909
- package/RELEASE_NOTES.md +1310 -66
- package/WINDOWS-SETUP.md +208 -208
- package/bin/agent-vibes +39 -39
- package/bin/agentvibes-voice-browser.js +1840 -1826
- package/bin/agentvibes.js +48 -2
- package/bin/mcp-server.js +121 -121
- package/bin/mcp-server.sh +206 -206
- package/bin/test-bmad-pr +78 -78
- package/mcp-server/QUICK_START.md +203 -203
- package/mcp-server/README.md +345 -345
- package/mcp-server/WINDOWS_SETUP.md +260 -260
- package/mcp-server/docs/troubleshooting-audio.md +313 -313
- package/mcp-server/examples/claude_desktop_config.json +11 -11
- package/mcp-server/examples/claude_desktop_config_piper.json +9 -9
- package/mcp-server/examples/custom_instructions.md +169 -169
- package/mcp-server/install-deps.js +130 -130
- package/mcp-server/pyproject.toml +52 -52
- package/mcp-server/requirements.txt +2 -2
- package/mcp-server/server.py +1465 -1417
- package/mcp-server/test_server.py +395 -395
- package/mcp-server/test_windows_script_parity.py +336 -0
- package/package.json +110 -112
- package/setup-windows.ps1 +815 -815
- package/src/bmad-detector.js +71 -71
- package/src/cli/list-personalities.js +110 -110
- package/src/cli/list-voices.js +114 -114
- package/src/commands/bmad-voices.js +394 -394
- package/src/commands/install-mcp.js +476 -476
- package/src/console/app.js +824 -806
- package/src/console/audio-env.js +20 -1
- package/src/console/brand-colors.js +13 -13
- package/src/console/constants/personalities.js +44 -0
- package/src/console/footer-config.js +50 -46
- package/src/console/modals/modal-overlay.js +247 -247
- package/src/console/navigation.js +62 -61
- package/src/console/tabs/agents-tab.js +1684 -369
- package/src/console/tabs/help-tab.js +261 -261
- package/src/console/tabs/install-tab.js +1007 -991
- package/src/console/tabs/music-tab.js +22 -8
- package/src/console/tabs/placeholder-tab.js +53 -46
- package/src/console/tabs/readme-tab.js +267 -267
- package/src/console/tabs/receiver-tab.js +1472 -0
- package/src/console/tabs/settings-tab.js +185 -402
- package/src/console/tabs/voices-tab.js +100 -21
- package/src/console/widgets/destroy-list.js +25 -0
- package/src/console/widgets/format-utils.js +89 -0
- package/src/console/widgets/notice.js +55 -0
- package/src/console/widgets/personality-picker.js +185 -0
- package/src/console/widgets/reverb-picker.js +94 -0
- package/src/console/widgets/track-picker.js +285 -0
- package/src/installer/music-file-input.js +304 -304
- package/src/installer.js +5882 -5777
- package/src/services/agent-voice-store.js +423 -163
- package/src/services/config-service.js +264 -264
- package/src/services/navigation-service.js +123 -123
- package/src/services/provider-service.js +132 -132
- package/src/services/verbosity-service.js +157 -157
- package/src/utils/audio-duration-validator.js +298 -298
- package/src/utils/audio-format-validator.js +277 -277
- package/src/utils/dependency-checker.js +469 -466
- package/src/utils/file-ownership-verifier.js +358 -358
- package/src/utils/list-formatter.js +194 -194
- package/src/utils/music-file-validator.js +285 -275
- package/src/utils/preview-list-prompt.js +136 -136
- package/src/utils/provider-validator.js +96 -12
- package/src/utils/secure-music-storage.js +412 -412
- package/templates/agentvibes-receiver.sh +482 -162
- package/templates/audio/welcome-music.mp3 +0 -0
- package/voice-assignments.json +8244 -8244
- package/.claude/config/background-music-position.txt +0 -1
|
@@ -32,11 +32,11 @@ const COLORS = {
|
|
|
32
32
|
sectionHdr: '#00897b', // Teal — section headers for Voices tab
|
|
33
33
|
labelFg: '#e3f2fd',
|
|
34
34
|
valueFg: '#f06292', // Light magenta — brand color
|
|
35
|
-
activeFg: '
|
|
35
|
+
activeFg: 'bright-cyan', // Cyan — active voice
|
|
36
36
|
favoriteFg: '#ffff00', // Yellow — favorite star
|
|
37
37
|
btnDefault: '#00695c', // Teal — Voices tab buttons
|
|
38
|
-
btnFocus: '#
|
|
39
|
-
btnFocusFg: '#
|
|
38
|
+
btnFocus: '#2e7d32', // Green — focused/selected
|
|
39
|
+
btnFocusFg: '#ffffff',
|
|
40
40
|
btnPress: '#ff00ff',
|
|
41
41
|
borderFg: '#00897b',
|
|
42
42
|
footerBg: '#00695c', // Teal — Voices tab footer
|
|
@@ -513,7 +513,7 @@ export function createVoicesTab(screen, services) {
|
|
|
513
513
|
keys: true,
|
|
514
514
|
style: {
|
|
515
515
|
fg: COLORS.valueFg,
|
|
516
|
-
bg: '#
|
|
516
|
+
bg: '#1a3a5c',
|
|
517
517
|
focus: { bg: '#283593' },
|
|
518
518
|
},
|
|
519
519
|
});
|
|
@@ -549,7 +549,7 @@ export function createVoicesTab(screen, services) {
|
|
|
549
549
|
fg: COLORS.labelFg,
|
|
550
550
|
bg: COLORS.contentBg,
|
|
551
551
|
border: { fg: COLORS.borderFg },
|
|
552
|
-
selected: { bg: '#
|
|
552
|
+
selected: { bg: '#2e7d32', fg: '#ffffff', bold: true },
|
|
553
553
|
item: { fg: COLORS.labelFg },
|
|
554
554
|
},
|
|
555
555
|
});
|
|
@@ -670,12 +670,24 @@ export function createVoicesTab(screen, services) {
|
|
|
670
670
|
const tempWav = path.join(os.tmpdir(), `agentvibes-preview-${Date.now()}.wav`);
|
|
671
671
|
const phrase = SAMPLE_PHRASES[Math.floor(Math.random() * SAMPLE_PHRASES.length)];
|
|
672
672
|
|
|
673
|
-
// Synthesize: spawn piper
|
|
673
|
+
// Synthesize: spawn piper; on Windows use the exe path directly
|
|
674
|
+
const isWindows = process.platform === 'win32' && !process.env.WSL_DISTRO_NAME;
|
|
675
|
+
let piperBin = 'piper';
|
|
676
|
+
if (isWindows) {
|
|
677
|
+
const localAppData = process.env.LOCALAPPDATA ||
|
|
678
|
+
(process.env.USERPROFILE ? path.join(process.env.USERPROFILE, 'AppData', 'Local') : null);
|
|
679
|
+
if (localAppData) {
|
|
680
|
+
const exePath = path.join(localAppData, 'Programs', 'Piper', 'piper.exe');
|
|
681
|
+
if (fs.existsSync(exePath)) piperBin = exePath;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
674
684
|
const piperArgs = ['--model', voicePath, '--output_file', tempWav];
|
|
675
685
|
if (ms.speakerId != null) piperArgs.push('--speaker', String(ms.speakerId));
|
|
676
|
-
|
|
686
|
+
// On Windows, avoid detached:true which opens a visible console window
|
|
687
|
+
const piper = spawn(piperBin, piperArgs, {
|
|
677
688
|
stdio: ['pipe', 'ignore', 'ignore'],
|
|
678
|
-
detached:
|
|
689
|
+
detached: !isWindows,
|
|
690
|
+
windowsHide: true,
|
|
679
691
|
env: _spawnEnv,
|
|
680
692
|
});
|
|
681
693
|
piper.stdin.write(phrase + '\n');
|
|
@@ -716,7 +728,8 @@ export function createVoicesTab(screen, services) {
|
|
|
716
728
|
}
|
|
717
729
|
const playProc = spawn(_wavP.bin, _wavP.args(tempWav), {
|
|
718
730
|
stdio: 'ignore',
|
|
719
|
-
detached:
|
|
731
|
+
detached: !isWindows,
|
|
732
|
+
windowsHide: true,
|
|
720
733
|
env: _spawnEnv,
|
|
721
734
|
});
|
|
722
735
|
// Race note: _playingVoiceId could change between piper exit and here
|
|
@@ -993,7 +1006,7 @@ export function createVoicesTab(screen, services) {
|
|
|
993
1006
|
refreshDisplay();
|
|
994
1007
|
_showVoiceChangedNotice(displayName);
|
|
995
1008
|
});
|
|
996
|
-
const okGlobalBtn = _makeBtn('Save Globally & Locally', '#
|
|
1009
|
+
const okGlobalBtn = _makeBtn('Save Globally & Locally', '#1565c0', 18, 5, () => {
|
|
997
1010
|
_activateVoice(voiceId);
|
|
998
1011
|
_activateVoiceGlobal(voiceId);
|
|
999
1012
|
refreshDisplay();
|
|
@@ -1100,26 +1113,91 @@ export function createVoicesTab(screen, services) {
|
|
|
1100
1113
|
function _startDownload() {
|
|
1101
1114
|
if (_downloading) return;
|
|
1102
1115
|
_downloading = true;
|
|
1103
|
-
statusLine.setContent(`{${COLORS.activeFg}-fg}⬇ Downloading ${modelToDownload}…{/${COLORS.activeFg}-fg}`);
|
|
1104
|
-
screen.render();
|
|
1105
1116
|
|
|
1106
|
-
//
|
|
1107
|
-
const
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1117
|
+
// Animated spinner
|
|
1118
|
+
const spinFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
1119
|
+
let spinIdx = 0;
|
|
1120
|
+
let dlPhase = 'Downloading model';
|
|
1121
|
+
const progressBar = (pct) => {
|
|
1122
|
+
const filled = Math.round(pct / 5);
|
|
1123
|
+
const empty = 20 - filled;
|
|
1124
|
+
return '█'.repeat(filled) + '░'.repeat(empty);
|
|
1125
|
+
};
|
|
1126
|
+
|
|
1127
|
+
const spinTimer = setInterval(() => {
|
|
1128
|
+
spinIdx = (spinIdx + 1) % spinFrames.length;
|
|
1129
|
+
const frame = spinFrames[spinIdx];
|
|
1130
|
+
statusLine.setContent(
|
|
1131
|
+
`{${COLORS.activeFg}-fg}${frame} ${dlPhase}… ${modelToDownload}{/${COLORS.activeFg}-fg}`
|
|
1132
|
+
);
|
|
1133
|
+
screen.render();
|
|
1134
|
+
}, 100);
|
|
1135
|
+
|
|
1136
|
+
// Download voice model — use PowerShell on Windows, bash on Unix
|
|
1137
|
+
const packageRoot = path.resolve(__dirname, '..', '..', '..');
|
|
1138
|
+
const isWindows = process.platform === 'win32' && !process.env.WSL_DISTRO_NAME;
|
|
1139
|
+
let dlProc;
|
|
1140
|
+
|
|
1141
|
+
if (isWindows) {
|
|
1142
|
+
const piperVoicesDir = resolvePiperVoicesDir();
|
|
1143
|
+
const hfBase = 'https://huggingface.co/rhasspy/piper-voices/resolve/main';
|
|
1144
|
+
const match = modelToDownload.match(/^([a-z]{2})_([A-Z]{2})-([a-zA-Z0-9_]+)-([a-z]+)$/);
|
|
1145
|
+
let modelUrl, configUrl;
|
|
1146
|
+
if (match) {
|
|
1147
|
+
const [, lang, region, speaker, quality] = match;
|
|
1148
|
+
const hfPath = `${lang}/${lang}_${region}/${speaker}/${quality}`;
|
|
1149
|
+
modelUrl = `${hfBase}/${hfPath}/${modelToDownload}.onnx`;
|
|
1150
|
+
configUrl = `${hfBase}/${hfPath}/${modelToDownload}.onnx.json`;
|
|
1151
|
+
} else {
|
|
1152
|
+
const customBase = 'https://huggingface.co/agentvibes/piper-custom-voices/resolve/main';
|
|
1153
|
+
modelUrl = `${customBase}/${modelToDownload}.onnx`;
|
|
1154
|
+
configUrl = `${customBase}/${modelToDownload}.onnx.json`;
|
|
1155
|
+
}
|
|
1156
|
+
const modelFile = path.join(piperVoicesDir, `${modelToDownload}.onnx`);
|
|
1157
|
+
const configFile = path.join(piperVoicesDir, `${modelToDownload}.onnx.json`);
|
|
1158
|
+
// PowerShell script with progress reporting
|
|
1159
|
+
const psScript = `
|
|
1160
|
+
$ErrorActionPreference = 'Stop'
|
|
1161
|
+
$ProgressPreference = 'SilentlyContinue'
|
|
1162
|
+
$voicesDir = '${piperVoicesDir.replace(/'/g, "''")}'
|
|
1163
|
+
if (-not (Test-Path $voicesDir)) { New-Item -ItemType Directory -Path $voicesDir -Force | Out-Null }
|
|
1164
|
+
Write-Output 'PHASE:model'
|
|
1165
|
+
Invoke-WebRequest -Uri '${modelUrl}' -OutFile '${modelFile.replace(/'/g, "''")}' -ErrorAction Stop
|
|
1166
|
+
Write-Output 'PHASE:config'
|
|
1167
|
+
Invoke-WebRequest -Uri '${configUrl}' -OutFile '${configFile.replace(/'/g, "''")}' -ErrorAction Stop
|
|
1168
|
+
Write-Output 'PHASE:done'
|
|
1169
|
+
`;
|
|
1170
|
+
dlProc = spawn('powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', psScript], {
|
|
1171
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1172
|
+
env: _spawnEnv,
|
|
1173
|
+
});
|
|
1174
|
+
} else {
|
|
1175
|
+
const managerScript = path.resolve(packageRoot, '.claude', 'hooks', 'piper-voice-manager.sh');
|
|
1176
|
+
dlProc = spawn('bash', ['-c', 'source "$1" && download_voice "$2"', '_', managerScript, modelToDownload], {
|
|
1177
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1178
|
+
env: _spawnEnv,
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1112
1181
|
_downloadProcess = dlProc;
|
|
1113
1182
|
|
|
1114
1183
|
let output = '';
|
|
1115
|
-
dlProc.stdout.on('data', (d) => {
|
|
1184
|
+
dlProc.stdout.on('data', (d) => {
|
|
1185
|
+
const chunk = d.toString();
|
|
1186
|
+
output += chunk;
|
|
1187
|
+
// Update phase based on progress markers
|
|
1188
|
+
if (chunk.includes('PHASE:config') || chunk.includes('config file')) {
|
|
1189
|
+
dlPhase = 'Downloading config';
|
|
1190
|
+
} else if (chunk.includes('PHASE:done') || chunk.includes('successfully')) {
|
|
1191
|
+
dlPhase = 'Finishing up';
|
|
1192
|
+
}
|
|
1193
|
+
});
|
|
1116
1194
|
dlProc.stderr.on('data', (d) => { output += d.toString(); });
|
|
1117
1195
|
|
|
1118
1196
|
dlProc.on('exit', (code) => {
|
|
1197
|
+
clearInterval(spinTimer);
|
|
1119
1198
|
_downloading = false;
|
|
1120
1199
|
_downloadProcess = null;
|
|
1121
1200
|
if (code === 0) {
|
|
1122
|
-
// Patch speaker names for freshly downloaded LibriTTS model
|
|
1123
1201
|
if (isLibriTTS) {
|
|
1124
1202
|
patchLibriTTSSpeakerNames();
|
|
1125
1203
|
_metaCache.clear();
|
|
@@ -1137,6 +1215,7 @@ export function createVoicesTab(screen, services) {
|
|
|
1137
1215
|
});
|
|
1138
1216
|
|
|
1139
1217
|
dlProc.on('error', () => {
|
|
1218
|
+
clearInterval(spinTimer);
|
|
1140
1219
|
_downloading = false;
|
|
1141
1220
|
_downloadProcess = null;
|
|
1142
1221
|
statusLine.setContent(`{red-fg}✗ Could not run download script{/red-fg}`);
|
|
@@ -1235,7 +1314,7 @@ export function createVoicesTab(screen, services) {
|
|
|
1235
1314
|
// Greyed-out row for uninstalled catalog voices
|
|
1236
1315
|
return `{bright-black-fg} ${star} ${name}${gender.padEnd(COL_GENDER_W)}${provider}{/bright-black-fg}`;
|
|
1237
1316
|
}
|
|
1238
|
-
return ` ${star}${dot} ${name}${gender.padEnd(COL_GENDER_W)}${provider}${isPrev ? ' (playing)' : ''}`;
|
|
1317
|
+
return `{${COLORS.labelFg}-fg} ${star}${dot} ${name}${gender.padEnd(COL_GENDER_W)}${provider}${isPrev ? ' (playing)' : ''}{/${COLORS.labelFg}-fg}`;
|
|
1239
1318
|
});
|
|
1240
1319
|
}
|
|
1241
1320
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentVibes TUI — Shared Widget: Modal Destroy Helper
|
|
3
|
+
*
|
|
4
|
+
* Force-invalidates blessed's olines buffer after destroying a modal widget.
|
|
5
|
+
* Without this, blessed skips repainting cells where lines==olines and the
|
|
6
|
+
* terminal retains stale modal content as ghost artifacts.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Destroy a blessed list/box widget and force full screen repaint.
|
|
11
|
+
*
|
|
12
|
+
* @param {object} widget - blessed widget to destroy
|
|
13
|
+
* @param {object} screen - blessed screen instance
|
|
14
|
+
* @param {Function} [onClose] - optional callback after destruction
|
|
15
|
+
*/
|
|
16
|
+
export function destroyList(widget, screen, onClose) {
|
|
17
|
+
widget.destroy();
|
|
18
|
+
try {
|
|
19
|
+
for (let r = 0; r < screen.height; r++)
|
|
20
|
+
for (let c = 0; c < screen.width; c++)
|
|
21
|
+
if (screen.olines[r]?.[c]) screen.olines[r][c][0] = -1;
|
|
22
|
+
} catch {}
|
|
23
|
+
onClose?.();
|
|
24
|
+
screen.render();
|
|
25
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentVibes TUI — Shared Format Utilities
|
|
3
|
+
*
|
|
4
|
+
* Pure formatting functions extracted from settings-tab.js to avoid
|
|
5
|
+
* circular imports between widgets and tabs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const TRACK_NAMES = Object.freeze({
|
|
9
|
+
'agentvibes_soft_flamenco_loop.mp3': '🎻 Soft Flamenco',
|
|
10
|
+
'agent_vibes_bachata_v1_loop.mp3': '🎺 Bachata',
|
|
11
|
+
'agent_vibes_salsa_v2_loop.mp3': '💃 Salsa',
|
|
12
|
+
'agent_vibes_cumbia_v1_loop.mp3': '🎸 Cumbia',
|
|
13
|
+
'agent_vibes_bossa_nova_v2_loop.mp3': '🌸 Bossa Nova',
|
|
14
|
+
'agent_vibes_japanese_city_pop_v1_loop.mp3': '🌆 Japanese City Pop',
|
|
15
|
+
'agent_vibes_chillwave_v2_loop.mp3': '🌊 Chillwave',
|
|
16
|
+
'agent_vibes_dark_chill_step_loop.mp3': '🌙 Dark Chill Step',
|
|
17
|
+
'agent_vibes_goa_trance_v2_loop.mp3': '🌀 Goa Trance',
|
|
18
|
+
'agent_vibes_harpsichord_v2_loop.mp3': '🎼 Harpsichord',
|
|
19
|
+
'agent_vibes_celtic_harp_v1_loop.mp3': '🎻 Celtic Harp',
|
|
20
|
+
'agent_vibes_hawaiian_slack_key_guitar_v2_loop.mp3': '🌺 Hawaiian Slack Key Guitar',
|
|
21
|
+
'agent_vibes_arabic_v2_loop.mp3': '🎵 Arabic Oud',
|
|
22
|
+
'agent_vibes_ganawa_ambient_v2_loop.mp3': '🪘 Gnawa Ambient',
|
|
23
|
+
'agent_vibes_tabla_dream_pop_v1_loop.mp3': '🥁 Tabla Dream Pop',
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {string} track - filename (e.g. 'agentvibes_soft_flamenco_loop.mp3')
|
|
28
|
+
* @returns {string}
|
|
29
|
+
*/
|
|
30
|
+
export function formatTrackName(track) {
|
|
31
|
+
if (!track) return 'None';
|
|
32
|
+
if (TRACK_NAMES[track]) return TRACK_NAMES[track];
|
|
33
|
+
return track
|
|
34
|
+
.replace(/\.[^.]+$/, '')
|
|
35
|
+
.replace(/^agentvibes_|^agent_vibes_/, '')
|
|
36
|
+
.replace(/_v\d+_loop$|_loop$|_v\d+$/, '')
|
|
37
|
+
.replace(/_/g, ' ')
|
|
38
|
+
.replace(/\b\w/g, c => c.toUpperCase());
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Beautify a raw voice identifier for display in narrow table columns.
|
|
43
|
+
*
|
|
44
|
+
* Examples:
|
|
45
|
+
* 16Speakers::Rose_Ibex → Rose Ibex
|
|
46
|
+
* 16Speakers::Emily_Cripps → Emily Cripps
|
|
47
|
+
* en_US-kusal-medium → Kusal
|
|
48
|
+
* en_US-lessac-high → Lessac
|
|
49
|
+
* en_US-libritts_r-medium → Libritts R
|
|
50
|
+
* kristin → Kristin
|
|
51
|
+
*
|
|
52
|
+
* @param {string} voice - raw voice identifier
|
|
53
|
+
* @returns {string}
|
|
54
|
+
*/
|
|
55
|
+
export function formatVoiceName(voice) {
|
|
56
|
+
if (!voice) return '(global)';
|
|
57
|
+
|
|
58
|
+
let name;
|
|
59
|
+
if (voice.includes('::')) {
|
|
60
|
+
// 16Speakers::Rose_Ibex → extract after '::'
|
|
61
|
+
name = voice.split('::')[1];
|
|
62
|
+
} else {
|
|
63
|
+
const parts = voice.split('-');
|
|
64
|
+
const QUALITIES = new Set(['high', 'medium', 'low']);
|
|
65
|
+
if (parts.length >= 2 && /^[a-z]{2}_[A-Z]{2}$/.test(parts[0])) {
|
|
66
|
+
// Strip locale prefix and quality suffix
|
|
67
|
+
name = parts.slice(1).filter(p => !QUALITIES.has(p)).join(' ');
|
|
68
|
+
} else {
|
|
69
|
+
name = voice;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Replace underscores with spaces, title-case each word
|
|
74
|
+
return name
|
|
75
|
+
.replace(/_/g, ' ')
|
|
76
|
+
.split(' ')
|
|
77
|
+
.filter(Boolean)
|
|
78
|
+
.map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
|
|
79
|
+
.join(' ') || '(global)';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @param {string} preset - 'off' | 'light' | 'medium' | 'heavy' | 'cathedral'
|
|
84
|
+
* @returns {string}
|
|
85
|
+
*/
|
|
86
|
+
export function formatReverbState(preset) {
|
|
87
|
+
const LABELS = { off: 'Off', light: 'Light (Small room)', medium: 'Medium (Conference room)', heavy: 'Heavy (Large hall)', cathedral: 'Cathedral (Epic space)' };
|
|
88
|
+
return LABELS[preset] ?? LABELS.light;
|
|
89
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentVibes TUI — Shared Widget: Notice Toast
|
|
3
|
+
*
|
|
4
|
+
* Displays a short auto-dismissing notice modal centred on screen.
|
|
5
|
+
* Usable from any tab; no settings-specific state required.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { destroyList } from './destroy-list.js';
|
|
9
|
+
|
|
10
|
+
const IS_TEST = process.env.AGENTVIBES_TEST_MODE === 'true';
|
|
11
|
+
let blessed;
|
|
12
|
+
if (!IS_TEST) {
|
|
13
|
+
const { default: b } = await import('blessed');
|
|
14
|
+
blessed = b;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Show a temporary notice that auto-dismisses after 2.5 seconds.
|
|
19
|
+
*
|
|
20
|
+
* @param {object} screen - blessed screen instance
|
|
21
|
+
* @param {string} message - text to display
|
|
22
|
+
* @param {object} [opts]
|
|
23
|
+
* @param {string} [opts.bg='#0a0e1a'] - background colour
|
|
24
|
+
* @param {string} [opts.fg='#e3f2fd'] - foreground colour
|
|
25
|
+
* @param {string} [opts.borderFg='bright-cyan'] - border colour
|
|
26
|
+
* @param {number} [opts.durationMs=2500] - auto-dismiss delay in ms
|
|
27
|
+
*/
|
|
28
|
+
export function showNotice(screen, message, opts = {}) {
|
|
29
|
+
const bg = opts.bg ?? '#0a0e1a';
|
|
30
|
+
const fg = opts.fg ?? '#e3f2fd';
|
|
31
|
+
const borderFg = opts.borderFg ?? 'bright-cyan';
|
|
32
|
+
const durationMs = opts.durationMs ?? 2500;
|
|
33
|
+
|
|
34
|
+
const width = Math.max(28, message.length + 6);
|
|
35
|
+
const modal = blessed.box({
|
|
36
|
+
parent: screen,
|
|
37
|
+
top: 'center',
|
|
38
|
+
left: 'center',
|
|
39
|
+
width,
|
|
40
|
+
height: 3,
|
|
41
|
+
border: { type: 'line' },
|
|
42
|
+
tags: true,
|
|
43
|
+
content: `{center}${message}{/center}`,
|
|
44
|
+
style: {
|
|
45
|
+
fg,
|
|
46
|
+
bg,
|
|
47
|
+
border: { fg: borderFg },
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
screen.render();
|
|
51
|
+
|
|
52
|
+
setTimeout(() => {
|
|
53
|
+
destroyList(modal, screen);
|
|
54
|
+
}, durationMs);
|
|
55
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentVibes TUI — Shared Widget: Personality Picker
|
|
3
|
+
*
|
|
4
|
+
* Inline modal list for selecting voice personalities with TTS preview.
|
|
5
|
+
* Extracted from settings-tab.js for reuse across tabs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { spawn } from 'node:child_process';
|
|
10
|
+
import { destroyList } from './destroy-list.js';
|
|
11
|
+
import { buildAudioEnv } from '../audio-env.js';
|
|
12
|
+
import { BRAND_PINK } from '../brand-colors.js';
|
|
13
|
+
import { PERSONALITY_EMOJIS, PERSONALITIES } from '../constants/personalities.js';
|
|
14
|
+
|
|
15
|
+
export { PERSONALITY_EMOJIS, PERSONALITIES };
|
|
16
|
+
|
|
17
|
+
const IS_TEST = process.env.AGENTVIBES_TEST_MODE === 'true';
|
|
18
|
+
let blessed;
|
|
19
|
+
if (!IS_TEST) {
|
|
20
|
+
const { default: b } = await import('blessed');
|
|
21
|
+
blessed = b;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const _modalTitle = (text) => ` {${BRAND_PINK}-fg}${text}{/${BRAND_PINK}-fg} `;
|
|
25
|
+
|
|
26
|
+
const PERSONALITY_PREVIEW_PHRASES = Object.freeze({
|
|
27
|
+
angry: "UNACCEPTABLE! This build time is a DISASTER! Fix it NOW or so help me!",
|
|
28
|
+
annoying: "Oh oh oh! Can I tell you something? Can I? Can I? PLEASE? It is so important!",
|
|
29
|
+
crass: "Well damn, that code runs like my uncle's truck. Barely, and it smells funny.",
|
|
30
|
+
dramatic: "The tests... have failed. I don't know how much longer I can do this.",
|
|
31
|
+
'dry-humor': "Your code worked. I too am surprised.",
|
|
32
|
+
flirty: "Ooh, a clean merge? You know exactly how to make my heart race.",
|
|
33
|
+
funny: "Why do programmers hate nature? Too many bugs. I will show myself out.",
|
|
34
|
+
grandpa: "Back in my day, we compiled by hand. Uphill. In the snow. Both ways.",
|
|
35
|
+
millennial: "I literally cannot even with this error. I am so done. Like, actually deceased.",
|
|
36
|
+
moody: "...It works. Whatever. Do not get used to it.",
|
|
37
|
+
pirate: "Arrr! The build be sailin' smooth today, matey! No barnacles in sight!",
|
|
38
|
+
poetic: "Like rivers to the sea, your code flows toward eventual compilation.",
|
|
39
|
+
professional: "I have completed the requested task and am prepared to document outcomes.",
|
|
40
|
+
rapper: "Yo! Clean code flowin', tests are glowin', no bugs showin'!",
|
|
41
|
+
robot: "TASK COMPLETE. EFFICIENCY: OPTIMAL. PROBABILITY OF SUCCESS: 97.3 PERCENT. BEEP.",
|
|
42
|
+
sarcastic: "Oh wow, another bug. What a completely unexpected surprise. Truly shocking.",
|
|
43
|
+
sassy: "Honey, whoever told you that was good code was not your friend.",
|
|
44
|
+
'surfer-dude':"Duuude! That commit totally shredded! Gnarly clean code, bro!",
|
|
45
|
+
zen: "The bug is not the enemy. The bug is the teacher. Breathe. Commit.",
|
|
46
|
+
random: "Who will I be today? Even I do not know. Expect the unexpected.",
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Open the personality picker modal.
|
|
51
|
+
*
|
|
52
|
+
* @param {object} screen - blessed screen
|
|
53
|
+
* @param {string} currentPersonality - current personality value
|
|
54
|
+
* @param {Function} onSelect - called with selected personality
|
|
55
|
+
* @param {Function} [onClose] - called after modal closes
|
|
56
|
+
*/
|
|
57
|
+
export function openPersonalityPicker(screen, currentPersonality, onSelect, onClose) {
|
|
58
|
+
const current = currentPersonality ?? 'none';
|
|
59
|
+
const currentIdx = Math.max(0, PERSONALITIES.indexOf(current));
|
|
60
|
+
|
|
61
|
+
const COLORS = {
|
|
62
|
+
btnFocus: '#2e7d32',
|
|
63
|
+
btnFocusFg: '#ffffff',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const list = blessed.list({
|
|
67
|
+
parent: screen,
|
|
68
|
+
top: 'center',
|
|
69
|
+
left: 'center',
|
|
70
|
+
width: 44,
|
|
71
|
+
height: Math.min(PERSONALITIES.length + 4, 22),
|
|
72
|
+
border: { type: 'line' },
|
|
73
|
+
tags: true,
|
|
74
|
+
label: _modalTitle('Select Personality'),
|
|
75
|
+
items: PERSONALITIES.map((p, i) => {
|
|
76
|
+
const emoji = PERSONALITY_EMOJIS[p] ?? '✨';
|
|
77
|
+
const label = p === 'none' ? 'None' : p.charAt(0).toUpperCase() + p.slice(1);
|
|
78
|
+
const mark = i === currentIdx ? '✅' : ' ';
|
|
79
|
+
return `${mark} ${emoji} ${label}`;
|
|
80
|
+
}),
|
|
81
|
+
keys: true,
|
|
82
|
+
vi: true,
|
|
83
|
+
mouse: true,
|
|
84
|
+
style: {
|
|
85
|
+
border: { fg: COLORS.btnFocus },
|
|
86
|
+
selected: { bg: COLORS.btnFocus, fg: COLORS.btnFocusFg, bold: true },
|
|
87
|
+
item: { fg: '#e3f2fd' },
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
list.select(currentIdx);
|
|
92
|
+
list.focus();
|
|
93
|
+
screen.render();
|
|
94
|
+
|
|
95
|
+
// TTS preview on hover
|
|
96
|
+
let _pickerTtsProc = null;
|
|
97
|
+
let _playingItemIdx = -1;
|
|
98
|
+
|
|
99
|
+
function _setItemPlaying(idx, playing) {
|
|
100
|
+
const item = list.items?.[idx];
|
|
101
|
+
if (!item) return;
|
|
102
|
+
const base = (item.content ?? '').replace(/ █$/, '').replace(/ \(playing\)$/, '');
|
|
103
|
+
item.setContent(playing ? `${base} (playing)` : base);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function _killPickerTts() {
|
|
107
|
+
if (_pickerTtsProc) {
|
|
108
|
+
try { process.kill(-_pickerTtsProc.pid, 'SIGTERM'); } catch {}
|
|
109
|
+
_pickerTtsProc = null;
|
|
110
|
+
}
|
|
111
|
+
if (_playingItemIdx >= 0) {
|
|
112
|
+
_setItemPlaying(_playingItemIdx, false);
|
|
113
|
+
_playingItemIdx = -1;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function _speakPreview(personality) {
|
|
118
|
+
_killPickerTts();
|
|
119
|
+
const phrase = PERSONALITY_PREVIEW_PHRASES[personality];
|
|
120
|
+
if (!phrase) return;
|
|
121
|
+
const ttsScript = path.join(process.cwd(), '.claude', 'hooks', 'play-tts.sh');
|
|
122
|
+
_pickerTtsProc = spawn('bash', [ttsScript, phrase], {
|
|
123
|
+
stdio: 'ignore',
|
|
124
|
+
detached: true,
|
|
125
|
+
env: buildAudioEnv(),
|
|
126
|
+
});
|
|
127
|
+
_playingItemIdx = list.selected;
|
|
128
|
+
_setItemPlaying(_playingItemIdx, true);
|
|
129
|
+
screen.render();
|
|
130
|
+
_pickerTtsProc.on('exit', () => {
|
|
131
|
+
if (_playingItemIdx >= 0) {
|
|
132
|
+
_setItemPlaying(_playingItemIdx, false);
|
|
133
|
+
_playingItemIdx = -1;
|
|
134
|
+
screen.render();
|
|
135
|
+
}
|
|
136
|
+
_pickerTtsProc = null;
|
|
137
|
+
});
|
|
138
|
+
_pickerTtsProc.unref();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
list.on('select item', () => {
|
|
142
|
+
_speakPreview(PERSONALITIES[list.selected]);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
list.key(['space'], () => {
|
|
146
|
+
if (_pickerTtsProc && _playingItemIdx === list.selected) {
|
|
147
|
+
_killPickerTts();
|
|
148
|
+
} else {
|
|
149
|
+
_speakPreview(PERSONALITIES[list.selected]);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Type-to-jump
|
|
154
|
+
const _jumpBlocked = new Set(['j', 'k', 'g', 'h', 'l', 'd', 'u', 'q']);
|
|
155
|
+
list.on('keypress', (ch, key) => {
|
|
156
|
+
if (!ch || key.ctrl || key.meta) return;
|
|
157
|
+
const lower = ch.toLowerCase();
|
|
158
|
+
if (!/^[a-z]$/.test(lower)) return;
|
|
159
|
+
if (_jumpBlocked.has(lower)) return;
|
|
160
|
+
const count = PERSONALITIES.length;
|
|
161
|
+
const start = list.selected ?? 0;
|
|
162
|
+
for (let i = 1; i <= count; i++) {
|
|
163
|
+
const idx = (start + i) % count;
|
|
164
|
+
if (PERSONALITIES[idx].startsWith(lower)) {
|
|
165
|
+
list.select(idx);
|
|
166
|
+
screen.render();
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
list.key(['enter'], () => {
|
|
173
|
+
const selected = PERSONALITIES[list.selected];
|
|
174
|
+
if (!selected) return;
|
|
175
|
+
_killPickerTts();
|
|
176
|
+
// Call onSelect before destroying to avoid stale-state re-renders
|
|
177
|
+
onSelect(selected);
|
|
178
|
+
destroyList(list, screen, onClose);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
list.key(['escape', 'q'], () => {
|
|
182
|
+
_killPickerTts();
|
|
183
|
+
destroyList(list, screen, onClose);
|
|
184
|
+
});
|
|
185
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentVibes TUI — Shared Widget: Reverb Preset Picker
|
|
3
|
+
*
|
|
4
|
+
* Inline modal list for selecting reverb presets.
|
|
5
|
+
* Extracted from settings-tab.js for reuse across tabs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { spawnSync } from 'node:child_process';
|
|
10
|
+
import { destroyList } from './destroy-list.js';
|
|
11
|
+
import { BRAND_PINK } from '../brand-colors.js';
|
|
12
|
+
|
|
13
|
+
const IS_TEST = process.env.AGENTVIBES_TEST_MODE === 'true';
|
|
14
|
+
let blessed;
|
|
15
|
+
if (!IS_TEST) {
|
|
16
|
+
const { default: b } = await import('blessed');
|
|
17
|
+
blessed = b;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const _modalTitle = (text) => ` {${BRAND_PINK}-fg}${text}{/${BRAND_PINK}-fg} `;
|
|
21
|
+
|
|
22
|
+
export const REVERB_PRESETS = Object.freeze([
|
|
23
|
+
{ label: 'Off (Dry, no reverb)', value: 'off' },
|
|
24
|
+
{ label: 'Light (Small room)', value: 'light' },
|
|
25
|
+
{ label: 'Medium (Conference room)', value: 'medium' },
|
|
26
|
+
{ label: 'Heavy (Large hall)', value: 'heavy' },
|
|
27
|
+
{ label: 'Cathedral (Epic space)', value: 'cathedral' },
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Open the reverb preset picker modal.
|
|
32
|
+
*
|
|
33
|
+
* @param {object} screen - blessed screen
|
|
34
|
+
* @param {string} currentPreset - current reverb preset value
|
|
35
|
+
* @param {Function} onSelect - called with selected preset value
|
|
36
|
+
* @param {Function} [onClose] - called after modal closes
|
|
37
|
+
* @param {object} [opts] - options
|
|
38
|
+
* @param {boolean} [opts.applyToEffectsManager=true] - whether to apply via effects-manager.sh
|
|
39
|
+
*/
|
|
40
|
+
export function openReverbPicker(screen, currentPreset, onSelect, onClose, opts = {}) {
|
|
41
|
+
const applyToEffectsManager = opts.applyToEffectsManager !== false;
|
|
42
|
+
const currentIdx = Math.max(0, REVERB_PRESETS.findIndex(p => p.value === currentPreset));
|
|
43
|
+
|
|
44
|
+
const COLORS = {
|
|
45
|
+
btnFocus: '#2e7d32',
|
|
46
|
+
btnFocusFg: '#ffffff',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const list = blessed.list({
|
|
50
|
+
parent: screen,
|
|
51
|
+
top: 'center',
|
|
52
|
+
left: 'center',
|
|
53
|
+
width: 40,
|
|
54
|
+
height: REVERB_PRESETS.length + 4,
|
|
55
|
+
border: { type: 'line' },
|
|
56
|
+
tags: true,
|
|
57
|
+
label: _modalTitle('Select Reverb Preset'),
|
|
58
|
+
items: REVERB_PRESETS.map((p, i) => (i === currentIdx ? `● ${p.label}` : ` ${p.label}`)),
|
|
59
|
+
keys: true,
|
|
60
|
+
vi: false,
|
|
61
|
+
mouse: true,
|
|
62
|
+
style: {
|
|
63
|
+
border: { fg: COLORS.btnFocus },
|
|
64
|
+
selected: { bg: COLORS.btnFocus, fg: COLORS.btnFocusFg, bold: true },
|
|
65
|
+
item: { fg: '#e3f2fd' },
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
list.select(currentIdx);
|
|
70
|
+
list.focus();
|
|
71
|
+
screen.render();
|
|
72
|
+
|
|
73
|
+
list.key(['enter', 'space'], () => {
|
|
74
|
+
const selected = REVERB_PRESETS[list.selected];
|
|
75
|
+
if (!selected) return;
|
|
76
|
+
|
|
77
|
+
if (applyToEffectsManager) {
|
|
78
|
+
const effectsScript = path.join(process.cwd(), '.claude', 'hooks', 'effects-manager.sh');
|
|
79
|
+
spawnSync('bash', [effectsScript, 'set-reverb', selected.value, 'default'], {
|
|
80
|
+
stdio: 'ignore',
|
|
81
|
+
timeout: 5000,
|
|
82
|
+
env: { ...process.env },
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Call onSelect before destroying to avoid stale-state re-renders
|
|
87
|
+
onSelect(selected.value);
|
|
88
|
+
destroyList(list, screen, onClose);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
list.key(['escape', 'q'], () => {
|
|
92
|
+
destroyList(list, screen, onClose);
|
|
93
|
+
});
|
|
94
|
+
}
|