agentvibes 4.0.1 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/config/audio-effects.cfg +3 -2
- package/.claude/config/background-music-position.txt +1 -1
- package/.claude/hooks/audio-processor.sh +87 -43
- package/.claude/hooks/bmad-speak.sh +184 -27
- package/.claude/hooks/play-tts-enhanced.sh +40 -5
- package/.claude/hooks/play-tts-macos.sh +29 -6
- package/.claude/hooks/play-tts-piper.sh +166 -65
- package/.claude/hooks/play-tts-soprano.sh +42 -6
- package/.claude/hooks/play-tts-ssh-remote.sh +117 -38
- package/.claude/hooks/play-tts.sh +12 -9
- package/.claude/hooks/session-start-tts.sh +10 -0
- package/.claude/hooks/stop-tts.sh +84 -0
- package/.claude/hooks/tts-queue-worker.sh +51 -20
- package/.claude/hooks/tts-queue.sh +37 -8
- package/.claude/hooks/voice-manager.sh +5 -1
- package/CLAUDE.md +0 -11
- package/README.md +176 -78
- package/RELEASE_NOTES.md +1197 -60
- package/bin/agentvibes-voice-browser.js +35 -21
- package/mcp-server/server.py +36 -0
- package/package.json +1 -3
- package/src/console/app.js +22 -4
- package/src/console/constants/personalities.js +44 -0
- package/src/console/footer-config.js +5 -1
- package/src/console/navigation.js +3 -2
- package/src/console/tabs/agents-tab.js +1219 -72
- package/src/console/tabs/placeholder-tab.js +9 -2
- package/src/console/tabs/receiver-tab.js +1212 -0
- package/src/console/tabs/settings-tab.js +33 -323
- package/src/console/widgets/destroy-list.js +25 -0
- package/src/console/widgets/format-utils.js +89 -0
- package/src/console/widgets/notice.js +55 -0
- package/src/console/widgets/personality-picker.js +185 -0
- package/src/console/widgets/reverb-picker.js +94 -0
- package/src/console/widgets/track-picker.js +285 -0
- package/src/installer.js +54 -2
- package/src/services/agent-voice-store.js +282 -22
- package/src/services/navigation-service.js +1 -1
- package/src/utils/music-file-validator.js +41 -31
- package/templates/agentvibes-receiver.sh +431 -111
|
@@ -21,6 +21,12 @@ import {
|
|
|
21
21
|
import { formatTrackLabel, scanTracks, getMusicFavorites, toggleMusicFavorite, applyTrackToAudioEffects } from './music-tab.js';
|
|
22
22
|
import { BRAND_PINK, BRAND_BLUE } from '../brand-colors.js';
|
|
23
23
|
import { buildAudioEnv, detectMp3Player, detectWavPlayer } from '../audio-env.js';
|
|
24
|
+
import { destroyList } from '../widgets/destroy-list.js';
|
|
25
|
+
import { openReverbPicker } from '../widgets/reverb-picker.js';
|
|
26
|
+
import { openPersonalityPicker } from '../widgets/personality-picker.js';
|
|
27
|
+
import { PERSONALITY_EMOJIS } from '../constants/personalities.js';
|
|
28
|
+
import { formatTrackName as _sharedFormatTrackName, formatReverbState as _sharedFormatReverbState } from '../widgets/format-utils.js';
|
|
29
|
+
import { showNotice as _showNoticeWidget } from '../widgets/notice.js';
|
|
24
30
|
|
|
25
31
|
const IS_TEST = process.env.AGENTVIBES_TEST_MODE === 'true';
|
|
26
32
|
|
|
@@ -74,82 +80,11 @@ const MUSIC_DEFAULTS = Object.freeze({ enabled: false, track: 'agentvibes_soft_f
|
|
|
74
80
|
// Verbosity display labels
|
|
75
81
|
const VERBOSITY_LABELS = Object.freeze({ high: 'High', medium: 'Medium', low: 'Low', minimal: 'Minimal', custom: 'Custom' });
|
|
76
82
|
|
|
77
|
-
// Personality emojis
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
dramatic: '🎭',
|
|
83
|
-
'dry-humor': '😐',
|
|
84
|
-
flirty: '😘',
|
|
85
|
-
funny: '😂',
|
|
86
|
-
grandpa: '👴',
|
|
87
|
-
millennial: '🙄',
|
|
88
|
-
moody: '😒',
|
|
89
|
-
none: '😊',
|
|
90
|
-
normal: '😊',
|
|
91
|
-
pirate: '⚓',
|
|
92
|
-
poetic: '📜',
|
|
93
|
-
professional: '👔',
|
|
94
|
-
rapper: '🎤',
|
|
95
|
-
robot: '🤖',
|
|
96
|
-
sarcastic: '😏',
|
|
97
|
-
sassy: '💁',
|
|
98
|
-
'surfer-dude':'🏄',
|
|
99
|
-
zen: '🧘',
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
// Known personalities (matches .claude/personalities/ directory)
|
|
103
|
-
const PERSONALITIES = Object.freeze([
|
|
104
|
-
'none', 'angry', 'annoying', 'crass', 'dramatic', 'dry-humor',
|
|
105
|
-
'flirty', 'funny', 'grandpa', 'millennial', 'moody', 'normal',
|
|
106
|
-
'pirate', 'poetic', 'professional', 'rapper', 'robot', 'sarcastic',
|
|
107
|
-
'sassy', 'surfer-dude', 'zen',
|
|
108
|
-
]);
|
|
109
|
-
|
|
110
|
-
// Preview phrases — one short, exemplary, in-character line per personality.
|
|
111
|
-
// Spoken automatically when the cursor lands on a personality in the picker.
|
|
112
|
-
const PERSONALITY_PREVIEW_PHRASES = Object.freeze({
|
|
113
|
-
angry: "UNACCEPTABLE! This build time is a DISASTER! Fix it NOW or so help me!",
|
|
114
|
-
annoying: "Oh oh oh! Can I tell you something? Can I? Can I? PLEASE? It is so important!",
|
|
115
|
-
crass: "Well damn, that code runs like my uncle's truck. Barely, and it smells funny.",
|
|
116
|
-
dramatic: "The tests... have failed. I don't know how much longer I can do this.",
|
|
117
|
-
'dry-humor': "Your code worked. I too am surprised.",
|
|
118
|
-
flirty: "Ooh, a clean merge? You know exactly how to make my heart race.",
|
|
119
|
-
funny: "Why do programmers hate nature? Too many bugs. I will show myself out.",
|
|
120
|
-
grandpa: "Back in my day, we compiled by hand. Uphill. In the snow. Both ways.",
|
|
121
|
-
millennial: "I literally cannot even with this error. I am so done. Like, actually deceased.",
|
|
122
|
-
moody: "...It works. Whatever. Do not get used to it.",
|
|
123
|
-
pirate: "Arrr! The build be sailin' smooth today, matey! No barnacles in sight!",
|
|
124
|
-
poetic: "Like rivers to the sea, your code flows toward eventual compilation.",
|
|
125
|
-
professional: "I have completed the requested task and am prepared to document outcomes.",
|
|
126
|
-
rapper: "Yo! Clean code flowin', tests are glowin', no bugs showin'!",
|
|
127
|
-
robot: "TASK COMPLETE. EFFICIENCY: OPTIMAL. PROBABILITY OF SUCCESS: 97.3 PERCENT. BEEP.",
|
|
128
|
-
sarcastic: "Oh wow, another bug. What a completely unexpected surprise. Truly shocking.",
|
|
129
|
-
sassy: "Honey, whoever told you that was good code was not your friend.",
|
|
130
|
-
'surfer-dude':"Duuude! That commit totally shredded! Gnarly clean code, bro!",
|
|
131
|
-
zen: "The bug is not the enemy. The bug is the teacher. Breathe. Commit.",
|
|
132
|
-
random: "Who will I be today? Even I do not know. Expect the unexpected.",
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
// Human-readable track display names — matches installer track picker (src/installer.js:2280)
|
|
136
|
-
const TRACK_NAMES = Object.freeze({
|
|
137
|
-
'agentvibes_soft_flamenco_loop.mp3': '🎻 Soft Flamenco',
|
|
138
|
-
'agent_vibes_bachata_v1_loop.mp3': '🎺 Bachata',
|
|
139
|
-
'agent_vibes_salsa_v2_loop.mp3': '💃 Salsa',
|
|
140
|
-
'agent_vibes_cumbia_v1_loop.mp3': '🎸 Cumbia',
|
|
141
|
-
'agent_vibes_bossa_nova_v2_loop.mp3': '🌸 Bossa Nova',
|
|
142
|
-
'agent_vibes_japanese_city_pop_v1_loop.mp3': '🌆 Japanese City Pop',
|
|
143
|
-
'agent_vibes_chillwave_v2_loop.mp3': '🌊 Chillwave',
|
|
144
|
-
'agent_vibes_dark_chill_step_loop.mp3': '🌙 Dark Chill Step',
|
|
145
|
-
'agent_vibes_goa_trance_v2_loop.mp3': '🌀 Goa Trance',
|
|
146
|
-
'agent_vibes_harpsichord_v2_loop.mp3': '🎼 Harpsichord',
|
|
147
|
-
'agent_vibes_celtic_harp_v1_loop.mp3': '🎻 Celtic Harp',
|
|
148
|
-
'agent_vibes_hawaiian_slack_key_guitar_v2_loop.mp3': '🌺 Hawaiian Slack Key Guitar',
|
|
149
|
-
'agent_vibes_arabic_v2_loop.mp3': '🎵 Arabic Oud',
|
|
150
|
-
'agent_vibes_ganawa_ambient_v2_loop.mp3': '🪘 Gnawa Ambient',
|
|
151
|
-
'agent_vibes_tabla_dream_pop_v1_loop.mp3': '🥁 Tabla Dream Pop',
|
|
152
|
-
});
|
|
83
|
+
// Personality emojis and names imported from src/console/constants/personalities.js
|
|
84
|
+
// (via the import at the top of this file)
|
|
85
|
+
|
|
86
|
+
// Human-readable track display names — moved to shared widgets/format-utils.js
|
|
87
|
+
// TRACK_NAMES constant removed (M1 dedup). Use formatTrackName() instead.
|
|
153
88
|
|
|
154
89
|
// Built-in track list for the picker (fallback when tracks dir is missing)
|
|
155
90
|
const BUILT_IN_TRACKS = [
|
|
@@ -166,10 +101,7 @@ const BUILT_IN_TRACKS = [
|
|
|
166
101
|
* @param {string} preset - 'off' | 'light' | 'medium' | 'heavy' | 'cathedral'
|
|
167
102
|
* @returns {string}
|
|
168
103
|
*/
|
|
169
|
-
export
|
|
170
|
-
const LABELS = { off: 'Off', light: 'Light (Small room)', medium: 'Medium (Conference room)', heavy: 'Heavy (Large hall)', cathedral: 'Cathedral (Epic space)' };
|
|
171
|
-
return LABELS[preset] ?? LABELS.light;
|
|
172
|
-
}
|
|
104
|
+
export const formatReverbState = _sharedFormatReverbState;
|
|
173
105
|
|
|
174
106
|
/**
|
|
175
107
|
* @param {boolean} enabled
|
|
@@ -192,18 +124,7 @@ export function formatVolume(volume) {
|
|
|
192
124
|
* @param {string} track - filename (e.g. 'agentvibes_soft_flamenco_loop.mp3')
|
|
193
125
|
* @returns {string}
|
|
194
126
|
*/
|
|
195
|
-
export
|
|
196
|
-
if (!track) return 'None';
|
|
197
|
-
if (TRACK_NAMES[track]) return TRACK_NAMES[track];
|
|
198
|
-
// Custom/unknown track: strip extension, agentvibes_/agent_vibes_ prefix,
|
|
199
|
-
// _v1/_v2/_loop/_v1_loop/_v2_loop suffixes, then title-case each word
|
|
200
|
-
return track
|
|
201
|
-
.replace(/\.[^.]+$/, '')
|
|
202
|
-
.replace(/^agentvibes_|^agent_vibes_/, '')
|
|
203
|
-
.replace(/_v\d+_loop$|_loop$|_v\d+$/, '')
|
|
204
|
-
.replace(/_/g, ' ')
|
|
205
|
-
.replace(/\b\w/g, c => c.toUpperCase());
|
|
206
|
-
}
|
|
127
|
+
export const formatTrackName = _sharedFormatTrackName;
|
|
207
128
|
|
|
208
129
|
/**
|
|
209
130
|
* @param {string} verbosity - 'high' | 'medium' | 'low'
|
|
@@ -1131,7 +1052,7 @@ export function createSettingsTab(screen, services) {
|
|
|
1131
1052
|
});
|
|
1132
1053
|
|
|
1133
1054
|
const reverbChangeBtn = _createButton(box, screen, 'Change', COLORS, () => {
|
|
1134
|
-
|
|
1055
|
+
openReverbPicker(screen, configService.getConfig().effects?.reverbPreset ?? 'light', (preset) => {
|
|
1135
1056
|
_setEffects(configService, { reverbPreset: preset });
|
|
1136
1057
|
refreshDisplay();
|
|
1137
1058
|
}, _restoreFocus);
|
|
@@ -1315,7 +1236,7 @@ export function createSettingsTab(screen, services) {
|
|
|
1315
1236
|
});
|
|
1316
1237
|
|
|
1317
1238
|
const personalityChangeBtn = _createButton(box, screen, 'Change', COLORS, () => {
|
|
1318
|
-
|
|
1239
|
+
openPersonalityPicker(screen, configService.getConfig().personality ?? 'none', (name) => {
|
|
1319
1240
|
configService.set('personality', name);
|
|
1320
1241
|
refreshDisplay();
|
|
1321
1242
|
}, _restoreFocus);
|
|
@@ -2453,19 +2374,14 @@ function _openProviderPicker(screen, providerService, onSelect, onClose) {
|
|
|
2453
2374
|
}
|
|
2454
2375
|
|
|
2455
2376
|
// ---------------------------------------------------------------------------
|
|
2456
|
-
// Private: Destroy
|
|
2457
|
-
//
|
|
2377
|
+
// Private: Destroy helper — now imported from shared widgets/destroy-list.js
|
|
2378
|
+
// (kept as comment for git blame traceability)
|
|
2458
2379
|
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
if (screen.olines[r]?.[c]) screen.olines[r][c][0] = -1;
|
|
2465
|
-
} catch {}
|
|
2466
|
-
onClose?.();
|
|
2467
|
-
screen.render();
|
|
2468
|
-
}
|
|
2380
|
+
// NOTE: The following line was the old _destroyList definition, now using shared import:
|
|
2381
|
+
// import { destroyList } from '../widgets/destroy-list.js';
|
|
2382
|
+
//
|
|
2383
|
+
// Old code removed to eliminate duplication (M1 fix).
|
|
2384
|
+
// The shared destroyList has identical behavior.
|
|
2469
2385
|
|
|
2470
2386
|
// ---------------------------------------------------------------------------
|
|
2471
2387
|
// Private: Show a temporary stub notice text
|
|
@@ -2540,7 +2456,7 @@ function _showSavePreview(screen, filePath, data, onConfirm, onClose) {
|
|
|
2540
2456
|
},
|
|
2541
2457
|
});
|
|
2542
2458
|
|
|
2543
|
-
function _close() {
|
|
2459
|
+
function _close() { destroyList(modal, screen, onClose); }
|
|
2544
2460
|
|
|
2545
2461
|
modal.key(['escape'], _close);
|
|
2546
2462
|
|
|
@@ -2568,27 +2484,7 @@ function _showSavePreview(screen, filePath, data, onConfirm, onClose) {
|
|
|
2568
2484
|
}
|
|
2569
2485
|
|
|
2570
2486
|
function _showNotice(screen, message) {
|
|
2571
|
-
|
|
2572
|
-
const modal = blessed.box({
|
|
2573
|
-
parent: screen,
|
|
2574
|
-
top: 'center',
|
|
2575
|
-
left: 'center',
|
|
2576
|
-
width,
|
|
2577
|
-
height: 3,
|
|
2578
|
-
border: { type: 'line' },
|
|
2579
|
-
tags: true,
|
|
2580
|
-
content: `{center}${message}{/center}`,
|
|
2581
|
-
style: {
|
|
2582
|
-
fg: '#e3f2fd',
|
|
2583
|
-
bg: COLORS.contentBg,
|
|
2584
|
-
border: { fg: '#00e5ff' },
|
|
2585
|
-
},
|
|
2586
|
-
});
|
|
2587
|
-
screen.render();
|
|
2588
|
-
|
|
2589
|
-
setTimeout(() => {
|
|
2590
|
-
_destroyList(modal, screen);
|
|
2591
|
-
}, 2500);
|
|
2487
|
+
_showNoticeWidget(screen, message, { bg: COLORS.contentBg });
|
|
2592
2488
|
}
|
|
2593
2489
|
|
|
2594
2490
|
// ---------------------------------------------------------------------------
|
|
@@ -2605,64 +2501,8 @@ function _setEffects(configService, partial) {
|
|
|
2605
2501
|
}
|
|
2606
2502
|
|
|
2607
2503
|
// ---------------------------------------------------------------------------
|
|
2608
|
-
// Private:
|
|
2609
|
-
|
|
2610
|
-
function _openReverbPicker(screen, configService, onSelect, onClose) {
|
|
2611
|
-
const PRESETS = [
|
|
2612
|
-
{ label: 'Off (Dry, no reverb)', value: 'off' },
|
|
2613
|
-
{ label: 'Light (Small room)', value: 'light' },
|
|
2614
|
-
{ label: 'Medium (Conference room)', value: 'medium' },
|
|
2615
|
-
{ label: 'Heavy (Large hall)', value: 'heavy' },
|
|
2616
|
-
{ label: 'Cathedral (Epic space)', value: 'cathedral' },
|
|
2617
|
-
];
|
|
2618
|
-
|
|
2619
|
-
const currentPreset = configService.getConfig().effects?.reverbPreset ?? 'light';
|
|
2620
|
-
const currentIdx = Math.max(0, PRESETS.findIndex(p => p.value === currentPreset));
|
|
2621
|
-
|
|
2622
|
-
const list = blessed.list({
|
|
2623
|
-
parent: screen,
|
|
2624
|
-
top: 'center',
|
|
2625
|
-
left: 'center',
|
|
2626
|
-
width: 40,
|
|
2627
|
-
height: PRESETS.length + 4,
|
|
2628
|
-
border: { type: 'line' },
|
|
2629
|
-
tags: true,
|
|
2630
|
-
label: _modalTitle('Select Reverb Preset'),
|
|
2631
|
-
items: PRESETS.map((p, i) => (i === currentIdx ? `● ${p.label}` : ` ${p.label}`)),
|
|
2632
|
-
keys: true,
|
|
2633
|
-
vi: false,
|
|
2634
|
-
mouse: true,
|
|
2635
|
-
style: {
|
|
2636
|
-
border: { fg: COLORS.btnFocus },
|
|
2637
|
-
selected: { bg: COLORS.btnFocus, fg: COLORS.btnFocusFg, bold: true },
|
|
2638
|
-
item: { fg: '#e3f2fd' },
|
|
2639
|
-
},
|
|
2640
|
-
});
|
|
2641
|
-
|
|
2642
|
-
list.select(currentIdx);
|
|
2643
|
-
list.focus();
|
|
2644
|
-
screen.render();
|
|
2645
|
-
|
|
2646
|
-
list.key(['enter', 'space'], () => {
|
|
2647
|
-
const selected = PRESETS[list.selected];
|
|
2648
|
-
if (!selected) return;
|
|
2649
|
-
_destroyList(list, screen, onClose);
|
|
2650
|
-
|
|
2651
|
-
// Apply to audio config via effects-manager.sh
|
|
2652
|
-
const effectsScript = path.join(process.cwd(), '.claude', 'hooks', 'effects-manager.sh');
|
|
2653
|
-
spawnSync('bash', [effectsScript, 'set-reverb', selected.value, 'default'], {
|
|
2654
|
-
stdio: 'ignore',
|
|
2655
|
-
timeout: 5000,
|
|
2656
|
-
env: { ...process.env },
|
|
2657
|
-
});
|
|
2658
|
-
|
|
2659
|
-
onSelect(selected.value);
|
|
2660
|
-
});
|
|
2661
|
-
|
|
2662
|
-
list.key(['escape', 'q'], () => {
|
|
2663
|
-
_destroyList(list, screen, onClose);
|
|
2664
|
-
});
|
|
2665
|
-
}
|
|
2504
|
+
// Private: _openReverbPicker removed — now using shared import:
|
|
2505
|
+
// import { openReverbPicker } from '../widgets/reverb-picker.js';
|
|
2666
2506
|
|
|
2667
2507
|
// ---------------------------------------------------------------------------
|
|
2668
2508
|
// Private: Background music config read/write helpers
|
|
@@ -2737,18 +2577,18 @@ function _openTrackPicker(screen, configService, onSelect, onClose) {
|
|
|
2737
2577
|
if (!selected) return;
|
|
2738
2578
|
if (selected.file === ADD_SENTINEL) {
|
|
2739
2579
|
// Destroy list first, then open path-input dialog
|
|
2740
|
-
|
|
2580
|
+
destroyList(list, screen);
|
|
2741
2581
|
_openCustomTrackInput(screen, tracksDir, (newFile) => {
|
|
2742
2582
|
onSelect(newFile);
|
|
2743
2583
|
}, onClose);
|
|
2744
2584
|
return;
|
|
2745
2585
|
}
|
|
2746
|
-
|
|
2586
|
+
destroyList(list, screen, onClose);
|
|
2747
2587
|
onSelect(selected.file);
|
|
2748
2588
|
});
|
|
2749
2589
|
|
|
2750
2590
|
list.key(['escape', 'q'], () => {
|
|
2751
|
-
|
|
2591
|
+
destroyList(list, screen, onClose);
|
|
2752
2592
|
});
|
|
2753
2593
|
}
|
|
2754
2594
|
|
|
@@ -3301,13 +3141,13 @@ function _openVerbosityPicker(screen, configService, onDone, onClose) {
|
|
|
3301
3141
|
list.key(['enter', 'space'], () => {
|
|
3302
3142
|
const selected = levels[list.selected];
|
|
3303
3143
|
if (!selected) return;
|
|
3304
|
-
|
|
3144
|
+
destroyList(list, screen, onClose);
|
|
3305
3145
|
configService.set('verbosity', selected.toLowerCase());
|
|
3306
3146
|
onDone();
|
|
3307
3147
|
});
|
|
3308
3148
|
|
|
3309
3149
|
list.key(['escape', 'q'], () => {
|
|
3310
|
-
|
|
3150
|
+
destroyList(list, screen, onClose);
|
|
3311
3151
|
});
|
|
3312
3152
|
}
|
|
3313
3153
|
|
|
@@ -3815,135 +3655,5 @@ function _openVoiceBrowserModal(screen, providerService, configService, navigati
|
|
|
3815
3655
|
}
|
|
3816
3656
|
|
|
3817
3657
|
// ---------------------------------------------------------------------------
|
|
3818
|
-
// Private:
|
|
3819
|
-
|
|
3820
|
-
function _openPersonalityPicker(screen, configService, onSelect, onClose) {
|
|
3821
|
-
const current = configService.getConfig().personality ?? 'none';
|
|
3822
|
-
const currentIdx = Math.max(0, PERSONALITIES.indexOf(current));
|
|
3823
|
-
|
|
3824
|
-
const list = blessed.list({
|
|
3825
|
-
parent: screen,
|
|
3826
|
-
top: 'center',
|
|
3827
|
-
left: 'center',
|
|
3828
|
-
width: 44,
|
|
3829
|
-
height: Math.min(PERSONALITIES.length + 4, 22),
|
|
3830
|
-
border: { type: 'line' },
|
|
3831
|
-
tags: true,
|
|
3832
|
-
label: _modalTitle('Select Personality'),
|
|
3833
|
-
items: PERSONALITIES.map((p, i) => {
|
|
3834
|
-
const emoji = PERSONALITY_EMOJIS[p] ?? '✨';
|
|
3835
|
-
const label = p === 'none' ? 'None' : p.charAt(0).toUpperCase() + p.slice(1);
|
|
3836
|
-
const mark = i === currentIdx ? '✅' : ' ';
|
|
3837
|
-
return `${mark} ${emoji} ${label}`;
|
|
3838
|
-
}),
|
|
3839
|
-
keys: true,
|
|
3840
|
-
vi: true,
|
|
3841
|
-
mouse: true,
|
|
3842
|
-
style: {
|
|
3843
|
-
border: { fg: COLORS.btnFocus },
|
|
3844
|
-
selected: { bg: COLORS.btnFocus, fg: COLORS.btnFocusFg, bold: true },
|
|
3845
|
-
item: { fg: '#e3f2fd' },
|
|
3846
|
-
},
|
|
3847
|
-
});
|
|
3848
|
-
|
|
3849
|
-
list.select(currentIdx);
|
|
3850
|
-
list.focus();
|
|
3851
|
-
screen.render();
|
|
3852
|
-
|
|
3853
|
-
// ---------- Hover TTS preview ----------
|
|
3854
|
-
|
|
3855
|
-
let _pickerTtsProc = null;
|
|
3856
|
-
let _playingItemIdx = -1;
|
|
3857
|
-
|
|
3858
|
-
// Add or remove " (playing)" from a list item (strips any trailing █ first)
|
|
3859
|
-
function _setItemPlaying(idx, playing) {
|
|
3860
|
-
const item = list.items?.[idx];
|
|
3861
|
-
if (!item) return;
|
|
3862
|
-
const base = (item.content ?? '').replace(/ █$/, '').replace(/ \(playing\)$/, '');
|
|
3863
|
-
item.setContent(playing ? `${base} (playing)` : base);
|
|
3864
|
-
}
|
|
3865
|
-
|
|
3866
|
-
function _killPickerTts() {
|
|
3867
|
-
if (_pickerTtsProc) {
|
|
3868
|
-
try { process.kill(-_pickerTtsProc.pid, 'SIGTERM'); } catch {}
|
|
3869
|
-
_pickerTtsProc = null;
|
|
3870
|
-
}
|
|
3871
|
-
if (_playingItemIdx >= 0) {
|
|
3872
|
-
_setItemPlaying(_playingItemIdx, false);
|
|
3873
|
-
_playingItemIdx = -1;
|
|
3874
|
-
}
|
|
3875
|
-
}
|
|
3876
|
-
|
|
3877
|
-
function _speakPersonalityPreview(personality) {
|
|
3878
|
-
_killPickerTts();
|
|
3879
|
-
const phrase = PERSONALITY_PREVIEW_PHRASES[personality];
|
|
3880
|
-
if (!phrase) return;
|
|
3881
|
-
const ttsScript = path.join(process.cwd(), '.claude', 'hooks', 'play-tts.sh');
|
|
3882
|
-
_pickerTtsProc = spawn('bash', [ttsScript, phrase], {
|
|
3883
|
-
stdio: 'ignore',
|
|
3884
|
-
detached: true,
|
|
3885
|
-
env: buildAudioEnv(),
|
|
3886
|
-
});
|
|
3887
|
-
_playingItemIdx = list.selected;
|
|
3888
|
-
_setItemPlaying(_playingItemIdx, true);
|
|
3889
|
-
screen.render();
|
|
3890
|
-
// Clear indicator when audio finishes naturally
|
|
3891
|
-
_pickerTtsProc.on('exit', () => {
|
|
3892
|
-
if (_playingItemIdx >= 0) {
|
|
3893
|
-
_setItemPlaying(_playingItemIdx, false);
|
|
3894
|
-
_playingItemIdx = -1;
|
|
3895
|
-
screen.render();
|
|
3896
|
-
}
|
|
3897
|
-
_pickerTtsProc = null;
|
|
3898
|
-
});
|
|
3899
|
-
_pickerTtsProc.unref();
|
|
3900
|
-
}
|
|
3901
|
-
|
|
3902
|
-
// Hover: auto-speaks preview phrase when cursor moves
|
|
3903
|
-
list.on('select item', () => {
|
|
3904
|
-
_speakPersonalityPreview(PERSONALITIES[list.selected]);
|
|
3905
|
-
});
|
|
3906
|
-
|
|
3907
|
-
// [Space] plays the selected personality, or stops if the same item is already playing.
|
|
3908
|
-
// Uses item-aware toggle so navigating with ↓ (which auto-plays) doesn't prevent Space from working.
|
|
3909
|
-
list.key(['space'], () => {
|
|
3910
|
-
if (_pickerTtsProc && _playingItemIdx === list.selected) {
|
|
3911
|
-
_killPickerTts(); // true toggle: stop only if this exact item is playing
|
|
3912
|
-
} else {
|
|
3913
|
-
_speakPersonalityPreview(PERSONALITIES[list.selected]); // play or switch
|
|
3914
|
-
}
|
|
3915
|
-
});
|
|
3916
|
-
|
|
3917
|
-
// Type-to-jump: press a letter to jump to the first matching personality (cycles on repeat)
|
|
3918
|
-
const _jumpBlocked = new Set(['j', 'k', 'g', 'h', 'l', 'd', 'u', 'q']);
|
|
3919
|
-
list.on('keypress', (ch, key) => {
|
|
3920
|
-
if (!ch || key.ctrl || key.meta) return;
|
|
3921
|
-
const lower = ch.toLowerCase();
|
|
3922
|
-
if (!/^[a-z]$/.test(lower)) return;
|
|
3923
|
-
if (_jumpBlocked.has(lower)) return;
|
|
3924
|
-
const count = PERSONALITIES.length;
|
|
3925
|
-
const start = list.selected ?? 0;
|
|
3926
|
-
for (let i = 1; i <= count; i++) {
|
|
3927
|
-
const idx = (start + i) % count;
|
|
3928
|
-
if (PERSONALITIES[idx].startsWith(lower)) {
|
|
3929
|
-
list.select(idx);
|
|
3930
|
-
screen.render();
|
|
3931
|
-
break;
|
|
3932
|
-
}
|
|
3933
|
-
}
|
|
3934
|
-
});
|
|
3935
|
-
|
|
3936
|
-
// [Enter] confirms selection
|
|
3937
|
-
list.key(['enter'], () => {
|
|
3938
|
-
const selected = PERSONALITIES[list.selected];
|
|
3939
|
-
if (!selected) return;
|
|
3940
|
-
_killPickerTts();
|
|
3941
|
-
_destroyList(list, screen, onClose);
|
|
3942
|
-
onSelect(selected);
|
|
3943
|
-
});
|
|
3944
|
-
|
|
3945
|
-
list.key(['escape', 'q'], () => {
|
|
3946
|
-
_killPickerTts();
|
|
3947
|
-
_destroyList(list, screen, onClose);
|
|
3948
|
-
});
|
|
3949
|
-
}
|
|
3658
|
+
// Private: _openPersonalityPicker removed — now using shared import:
|
|
3659
|
+
// import { openPersonalityPicker } from '../widgets/personality-picker.js';
|
|
@@ -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='#00e5ff'] - 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 ?? '#00e5ff';
|
|
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
|
+
}
|