agentvibes 5.11.2 → 5.12.0-alpha.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 (97) hide show
  1. package/.agentvibes/config.json +1 -15
  2. package/.claude/activation-instructions +54 -54
  3. package/.claude/commands/agent-vibes-bmad-voices.md +117 -117
  4. package/.claude/commands/agent-vibes-rdp.md +24 -24
  5. package/.claude/config/audio-effects.cfg +5 -5
  6. package/.claude/config/background-music-enabled.txt +1 -1
  7. package/.claude/docs/TERMUX_SETUP.md +408 -408
  8. package/.claude/github-star-reminder.txt +1 -1
  9. package/.claude/hooks/audio-processor.sh +1 -1
  10. package/.claude/hooks/background-music-manager.sh +2 -1
  11. package/.claude/hooks/bmad-speak-enhanced.sh +37 -22
  12. package/.claude/hooks/bmad-speak.sh +26 -8
  13. package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +1 -1
  14. package/.claude/hooks/play-tts-kokoro.sh +9 -0
  15. package/.claude/hooks/play-tts-piper.sh +13 -1
  16. package/.claude/hooks/play-tts-ssh-remote.sh +78 -5
  17. package/.claude/hooks/play-tts-termux-ssh.sh +1 -1
  18. package/.claude/hooks/play-tts.sh +85 -4
  19. package/.claude/hooks-windows/background-music-manager.ps1 +2 -1
  20. package/.claude/hooks-windows/play-tts-kokoro.ps1 +14 -0
  21. package/.claude/hooks-windows/play-tts.ps1 +163 -30
  22. package/.claude/hooks-windows/voice-manager-windows.ps1 +101 -1
  23. package/.clawdbot/README.md +105 -105
  24. package/LICENSE +190 -190
  25. package/RELEASE_NOTES.md +29 -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/mcp-server.sh +6 -19
  30. package/bin/resolve-utterance.js +137 -0
  31. package/bin/test-bmad-pr +78 -78
  32. package/mcp-server/QUICK_START.md +203 -203
  33. package/mcp-server/README.md +345 -345
  34. package/mcp-server/examples/claude_desktop_config.json +11 -11
  35. package/mcp-server/examples/claude_desktop_config_piper.json +9 -9
  36. package/mcp-server/examples/custom_instructions.md +169 -169
  37. package/mcp-server/pyproject.toml +52 -52
  38. package/mcp-server/requirements.txt +2 -2
  39. package/mcp-server/server.py +280 -99
  40. package/mcp-server/test_mcp_correctness.py +486 -0
  41. package/mcp-server/test_server.py +395 -395
  42. package/mcp-server/test_windows_script_parity.py +341 -336
  43. package/package.json +3 -2
  44. package/setup-windows.ps1 +867 -815
  45. package/src/cli/list-personalities.js +110 -110
  46. package/src/cli/list-voices.js +114 -114
  47. package/src/commands/bmad-voices.js +394 -394
  48. package/src/console/app.js +13 -2
  49. package/src/console/brand-colors.js +13 -13
  50. package/src/console/constants/personalities.js +44 -44
  51. package/src/console/tabs/agents-tab.js +82 -9
  52. package/src/console/tabs/help-tab.js +314 -314
  53. package/src/console/tabs/music-tab.js +7 -1
  54. package/src/console/tabs/readme-tab.js +272 -272
  55. package/src/console/tabs/settings-tab.js +29 -1
  56. package/src/console/tabs/setup-tab.js +4 -4
  57. package/src/console/widgets/destroy-list.js +25 -25
  58. package/src/console/widgets/notice.js +55 -55
  59. package/src/console/widgets/track-picker.js +3 -3
  60. package/src/i18n/de.js +0 -1
  61. package/src/i18n/en.js +0 -1
  62. package/src/i18n/es.js +0 -1
  63. package/src/i18n/fr.js +0 -1
  64. package/src/i18n/hi.js +0 -1
  65. package/src/i18n/ja.js +0 -1
  66. package/src/i18n/ko.js +0 -1
  67. package/src/i18n/pt.js +0 -1
  68. package/src/i18n/zh-CN.js +0 -1
  69. package/src/installer/language-screen.js +31 -31
  70. package/src/installer/music-file-input.js +304 -304
  71. package/src/installer.js +205 -15
  72. package/src/services/config-service.js +264 -264
  73. package/src/services/language-service.js +47 -47
  74. package/src/services/llm-provider-service.js +7 -7
  75. package/src/services/utterance-loader.js +280 -0
  76. package/src/services/utterance-resolver.js +526 -0
  77. package/src/utils/audio-format-validator.js +277 -277
  78. package/src/utils/dependency-checker.js +469 -469
  79. package/src/utils/file-ownership-verifier.js +358 -358
  80. package/src/utils/music-file-validator.js +285 -285
  81. package/src/utils/preview-list-prompt.js +144 -144
  82. package/src/utils/secure-music-storage.js +412 -412
  83. package/templates/agentvibes-receiver.sh +74 -12
  84. package/voice-assignments.json +8245 -0
  85. package/.agentvibes/install-manifest.json +0 -342
  86. package/.claude/config/background-music-position.txt +0 -27
  87. package/.claude/config/background-music-volume.txt +0 -1
  88. package/.claude/config/background-music.cfg +0 -1
  89. package/.claude/config/background-music.txt +0 -1
  90. package/.claude/config/language.txt +0 -1
  91. package/.claude/config/personality.txt +0 -1
  92. package/.claude/config/tts-speech-rate.txt +0 -1
  93. package/.claude/config/tts-verbosity.txt +0 -1
  94. package/.claude/hooks-windows/audio-cache-utils.ps1.user.bak +0 -119
  95. package/.claude/hooks-windows/soprano-gradio-synth.py.user.bak +0 -153
  96. package/.claude/piper-voices-dir.txt +0 -1
  97. package/.mcp.json +0 -22
