agentvibes 5.6.0 → 5.6.2
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 +3 -38
- package/.claude/config/audio-effects.cfg +1 -1
- package/.claude/config/background-music-enabled.txt +1 -1
- package/.claude/config/background-music-position.txt +6 -6
- package/.claude/github-star-reminder.txt +1 -1
- package/.claude/hooks/play-tts-ssh-remote.sh +119 -42
- package/.claude/hooks/play-tts-windows-receiver.sh +31 -0
- package/.claude/hooks/stop.sh +2 -27
- package/.claude/hooks-windows/play-tts-windows-sapi.ps1 +108 -108
- package/.claude/hooks-windows/play-tts.ps1 +58 -8
- package/.claude/piper-voices-dir.txt +1 -1
- package/.clawdbot/skill/README.md +326 -0
- package/.mcp.json +17 -27
- package/README.md +15 -2
- package/RELEASE_NOTES.md +64 -0
- package/bin/agent-vibes +39 -39
- package/package.json +1 -1
- package/src/bmad-detector.js +71 -71
- package/src/cli/list-personalities.js +110 -110
- package/src/cli/list-voices.js +114 -114
- package/src/commands/bmad-voices.js +394 -394
- package/src/commands/install-mcp.js +476 -476
- package/src/console/brand-colors.js +13 -13
- package/src/console/constants/personalities.js +44 -44
- package/src/console/modals/modal-overlay.js +247 -247
- package/src/console/navigation.js +5 -1
- package/src/console/tabs/agents-tab.js +5 -5
- package/src/console/tabs/help-tab.js +314 -314
- package/src/console/tabs/readme-tab.js +272 -272
- package/src/console/tabs/setup-tab.js +32 -17
- package/src/console/tabs/voices-tab.js +2 -2
- package/src/console/widgets/destroy-list.js +25 -25
- package/src/console/widgets/notice.js +55 -55
- package/src/console/widgets/personality-picker.js +213 -213
- package/src/console/widgets/reverb-picker.js +97 -97
- package/src/console/widgets/track-picker.js +1 -1
- package/src/i18n/de.js +202 -202
- package/src/i18n/es.js +202 -202
- package/src/i18n/fr.js +202 -202
- package/src/i18n/hi.js +202 -202
- package/src/i18n/ja.js +202 -202
- package/src/i18n/ko.js +202 -202
- package/src/i18n/pt.js +202 -202
- package/src/i18n/strings.js +54 -54
- package/src/i18n/zh-CN.js +202 -202
- package/src/installer/language-screen.js +31 -31
- package/src/installer/music-file-input.js +304 -304
- package/src/services/agent-voice-store.js +420 -423
- package/src/services/config-service.js +264 -264
- package/src/services/language-service.js +47 -47
- package/src/services/llm-provider-service.js +11 -4
- package/src/services/navigation-service.js +34 -10
- package/src/services/provider-service.js +143 -143
- package/src/utils/audio-duration-validator.js +298 -298
- package/src/utils/audio-format-validator.js +277 -277
- package/src/utils/dependency-checker.js +469 -469
- package/src/utils/file-ownership-verifier.js +358 -358
- package/src/utils/list-formatter.js +194 -194
- package/src/utils/music-file-validator.js +285 -285
- package/src/utils/preview-list-prompt.js +136 -136
- package/src/utils/secure-music-storage.js +412 -412
- package/.agentvibes/LITE-MODE.md +0 -236
- package/.agentvibes/README.md +0 -136
- package/.agentvibes/backup/session-start-tts.sh.20251210_212814 +0 -141
- package/.agentvibes/backups/agents/analyst_20260204_144958.md +0 -78
- package/.agentvibes/backups/agents/architect_20260204_144958.md +0 -72
- package/.agentvibes/backups/agents/dev_20260204_144958.md +0 -74
- package/.agentvibes/backups/agents/pm_20260204_144958.md +0 -72
- package/.agentvibes/backups/agents/quick-flow-solo-dev_20260204_144958.md +0 -64
- package/.agentvibes/backups/agents/sm_20260204_144958.md +0 -87
- package/.agentvibes/backups/agents/tea_20260204_144958.md +0 -79
- package/.agentvibes/backups/agents/tech-writer_20260204_144958.md +0 -82
- package/.agentvibes/backups/agents/ux-designer_20260204_144958.md +0 -80
- package/.agentvibes/config/README-personality-defaults.md +0 -162
- package/.agentvibes/config/agentvibes.json +0 -1
- package/.agentvibes/config/mode.txt +0 -1
- package/.agentvibes/config/personality-voice-defaults.default.json +0 -21
- package/.agentvibes/config/save-audio.txt +0 -1
- package/.agentvibes/config/voice-metadata.json +0 -160
- package/.agentvibes/hooks/help.sh +0 -191
- package/.agentvibes/hooks/post-tool-use-lite.sh +0 -111
- package/.agentvibes/hooks/save-audio-manager.sh +0 -162
- package/.agentvibes/hooks/session-start-full-optimized.sh +0 -102
- package/.agentvibes/hooks/session-start-full.sh +0 -142
- package/.agentvibes/hooks/session-start-lite-v2.sh +0 -34
- package/.agentvibes/hooks/session-start-lite.sh +0 -29
- package/.agentvibes/hooks/stop-lite.sh +0 -115
- package/.agentvibes/hooks/switch-mode.sh +0 -215
- package/.agentvibes/output-styles/audio-summary.md +0 -30
- package/.claude/audio/voice-samples/piper/alan.wav +0 -0
- package/.claude/audio/voice-samples/piper/amy.wav +0 -0
- package/.claude/audio/voice-samples/piper/charlotte.wav +0 -0
- package/.claude/audio/voice-samples/piper/joe.wav +0 -0
- package/.claude/audio/voice-samples/piper/john.wav +0 -0
- package/.claude/audio/voice-samples/piper/katherine.wav +0 -0
- package/.claude/audio/voice-samples/piper/kristin.wav +0 -0
- package/.claude/audio/voice-samples/piper/linda.wav +0 -0
- package/.claude/audio/voice-samples/piper/marcus.wav +0 -0
- package/.claude/audio/voice-samples/piper/ryan.wav +0 -0
- package/.claude/hooks/post-response.sh +0 -41
- package/bin/ensure-soprano-running.sh +0 -43
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AgentVibes TUI — Shared Widget: Modal Destroy Helper
|
|
3
|
-
*
|
|
4
|
-
* Force-invalidates blessed's olines buffer after destroying a modal widget.
|
|
5
|
-
* Without this, blessed skips repainting cells where lines==olines and the
|
|
6
|
-
* terminal retains stale modal content as ghost artifacts.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Destroy a blessed list/box widget and force full screen repaint.
|
|
11
|
-
*
|
|
12
|
-
* @param {object} widget - blessed widget to destroy
|
|
13
|
-
* @param {object} screen - blessed screen instance
|
|
14
|
-
* @param {Function} [onClose] - optional callback after destruction
|
|
15
|
-
*/
|
|
16
|
-
export function destroyList(widget, screen, onClose) {
|
|
17
|
-
widget.destroy();
|
|
18
|
-
try {
|
|
19
|
-
for (let r = 0; r < screen.height; r++)
|
|
20
|
-
for (let c = 0; c < screen.width; c++)
|
|
21
|
-
if (screen.olines[r]?.[c]) screen.olines[r][c][0] = -1;
|
|
22
|
-
} catch {}
|
|
23
|
-
onClose?.();
|
|
24
|
-
screen.render();
|
|
25
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* AgentVibes TUI — Shared Widget: Modal Destroy Helper
|
|
3
|
+
*
|
|
4
|
+
* Force-invalidates blessed's olines buffer after destroying a modal widget.
|
|
5
|
+
* Without this, blessed skips repainting cells where lines==olines and the
|
|
6
|
+
* terminal retains stale modal content as ghost artifacts.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Destroy a blessed list/box widget and force full screen repaint.
|
|
11
|
+
*
|
|
12
|
+
* @param {object} widget - blessed widget to destroy
|
|
13
|
+
* @param {object} screen - blessed screen instance
|
|
14
|
+
* @param {Function} [onClose] - optional callback after destruction
|
|
15
|
+
*/
|
|
16
|
+
export function destroyList(widget, screen, onClose) {
|
|
17
|
+
widget.destroy();
|
|
18
|
+
try {
|
|
19
|
+
for (let r = 0; r < screen.height; r++)
|
|
20
|
+
for (let c = 0; c < screen.width; c++)
|
|
21
|
+
if (screen.olines[r]?.[c]) screen.olines[r][c][0] = -1;
|
|
22
|
+
} catch {}
|
|
23
|
+
onClose?.();
|
|
24
|
+
screen.render();
|
|
25
|
+
}
|
|
@@ -1,55 +1,55 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AgentVibes TUI — Shared Widget: Notice Toast
|
|
3
|
-
*
|
|
4
|
-
* Displays a short auto-dismissing notice modal centred on screen.
|
|
5
|
-
* Usable from any tab; no settings-specific state required.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { destroyList } from './destroy-list.js';
|
|
9
|
-
|
|
10
|
-
const IS_TEST = process.env.AGENTVIBES_TEST_MODE === 'true';
|
|
11
|
-
let blessed;
|
|
12
|
-
if (!IS_TEST) {
|
|
13
|
-
const { default: b } = await import('blessed');
|
|
14
|
-
blessed = b;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Show a temporary notice that auto-dismisses after 2.5 seconds.
|
|
19
|
-
*
|
|
20
|
-
* @param {object} screen - blessed screen instance
|
|
21
|
-
* @param {string} message - text to display
|
|
22
|
-
* @param {object} [opts]
|
|
23
|
-
* @param {string} [opts.bg='#0a0e1a'] - background colour
|
|
24
|
-
* @param {string} [opts.fg='#e3f2fd'] - foreground colour
|
|
25
|
-
* @param {string} [opts.borderFg='bright-cyan'] - border colour
|
|
26
|
-
* @param {number} [opts.durationMs=2500] - auto-dismiss delay in ms
|
|
27
|
-
*/
|
|
28
|
-
export function showNotice(screen, message, opts = {}) {
|
|
29
|
-
const bg = opts.bg ?? '#0a0e1a';
|
|
30
|
-
const fg = opts.fg ?? '#e3f2fd';
|
|
31
|
-
const borderFg = opts.borderFg ?? 'bright-cyan';
|
|
32
|
-
const durationMs = opts.durationMs ?? 2500;
|
|
33
|
-
|
|
34
|
-
const width = Math.max(28, message.length + 6);
|
|
35
|
-
const modal = blessed.box({
|
|
36
|
-
parent: screen,
|
|
37
|
-
top: 'center',
|
|
38
|
-
left: 'center',
|
|
39
|
-
width,
|
|
40
|
-
height: 3,
|
|
41
|
-
border: { type: 'line' },
|
|
42
|
-
tags: true,
|
|
43
|
-
content: `{center}${message}{/center}`,
|
|
44
|
-
style: {
|
|
45
|
-
fg,
|
|
46
|
-
bg,
|
|
47
|
-
border: { fg: borderFg },
|
|
48
|
-
},
|
|
49
|
-
});
|
|
50
|
-
screen.render();
|
|
51
|
-
|
|
52
|
-
setTimeout(() => {
|
|
53
|
-
destroyList(modal, screen);
|
|
54
|
-
}, durationMs);
|
|
55
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* AgentVibes TUI — Shared Widget: Notice Toast
|
|
3
|
+
*
|
|
4
|
+
* Displays a short auto-dismissing notice modal centred on screen.
|
|
5
|
+
* Usable from any tab; no settings-specific state required.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { destroyList } from './destroy-list.js';
|
|
9
|
+
|
|
10
|
+
const IS_TEST = process.env.AGENTVIBES_TEST_MODE === 'true';
|
|
11
|
+
let blessed;
|
|
12
|
+
if (!IS_TEST) {
|
|
13
|
+
const { default: b } = await import('blessed');
|
|
14
|
+
blessed = b;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Show a temporary notice that auto-dismisses after 2.5 seconds.
|
|
19
|
+
*
|
|
20
|
+
* @param {object} screen - blessed screen instance
|
|
21
|
+
* @param {string} message - text to display
|
|
22
|
+
* @param {object} [opts]
|
|
23
|
+
* @param {string} [opts.bg='#0a0e1a'] - background colour
|
|
24
|
+
* @param {string} [opts.fg='#e3f2fd'] - foreground colour
|
|
25
|
+
* @param {string} [opts.borderFg='bright-cyan'] - border colour
|
|
26
|
+
* @param {number} [opts.durationMs=2500] - auto-dismiss delay in ms
|
|
27
|
+
*/
|
|
28
|
+
export function showNotice(screen, message, opts = {}) {
|
|
29
|
+
const bg = opts.bg ?? '#0a0e1a';
|
|
30
|
+
const fg = opts.fg ?? '#e3f2fd';
|
|
31
|
+
const borderFg = opts.borderFg ?? 'bright-cyan';
|
|
32
|
+
const durationMs = opts.durationMs ?? 2500;
|
|
33
|
+
|
|
34
|
+
const width = Math.max(28, message.length + 6);
|
|
35
|
+
const modal = blessed.box({
|
|
36
|
+
parent: screen,
|
|
37
|
+
top: 'center',
|
|
38
|
+
left: 'center',
|
|
39
|
+
width,
|
|
40
|
+
height: 3,
|
|
41
|
+
border: { type: 'line' },
|
|
42
|
+
tags: true,
|
|
43
|
+
content: `{center}${message}{/center}`,
|
|
44
|
+
style: {
|
|
45
|
+
fg,
|
|
46
|
+
bg,
|
|
47
|
+
border: { fg: borderFg },
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
screen.render();
|
|
51
|
+
|
|
52
|
+
setTimeout(() => {
|
|
53
|
+
destroyList(modal, screen);
|
|
54
|
+
}, durationMs);
|
|
55
|
+
}
|
|
@@ -1,213 +1,213 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AgentVibes TUI — Shared Widget: Personality Picker
|
|
3
|
-
*
|
|
4
|
-
* Inline modal list for selecting voice personalities with TTS preview.
|
|
5
|
-
* Extracted from settings-tab.js for reuse across tabs.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import path from 'node:path';
|
|
9
|
-
import fs from 'node:fs';
|
|
10
|
-
import os from 'node:os';
|
|
11
|
-
import { spawn } from 'node:child_process';
|
|
12
|
-
import { destroyList } from './destroy-list.js';
|
|
13
|
-
import { buildAudioEnv } from '../audio-env.js';
|
|
14
|
-
import { BRAND_PINK } from '../brand-colors.js';
|
|
15
|
-
import { PERSONALITY_EMOJIS, PERSONALITIES } from '../constants/personalities.js';
|
|
16
|
-
|
|
17
|
-
export { PERSONALITY_EMOJIS, PERSONALITIES };
|
|
18
|
-
|
|
19
|
-
const IS_TEST = process.env.AGENTVIBES_TEST_MODE === 'true';
|
|
20
|
-
let blessed;
|
|
21
|
-
if (!IS_TEST) {
|
|
22
|
-
const { default: b } = await import('blessed');
|
|
23
|
-
blessed = b;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const _modalTitle = (text) => ` {${BRAND_PINK}-fg}${text}{/${BRAND_PINK}-fg} `;
|
|
27
|
-
|
|
28
|
-
const PERSONALITY_PREVIEW_PHRASES = Object.freeze({
|
|
29
|
-
angry: "UNACCEPTABLE! This build time is a DISASTER! Fix it NOW or so help me!",
|
|
30
|
-
annoying: "Oh oh oh! Can I tell you something? Can I? Can I? PLEASE? It is so important!",
|
|
31
|
-
crass: "Well damn, that code runs like my uncle's truck. Barely, and it smells funny.",
|
|
32
|
-
dramatic: "The tests... have failed. I don't know how much longer I can do this.",
|
|
33
|
-
'dry-humor': "Your code worked. I too am surprised.",
|
|
34
|
-
flirty: "Ooh, a clean merge? You know exactly how to make my heart race.",
|
|
35
|
-
funny: "Why do programmers hate nature? Too many bugs. I will show myself out.",
|
|
36
|
-
grandpa: "Back in my day, we compiled by hand. Uphill. In the snow. Both ways.",
|
|
37
|
-
millennial: "I literally cannot even with this error. I am so done. Like, actually deceased.",
|
|
38
|
-
moody: "...It works. Whatever. Do not get used to it.",
|
|
39
|
-
pirate: "Arrr! The build be sailin' smooth today, matey! No barnacles in sight!",
|
|
40
|
-
poetic: "Like rivers to the sea, your code flows toward eventual compilation.",
|
|
41
|
-
professional: "I have completed the requested task and am prepared to document outcomes.",
|
|
42
|
-
rapper: "Yo! Clean code flowin', tests are glowin', no bugs showin'!",
|
|
43
|
-
robot: "TASK COMPLETE. EFFICIENCY: OPTIMAL. PROBABILITY OF SUCCESS: 97.3 PERCENT. BEEP.",
|
|
44
|
-
sarcastic: "Oh wow, another bug. What a completely unexpected surprise. Truly shocking.",
|
|
45
|
-
sassy: "Honey, whoever told you that was good code was not your friend.",
|
|
46
|
-
'surfer-dude':"Duuude! That commit totally shredded! Gnarly clean code, bro!",
|
|
47
|
-
zen: "The bug is not the enemy. The bug is the teacher. Breathe. Commit.",
|
|
48
|
-
random: "Who will I be today? Even I do not know. Expect the unexpected.",
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Open the personality picker modal.
|
|
53
|
-
*
|
|
54
|
-
* @param {object} screen - blessed screen
|
|
55
|
-
* @param {string} currentPersonality - current personality value
|
|
56
|
-
* @param {Function} onSelect - called with selected personality
|
|
57
|
-
* @param {Function} [onClose] - called after modal closes
|
|
58
|
-
*/
|
|
59
|
-
export function openPersonalityPicker(screen, currentPersonality, onSelect, onClose) {
|
|
60
|
-
const current = currentPersonality ?? 'none';
|
|
61
|
-
const currentIdx = Math.max(0, PERSONALITIES.indexOf(current));
|
|
62
|
-
|
|
63
|
-
const COLORS = {
|
|
64
|
-
btnFocus: '#2e7d32',
|
|
65
|
-
btnFocusFg: '#ffffff',
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const list = blessed.list({
|
|
69
|
-
parent: screen,
|
|
70
|
-
top: 'center',
|
|
71
|
-
left: 'center',
|
|
72
|
-
width: 44,
|
|
73
|
-
height: Math.min(PERSONALITIES.length + 4, 22),
|
|
74
|
-
border: { type: 'line' },
|
|
75
|
-
tags: true,
|
|
76
|
-
label: _modalTitle('Select Personality'),
|
|
77
|
-
items: PERSONALITIES.map((p, i) => {
|
|
78
|
-
const emoji = PERSONALITY_EMOJIS[p] ?? '✨';
|
|
79
|
-
const label = p === 'none' ? 'None' : p.charAt(0).toUpperCase() + p.slice(1);
|
|
80
|
-
const mark = i === currentIdx ? '✅' : ' ';
|
|
81
|
-
return `${mark} ${emoji} ${label}`;
|
|
82
|
-
}),
|
|
83
|
-
keys: true,
|
|
84
|
-
vi: true,
|
|
85
|
-
mouse: true,
|
|
86
|
-
style: {
|
|
87
|
-
border: { fg: COLORS.btnFocus },
|
|
88
|
-
selected: { bg: COLORS.btnFocus, fg: COLORS.btnFocusFg, bold: true },
|
|
89
|
-
item: { fg: '#e3f2fd' },
|
|
90
|
-
},
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
list.select(currentIdx);
|
|
94
|
-
list.focus();
|
|
95
|
-
screen.render();
|
|
96
|
-
|
|
97
|
-
// TTS preview on hover
|
|
98
|
-
let _pickerTtsProc = null;
|
|
99
|
-
let _playingItemIdx = -1;
|
|
100
|
-
|
|
101
|
-
function _setItemPlaying(idx, playing) {
|
|
102
|
-
const item = list.items?.[idx];
|
|
103
|
-
if (!item) return;
|
|
104
|
-
const base = (item.content ?? '').replace(/ █$/, '').replace(/ \(playing\)$/, '');
|
|
105
|
-
item.setContent(playing ? `${base} (playing)` : base);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function _killPickerTts() {
|
|
109
|
-
if (_pickerTtsProc) {
|
|
110
|
-
const _isWin = process.platform === 'win32' && !process.env.WSL_DISTRO_NAME;
|
|
111
|
-
if (_isWin) {
|
|
112
|
-
try { _pickerTtsProc.kill(); } catch {}
|
|
113
|
-
} else {
|
|
114
|
-
try { process.kill(-_pickerTtsProc.pid, 'SIGTERM'); } catch {}
|
|
115
|
-
}
|
|
116
|
-
_pickerTtsProc = null;
|
|
117
|
-
}
|
|
118
|
-
if (_playingItemIdx >= 0) {
|
|
119
|
-
_setItemPlaying(_playingItemIdx, false);
|
|
120
|
-
_playingItemIdx = -1;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function _speakPreview(personality) {
|
|
125
|
-
_killPickerTts();
|
|
126
|
-
const phrase = PERSONALITY_PREVIEW_PHRASES[personality];
|
|
127
|
-
if (!phrase) return;
|
|
128
|
-
const _isWin = process.platform === 'win32' && !process.env.WSL_DISTRO_NAME;
|
|
129
|
-
const _env = buildAudioEnv();
|
|
130
|
-
if (_isWin) {
|
|
131
|
-
// Prefer project-local install, fall back to global ~/.claude install
|
|
132
|
-
const _cwdScript = path.join(process.cwd(), '.claude', 'hooks-windows', 'play-tts.ps1');
|
|
133
|
-
const _homeScript = path.join(os.homedir(), '.claude', 'hooks-windows', 'play-tts.ps1');
|
|
134
|
-
const ttsScript = fs.existsSync(_cwdScript) ? _cwdScript : _homeScript;
|
|
135
|
-
_pickerTtsProc = spawn('powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', ttsScript, phrase], {
|
|
136
|
-
stdio: 'ignore',
|
|
137
|
-
env: _env,
|
|
138
|
-
});
|
|
139
|
-
} else {
|
|
140
|
-
const ttsScript = path.join(process.cwd(), '.claude', 'hooks', 'play-tts.sh');
|
|
141
|
-
_pickerTtsProc = spawn('bash', [ttsScript, phrase], {
|
|
142
|
-
stdio: 'ignore',
|
|
143
|
-
detached: true,
|
|
144
|
-
env: _env,
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
_playingItemIdx = list.selected;
|
|
148
|
-
_setItemPlaying(_playingItemIdx, true);
|
|
149
|
-
screen.render();
|
|
150
|
-
_pickerTtsProc.on('exit', () => {
|
|
151
|
-
if (_playingItemIdx >= 0) {
|
|
152
|
-
_setItemPlaying(_playingItemIdx, false);
|
|
153
|
-
_playingItemIdx = -1;
|
|
154
|
-
screen.render();
|
|
155
|
-
}
|
|
156
|
-
_pickerTtsProc = null;
|
|
157
|
-
});
|
|
158
|
-
_pickerTtsProc.on('error', () => {
|
|
159
|
-
_pickerTtsProc = null;
|
|
160
|
-
if (_playingItemIdx >= 0) {
|
|
161
|
-
_setItemPlaying(_playingItemIdx, false);
|
|
162
|
-
_playingItemIdx = -1;
|
|
163
|
-
screen.render();
|
|
164
|
-
}
|
|
165
|
-
});
|
|
166
|
-
_pickerTtsProc.unref();
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
list.on('select item', () => {
|
|
170
|
-
_speakPreview(PERSONALITIES[list.selected]);
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
list.key(['space'], () => {
|
|
174
|
-
if (_pickerTtsProc && _playingItemIdx === list.selected) {
|
|
175
|
-
_killPickerTts();
|
|
176
|
-
} else {
|
|
177
|
-
_speakPreview(PERSONALITIES[list.selected]);
|
|
178
|
-
}
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
// Type-to-jump
|
|
182
|
-
const _jumpBlocked = new Set(['j', 'k', 'g', 'h', 'l', 'd', 'u', 'q']);
|
|
183
|
-
list.on('keypress', (ch, key) => {
|
|
184
|
-
if (!ch || key.ctrl || key.meta) return;
|
|
185
|
-
const lower = ch.toLowerCase();
|
|
186
|
-
if (!/^[a-z]$/.test(lower)) return;
|
|
187
|
-
if (_jumpBlocked.has(lower)) return;
|
|
188
|
-
const count = PERSONALITIES.length;
|
|
189
|
-
const start = list.selected ?? 0;
|
|
190
|
-
for (let i = 1; i <= count; i++) {
|
|
191
|
-
const idx = (start + i) % count;
|
|
192
|
-
if (PERSONALITIES[idx].startsWith(lower)) {
|
|
193
|
-
list.select(idx);
|
|
194
|
-
screen.render();
|
|
195
|
-
break;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
list.key(['enter'], () => {
|
|
201
|
-
const selected = PERSONALITIES[list.selected];
|
|
202
|
-
if (!selected) return;
|
|
203
|
-
_killPickerTts();
|
|
204
|
-
// Call onSelect before destroying to avoid stale-state re-renders
|
|
205
|
-
onSelect(selected);
|
|
206
|
-
destroyList(list, screen, onClose);
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
list.key(['escape', 'q'], () => {
|
|
210
|
-
_killPickerTts();
|
|
211
|
-
destroyList(list, screen, onClose);
|
|
212
|
-
});
|
|
213
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* AgentVibes TUI — Shared Widget: Personality Picker
|
|
3
|
+
*
|
|
4
|
+
* Inline modal list for selecting voice personalities with TTS preview.
|
|
5
|
+
* Extracted from settings-tab.js for reuse across tabs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import os from 'node:os';
|
|
11
|
+
import { spawn } from 'node:child_process';
|
|
12
|
+
import { destroyList } from './destroy-list.js';
|
|
13
|
+
import { buildAudioEnv } from '../audio-env.js';
|
|
14
|
+
import { BRAND_PINK } from '../brand-colors.js';
|
|
15
|
+
import { PERSONALITY_EMOJIS, PERSONALITIES } from '../constants/personalities.js';
|
|
16
|
+
|
|
17
|
+
export { PERSONALITY_EMOJIS, PERSONALITIES };
|
|
18
|
+
|
|
19
|
+
const IS_TEST = process.env.AGENTVIBES_TEST_MODE === 'true';
|
|
20
|
+
let blessed;
|
|
21
|
+
if (!IS_TEST) {
|
|
22
|
+
const { default: b } = await import('blessed');
|
|
23
|
+
blessed = b;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const _modalTitle = (text) => ` {${BRAND_PINK}-fg}${text}{/${BRAND_PINK}-fg} `;
|
|
27
|
+
|
|
28
|
+
const PERSONALITY_PREVIEW_PHRASES = Object.freeze({
|
|
29
|
+
angry: "UNACCEPTABLE! This build time is a DISASTER! Fix it NOW or so help me!",
|
|
30
|
+
annoying: "Oh oh oh! Can I tell you something? Can I? Can I? PLEASE? It is so important!",
|
|
31
|
+
crass: "Well damn, that code runs like my uncle's truck. Barely, and it smells funny.",
|
|
32
|
+
dramatic: "The tests... have failed. I don't know how much longer I can do this.",
|
|
33
|
+
'dry-humor': "Your code worked. I too am surprised.",
|
|
34
|
+
flirty: "Ooh, a clean merge? You know exactly how to make my heart race.",
|
|
35
|
+
funny: "Why do programmers hate nature? Too many bugs. I will show myself out.",
|
|
36
|
+
grandpa: "Back in my day, we compiled by hand. Uphill. In the snow. Both ways.",
|
|
37
|
+
millennial: "I literally cannot even with this error. I am so done. Like, actually deceased.",
|
|
38
|
+
moody: "...It works. Whatever. Do not get used to it.",
|
|
39
|
+
pirate: "Arrr! The build be sailin' smooth today, matey! No barnacles in sight!",
|
|
40
|
+
poetic: "Like rivers to the sea, your code flows toward eventual compilation.",
|
|
41
|
+
professional: "I have completed the requested task and am prepared to document outcomes.",
|
|
42
|
+
rapper: "Yo! Clean code flowin', tests are glowin', no bugs showin'!",
|
|
43
|
+
robot: "TASK COMPLETE. EFFICIENCY: OPTIMAL. PROBABILITY OF SUCCESS: 97.3 PERCENT. BEEP.",
|
|
44
|
+
sarcastic: "Oh wow, another bug. What a completely unexpected surprise. Truly shocking.",
|
|
45
|
+
sassy: "Honey, whoever told you that was good code was not your friend.",
|
|
46
|
+
'surfer-dude':"Duuude! That commit totally shredded! Gnarly clean code, bro!",
|
|
47
|
+
zen: "The bug is not the enemy. The bug is the teacher. Breathe. Commit.",
|
|
48
|
+
random: "Who will I be today? Even I do not know. Expect the unexpected.",
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Open the personality picker modal.
|
|
53
|
+
*
|
|
54
|
+
* @param {object} screen - blessed screen
|
|
55
|
+
* @param {string} currentPersonality - current personality value
|
|
56
|
+
* @param {Function} onSelect - called with selected personality
|
|
57
|
+
* @param {Function} [onClose] - called after modal closes
|
|
58
|
+
*/
|
|
59
|
+
export function openPersonalityPicker(screen, currentPersonality, onSelect, onClose) {
|
|
60
|
+
const current = currentPersonality ?? 'none';
|
|
61
|
+
const currentIdx = Math.max(0, PERSONALITIES.indexOf(current));
|
|
62
|
+
|
|
63
|
+
const COLORS = {
|
|
64
|
+
btnFocus: '#2e7d32',
|
|
65
|
+
btnFocusFg: '#ffffff',
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const list = blessed.list({
|
|
69
|
+
parent: screen,
|
|
70
|
+
top: 'center',
|
|
71
|
+
left: 'center',
|
|
72
|
+
width: 44,
|
|
73
|
+
height: Math.min(PERSONALITIES.length + 4, 22),
|
|
74
|
+
border: { type: 'line' },
|
|
75
|
+
tags: true,
|
|
76
|
+
label: _modalTitle('Select Personality'),
|
|
77
|
+
items: PERSONALITIES.map((p, i) => {
|
|
78
|
+
const emoji = PERSONALITY_EMOJIS[p] ?? '✨';
|
|
79
|
+
const label = p === 'none' ? 'None' : p.charAt(0).toUpperCase() + p.slice(1);
|
|
80
|
+
const mark = i === currentIdx ? '✅' : ' ';
|
|
81
|
+
return `${mark} ${emoji} ${label}`;
|
|
82
|
+
}),
|
|
83
|
+
keys: true,
|
|
84
|
+
vi: true,
|
|
85
|
+
mouse: true,
|
|
86
|
+
style: {
|
|
87
|
+
border: { fg: COLORS.btnFocus },
|
|
88
|
+
selected: { bg: COLORS.btnFocus, fg: COLORS.btnFocusFg, bold: true },
|
|
89
|
+
item: { fg: '#e3f2fd' },
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
list.select(currentIdx);
|
|
94
|
+
list.focus();
|
|
95
|
+
screen.render();
|
|
96
|
+
|
|
97
|
+
// TTS preview on hover
|
|
98
|
+
let _pickerTtsProc = null;
|
|
99
|
+
let _playingItemIdx = -1;
|
|
100
|
+
|
|
101
|
+
function _setItemPlaying(idx, playing) {
|
|
102
|
+
const item = list.items?.[idx];
|
|
103
|
+
if (!item) return;
|
|
104
|
+
const base = (item.content ?? '').replace(/ █$/, '').replace(/ \(playing\)$/, '');
|
|
105
|
+
item.setContent(playing ? `${base} (playing)` : base);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function _killPickerTts() {
|
|
109
|
+
if (_pickerTtsProc) {
|
|
110
|
+
const _isWin = process.platform === 'win32' && !process.env.WSL_DISTRO_NAME;
|
|
111
|
+
if (_isWin) {
|
|
112
|
+
try { _pickerTtsProc.kill(); } catch {}
|
|
113
|
+
} else {
|
|
114
|
+
try { process.kill(-_pickerTtsProc.pid, 'SIGTERM'); } catch {}
|
|
115
|
+
}
|
|
116
|
+
_pickerTtsProc = null;
|
|
117
|
+
}
|
|
118
|
+
if (_playingItemIdx >= 0) {
|
|
119
|
+
_setItemPlaying(_playingItemIdx, false);
|
|
120
|
+
_playingItemIdx = -1;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function _speakPreview(personality) {
|
|
125
|
+
_killPickerTts();
|
|
126
|
+
const phrase = PERSONALITY_PREVIEW_PHRASES[personality];
|
|
127
|
+
if (!phrase) return;
|
|
128
|
+
const _isWin = process.platform === 'win32' && !process.env.WSL_DISTRO_NAME;
|
|
129
|
+
const _env = buildAudioEnv();
|
|
130
|
+
if (_isWin) {
|
|
131
|
+
// Prefer project-local install, fall back to global ~/.claude install
|
|
132
|
+
const _cwdScript = path.join(process.cwd(), '.claude', 'hooks-windows', 'play-tts.ps1');
|
|
133
|
+
const _homeScript = path.join(os.homedir(), '.claude', 'hooks-windows', 'play-tts.ps1');
|
|
134
|
+
const ttsScript = fs.existsSync(_cwdScript) ? _cwdScript : _homeScript;
|
|
135
|
+
_pickerTtsProc = spawn('powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', ttsScript, phrase], {
|
|
136
|
+
stdio: 'ignore',
|
|
137
|
+
env: _env,
|
|
138
|
+
});
|
|
139
|
+
} else {
|
|
140
|
+
const ttsScript = path.join(process.cwd(), '.claude', 'hooks', 'play-tts.sh');
|
|
141
|
+
_pickerTtsProc = spawn('bash', [ttsScript, phrase], {
|
|
142
|
+
stdio: 'ignore',
|
|
143
|
+
detached: true,
|
|
144
|
+
env: _env,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
_playingItemIdx = list.selected;
|
|
148
|
+
_setItemPlaying(_playingItemIdx, true);
|
|
149
|
+
screen.render();
|
|
150
|
+
_pickerTtsProc.on('exit', () => {
|
|
151
|
+
if (_playingItemIdx >= 0) {
|
|
152
|
+
_setItemPlaying(_playingItemIdx, false);
|
|
153
|
+
_playingItemIdx = -1;
|
|
154
|
+
screen.render();
|
|
155
|
+
}
|
|
156
|
+
_pickerTtsProc = null;
|
|
157
|
+
});
|
|
158
|
+
_pickerTtsProc.on('error', () => {
|
|
159
|
+
_pickerTtsProc = null;
|
|
160
|
+
if (_playingItemIdx >= 0) {
|
|
161
|
+
_setItemPlaying(_playingItemIdx, false);
|
|
162
|
+
_playingItemIdx = -1;
|
|
163
|
+
screen.render();
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
_pickerTtsProc.unref();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
list.on('select item', () => {
|
|
170
|
+
_speakPreview(PERSONALITIES[list.selected]);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
list.key(['space'], () => {
|
|
174
|
+
if (_pickerTtsProc && _playingItemIdx === list.selected) {
|
|
175
|
+
_killPickerTts();
|
|
176
|
+
} else {
|
|
177
|
+
_speakPreview(PERSONALITIES[list.selected]);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Type-to-jump
|
|
182
|
+
const _jumpBlocked = new Set(['j', 'k', 'g', 'h', 'l', 'd', 'u', 'q']);
|
|
183
|
+
list.on('keypress', (ch, key) => {
|
|
184
|
+
if (!ch || key.ctrl || key.meta) return;
|
|
185
|
+
const lower = ch.toLowerCase();
|
|
186
|
+
if (!/^[a-z]$/.test(lower)) return;
|
|
187
|
+
if (_jumpBlocked.has(lower)) return;
|
|
188
|
+
const count = PERSONALITIES.length;
|
|
189
|
+
const start = list.selected ?? 0;
|
|
190
|
+
for (let i = 1; i <= count; i++) {
|
|
191
|
+
const idx = (start + i) % count;
|
|
192
|
+
if (PERSONALITIES[idx].startsWith(lower)) {
|
|
193
|
+
list.select(idx);
|
|
194
|
+
screen.render();
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
list.key(['enter'], () => {
|
|
201
|
+
const selected = PERSONALITIES[list.selected];
|
|
202
|
+
if (!selected) return;
|
|
203
|
+
_killPickerTts();
|
|
204
|
+
// Call onSelect before destroying to avoid stale-state re-renders
|
|
205
|
+
onSelect(selected);
|
|
206
|
+
destroyList(list, screen, onClose);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
list.key(['escape', 'q'], () => {
|
|
210
|
+
_killPickerTts();
|
|
211
|
+
destroyList(list, screen, onClose);
|
|
212
|
+
});
|
|
213
|
+
}
|