agentvibes 5.13.1 → 5.15.0

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 (84) hide show
  1. package/.claude/audio/ui/CREDITS.txt +16 -16
  2. package/.claude/commands/agent-vibes-bmad-voices.md +117 -117
  3. package/.claude/commands/agent-vibes-rdp.md +24 -24
  4. package/.claude/config/audio-effects.cfg.sample +52 -52
  5. package/.claude/docs/TERMUX_SETUP.md +408 -408
  6. package/.claude/github-star-reminder.txt +1 -1
  7. package/.claude/hooks/kokoro-server.py +219 -219
  8. package/.claude/hooks/kokoro-tts.py +141 -141
  9. package/.claude/hooks/party-stage-roster.py +328 -328
  10. package/.claude/hooks/play-tts-kokoro.sh +7 -2
  11. package/.claude/hooks/play-tts-ssh-remote.sh +38 -3
  12. package/.claude/hooks/requirements.txt +6 -6
  13. package/.claude/hooks/soprano-gradio-synth.py +139 -139
  14. package/.claude/hooks/translator.py +237 -237
  15. package/.claude/hooks-windows/agentvibes-session-id.ps1 +83 -0
  16. package/.claude/hooks-windows/kokoro-server.py +219 -219
  17. package/.claude/hooks-windows/kokoro-tts.py +107 -107
  18. package/.claude/hooks-windows/play-tts.ps1 +112 -1
  19. package/.claude/hooks-windows/session-start-tts.ps1 +36 -0
  20. package/.claude/hooks-windows/soprano-gradio-synth.py +153 -153
  21. package/.claude/verbosity.txt +1 -1
  22. package/.clawdbot/README.md +105 -105
  23. package/CLAUDE.md +84 -176
  24. package/README.md +35 -2
  25. package/RELEASE_NOTES.md +139 -0
  26. package/WINDOWS-SETUP.md +208 -208
  27. package/bin/agent-vibes +39 -39
  28. package/bin/mcp-server.js +121 -121
  29. package/bin/test-bmad-pr +78 -78
  30. package/mcp-server/QUICK_START.md +203 -203
  31. package/mcp-server/README.md +345 -345
  32. package/mcp-server/examples/claude_desktop_config.json +11 -11
  33. package/mcp-server/examples/claude_desktop_config_piper.json +9 -9
  34. package/mcp-server/examples/custom_instructions.md +169 -169
  35. package/mcp-server/install-deps.js +12 -2
  36. package/mcp-server/server.py +2111 -2085
  37. package/mcp-server/test_mcp_correctness.py +504 -504
  38. package/mcp-server/test_windows_script_parity.py +339 -339
  39. package/package.json +6 -3
  40. package/setup-ssh-receiver.ps1 +259 -0
  41. package/setup-windows.ps1 +39 -29
  42. package/src/cli/list-personalities.js +110 -110
  43. package/src/commands/bmad-voices.js +394 -394
  44. package/src/console/brand-colors.js +13 -13
  45. package/src/console/constants/personalities.js +44 -44
  46. package/src/console/tabs/agents-tab.js +14 -4
  47. package/src/console/tabs/help-tab.js +314 -314
  48. package/src/console/tabs/readme-tab.js +272 -272
  49. package/src/console/tabs/settings-tab.js +31 -6
  50. package/src/console/tabs/setup-tab.js +154 -32
  51. package/src/console/tabs/voices-tab.js +48 -6
  52. package/src/console/widgets/destroy-list.js +25 -25
  53. package/src/console/widgets/notice.js +55 -55
  54. package/src/installer/language-screen.js +31 -31
  55. package/src/installer/music-file-input.js +304 -304
  56. package/src/installer.js +98 -37
  57. package/src/services/language-service.js +47 -47
  58. package/src/services/llm-provider-service.js +4 -1
  59. package/src/services/provider-voice-catalog.js +9 -5
  60. package/src/utils/audio-format-validator.js +277 -277
  61. package/src/utils/dependency-checker.js +469 -469
  62. package/src/utils/file-ownership-verifier.js +358 -358
  63. package/src/utils/music-file-validator.js +285 -285
  64. package/src/utils/preview-list-prompt.js +144 -144
  65. package/src/utils/secure-music-storage.js +412 -412
  66. package/templates/agentvibes-receiver.ps1 +311 -0
  67. package/templates/agentvibes-receiver.sh +297 -297
  68. package/voice-assignments.json +8244 -8244
  69. package/.agentvibes/config.json +0 -17
  70. package/.agentvibes/install-manifest.json +0 -362
  71. package/.claude/config/audio-effects.cfg +0 -7
  72. package/.claude/config/audio-effects.cfg.bak-kokoro +0 -7
  73. package/.claude/config/background-music-enabled.txt +0 -1
  74. package/.claude/config/background-music-position.txt +0 -27
  75. package/.claude/config/background-music-volume.txt +0 -1
  76. package/.claude/config/background-music.cfg +0 -1
  77. package/.claude/config/background-music.txt +0 -1
  78. package/.claude/config/language.txt +0 -1
  79. package/.claude/config/personality.txt +0 -1
  80. package/.claude/config/reverb-level.txt +0 -1
  81. package/.claude/config/tts-speech-rate.txt +0 -1
  82. package/.claude/config/tts-verbosity.txt +0 -1
  83. package/.claude/hooks-windows/audio-cache-utils.ps1.user.bak +0 -119
  84. package/.claude/hooks-windows/soprano-gradio-synth.py.user.bak +0 -153
