agentvibes 5.1.3 → 5.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/.agentvibes/config.json +23 -13
- package/.claude/commands/agent-vibes/verbosity.md +98 -89
- package/.claude/config/audio-effects.cfg +6 -1
- package/.claude/hooks/bmad-speak.sh +2 -2
- package/.claude/hooks/piper-download-voices.sh +233 -225
- package/.claude/hooks/piper-installer.sh +1 -1
- package/.claude/hooks/piper-voice-manager.sh +125 -0
- package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +97 -90
- package/.claude/hooks/play-tts-enhanced.sh +1 -1
- package/.claude/hooks/play-tts-piper.sh +16 -5
- package/.claude/hooks/play-tts-ssh-remote.sh +168 -167
- package/.claude/hooks/play-tts.sh +31 -9
- package/.claude/hooks/session-start-tts.sh +4 -1
- package/.claude/hooks/stop-tts.sh +1 -1
- package/.claude/hooks/verbosity-manager.sh +185 -178
- package/.claude/hooks-windows/download-extra-voices.ps1 +243 -185
- package/.claude/hooks-windows/play-tts-piper.ps1 +7 -2
- package/.claude/hooks-windows/play-tts.ps1 +219 -65
- package/.claude/hooks-windows/session-start-tts.ps1 +2 -1
- package/.claude/hooks-windows/verbosity-manager.ps1 +126 -119
- package/README.md +24 -1
- package/RELEASE_NOTES.md +113 -0
- package/bin/agentvibes-voice-browser.js +1939 -1840
- package/mcp-server/server.py +75 -25
- package/package.json +1 -1
- package/src/console/tabs/receiver-tab.js +1527 -1483
- package/src/console/tabs/settings-tab.js +2 -2
- package/src/console/tabs/setup-tab.js +122 -20
- package/src/console/tabs/voices-tab.js +130 -13
- package/src/i18n/en.js +202 -202
- package/src/installer.js +29 -25
- package/src/services/llm-provider-service.js +114 -11
- package/src/services/verbosity-service.js +159 -157
- package/templates/agentvibes-receiver.sh +3 -2
|
@@ -64,7 +64,7 @@ const FOOTER_TEXT =
|
|
|
64
64
|
'[↑↓] Navigate [Enter] Edit [Esc] Tab Bar';
|
|
65
65
|
|
|
66
66
|
const MUSIC_DEFAULTS = Object.freeze({ enabled: false, track: 'agentvibes_soft_flamenco_loop.mp3', volume: 20 });
|
|
67
|
-
const VERBOSITY_LABELS = Object.freeze({ high: 'High', medium: 'Medium', low: 'Low', minimal: 'Minimal', custom: 'Custom' });
|
|
67
|
+
const VERBOSITY_LABELS = Object.freeze({ high: 'High', medium: 'Medium', low: 'Low', caveman: 'Caveman', minimal: 'Minimal', custom: 'Custom' });
|
|
68
68
|
|
|
69
69
|
// ---------------------------------------------------------------------------
|
|
70
70
|
// Exported format helpers (pure functions — used by tests and UI)
|
|
@@ -740,7 +740,7 @@ export function createSettingsTab(screen, services) {
|
|
|
740
740
|
function _editVerbosity() {
|
|
741
741
|
navigationService?.openModal();
|
|
742
742
|
|
|
743
|
-
const levels = ['high', 'medium', 'low'];
|
|
743
|
+
const levels = ['high', 'medium', 'low', 'caveman'];
|
|
744
744
|
const modal = blessed.list({
|
|
745
745
|
parent: screen,
|
|
746
746
|
top: 'center',
|
|
@@ -38,7 +38,7 @@ import { openReverbPicker, REVERB_PRESETS } from '../widgets/reverb-picker.js';
|
|
|
38
38
|
import { openTrackPicker, openVolumeInput } from '../widgets/track-picker.js';
|
|
39
39
|
import { formatTrackName } from '../widgets/format-utils.js';
|
|
40
40
|
import { destroyList } from '../widgets/destroy-list.js';
|
|
41
|
-
import { scanInstalledVoices, getVoiceMeta, genderIconTag, PIPER_VOICES_DIR, SAMPLE_PHRASES, parseMultiSpeaker, getFavorites, toggleFavorite } from './voices-tab.js';
|
|
41
|
+
import { scanInstalledVoices, getVoiceMeta, genderIconTag, PIPER_VOICES_DIR, SAMPLE_PHRASES, parseMultiSpeaker, getFavorites, getThumbsDown, toggleFavorite, toggleThumbsUp, toggleThumbsDown } from './voices-tab.js';
|
|
42
42
|
import { attachBtnBlink } from './agents-tab.js';
|
|
43
43
|
import { buildAudioEnv, detectWavPlayer } from '../audio-env.js';
|
|
44
44
|
import { spawn } from 'node:child_process';
|
|
@@ -565,6 +565,13 @@ export function createSetupTab(screen, services) {
|
|
|
565
565
|
},
|
|
566
566
|
});
|
|
567
567
|
|
|
568
|
+
// The "default" provider is config-only — it has no install/remove
|
|
569
|
+
// semantics. Hide those buttons and only show Configure.
|
|
570
|
+
if (provider.isDefault) {
|
|
571
|
+
installBtn.hide();
|
|
572
|
+
removeBtn.hide();
|
|
573
|
+
}
|
|
574
|
+
|
|
568
575
|
// Wire actions
|
|
569
576
|
installBtn.on('press', async () => { await handleProviderInstall(provider); });
|
|
570
577
|
installBtn.key(['enter', 'space'], async () => { await handleProviderInstall(provider); });
|
|
@@ -575,8 +582,10 @@ export function createSetupTab(screen, services) {
|
|
|
575
582
|
configBtn.on('press', async () => { await handleProviderConfigure(provider); });
|
|
576
583
|
configBtn.key(['enter', 'space'], async () => { await handleProviderConfigure(provider); });
|
|
577
584
|
|
|
578
|
-
// Navigation on each button
|
|
579
|
-
|
|
585
|
+
// Navigation on each button — for the default provider, only Configure
|
|
586
|
+
// is focusable since install/remove are hidden.
|
|
587
|
+
const navButtons = provider.isDefault ? [configBtn] : [installBtn, removeBtn, configBtn];
|
|
588
|
+
for (const btn of navButtons) {
|
|
580
589
|
btn.key(['tab', 'right'], () => { cycleFocus(1); });
|
|
581
590
|
btn.key(['S-tab', 'left'], () => { cycleFocus(-1); });
|
|
582
591
|
btn.key(['escape'], () => {
|
|
@@ -609,10 +618,17 @@ export function createSetupTab(screen, services) {
|
|
|
609
618
|
return { installBtn, removeBtn, configBtn };
|
|
610
619
|
}
|
|
611
620
|
|
|
612
|
-
// Build all provider rows
|
|
621
|
+
// Build all provider rows.
|
|
622
|
+
// For the default provider, install/remove are hidden — push configBtn
|
|
623
|
+
// three times so the row-of-3 arrow-nav arithmetic still works (every
|
|
624
|
+
// "slot" in the default row lands on Configure, the only visible button).
|
|
613
625
|
for (let i = 0; i < PROVIDERS.length; i++) {
|
|
614
626
|
const { installBtn, removeBtn, configBtn } = createProviderRow(PROVIDERS[i], i);
|
|
615
|
-
|
|
627
|
+
if (PROVIDERS[i].isDefault) {
|
|
628
|
+
providerFocusableItems.push(configBtn, configBtn, configBtn);
|
|
629
|
+
} else {
|
|
630
|
+
providerFocusableItems.push(installBtn, removeBtn, configBtn);
|
|
631
|
+
}
|
|
616
632
|
}
|
|
617
633
|
|
|
618
634
|
function cycleFocus(dir) {
|
|
@@ -624,6 +640,10 @@ export function createSetupTab(screen, services) {
|
|
|
624
640
|
// ── Provider install/remove handlers ──────────────────────────────────────
|
|
625
641
|
|
|
626
642
|
async function handleProviderInstall(provider) {
|
|
643
|
+
// Remember which button the user was on so we can advance focus to
|
|
644
|
+
// the NEXT row (same column) after they dismiss the info page.
|
|
645
|
+
_preInfoFocusIndex = providerFocusIndex;
|
|
646
|
+
|
|
627
647
|
if (provider.id === 'claude-code') {
|
|
628
648
|
const wasInstalled = installedState[provider.id];
|
|
629
649
|
const result = await installClaudeMcp(targetDir);
|
|
@@ -643,7 +663,6 @@ export function createSetupTab(screen, services) {
|
|
|
643
663
|
if (provider.id === 'openai-codex') {
|
|
644
664
|
const wasInstalled = installedState[provider.id];
|
|
645
665
|
const result = await installCodexMcp(targetDir);
|
|
646
|
-
await installCopilotMcp(targetDir);
|
|
647
666
|
await installCodexInstructions(targetDir, packageDir);
|
|
648
667
|
await installCodexHooks(targetDir, packageDir);
|
|
649
668
|
await refreshInstalledState();
|
|
@@ -652,6 +671,10 @@ export function createSetupTab(screen, services) {
|
|
|
652
671
|
}
|
|
653
672
|
|
|
654
673
|
async function handleProviderRemove(provider) {
|
|
674
|
+
// Remember which button the user was on so we can advance focus to
|
|
675
|
+
// the NEXT row (same column) after they dismiss the info page.
|
|
676
|
+
_preInfoFocusIndex = providerFocusIndex;
|
|
677
|
+
|
|
655
678
|
if (provider.id === 'claude-code') {
|
|
656
679
|
const result = await uninstallClaude(targetDir);
|
|
657
680
|
await refreshInstalledState();
|
|
@@ -668,7 +691,6 @@ export function createSetupTab(screen, services) {
|
|
|
668
691
|
|
|
669
692
|
if (provider.id === 'openai-codex') {
|
|
670
693
|
await removeCodexMcp(targetDir);
|
|
671
|
-
await removeCopilotMcp(targetDir);
|
|
672
694
|
await removeCodexInstructions(targetDir);
|
|
673
695
|
await removeCodexHooks(targetDir);
|
|
674
696
|
await refreshInstalledState();
|
|
@@ -683,6 +705,7 @@ export function createSetupTab(screen, services) {
|
|
|
683
705
|
'claude-code': 'claude-code',
|
|
684
706
|
'github-copilot': 'copilot',
|
|
685
707
|
'openai-codex': 'codex',
|
|
708
|
+
'default': 'default',
|
|
686
709
|
};
|
|
687
710
|
const llmKey = llmKeyMap[provider.id] || provider.id;
|
|
688
711
|
const config = loadLlmConfigSync(llmKey, targetDir);
|
|
@@ -701,6 +724,7 @@ export function createSetupTab(screen, services) {
|
|
|
701
724
|
'claude-code': 'Claude Code here',
|
|
702
725
|
'copilot': 'Copilot here',
|
|
703
726
|
'codex': 'Codex here',
|
|
727
|
+
'default': '', // empty by default — user sets via Configure
|
|
704
728
|
};
|
|
705
729
|
|
|
706
730
|
// Read global defaults for display
|
|
@@ -865,7 +889,7 @@ export function createSetupTab(screen, services) {
|
|
|
865
889
|
const hooksSubdir = process.platform === 'win32' ? 'hooks-windows' : 'hooks';
|
|
866
890
|
const isWin = process.platform === 'win32' && !process.env.WSL_DISTRO_NAME;
|
|
867
891
|
// Don't include pretext — play-tts already prepends it from the config
|
|
868
|
-
const sampleText = '
|
|
892
|
+
const sampleText = 'This is how your audio settings sound right now.';
|
|
869
893
|
|
|
870
894
|
let cmd, args;
|
|
871
895
|
if (isWin) {
|
|
@@ -1202,18 +1226,20 @@ export function createSetupTab(screen, services) {
|
|
|
1202
1226
|
|
|
1203
1227
|
blessed.text({
|
|
1204
1228
|
parent: vpModal, bottom: 2, left: 2, right: 2, tags: true,
|
|
1205
|
-
content: '{white-fg}[↑↓] Nav [PgUp/PgDn] Page [
|
|
1229
|
+
content: '{white-fg}[↑↓] Nav [PgUp/PgDn] Page [a-z] Jump [Enter] Select [Space] Preview [+] 👍 [-] 👎 [Esc] Cancel{/white-fg}',
|
|
1206
1230
|
style: { bg: COLORS.contentBg },
|
|
1207
1231
|
});
|
|
1208
1232
|
|
|
1209
1233
|
function _buildVoiceItems(voices) {
|
|
1210
1234
|
const favs = getFavorites(configService);
|
|
1235
|
+
const td = getThumbsDown(configService);
|
|
1211
1236
|
return voices.map(v => {
|
|
1212
1237
|
const isActive = v === draft.voice;
|
|
1213
1238
|
const isPrev = v === _previewVoiceId;
|
|
1214
|
-
const
|
|
1239
|
+
const isUp = favs.includes(v);
|
|
1240
|
+
const isDown = td.includes(v);
|
|
1215
1241
|
const dot = isPrev ? '♪' : (isActive ? '●' : ' ');
|
|
1216
|
-
const star =
|
|
1242
|
+
const star = isUp ? '{green-fg}👍{/green-fg}' : (isDown ? '{red-fg}👎{/red-fg}' : ' ');
|
|
1217
1243
|
const meta = getVoiceMeta(v);
|
|
1218
1244
|
const name = meta.displayName.length > COL_N
|
|
1219
1245
|
? meta.displayName.slice(0, COL_N - 1) + '…'
|
|
@@ -1242,13 +1268,52 @@ export function createSetupTab(screen, services) {
|
|
|
1242
1268
|
if (_previewVoiceId === voiceId) { _killVP(); vpPreviewLine.setContent(''); _refreshVP(); return; }
|
|
1243
1269
|
_killVP();
|
|
1244
1270
|
|
|
1271
|
+
const phrase = SAMPLE_PHRASES[Math.floor(Math.random() * SAMPLE_PHRASES.length)];
|
|
1272
|
+
|
|
1273
|
+
// Route through remote provider if active
|
|
1274
|
+
const _remoteProviders = ['ssh-remote', 'agentvibes-receiver'];
|
|
1275
|
+
let _activeProvider = '';
|
|
1276
|
+
try {
|
|
1277
|
+
const _projectRoot = path.resolve(__dirname, '..', '..');
|
|
1278
|
+
const _provPaths = [
|
|
1279
|
+
path.join(_projectRoot, '.claude', 'tts-provider.txt'),
|
|
1280
|
+
path.join(os.homedir(), '.claude', 'tts-provider.txt'),
|
|
1281
|
+
];
|
|
1282
|
+
for (const p of _provPaths) {
|
|
1283
|
+
if (fs.existsSync(p)) { _activeProvider = fs.readFileSync(p, 'utf8').trim(); break; }
|
|
1284
|
+
}
|
|
1285
|
+
} catch {}
|
|
1286
|
+
|
|
1287
|
+
if (_remoteProviders.includes(_activeProvider)) {
|
|
1288
|
+
const _projectRoot = path.resolve(__dirname, '..', '..');
|
|
1289
|
+
let rProc;
|
|
1290
|
+
if (_isWin) {
|
|
1291
|
+
const _playTts = path.join(_projectRoot, '.claude', 'hooks-windows', 'play-tts.ps1');
|
|
1292
|
+
rProc = spawn('powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', _playTts, phrase, voiceId], {
|
|
1293
|
+
stdio: 'ignore', detached: false, windowsHide: true, env: _spawnEnv,
|
|
1294
|
+
});
|
|
1295
|
+
} else {
|
|
1296
|
+
const _playTts = path.join(_projectRoot, '.claude', 'hooks', 'play-tts.sh');
|
|
1297
|
+
rProc = spawn('bash', [_playTts, phrase, voiceId], {
|
|
1298
|
+
stdio: 'ignore', detached: true, env: _spawnEnv,
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
_previewProc = rProc;
|
|
1302
|
+
_previewVoiceId = voiceId;
|
|
1303
|
+
if (!_vpClosed) { vpPreviewLine.setContent(`{cyan-fg}♪ Playing (remote): ${voiceId}{/cyan-fg}`); screen.render(); }
|
|
1304
|
+
rProc.on('exit', () => {
|
|
1305
|
+
if (_previewVoiceId === voiceId) { _previewVoiceId = null; _previewProc = null; if (!_vpClosed) { vpPreviewLine.setContent(''); _refreshVP(); } }
|
|
1306
|
+
});
|
|
1307
|
+
rProc.on('error', () => { _previewProc = null; _previewVoiceId = null; });
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1245
1311
|
const _ms = parseMultiSpeaker(voiceId);
|
|
1246
1312
|
const voicePath = path.resolve(PIPER_VOICES_DIR, _ms.model + '.onnx');
|
|
1247
1313
|
const safeBase = path.resolve(PIPER_VOICES_DIR);
|
|
1248
1314
|
if (!voicePath.startsWith(safeBase + path.sep) && voicePath !== safeBase) return;
|
|
1249
1315
|
|
|
1250
1316
|
const tempWav = _secureTempWav('vp');
|
|
1251
|
-
const phrase = SAMPLE_PHRASES[Math.floor(Math.random() * SAMPLE_PHRASES.length)];
|
|
1252
1317
|
|
|
1253
1318
|
let _piperBin = 'piper';
|
|
1254
1319
|
if (_isWin) {
|
|
@@ -1307,9 +1372,13 @@ export function createSetupTab(screen, services) {
|
|
|
1307
1372
|
const sel = _allVoices[vpList.selected];
|
|
1308
1373
|
if (sel) _previewVoice(sel);
|
|
1309
1374
|
});
|
|
1310
|
-
vpList.key(['*'], () => {
|
|
1375
|
+
vpList.key(['*', '+'], () => {
|
|
1376
|
+
const sel = _allVoices[vpList.selected];
|
|
1377
|
+
if (sel) { toggleThumbsUp(configService, sel); _refreshVP(); }
|
|
1378
|
+
});
|
|
1379
|
+
vpList.key(['-'], () => {
|
|
1311
1380
|
const sel = _allVoices[vpList.selected];
|
|
1312
|
-
if (sel) {
|
|
1381
|
+
if (sel) { toggleThumbsDown(configService, sel); _refreshVP(); }
|
|
1313
1382
|
});
|
|
1314
1383
|
vpList.key(['escape', 'q'], _closeVP);
|
|
1315
1384
|
|
|
@@ -1432,10 +1501,18 @@ export function createSetupTab(screen, services) {
|
|
|
1432
1501
|
function showAllProviderRows() {
|
|
1433
1502
|
providerHeader.show();
|
|
1434
1503
|
for (const row of providerRows) {
|
|
1504
|
+
const provider = PROVIDERS.find(p => p.id === row.id);
|
|
1435
1505
|
row.label.show();
|
|
1436
1506
|
row.statusText.show();
|
|
1437
|
-
|
|
1438
|
-
|
|
1507
|
+
// The "default" provider has no install/remove semantics — keep its
|
|
1508
|
+
// install/remove buttons hidden so only Configure shows.
|
|
1509
|
+
if (provider?.isDefault) {
|
|
1510
|
+
row.installBtn.hide();
|
|
1511
|
+
row.removeBtn.hide();
|
|
1512
|
+
} else {
|
|
1513
|
+
row.installBtn.show();
|
|
1514
|
+
row.removeBtn.show();
|
|
1515
|
+
}
|
|
1439
1516
|
row.configBtn.show();
|
|
1440
1517
|
}
|
|
1441
1518
|
}
|
|
@@ -1587,28 +1664,53 @@ export function createSetupTab(screen, services) {
|
|
|
1587
1664
|
screen.render();
|
|
1588
1665
|
}
|
|
1589
1666
|
|
|
1590
|
-
function showProviderListView() {
|
|
1667
|
+
function showProviderListView(targetIdx = 0) {
|
|
1591
1668
|
providerView = 'list';
|
|
1592
1669
|
infoBox.hide();
|
|
1593
1670
|
contentBox.hide();
|
|
1594
1671
|
showAllProviderRows();
|
|
1595
|
-
|
|
1596
|
-
if (
|
|
1672
|
+
const max = providerFocusableItems.length;
|
|
1673
|
+
if (max > 0) {
|
|
1674
|
+
providerFocusIndex = ((targetIdx % max) + max) % max;
|
|
1675
|
+
providerFocusableItems[providerFocusIndex].focus();
|
|
1676
|
+
}
|
|
1597
1677
|
screen.render();
|
|
1598
1678
|
}
|
|
1599
1679
|
|
|
1600
1680
|
infoBox.key(['escape', 'enter'], () => {
|
|
1601
|
-
|
|
1681
|
+
// After dismissing the install/remove info page, advance focus to the
|
|
1682
|
+
// NEXT provider row but keep the same column (Install stays on Install,
|
|
1683
|
+
// Remove stays on Remove, Configure stays on Configure). Each row has
|
|
1684
|
+
// 3 focusable slots so +3 moves one full row down with wraparound.
|
|
1685
|
+
const max = providerFocusableItems.length;
|
|
1686
|
+
const nextIdx = max > 0 ? (_preInfoFocusIndex + 3) % max : 0;
|
|
1687
|
+
showProviderListView(nextIdx);
|
|
1602
1688
|
});
|
|
1603
1689
|
|
|
1690
|
+
// Captured by handleProviderInstall/Remove right before showing info.
|
|
1691
|
+
// Defaults to 0 so the first-time flow still lands on Claude Code Install.
|
|
1692
|
+
let _preInfoFocusIndex = 0;
|
|
1693
|
+
|
|
1604
1694
|
async function refreshInstalledState() {
|
|
1605
1695
|
for (const p of PROVIDERS) {
|
|
1696
|
+
// The "default" provider is config-only — always treat as available.
|
|
1697
|
+
if (p.isDefault) {
|
|
1698
|
+
installedState[p.id] = true;
|
|
1699
|
+
continue;
|
|
1700
|
+
}
|
|
1606
1701
|
const checkFn = p.id === 'claude-code' ? checkClaudeInstalled
|
|
1607
1702
|
: p.id === 'github-copilot' ? checkCopilotInstalled
|
|
1608
1703
|
: checkCodexInstalled;
|
|
1609
1704
|
installedState[p.id] = await checkFn(targetDir);
|
|
1610
1705
|
}
|
|
1611
1706
|
for (const row of providerRows) {
|
|
1707
|
+
const provider = PROVIDERS.find(p => p.id === row.id);
|
|
1708
|
+
// The default provider has no install state to display — show its
|
|
1709
|
+
// config-only nature instead.
|
|
1710
|
+
if (provider?.isDefault) {
|
|
1711
|
+
row.statusText.setContent('{cyan-fg}[Config Only]{/cyan-fg}');
|
|
1712
|
+
continue;
|
|
1713
|
+
}
|
|
1612
1714
|
const installed = installedState[row.id];
|
|
1613
1715
|
row.statusText.setContent(
|
|
1614
1716
|
installed
|
|
@@ -471,29 +471,85 @@ export function scanInstalledVoices() {
|
|
|
471
471
|
}
|
|
472
472
|
|
|
473
473
|
/**
|
|
474
|
-
* Get favorites array from config.
|
|
474
|
+
* Get favorites (thumbs-up) array from config.
|
|
475
475
|
* @param {object} configService
|
|
476
476
|
* @returns {string[]}
|
|
477
477
|
*/
|
|
478
478
|
export function getFavorites(configService) {
|
|
479
|
-
const
|
|
479
|
+
const cfg = configService.getConfig();
|
|
480
|
+
// Prefer thumbsUp if present, fall back to legacy favorites
|
|
481
|
+
const favs = cfg.thumbsUp ?? cfg.favorites;
|
|
480
482
|
return Array.isArray(favs) ? favs : [];
|
|
481
483
|
}
|
|
482
484
|
|
|
483
485
|
/**
|
|
484
|
-
*
|
|
486
|
+
* Get thumbs-down array from config.
|
|
487
|
+
* @param {object} configService
|
|
488
|
+
* @returns {string[]}
|
|
489
|
+
*/
|
|
490
|
+
export function getThumbsDown(configService) {
|
|
491
|
+
const td = configService.getConfig().thumbsDown;
|
|
492
|
+
return Array.isArray(td) ? td : [];
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Toggle thumbs-up on a voice. Clears thumbs-down if set.
|
|
485
497
|
* @param {object} configService
|
|
486
498
|
* @param {string} voiceId
|
|
499
|
+
* @returns {'added'|'removed'}
|
|
487
500
|
*/
|
|
488
|
-
export function
|
|
501
|
+
export function toggleThumbsUp(configService, voiceId) {
|
|
489
502
|
const favs = getFavorites(configService);
|
|
490
503
|
const idx = favs.indexOf(voiceId);
|
|
504
|
+
let result;
|
|
491
505
|
if (idx >= 0) {
|
|
492
506
|
favs.splice(idx, 1);
|
|
507
|
+
result = 'removed';
|
|
493
508
|
} else {
|
|
494
509
|
favs.push(voiceId);
|
|
510
|
+
// Remove from thumbs-down
|
|
511
|
+
const td = getThumbsDown(configService);
|
|
512
|
+
const tdIdx = td.indexOf(voiceId);
|
|
513
|
+
if (tdIdx >= 0) { td.splice(tdIdx, 1); configService.set('thumbsDown', td); }
|
|
514
|
+
result = 'added';
|
|
495
515
|
}
|
|
496
|
-
configService.set('
|
|
516
|
+
configService.set('thumbsUp', favs);
|
|
517
|
+
configService.set('favorites', favs); // backward compat
|
|
518
|
+
return result;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Toggle thumbs-down on a voice. Clears thumbs-up if set.
|
|
523
|
+
* @param {object} configService
|
|
524
|
+
* @param {string} voiceId
|
|
525
|
+
* @returns {'added'|'removed'}
|
|
526
|
+
*/
|
|
527
|
+
export function toggleThumbsDown(configService, voiceId) {
|
|
528
|
+
const td = getThumbsDown(configService);
|
|
529
|
+
const idx = td.indexOf(voiceId);
|
|
530
|
+
let result;
|
|
531
|
+
if (idx >= 0) {
|
|
532
|
+
td.splice(idx, 1);
|
|
533
|
+
result = 'removed';
|
|
534
|
+
} else {
|
|
535
|
+
td.push(voiceId);
|
|
536
|
+
// Remove from thumbs-up / favorites
|
|
537
|
+
const favs = getFavorites(configService);
|
|
538
|
+
const fIdx = favs.indexOf(voiceId);
|
|
539
|
+
if (fIdx >= 0) { favs.splice(fIdx, 1); configService.set('thumbsUp', favs); configService.set('favorites', favs); }
|
|
540
|
+
result = 'added';
|
|
541
|
+
}
|
|
542
|
+
configService.set('thumbsDown', td);
|
|
543
|
+
return result;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Toggle a voice in the favorites list (legacy compat — calls toggleThumbsUp).
|
|
548
|
+
* @param {object} configService
|
|
549
|
+
* @param {string} voiceId
|
|
550
|
+
*/
|
|
551
|
+
export function toggleFavorite(configService, voiceId) {
|
|
552
|
+
toggleThumbsUp(configService, voiceId);
|
|
497
553
|
}
|
|
498
554
|
|
|
499
555
|
// ---------------------------------------------------------------------------
|
|
@@ -708,7 +764,7 @@ export function createVoicesTab(screen, services) {
|
|
|
708
764
|
|
|
709
765
|
// -------------------------------------------------------------------------
|
|
710
766
|
// Hint text shown in previewLine when the list has focus and nothing is playing
|
|
711
|
-
const HINT_TEXT = '{white-fg}[Space] preview [Enter] select
|
|
767
|
+
const HINT_TEXT = '{white-fg}[Space] preview [Enter] select [+] thumbs up [-] thumbs down{/white-fg}';
|
|
712
768
|
let _listFocused = false;
|
|
713
769
|
|
|
714
770
|
// Inline selection hint appended to the currently highlighted voice row.
|
|
@@ -811,6 +867,55 @@ export function createVoicesTab(screen, services) {
|
|
|
811
867
|
_killPlayingProcess();
|
|
812
868
|
_playingVoiceId = null;
|
|
813
869
|
|
|
870
|
+
// Check if we should route through remote provider (ssh-remote / agentvibes-receiver)
|
|
871
|
+
const projectRoot = path.resolve(__dirname, '..', '..');
|
|
872
|
+
const remoteProviders = ['ssh-remote', 'agentvibes-receiver'];
|
|
873
|
+
let activeProvider = '';
|
|
874
|
+
try {
|
|
875
|
+
const providerPaths = [
|
|
876
|
+
path.join(projectRoot, '.claude', 'tts-provider.txt'),
|
|
877
|
+
path.join(os.homedir(), '.claude', 'tts-provider.txt'),
|
|
878
|
+
];
|
|
879
|
+
for (const p of providerPaths) {
|
|
880
|
+
if (fs.existsSync(p)) { activeProvider = fs.readFileSync(p, 'utf8').trim(); break; }
|
|
881
|
+
}
|
|
882
|
+
} catch {}
|
|
883
|
+
|
|
884
|
+
if (remoteProviders.includes(activeProvider)) {
|
|
885
|
+
const isWindows = process.platform === 'win32' && !process.env.WSL_DISTRO_NAME;
|
|
886
|
+
const phrase = SAMPLE_PHRASES[Math.floor(Math.random() * SAMPLE_PHRASES.length)];
|
|
887
|
+
let proc;
|
|
888
|
+
if (isWindows) {
|
|
889
|
+
const playTts = path.join(projectRoot, '.claude', 'hooks-windows', 'play-tts.ps1');
|
|
890
|
+
proc = spawn('powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', playTts, phrase, voiceId], {
|
|
891
|
+
stdio: 'ignore', detached: false, windowsHide: true, env: _spawnEnv,
|
|
892
|
+
});
|
|
893
|
+
} else {
|
|
894
|
+
const playTts = path.join(projectRoot, '.claude', 'hooks', 'play-tts.sh');
|
|
895
|
+
proc = spawn('bash', [playTts, phrase, voiceId], {
|
|
896
|
+
stdio: 'ignore', detached: true, env: _spawnEnv,
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
_playingProcess = proc;
|
|
900
|
+
_playingVoiceId = voiceId;
|
|
901
|
+
previewLine.setContent(`{${COLORS.activeFg}-fg}♪ Playing (remote): ${voiceId}{/${COLORS.activeFg}-fg}`);
|
|
902
|
+
screen.render();
|
|
903
|
+
proc.on('exit', () => {
|
|
904
|
+
if (_playingVoiceId === voiceId) {
|
|
905
|
+
_playingVoiceId = null; _playingProcess = null;
|
|
906
|
+
previewLine.setContent(_listFocused ? HINT_TEXT : '');
|
|
907
|
+
refreshDisplay();
|
|
908
|
+
}
|
|
909
|
+
});
|
|
910
|
+
proc.on('error', () => {
|
|
911
|
+
_playingVoiceId = null; _playingProcess = null;
|
|
912
|
+
previewLine.setContent(`{red-fg}Remote preview failed{/red-fg}`);
|
|
913
|
+
screen.render();
|
|
914
|
+
setTimeout(() => { previewLine.setContent(_listFocused ? HINT_TEXT : ''); screen.render(); }, 4000);
|
|
915
|
+
});
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
|
|
814
919
|
// Resolve model path (may be multi-speaker)
|
|
815
920
|
const ms = parseMultiSpeaker(voiceId);
|
|
816
921
|
const voicePath = path.resolve(PIPER_VOICES_DIR, ms.model + '.onnx');
|
|
@@ -1435,13 +1540,14 @@ export function createVoicesTab(screen, services) {
|
|
|
1435
1540
|
return _installedSet.has(voiceId);
|
|
1436
1541
|
}
|
|
1437
1542
|
|
|
1438
|
-
function _buildListItems(voices, active, favorites) {
|
|
1543
|
+
function _buildListItems(voices, active, favorites, thumbsDown) {
|
|
1439
1544
|
return voices.map(v => {
|
|
1440
1545
|
const installed = _isInstalled(v);
|
|
1441
|
-
const
|
|
1546
|
+
const isUp = favorites.includes(v);
|
|
1547
|
+
const isDown = thumbsDown.includes(v);
|
|
1442
1548
|
const isActive = v === active;
|
|
1443
1549
|
const isPrev = v === _playingVoiceId;
|
|
1444
|
-
const star =
|
|
1550
|
+
const star = isUp ? '{green-fg}👍{/green-fg}' : (isDown ? '{red-fg}👎{/red-fg}' : ' ');
|
|
1445
1551
|
const dot = isPrev ? '♪' : (isActive ? '{green-fg}✓{/green-fg}' : ' ');
|
|
1446
1552
|
|
|
1447
1553
|
let displayName, gender, provider;
|
|
@@ -1531,8 +1637,9 @@ export function createVoicesTab(screen, services) {
|
|
|
1531
1637
|
|
|
1532
1638
|
const active = providerService.getActiveVoiceId();
|
|
1533
1639
|
const favorites = getFavorites(configService);
|
|
1640
|
+
const thumbsDown = getThumbsDown(configService);
|
|
1534
1641
|
const filtered = _getFilteredVoices();
|
|
1535
|
-
const items = _buildListItems(filtered, active, favorites);
|
|
1642
|
+
const items = _buildListItems(filtered, active, favorites, thumbsDown);
|
|
1536
1643
|
|
|
1537
1644
|
voiceList.setItems(items.length > 0 ? items : [' (no voices found — install piper first)']);
|
|
1538
1645
|
const maxIdx = Math.max(0, (items.length > 0 ? items.length : 1) - 1);
|
|
@@ -1620,12 +1727,22 @@ export function createVoicesTab(screen, services) {
|
|
|
1620
1727
|
if (typeof focusMainTabBar === 'function') { focusMainTabBar(); screen.render(); }
|
|
1621
1728
|
});
|
|
1622
1729
|
|
|
1623
|
-
// '
|
|
1624
|
-
voiceList.key(['*'], () => {
|
|
1730
|
+
// '*' or '+' in voiceList toggles thumbs-up
|
|
1731
|
+
voiceList.key(['*', '+'], () => {
|
|
1625
1732
|
const voices = _getFilteredVoices();
|
|
1626
1733
|
const selected = voices[voiceList.selected];
|
|
1627
1734
|
if (selected) {
|
|
1628
|
-
|
|
1735
|
+
toggleThumbsUp(configService, selected);
|
|
1736
|
+
refreshDisplay();
|
|
1737
|
+
}
|
|
1738
|
+
});
|
|
1739
|
+
|
|
1740
|
+
// '-' in voiceList toggles thumbs-down
|
|
1741
|
+
voiceList.key(['-'], () => {
|
|
1742
|
+
const voices = _getFilteredVoices();
|
|
1743
|
+
const selected = voices[voiceList.selected];
|
|
1744
|
+
if (selected) {
|
|
1745
|
+
toggleThumbsDown(configService, selected);
|
|
1629
1746
|
refreshDisplay();
|
|
1630
1747
|
}
|
|
1631
1748
|
});
|