agentvibes 5.14.0 → 5.15.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/.claude/github-star-reminder.txt +1 -1
- package/.claude/hooks-windows/agentvibes-session-id.ps1 +83 -0
- package/.claude/hooks-windows/play-tts.ps1 +95 -1
- package/.claude/hooks-windows/session-start-tts.ps1 +36 -0
- package/README.md +16 -2
- package/RELEASE_NOTES.md +103 -0
- package/package.json +1 -1
- package/src/console/footer-config.js +1 -1
- package/src/console/music-preview.js +18 -2
- package/src/console/preview-transport.js +174 -0
- package/src/console/tabs/agents-tab.js +30 -10
- package/src/console/tabs/music-tab.js +52 -11
- package/src/console/tabs/setup-tab.js +36 -20
- package/src/console/widgets/track-picker.js +19 -10
- package/src/i18n/de.js +1 -1
- package/src/i18n/en.js +2 -2
- package/src/i18n/es.js +1 -1
- package/src/i18n/fr.js +1 -1
- package/src/i18n/hi.js +1 -1
- package/src/i18n/ja.js +1 -1
- package/src/i18n/ko.js +1 -1
- package/src/i18n/pt.js +1 -1
- package/src/i18n/zh-CN.js +1 -1
- package/src/installer.js +7 -2
- package/src/services/agent-voice-store.js +83 -41
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
getFavorites, getThumbsDown, toggleThumbsUp, toggleThumbsDown,
|
|
21
21
|
} from './voices-tab.js';
|
|
22
22
|
import { buildAudioEnv, detectWavPlayer, detectRemoteLlm } from '../audio-env.js';
|
|
23
|
+
import { createRowSpinner } from '../preview-transport.js';
|
|
23
24
|
import { voicesForProvider } from '../../services/provider-voice-catalog.js';
|
|
24
25
|
import { destroyList } from '../widgets/destroy-list.js';
|
|
25
26
|
import { BRAND_PINK } from '../brand-colors.js';
|
|
@@ -139,7 +140,7 @@ const COLORS = {
|
|
|
139
140
|
linkFg: 'bright-cyan',
|
|
140
141
|
};
|
|
141
142
|
|
|
142
|
-
const _FOOTER_BMAD_EN = '[↑↓/jk] Navigate [Space] Preview [Enter] Configure [A] Auto-assign [B] Bulk [
|
|
143
|
+
const _FOOTER_BMAD_EN = '[↑↓/jk] Navigate [Space] Preview [Enter] Configure [A] Auto-assign [B] Bulk [Del] Reset [Q] Quit';
|
|
143
144
|
const _FOOTER_NOBMAD_EN = '[Tab] Switch Tab [Q] Quit';
|
|
144
145
|
|
|
145
146
|
const _modalTitle = (text) => ` {${BRAND_PINK}-fg}${text}{/${BRAND_PINK}-fg} `;
|
|
@@ -332,6 +333,10 @@ ${_tl('bmadDesc')}
|
|
|
332
333
|
if (typeof focusMainTabBar === 'function') { focusMainTabBar(); screen.render(); }
|
|
333
334
|
});
|
|
334
335
|
|
|
336
|
+
// Manual re-check: after installing BMAD from a separate terminal, Enter
|
|
337
|
+
// re-scans without leaving the tab. (Switching tabs also re-scans via onFocus.)
|
|
338
|
+
onboardingBox.key(['enter'], () => { refreshDisplay(); });
|
|
339
|
+
|
|
335
340
|
// -------------------------------------------------------------------------
|
|
336
341
|
// BMAD state — section header
|
|
337
342
|
|
|
@@ -421,7 +426,7 @@ ${_tl('bmadDesc')}
|
|
|
421
426
|
left: 4,
|
|
422
427
|
hidden: true,
|
|
423
428
|
tags: true,
|
|
424
|
-
content: '{#546e7a-fg}[Space] Preview [Enter] Configure [
|
|
429
|
+
content: '{#546e7a-fg}[Space] Preview [Enter] Configure [Del] Reset [A] Auto-assign [B] Bulk Edit{/#546e7a-fg}',
|
|
425
430
|
style: { bg: COLORS.contentBg },
|
|
426
431
|
});
|
|
427
432
|
|
|
@@ -468,7 +473,7 @@ ${_tl('bmadDesc')}
|
|
|
468
473
|
return btn;
|
|
469
474
|
}
|
|
470
475
|
|
|
471
|
-
const resetBtn = _createBtn('[
|
|
476
|
+
const resetBtn = _createBtn('[Del] Reset', () => {
|
|
472
477
|
const agent = _agents[agentList.selected ?? 0];
|
|
473
478
|
if (agent) {
|
|
474
479
|
voiceStore.resetAgentProfile(agent.id);
|
|
@@ -1073,6 +1078,11 @@ ${_tl('bmadDesc')}
|
|
|
1073
1078
|
content: '', style: { fg: 'bright-cyan', bg: COLORS.contentBg },
|
|
1074
1079
|
});
|
|
1075
1080
|
|
|
1081
|
+
// Row spinner: "⠹ Previewing (locally|remotely via SSH) (Space to stop)" ON
|
|
1082
|
+
// the selected row — shared with every other picker. renderItem restores the
|
|
1083
|
+
// row on stop. vpPreviewLine is retained for parity but no longer shows preview.
|
|
1084
|
+
const _vpSpin = createRowSpinner(vpList, screen, (i) => _buildVoiceItems([_allVoices[i]])[0], { isClosed: () => _vpClosed });
|
|
1085
|
+
|
|
1076
1086
|
blessed.text({
|
|
1077
1087
|
parent: vpModal, bottom: 3, left: 2, right: 2, height: 1, tags: true,
|
|
1078
1088
|
content: '{white-fg}[↑↓] Nav [PgUp/PgDn] Page [a-z] Jump{/white-fg}',
|
|
@@ -1119,7 +1129,7 @@ ${_tl('bmadDesc')}
|
|
|
1119
1129
|
}
|
|
1120
1130
|
|
|
1121
1131
|
function _previewVoice(voiceId) {
|
|
1122
|
-
if (_previewVoiceId === voiceId) { _killVP();
|
|
1132
|
+
if (_previewVoiceId === voiceId) { _killVP(); _vpSpin.stop(); return; }
|
|
1123
1133
|
_killVP();
|
|
1124
1134
|
if (_previewMinTimer) { clearTimeout(_previewMinTimer); _previewMinTimer = null; }
|
|
1125
1135
|
|
|
@@ -1131,15 +1141,15 @@ ${_tl('bmadDesc')}
|
|
|
1131
1141
|
const playTtsScript = _hookScript('hooks-windows', 'play-tts.ps1');
|
|
1132
1142
|
if (!fs.existsSync(playTtsScript)) return;
|
|
1133
1143
|
_previewVoiceId = voiceId;
|
|
1134
|
-
if (!_vpClosed) {
|
|
1144
|
+
if (!_vpClosed) { _vpSpin.start(vpList.selected, false); }
|
|
1135
1145
|
_previewProc = spawn('powershell', [ // NOSONAR
|
|
1136
1146
|
'-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', playTtsScript, phrase, voiceId,
|
|
1137
1147
|
], { stdio: 'ignore', detached: false, windowsHide: true,
|
|
1138
1148
|
env: { ..._spawnEnv, AGENTVIBES_VOICE_SOURCE: 'audition' } });
|
|
1139
1149
|
_previewProc.on('exit', () => {
|
|
1140
|
-
if (_previewVoiceId === voiceId) { _previewVoiceId = null; _previewProc = null;
|
|
1150
|
+
if (_previewVoiceId === voiceId) { _previewVoiceId = null; _previewProc = null; _vpSpin.stop(); }
|
|
1141
1151
|
});
|
|
1142
|
-
_previewProc.on('error', () => { _previewProc = null; _previewVoiceId = null; });
|
|
1152
|
+
_previewProc.on('error', () => { _previewProc = null; _previewVoiceId = null; _vpSpin.stop(); });
|
|
1143
1153
|
return;
|
|
1144
1154
|
}
|
|
1145
1155
|
|
|
@@ -1166,12 +1176,12 @@ ${_tl('bmadDesc')}
|
|
|
1166
1176
|
cwd: _projectRoot,
|
|
1167
1177
|
});
|
|
1168
1178
|
_previewVoiceId = voiceId;
|
|
1169
|
-
if (!_vpClosed) {
|
|
1179
|
+
if (!_vpClosed) { _vpSpin.start(vpList.selected, !!remoteLlm); }
|
|
1170
1180
|
|
|
1171
1181
|
const _clearAfterMinDisplay = () => {
|
|
1172
1182
|
if (_previewVoiceId === voiceId) {
|
|
1173
1183
|
_previewVoiceId = null; _previewProc = null;
|
|
1174
|
-
|
|
1184
|
+
_vpSpin.stop();
|
|
1175
1185
|
}
|
|
1176
1186
|
_previewMinTimer = null;
|
|
1177
1187
|
};
|
|
@@ -1878,7 +1888,12 @@ ${_tl('bmadDesc')}
|
|
|
1878
1888
|
// -------------------------------------------------------------------------
|
|
1879
1889
|
// Key bindings
|
|
1880
1890
|
|
|
1881
|
-
|
|
1891
|
+
// Reset uses Delete/Backspace — NOT a letter. 'x'/'X' is the GLOBAL shortcut
|
|
1892
|
+
// for the Receiver tab (navigation.js KEY_TO_TAB), and screen-level keys fire
|
|
1893
|
+
// even while this list is focused, so binding Reset to X reset the agent AND
|
|
1894
|
+
// jumped to the Receiver tab. Delete has no global binding and doesn't clash
|
|
1895
|
+
// with type-to-jump.
|
|
1896
|
+
agentList.key(['delete', 'backspace'], () => {
|
|
1882
1897
|
const agent = _agents[agentList.selected ?? 0];
|
|
1883
1898
|
if (agent) {
|
|
1884
1899
|
voiceStore.resetAgentProfile(agent.id);
|
|
@@ -2038,6 +2053,11 @@ ${_tl('bmadDesc')}
|
|
|
2038
2053
|
},
|
|
2039
2054
|
|
|
2040
2055
|
onFocus() {
|
|
2056
|
+
// Re-detect on focus so BMAD installed in another tab/terminal (or just
|
|
2057
|
+
// now, via the onboarding install command) appears without restarting the
|
|
2058
|
+
// TUI. Tab switching invokes onFocus, so returning to this tab IS the
|
|
2059
|
+
// rescan — onboarding ⇄ agent list flips automatically.
|
|
2060
|
+
refreshDisplay();
|
|
2041
2061
|
if (_bmadDetected) {
|
|
2042
2062
|
agentList.focus();
|
|
2043
2063
|
} else {
|
|
@@ -13,7 +13,7 @@ import fs from 'node:fs';
|
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
15
|
import { buildAudioEnv, spawnMp3Player } from '../audio-env.js';
|
|
16
|
-
import { resolveMusicProvider, spawnMusicRemote } from '../music-preview.js';
|
|
16
|
+
import { resolveMusicProvider, spawnMusicRemote, createRowSpinner } from '../music-preview.js';
|
|
17
17
|
import { playBlingCue } from '../bling.js';
|
|
18
18
|
import { t } from '../../i18n/strings.js';
|
|
19
19
|
|
|
@@ -372,6 +372,20 @@ export function createMusicTab(screen, services) {
|
|
|
372
372
|
},
|
|
373
373
|
});
|
|
374
374
|
|
|
375
|
+
// Row spinner: paints "⠹ Previewing (locally|remotely via SSH) (Space to stop)"
|
|
376
|
+
// ON the selected track row — shared with every other picker. renderItem
|
|
377
|
+
// restores the row's normal content when the preview stops.
|
|
378
|
+
const _trackSpin = createRowSpinner(trackList, screen, (i) => {
|
|
379
|
+
const t = _getVisibleTracks()[i];
|
|
380
|
+
if (!t) return '';
|
|
381
|
+
const { track: activeTrackId } = _getMusic(configService);
|
|
382
|
+
return _buildListItems([t], activeTrackId, getMusicFavorites(configService))[0];
|
|
383
|
+
// static: track rows carry double-width emoji; animating/reallocating them
|
|
384
|
+
// desyncs blessed's terminal output (jumps/corruption). Write the indicator
|
|
385
|
+
// ONCE (padded to full width to clear the old row), no animation, no realloc —
|
|
386
|
+
// the least-fragile option for emoji rows.
|
|
387
|
+
}, { isClosed: () => box.hidden, static: true });
|
|
388
|
+
|
|
375
389
|
// -------------------------------------------------------------------------
|
|
376
390
|
// Status panel
|
|
377
391
|
|
|
@@ -522,7 +536,23 @@ export function createMusicTab(screen, services) {
|
|
|
522
536
|
return _stripHint(_stripBlink(raw));
|
|
523
537
|
}
|
|
524
538
|
|
|
539
|
+
// Strip hint text + blink cursor from EVERY row and forget the hint anchor.
|
|
540
|
+
// Called when a preview starts so the internal buffer is clean before the
|
|
541
|
+
// spinner's full-repaint (otherwise a stale hint/blink would be faithfully
|
|
542
|
+
// re-rendered by the realloc).
|
|
543
|
+
function _clearRowDecorations() {
|
|
544
|
+
const items = trackList.items || [];
|
|
545
|
+
for (let i = 0; i < items.length; i++) {
|
|
546
|
+
if (items[i]) items[i].setContent(_stripDecorations(items[i].content));
|
|
547
|
+
}
|
|
548
|
+
_hintIdx = -1; _hintBase = '';
|
|
549
|
+
}
|
|
550
|
+
|
|
525
551
|
function _updateHint(idx) {
|
|
552
|
+
// While a preview spinner is animating a row, it owns row rendering — the
|
|
553
|
+
// inline hint must not rewrite rows (it would fight the 80ms spinner and
|
|
554
|
+
// leave ghost characters on the previewing row when the cursor moves).
|
|
555
|
+
if (_trackSpin.isActive()) return;
|
|
526
556
|
const items = trackList.items;
|
|
527
557
|
// Restore previously hinted row — pad with spaces to overwrite ghost hint text
|
|
528
558
|
const _pad = ' '.repeat(60);
|
|
@@ -621,17 +651,20 @@ export function createMusicTab(screen, services) {
|
|
|
621
651
|
if (_remotePlayingTrackId === trackId) {
|
|
622
652
|
_sendMusicRemote(_mp, { stop: true });
|
|
623
653
|
_remotePlayingTrackId = null;
|
|
654
|
+
_trackSpin.stop();
|
|
655
|
+
if (_listFocused) _updateHint(trackList.selected);
|
|
624
656
|
previewLine.setContent(_listFocused ? _hintText() : '');
|
|
625
657
|
screen.render();
|
|
626
658
|
return;
|
|
627
659
|
}
|
|
628
660
|
// Bling first (fire-and-forget, plays locally) — same readiness cue as the
|
|
629
|
-
// voice preview — then forward the track to the receiver.
|
|
661
|
+
// voice preview — then forward the track to the receiver. The receiver keeps
|
|
662
|
+
// playing until an explicit stop, so the row spinner persists (no floor).
|
|
630
663
|
playBlingCue(_PKG_ROOT);
|
|
631
|
-
const rlabel = _allTracks.find(t => t.id === trackId)?.label ?? formatTrackLabel(trackId);
|
|
632
664
|
if (_sendMusicRemote(_mp, { track: trackId })) {
|
|
633
665
|
_remotePlayingTrackId = trackId;
|
|
634
|
-
|
|
666
|
+
_clearRowDecorations(); // spinner owns the rows; wipe stale hint/blink first
|
|
667
|
+
_trackSpin.start(trackList.selected, true);
|
|
635
668
|
}
|
|
636
669
|
screen.render();
|
|
637
670
|
return;
|
|
@@ -642,6 +675,8 @@ export function createMusicTab(screen, services) {
|
|
|
642
675
|
if (_playingTrackId === trackId) {
|
|
643
676
|
_killPlayingProcess();
|
|
644
677
|
_playingTrackId = null;
|
|
678
|
+
_trackSpin.stop();
|
|
679
|
+
if (_listFocused) _updateHint(trackList.selected);
|
|
645
680
|
previewLine.setContent(_listFocused ? _hintText() : '');
|
|
646
681
|
screen.render();
|
|
647
682
|
return;
|
|
@@ -668,17 +703,15 @@ export function createMusicTab(screen, services) {
|
|
|
668
703
|
|
|
669
704
|
_playingProcess = proc;
|
|
670
705
|
_playingTrackId = trackId;
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
previewLine.setContent(`{${COLORS.playingFg}-fg}♪ Previewing: ${label} (Space again to stop){/${COLORS.playingFg}-fg}`);
|
|
706
|
+
_clearRowDecorations(); // spinner owns the rows; wipe stale hint/blink first
|
|
707
|
+
_trackSpin.start(trackList.selected, false);
|
|
674
708
|
screen.render();
|
|
675
709
|
|
|
676
710
|
proc.on('exit', () => {
|
|
677
711
|
if (_playingTrackId === trackId) {
|
|
678
712
|
_playingTrackId = null;
|
|
679
713
|
_playingProcess = null;
|
|
680
|
-
|
|
681
|
-
refreshDisplay(); // clears (playing) label
|
|
714
|
+
_trackSpin.stop();
|
|
682
715
|
}
|
|
683
716
|
});
|
|
684
717
|
|
|
@@ -687,7 +720,7 @@ export function createMusicTab(screen, services) {
|
|
|
687
720
|
_killPlayingProcess();
|
|
688
721
|
_playingTrackId = null;
|
|
689
722
|
_playingProcess = null;
|
|
690
|
-
|
|
723
|
+
_trackSpin.stop();
|
|
691
724
|
}
|
|
692
725
|
});
|
|
693
726
|
}
|
|
@@ -819,6 +852,7 @@ export function createMusicTab(screen, services) {
|
|
|
819
852
|
function _close() {
|
|
820
853
|
_killPlayingProcess();
|
|
821
854
|
_playingTrackId = null;
|
|
855
|
+
_trackSpin.stop();
|
|
822
856
|
previewLine.setContent(_listFocused ? _hintText() : '');
|
|
823
857
|
modal.destroy();
|
|
824
858
|
trackList.focus();
|
|
@@ -942,8 +976,9 @@ export function createMusicTab(screen, services) {
|
|
|
942
976
|
trackList.key(['space'], () => {
|
|
943
977
|
const trackId = _getSelectedTrackId();
|
|
944
978
|
if (trackId) {
|
|
979
|
+
// No refreshDisplay() here — it rebuilds every row via setItems and would
|
|
980
|
+
// clobber the preview spinner's row (the spinner now shows preview state).
|
|
945
981
|
_playTrack(trackId);
|
|
946
|
-
refreshDisplay();
|
|
947
982
|
}
|
|
948
983
|
});
|
|
949
984
|
|
|
@@ -1013,6 +1048,10 @@ export function createMusicTab(screen, services) {
|
|
|
1013
1048
|
let _tlBlink = { interval: null, on: false, sel: -1 };
|
|
1014
1049
|
process.on('exit', () => { if (_tlBlink.interval) clearInterval(_tlBlink.interval); });
|
|
1015
1050
|
function _tlTick() {
|
|
1051
|
+
// While a preview is animating a row, the preview spinner (80ms) owns the
|
|
1052
|
+
// display — don't fight it with the 500ms blink cursor (that interleaving
|
|
1053
|
+
// left ghost characters on the previewing row).
|
|
1054
|
+
if (_trackSpin.isActive()) return;
|
|
1016
1055
|
_tlBlink.on = !_tlBlink.on;
|
|
1017
1056
|
const items = trackList.items;
|
|
1018
1057
|
const cur = trackList.selected ?? 0;
|
|
@@ -1134,6 +1173,7 @@ export function createMusicTab(screen, services) {
|
|
|
1134
1173
|
// Stop any preview when leaving the tab
|
|
1135
1174
|
_killPlayingProcess();
|
|
1136
1175
|
_playingTrackId = null;
|
|
1176
|
+
_trackSpin.stop();
|
|
1137
1177
|
previewLine.setContent('');
|
|
1138
1178
|
box.hide();
|
|
1139
1179
|
screen.render();
|
|
@@ -1148,6 +1188,7 @@ export function createMusicTab(screen, services) {
|
|
|
1148
1188
|
// Stop preview when focus leaves Music tab
|
|
1149
1189
|
_killPlayingProcess();
|
|
1150
1190
|
_playingTrackId = null;
|
|
1191
|
+
_trackSpin.stop();
|
|
1151
1192
|
},
|
|
1152
1193
|
|
|
1153
1194
|
getFooterText() {
|
|
@@ -46,6 +46,7 @@ import { destroyList } from '../widgets/destroy-list.js';
|
|
|
46
46
|
import { scanInstalledVoices, getVoiceMeta, previewPhrase, genderIconTag, formatVoiceRow, voiceRowHeader, PIPER_VOICES_DIR, SAMPLE_PHRASES, parseMultiSpeaker, getFavorites, getThumbsDown, toggleFavorite, toggleThumbsUp, toggleThumbsDown } from './voices-tab.js';
|
|
47
47
|
import { attachBtnBlink } from './agents-tab.js';
|
|
48
48
|
import { buildAudioEnv, detectWavPlayer } from '../audio-env.js';
|
|
49
|
+
import { previewRowContent, createRowSpinner, padTaggedTo } from '../preview-transport.js';
|
|
49
50
|
import { buildBlingCommand, playBlingCue } from '../bling.js';
|
|
50
51
|
import { spawn, spawnSync } from 'node:child_process';
|
|
51
52
|
|
|
@@ -2907,6 +2908,7 @@ export function createSetupTab(screen, services) {
|
|
|
2907
2908
|
let _kSpinFrame = 0;
|
|
2908
2909
|
let _kSpinningIdx = -1;
|
|
2909
2910
|
let _kSpinStartTs = 0;
|
|
2911
|
+
let _kSpinRemote = false; // whether the active preview is forwarded to the receiver
|
|
2910
2912
|
let _kFloorTimer = null; // pending _stopKSpinnerWithFloor timer (tracked so it can be cancelled)
|
|
2911
2913
|
// Remote SSH preview is fire-and-forget: play-tts-ssh-remote.sh backgrounds
|
|
2912
2914
|
// the ssh call and exits within milliseconds, so the row spinner would be
|
|
@@ -2916,17 +2918,20 @@ export function createSetupTab(screen, services) {
|
|
|
2916
2918
|
// visible "preview sent" cue (fire-and-forget has no playback signal to await).
|
|
2917
2919
|
const _K_MIN_SPIN_MS = 1100;
|
|
2918
2920
|
|
|
2919
|
-
function _startKSpinner(listIdx) {
|
|
2921
|
+
function _startKSpinner(listIdx, remote = false) {
|
|
2920
2922
|
_stopKSpinner();
|
|
2921
2923
|
_kSpinningIdx = listIdx;
|
|
2922
2924
|
_kSpinFrame = 0;
|
|
2923
2925
|
_kSpinStartTs = Date.now();
|
|
2926
|
+
_kSpinRemote = remote;
|
|
2924
2927
|
_kSpinInterval = setInterval(() => {
|
|
2925
2928
|
if (_kClosed) { _stopKSpinner(); return; }
|
|
2926
|
-
|
|
2927
|
-
|
|
2929
|
+
// Row indicator: "⠹ Previewing (locally|remotely via SSH) (Space to stop)".
|
|
2930
|
+
// The row IS the selected voice, so the name is intentionally omitted.
|
|
2931
|
+
kPicker.setItem(listIdx, padTaggedTo(previewRowContent(_K_SPIN[_kSpinFrame++ % _K_SPIN.length], _kSpinRemote), kPicker.width || 78));
|
|
2928
2932
|
screen.render();
|
|
2929
2933
|
}, 80);
|
|
2934
|
+
if (_kSpinInterval.unref) _kSpinInterval.unref(); // never keep the process alive
|
|
2930
2935
|
}
|
|
2931
2936
|
|
|
2932
2937
|
function _stopKSpinner() {
|
|
@@ -3277,7 +3282,7 @@ export function createSetupTab(screen, services) {
|
|
|
3277
3282
|
|
|
3278
3283
|
// ── Remote preview: route through SSH pipeline so receiver plays it ──
|
|
3279
3284
|
if (_validSshHost) {
|
|
3280
|
-
_startKSpinner(kPicker.selected);
|
|
3285
|
+
_startKSpinner(kPicker.selected, true);
|
|
3281
3286
|
const hookDir = path.join(packageDir, '.claude', 'hooks');
|
|
3282
3287
|
const remoteEnv = { ...process.env, CLAUDE_PROJECT_DIR: targetDir, AGENTVIBES_SSH_HOST: _sshHost };
|
|
3283
3288
|
if (_validSshKey) remoteEnv.AGENTVIBES_SSH_KEY = _sshKey;
|
|
@@ -3392,6 +3397,7 @@ export function createSetupTab(screen, services) {
|
|
|
3392
3397
|
}
|
|
3393
3398
|
screen.render();
|
|
3394
3399
|
}, 120);
|
|
3400
|
+
if (_kAnimInterval.unref) _kAnimInterval.unref(); // never keep the process alive
|
|
3395
3401
|
}
|
|
3396
3402
|
|
|
3397
3403
|
function _stopDlAnim() {
|
|
@@ -3788,10 +3794,15 @@ export function createSetupTab(screen, services) {
|
|
|
3788
3794
|
function _setHint(text) { elBox.setLabel(text || _defaultHint); screen.render(); }
|
|
3789
3795
|
_setHint(_defaultHint);
|
|
3790
3796
|
|
|
3797
|
+
// Row spinner: "⠹ Previewing (locally) (Space to stop)" on the selected row.
|
|
3798
|
+
// ElevenLabs preview always plays locally today, so the badge is honestly
|
|
3799
|
+
// local; _setHint stays for error messages only.
|
|
3800
|
+
const _elSpin = createRowSpinner(elPicker, screen, (i) => _items[i], { isClosed: () => _elClosed });
|
|
3801
|
+
|
|
3791
3802
|
function _previewEl() {
|
|
3792
3803
|
if (_elPreviewProc) { // toggle off
|
|
3793
3804
|
_killElPreview();
|
|
3794
|
-
|
|
3805
|
+
_elSpin.stop();
|
|
3795
3806
|
return;
|
|
3796
3807
|
}
|
|
3797
3808
|
const v = ELEVENLABS_VOICES[elPicker.selected];
|
|
@@ -3808,18 +3819,17 @@ export function createSetupTab(screen, services) {
|
|
|
3808
3819
|
return;
|
|
3809
3820
|
}
|
|
3810
3821
|
_elPreviewProc = proc;
|
|
3811
|
-
|
|
3822
|
+
_elSpin.start(elPicker.selected, false);
|
|
3812
3823
|
proc.on('exit', (code) => {
|
|
3813
3824
|
_elPreviewProc = null;
|
|
3814
3825
|
if (_elClosed) return;
|
|
3826
|
+
_elSpin.stop();
|
|
3815
3827
|
if (code && code !== 0) {
|
|
3816
3828
|
_setHint(' {red-fg}Preview failed — check API key / plan{/red-fg} ');
|
|
3817
3829
|
setTimeout(() => { if (!_elClosed) _setHint(_defaultHint); }, 3000);
|
|
3818
|
-
} else {
|
|
3819
|
-
_setHint(_defaultHint);
|
|
3820
3830
|
}
|
|
3821
3831
|
});
|
|
3822
|
-
proc.on('error', () => { _elPreviewProc = null; if (!_elClosed) _setHint(' {red-fg}Preview failed{/red-fg} '); });
|
|
3832
|
+
proc.on('error', () => { _elPreviewProc = null; _elSpin.stop(); if (!_elClosed) _setHint(' {red-fg}Preview failed{/red-fg} '); });
|
|
3823
3833
|
}
|
|
3824
3834
|
|
|
3825
3835
|
elPicker.key(['enter'], () => {
|
|
@@ -4137,6 +4147,11 @@ export function createSetupTab(screen, services) {
|
|
|
4137
4147
|
content: ' ', style: { fg: 'cyan', bg: COLORS.contentBg },
|
|
4138
4148
|
});
|
|
4139
4149
|
|
|
4150
|
+
// Row spinner: paints "⠹ Previewing (locally|remotely via SSH) (Space to stop)"
|
|
4151
|
+
// ON the selected row (shared with every other picker). renderItem restores the
|
|
4152
|
+
// row's normal content on stop. vpPreviewLine is kept for error messages only.
|
|
4153
|
+
const _vpSpin = createRowSpinner(vpList, screen, (i) => _buildVoiceItems([_allVoices[i]])[0], { isClosed: () => _vpClosed });
|
|
4154
|
+
|
|
4140
4155
|
// Movement hints at the bottom (primary actions live in the top help bar, so
|
|
4141
4156
|
// they are not duplicated here). Standardized [key] = label formatting.
|
|
4142
4157
|
blessed.text({
|
|
@@ -4186,7 +4201,7 @@ export function createSetupTab(screen, services) {
|
|
|
4186
4201
|
}
|
|
4187
4202
|
|
|
4188
4203
|
function _previewVoice(voiceId) {
|
|
4189
|
-
if (_previewVoiceId === voiceId) { _killVP();
|
|
4204
|
+
if (_previewVoiceId === voiceId) { _killVP(); _vpSpin.stop(); return; }
|
|
4190
4205
|
_killVP();
|
|
4191
4206
|
|
|
4192
4207
|
const phrase = previewPhrase(voiceId);
|
|
@@ -4238,12 +4253,12 @@ export function createSetupTab(screen, services) {
|
|
|
4238
4253
|
}
|
|
4239
4254
|
_previewProc = rProc;
|
|
4240
4255
|
_previewVoiceId = voiceId;
|
|
4241
|
-
if (!_vpClosed) {
|
|
4256
|
+
if (!_vpClosed) { _vpSpin.start(vpList.selected, true); }
|
|
4242
4257
|
rProc.on('exit', () => {
|
|
4243
4258
|
if (_previewVoiceId === voiceId) {
|
|
4244
4259
|
_previewVoiceId = null; _previewProc = null;
|
|
4245
|
-
//
|
|
4246
|
-
|
|
4260
|
+
// Fire-and-forget SSH exits in ms; keep the row cue up briefly, then restore.
|
|
4261
|
+
_vpSpin.stopWithFloor();
|
|
4247
4262
|
}
|
|
4248
4263
|
});
|
|
4249
4264
|
rProc.on('error', () => { _previewProc = null; _previewVoiceId = null; });
|
|
@@ -4279,9 +4294,9 @@ export function createSetupTab(screen, services) {
|
|
|
4279
4294
|
}
|
|
4280
4295
|
_previewProc = nProc;
|
|
4281
4296
|
_previewVoiceId = voiceId;
|
|
4282
|
-
if (!_vpClosed) {
|
|
4283
|
-
nProc.on('exit', () => { if (_previewVoiceId === voiceId) { _previewVoiceId = null; _previewProc = null;
|
|
4284
|
-
nProc.on('error', () => { _previewProc = null; _previewVoiceId = null; });
|
|
4297
|
+
if (!_vpClosed) { _vpSpin.start(vpList.selected, false); }
|
|
4298
|
+
nProc.on('exit', () => { if (_previewVoiceId === voiceId) { _previewVoiceId = null; _previewProc = null; _vpSpin.stop(); } });
|
|
4299
|
+
nProc.on('error', () => { _previewProc = null; _previewVoiceId = null; _vpSpin.stop(); });
|
|
4285
4300
|
return;
|
|
4286
4301
|
}
|
|
4287
4302
|
|
|
@@ -4316,14 +4331,14 @@ export function createSetupTab(screen, services) {
|
|
|
4316
4331
|
_previewVoiceId = voiceId;
|
|
4317
4332
|
|
|
4318
4333
|
if (!_vpClosed) {
|
|
4319
|
-
|
|
4320
|
-
_refreshVP();
|
|
4334
|
+
_vpSpin.start(vpList.selected, false);
|
|
4321
4335
|
}
|
|
4322
4336
|
|
|
4323
4337
|
piper.on('exit', (code) => {
|
|
4324
4338
|
if (_previewVoiceId !== voiceId) { try { fs.unlinkSync(tempWav); } catch {} return; }
|
|
4325
4339
|
if (code !== 0) {
|
|
4326
4340
|
_previewProc = null; _previewVoiceId = null;
|
|
4341
|
+
_vpSpin.stop();
|
|
4327
4342
|
if (!_vpClosed) {
|
|
4328
4343
|
vpPreviewLine.setContent('{red-fg}♪ Preview failed — is Piper installed?{/red-fg}');
|
|
4329
4344
|
screen.render();
|
|
@@ -4341,14 +4356,15 @@ export function createSetupTab(screen, services) {
|
|
|
4341
4356
|
env: _spawnEnv,
|
|
4342
4357
|
});
|
|
4343
4358
|
_previewProc = pp;
|
|
4344
|
-
|
|
4359
|
+
// Row spinner already running from the synth phase — keep it through playback.
|
|
4345
4360
|
pp.on('exit', () => {
|
|
4346
|
-
if (_previewVoiceId === voiceId) { _previewVoiceId = null; _previewProc = null;
|
|
4361
|
+
if (_previewVoiceId === voiceId) { _previewVoiceId = null; _previewProc = null; _vpSpin.stop(); }
|
|
4347
4362
|
try { fs.unlinkSync(tempWav); } catch {}
|
|
4348
4363
|
});
|
|
4349
4364
|
});
|
|
4350
4365
|
piper.on('error', () => {
|
|
4351
4366
|
_previewProc = null; _previewVoiceId = null;
|
|
4367
|
+
_vpSpin.stop();
|
|
4352
4368
|
if (!_vpClosed) {
|
|
4353
4369
|
vpPreviewLine.setContent('{red-fg}♪ Cannot find Piper — install it first{/red-fg}');
|
|
4354
4370
|
screen.render();
|
|
@@ -14,7 +14,7 @@ import { BRAND_PINK } from '../brand-colors.js';
|
|
|
14
14
|
import { renderHelpBar, selectorTitle } from './help-bar.js';
|
|
15
15
|
import { formatTrackName } from './format-utils.js';
|
|
16
16
|
import { buildAudioEnv, spawnMp3Player } from '../audio-env.js';
|
|
17
|
-
import { resolveMusicProvider, spawnMusicRemote } from '../music-preview.js';
|
|
17
|
+
import { resolveMusicProvider, spawnMusicRemote, createRowSpinner } from '../music-preview.js';
|
|
18
18
|
import { playBlingCue } from '../bling.js';
|
|
19
19
|
|
|
20
20
|
// AgentVibes package/repo root — resolves bundled assets (bling cue) and is the
|
|
@@ -231,12 +231,20 @@ export function openTrackPicker(screen, currentTrack, currentVolume, onSelect, o
|
|
|
231
231
|
},
|
|
232
232
|
});
|
|
233
233
|
|
|
234
|
-
// Transient
|
|
234
|
+
// Transient error status shows in the title area; idle restores the title.
|
|
235
235
|
function _setStatus(text) {
|
|
236
236
|
box.setLabel(text || TITLE);
|
|
237
237
|
screen.render();
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
+
// Row spinner: paints "⠹ Previewing (locally|remotely via SSH) (Space to stop)"
|
|
241
|
+
// ON the selected track row — shared with every other picker. _setStatus stays
|
|
242
|
+
// for error messages only.
|
|
243
|
+
let _closed = false;
|
|
244
|
+
const _rowSpin = createRowSpinner(list, screen, (i) => (
|
|
245
|
+
tracks[i] && tracks[i].file === currentTrack ? `● ${tracks[i].label}` : ` ${tracks[i] ? tracks[i].label : ''}`
|
|
246
|
+
), { isClosed: () => _closed });
|
|
247
|
+
|
|
240
248
|
if (currentIdx >= 0) list.select(currentIdx);
|
|
241
249
|
list.focus();
|
|
242
250
|
screen.render();
|
|
@@ -263,6 +271,7 @@ export function openTrackPicker(screen, currentTrack, currentVolume, onSelect, o
|
|
|
263
271
|
if (mp.remote) _sendRemote(mp, { stop: true });
|
|
264
272
|
_remotePreviewTrackId = null;
|
|
265
273
|
}
|
|
274
|
+
_rowSpin.stop();
|
|
266
275
|
}
|
|
267
276
|
|
|
268
277
|
/**
|
|
@@ -310,16 +319,16 @@ export function openTrackPicker(screen, currentTrack, currentVolume, onSelect, o
|
|
|
310
319
|
if (_remotePreviewTrackId === trackFile) {
|
|
311
320
|
_sendRemote(_mp, { stop: true });
|
|
312
321
|
_remotePreviewTrackId = null;
|
|
313
|
-
|
|
322
|
+
_rowSpin.stop();
|
|
314
323
|
return;
|
|
315
324
|
}
|
|
316
325
|
// Bling first (fire-and-forget, plays locally like the voice preview),
|
|
317
|
-
// then forward the track to the receiver.
|
|
326
|
+
// then forward the track to the receiver. The receiver keeps playing until
|
|
327
|
+
// an explicit stop, so the row spinner persists (no floor).
|
|
318
328
|
playBlingCue(_PKG_ROOT);
|
|
319
329
|
if (_sendRemote(_mp, { track: trackFile })) {
|
|
320
330
|
_remotePreviewTrackId = trackFile;
|
|
321
|
-
|
|
322
|
-
_setStatus(` {bright-cyan-fg}♪ ${rlabel} (Space to stop){/bright-cyan-fg} `);
|
|
331
|
+
_rowSpin.start(list.selected, true);
|
|
323
332
|
}
|
|
324
333
|
return;
|
|
325
334
|
}
|
|
@@ -357,25 +366,25 @@ export function openTrackPicker(screen, currentTrack, currentVolume, onSelect, o
|
|
|
357
366
|
|
|
358
367
|
_previewProc = proc;
|
|
359
368
|
_previewTrackId = trackFile;
|
|
360
|
-
|
|
361
|
-
const label = tracks.find(t => t.file === trackFile)?.label ?? trackFile;
|
|
362
|
-
_setStatus(` {bright-cyan-fg}♪ ${label} (Space to stop){/bright-cyan-fg} `);
|
|
369
|
+
_rowSpin.start(list.selected, false);
|
|
363
370
|
|
|
364
371
|
proc.on('exit', () => {
|
|
365
372
|
if (_previewTrackId === trackFile) {
|
|
366
373
|
_previewTrackId = null;
|
|
367
374
|
_previewProc = null;
|
|
368
|
-
|
|
375
|
+
_rowSpin.stop();
|
|
369
376
|
}
|
|
370
377
|
});
|
|
371
378
|
|
|
372
379
|
proc.on('error', () => {
|
|
373
380
|
_previewTrackId = null;
|
|
374
381
|
_previewProc = null;
|
|
382
|
+
_rowSpin.stop();
|
|
375
383
|
});
|
|
376
384
|
}
|
|
377
385
|
|
|
378
386
|
function _close(callback) {
|
|
387
|
+
_closed = true;
|
|
379
388
|
_killPreview();
|
|
380
389
|
if (callback) {
|
|
381
390
|
callback();
|
package/src/i18n/de.js
CHANGED
|
@@ -199,5 +199,5 @@ export default {
|
|
|
199
199
|
readmeScrollMore: "↓ Scrollen für mehr Inhalt ↓",
|
|
200
200
|
readmeNotFound: "*(Keine README.md im aktuellen Verzeichnis gefunden)*",
|
|
201
201
|
bmadFooterNobmad: "[Tab] Tab wechseln [Q] Beenden",
|
|
202
|
-
bmadFooterBmad: "[↑↓/jk] Navigieren [Space] Vorschau [Enter] Konfigurieren [A] Auto-zuweisen [B] Stapel [
|
|
202
|
+
bmadFooterBmad: "[↑↓/jk] Navigieren [Space] Vorschau [Enter] Konfigurieren [A] Auto-zuweisen [B] Stapel [Del] Zurücksetzen [Q] Beenden",
|
|
203
203
|
};
|
package/src/i18n/en.js
CHANGED
|
@@ -198,6 +198,6 @@ export default {
|
|
|
198
198
|
helpSearchLabel: "Search:",
|
|
199
199
|
readmeScrollMore: "↓ Scroll for more content ↓",
|
|
200
200
|
readmeNotFound: "*(No README.md found in current directory)*",
|
|
201
|
-
bmadFooterNobmad: "[Tab] Switch Tab [Q] Quit",
|
|
202
|
-
bmadFooterBmad: "[↑↓/jk] Navigate [Space] Preview [Enter] Configure [A] Auto-assign [B] Bulk [
|
|
201
|
+
bmadFooterNobmad: "[Enter] Re-check [Tab] Switch Tab [Q] Quit",
|
|
202
|
+
bmadFooterBmad: "[↑↓/jk] Navigate [Space] Preview [Enter] Configure [A] Auto-assign [B] Bulk [Del] Reset [Q] Quit",
|
|
203
203
|
};
|
package/src/i18n/es.js
CHANGED
|
@@ -199,5 +199,5 @@ export default {
|
|
|
199
199
|
readmeScrollMore: "↓ Desplázate para ver más ↓",
|
|
200
200
|
readmeNotFound: "*(No se encontró README.md en el directorio actual)*",
|
|
201
201
|
bmadFooterNobmad: "[Tab] Cambiar Pestaña [Q] Salir",
|
|
202
|
-
bmadFooterBmad: "[↑↓/jk] Navegar [Space] Previsualizar [Enter] Configurar [A] Auto-asignar [B] Masivo [
|
|
202
|
+
bmadFooterBmad: "[↑↓/jk] Navegar [Space] Previsualizar [Enter] Configurar [A] Auto-asignar [B] Masivo [Del] Restablecer [Q] Salir",
|
|
203
203
|
};
|
package/src/i18n/fr.js
CHANGED
|
@@ -199,5 +199,5 @@ export default {
|
|
|
199
199
|
readmeScrollMore: "↓ Défiler pour plus de contenu ↓",
|
|
200
200
|
readmeNotFound: "*(Aucun README.md trouvé dans le répertoire actuel)*",
|
|
201
201
|
bmadFooterNobmad: "[Tab] Changer d'onglet [Q] Quitter",
|
|
202
|
-
bmadFooterBmad: "[↑↓/jk] Naviguer [Space] Aperçu [Enter] Configurer [A] Auto-assigner [B] Lot [
|
|
202
|
+
bmadFooterBmad: "[↑↓/jk] Naviguer [Space] Aperçu [Enter] Configurer [A] Auto-assigner [B] Lot [Del] Réinitialiser [Q] Quitter",
|
|
203
203
|
};
|
package/src/i18n/hi.js
CHANGED
|
@@ -199,5 +199,5 @@ export default {
|
|
|
199
199
|
readmeScrollMore: "↓ अधिक सामग्री के लिए स्क्रॉल करें ↓",
|
|
200
200
|
readmeNotFound: "*(वर्तमान निर्देशिका में README.md नहीं मिला)*",
|
|
201
201
|
bmadFooterNobmad: "[Tab] टैब बदलें [Q] बाहर",
|
|
202
|
-
bmadFooterBmad: "[↑↓/jk] नेविगेट [Space] प्रीव्यू [Enter] कॉन्फ़िगर [A] ऑटो-असाइन [B] बल्क [
|
|
202
|
+
bmadFooterBmad: "[↑↓/jk] नेविगेट [Space] प्रीव्यू [Enter] कॉन्फ़िगर [A] ऑटो-असाइन [B] बल्क [Del] रीसेट [Q] बाहर",
|
|
203
203
|
};
|
package/src/i18n/ja.js
CHANGED
|
@@ -199,5 +199,5 @@ export default {
|
|
|
199
199
|
readmeScrollMore: "↓ スクロールして続きを見る ↓",
|
|
200
200
|
readmeNotFound: "*(現在のディレクトリにREADME.mdが見つかりません)*",
|
|
201
201
|
bmadFooterNobmad: "[Tab] タブ切替 [Q] 終了",
|
|
202
|
-
bmadFooterBmad: "[↑↓/jk] 移動 [Space] プレビュー [Enter] 設定 [A] 自動割当 [B] 一括 [
|
|
202
|
+
bmadFooterBmad: "[↑↓/jk] 移動 [Space] プレビュー [Enter] 設定 [A] 自動割当 [B] 一括 [Del] リセット [Q] 終了",
|
|
203
203
|
};
|
package/src/i18n/ko.js
CHANGED
|
@@ -199,5 +199,5 @@ export default {
|
|
|
199
199
|
readmeScrollMore: "↓ 아래로 스크롤하여 더 보기 ↓",
|
|
200
200
|
readmeNotFound: "*(현재 디렉토리에서 README.md를 찾을 수 없습니다)*",
|
|
201
201
|
bmadFooterNobmad: "[Tab] 탭 전환 [Q] 나가기",
|
|
202
|
-
bmadFooterBmad: "[↑↓/jk] 탐색 [Space] 미리보기 [Enter] 설정 [A] 자동배정 [B] 일괄 [
|
|
202
|
+
bmadFooterBmad: "[↑↓/jk] 탐색 [Space] 미리보기 [Enter] 설정 [A] 자동배정 [B] 일괄 [Del] 초기화 [Q] 나가기",
|
|
203
203
|
};
|
package/src/i18n/pt.js
CHANGED
|
@@ -199,5 +199,5 @@ export default {
|
|
|
199
199
|
readmeScrollMore: "↓ Role para mais conteúdo ↓",
|
|
200
200
|
readmeNotFound: "*(Nenhum README.md encontrado no diretório atual)*",
|
|
201
201
|
bmadFooterNobmad: "[Tab] Mudar Aba [Q] Sair",
|
|
202
|
-
bmadFooterBmad: "[↑↓/jk] Navegar [Space] Visualizar [Enter] Configurar [A] Auto-atribuir [B] Lote [
|
|
202
|
+
bmadFooterBmad: "[↑↓/jk] Navegar [Space] Visualizar [Enter] Configurar [A] Auto-atribuir [B] Lote [Del] Redefinir [Q] Sair",
|
|
203
203
|
};
|
package/src/i18n/zh-CN.js
CHANGED
|
@@ -199,5 +199,5 @@ export default {
|
|
|
199
199
|
readmeScrollMore: "↓ 向下滚动查看更多 ↓",
|
|
200
200
|
readmeNotFound: "*(当前目录中未找到README.md)*",
|
|
201
201
|
bmadFooterNobmad: "[Tab] 切换标签 [Q] 退出",
|
|
202
|
-
bmadFooterBmad: "[↑↓/jk] 导航 [Space] 预览 [Enter] 配置 [A] 自动分配 [B] 批量 [
|
|
202
|
+
bmadFooterBmad: "[↑↓/jk] 导航 [Space] 预览 [Enter] 配置 [A] 自动分配 [B] 批量 [Del] 重置 [Q] 退出",
|
|
203
203
|
};
|
package/src/installer.js
CHANGED
|
@@ -5442,8 +5442,13 @@ async function updateCommandFiles(targetDir, spinner) {
|
|
|
5442
5442
|
* These hooks contain bug fixes (e.g. markdown stripping) that must propagate
|
|
5443
5443
|
* on every `npx agentvibes update` regardless of target directory.
|
|
5444
5444
|
*/
|
|
5445
|
-
|
|
5446
|
-
|
|
5445
|
+
// agentvibes-session-id.{sh,ps1} are listed because play-tts.{sh,ps1} SHELL OUT to
|
|
5446
|
+
// them to expand a {{session}} pretext. They were absent here, so on a global
|
|
5447
|
+
// install the global-update path never shipped them and the self-ID silently
|
|
5448
|
+
// degraded to an empty string forever — the callers fail soft by design, so the
|
|
5449
|
+
// omission produced no error, just a feature that never worked.
|
|
5450
|
+
const CRITICAL_HOOKS = ['stop-tts.sh', 'stop.sh', 'play-tts.sh', 'play-tts-piper.sh', 'audio-processor.sh', 'session-start-tts.sh', 'bmad-party-speak.sh', 'agentvibes-session-id.sh'];
|
|
5451
|
+
const CRITICAL_HOOKS_WINDOWS = ['play-tts.ps1', 'play-tts-piper.ps1', 'audio-processor.ps1', 'session-start-tts.ps1', 'bmad-speak.ps1', 'bmad-party-speak.ps1', 'tts-watcher.ps1', 'agentvibes-session-id.ps1'];
|
|
5447
5452
|
|
|
5448
5453
|
/**
|
|
5449
5454
|
* Update critical hooks in the global ~/.claude/hooks/ directory if it exists.
|