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.
@@ -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
- .sort()
186
- .map(f => ({ id: f, label: formatTrackLabel(f), isBuiltIn: builtInIds.has(f) }));
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
- // Known limitation: _updateHint and _tlTick (blink) can interleave,
474
- // causing brief visual glitches. The _refreshing guard prevents the worst
475
- // cases but is not a complete fix. Acceptable for a TUI animation.
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
- const hadBlink = (items[_hintIdx].content ?? '').endsWith(' █');
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
- let c = items[idx].content ?? '';
487
- const hasBlink = c.endsWith(' █');
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 ?? '').replace(/ █$/, ''));
935
+ items[_tlBlink.sel].setContent(_stripBlink(items[_tlBlink.sel].content));
914
936
  }
915
937
  _tlBlink.sel = cur;
916
938
  if (items[cur]) {
917
- const base = (items[cur].content ?? '').replace(/ █$/, '');
918
- items[cur].setContent(_tlBlink.on ? `${base} █` : base);
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
- if (items[_tlBlink.sel]) items[_tlBlink.sel].setContent((items[_tlBlink.sel].content ?? '') + ' █');
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
- // Restore the hinted item to its clean base; for non-hinted items just strip █
943
- items[sel].setContent(sel === _hintIdx ? _hintBase : (items[sel].content ?? '').replace(/ █$/, ''));
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(_hintBase);
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: 2,
572
- content: 'Search:', style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
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: 3, left: 2, right: 2, bottom: 5,
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}[↑↓/jk] Navigate [Enter] Select [Space] Preview [/] Search [Esc] Cancel{/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
- return ` ${dot} ${name}${meta.gender.padEnd(COL_G)}${meta.provider}`;
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
- const filtered = _getFiltered();
639
- const items = _buildItems(filtered);
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 filtered = _getFiltered();
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 filtered = _getFiltered();
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 = _getFiltered().indexOf(currentVoice);
732
+ const activeIdx = _allVoices.indexOf(currentVoice);
731
733
  if (activeIdx >= 0) vpList.select(activeIdx);
732
734
  vpList.focus();
733
735
  screen.render();