agentvibes 5.6.8 → 5.7.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 (128) hide show
  1. package/.agentvibes/config.json +2 -0
  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 +2 -2
  5. package/.claude/config/background-music-position.txt +0 -1
  6. package/.claude/docs/TERMUX_SETUP.md +408 -408
  7. package/.claude/github-star-reminder.txt +1 -1
  8. package/.claude/hooks/audio-cache-utils.sh +0 -0
  9. package/.claude/hooks/audio-processor.sh +0 -0
  10. package/.claude/hooks/background-music-manager.sh +0 -0
  11. package/.claude/hooks/bmad-party-manager.sh +225 -0
  12. package/.claude/hooks/bmad-party-speak.sh +0 -0
  13. package/.claude/hooks/bmad-speak-enhanced.sh +0 -0
  14. package/.claude/hooks/bmad-speak.sh +0 -0
  15. package/.claude/hooks/bmad-tts-injector.sh +49 -21
  16. package/.claude/hooks/bmad-voice-manager.sh +0 -0
  17. package/.claude/hooks/clawdbot-receiver-SECURE.sh +0 -0
  18. package/.claude/hooks/clawdbot-receiver.sh +0 -0
  19. package/.claude/hooks/clean-audio-cache.sh +0 -0
  20. package/.claude/hooks/cleanup-cache.sh +0 -0
  21. package/.claude/hooks/configure-rdp-mode.sh +0 -0
  22. package/.claude/hooks/download-extra-voices.sh +0 -0
  23. package/.claude/hooks/effects-manager.sh +0 -0
  24. package/.claude/hooks/github-star-reminder.sh +0 -0
  25. package/.claude/hooks/language-manager.sh +0 -0
  26. package/.claude/hooks/learn-manager.sh +0 -0
  27. package/.claude/hooks/macos-voice-manager.sh +0 -0
  28. package/.claude/hooks/migrate-background-music.sh +0 -0
  29. package/.claude/hooks/migrate-to-agentvibes.sh +0 -0
  30. package/.claude/hooks/optimize-background-music.sh +0 -0
  31. package/.claude/hooks/path-resolver.sh +0 -0
  32. package/.claude/hooks/personality-manager.sh +0 -0
  33. package/.claude/hooks/piper-download-voices.sh +0 -0
  34. package/.claude/hooks/piper-installer.sh +0 -0
  35. package/.claude/hooks/piper-multispeaker-registry.sh +0 -0
  36. package/.claude/hooks/piper-voice-manager.sh +0 -0
  37. package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +0 -0
  38. package/.claude/hooks/play-tts-enhanced.sh +0 -0
  39. package/.claude/hooks/play-tts-macos.sh +0 -0
  40. package/.claude/hooks/play-tts-piper.sh +1 -1
  41. package/.claude/hooks/play-tts-soprano.sh +0 -0
  42. package/.claude/hooks/play-tts-ssh-remote.sh +0 -0
  43. package/.claude/hooks/play-tts-termux-ssh.sh +0 -0
  44. package/.claude/hooks/play-tts-windows-receiver.sh +0 -0
  45. package/.claude/hooks/play-tts.sh +4 -0
  46. package/.claude/hooks/prepare-release.sh +0 -0
  47. package/.claude/hooks/provider-commands.sh +16 -4
  48. package/.claude/hooks/provider-manager.sh +38 -0
  49. package/.claude/hooks/replay-target-audio.sh +0 -0
  50. package/.claude/hooks/sentiment-manager.sh +0 -0
  51. package/.claude/hooks/session-start-tts.sh +0 -0
  52. package/.claude/hooks/soprano-gradio-synth.py +0 -0
  53. package/.claude/hooks/speed-manager.sh +0 -0
  54. package/.claude/hooks/stop-tts.sh +0 -0
  55. package/.claude/hooks/stop.sh +38 -0
  56. package/.claude/hooks/termux-installer.sh +0 -0
  57. package/.claude/hooks/translate-manager.sh +0 -0
  58. package/.claude/hooks/translator.py +0 -0
  59. package/.claude/hooks/tts-queue-worker.sh +0 -0
  60. package/.claude/hooks/tts-queue.sh +0 -0
  61. package/.claude/hooks/verbosity-manager.sh +0 -0
  62. package/.claude/hooks/voice-manager.sh +50 -2
  63. package/.claude/hooks-windows/audio-cache-utils.ps1 +119 -119
  64. package/.claude/hooks-windows/play-tts.ps1 +34 -1
  65. package/.claude/hooks-windows/tts-watcher.ps1 +122 -0
  66. package/.claude/piper-voices-dir.txt +1 -0
  67. package/.clawdbot/README.md +105 -105
  68. package/.mcp.json +14 -5
  69. package/README.md +10 -2
  70. package/RELEASE_NOTES.md +61 -0
  71. package/WINDOWS-SETUP.md +208 -208
  72. package/bin/agent-vibes +39 -39
  73. package/bin/agentvibes-voice-browser.js +59 -4
  74. package/bin/agentvibes.js +0 -0
  75. package/bin/mcp-server.js +121 -121
  76. package/bin/mcp-server.sh +0 -0
  77. package/bin/test-bmad-pr +78 -78
  78. package/mcp-server/QUICK_START.md +203 -203
  79. package/mcp-server/README.md +345 -345
  80. package/mcp-server/WINDOWS_SETUP.md +260 -260
  81. package/mcp-server/docs/troubleshooting-audio.md +313 -313
  82. package/mcp-server/examples/claude_desktop_config.json +11 -11
  83. package/mcp-server/examples/claude_desktop_config_piper.json +9 -9
  84. package/mcp-server/examples/custom_instructions.md +169 -169
  85. package/mcp-server/install-deps.js +177 -130
  86. package/mcp-server/server.py +1797 -1787
  87. package/mcp-server/test_server.py +0 -0
  88. package/package.json +1 -1
  89. package/src/bmad-detector.js +85 -71
  90. package/src/cli/list-personalities.js +110 -110
  91. package/src/cli/list-voices.js +114 -114
  92. package/src/commands/bmad-voices.js +394 -394
  93. package/src/commands/install-mcp.js +476 -476
  94. package/src/console/brand-colors.js +13 -13
  95. package/src/console/constants/personalities.js +44 -44
  96. package/src/console/tabs/help-tab.js +314 -314
  97. package/src/console/tabs/music-tab.js +18 -2
  98. package/src/console/tabs/readme-tab.js +272 -272
  99. package/src/console/widgets/destroy-list.js +25 -25
  100. package/src/console/widgets/notice.js +55 -55
  101. package/src/console/widgets/personality-picker.js +213 -213
  102. package/src/i18n/de.js +202 -202
  103. package/src/i18n/es.js +202 -202
  104. package/src/i18n/fr.js +202 -202
  105. package/src/i18n/hi.js +202 -202
  106. package/src/i18n/ja.js +202 -202
  107. package/src/i18n/ko.js +202 -202
  108. package/src/i18n/pt.js +202 -202
  109. package/src/i18n/strings.js +54 -54
  110. package/src/i18n/zh-CN.js +202 -202
  111. package/src/installer/language-screen.js +31 -31
  112. package/src/installer/music-file-input.js +304 -304
  113. package/src/installer.js +70 -7
  114. package/src/services/agent-voice-store.js +59 -12
  115. package/src/services/config-service.js +264 -264
  116. package/src/services/language-service.js +47 -47
  117. package/src/services/provider-service.js +143 -143
  118. package/src/utils/audio-duration-validator.js +298 -298
  119. package/src/utils/audio-format-validator.js +277 -277
  120. package/src/utils/dependency-checker.js +469 -469
  121. package/src/utils/file-ownership-verifier.js +358 -358
  122. package/src/utils/list-formatter.js +194 -194
  123. package/src/utils/music-file-validator.js +285 -285
  124. package/src/utils/preview-list-prompt.js +136 -136
  125. package/src/utils/secure-music-storage.js +412 -412
  126. package/templates/agentvibes-receiver.sh +0 -0
  127. package/templates/audio/welcome-music.mp3 +0 -0
  128. package/.claude/hooks/play-tts-agentvibes-receiver.sh +0 -1
@@ -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
+ }
@@ -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
+ }