agentvibes 5.0.0 → 5.1.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/audio/tracks/Drifting Down the Hall.mp3 +0 -0
- package/.claude/audio/tracks/Late Night Hip Hop Groove.mp3 +0 -0
- package/.claude/audio/tracks/Midnight Charleston Stomp.mp3 +0 -0
- package/.claude/config/audio-effects.cfg +1 -1
- package/.claude/hooks/play-tts.sh +10 -3
- package/.claude/hooks-windows/play-tts.ps1 +8 -1
- package/README.md +22 -2
- package/RELEASE_NOTES.md +76 -0
- package/package.json +1 -1
- package/src/console/tabs/agents-tab.js +65 -62
- package/src/console/tabs/music-tab.js +49 -19
- package/src/console/tabs/settings-tab.js +39 -37
- package/src/console/tabs/setup-tab.js +346 -86
- package/src/console/tabs/voices-tab.js +152 -29
- package/src/console/widgets/format-utils.js +92 -89
- package/src/console/widgets/track-picker.js +325 -322
- package/src/installer.js +8 -8
- package/src/services/llm-provider-service.js +79 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -51,4 +51,4 @@ _party_mode|compand 0.3,1 6:-70,-60,-20|agent_vibes_dark_chill_step_loop.mp3|0.4
|
|
|
51
51
|
# Default (no agent specified) - clean with Bachata background|||
|
|
52
52
|
default||agent_vibes_chillwave_v2_loop.mp3|0.15
|
|
53
53
|
analyst|reverb 70 50 100|agentvibes_soft_flamenco_loop.mp3|0.30
|
|
54
|
-
llm:claude-code|light|agent_vibes_chillwave_v2_loop.mp3|0.15|en_US-lessac-high|
|
|
54
|
+
llm:claude-code|light|agent_vibes_chillwave_v2_loop.mp3|0.15|en_US-lessac-high|Claude Code here|piper
|
|
@@ -131,20 +131,23 @@ TEXT="${TEXT//\\./.}" # Remove \. (keep the period)
|
|
|
131
131
|
_LLM_VOICE=""
|
|
132
132
|
_LLM_PRETEXT=""
|
|
133
133
|
_LLM_REVERB=""
|
|
134
|
+
_LLM_ENGINE=""
|
|
134
135
|
if [[ -n "$LLM_PROVIDER" ]]; then
|
|
135
136
|
_llm_key="llm:${LLM_PROVIDER}"
|
|
136
137
|
for _cfg in \
|
|
137
138
|
"$PROJECT_ROOT/.claude/config/audio-effects.cfg" \
|
|
138
139
|
"$HOME/.claude/config/audio-effects.cfg"; do
|
|
139
140
|
if [[ -z "$_LLM_VOICE" && -z "$_LLM_PRETEXT" && -f "$_cfg" ]]; then
|
|
140
|
-
while IFS='|' read -r _key _reverb _bgfile _bgvol _voice _pretext _rest; do
|
|
141
|
+
while IFS='|' read -r _key _reverb _bgfile _bgvol _voice _pretext _engine _rest; do
|
|
141
142
|
if [[ "$_key" == "$_llm_key" ]]; then
|
|
142
143
|
_reverb="${_reverb## }"; _reverb="${_reverb%% }"
|
|
143
144
|
_voice="${_voice## }"; _voice="${_voice%% }"
|
|
144
145
|
_pretext="${_pretext## }"; _pretext="${_pretext%% }"
|
|
146
|
+
_engine="${_engine## }"; _engine="${_engine%% }"
|
|
145
147
|
[[ -n "$_reverb" ]] && _LLM_REVERB="$_reverb"
|
|
146
148
|
[[ -n "$_voice" ]] && _LLM_VOICE="$_voice"
|
|
147
149
|
[[ -n "$_pretext" ]] && _LLM_PRETEXT="$_pretext"
|
|
150
|
+
[[ -n "$_engine" ]] && _LLM_ENGINE="$_engine"
|
|
148
151
|
break
|
|
149
152
|
fi
|
|
150
153
|
done < "$_cfg"
|
|
@@ -187,8 +190,12 @@ fi
|
|
|
187
190
|
# Source provider manager to get active provider
|
|
188
191
|
source "$SCRIPT_DIR/provider-manager.sh"
|
|
189
192
|
|
|
190
|
-
# Get active provider
|
|
191
|
-
|
|
193
|
+
# Get active provider (LLM-specific engine overrides global)
|
|
194
|
+
if [[ -n "$_LLM_ENGINE" ]]; then
|
|
195
|
+
ACTIVE_PROVIDER="$_LLM_ENGINE"
|
|
196
|
+
else
|
|
197
|
+
ACTIVE_PROVIDER=$(get_active_provider)
|
|
198
|
+
fi
|
|
192
199
|
|
|
193
200
|
# Show GitHub star reminder (once per day)
|
|
194
201
|
"$SCRIPT_DIR/github-star-reminder.sh" 2>/dev/null || true
|
|
@@ -51,6 +51,7 @@ if (Test-Path $MuteFile) {
|
|
|
51
51
|
$LlmVoice = ""
|
|
52
52
|
$LlmPretext = ""
|
|
53
53
|
$LlmReverb = ""
|
|
54
|
+
$LlmEngine = ""
|
|
54
55
|
$ProjectRoot = Split-Path -Parent $ClaudeDir
|
|
55
56
|
$ConfigDir = "$ClaudeDir\config"
|
|
56
57
|
|
|
@@ -76,6 +77,9 @@ if ($llm) {
|
|
|
76
77
|
if ($parts.Length -ge 6 -and $parts[5].Trim()) {
|
|
77
78
|
$LlmPretext = $parts[5].Trim()
|
|
78
79
|
}
|
|
80
|
+
if ($parts.Length -ge 7 -and $parts[6].Trim()) {
|
|
81
|
+
$LlmEngine = $parts[6].Trim()
|
|
82
|
+
}
|
|
79
83
|
break
|
|
80
84
|
}
|
|
81
85
|
}
|
|
@@ -119,8 +123,11 @@ if ($Pretext) {
|
|
|
119
123
|
}
|
|
120
124
|
|
|
121
125
|
# Determine active provider
|
|
126
|
+
# LLM-specific engine overrides global provider
|
|
122
127
|
$ActiveProvider = "sapi"
|
|
123
|
-
if (
|
|
128
|
+
if ($LlmEngine) {
|
|
129
|
+
$ActiveProvider = $LlmEngine
|
|
130
|
+
} elseif (Test-Path $ProviderFile) {
|
|
124
131
|
$ActiveProvider = (Get-Content $ProviderFile -Raw).Trim()
|
|
125
132
|
}
|
|
126
133
|
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
[](https://github.com/paulpreibisch/AgentVibes/actions/workflows/publish.yml)
|
|
12
12
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
13
13
|
|
|
14
|
-
**Author**: Paul Preibisch ([@997Fire](https://x.com/997Fire)) | **Version**: v5.
|
|
14
|
+
**Author**: Paul Preibisch ([@997Fire](https://x.com/997Fire)) | **Version**: v5.1.1
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
@@ -43,7 +43,27 @@ Whether you're using Claude Code, GitHub Copilot, OpenAI Codex, Claude Desktop,
|
|
|
43
43
|
|
|
44
44
|
---
|
|
45
45
|
|
|
46
|
-
##
|
|
46
|
+
## 🩹 v5.1.1 — Windows TTS Hook Hotfix
|
|
47
|
+
|
|
48
|
+
- **`play-tts.ps1 -llm` parameter restored** — npm-published v5.1.0 shipped a regressed copy without `-llm` support, breaking the Setup tab Preview button and the agentvibes MCP `text_to_speech` tool on Windows. Fixed in v5.1.1. If you hit the error, clear your npx cache: `npm cache clean --force` then reinstall.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## 🎙️ NEW IN v5.1.0 — Voice Picker Overhaul + Auto-Save Agent Modal
|
|
53
|
+
|
|
54
|
+
- **Auto-save in agent modal** — Voice/personality/music/reverb/pretext changes save automatically as you edit them. Brief "✓ Saved!" toast confirms each change.
|
|
55
|
+
- **Unique LibriTTS names** — 904 speakers get deterministic surnames: **Anna Bell**, **Anna Carter**, …, **Anna Quinn**. No more "Anna-2", "Anna-3" duplicates.
|
|
56
|
+
- **Pink ♀ / blue ♂ gender symbols** — Colored gender indicators in the main Voices tab and all voice picker modals.
|
|
57
|
+
- **First-letter quick jump** — Press `a`–`z` in any voice picker to jump to that letter. `q`, `j`, `k`, `g`, `h`, `l` reserved for nav/cancel.
|
|
58
|
+
- **PgUp / PgDn / Home / End** in voice pickers
|
|
59
|
+
- **3 new background music tracks** — Late Night Hip Hop Groove, Drifting Down the Hall, Midnight Charleston Stomp
|
|
60
|
+
- **Search bar removed from voice pickers** — replaced by first-letter jump (faster, no focus issues)
|
|
61
|
+
- **Voices tab corruption fix** — uninstalled rows no longer lose their Provider column when navigated onto
|
|
62
|
+
- **Music + Voices tab blink artifacts gone**
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 🚀 v5.0.0 — Multi-Provider Support: Claude Code + Copilot + Codex
|
|
47
67
|
|
|
48
68
|
- **GitHub Copilot + OpenAI Codex in VS Code** — AgentVibes now supports all three major AI coding assistants. Install and configure each from the TUI.
|
|
49
69
|
- **One Setup tab** — 4-step wizard (Language → Deps → TTS Engine → Providers) replaces old installer + LLM tabs. Returning users skip to Providers.
|
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,5 +1,81 @@
|
|
|
1
1
|
# AgentVibes Release Notes
|
|
2
2
|
|
|
3
|
+
## 🩹 v5.1.1 — Windows TTS Hook Hotfix
|
|
4
|
+
|
|
5
|
+
**Release Date:** April 2026
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
- **Windows `play-tts.ps1` `-llm` parameter restored** — A regression caused the npm-published v5.1.0 package to ship a `play-tts.ps1` that lacked the `-llm` parameter and the per-LLM config lookup. Per-LLM TTS routing on Windows failed with `A parameter cannot be found that matches parameter name 'llm'`. This affected:
|
|
10
|
+
- **Setup tab Preview button** for any provider configured with per-LLM voice/effects
|
|
11
|
+
- **agentvibes MCP `text_to_speech` tool** when called from Codex / Copilot / Claude Code
|
|
12
|
+
- Any code path that invokes `play-tts.ps1 ... -llm <provider>`
|
|
13
|
+
|
|
14
|
+
The git tag `v5.1.0` had the correct file all along — only the npm tarball was affected, because `npm publish` packs the working tree (which contained an uncommitted local regression) instead of the git tag.
|
|
15
|
+
|
|
16
|
+
### How to Update
|
|
17
|
+
|
|
18
|
+
If you installed v5.1.0 from npm and hit the `-llm` error, clear your npx cache and reinstall:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
npm cache clean --force
|
|
22
|
+
npx --yes agentvibes@5.1.1
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Note for Maintainers
|
|
26
|
+
|
|
27
|
+
To prevent this kind of working-tree-vs-tag drift in future releases, the release workflow should run `npm publish` from a freshly checked-out clone of the tag, not from the development working directory.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 🎙️ v5.1.0 — Voice Picker Overhaul + Auto-Save Agent Modal
|
|
32
|
+
|
|
33
|
+
**Release Date:** April 2026
|
|
34
|
+
|
|
35
|
+
### New Features
|
|
36
|
+
|
|
37
|
+
- **Auto-save in agent edit modal** — Per-agent voice/personality/music/reverb/pretext changes now save automatically as you edit them. The explicit Save button is gone; a brief "✓ Saved!" toast confirms each change. Cancel and Reset to Defaults still behave as before.
|
|
38
|
+
|
|
39
|
+
- **Unique LibriTTS speaker names** — The 904 LibriTTS speakers no longer show as "Anna", "Anna-2", "Anna-3", … "Anna-16". Each gets a deterministic surname from a 16-name pool: **Anna Bell**, **Anna Carter**, **Anna Davis**, …, **Anna Quinn**. Underlying voice IDs are unchanged so existing user configs still resolve.
|
|
40
|
+
|
|
41
|
+
- **Pink/blue gender symbols** — Female voices show **♀** in pink (magenta), male voices show **♂** in light blue (bright-cyan), unknown shows `—`. Header `Gender` column replaced with colored `♀/♂` (10 → 4 chars wide), freeing room for longer names. Applied to the main Voices tab AND all 3 voice picker modals (Setup, Agents, Settings).
|
|
42
|
+
|
|
43
|
+
- **First-letter quick jump in voice pickers** — Press any letter `a`–`z` to jump to the first voice starting with that letter. Reserved keys (`q`, `j`, `k`, `g`, `h`, `l`) are blocked so they keep their cancel / vi-nav meanings.
|
|
44
|
+
|
|
45
|
+
- **Page navigation in voice pickers** — `PgUp`, `PgDn`, `Home`, `End` now work in all voice picker modals.
|
|
46
|
+
|
|
47
|
+
- **3 new background music tracks** — `Late Night Hip Hop Groove`, `Drifting Down the Hall` (90s vibes), and `Midnight Charleston Stomp` (swing). Track count goes from 15 → 18.
|
|
48
|
+
|
|
49
|
+
### Improvements
|
|
50
|
+
|
|
51
|
+
- **Voice picker search bar removed** — Replaced with first-letter quick jump. The old search textbox had focus issues that swallowed nav keys. The jump is faster for typical "find voice X" use.
|
|
52
|
+
|
|
53
|
+
- **Track list sorting fixed** — Tracks with emoji prefixes (e.g. `🎤 Late Night Hip Hop Groove`) now sort by the alphabetic part of the name, not the emoji codepoint. Order is consistent across Node/ICU versions.
|
|
54
|
+
|
|
55
|
+
- **Favorite hotkey is now `*` only** — Removed the duplicate `f` binding for marking favorites in voice pickers and the main Voices tab. `f` is now free for first-letter jump (e.g. jumping to Frank or Felix). The `*` marker remains the canonical way to toggle favorites.
|
|
56
|
+
|
|
57
|
+
### Bug Fixes
|
|
58
|
+
|
|
59
|
+
- **Voices tab uninstalled rows no longer corrupt** — Selecting an uninstalled voice was visually deleting its Provider column due to a regex strip that over-matched the row's `bright-black-fg` wrapper. Replaced with a precise hint anchor that only strips the exact hint text.
|
|
60
|
+
|
|
61
|
+
- **Music tab + Voices tab blink artifacts gone** — `█` cursors no longer leave stray blocks behind when scrolling rapidly through the list. Both tabs now use a precise blink-strip helper instead of the fragile position-based slicer.
|
|
62
|
+
|
|
63
|
+
- **Setup tab no longer silently fails** — `_renderScreen3` was wrapping the entire `setupCompleted` write block in a single empty `try/catch {}`. Corrupt local config files are now backed up to `config.json.bak` and rewritten fresh, with errors logged to stderr — no more "stuck repeating setup" with no explanation.
|
|
64
|
+
|
|
65
|
+
- **Voice picker `q` cancel now works** — The new first-letter jump was swallowing `q` (and other vi nav keys). Reserved key blocklist added.
|
|
66
|
+
|
|
67
|
+
- **Track picker case-insensitive sort** — New tracks with Title Case names (`Late Night Hip Hop Groove.mp3`) no longer jump to the top of the list above the lowercase `agent_vibes_*` tracks.
|
|
68
|
+
|
|
69
|
+
### User Impact
|
|
70
|
+
|
|
71
|
+
- Editing an agent's voice or settings is now faster — no need to remember to click Save
|
|
72
|
+
- The voice picker is dramatically less cluttered with 904 LibriTTS speakers all having unique, friendly names
|
|
73
|
+
- Gender at a glance via colored symbols
|
|
74
|
+
- Three new music tracks for variety
|
|
75
|
+
- Blink/scroll artifacts gone in both Voices and Music tabs
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
3
79
|
## 🚀 v5.0.0 — Multi-Provider Support: Claude Code + Copilot + Codex
|
|
4
80
|
|
|
5
81
|
**Release Date:** April 2026
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "agentvibes",
|
|
4
|
-
"version": "5.
|
|
4
|
+
"version": "5.1.1",
|
|
5
5
|
"description": "Now your AI Agents can finally talk back! Professional TTS voice for Claude Code, Claude Desktop (via MCP), and Clawdbot with multi-provider support.",
|
|
6
6
|
"homepage": "https://agentvibes.org",
|
|
7
7
|
"keywords": [
|
|
@@ -16,7 +16,7 @@ import { openTrackPicker, openVolumeInput } from '../widgets/track-picker.js';
|
|
|
16
16
|
import { formatReverbState, formatTrackName, formatVoiceName } from '../widgets/format-utils.js';
|
|
17
17
|
import {
|
|
18
18
|
PIPER_VOICES_DIR, SAMPLE_PHRASES,
|
|
19
|
-
parseMultiSpeaker, scanInstalledVoices, getVoiceMeta,
|
|
19
|
+
parseMultiSpeaker, scanInstalledVoices, getVoiceMeta, genderIconTag,
|
|
20
20
|
} from './voices-tab.js';
|
|
21
21
|
import { buildAudioEnv, detectWavPlayer } from '../audio-env.js';
|
|
22
22
|
import { destroyList } from '../widgets/destroy-list.js';
|
|
@@ -729,8 +729,10 @@ ${_tl('bmadDesc')}
|
|
|
729
729
|
btnBlink.startSpinner(previewBtn);
|
|
730
730
|
});
|
|
731
731
|
|
|
732
|
-
|
|
733
|
-
|
|
732
|
+
// Auto-save the current draft (called after every field change).
|
|
733
|
+
// Only persists fields that differ from the global defaults — same logic
|
|
734
|
+
// the explicit Save button used to use, just triggered automatically.
|
|
735
|
+
function _autoSaveAgent() {
|
|
734
736
|
const toSave = {};
|
|
735
737
|
if (draft.voice && draft.voice !== globalCfg.voice) toSave.voice = draft.voice;
|
|
736
738
|
if (draft.pretext !== AgentVoiceStore.getDefaultPretext(agent.displayName, agent.title)) toSave.pretext = draft.pretext;
|
|
@@ -742,22 +744,20 @@ ${_tl('bmadDesc')}
|
|
|
742
744
|
toSave.backgroundMusic = draft.backgroundMusic;
|
|
743
745
|
}
|
|
744
746
|
voiceStore.setAgentProfile(agent.id, toSave);
|
|
745
|
-
_closeModal();
|
|
746
747
|
refreshDisplay();
|
|
747
|
-
// Show temporary "Saved!" toast
|
|
748
748
|
_showSavedToast(agent.displayName);
|
|
749
|
-
}
|
|
749
|
+
}
|
|
750
750
|
|
|
751
|
-
const
|
|
751
|
+
const resetBtn = _modalBtn('Reset to Defaults', 18, () => {
|
|
752
752
|
voiceStore.resetAgentProfile(agent.id);
|
|
753
753
|
_closeModal();
|
|
754
754
|
refreshDisplay();
|
|
755
755
|
});
|
|
756
756
|
|
|
757
|
-
const
|
|
757
|
+
const closeBtn = _modalBtn('Cancel', 42, _closeModal);
|
|
758
758
|
|
|
759
759
|
// Blinking █ cursor + preview spinner — reusable across all modal buttons
|
|
760
|
-
const btnBlink = attachBtnBlink([previewBtn,
|
|
760
|
+
const btnBlink = attachBtnBlink([previewBtn, resetBtn, closeBtn], screen);
|
|
761
761
|
|
|
762
762
|
function _closeModal() {
|
|
763
763
|
if (_closed) return;
|
|
@@ -779,6 +779,7 @@ ${_tl('bmadDesc')}
|
|
|
779
779
|
switch (field.key) {
|
|
780
780
|
case 'voice':
|
|
781
781
|
_openVoicePickerForAgent(agent, draft, () => {
|
|
782
|
+
_autoSaveAgent();
|
|
782
783
|
fieldList.setItems(_fieldItems());
|
|
783
784
|
fieldList.select(idx);
|
|
784
785
|
fieldList.focus();
|
|
@@ -788,6 +789,7 @@ ${_tl('bmadDesc')}
|
|
|
788
789
|
|
|
789
790
|
case 'pretext':
|
|
790
791
|
_openPretextEditor(modal, draft, () => {
|
|
792
|
+
_autoSaveAgent();
|
|
791
793
|
fieldList.setItems(_fieldItems());
|
|
792
794
|
fieldList.select(idx);
|
|
793
795
|
fieldList.focus();
|
|
@@ -798,6 +800,7 @@ ${_tl('bmadDesc')}
|
|
|
798
800
|
case 'reverbPreset':
|
|
799
801
|
openReverbPicker(screen, draft.reverbPreset, (val) => {
|
|
800
802
|
draft.reverbPreset = val;
|
|
803
|
+
_autoSaveAgent();
|
|
801
804
|
fieldList.setItems(_fieldItems());
|
|
802
805
|
fieldList.select(idx);
|
|
803
806
|
fieldList.focus();
|
|
@@ -811,6 +814,7 @@ ${_tl('bmadDesc')}
|
|
|
811
814
|
case 'personality':
|
|
812
815
|
openPersonalityPicker(screen, draft.personality, (val) => {
|
|
813
816
|
draft.personality = val;
|
|
817
|
+
_autoSaveAgent();
|
|
814
818
|
fieldList.setItems(_fieldItems());
|
|
815
819
|
fieldList.select(idx);
|
|
816
820
|
fieldList.focus();
|
|
@@ -825,6 +829,7 @@ ${_tl('bmadDesc')}
|
|
|
825
829
|
openTrackPicker(screen, draft.backgroundMusic.track, draft.backgroundMusic.volume, (track) => {
|
|
826
830
|
draft.backgroundMusic.track = track;
|
|
827
831
|
draft.backgroundMusic.enabled = true;
|
|
832
|
+
_autoSaveAgent();
|
|
828
833
|
fieldList.setItems(_fieldItems());
|
|
829
834
|
fieldList.select(idx);
|
|
830
835
|
fieldList.focus();
|
|
@@ -839,6 +844,7 @@ ${_tl('bmadDesc')}
|
|
|
839
844
|
openVolumeInput(screen, draft.backgroundMusic.volume, (volume) => {
|
|
840
845
|
draft.backgroundMusic.volume = volume;
|
|
841
846
|
if (draft.backgroundMusic.track) draft.backgroundMusic.enabled = true;
|
|
847
|
+
_autoSaveAgent();
|
|
842
848
|
fieldList.setItems(_fieldItems());
|
|
843
849
|
fieldList.select(idx);
|
|
844
850
|
fieldList.focus();
|
|
@@ -861,9 +867,8 @@ ${_tl('bmadDesc')}
|
|
|
861
867
|
// Escape = close
|
|
862
868
|
fieldList.key(['escape', 'q'], _closeModal);
|
|
863
869
|
previewBtn.key(['escape'], _closeModal);
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
cancelBtn.key(['escape'], _closeModal);
|
|
870
|
+
resetBtn.key(['escape'], _closeModal);
|
|
871
|
+
closeBtn.key(['escape'], _closeModal);
|
|
867
872
|
|
|
868
873
|
// Tab + arrow navigation within modal
|
|
869
874
|
fieldList.key(['tab'], () => { previewBtn.focus(); screen.render(); });
|
|
@@ -887,23 +892,19 @@ ${_tl('bmadDesc')}
|
|
|
887
892
|
_prevFieldSel = cur;
|
|
888
893
|
});
|
|
889
894
|
|
|
890
|
-
// Wrap: up on buttons → back to field list
|
|
895
|
+
// Wrap: up on buttons → back to field list
|
|
891
896
|
previewBtn.key(['up'], () => { fieldList.focus(); fieldList.select(FIELDS.length - 1); screen.render(); });
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
cancelBtn.key(['up'], () => { fieldList.focus(); fieldList.select(FIELDS.length - 1); screen.render(); });
|
|
895
|
-
|
|
896
|
-
previewBtn.key(['tab', 'right'], () => { saveBtn.focus(); screen.render(); });
|
|
897
|
-
previewBtn.key(['left'], () => { cancelBtn.focus(); screen.render(); });
|
|
897
|
+
resetBtn.key(['up'], () => { fieldList.focus(); fieldList.select(FIELDS.length - 1); screen.render(); });
|
|
898
|
+
closeBtn.key(['up'], () => { fieldList.focus(); fieldList.select(FIELDS.length - 1); screen.render(); });
|
|
898
899
|
|
|
899
|
-
|
|
900
|
-
|
|
900
|
+
previewBtn.key(['tab', 'right'], () => { resetBtn.focus(); screen.render(); });
|
|
901
|
+
previewBtn.key(['left'], () => { closeBtn.focus(); screen.render(); });
|
|
901
902
|
|
|
902
|
-
|
|
903
|
-
|
|
903
|
+
resetBtn.key(['tab', 'right'], () => { closeBtn.focus(); screen.render(); });
|
|
904
|
+
resetBtn.key(['left'], () => { previewBtn.focus(); screen.render(); });
|
|
904
905
|
|
|
905
|
-
|
|
906
|
-
|
|
906
|
+
closeBtn.key(['tab', 'right'], () => { fieldList.focus(); screen.render(); });
|
|
907
|
+
closeBtn.key(['left'], () => { resetBtn.focus(); screen.render(); });
|
|
907
908
|
|
|
908
909
|
fieldList.focus();
|
|
909
910
|
screen.render();
|
|
@@ -914,7 +915,6 @@ ${_tl('bmadDesc')}
|
|
|
914
915
|
|
|
915
916
|
function _openVoicePickerForAgent(agent, draft, onDone) {
|
|
916
917
|
let _allVoices = [];
|
|
917
|
-
let _filterText = '';
|
|
918
918
|
let _previewProc = null;
|
|
919
919
|
let _previewVoiceId = null;
|
|
920
920
|
let _vpClosed = false;
|
|
@@ -959,29 +959,19 @@ ${_tl('bmadDesc')}
|
|
|
959
959
|
});
|
|
960
960
|
vpModal.setFront();
|
|
961
961
|
|
|
962
|
-
// Search
|
|
963
|
-
blessed.text({
|
|
964
|
-
parent: vpModal, top: 1, left: 2,
|
|
965
|
-
content: 'Search:', style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
|
|
966
|
-
});
|
|
967
|
-
const vpSearch = blessed.textbox({
|
|
968
|
-
parent: vpModal, top: 1, left: 11, width: 40, height: 1,
|
|
969
|
-
inputOnFocus: true, keys: true,
|
|
970
|
-
style: { fg: COLORS.valueFg, bg: '#1a3a5c', focus: { bg: '#245a80' } },
|
|
971
|
-
});
|
|
972
|
-
|
|
973
962
|
// Column header
|
|
974
|
-
const COL_N =
|
|
975
|
-
const COL_G =
|
|
963
|
+
const COL_N = 30;
|
|
964
|
+
const COL_G = 4;
|
|
976
965
|
blessed.text({
|
|
977
|
-
parent: vpModal, top:
|
|
978
|
-
content: `{bright-cyan-fg}${'Name'.padEnd(COL_N)}
|
|
966
|
+
parent: vpModal, top: 1, left: 6, tags: true,
|
|
967
|
+
content: `{bright-cyan-fg}${'Name'.padEnd(COL_N)}{/bright-cyan-fg}{magenta-fg}♀{/magenta-fg}/{bright-cyan-fg}♂{/bright-cyan-fg} {bright-cyan-fg}Provider{/bright-cyan-fg}`,
|
|
979
968
|
style: { bg: COLORS.contentBg },
|
|
980
969
|
});
|
|
981
970
|
|
|
982
971
|
const vpList = blessed.list({
|
|
983
|
-
parent: vpModal, top:
|
|
972
|
+
parent: vpModal, top: 2, left: 2, right: 2, bottom: 5,
|
|
984
973
|
keys: true, vi: true, mouse: true,
|
|
974
|
+
tags: true,
|
|
985
975
|
border: { type: 'line' },
|
|
986
976
|
scrollbar: { ch: '│', style: { fg: COLORS.borderFg } },
|
|
987
977
|
style: {
|
|
@@ -1004,16 +994,10 @@ ${_tl('bmadDesc')}
|
|
|
1004
994
|
|
|
1005
995
|
blessed.text({
|
|
1006
996
|
parent: vpModal, bottom: 2, left: 2, right: 2, tags: true,
|
|
1007
|
-
content: '{#455a64-fg}[
|
|
997
|
+
content: '{#455a64-fg}[↑↓] Nav [PgUp/PgDn] Page [Home/End] [a-z] Jump [Enter] Select [Space] Preview [Esc] Cancel{/#455a64-fg}',
|
|
1008
998
|
style: { bg: COLORS.contentBg },
|
|
1009
999
|
});
|
|
1010
1000
|
|
|
1011
|
-
function _getFiltered() {
|
|
1012
|
-
if (!_filterText) return _allVoices;
|
|
1013
|
-
const f = _filterText.toLowerCase();
|
|
1014
|
-
return _allVoices.filter(v => v.toLowerCase().includes(f));
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
1001
|
function _buildVoiceItems(voices) {
|
|
1018
1002
|
return voices.map(v => {
|
|
1019
1003
|
const isActive = v === draft.voice;
|
|
@@ -1023,7 +1007,8 @@ ${_tl('bmadDesc')}
|
|
|
1023
1007
|
const name = meta.displayName.length > COL_N
|
|
1024
1008
|
? meta.displayName.slice(0, COL_N - 1) + '…'
|
|
1025
1009
|
: meta.displayName.padEnd(COL_N);
|
|
1026
|
-
|
|
1010
|
+
// genderIconTag has invisible color tags — pad with literal spaces (1 visible char + 3 spaces = 4)
|
|
1011
|
+
return ` ${dot} ${name}${genderIconTag(meta.gender)} ${meta.provider}`;
|
|
1027
1012
|
});
|
|
1028
1013
|
}
|
|
1029
1014
|
|
|
@@ -1032,8 +1017,10 @@ ${_tl('bmadDesc')}
|
|
|
1032
1017
|
const savedIdx = vpList.selected ?? 0;
|
|
1033
1018
|
const savedScroll = vpList.childBase ?? 0;
|
|
1034
1019
|
_allVoices = scanInstalledVoices();
|
|
1035
|
-
|
|
1036
|
-
|
|
1020
|
+
// Sort by display name so the first-letter quick jump is intuitive
|
|
1021
|
+
_allVoices.sort((a, b) => getVoiceMeta(a).displayName.localeCompare(
|
|
1022
|
+
getVoiceMeta(b).displayName, undefined, { sensitivity: 'base' }));
|
|
1023
|
+
const items = _buildVoiceItems(_allVoices);
|
|
1037
1024
|
vpList.setItems(items.length > 0 ? items : [' (no voices found)']);
|
|
1038
1025
|
vpList.select(Math.min(savedIdx, items.length - 1));
|
|
1039
1026
|
vpList.childBase = Math.min(savedScroll, Math.max(0, items.length - (vpList.height - 2)));
|
|
@@ -1104,25 +1091,41 @@ ${_tl('bmadDesc')}
|
|
|
1104
1091
|
piper.on('error', () => { _previewProc = null; _previewVoiceId = null; });
|
|
1105
1092
|
}
|
|
1106
1093
|
|
|
1107
|
-
vpSearch.on('keypress', () => {
|
|
1108
|
-
setTimeout(() => { _filterText = vpSearch.getValue().trim(); _refreshVP(); }, 0);
|
|
1109
|
-
});
|
|
1110
|
-
vpSearch.key(['escape'], () => { vpList.focus(); screen.render(); });
|
|
1111
|
-
vpList.key(['/'], () => { vpSearch.clearValue(); vpSearch.focus(); screen.render(); });
|
|
1112
1094
|
vpList.key(['enter'], () => {
|
|
1113
|
-
const
|
|
1114
|
-
const sel = filtered[vpList.selected];
|
|
1095
|
+
const sel = _allVoices[vpList.selected];
|
|
1115
1096
|
if (sel) { draft.voice = sel; _closeVP(); }
|
|
1116
1097
|
});
|
|
1117
1098
|
vpList.key(['space'], () => {
|
|
1118
|
-
const
|
|
1119
|
-
const sel = filtered[vpList.selected];
|
|
1099
|
+
const sel = _allVoices[vpList.selected];
|
|
1120
1100
|
if (sel) _previewVoice(sel);
|
|
1121
1101
|
});
|
|
1122
1102
|
vpList.key(['escape', 'q'], _closeVP);
|
|
1123
1103
|
|
|
1104
|
+
// PageUp / PageDown / Home / End navigation
|
|
1105
|
+
const _pageSize = () => Math.max(1, (vpList.height ?? 10) - 2);
|
|
1106
|
+
vpList.key(['pageup'], () => { vpList.up(_pageSize()); screen.render(); });
|
|
1107
|
+
vpList.key(['pagedown'], () => { vpList.down(_pageSize()); screen.render(); });
|
|
1108
|
+
vpList.key(['home'], () => { vpList.select(0); screen.render(); });
|
|
1109
|
+
vpList.key(['end'], () => { vpList.select(Math.max(0, _allVoices.length - 1)); screen.render(); });
|
|
1110
|
+
|
|
1111
|
+
// First-letter quick jump: typing 'a' jumps to the first voice starting
|
|
1112
|
+
// with A. Block keys reserved by the list widget (vi nav, cancel) so
|
|
1113
|
+
// they don't get swallowed: q (cancel), j/k/g/h/l (vi navigation).
|
|
1114
|
+
const _vpJumpBlocked = new Set(['j', 'k', 'g', 'h', 'l', 'q']);
|
|
1115
|
+
vpList.on('keypress', (ch, key) => {
|
|
1116
|
+
if (!ch || key?.ctrl || key?.meta) return;
|
|
1117
|
+
if (!/^[a-zA-Z]$/.test(ch)) return;
|
|
1118
|
+
const target = ch.toLowerCase();
|
|
1119
|
+
if (_vpJumpBlocked.has(target)) return;
|
|
1120
|
+
const idx = _allVoices.findIndex(v => {
|
|
1121
|
+
const name = getVoiceMeta(v).displayName.toLowerCase();
|
|
1122
|
+
return name.startsWith(target);
|
|
1123
|
+
});
|
|
1124
|
+
if (idx >= 0) { vpList.select(idx); screen.render(); }
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1124
1127
|
_refreshVP();
|
|
1125
|
-
const activeIdx =
|
|
1128
|
+
const activeIdx = _allVoices.indexOf(draft.voice);
|
|
1126
1129
|
if (activeIdx >= 0) vpList.select(activeIdx);
|
|
1127
1130
|
vpList.focus();
|
|
1128
1131
|
screen.render();
|
|
@@ -75,6 +75,9 @@ const TRACK_DISPLAY = Object.freeze({
|
|
|
75
75
|
'agent_vibes_japanese_city_pop_v1_loop.mp3': '🌆 Japanese City Pop',
|
|
76
76
|
'agent_vibes_salsa_v2_loop.mp3': '💃 Salsa',
|
|
77
77
|
'agent_vibes_tabla_dream_pop_v1_loop.mp3': '🥁 Tabla Dream Pop',
|
|
78
|
+
'Late Night Hip Hop Groove.mp3': '🎤 Late Night Hip Hop Groove',
|
|
79
|
+
'Drifting Down the Hall.mp3': '🌃 Drifting Down the Hall',
|
|
80
|
+
'Midnight Charleston Stomp.mp3': '🎩 Midnight Charleston Stomp',
|
|
78
81
|
});
|
|
79
82
|
|
|
80
83
|
const BUILT_IN_TRACK_CATALOG = Object.freeze([
|
|
@@ -93,6 +96,9 @@ const BUILT_IN_TRACK_CATALOG = Object.freeze([
|
|
|
93
96
|
{ id: 'agent_vibes_japanese_city_pop_v1_loop.mp3', label: '🌆 Japanese City Pop' },
|
|
94
97
|
{ id: 'agent_vibes_salsa_v2_loop.mp3', label: '💃 Salsa' },
|
|
95
98
|
{ id: 'agent_vibes_tabla_dream_pop_v1_loop.mp3', label: '🥁 Tabla Dream Pop' },
|
|
99
|
+
{ id: 'Late Night Hip Hop Groove.mp3', label: '🎤 Late Night Hip Hop Groove' },
|
|
100
|
+
{ id: 'Drifting Down the Hall.mp3', label: '🌃 Drifting Down the Hall' },
|
|
101
|
+
{ id: 'Midnight Charleston Stomp.mp3', label: '🎩 Midnight Charleston Stomp' },
|
|
96
102
|
]);
|
|
97
103
|
|
|
98
104
|
// ---------------------------------------------------------------------------
|
|
@@ -180,10 +186,13 @@ export function scanTracks() {
|
|
|
180
186
|
try {
|
|
181
187
|
const files = fs.readdirSync(tracksDir);
|
|
182
188
|
const builtInIds = new Set(BUILT_IN_TRACK_CATALOG.map(t => t.id));
|
|
189
|
+
// Sort by the alphabetic part of the label (skip leading emoji/symbols)
|
|
190
|
+
// so the order reflects the track NAME, not the emoji codepoint.
|
|
191
|
+
const _sortKey = (s) => s.replace(/^[^a-zA-Z]+/, '');
|
|
183
192
|
return files
|
|
184
193
|
.filter(f => /\.mp3$/i.test(f))
|
|
185
|
-
.
|
|
186
|
-
.
|
|
194
|
+
.map(f => ({ id: f, label: formatTrackLabel(f), isBuiltIn: builtInIds.has(f) }))
|
|
195
|
+
.sort((a, b) => _sortKey(a.label).localeCompare(_sortKey(b.label), undefined, { sensitivity: 'base' }));
|
|
187
196
|
} catch {
|
|
188
197
|
// Directory not found or unreadable — use the static catalog
|
|
189
198
|
return BUILT_IN_TRACK_CATALOG.map(t => ({ ...t, isBuiltIn: true }));
|
|
@@ -470,24 +479,36 @@ export function createMusicTab(screen, services) {
|
|
|
470
479
|
let _hintBase = ''; // content of items[_hintIdx] before hint was appended
|
|
471
480
|
let _refreshing = false;
|
|
472
481
|
|
|
473
|
-
//
|
|
474
|
-
//
|
|
475
|
-
//
|
|
482
|
+
// Decoration helpers — strip blink cursor and (optionally) hint text from
|
|
483
|
+
// a list item's content. We strip hint by anchoring on the EXACT text from
|
|
484
|
+
// _rowHint() rather than a generic regex, so we cannot accidentally erase
|
|
485
|
+
// unrelated content that happens to contain similar tags.
|
|
486
|
+
const _BLINK_RE = / █/g;
|
|
487
|
+
function _stripBlink(raw) {
|
|
488
|
+
return (raw ?? '').replace(_BLINK_RE, '');
|
|
489
|
+
}
|
|
490
|
+
function _stripHint(raw) {
|
|
491
|
+
const hint = _rowHint();
|
|
492
|
+
if (!hint) return raw;
|
|
493
|
+
const noBlink = (raw ?? '').replace(_BLINK_RE, '');
|
|
494
|
+
if (noBlink.endsWith(hint)) return noBlink.slice(0, -hint.length);
|
|
495
|
+
return noBlink;
|
|
496
|
+
}
|
|
497
|
+
function _stripDecorations(raw) {
|
|
498
|
+
return _stripHint(_stripBlink(raw));
|
|
499
|
+
}
|
|
500
|
+
|
|
476
501
|
function _updateHint(idx) {
|
|
477
502
|
const items = trackList.items;
|
|
478
503
|
// Restore previously hinted row — pad with spaces to overwrite ghost hint text
|
|
479
504
|
const _pad = ' '.repeat(60);
|
|
480
505
|
if (_hintIdx >= 0 && _hintIdx !== idx && items[_hintIdx]) {
|
|
481
|
-
|
|
482
|
-
items[_hintIdx].setContent((hadBlink ? _hintBase + ' █' : _hintBase) + _pad);
|
|
506
|
+
items[_hintIdx].setContent(_hintBase + _pad);
|
|
483
507
|
}
|
|
484
508
|
// Add hint to the new row, saving its clean base first
|
|
485
509
|
if (idx >= 0 && items[idx]) {
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
if (hasBlink) c = c.slice(0, -2);
|
|
489
|
-
_hintBase = c;
|
|
490
|
-
items[idx].setContent(c + _rowHint() + (hasBlink ? ' █' : ''));
|
|
510
|
+
_hintBase = _stripDecorations(items[idx].content);
|
|
511
|
+
items[idx].setContent(_hintBase + _rowHint());
|
|
491
512
|
} else {
|
|
492
513
|
_hintBase = '';
|
|
493
514
|
}
|
|
@@ -909,13 +930,14 @@ export function createMusicTab(screen, services) {
|
|
|
909
930
|
_tlBlink.on = !_tlBlink.on;
|
|
910
931
|
const items = trackList.items;
|
|
911
932
|
const cur = trackList.selected ?? 0;
|
|
933
|
+
// Clean old row — only strip blink (hint, if any, is preserved)
|
|
912
934
|
if (_tlBlink.sel !== cur && _tlBlink.sel >= 0 && items[_tlBlink.sel]) {
|
|
913
|
-
items[_tlBlink.sel].setContent((items[_tlBlink.sel].content
|
|
935
|
+
items[_tlBlink.sel].setContent(_stripBlink(items[_tlBlink.sel].content));
|
|
914
936
|
}
|
|
915
937
|
_tlBlink.sel = cur;
|
|
916
938
|
if (items[cur]) {
|
|
917
|
-
const
|
|
918
|
-
items[cur].setContent(_tlBlink.on ? `${
|
|
939
|
+
const raw = _stripBlink(items[cur].content);
|
|
940
|
+
items[cur].setContent(_tlBlink.on ? `${raw} █` : raw);
|
|
919
941
|
}
|
|
920
942
|
screen.render();
|
|
921
943
|
}
|
|
@@ -927,7 +949,11 @@ export function createMusicTab(screen, services) {
|
|
|
927
949
|
_hintBase = '';
|
|
928
950
|
_updateHint(_tlBlink.sel);
|
|
929
951
|
const items = trackList.items;
|
|
930
|
-
|
|
952
|
+
// Append blink cursor — content already has hint from _updateHint
|
|
953
|
+
if (items[_tlBlink.sel]) {
|
|
954
|
+
const raw = _stripBlink(items[_tlBlink.sel].content);
|
|
955
|
+
items[_tlBlink.sel].setContent(raw + ' █');
|
|
956
|
+
}
|
|
931
957
|
if (!_playingTrackId) previewLine.setContent(_hintText());
|
|
932
958
|
screen.render();
|
|
933
959
|
_tlBlink.interval = setInterval(_tlTick, 500);
|
|
@@ -937,13 +963,17 @@ export function createMusicTab(screen, services) {
|
|
|
937
963
|
if (!_playingTrackId) previewLine.setContent('');
|
|
938
964
|
if (_tlBlink.interval) { clearInterval(_tlBlink.interval); _tlBlink.interval = null; }
|
|
939
965
|
const items = trackList.items;
|
|
966
|
+
// Strip blink from selected row, strip both from the hinted row
|
|
940
967
|
const sel = trackList.selected ?? 0;
|
|
941
968
|
if (items[sel]) {
|
|
942
|
-
|
|
943
|
-
|
|
969
|
+
if (sel === _hintIdx) {
|
|
970
|
+
items[sel].setContent(_stripDecorations(items[sel].content));
|
|
971
|
+
} else {
|
|
972
|
+
items[sel].setContent(_stripBlink(items[sel].content));
|
|
973
|
+
}
|
|
944
974
|
}
|
|
945
975
|
if (_hintIdx >= 0 && _hintIdx !== sel && items[_hintIdx]) {
|
|
946
|
-
items[_hintIdx].setContent(
|
|
976
|
+
items[_hintIdx].setContent(_stripDecorations(items[_hintIdx].content));
|
|
947
977
|
}
|
|
948
978
|
_hintIdx = -1;
|
|
949
979
|
_hintBase = '';
|