agentvibes 5.1.4 → 5.2.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/.agentvibes/config.json +23 -13
- package/.claude/commands/agent-vibes/verbosity.md +98 -89
- package/.claude/config/audio-effects.cfg +4 -1
- package/.claude/hooks/audio-cache-utils.sh +246 -246
- package/.claude/hooks/background-music-manager.sh +404 -404
- package/.claude/hooks/bmad-speak-enhanced.sh +165 -165
- package/.claude/hooks/bmad-speak.sh +290 -290
- package/.claude/hooks/bmad-tts-injector.sh +568 -568
- package/.claude/hooks/bmad-voice-manager.sh +928 -928
- package/.claude/hooks/clawdbot-receiver-SECURE.sh +129 -129
- package/.claude/hooks/clawdbot-receiver.sh +107 -107
- package/.claude/hooks/clean-audio-cache.sh +22 -22
- package/.claude/hooks/cleanup-cache.sh +106 -106
- package/.claude/hooks/configure-rdp-mode.sh +137 -137
- package/.claude/hooks/download-extra-voices.sh +244 -244
- package/.claude/hooks/effects-manager.sh +268 -268
- package/.claude/hooks/github-star-reminder.sh +154 -154
- package/.claude/hooks/language-manager.sh +362 -362
- package/.claude/hooks/learn-manager.sh +492 -492
- package/.claude/hooks/macos-voice-manager.sh +205 -205
- package/.claude/hooks/migrate-background-music.sh +125 -125
- package/.claude/hooks/migrate-to-agentvibes.sh +161 -161
- package/.claude/hooks/optimize-background-music.sh +87 -87
- package/.claude/hooks/path-resolver.sh +60 -60
- package/.claude/hooks/personality-manager.sh +448 -448
- package/.claude/hooks/piper-download-voices.sh +233 -225
- package/.claude/hooks/piper-installer.sh +292 -292
- package/.claude/hooks/piper-multispeaker-registry.sh +171 -171
- 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 +105 -105
- package/.claude/hooks/play-tts-piper.sh +16 -5
- package/.claude/hooks/play-tts-ssh-remote.sh +168 -167
- package/.claude/hooks/play-tts-termux-ssh.sh +169 -169
- package/.claude/hooks/play-tts.sh +35 -14
- package/.claude/hooks/prepare-release.sh +54 -54
- package/.claude/hooks/provider-commands.sh +617 -617
- package/.claude/hooks/provider-manager.sh +399 -399
- package/.claude/hooks/replay-target-audio.sh +95 -95
- package/.claude/hooks/sentiment-manager.sh +201 -201
- package/.claude/hooks/session-start-tts.sh +4 -1
- package/.claude/hooks/speed-manager.sh +291 -291
- package/.claude/hooks/stop-tts.sh +84 -84
- package/.claude/hooks/termux-installer.sh +261 -261
- package/.claude/hooks/translate-manager.sh +341 -341
- package/.claude/hooks/tts-queue-worker.sh +145 -145
- package/.claude/hooks/tts-queue.sh +165 -165
- package/.claude/hooks/verbosity-manager.sh +185 -178
- package/.claude/hooks/voice-manager.sh +552 -548
- 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 +9 -3
- package/.claude/hooks-windows/session-start-tts.ps1 +2 -1
- package/.claude/hooks-windows/verbosity-manager.ps1 +126 -119
- package/README.md +19 -2
- package/RELEASE_NOTES.md +74 -0
- package/bin/agentvibes-voice-browser.js +1939 -1840
- package/bin/mcp-server.sh +206 -206
- package/mcp-server/server.py +87 -15
- 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 +112 -31
- package/src/console/tabs/voices-tab.js +130 -13
- package/src/i18n/en.js +202 -202
- package/src/installer.js +79 -213
- package/src/services/llm-provider-service.js +126 -75
- 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';
|
|
@@ -605,12 +605,19 @@ export function createSetupTab(screen, services) {
|
|
|
605
605
|
}
|
|
606
606
|
});
|
|
607
607
|
btn.key(['down'], () => {
|
|
608
|
+
// Column-preserving down nav. If pressing down from Install/Remove
|
|
609
|
+
// would land on the Default row (which has no Install/Remove — all
|
|
610
|
+
// three slots are configBtn duplicates), don't move. Configure
|
|
611
|
+
// column navigates normally into Default row's Configure.
|
|
612
|
+
const col = providerFocusIndex % 3;
|
|
608
613
|
const nextIdx = providerFocusIndex + 3;
|
|
609
|
-
if (nextIdx
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
+
if (nextIdx >= providerFocusableItems.length) return;
|
|
615
|
+
const nextRowIdx = Math.floor(nextIdx / 3);
|
|
616
|
+
const nextRow = PROVIDERS[nextRowIdx];
|
|
617
|
+
if (col < 2 && nextRow && nextRow.isDefault) return; // skip Default from Install/Remove
|
|
618
|
+
providerFocusIndex = nextIdx;
|
|
619
|
+
providerFocusableItems[providerFocusIndex].focus();
|
|
620
|
+
screen.render();
|
|
614
621
|
});
|
|
615
622
|
}
|
|
616
623
|
|
|
@@ -889,7 +896,7 @@ export function createSetupTab(screen, services) {
|
|
|
889
896
|
const hooksSubdir = process.platform === 'win32' ? 'hooks-windows' : 'hooks';
|
|
890
897
|
const isWin = process.platform === 'win32' && !process.env.WSL_DISTRO_NAME;
|
|
891
898
|
// Don't include pretext — play-tts already prepends it from the config
|
|
892
|
-
const sampleText = '
|
|
899
|
+
const sampleText = 'This is how your audio settings sound right now.';
|
|
893
900
|
|
|
894
901
|
let cmd, args;
|
|
895
902
|
if (isWin) {
|
|
@@ -1226,18 +1233,20 @@ export function createSetupTab(screen, services) {
|
|
|
1226
1233
|
|
|
1227
1234
|
blessed.text({
|
|
1228
1235
|
parent: vpModal, bottom: 2, left: 2, right: 2, tags: true,
|
|
1229
|
-
content: '{white-fg}[↑↓] Nav [PgUp/PgDn] Page [
|
|
1236
|
+
content: '{white-fg}[↑↓] Nav [PgUp/PgDn] Page [a-z] Jump [Enter] Select [Space] Preview [+] 👍 [-] 👎 [Esc] Cancel{/white-fg}',
|
|
1230
1237
|
style: { bg: COLORS.contentBg },
|
|
1231
1238
|
});
|
|
1232
1239
|
|
|
1233
1240
|
function _buildVoiceItems(voices) {
|
|
1234
1241
|
const favs = getFavorites(configService);
|
|
1242
|
+
const td = getThumbsDown(configService);
|
|
1235
1243
|
return voices.map(v => {
|
|
1236
1244
|
const isActive = v === draft.voice;
|
|
1237
1245
|
const isPrev = v === _previewVoiceId;
|
|
1238
|
-
const
|
|
1246
|
+
const isUp = favs.includes(v);
|
|
1247
|
+
const isDown = td.includes(v);
|
|
1239
1248
|
const dot = isPrev ? '♪' : (isActive ? '●' : ' ');
|
|
1240
|
-
const star =
|
|
1249
|
+
const star = isUp ? '{green-fg}👍{/green-fg}' : (isDown ? '{red-fg}👎{/red-fg}' : ' ');
|
|
1241
1250
|
const meta = getVoiceMeta(v);
|
|
1242
1251
|
const name = meta.displayName.length > COL_N
|
|
1243
1252
|
? meta.displayName.slice(0, COL_N - 1) + '…'
|
|
@@ -1266,13 +1275,52 @@ export function createSetupTab(screen, services) {
|
|
|
1266
1275
|
if (_previewVoiceId === voiceId) { _killVP(); vpPreviewLine.setContent(''); _refreshVP(); return; }
|
|
1267
1276
|
_killVP();
|
|
1268
1277
|
|
|
1278
|
+
const phrase = SAMPLE_PHRASES[Math.floor(Math.random() * SAMPLE_PHRASES.length)];
|
|
1279
|
+
|
|
1280
|
+
// Route through remote provider if active
|
|
1281
|
+
const _remoteProviders = ['ssh-remote', 'agentvibes-receiver'];
|
|
1282
|
+
let _activeProvider = '';
|
|
1283
|
+
try {
|
|
1284
|
+
const _projectRoot = path.resolve(__dirname, '..', '..');
|
|
1285
|
+
const _provPaths = [
|
|
1286
|
+
path.join(_projectRoot, '.claude', 'tts-provider.txt'),
|
|
1287
|
+
path.join(os.homedir(), '.claude', 'tts-provider.txt'),
|
|
1288
|
+
];
|
|
1289
|
+
for (const p of _provPaths) {
|
|
1290
|
+
if (fs.existsSync(p)) { _activeProvider = fs.readFileSync(p, 'utf8').trim(); break; }
|
|
1291
|
+
}
|
|
1292
|
+
} catch {}
|
|
1293
|
+
|
|
1294
|
+
if (_remoteProviders.includes(_activeProvider)) {
|
|
1295
|
+
const _projectRoot = path.resolve(__dirname, '..', '..');
|
|
1296
|
+
let rProc;
|
|
1297
|
+
if (_isWin) {
|
|
1298
|
+
const _playTts = path.join(_projectRoot, '.claude', 'hooks-windows', 'play-tts.ps1');
|
|
1299
|
+
rProc = spawn('powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', _playTts, phrase, voiceId], {
|
|
1300
|
+
stdio: 'ignore', detached: false, windowsHide: true, env: _spawnEnv,
|
|
1301
|
+
});
|
|
1302
|
+
} else {
|
|
1303
|
+
const _playTts = path.join(_projectRoot, '.claude', 'hooks', 'play-tts.sh');
|
|
1304
|
+
rProc = spawn('bash', [_playTts, phrase, voiceId], {
|
|
1305
|
+
stdio: 'ignore', detached: true, env: _spawnEnv,
|
|
1306
|
+
});
|
|
1307
|
+
}
|
|
1308
|
+
_previewProc = rProc;
|
|
1309
|
+
_previewVoiceId = voiceId;
|
|
1310
|
+
if (!_vpClosed) { vpPreviewLine.setContent(`{cyan-fg}♪ Playing (remote): ${voiceId}{/cyan-fg}`); screen.render(); }
|
|
1311
|
+
rProc.on('exit', () => {
|
|
1312
|
+
if (_previewVoiceId === voiceId) { _previewVoiceId = null; _previewProc = null; if (!_vpClosed) { vpPreviewLine.setContent(''); _refreshVP(); } }
|
|
1313
|
+
});
|
|
1314
|
+
rProc.on('error', () => { _previewProc = null; _previewVoiceId = null; });
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1269
1318
|
const _ms = parseMultiSpeaker(voiceId);
|
|
1270
1319
|
const voicePath = path.resolve(PIPER_VOICES_DIR, _ms.model + '.onnx');
|
|
1271
1320
|
const safeBase = path.resolve(PIPER_VOICES_DIR);
|
|
1272
1321
|
if (!voicePath.startsWith(safeBase + path.sep) && voicePath !== safeBase) return;
|
|
1273
1322
|
|
|
1274
1323
|
const tempWav = _secureTempWav('vp');
|
|
1275
|
-
const phrase = SAMPLE_PHRASES[Math.floor(Math.random() * SAMPLE_PHRASES.length)];
|
|
1276
1324
|
|
|
1277
1325
|
let _piperBin = 'piper';
|
|
1278
1326
|
if (_isWin) {
|
|
@@ -1331,9 +1379,13 @@ export function createSetupTab(screen, services) {
|
|
|
1331
1379
|
const sel = _allVoices[vpList.selected];
|
|
1332
1380
|
if (sel) _previewVoice(sel);
|
|
1333
1381
|
});
|
|
1334
|
-
vpList.key(['*'], () => {
|
|
1382
|
+
vpList.key(['*', '+'], () => {
|
|
1383
|
+
const sel = _allVoices[vpList.selected];
|
|
1384
|
+
if (sel) { toggleThumbsUp(configService, sel); _refreshVP(); }
|
|
1385
|
+
});
|
|
1386
|
+
vpList.key(['-'], () => {
|
|
1335
1387
|
const sel = _allVoices[vpList.selected];
|
|
1336
|
-
if (sel) {
|
|
1388
|
+
if (sel) { toggleThumbsDown(configService, sel); _refreshVP(); }
|
|
1337
1389
|
});
|
|
1338
1390
|
vpList.key(['escape', 'q'], _closeVP);
|
|
1339
1391
|
|
|
@@ -1477,7 +1529,6 @@ export function createSetupTab(screen, services) {
|
|
|
1477
1529
|
hideAllProviderRows();
|
|
1478
1530
|
contentBox.hide();
|
|
1479
1531
|
|
|
1480
|
-
const mcpPath = path.join(targetDir, '.mcp.json');
|
|
1481
1532
|
const hooksDir = path.join(targetDir, '.claude', process.platform === 'win32' ? 'hooks-windows' : 'hooks');
|
|
1482
1533
|
const installed = installedState['claude-code'];
|
|
1483
1534
|
const verb = wasInstalled ? 'reinstalled' : 'installed';
|
|
@@ -1487,9 +1538,14 @@ export function createSetupTab(screen, services) {
|
|
|
1487
1538
|
lines.push('');
|
|
1488
1539
|
|
|
1489
1540
|
if (result) {
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1541
|
+
if (result.success) {
|
|
1542
|
+
lines.push(`{green-fg}AgentVibes for Claude Code ${verb}!{/green-fg}`);
|
|
1543
|
+
if (result.mcpError) {
|
|
1544
|
+
lines.push(`{yellow-fg}Warning:{/yellow-fg} ${result.mcpError}`);
|
|
1545
|
+
}
|
|
1546
|
+
} else {
|
|
1547
|
+
lines.push(`{red-fg}Installation failed:{/red-fg} ${result.error || 'Unknown error'}`);
|
|
1548
|
+
}
|
|
1493
1549
|
} else {
|
|
1494
1550
|
lines.push(installed
|
|
1495
1551
|
? '{green-fg}Installed{/green-fg}'
|
|
@@ -1498,10 +1554,7 @@ export function createSetupTab(screen, services) {
|
|
|
1498
1554
|
|
|
1499
1555
|
lines.push('');
|
|
1500
1556
|
lines.push(`{bold}{cyan-fg}What ${result ? `got ${verb}` : 'gets installed'}:{/cyan-fg}{/bold}`);
|
|
1501
|
-
lines.push('');
|
|
1502
|
-
lines.push(' {yellow-fg}1.{/yellow-fg} {bold}.mcp.json{/bold} (project root)');
|
|
1503
|
-
lines.push(` Location: ${mcpPath}`);
|
|
1504
|
-
lines.push(' Registers the AgentVibes MCP server for Claude Code.');
|
|
1557
|
+
lines.push(' {yellow-fg}1.{/yellow-fg} {bold}.mcp.json{/bold} (MCP server — natural language voice control)');
|
|
1505
1558
|
lines.push('');
|
|
1506
1559
|
lines.push(' {yellow-fg}2.{/yellow-fg} {bold}.claude/hooks/{/bold} (session-start + pre-tool hooks)');
|
|
1507
1560
|
lines.push(` Location: ${hooksDir}`);
|
|
@@ -1530,9 +1583,14 @@ export function createSetupTab(screen, services) {
|
|
|
1530
1583
|
const lines = [];
|
|
1531
1584
|
lines.push('{bold}{cyan-fg}GitHub Copilot -- AgentVibes Integration{/cyan-fg}{/bold}');
|
|
1532
1585
|
lines.push('');
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1586
|
+
if (result.success) {
|
|
1587
|
+
lines.push(`{green-fg}AgentVibes for Copilot ${verb}!{/green-fg}`);
|
|
1588
|
+
if (result.mcpError) {
|
|
1589
|
+
lines.push(`{yellow-fg}MCP config failed:{/yellow-fg} ${result.mcpError}`);
|
|
1590
|
+
}
|
|
1591
|
+
} else {
|
|
1592
|
+
lines.push(`{red-fg}Installation failed:{/red-fg} ${result.error || 'Unknown error'}`);
|
|
1593
|
+
}
|
|
1536
1594
|
lines.push('');
|
|
1537
1595
|
lines.push(`{bold}{cyan-fg}What got ${verb}:{/cyan-fg}{/bold}`);
|
|
1538
1596
|
lines.push('');
|
|
@@ -1559,9 +1617,14 @@ export function createSetupTab(screen, services) {
|
|
|
1559
1617
|
const lines = [];
|
|
1560
1618
|
lines.push('{bold}{cyan-fg}OpenAI Codex -- AgentVibes Integration{/cyan-fg}{/bold}');
|
|
1561
1619
|
lines.push('');
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1620
|
+
if (result.success) {
|
|
1621
|
+
lines.push(`{green-fg}AgentVibes for Codex ${verb}!{/green-fg}`);
|
|
1622
|
+
if (result.mcpError) {
|
|
1623
|
+
lines.push(`{yellow-fg}MCP config failed:{/yellow-fg} ${result.mcpError}`);
|
|
1624
|
+
}
|
|
1625
|
+
} else {
|
|
1626
|
+
lines.push(`{red-fg}Installation failed:{/red-fg} ${result.error || 'Unknown error'}`);
|
|
1627
|
+
}
|
|
1565
1628
|
lines.push('');
|
|
1566
1629
|
lines.push(`{bold}{cyan-fg}What got ${verb}:{/cyan-fg}{/bold}`);
|
|
1567
1630
|
lines.push('');
|
|
@@ -1634,11 +1697,29 @@ export function createSetupTab(screen, services) {
|
|
|
1634
1697
|
|
|
1635
1698
|
infoBox.key(['escape', 'enter'], () => {
|
|
1636
1699
|
// After dismissing the install/remove info page, advance focus to the
|
|
1637
|
-
// NEXT provider row but keep the same column (Install
|
|
1638
|
-
//
|
|
1639
|
-
//
|
|
1700
|
+
// NEXT provider row but keep the same column (Install/Remove/Configure).
|
|
1701
|
+
// Each row has 3 focusable slots, so +3 moves one full row down.
|
|
1702
|
+
//
|
|
1703
|
+
// Special case: when leaving the LAST installable provider (Codex) from
|
|
1704
|
+
// Install or Remove column, skip the Default row (it has no Install or
|
|
1705
|
+
// Remove) and wrap to the FIRST Configure button (Claude Code Configure).
|
|
1706
|
+
// This lets the user cleanly walk all three installs, then all three
|
|
1707
|
+
// Configures, ending on Default Configure.
|
|
1640
1708
|
const max = providerFocusableItems.length;
|
|
1641
|
-
|
|
1709
|
+
if (max === 0) { showProviderListView(0); return; }
|
|
1710
|
+
const col = _preInfoFocusIndex % 3; // 0=Install, 1=Remove, 2=Configure
|
|
1711
|
+
const row = Math.floor(_preInfoFocusIndex / 3);
|
|
1712
|
+
const nextRow = PROVIDERS[row + 1];
|
|
1713
|
+
const nextRowIsDefault = nextRow && nextRow.isDefault;
|
|
1714
|
+
let nextIdx;
|
|
1715
|
+
if (col < 2 && nextRowIsDefault) {
|
|
1716
|
+
// Last Install/Remove → jump to the FIRST non-default provider's
|
|
1717
|
+
// Configure column (dynamic: don't hardcode PROVIDERS[0]).
|
|
1718
|
+
const firstInstallableIdx = PROVIDERS.findIndex(p => !p.isDefault);
|
|
1719
|
+
nextIdx = firstInstallableIdx >= 0 ? firstInstallableIdx * 3 + 2 : (_preInfoFocusIndex + 3) % max;
|
|
1720
|
+
} else {
|
|
1721
|
+
nextIdx = (_preInfoFocusIndex + 3) % max;
|
|
1722
|
+
}
|
|
1642
1723
|
showProviderListView(nextIdx);
|
|
1643
1724
|
});
|
|
1644
1725
|
|
|
@@ -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
|
});
|