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,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
+ }
@@ -725,7 +725,7 @@ export function createSetupTab(screen, services) {
725
725
  // Guard against double-open (key repeat, double-click)
726
726
  if (navigationService?.isModalOpen()) return;
727
727
  let _closed = false;
728
- navigationService?.openModal();
728
+ navigationService?.openModal(null, _closeModal);
729
729
 
730
730
  const defaultPretext = {
731
731
  'claude-code': 'Claude Code here',
@@ -1035,7 +1035,7 @@ export function createSetupTab(screen, services) {
1035
1035
  }
1036
1036
  });
1037
1037
 
1038
- fieldList.key(['escape'], _closeModal);
1038
+ fieldList.key(['escape', 'q', 'Q'], _closeModal);
1039
1039
 
1040
1040
  // Remove selection highlight when field list loses focus
1041
1041
  fieldList.on('blur', () => {
@@ -1081,14 +1081,14 @@ export function createSetupTab(screen, services) {
1081
1081
  allBtns[(i - 1 + allBtns.length) % allBtns.length].focus();
1082
1082
  screen.render();
1083
1083
  });
1084
- allBtns[i].key(['escape'], _closeModal);
1084
+ allBtns[i].key(['escape', 'q', 'Q'], _closeModal);
1085
1085
  allBtns[i].key(['up'], () => {
1086
1086
  fieldList.focus();
1087
1087
  screen.render();
1088
1088
  });
1089
1089
  }
1090
1090
 
1091
- modal.key(['escape'], _closeModal);
1091
+ modal.key(['escape', 'q', 'Q'], _closeModal);
1092
1092
  fieldList.focus();
1093
1093
  screen.render();
1094
1094
  }
@@ -1096,7 +1096,12 @@ export function createSetupTab(screen, services) {
1096
1096
  // ── TTS Engine picker (for config modal) ──────────────────────────────────
1097
1097
 
1098
1098
  function _openTtsEnginePicker(draft, onDone) {
1099
- navigationService?.openModal();
1099
+ function _closePicker() {
1100
+ navigationService?.closeModal();
1101
+ destroyList(picker, screen);
1102
+ onDone();
1103
+ }
1104
+ navigationService?.openModal(null, _closePicker);
1100
1105
 
1101
1106
  const engines = getEngineStatuses();
1102
1107
  const items = engines.map(e => {
@@ -1136,16 +1141,10 @@ export function createSetupTab(screen, services) {
1136
1141
  } else {
1137
1142
  draft.ttsEngine = engines[idx - 1].id;
1138
1143
  }
1139
- navigationService?.closeModal();
1140
- destroyList(picker, screen);
1141
- onDone();
1144
+ _closePicker();
1142
1145
  });
1143
1146
 
1144
- picker.key(['escape'], () => {
1145
- navigationService?.closeModal();
1146
- destroyList(picker, screen);
1147
- onDone();
1148
- });
1147
+ picker.key(['escape', 'q', 'Q'], _closePicker);
1149
1148
 
1150
1149
  picker.focus();
1151
1150
  screen.render();
@@ -1162,7 +1161,7 @@ export function createSetupTab(screen, services) {
1162
1161
  }
1163
1162
 
1164
1163
  function _openVoicePickerForLlm(draft, onDone) {
1165
- navigationService?.openModal();
1164
+ navigationService?.openModal(null, _closeVP);
1166
1165
 
1167
1166
  let _allVoices = [];
1168
1167
  let _previewProc = null;
@@ -1355,7 +1354,16 @@ export function createSetupTab(screen, services) {
1355
1354
 
1356
1355
  piper.on('exit', (code) => {
1357
1356
  if (_previewVoiceId !== voiceId) { try { fs.unlinkSync(tempWav); } catch {} return; }
1358
- if (code !== 0) { _previewProc = null; _previewVoiceId = null; try { fs.unlinkSync(tempWav); } catch {} return; }
1357
+ if (code !== 0) {
1358
+ _previewProc = null; _previewVoiceId = null;
1359
+ if (!_vpClosed) {
1360
+ vpPreviewLine.setContent('{red-fg}♪ Preview failed — is Piper installed?{/red-fg}');
1361
+ screen.render();
1362
+ setTimeout(() => { if (!_vpClosed) { vpPreviewLine.setContent(''); screen.render(); } }, 4000);
1363
+ }
1364
+ try { fs.unlinkSync(tempWav); } catch {};
1365
+ return;
1366
+ }
1359
1367
  const wp = detectWavPlayer(_spawnEnv);
1360
1368
  if (!wp) return;
1361
1369
  const pp = spawn(wp.bin, wp.args(tempWav), {
@@ -1371,7 +1379,14 @@ export function createSetupTab(screen, services) {
1371
1379
  try { fs.unlinkSync(tempWav); } catch {}
1372
1380
  });
1373
1381
  });
1374
- piper.on('error', () => { _previewProc = null; _previewVoiceId = null; });
1382
+ piper.on('error', () => {
1383
+ _previewProc = null; _previewVoiceId = null;
1384
+ if (!_vpClosed) {
1385
+ vpPreviewLine.setContent('{red-fg}♪ Cannot find Piper — install it first{/red-fg}');
1386
+ screen.render();
1387
+ setTimeout(() => { if (!_vpClosed) { vpPreviewLine.setContent(''); screen.render(); } }, 4000);
1388
+ }
1389
+ });
1375
1390
  }
1376
1391
 
1377
1392
  vpList.key(['enter'], () => {
@@ -1390,7 +1405,7 @@ export function createSetupTab(screen, services) {
1390
1405
  const sel = _allVoices[vpList.selected];
1391
1406
  if (sel) { toggleThumbsDown(configService, sel); _refreshVP(); }
1392
1407
  });
1393
- vpList.key(['escape', 'q'], _closeVP);
1408
+ vpList.key(['escape', 'q', 'Q'], _closeVP);
1394
1409
 
1395
1410
  // PageUp / PageDown / Home / End navigation
1396
1411
  const _pageSize = () => Math.max(1, (vpList.height ?? 10) - 2);
@@ -1349,7 +1349,7 @@ export function createVoicesTab(screen, services) {
1349
1349
  okGlobalBtn.key(['left'], () => { okLocalBtn.focus(); screen.render(); });
1350
1350
  okLocalBtn.key(['left'], () => { previewBtn.focus(); screen.render(); });
1351
1351
 
1352
- modal.key(['escape', 'q'], _close);
1352
+ modal.key(['escape', 'q', 'Q'], _close);
1353
1353
 
1354
1354
  modal.setFront();
1355
1355
  okLocalBtn.focus();
@@ -1550,7 +1550,7 @@ export function createVoicesTab(screen, services) {
1550
1550
  dlBtn.key(['left'], () => { cancelBtn.focus(); screen.render(); });
1551
1551
  cancelBtn.key(['left'], () => { dlBtn.focus(); screen.render(); });
1552
1552
 
1553
- modal.key(['escape', 'q'], () => { if (!_downloading) _close(); });
1553
+ modal.key(['escape', 'q', 'Q'], () => { if (!_downloading) _close(); });
1554
1554
 
1555
1555
  modal.setFront();
1556
1556
  dlBtn.focus();