@@ -1,272 +1,272 @@
1
- /**
2
- * AgentVibes TUI Console — Readme Tab
3
- * Epic 13: Story 13.2
4
- *
5
- * Implements the Tab Component Contract:
6
- * createReadmeTab(screen, services) → { box, show, hide, onFocus, onBlur, getFooterText, getFooterColor }
7
- *
8
- * Features: renders README.md with styled markdown, scrolling, search.
9
- */
10
-
11
- import fs from 'node:fs';
12
- import path from 'node:path';
13
- import { t } from '../../i18n/strings.js';
14
-
15
- const IS_TEST = process.env.AGENTVIBES_TEST_MODE === 'true';
16
-
17
- let blessed;
18
- if (!IS_TEST) {
19
- const { default: b } = await import('blessed');
20
- blessed = b;
21
- }
22
-
23
- // ---------------------------------------------------------------------------
24
-
25
- const COLORS = {
26
- contentBg: '#0a0e1a',
27
- h1Fg: '#69f0ae', // Green — H1
28
- h2Fg: '#82b1ff', // Blue — H2
29
- h3Fg: '#80d8ff', // Light blue — H3
30
- codeFg: '#ffff00', // Yellow — code spans
31
- boldFg: '#e3f2fd', // Bright — bold text
32
- quoteFg: '#90a4ae', // Gray — blockquotes
33
- labelFg: '#e3f2fd',
34
- borderFg: '#455a64',
35
- footerBg: '#455a64', // Dark gray — Readme tab footer
36
- };
37
-
38
- const _FOOTER_TEXT_EN = '[↑↓/jk] Scroll [PgUp/PgDn] Page [/] Search [S/V/M/A/R] Tab [Q] Quit';
39
-
40
- // ---------------------------------------------------------------------------
41
- // Markdown renderer (story 13.2)
42
-
43
- /**
44
- * Render a single markdown line to blessed tagged string.
45
- * Handles: H1/H2/H3, bold (**), code spans (`), blockquotes (>), plain text.
46
- *
47
- * @param {string} line
48
- * @returns {string}
49
- */
50
- export function renderMarkdownLine(line) {
51
- // H1
52
- if (/^# /.test(line)) {
53
- const text = line.replace(/^# /, '');
54
- return `{bold}{${COLORS.h1Fg}-fg}${text}{/${COLORS.h1Fg}-fg}{/bold}`;
55
- }
56
- // H2
57
- if (/^## /.test(line)) {
58
- const text = line.replace(/^## /, '');
59
- return `{bold}{${COLORS.h2Fg}-fg}${text}{/${COLORS.h2Fg}-fg}{/bold}`;
60
- }
61
- // H3
62
- if (/^### /.test(line)) {
63
- const text = line.replace(/^### /, '');
64
- return `{${COLORS.h3Fg}-fg}${text}{/${COLORS.h3Fg}-fg}`;
65
- }
66
- // Blockquote
67
- if (/^> /.test(line)) {
68
- const text = line.replace(/^> /, '');
69
- return `{${COLORS.quoteFg}-fg}│ ${text}{/${COLORS.quoteFg}-fg}`;
70
- }
71
- // Horizontal rule
72
- if (/^---+$/.test(line.trim())) {
73
- return `{${COLORS.quoteFg}-fg}${'─'.repeat(66)}{/${COLORS.quoteFg}-fg}`;
74
- }
75
-
76
- // Inline: code spans, bold
77
- let result = line;
78
- // Escape existing blessed tags (literal braces in content)
79
- // Code spans: `text`
80
- result = result.replace(/`([^`]+)`/g, `{${COLORS.codeFg}-fg}$1{/${COLORS.codeFg}-fg}`);
81
- // Bold: **text**
82
- result = result.replace(/\*\*([^*]+)\*\*/g, `{bold}$1{/bold}`);
83
- // Italic: *text*
84
- result = result.replace(/\*([^*]+)\*/g, `{${COLORS.h3Fg}-fg}$1{/${COLORS.h3Fg}-fg}`);
85
-
86
- return result;
87
- }
88
-
89
- /**
90
- * Render full markdown content to blessed tagged multi-line string.
91
- * @param {string} markdown
92
- * @returns {string}
93
- */
94
- export function renderMarkdown(markdown) {
95
- return markdown
96
- .split('\n')
97
- .map(line => renderMarkdownLine(line))
98
- .join('\n');
99
- }
100
-
101
- // ---------------------------------------------------------------------------
102
-
103
- function createTestStub() {
104
- return {
105
- box: {},
106
- show: () => {},
107
- hide: () => {},
108
- onFocus: () => {},
109
- onBlur: () => {},
110
- getFooterText: () => _FOOTER_TEXT_EN,
111
- getFooterColor: () => COLORS.footerBg,
112
- };
113
- }
114
-
115
- // ---------------------------------------------------------------------------
116
-
117
- /**
118
- * Create the Readme tab component.
119
- *
120
- * @param {object} screen - Blessed screen instance
121
- * @param {object} services
122
- * @returns {{ box, show, hide, onFocus, onBlur, getFooterText, getFooterColor }}
123
- */
124
- export function createReadmeTab(screen, services) {
125
- if (IS_TEST) return createTestStub();
126
-
127
- const { focusMainTabBar, languageService } = services;
128
- const _tl = (key) => languageService ? languageService.t(key) : t('en', key);
129
-
130
- // -------------------------------------------------------------------------
131
- // Container
132
-
133
- const box = blessed.box({
134
- parent: screen,
135
- top: 5,
136
- left: 0,
137
- width: '100%',
138
- bottom: 2,
139
- hidden: true,
140
- style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
141
- border: { type: 'line' },
142
- borderStyle: { fg: COLORS.borderFg },
143
- });
144
-
145
- // -------------------------------------------------------------------------
146
- // Load README.md
147
-
148
- function _loadReadme() {
149
- // Package root — works whether installed globally (node_modules/.bin) or run from source
150
- const pkgRoot = path.resolve(new URL(import.meta.url).pathname, '..', '..', '..', '..');
151
- const candidates = [
152
- path.resolve(process.cwd(), 'README.md'),
153
- path.resolve(process.cwd(), 'readme.md'),
154
- path.resolve(pkgRoot, 'README.md'), // AgentVibes package README fallback
155
- ];
156
- for (const p of candidates) {
157
- if (fs.existsSync(p)) {
158
- try {
159
- return fs.readFileSync(p, 'utf8');
160
- } catch {
161
- // Unreadable
162
- }
163
- }
164
- }
165
- return `# README\n\n${_tl('readmeNotFound')}`;
166
- }
167
-
168
- // -------------------------------------------------------------------------
169
- // Scrollable content
170
-
171
- const scrollBox = blessed.box({
172
- parent: box,
173
- top: 1,
174
- left: 2,
175
- width: '96%',
176
- bottom: 4,
177
- scrollable: true,
178
- alwaysScroll: true,
179
- tags: true,
180
- keys: true,
181
- vi: true,
182
- mouse: true,
183
- scrollbar: { ch: '│', style: { fg: COLORS.borderFg } },
184
- content: '',
185
- style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
186
- });
187
-
188
- // Scroll indicator
189
- const scrollIndicator = blessed.text({
190
- parent: box,
191
- bottom: 2,
192
- right: 2,
193
- content: '',
194
- tags: true,
195
- style: { fg: COLORS.quoteFg, bg: COLORS.contentBg },
196
- });
197
-
198
- // -------------------------------------------------------------------------
199
- // Render
200
-
201
- function refreshContent() {
202
- const markdown = _loadReadme();
203
- const rendered = renderMarkdown(markdown);
204
- scrollBox.setContent(rendered);
205
- scrollIndicator.setContent(`{#607d8b-fg}${_tl('readmeScrollMore')}{/#607d8b-fg}`);
206
- screen.render();
207
- }
208
-
209
- // Scroll events update indicator
210
- scrollBox.on('scroll', () => {
211
- const atBottom = scrollBox.getScrollPerc() >= 99;
212
- scrollIndicator.setContent(
213
- atBottom ? '' : `{#607d8b-fg}${_tl('readmeScrollMore')}{/#607d8b-fg}`
214
- );
215
- screen.render();
216
- });
217
-
218
- // PgUp/PgDn
219
- scrollBox.key(['pageup'], () => {
220
- scrollBox.scroll(-10);
221
- screen.render();
222
- });
223
- scrollBox.key(['pagedown'], () => {
224
- scrollBox.scroll(10);
225
- screen.render();
226
- });
227
-
228
- // [↑] at top of content → jump to main header tab bar
229
- scrollBox.key(['up'], () => {
230
- if (scrollBox.getScroll() === 0 && typeof focusMainTabBar === 'function') {
231
- focusMainTabBar();
232
- }
233
- });
234
-
235
- // Escape → return to header tab bar
236
- scrollBox.key(['escape'], () => {
237
- if (typeof focusMainTabBar === 'function') { focusMainTabBar(); screen.render(); }
238
- });
239
-
240
- // -------------------------------------------------------------------------
241
- // Tab Component Contract
242
-
243
- return {
244
- box,
245
-
246
- show() {
247
- box.show();
248
- refreshContent();
249
- screen.render();
250
- },
251
-
252
- hide() {
253
- box.hide();
254
- screen.render();
255
- },
256
-
257
- onFocus() {
258
- scrollBox.focus();
259
- screen.render();
260
- },
261
-
262
- onBlur() {},
263
-
264
- getFooterText() {
265
- return _tl('readmeFooter');
266
- },
267
-
268
- getFooterColor() {
269
- return COLORS.footerBg;
270
- },
271
- };
272
- }
1
+ /**
2
+ * AgentVibes TUI Console — Readme Tab
3
+ * Epic 13: Story 13.2
4
+ *
5
+ * Implements the Tab Component Contract:
6
+ * createReadmeTab(screen, services) → { box, show, hide, onFocus, onBlur, getFooterText, getFooterColor }
7
+ *
8
+ * Features: renders README.md with styled markdown, scrolling, search.
9
+ */
10
+
11
+ import fs from 'node:fs';
12
+ import path from 'node:path';
13
+ import { t } from '../../i18n/strings.js';
14
+
15
+ const IS_TEST = process.env.AGENTVIBES_TEST_MODE === 'true';
16
+
17
+ let blessed;
18
+ if (!IS_TEST) {
19
+ const { default: b } = await import('blessed');
20
+ blessed = b;
21
+ }
22
+
23
+ // ---------------------------------------------------------------------------
24
+
25
+ const COLORS = {
26
+ contentBg: '#0a0e1a',
27
+ h1Fg: '#69f0ae', // Green — H1
28
+ h2Fg: '#82b1ff', // Blue — H2
29
+ h3Fg: '#80d8ff', // Light blue — H3
30
+ codeFg: '#ffff00', // Yellow — code spans
31
+ boldFg: '#e3f2fd', // Bright — bold text
32
+ quoteFg: '#90a4ae', // Gray — blockquotes
33
+ labelFg: '#e3f2fd',
34
+ borderFg: '#455a64',
35
+ footerBg: '#455a64', // Dark gray — Readme tab footer
36
+ };
37
+
38
+ const _FOOTER_TEXT_EN = '[↑↓/jk] Scroll [PgUp/PgDn] Page [/] Search [S/V/M/A/R] Tab [Q] Quit';
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Markdown renderer (story 13.2)
42
+
43
+ /**
44
+ * Render a single markdown line to blessed tagged string.
45
+ * Handles: H1/H2/H3, bold (**), code spans (`), blockquotes (>), plain text.
46
+ *
47
+ * @param {string} line
48
+ * @returns {string}
49
+ */
50
+ export function renderMarkdownLine(line) {
51
+ // H1
52
+ if (/^# /.test(line)) {
53
+ const text = line.replace(/^# /, '');
54
+ return `{bold}{${COLORS.h1Fg}-fg}${text}{/${COLORS.h1Fg}-fg}{/bold}`;
55
+ }
56
+ // H2
57
+ if (/^## /.test(line)) {
58
+ const text = line.replace(/^## /, '');
59
+ return `{bold}{${COLORS.h2Fg}-fg}${text}{/${COLORS.h2Fg}-fg}{/bold}`;
60
+ }
61
+ // H3
62
+ if (/^### /.test(line)) {
63
+ const text = line.replace(/^### /, '');
64
+ return `{${COLORS.h3Fg}-fg}${text}{/${COLORS.h3Fg}-fg}`;
65
+ }
66
+ // Blockquote
67
+ if (/^> /.test(line)) {
68
+ const text = line.replace(/^> /, '');
69
+ return `{${COLORS.quoteFg}-fg}│ ${text}{/${COLORS.quoteFg}-fg}`;
70
+ }
71
+ // Horizontal rule
72
+ if (/^---+$/.test(line.trim())) {
73
+ return `{${COLORS.quoteFg}-fg}${'─'.repeat(66)}{/${COLORS.quoteFg}-fg}`;
74
+ }
75
+
76
+ // Inline: code spans, bold
77
+ let result = line;
78
+ // Escape existing blessed tags (literal braces in content)
79
+ // Code spans: `text`
80
+ result = result.replace(/`([^`]+)`/g, `{${COLORS.codeFg}-fg}$1{/${COLORS.codeFg}-fg}`);
81
+ // Bold: **text**
82
+ result = result.replace(/\*\*([^*]+)\*\*/g, `{bold}$1{/bold}`);
83
+ // Italic: *text*
84
+ result = result.replace(/\*([^*]+)\*/g, `{${COLORS.h3Fg}-fg}$1{/${COLORS.h3Fg}-fg}`);
85
+
86
+ return result;
87
+ }
88
+
89
+ /**
90
+ * Render full markdown content to blessed tagged multi-line string.
91
+ * @param {string} markdown
92
+ * @returns {string}
93
+ */
94
+ export function renderMarkdown(markdown) {
95
+ return markdown
96
+ .split('\n')
97
+ .map(line => renderMarkdownLine(line))
98
+ .join('\n');
99
+ }
100
+
101
+ // ---------------------------------------------------------------------------
102
+
103
+ function createTestStub() {
104
+ return {
105
+ box: {},
106
+ show: () => {},
107
+ hide: () => {},
108
+ onFocus: () => {},
109
+ onBlur: () => {},
110
+ getFooterText: () => _FOOTER_TEXT_EN,
111
+ getFooterColor: () => COLORS.footerBg,
112
+ };
113
+ }
114
+
115
+ // ---------------------------------------------------------------------------
116
+
117
+ /**
118
+ * Create the Readme tab component.
119
+ *
120
+ * @param {object} screen - Blessed screen instance
121
+ * @param {object} services
122
+ * @returns {{ box, show, hide, onFocus, onBlur, getFooterText, getFooterColor }}
123
+ */
124
+ export function createReadmeTab(screen, services) {
125
+ if (IS_TEST) return createTestStub();
126
+
127
+ const { focusMainTabBar, languageService } = services;
128
+ const _tl = (key) => languageService ? languageService.t(key) : t('en', key);
129
+
130
+ // -------------------------------------------------------------------------
131
+ // Container
132
+
133
+ const box = blessed.box({
134
+ parent: screen,
135
+ top: 5,
136
+ left: 0,
137
+ width: '100%',
138
+ bottom: 2,
139
+ hidden: true,
140
+ style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
141
+ border: { type: 'line' },
142
+ borderStyle: { fg: COLORS.borderFg },
143
+ });
144
+
145
+ // -------------------------------------------------------------------------
146
+ // Load README.md
147
+
148
+ function _loadReadme() {
149
+ // Package root — works whether installed globally (node_modules/.bin) or run from source
150
+ const pkgRoot = path.resolve(new URL(import.meta.url).pathname, '..', '..', '..', '..');
151
+ const candidates = [
152
+ path.resolve(process.cwd(), 'README.md'),
153
+ path.resolve(process.cwd(), 'readme.md'),
154
+ path.resolve(pkgRoot, 'README.md'), // AgentVibes package README fallback
155
+ ];
156
+ for (const p of candidates) {
157
+ if (fs.existsSync(p)) {
158
+ try {
159
+ return fs.readFileSync(p, 'utf8');
160
+ } catch {
161
+ // Unreadable
162
+ }
163
+ }
164
+ }
165
+ return `# README\n\n${_tl('readmeNotFound')}`;
166
+ }
167
+
168
+ // -------------------------------------------------------------------------
169
+ // Scrollable content
170
+
171
+ const scrollBox = blessed.box({
172
+ parent: box,
173
+ top: 1,
174
+ left: 2,
175
+ width: '96%',
176
+ bottom: 4,
177
+ scrollable: true,
178
+ alwaysScroll: true,
179
+ tags: true,
180
+ keys: true,
181
+ vi: true,
182
+ mouse: true,
183
+ scrollbar: { ch: '│', style: { fg: COLORS.borderFg } },
184
+ content: '',
185
+ style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
186
+ });
187
+
188
+ // Scroll indicator
189
+ const scrollIndicator = blessed.text({
190
+ parent: box,
191
+ bottom: 2,
192
+ right: 2,
193
+ content: '',
194
+ tags: true,
195
+ style: { fg: COLORS.quoteFg, bg: COLORS.contentBg },
196
+ });
197
+
198
+ // -------------------------------------------------------------------------
199
+ // Render
200
+
201
+ function refreshContent() {
202
+ const markdown = _loadReadme();
203
+ const rendered = renderMarkdown(markdown);
204
+ scrollBox.setContent(rendered);
205
+ scrollIndicator.setContent(`{#607d8b-fg}${_tl('readmeScrollMore')}{/#607d8b-fg}`);
206
+ screen.render();
207
+ }
208
+
209
+ // Scroll events update indicator
210
+ scrollBox.on('scroll', () => {
211
+ const atBottom = scrollBox.getScrollPerc() >= 99;
212
+ scrollIndicator.setContent(
213
+ atBottom ? '' : `{#607d8b-fg}${_tl('readmeScrollMore')}{/#607d8b-fg}`
214
+ );
215
+ screen.render();
216
+ });
217
+
218
+ // PgUp/PgDn
219
+ scrollBox.key(['pageup'], () => {
220
+ scrollBox.scroll(-10);
221
+ screen.render();
222
+ });
223
+ scrollBox.key(['pagedown'], () => {
224
+ scrollBox.scroll(10);
225
+ screen.render();
226
+ });
227
+
228
+ // [↑] at top of content → jump to main header tab bar
229
+ scrollBox.key(['up'], () => {
230
+ if (scrollBox.getScroll() === 0 && typeof focusMainTabBar === 'function') {
231
+ focusMainTabBar();
232
+ }
233
+ });
234
+
235
+ // Escape → return to header tab bar
236
+ scrollBox.key(['escape'], () => {
237
+ if (typeof focusMainTabBar === 'function') { focusMainTabBar(); screen.render(); }
238
+ });
239
+
240
+ // -------------------------------------------------------------------------
241
+ // Tab Component Contract
242
+
243
+ return {
244
+ box,
245
+
246
+ show() {
247
+ box.show();
248
+ refreshContent();
249
+ screen.render();
250
+ },
251
+
252
+ hide() {
253
+ box.hide();
254
+ screen.render();
255
+ },
256
+
257
+ onFocus() {
258
+ scrollBox.focus();
259
+ screen.render();
260
+ },
261
+
262
+ onBlur() {},
263
+
264
+ getFooterText() {
265
+ return _tl('readmeFooter');
266
+ },
267
+
268
+ getFooterColor() {
269
+ return COLORS.footerBg;
270
+ },
271
+ };
272
+ }
@@ -21,7 +21,7 @@ import crypto from 'node:crypto';
21
21
  import { spawn } from 'node:child_process';
22
22
  import { fileURLToPath } from 'node:url';
23
23
  import {
24
- scanInstalledVoices, getVoiceMeta, genderIconTag, PIPER_VOICES_DIR, SAMPLE_PHRASES, parseMultiSpeaker,
24
+ scanInstalledVoices, getVoiceMeta, previewPhrase, genderIconTag, PIPER_VOICES_DIR, parseMultiSpeaker,
25
25
  } from './voices-tab.js';
26
26
  import { voicesForProvider } from '../../services/provider-voice-catalog.js';
27
27
  import { LanguageService } from '../../services/language-service.js';
@@ -210,9 +210,12 @@ export function createSettingsTab(screen, services) {
210
210
  if (dst === 'remote') {
211
211
  const alias = configService?.getConfig?.()?.audio_ssh_alias ?? '';
212
212
  const mode = configService?.getConfig?.()?.audio_stream_mode ?? 'text';
213
- return `Remote (${alias || 'no alias'}) ${mode === 'pulse' ? 'PulseAudio' : 'Text Only'}`;
213
+ // Remote value in REDaudio leaves this machine (a deliberate,
214
+ // "here be dragons" destination). Rendered raw (see _refreshValues).
215
+ return `{red-fg}Remote (${alias || 'no alias'}) — ${mode === 'pulse' ? 'PulseAudio' : 'Text Only'}{/red-fg}`;
214
216
  }
215
- return 'Local';
217
+ // Local value in GREEN — audio plays here, the safe default.
218
+ return '{green-fg}Local{/green-fg}';
216
219
  },
217
220
  desc: 'Play audio locally or stream to a remote host via SSH',
218
221
  },
@@ -360,6 +363,10 @@ export function createSettingsTab(screen, services) {
360
363
  const val = w.setting.getValue();
361
364
  if (w.setting.isAction) {
362
365
  w.valueWidget.setContent('');
366
+ } else if (w.setting.key === 'audioDst') {
367
+ // audioDst supplies its own colour (green=Local / red=Remote); don't
368
+ // wrap it in the default yellow.
369
+ w.valueWidget.setContent(val);
363
370
  } else {
364
371
  w.valueWidget.setContent(`{yellow-fg}${val}{/yellow-fg}`);
365
372
  }
@@ -817,7 +824,8 @@ export function createSettingsTab(screen, services) {
817
824
  if (_previewVoiceId === voiceId) { _killVP(); vpPreviewLine.setContent(''); _refreshVP(); return; }
818
825
  _killVP();
819
826
 
820
- const phrase = SAMPLE_PHRASES[Math.floor(Math.random() * SAMPLE_PHRASES.length)]; // NOSONAR
827
+ // Announce the voice + its engine, consistent with every other picker.
828
+ const phrase = previewPhrase(voiceId);
821
829
 
822
830
  if (_isWin) {
823
831
  // Which engine is being previewed? (Default TTS Engine selector.)
@@ -854,7 +862,8 @@ export function createSettingsTab(screen, services) {
854
862
  _refreshVP();
855
863
  }
856
864
  _previewProc = spawn('powershell', _psArgs, // NOSONAR
857
- { stdio: 'ignore', detached: false, windowsHide: true, env: _spawnEnv });
865
+ { stdio: 'ignore', detached: false, windowsHide: true,
866
+ env: { ..._spawnEnv, AGENTVIBES_VOICE_SOURCE: 'audition' } });
858
867
  _previewProc.on('exit', () => {
859
868
  if (_previewVoiceId === voiceId) {
860
869
  _previewVoiceId = null;
@@ -874,10 +883,26 @@ export function createSettingsTab(screen, services) {
874
883
  const args = [playTtsScript, phrase, voiceId];
875
884
  if (remoteLlm) args.push('--llm', remoteLlm);
876
885
 
886
+ // Mark this as an AUDITION so the resolver keeps the EXACT voice we're
887
+ // previewing (F1) instead of demoting it to the per-LLM/provider voice —
888
+ // essential for engines whose ids aren't self-identifying (SAPI/macOS).
889
+ // Also force the receiver engine to the previewed voice's engine for this
890
+ // one send, so a SAPI/macOS voice actually renders in its own engine.
891
+ let _pvEngine = configService?.getConfig?.()?.ttsEngine
892
+ || providerService?.getActiveProvider?.() || 'piper';
893
+ // Guard: a transport (ssh-remote/agentvibes-receiver) is not an engine —
894
+ // fall back to piper so we never send the receiver a transport id as its
895
+ // engine (Fable review; mirrors voices-tab).
896
+ if (_pvEngine === 'ssh-remote' || _pvEngine === 'agentvibes-receiver') _pvEngine = 'piper';
877
897
  _previewProc = spawn('bash', args, { // NOSONAR
878
898
  stdio: 'ignore',
879
899
  detached: true,
880
- env: { ..._spawnEnv, CLAUDE_PROJECT_DIR: _projectRoot },
900
+ env: {
901
+ ..._spawnEnv,
902
+ CLAUDE_PROJECT_DIR: _projectRoot,
903
+ AGENTVIBES_VOICE_SOURCE: 'audition',
904
+ AGENTVIBES_RECEIVER_PROVIDER_OVERRIDE: receiverProviderId(_pvEngine),
905
+ },
881
906
  cwd: _projectRoot,
882
907
  });
883
908
  _previewVoiceId = voiceId;