@@ -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
+ }
@@ -127,6 +127,9 @@ export function createSettingsTab(screen, services) {
127
127
 
128
128
  const { configService, providerService, navigationService, focusMainTabBar, languageService } = services;
129
129
 
130
+ // Capture cwd once at construction (matches agents-tab.js pattern)
131
+ const _projectRoot = process.cwd();
132
+
130
133
  // ── Container ────────────────────────────────────────────────────────────
131
134
 
132
135
  const box = blessed.box({
@@ -646,6 +649,31 @@ export function createSettingsTab(screen, services) {
646
649
  _killVP();
647
650
 
648
651
  const phrase = SAMPLE_PHRASES[Math.floor(Math.random() * SAMPLE_PHRASES.length)]; // NOSONAR
652
+
653
+ if (_isWin) {
654
+ // Windows: route through play-tts.ps1 (same pattern as non-Windows bash route)
655
+ const playTtsScript = path.join(_projectRoot, '.claude', 'hooks-windows', 'play-tts.ps1');
656
+ if (!fs.existsSync(playTtsScript)) return;
657
+ _previewVoiceId = voiceId;
658
+ if (!_vpClosed) {
659
+ vpPreviewLine.setContent(`{cyan-fg}♪ Playing: ${voiceId}...{/cyan-fg}`);
660
+ _refreshVP();
661
+ }
662
+ _previewProc = spawn('powershell', [ // NOSONAR
663
+ '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', playTtsScript, phrase, voiceId,
664
+ ], { stdio: 'ignore', detached: false, windowsHide: true, env: _spawnEnv });
665
+ _previewProc.on('exit', () => {
666
+ if (_previewVoiceId === voiceId) {
667
+ _previewVoiceId = null;
668
+ _previewProc = null;
669
+ if (!_vpClosed) { vpPreviewLine.setContent(''); _refreshVP(); }
670
+ }
671
+ });
672
+ _previewProc.on('error', () => { _previewProc = null; _previewVoiceId = null; });
673
+ return;
674
+ }
675
+
676
+ // Non-Windows: use bash play-tts.sh
649
677
  const playTtsScript = path.join(_projectRoot, '.claude', 'hooks', 'play-tts.sh');
650
678
  if (!fs.existsSync(playTtsScript)) return;
651
679
 
@@ -656,7 +684,7 @@ export function createSettingsTab(screen, services) {
656
684
  _previewProc = spawn('bash', args, { // NOSONAR
657
685
  stdio: 'ignore',
658
686
  detached: true,
659
- env: _spawnEnv,
687
+ env: { ..._spawnEnv, CLAUDE_PROJECT_DIR: _projectRoot },
660
688
  cwd: _projectRoot,
661
689
  });
662
690
  _previewVoiceId = voiceId;
@@ -1144,7 +1144,7 @@ export function createSetupTab(screen, services) {
1144
1144
  pretext: llmConfig.pretext || '',
1145
1145
  reverbPreset: llmConfig.effects || 'off',
1146
1146
  bgTrack: llmConfig.bgTrack || '',
1147
- bgVolume: llmConfig.bgVolume || '0.15',
1147
+ bgVolume: llmConfig.bgVolume || '0.20',
1148
1148
  // Hermes SSH fields
1149
1149
  mode: currentCfg.mode === 'remote' ? 'remote' : 'local',
1150
1150
  sshKey: currentCfg.sshKey || '',
@@ -1330,7 +1330,7 @@ export function createSetupTab(screen, services) {
1330
1330
  const previewBtn = _modalBtn('Preview', 4, _playPreview);
1331
1331
  const resetBtn = _modalBtn('Reset', 18, () => {
1332
1332
  draft.ttsEngine = ''; draft.voice = ''; draft.pretext = '';
1333
- draft.reverbPreset = 'off'; draft.bgTrack = ''; draft.bgVolume = '0.15';
1333
+ draft.reverbPreset = 'off'; draft.bgTrack = ''; draft.bgVolume = '0.20';
1334
1334
  _autoSave();
1335
1335
  fieldList.setItems(_fieldItems());
1336
1336
  fieldList.focus();
@@ -2036,7 +2036,7 @@ export function createSetupTab(screen, services) {
2036
2036
  pretext: config.pretext || defaultPretext[llmKey] || '',
2037
2037
  reverbPreset: config.effects || 'off',
2038
2038
  bgTrack: config.bgTrack || '',
2039
- bgVolume: config.bgVolume || '0.15',
2039
+ bgVolume: config.bgVolume || '0.20',
2040
2040
  // SSH destination fields
2041
2041
  mode: sshCfg.mode === 'remote' ? 'remote' : 'local',
2042
2042
  sshKey: sshCfg.sshKey || '',
@@ -2329,7 +2329,7 @@ export function createSetupTab(screen, services) {
2329
2329
  draft.pretext = defaultPretext[llmKey] || '';
2330
2330
  draft.reverbPreset = 'off';
2331
2331
  draft.bgTrack = '';
2332
- draft.bgVolume = '0.15';
2332
+ draft.bgVolume = '0.20';
2333
2333
  draft.mode = 'local';
2334
2334
  draft.connType = 'manual';
2335
2335
  draft.sshKey = '';