agentvibes 4.2.0 → 4.4.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/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 +2 -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 -433
- 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 -269
- 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 -105
- package/.claude/hooks/play-tts-macos.sh +368 -368
- package/.claude/hooks/play-tts-piper.sh +679 -679
- package/.claude/hooks/play-tts-soprano.sh +356 -356
- package/.claude/hooks/play-tts-ssh-remote.sh +167 -167
- package/.claude/hooks/play-tts-termux-ssh.sh +169 -169
- package/.claude/hooks/play-tts.sh +301 -301
- 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 -81
- package/.claude/hooks/soprano-gradio-synth.py +139 -139
- package/.claude/hooks/speed-manager.sh +291 -291
- package/.claude/hooks/stop-tts.sh +84 -84
- 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 -145
- package/.claude/hooks/tts-queue.sh +165 -165
- package/.claude/hooks/verbosity-manager.sh +178 -178
- package/.claude/hooks/voice-manager.sh +548 -548
- 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 -170
- package/README.md +2029 -2007
- package/RELEASE_NOTES.md +1310 -1203
- package/WINDOWS-SETUP.md +208 -208
- package/bin/agent-vibes +39 -39
- package/bin/agentvibes-voice-browser.js +1840 -1840
- 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 -1453
- package/mcp-server/test_server.py +395 -395
- package/mcp-server/test_windows_script_parity.py +336 -0
- package/package.json +110 -110
- 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 -824
- package/src/console/audio-env.js +20 -1
- package/src/console/brand-colors.js +13 -13
- package/src/console/constants/personalities.js +44 -44
- package/src/console/footer-config.js +50 -50
- package/src/console/modals/modal-overlay.js +247 -247
- package/src/console/navigation.js +62 -62
- package/src/console/tabs/agents-tab.js +1684 -1516
- 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 -53
- package/src/console/tabs/readme-tab.js +267 -267
- package/src/console/tabs/receiver-tab.js +1472 -1212
- package/src/console/tabs/settings-tab.js +208 -84
- package/src/console/tabs/voices-tab.js +100 -21
- package/src/console/widgets/destroy-list.js +25 -25
- package/src/console/widgets/format-utils.js +89 -89
- package/src/console/widgets/notice.js +55 -55
- package/src/console/widgets/personality-picker.js +185 -185
- package/src/console/widgets/reverb-picker.js +94 -94
- package/src/console/widgets/track-picker.js +285 -285
- package/src/installer/music-file-input.js +304 -304
- package/src/installer.js +5895 -5829
- package/src/services/agent-voice-store.js +423 -423
- package/src/services/config-service.js +264 -264
- package/src/services/navigation-service.js +123 -123
- package/src/services/provider-service.js +143 -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 -285
- 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 -482
- package/templates/audio/welcome-music.mp3 +0 -0
- package/voice-assignments.json +8244 -8244
- package/.claude/config/background-music-position.txt +0 -1
|
@@ -1,285 +1,285 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AgentVibes TUI — Shared Widget: Background Music Track Picker
|
|
3
|
-
*
|
|
4
|
-
* Inline modal list for selecting background music tracks.
|
|
5
|
-
* Extracted from settings-tab.js for reuse across tabs.
|
|
6
|
-
* Space previews track, Enter selects.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import fs from 'node:fs';
|
|
10
|
-
import path from 'node:path';
|
|
11
|
-
import { spawn } from 'node:child_process';
|
|
12
|
-
import { destroyList } from './destroy-list.js';
|
|
13
|
-
import { BRAND_PINK } from '../brand-colors.js';
|
|
14
|
-
import { formatTrackName } from './format-utils.js';
|
|
15
|
-
import { buildAudioEnv, detectMp3Player } from '../audio-env.js';
|
|
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
|
-
const _hintLabel = '{#455a64-fg}[Space] Preview [Enter] Select [Esc] Cancel{/#455a64-fg}';
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Open a small volume input modal (0–100).
|
|
29
|
-
* Left/Right arrows adjust by 5; type a number directly; Enter confirms.
|
|
30
|
-
*
|
|
31
|
-
* @param {object} screen - blessed screen
|
|
32
|
-
* @param {number} currentVol - current volume (0-100)
|
|
33
|
-
* @param {Function} onConfirm - called with volume (number) on Enter
|
|
34
|
-
* @param {Function} [onClose] - called when modal closes (confirm or cancel)
|
|
35
|
-
*/
|
|
36
|
-
export function openVolumeInput(screen, currentVol, onConfirm, onClose) {
|
|
37
|
-
if (IS_TEST) { onConfirm(currentVol ?? 70); return; }
|
|
38
|
-
let vol = (Number.isFinite(currentVol) && currentVol >= 0 && currentVol <= 100)
|
|
39
|
-
? currentVol : 70;
|
|
40
|
-
|
|
41
|
-
const box = blessed.box({
|
|
42
|
-
parent: screen,
|
|
43
|
-
top: 'center',
|
|
44
|
-
left: 'center',
|
|
45
|
-
width: 38,
|
|
46
|
-
height: 8,
|
|
47
|
-
border: { type: 'line' },
|
|
48
|
-
tags: true,
|
|
49
|
-
label: _modalTitle('Music Volume'),
|
|
50
|
-
style: { border: { fg: '
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
const barText = blessed.text({
|
|
54
|
-
parent: box,
|
|
55
|
-
top: 1,
|
|
56
|
-
left: 2,
|
|
57
|
-
width: 32,
|
|
58
|
-
tags: true,
|
|
59
|
-
content: '',
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
const hint = blessed.text({
|
|
63
|
-
parent: box,
|
|
64
|
-
top: 5,
|
|
65
|
-
left: 1,
|
|
66
|
-
width: 34,
|
|
67
|
-
tags: true,
|
|
68
|
-
content: '{#455a64-fg}[←→] ±5 [1-9] type [Enter] OK [Esc] Cancel{/#455a64-fg}',
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
function _renderBar() {
|
|
72
|
-
const filled = Math.round(vol / 5);
|
|
73
|
-
const empty = 20 - filled;
|
|
74
|
-
const bar = '{
|
|
75
|
-
'{#263238-fg}' + '░'.repeat(empty) + '{/#263238-fg}';
|
|
76
|
-
barText.setContent(`{#90a4ae-fg}Volume:{/#90a4ae-fg} ${bar} {bold}${vol}%{/bold}`);
|
|
77
|
-
screen.render();
|
|
78
|
-
}
|
|
79
|
-
_renderBar();
|
|
80
|
-
|
|
81
|
-
// Capture keypress directly on screen to avoid input mode issues
|
|
82
|
-
let _digits = '';
|
|
83
|
-
function _onKey(ch, key) {
|
|
84
|
-
const name = key?.name ?? '';
|
|
85
|
-
if (name === 'enter') { _close(true); return; }
|
|
86
|
-
if (name === 'escape') { _close(false); return; }
|
|
87
|
-
if (name === 'left') { vol = Math.max(0, vol - 5); _digits = ''; _renderBar(); return; }
|
|
88
|
-
if (name === 'right') { vol = Math.min(100, vol + 5); _digits = ''; _renderBar(); return; }
|
|
89
|
-
if (ch && /^[0-9]$/.test(ch)) {
|
|
90
|
-
_digits += ch;
|
|
91
|
-
const n = parseInt(_digits, 10);
|
|
92
|
-
if (n >= 0 && n <= 100) { vol = n; _renderBar(); }
|
|
93
|
-
if (_digits.length >= 3) _digits = '';
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
screen.on('keypress', _onKey);
|
|
97
|
-
|
|
98
|
-
function _close(confirm) {
|
|
99
|
-
screen.removeListener('keypress', _onKey);
|
|
100
|
-
box.destroy();
|
|
101
|
-
screen.render();
|
|
102
|
-
if (confirm && onConfirm) onConfirm(vol);
|
|
103
|
-
if (onClose) onClose();
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const BUILT_IN_TRACKS = [
|
|
108
|
-
{ label: '🎻 Soft Flamenco', file: 'agentvibes_soft_flamenco_loop.mp3' },
|
|
109
|
-
{ label: '🌸 Bossa Nova', file: 'agent_vibes_bossa_nova_v2_loop.mp3' },
|
|
110
|
-
{ label: '🌊 Chillwave', file: 'agent_vibes_chillwave_v2_loop.mp3' },
|
|
111
|
-
{ label: '🪘 Gnawa Ambient', file: 'agent_vibes_ganawa_ambient_v2_loop.mp3' },
|
|
112
|
-
];
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Open the background music track picker modal.
|
|
116
|
-
* After selecting a track, prompts for volume (0-100) via openVolumeInput.
|
|
117
|
-
*
|
|
118
|
-
* @param {object} screen - blessed screen
|
|
119
|
-
* @param {string} currentTrack - currently selected track filename
|
|
120
|
-
* @param {number} currentVolume - currently set volume (0-100, default 70)
|
|
121
|
-
* @param {Function} onSelect - called with (trackFile, volume)
|
|
122
|
-
* @param {Function} [onClose] - called after modal fully closes
|
|
123
|
-
*/
|
|
124
|
-
export function openTrackPicker(screen, currentTrack, currentVolume, onSelect, onClose) {
|
|
125
|
-
const tracksDir = path.join(process.cwd(), '.claude', 'audio', 'tracks');
|
|
126
|
-
let tracks;
|
|
127
|
-
try {
|
|
128
|
-
const files = fs.readdirSync(tracksDir);
|
|
129
|
-
tracks = files
|
|
130
|
-
.filter(f => /\.mp3$/i.test(f))
|
|
131
|
-
.sort()
|
|
132
|
-
.map(f => ({ file: f, label: formatTrackName(f) }));
|
|
133
|
-
} catch {
|
|
134
|
-
tracks = BUILT_IN_TRACKS;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const COLORS = {
|
|
138
|
-
btnFocus: '#
|
|
139
|
-
btnFocusFg: '#
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
const items = tracks.map(t =>
|
|
143
|
-
t.file === currentTrack ? `● ${t.label}` : ` ${t.label}`
|
|
144
|
-
);
|
|
145
|
-
const currentIdx = tracks.findIndex(t => t.file === currentTrack);
|
|
146
|
-
|
|
147
|
-
const listHeight = Math.min(tracks.length + 6, Math.floor(screen.rows * 0.7));
|
|
148
|
-
const list = blessed.list({
|
|
149
|
-
parent: screen,
|
|
150
|
-
top: 'center',
|
|
151
|
-
left: 'center',
|
|
152
|
-
width: 54,
|
|
153
|
-
height: listHeight,
|
|
154
|
-
border: { type: 'line' },
|
|
155
|
-
tags: true,
|
|
156
|
-
label: _modalTitle('Select Track'),
|
|
157
|
-
items,
|
|
158
|
-
keys: true,
|
|
159
|
-
vi: false,
|
|
160
|
-
mouse: true,
|
|
161
|
-
scrollable: true,
|
|
162
|
-
scrollbar: { ch: '│', track: { bg: '#1e2a3a' }, style: { fg: COLORS.btnFocus } },
|
|
163
|
-
style: {
|
|
164
|
-
border: { fg: COLORS.btnFocus },
|
|
165
|
-
selected: { bg: COLORS.btnFocus, fg: COLORS.btnFocusFg, bold: true },
|
|
166
|
-
item: { fg: '#e3f2fd' },
|
|
167
|
-
},
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
// Helper: update hint text in the bottom border label
|
|
171
|
-
function _setHint(text) {
|
|
172
|
-
list.setLabel({ text: _modalTitle('Select Track'), side: 'left' });
|
|
173
|
-
// Use _ prefix convention for bottom border content (blessed doesn't have setFooter)
|
|
174
|
-
list._label2 && list._label2.destroy();
|
|
175
|
-
list._label2 = blessed.text({
|
|
176
|
-
parent: list,
|
|
177
|
-
bottom: -1,
|
|
178
|
-
left: 1,
|
|
179
|
-
width: 50,
|
|
180
|
-
height: 1,
|
|
181
|
-
tags: true,
|
|
182
|
-
content: text,
|
|
183
|
-
style: { fg: '#e3f2fd' },
|
|
184
|
-
});
|
|
185
|
-
screen.render();
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
_setHint(_hintLabel);
|
|
189
|
-
|
|
190
|
-
if (currentIdx >= 0) list.select(currentIdx);
|
|
191
|
-
list.focus();
|
|
192
|
-
screen.render();
|
|
193
|
-
|
|
194
|
-
// Preview playback state
|
|
195
|
-
const _spawnEnv = buildAudioEnv();
|
|
196
|
-
const _mp3Player = detectMp3Player(_spawnEnv);
|
|
197
|
-
let _previewProc = null;
|
|
198
|
-
let _previewTrackId = null;
|
|
199
|
-
|
|
200
|
-
function _killPreview() {
|
|
201
|
-
if (_previewProc) {
|
|
202
|
-
try { process.kill(-_previewProc.pid, 'SIGTERM'); } catch {}
|
|
203
|
-
_previewProc = null;
|
|
204
|
-
}
|
|
205
|
-
_previewTrackId = null;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function _previewTrack(trackFile) {
|
|
209
|
-
// Toggle off if same track
|
|
210
|
-
if (_previewTrackId === trackFile) {
|
|
211
|
-
_killPreview();
|
|
212
|
-
_setHint(_hintLabel);
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
_killPreview();
|
|
217
|
-
|
|
218
|
-
const trackPath = path.resolve(tracksDir, trackFile);
|
|
219
|
-
const safeBase = path.resolve(tracksDir);
|
|
220
|
-
if (!trackPath.startsWith(safeBase + path.sep) && trackPath !== safeBase) return;
|
|
221
|
-
|
|
222
|
-
if (!_mp3Player || !fs.existsSync(trackPath)) {
|
|
223
|
-
_setHint('{red-fg}No MP3 player found or track missing{/red-fg}');
|
|
224
|
-
setTimeout(() => {
|
|
225
|
-
_setHint(_hintLabel);
|
|
226
|
-
}, 3000);
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
_previewProc = spawn(_mp3Player.bin, _mp3Player.args(trackPath), {
|
|
231
|
-
stdio: 'ignore', detached: true, env: _spawnEnv,
|
|
232
|
-
});
|
|
233
|
-
_previewTrackId = trackFile;
|
|
234
|
-
|
|
235
|
-
const label = tracks.find(t => t.file === trackFile)?.label ?? trackFile;
|
|
236
|
-
_setHint(`{
|
|
237
|
-
|
|
238
|
-
_previewProc.on('exit', () => {
|
|
239
|
-
if (_previewTrackId === trackFile) {
|
|
240
|
-
_previewTrackId = null;
|
|
241
|
-
_previewProc = null;
|
|
242
|
-
_setHint(_hintLabel);
|
|
243
|
-
}
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
_previewProc.on('error', () => {
|
|
247
|
-
_previewTrackId = null;
|
|
248
|
-
_previewProc = null;
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function _close(callback) {
|
|
253
|
-
_killPreview();
|
|
254
|
-
if (list._label2) list._label2.destroy();
|
|
255
|
-
if (callback) {
|
|
256
|
-
callback();
|
|
257
|
-
destroyList(list, screen, onClose);
|
|
258
|
-
} else {
|
|
259
|
-
destroyList(list, screen, onClose);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// Space = preview
|
|
264
|
-
list.key(['space'], () => {
|
|
265
|
-
const selected = tracks[list.selected];
|
|
266
|
-
if (selected) _previewTrack(selected.file);
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
// Enter = select track, then prompt for volume
|
|
270
|
-
list.key(['enter'], () => {
|
|
271
|
-
const selected = tracks[list.selected];
|
|
272
|
-
if (!selected) return;
|
|
273
|
-
// Close the track list first (without firing onClose yet), then open volume input
|
|
274
|
-
_killPreview();
|
|
275
|
-
if (list._label2) list._label2.destroy();
|
|
276
|
-
destroyList(list, screen, null);
|
|
277
|
-
openVolumeInput(screen, currentVolume ?? 70, (volume) => {
|
|
278
|
-
onSelect(selected.file, volume);
|
|
279
|
-
}, onClose);
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
list.key(['escape', 'q'], () => {
|
|
283
|
-
_close();
|
|
284
|
-
});
|
|
285
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* AgentVibes TUI — Shared Widget: Background Music Track Picker
|
|
3
|
+
*
|
|
4
|
+
* Inline modal list for selecting background music tracks.
|
|
5
|
+
* Extracted from settings-tab.js for reuse across tabs.
|
|
6
|
+
* Space previews track, Enter selects.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { spawn } from 'node:child_process';
|
|
12
|
+
import { destroyList } from './destroy-list.js';
|
|
13
|
+
import { BRAND_PINK } from '../brand-colors.js';
|
|
14
|
+
import { formatTrackName } from './format-utils.js';
|
|
15
|
+
import { buildAudioEnv, detectMp3Player } from '../audio-env.js';
|
|
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
|
+
const _hintLabel = '{#455a64-fg}[Space] Preview [Enter] Select [Esc] Cancel{/#455a64-fg}';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Open a small volume input modal (0–100).
|
|
29
|
+
* Left/Right arrows adjust by 5; type a number directly; Enter confirms.
|
|
30
|
+
*
|
|
31
|
+
* @param {object} screen - blessed screen
|
|
32
|
+
* @param {number} currentVol - current volume (0-100)
|
|
33
|
+
* @param {Function} onConfirm - called with volume (number) on Enter
|
|
34
|
+
* @param {Function} [onClose] - called when modal closes (confirm or cancel)
|
|
35
|
+
*/
|
|
36
|
+
export function openVolumeInput(screen, currentVol, onConfirm, onClose) {
|
|
37
|
+
if (IS_TEST) { onConfirm(currentVol ?? 70); return; }
|
|
38
|
+
let vol = (Number.isFinite(currentVol) && currentVol >= 0 && currentVol <= 100)
|
|
39
|
+
? currentVol : 70;
|
|
40
|
+
|
|
41
|
+
const box = blessed.box({
|
|
42
|
+
parent: screen,
|
|
43
|
+
top: 'center',
|
|
44
|
+
left: 'center',
|
|
45
|
+
width: 38,
|
|
46
|
+
height: 8,
|
|
47
|
+
border: { type: 'line' },
|
|
48
|
+
tags: true,
|
|
49
|
+
label: _modalTitle('Music Volume'),
|
|
50
|
+
style: { border: { fg: 'bright-cyan' } },
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const barText = blessed.text({
|
|
54
|
+
parent: box,
|
|
55
|
+
top: 1,
|
|
56
|
+
left: 2,
|
|
57
|
+
width: 32,
|
|
58
|
+
tags: true,
|
|
59
|
+
content: '',
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const hint = blessed.text({
|
|
63
|
+
parent: box,
|
|
64
|
+
top: 5,
|
|
65
|
+
left: 1,
|
|
66
|
+
width: 34,
|
|
67
|
+
tags: true,
|
|
68
|
+
content: '{#455a64-fg}[←→] ±5 [1-9] type [Enter] OK [Esc] Cancel{/#455a64-fg}',
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
function _renderBar() {
|
|
72
|
+
const filled = Math.round(vol / 5);
|
|
73
|
+
const empty = 20 - filled;
|
|
74
|
+
const bar = '{bright-cyan-fg}' + '█'.repeat(filled) + '{/bright-cyan-fg}' +
|
|
75
|
+
'{#263238-fg}' + '░'.repeat(empty) + '{/#263238-fg}';
|
|
76
|
+
barText.setContent(`{#90a4ae-fg}Volume:{/#90a4ae-fg} ${bar} {bold}${vol}%{/bold}`);
|
|
77
|
+
screen.render();
|
|
78
|
+
}
|
|
79
|
+
_renderBar();
|
|
80
|
+
|
|
81
|
+
// Capture keypress directly on screen to avoid input mode issues
|
|
82
|
+
let _digits = '';
|
|
83
|
+
function _onKey(ch, key) {
|
|
84
|
+
const name = key?.name ?? '';
|
|
85
|
+
if (name === 'enter') { _close(true); return; }
|
|
86
|
+
if (name === 'escape') { _close(false); return; }
|
|
87
|
+
if (name === 'left') { vol = Math.max(0, vol - 5); _digits = ''; _renderBar(); return; }
|
|
88
|
+
if (name === 'right') { vol = Math.min(100, vol + 5); _digits = ''; _renderBar(); return; }
|
|
89
|
+
if (ch && /^[0-9]$/.test(ch)) {
|
|
90
|
+
_digits += ch;
|
|
91
|
+
const n = parseInt(_digits, 10);
|
|
92
|
+
if (n >= 0 && n <= 100) { vol = n; _renderBar(); }
|
|
93
|
+
if (_digits.length >= 3) _digits = '';
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
screen.on('keypress', _onKey);
|
|
97
|
+
|
|
98
|
+
function _close(confirm) {
|
|
99
|
+
screen.removeListener('keypress', _onKey);
|
|
100
|
+
box.destroy();
|
|
101
|
+
screen.render();
|
|
102
|
+
if (confirm && onConfirm) onConfirm(vol);
|
|
103
|
+
if (onClose) onClose();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const BUILT_IN_TRACKS = [
|
|
108
|
+
{ label: '🎻 Soft Flamenco', file: 'agentvibes_soft_flamenco_loop.mp3' },
|
|
109
|
+
{ label: '🌸 Bossa Nova', file: 'agent_vibes_bossa_nova_v2_loop.mp3' },
|
|
110
|
+
{ label: '🌊 Chillwave', file: 'agent_vibes_chillwave_v2_loop.mp3' },
|
|
111
|
+
{ label: '🪘 Gnawa Ambient', file: 'agent_vibes_ganawa_ambient_v2_loop.mp3' },
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Open the background music track picker modal.
|
|
116
|
+
* After selecting a track, prompts for volume (0-100) via openVolumeInput.
|
|
117
|
+
*
|
|
118
|
+
* @param {object} screen - blessed screen
|
|
119
|
+
* @param {string} currentTrack - currently selected track filename
|
|
120
|
+
* @param {number} currentVolume - currently set volume (0-100, default 70)
|
|
121
|
+
* @param {Function} onSelect - called with (trackFile, volume)
|
|
122
|
+
* @param {Function} [onClose] - called after modal fully closes
|
|
123
|
+
*/
|
|
124
|
+
export function openTrackPicker(screen, currentTrack, currentVolume, onSelect, onClose) {
|
|
125
|
+
const tracksDir = path.join(process.cwd(), '.claude', 'audio', 'tracks');
|
|
126
|
+
let tracks;
|
|
127
|
+
try {
|
|
128
|
+
const files = fs.readdirSync(tracksDir);
|
|
129
|
+
tracks = files
|
|
130
|
+
.filter(f => /\.mp3$/i.test(f))
|
|
131
|
+
.sort()
|
|
132
|
+
.map(f => ({ file: f, label: formatTrackName(f) }));
|
|
133
|
+
} catch {
|
|
134
|
+
tracks = BUILT_IN_TRACKS;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const COLORS = {
|
|
138
|
+
btnFocus: '#2e7d32',
|
|
139
|
+
btnFocusFg: '#ffffff',
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const items = tracks.map(t =>
|
|
143
|
+
t.file === currentTrack ? `● ${t.label}` : ` ${t.label}`
|
|
144
|
+
);
|
|
145
|
+
const currentIdx = tracks.findIndex(t => t.file === currentTrack);
|
|
146
|
+
|
|
147
|
+
const listHeight = Math.min(tracks.length + 6, Math.floor(screen.rows * 0.7));
|
|
148
|
+
const list = blessed.list({
|
|
149
|
+
parent: screen,
|
|
150
|
+
top: 'center',
|
|
151
|
+
left: 'center',
|
|
152
|
+
width: 54,
|
|
153
|
+
height: listHeight,
|
|
154
|
+
border: { type: 'line' },
|
|
155
|
+
tags: true,
|
|
156
|
+
label: _modalTitle('Select Track'),
|
|
157
|
+
items,
|
|
158
|
+
keys: true,
|
|
159
|
+
vi: false,
|
|
160
|
+
mouse: true,
|
|
161
|
+
scrollable: true,
|
|
162
|
+
scrollbar: { ch: '│', track: { bg: '#1e2a3a' }, style: { fg: COLORS.btnFocus } },
|
|
163
|
+
style: {
|
|
164
|
+
border: { fg: COLORS.btnFocus },
|
|
165
|
+
selected: { bg: COLORS.btnFocus, fg: COLORS.btnFocusFg, bold: true },
|
|
166
|
+
item: { fg: '#e3f2fd' },
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Helper: update hint text in the bottom border label
|
|
171
|
+
function _setHint(text) {
|
|
172
|
+
list.setLabel({ text: _modalTitle('Select Track'), side: 'left' });
|
|
173
|
+
// Use _ prefix convention for bottom border content (blessed doesn't have setFooter)
|
|
174
|
+
list._label2 && list._label2.destroy();
|
|
175
|
+
list._label2 = blessed.text({
|
|
176
|
+
parent: list,
|
|
177
|
+
bottom: -1,
|
|
178
|
+
left: 1,
|
|
179
|
+
width: 50,
|
|
180
|
+
height: 1,
|
|
181
|
+
tags: true,
|
|
182
|
+
content: text,
|
|
183
|
+
style: { fg: '#e3f2fd' },
|
|
184
|
+
});
|
|
185
|
+
screen.render();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
_setHint(_hintLabel);
|
|
189
|
+
|
|
190
|
+
if (currentIdx >= 0) list.select(currentIdx);
|
|
191
|
+
list.focus();
|
|
192
|
+
screen.render();
|
|
193
|
+
|
|
194
|
+
// Preview playback state
|
|
195
|
+
const _spawnEnv = buildAudioEnv();
|
|
196
|
+
const _mp3Player = detectMp3Player(_spawnEnv);
|
|
197
|
+
let _previewProc = null;
|
|
198
|
+
let _previewTrackId = null;
|
|
199
|
+
|
|
200
|
+
function _killPreview() {
|
|
201
|
+
if (_previewProc) {
|
|
202
|
+
try { process.kill(-_previewProc.pid, 'SIGTERM'); } catch {}
|
|
203
|
+
_previewProc = null;
|
|
204
|
+
}
|
|
205
|
+
_previewTrackId = null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function _previewTrack(trackFile) {
|
|
209
|
+
// Toggle off if same track
|
|
210
|
+
if (_previewTrackId === trackFile) {
|
|
211
|
+
_killPreview();
|
|
212
|
+
_setHint(_hintLabel);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
_killPreview();
|
|
217
|
+
|
|
218
|
+
const trackPath = path.resolve(tracksDir, trackFile);
|
|
219
|
+
const safeBase = path.resolve(tracksDir);
|
|
220
|
+
if (!trackPath.startsWith(safeBase + path.sep) && trackPath !== safeBase) return;
|
|
221
|
+
|
|
222
|
+
if (!_mp3Player || !fs.existsSync(trackPath)) {
|
|
223
|
+
_setHint('{red-fg}No MP3 player found or track missing{/red-fg}');
|
|
224
|
+
setTimeout(() => {
|
|
225
|
+
_setHint(_hintLabel);
|
|
226
|
+
}, 3000);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
_previewProc = spawn(_mp3Player.bin, _mp3Player.args(trackPath), {
|
|
231
|
+
stdio: 'ignore', detached: true, env: _spawnEnv,
|
|
232
|
+
});
|
|
233
|
+
_previewTrackId = trackFile;
|
|
234
|
+
|
|
235
|
+
const label = tracks.find(t => t.file === trackFile)?.label ?? trackFile;
|
|
236
|
+
_setHint(`{bright-cyan-fg}♪ Previewing: ${label} (Space to stop){/bright-cyan-fg}`);
|
|
237
|
+
|
|
238
|
+
_previewProc.on('exit', () => {
|
|
239
|
+
if (_previewTrackId === trackFile) {
|
|
240
|
+
_previewTrackId = null;
|
|
241
|
+
_previewProc = null;
|
|
242
|
+
_setHint(_hintLabel);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
_previewProc.on('error', () => {
|
|
247
|
+
_previewTrackId = null;
|
|
248
|
+
_previewProc = null;
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function _close(callback) {
|
|
253
|
+
_killPreview();
|
|
254
|
+
if (list._label2) list._label2.destroy();
|
|
255
|
+
if (callback) {
|
|
256
|
+
callback();
|
|
257
|
+
destroyList(list, screen, onClose);
|
|
258
|
+
} else {
|
|
259
|
+
destroyList(list, screen, onClose);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Space = preview
|
|
264
|
+
list.key(['space'], () => {
|
|
265
|
+
const selected = tracks[list.selected];
|
|
266
|
+
if (selected) _previewTrack(selected.file);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// Enter = select track, then prompt for volume
|
|
270
|
+
list.key(['enter'], () => {
|
|
271
|
+
const selected = tracks[list.selected];
|
|
272
|
+
if (!selected) return;
|
|
273
|
+
// Close the track list first (without firing onClose yet), then open volume input
|
|
274
|
+
_killPreview();
|
|
275
|
+
if (list._label2) list._label2.destroy();
|
|
276
|
+
destroyList(list, screen, null);
|
|
277
|
+
openVolumeInput(screen, currentVolume ?? 70, (volume) => {
|
|
278
|
+
onSelect(selected.file, volume);
|
|
279
|
+
}, onClose);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
list.key(['escape', 'q'], () => {
|
|
283
|
+
_close();
|
|
284
|
+
});
|
|
285
|
+
}
|