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