agentvibes 5.5.0 → 5.6.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.
Files changed (100) hide show
  1. package/.agentvibes/config.json +3 -30
  2. package/.claude/config/background-music-enabled.txt +1 -1
  3. package/.claude/config/background-music-position.txt +6 -6
  4. package/.claude/github-star-reminder.txt +1 -1
  5. package/.claude/hooks/play-tts-ssh-remote.sh +119 -42
  6. package/.claude/hooks/play-tts-windows-receiver.sh +31 -0
  7. package/.claude/hooks/stop.sh +2 -27
  8. package/.claude/hooks-windows/play-tts-windows-sapi.ps1 +108 -108
  9. package/.claude/hooks-windows/play-tts.ps1 +23 -7
  10. package/.claude/piper-voices-dir.txt +1 -1
  11. package/.clawdbot/skill/README.md +326 -0
  12. package/.mcp.json +17 -27
  13. package/README.md +73 -82
  14. package/RELEASE_NOTES.md +61 -0
  15. package/bin/agent-vibes +39 -39
  16. package/package.json +1 -1
  17. package/src/bmad-detector.js +71 -71
  18. package/src/cli/list-personalities.js +110 -110
  19. package/src/cli/list-voices.js +114 -114
  20. package/src/commands/bmad-voices.js +394 -394
  21. package/src/commands/install-mcp.js +476 -476
  22. package/src/console/brand-colors.js +13 -13
  23. package/src/console/constants/personalities.js +44 -44
  24. package/src/console/modals/modal-overlay.js +247 -247
  25. package/src/console/navigation.js +5 -1
  26. package/src/console/tabs/agents-tab.js +5 -5
  27. package/src/console/tabs/help-tab.js +314 -314
  28. package/src/console/tabs/readme-tab.js +272 -272
  29. package/src/console/tabs/setup-tab.js +32 -17
  30. package/src/console/tabs/voices-tab.js +2 -2
  31. package/src/console/widgets/destroy-list.js +25 -25
  32. package/src/console/widgets/notice.js +55 -55
  33. package/src/console/widgets/personality-picker.js +213 -213
  34. package/src/console/widgets/reverb-picker.js +97 -97
  35. package/src/console/widgets/track-picker.js +1 -1
  36. package/src/i18n/de.js +202 -202
  37. package/src/i18n/es.js +202 -202
  38. package/src/i18n/fr.js +202 -202
  39. package/src/i18n/hi.js +202 -202
  40. package/src/i18n/ja.js +202 -202
  41. package/src/i18n/ko.js +202 -202
  42. package/src/i18n/pt.js +202 -202
  43. package/src/i18n/strings.js +54 -54
  44. package/src/i18n/zh-CN.js +202 -202
  45. package/src/installer/language-screen.js +31 -31
  46. package/src/installer/music-file-input.js +304 -304
  47. package/src/services/agent-voice-store.js +420 -423
  48. package/src/services/config-service.js +264 -264
  49. package/src/services/language-service.js +47 -47
  50. package/src/services/llm-provider-service.js +11 -4
  51. package/src/services/navigation-service.js +34 -10
  52. package/src/services/provider-service.js +143 -143
  53. package/src/utils/audio-duration-validator.js +298 -298
  54. package/src/utils/audio-format-validator.js +277 -277
  55. package/src/utils/dependency-checker.js +469 -469
  56. package/src/utils/file-ownership-verifier.js +358 -358
  57. package/src/utils/list-formatter.js +194 -194
  58. package/src/utils/music-file-validator.js +285 -285
  59. package/src/utils/preview-list-prompt.js +136 -136
  60. package/src/utils/secure-music-storage.js +412 -412
  61. package/.agentvibes/LITE-MODE.md +0 -236
  62. package/.agentvibes/README.md +0 -136
  63. package/.agentvibes/backup/session-start-tts.sh.20251210_212814 +0 -141
  64. package/.agentvibes/backups/agents/analyst_20260204_144958.md +0 -78
  65. package/.agentvibes/backups/agents/architect_20260204_144958.md +0 -72
  66. package/.agentvibes/backups/agents/dev_20260204_144958.md +0 -74
  67. package/.agentvibes/backups/agents/pm_20260204_144958.md +0 -72
  68. package/.agentvibes/backups/agents/quick-flow-solo-dev_20260204_144958.md +0 -64
  69. package/.agentvibes/backups/agents/sm_20260204_144958.md +0 -87
  70. package/.agentvibes/backups/agents/tea_20260204_144958.md +0 -79
  71. package/.agentvibes/backups/agents/tech-writer_20260204_144958.md +0 -82
  72. package/.agentvibes/backups/agents/ux-designer_20260204_144958.md +0 -80
  73. package/.agentvibes/config/README-personality-defaults.md +0 -162
  74. package/.agentvibes/config/agentvibes.json +0 -1
  75. package/.agentvibes/config/mode.txt +0 -1
  76. package/.agentvibes/config/personality-voice-defaults.default.json +0 -21
  77. package/.agentvibes/config/save-audio.txt +0 -1
  78. package/.agentvibes/config/voice-metadata.json +0 -160
  79. package/.agentvibes/hooks/help.sh +0 -191
  80. package/.agentvibes/hooks/post-tool-use-lite.sh +0 -111
  81. package/.agentvibes/hooks/save-audio-manager.sh +0 -162
  82. package/.agentvibes/hooks/session-start-full-optimized.sh +0 -102
  83. package/.agentvibes/hooks/session-start-full.sh +0 -142
  84. package/.agentvibes/hooks/session-start-lite-v2.sh +0 -34
  85. package/.agentvibes/hooks/session-start-lite.sh +0 -29
  86. package/.agentvibes/hooks/stop-lite.sh +0 -115
  87. package/.agentvibes/hooks/switch-mode.sh +0 -215
  88. package/.agentvibes/output-styles/audio-summary.md +0 -30
  89. package/.claude/audio/voice-samples/piper/alan.wav +0 -0
  90. package/.claude/audio/voice-samples/piper/amy.wav +0 -0
  91. package/.claude/audio/voice-samples/piper/charlotte.wav +0 -0
  92. package/.claude/audio/voice-samples/piper/joe.wav +0 -0
  93. package/.claude/audio/voice-samples/piper/john.wav +0 -0
  94. package/.claude/audio/voice-samples/piper/katherine.wav +0 -0
  95. package/.claude/audio/voice-samples/piper/kristin.wav +0 -0
  96. package/.claude/audio/voice-samples/piper/linda.wav +0 -0
  97. package/.claude/audio/voice-samples/piper/marcus.wav +0 -0
  98. package/.claude/audio/voice-samples/piper/ryan.wav +0 -0
  99. package/.claude/hooks/post-response.sh +0 -41
  100. 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
+ }