agentvibes 5.0.0 → 5.1.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/audio/tracks/Drifting Down the Hall.mp3 +0 -0
- package/.claude/audio/tracks/Late Night Hip Hop Groove.mp3 +0 -0
- package/.claude/audio/tracks/Midnight Charleston Stomp.mp3 +0 -0
- package/.claude/config/audio-effects.cfg +1 -1
- package/.claude/hooks/play-tts.sh +10 -3
- package/.claude/hooks-windows/play-tts.ps1 +37 -107
- package/README.md +16 -2
- package/RELEASE_NOTES.md +48 -0
- package/package.json +1 -1
- package/src/console/tabs/agents-tab.js +65 -62
- package/src/console/tabs/music-tab.js +49 -19
- package/src/console/tabs/settings-tab.js +39 -37
- package/src/console/tabs/setup-tab.js +346 -86
- package/src/console/tabs/voices-tab.js +152 -29
- package/src/console/widgets/format-utils.js +92 -89
- package/src/console/widgets/track-picker.js +325 -322
- package/src/installer.js +8 -8
- package/src/services/llm-provider-service.js +79 -0
|
@@ -75,6 +75,9 @@ const TRACK_DISPLAY = Object.freeze({
|
|
|
75
75
|
'agent_vibes_japanese_city_pop_v1_loop.mp3': '🌆 Japanese City Pop',
|
|
76
76
|
'agent_vibes_salsa_v2_loop.mp3': '💃 Salsa',
|
|
77
77
|
'agent_vibes_tabla_dream_pop_v1_loop.mp3': '🥁 Tabla Dream Pop',
|
|
78
|
+
'Late Night Hip Hop Groove.mp3': '🎤 Late Night Hip Hop Groove',
|
|
79
|
+
'Drifting Down the Hall.mp3': '🌃 Drifting Down the Hall',
|
|
80
|
+
'Midnight Charleston Stomp.mp3': '🎩 Midnight Charleston Stomp',
|
|
78
81
|
});
|
|
79
82
|
|
|
80
83
|
const BUILT_IN_TRACK_CATALOG = Object.freeze([
|
|
@@ -93,6 +96,9 @@ const BUILT_IN_TRACK_CATALOG = Object.freeze([
|
|
|
93
96
|
{ id: 'agent_vibes_japanese_city_pop_v1_loop.mp3', label: '🌆 Japanese City Pop' },
|
|
94
97
|
{ id: 'agent_vibes_salsa_v2_loop.mp3', label: '💃 Salsa' },
|
|
95
98
|
{ id: 'agent_vibes_tabla_dream_pop_v1_loop.mp3', label: '🥁 Tabla Dream Pop' },
|
|
99
|
+
{ id: 'Late Night Hip Hop Groove.mp3', label: '🎤 Late Night Hip Hop Groove' },
|
|
100
|
+
{ id: 'Drifting Down the Hall.mp3', label: '🌃 Drifting Down the Hall' },
|
|
101
|
+
{ id: 'Midnight Charleston Stomp.mp3', label: '🎩 Midnight Charleston Stomp' },
|
|
96
102
|
]);
|
|
97
103
|
|
|
98
104
|
// ---------------------------------------------------------------------------
|
|
@@ -180,10 +186,13 @@ export function scanTracks() {
|
|
|
180
186
|
try {
|
|
181
187
|
const files = fs.readdirSync(tracksDir);
|
|
182
188
|
const builtInIds = new Set(BUILT_IN_TRACK_CATALOG.map(t => t.id));
|
|
189
|
+
// Sort by the alphabetic part of the label (skip leading emoji/symbols)
|
|
190
|
+
// so the order reflects the track NAME, not the emoji codepoint.
|
|
191
|
+
const _sortKey = (s) => s.replace(/^[^a-zA-Z]+/, '');
|
|
183
192
|
return files
|
|
184
193
|
.filter(f => /\.mp3$/i.test(f))
|
|
185
|
-
.
|
|
186
|
-
.
|
|
194
|
+
.map(f => ({ id: f, label: formatTrackLabel(f), isBuiltIn: builtInIds.has(f) }))
|
|
195
|
+
.sort((a, b) => _sortKey(a.label).localeCompare(_sortKey(b.label), undefined, { sensitivity: 'base' }));
|
|
187
196
|
} catch {
|
|
188
197
|
// Directory not found or unreadable — use the static catalog
|
|
189
198
|
return BUILT_IN_TRACK_CATALOG.map(t => ({ ...t, isBuiltIn: true }));
|
|
@@ -470,24 +479,36 @@ export function createMusicTab(screen, services) {
|
|
|
470
479
|
let _hintBase = ''; // content of items[_hintIdx] before hint was appended
|
|
471
480
|
let _refreshing = false;
|
|
472
481
|
|
|
473
|
-
//
|
|
474
|
-
//
|
|
475
|
-
//
|
|
482
|
+
// Decoration helpers — strip blink cursor and (optionally) hint text from
|
|
483
|
+
// a list item's content. We strip hint by anchoring on the EXACT text from
|
|
484
|
+
// _rowHint() rather than a generic regex, so we cannot accidentally erase
|
|
485
|
+
// unrelated content that happens to contain similar tags.
|
|
486
|
+
const _BLINK_RE = / █/g;
|
|
487
|
+
function _stripBlink(raw) {
|
|
488
|
+
return (raw ?? '').replace(_BLINK_RE, '');
|
|
489
|
+
}
|
|
490
|
+
function _stripHint(raw) {
|
|
491
|
+
const hint = _rowHint();
|
|
492
|
+
if (!hint) return raw;
|
|
493
|
+
const noBlink = (raw ?? '').replace(_BLINK_RE, '');
|
|
494
|
+
if (noBlink.endsWith(hint)) return noBlink.slice(0, -hint.length);
|
|
495
|
+
return noBlink;
|
|
496
|
+
}
|
|
497
|
+
function _stripDecorations(raw) {
|
|
498
|
+
return _stripHint(_stripBlink(raw));
|
|
499
|
+
}
|
|
500
|
+
|
|
476
501
|
function _updateHint(idx) {
|
|
477
502
|
const items = trackList.items;
|
|
478
503
|
// Restore previously hinted row — pad with spaces to overwrite ghost hint text
|
|
479
504
|
const _pad = ' '.repeat(60);
|
|
480
505
|
if (_hintIdx >= 0 && _hintIdx !== idx && items[_hintIdx]) {
|
|
481
|
-
|
|
482
|
-
items[_hintIdx].setContent((hadBlink ? _hintBase + ' █' : _hintBase) + _pad);
|
|
506
|
+
items[_hintIdx].setContent(_hintBase + _pad);
|
|
483
507
|
}
|
|
484
508
|
// Add hint to the new row, saving its clean base first
|
|
485
509
|
if (idx >= 0 && items[idx]) {
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
if (hasBlink) c = c.slice(0, -2);
|
|
489
|
-
_hintBase = c;
|
|
490
|
-
items[idx].setContent(c + _rowHint() + (hasBlink ? ' █' : ''));
|
|
510
|
+
_hintBase = _stripDecorations(items[idx].content);
|
|
511
|
+
items[idx].setContent(_hintBase + _rowHint());
|
|
491
512
|
} else {
|
|
492
513
|
_hintBase = '';
|
|
493
514
|
}
|
|
@@ -909,13 +930,14 @@ export function createMusicTab(screen, services) {
|
|
|
909
930
|
_tlBlink.on = !_tlBlink.on;
|
|
910
931
|
const items = trackList.items;
|
|
911
932
|
const cur = trackList.selected ?? 0;
|
|
933
|
+
// Clean old row — only strip blink (hint, if any, is preserved)
|
|
912
934
|
if (_tlBlink.sel !== cur && _tlBlink.sel >= 0 && items[_tlBlink.sel]) {
|
|
913
|
-
items[_tlBlink.sel].setContent((items[_tlBlink.sel].content
|
|
935
|
+
items[_tlBlink.sel].setContent(_stripBlink(items[_tlBlink.sel].content));
|
|
914
936
|
}
|
|
915
937
|
_tlBlink.sel = cur;
|
|
916
938
|
if (items[cur]) {
|
|
917
|
-
const
|
|
918
|
-
items[cur].setContent(_tlBlink.on ? `${
|
|
939
|
+
const raw = _stripBlink(items[cur].content);
|
|
940
|
+
items[cur].setContent(_tlBlink.on ? `${raw} █` : raw);
|
|
919
941
|
}
|
|
920
942
|
screen.render();
|
|
921
943
|
}
|
|
@@ -927,7 +949,11 @@ export function createMusicTab(screen, services) {
|
|
|
927
949
|
_hintBase = '';
|
|
928
950
|
_updateHint(_tlBlink.sel);
|
|
929
951
|
const items = trackList.items;
|
|
930
|
-
|
|
952
|
+
// Append blink cursor — content already has hint from _updateHint
|
|
953
|
+
if (items[_tlBlink.sel]) {
|
|
954
|
+
const raw = _stripBlink(items[_tlBlink.sel].content);
|
|
955
|
+
items[_tlBlink.sel].setContent(raw + ' █');
|
|
956
|
+
}
|
|
931
957
|
if (!_playingTrackId) previewLine.setContent(_hintText());
|
|
932
958
|
screen.render();
|
|
933
959
|
_tlBlink.interval = setInterval(_tlTick, 500);
|
|
@@ -937,13 +963,17 @@ export function createMusicTab(screen, services) {
|
|
|
937
963
|
if (!_playingTrackId) previewLine.setContent('');
|
|
938
964
|
if (_tlBlink.interval) { clearInterval(_tlBlink.interval); _tlBlink.interval = null; }
|
|
939
965
|
const items = trackList.items;
|
|
966
|
+
// Strip blink from selected row, strip both from the hinted row
|
|
940
967
|
const sel = trackList.selected ?? 0;
|
|
941
968
|
if (items[sel]) {
|
|
942
|
-
|
|
943
|
-
|
|
969
|
+
if (sel === _hintIdx) {
|
|
970
|
+
items[sel].setContent(_stripDecorations(items[sel].content));
|
|
971
|
+
} else {
|
|
972
|
+
items[sel].setContent(_stripBlink(items[sel].content));
|
|
973
|
+
}
|
|
944
974
|
}
|
|
945
975
|
if (_hintIdx >= 0 && _hintIdx !== sel && items[_hintIdx]) {
|
|
946
|
-
items[_hintIdx].setContent(
|
|
976
|
+
items[_hintIdx].setContent(_stripDecorations(items[_hintIdx].content));
|
|
947
977
|
}
|
|
948
978
|
_hintIdx = -1;
|
|
949
979
|
_hintBase = '';
|
|
@@ -20,7 +20,7 @@ import os from 'node:os';
|
|
|
20
20
|
import crypto from 'node:crypto';
|
|
21
21
|
import { spawn } from 'node:child_process';
|
|
22
22
|
import {
|
|
23
|
-
scanInstalledVoices, getVoiceMeta, PIPER_VOICES_DIR, SAMPLE_PHRASES, parseMultiSpeaker,
|
|
23
|
+
scanInstalledVoices, getVoiceMeta, genderIconTag, PIPER_VOICES_DIR, SAMPLE_PHRASES, parseMultiSpeaker,
|
|
24
24
|
} from './voices-tab.js';
|
|
25
25
|
import { LanguageService } from '../../services/language-service.js';
|
|
26
26
|
import { SUPPORTED_LANGUAGES, t } from '../../i18n/strings.js';
|
|
@@ -525,7 +525,6 @@ export function createSettingsTab(screen, services) {
|
|
|
525
525
|
navigationService?.openModal();
|
|
526
526
|
|
|
527
527
|
let _allVoices = [];
|
|
528
|
-
let _filterText = '';
|
|
529
528
|
let _previewProc = null;
|
|
530
529
|
let _previewVoiceId = null;
|
|
531
530
|
let _vpClosed = false;
|
|
@@ -567,26 +566,16 @@ export function createSettingsTab(screen, services) {
|
|
|
567
566
|
});
|
|
568
567
|
vpModal.setFront();
|
|
569
568
|
|
|
569
|
+
const COL_N = 30;
|
|
570
|
+
const COL_G = 4;
|
|
570
571
|
blessed.text({
|
|
571
|
-
parent: vpModal, top: 1, left:
|
|
572
|
-
content: '
|
|
573
|
-
});
|
|
574
|
-
const vpSearch = blessed.textbox({
|
|
575
|
-
parent: vpModal, top: 1, left: 11, width: 40, height: 1,
|
|
576
|
-
inputOnFocus: true, keys: true,
|
|
577
|
-
style: { fg: COLORS.valueFg, bg: 'blue', focus: { bg: 'cyan' } },
|
|
578
|
-
});
|
|
579
|
-
|
|
580
|
-
const COL_N = 28;
|
|
581
|
-
const COL_G = 10;
|
|
582
|
-
blessed.text({
|
|
583
|
-
parent: vpModal, top: 2, left: 6, tags: true,
|
|
584
|
-
content: `{cyan-fg}${'Name'.padEnd(COL_N)}${'Gender'.padEnd(COL_G)}Provider{/cyan-fg}`,
|
|
572
|
+
parent: vpModal, top: 1, left: 6, tags: true,
|
|
573
|
+
content: `{cyan-fg}${'Name'.padEnd(COL_N)}{/cyan-fg}{magenta-fg}♀{/magenta-fg}/{bright-cyan-fg}♂{/bright-cyan-fg} {cyan-fg}Provider{/cyan-fg}`,
|
|
585
574
|
style: { bg: COLORS.contentBg },
|
|
586
575
|
});
|
|
587
576
|
|
|
588
577
|
const vpList = blessed.list({
|
|
589
|
-
parent: vpModal, top:
|
|
578
|
+
parent: vpModal, top: 2, left: 2, right: 2, bottom: 5,
|
|
590
579
|
keys: true, vi: true, mouse: true,
|
|
591
580
|
border: { type: 'line' },
|
|
592
581
|
scrollbar: { ch: '|', style: { fg: 'cyan' } },
|
|
@@ -606,16 +595,10 @@ export function createSettingsTab(screen, services) {
|
|
|
606
595
|
|
|
607
596
|
blessed.text({
|
|
608
597
|
parent: vpModal, bottom: 2, left: 2, right: 2, tags: true,
|
|
609
|
-
content: '{white-fg}[
|
|
598
|
+
content: '{white-fg}[↑↓] Nav [PgUp/PgDn] Page [Home/End] [a-z] Jump [Enter] Select [Space] Preview [Esc] Cancel{/white-fg}',
|
|
610
599
|
style: { bg: COLORS.contentBg },
|
|
611
600
|
});
|
|
612
601
|
|
|
613
|
-
function _getFiltered() {
|
|
614
|
-
if (!_filterText) return _allVoices;
|
|
615
|
-
const f = _filterText.toLowerCase();
|
|
616
|
-
return _allVoices.filter(v => v.toLowerCase().includes(f));
|
|
617
|
-
}
|
|
618
|
-
|
|
619
602
|
function _buildItems(voices) {
|
|
620
603
|
const currentVoice = providerService?.getActiveVoiceId() ?? '';
|
|
621
604
|
return voices.map(v => {
|
|
@@ -626,7 +609,8 @@ export function createSettingsTab(screen, services) {
|
|
|
626
609
|
const name = meta.displayName.length > COL_N
|
|
627
610
|
? meta.displayName.slice(0, COL_N - 1) + '…'
|
|
628
611
|
: meta.displayName.padEnd(COL_N);
|
|
629
|
-
|
|
612
|
+
// genderIconTag has invisible color tags — pad with literal spaces (1 visible char + 3 spaces = 4)
|
|
613
|
+
return ` ${dot} ${name}${genderIconTag(meta.gender)} ${meta.provider}`;
|
|
630
614
|
});
|
|
631
615
|
}
|
|
632
616
|
|
|
@@ -635,8 +619,10 @@ export function createSettingsTab(screen, services) {
|
|
|
635
619
|
const savedIdx = vpList.selected ?? 0;
|
|
636
620
|
const savedScroll = vpList.childBase ?? 0;
|
|
637
621
|
_allVoices = scanInstalledVoices();
|
|
638
|
-
|
|
639
|
-
|
|
622
|
+
// Sort by display name so the first-letter quick jump is intuitive
|
|
623
|
+
_allVoices.sort((a, b) => getVoiceMeta(a).displayName.localeCompare(
|
|
624
|
+
getVoiceMeta(b).displayName, undefined, { sensitivity: 'base' }));
|
|
625
|
+
const items = _buildItems(_allVoices);
|
|
640
626
|
vpList.setItems(items.length > 0 ? items : [' (no voices found)']);
|
|
641
627
|
vpList.select(Math.min(savedIdx, items.length - 1));
|
|
642
628
|
vpList.childBase = Math.min(savedScroll, Math.max(0, items.length - (vpList.height - 2)));
|
|
@@ -704,14 +690,8 @@ export function createSettingsTab(screen, services) {
|
|
|
704
690
|
piper.on('error', () => { _previewProc = null; _previewVoiceId = null; });
|
|
705
691
|
}
|
|
706
692
|
|
|
707
|
-
vpSearch.on('keypress', () => {
|
|
708
|
-
setTimeout(() => { _filterText = vpSearch.getValue().trim(); _refreshVP(); }, 0);
|
|
709
|
-
});
|
|
710
|
-
vpSearch.key(['escape'], () => { vpList.focus(); screen.render(); });
|
|
711
|
-
vpList.key(['/'], () => { vpSearch.clearValue(); vpSearch.focus(); screen.render(); });
|
|
712
693
|
vpList.key(['enter'], () => {
|
|
713
|
-
const
|
|
714
|
-
const sel = filtered[vpList.selected];
|
|
694
|
+
const sel = _allVoices[vpList.selected];
|
|
715
695
|
if (sel) {
|
|
716
696
|
if (providerService) providerService.setActiveVoice(sel);
|
|
717
697
|
else configService.set('voice', sel);
|
|
@@ -719,15 +699,37 @@ export function createSettingsTab(screen, services) {
|
|
|
719
699
|
_closeVP();
|
|
720
700
|
});
|
|
721
701
|
vpList.key(['space'], () => {
|
|
722
|
-
const
|
|
723
|
-
const sel = filtered[vpList.selected];
|
|
702
|
+
const sel = _allVoices[vpList.selected];
|
|
724
703
|
if (sel) _previewVoice(sel);
|
|
725
704
|
});
|
|
726
705
|
vpList.key(['escape', 'q'], _closeVP);
|
|
727
706
|
|
|
707
|
+
// PageUp / PageDown / Home / End navigation
|
|
708
|
+
const _pageSize = () => Math.max(1, (vpList.height ?? 10) - 2);
|
|
709
|
+
vpList.key(['pageup'], () => { vpList.up(_pageSize()); screen.render(); });
|
|
710
|
+
vpList.key(['pagedown'], () => { vpList.down(_pageSize()); screen.render(); });
|
|
711
|
+
vpList.key(['home'], () => { vpList.select(0); screen.render(); });
|
|
712
|
+
vpList.key(['end'], () => { vpList.select(Math.max(0, _allVoices.length - 1)); screen.render(); });
|
|
713
|
+
|
|
714
|
+
// First-letter quick jump: typing 'a' jumps to the first voice starting
|
|
715
|
+
// with A. Block keys reserved by the list widget (vi nav, cancel) so
|
|
716
|
+
// they don't get swallowed: q (cancel), j/k/g/h/l (vi navigation).
|
|
717
|
+
const _vpJumpBlocked = new Set(['j', 'k', 'g', 'h', 'l', 'q']);
|
|
718
|
+
vpList.on('keypress', (ch, key) => {
|
|
719
|
+
if (!ch || key?.ctrl || key?.meta) return;
|
|
720
|
+
if (!/^[a-zA-Z]$/.test(ch)) return;
|
|
721
|
+
const target = ch.toLowerCase();
|
|
722
|
+
if (_vpJumpBlocked.has(target)) return;
|
|
723
|
+
const idx = _allVoices.findIndex(v => {
|
|
724
|
+
const name = getVoiceMeta(v).displayName.toLowerCase();
|
|
725
|
+
return name.startsWith(target);
|
|
726
|
+
});
|
|
727
|
+
if (idx >= 0) { vpList.select(idx); screen.render(); }
|
|
728
|
+
});
|
|
729
|
+
|
|
728
730
|
_refreshVP();
|
|
729
731
|
const currentVoice = providerService?.getActiveVoiceId() ?? '';
|
|
730
|
-
const activeIdx =
|
|
732
|
+
const activeIdx = _allVoices.indexOf(currentVoice);
|
|
731
733
|
if (activeIdx >= 0) vpList.select(activeIdx);
|
|
732
734
|
vpList.focus();
|
|
733
735
|
screen.render();
|