agentvibes 3.5.9 → 4.0.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.
- package/.agentvibes/bmad/bmad-voices-enabled.flag +0 -0
- package/.agentvibes/bmad/bmad-voices.md +69 -0
- package/.claude/config/audio-effects.cfg +1 -1
- package/.claude/config/background-music-position.txt +1 -27
- package/.claude/github-star-reminder.txt +1 -1
- package/.claude/hooks/audio-processor.sh +32 -17
- package/.claude/hooks/bmad-speak-enhanced.sh +5 -5
- package/.claude/hooks/bmad-speak.sh +4 -4
- package/.claude/hooks/bmad-voice-manager.sh +8 -8
- package/.claude/hooks/clawdbot-receiver-SECURE.sh +23 -25
- package/.claude/hooks/clawdbot-receiver.sh +28 -4
- package/.claude/hooks/language-manager.sh +1 -1
- package/.claude/hooks/path-resolver.sh +60 -0
- package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +90 -0
- package/.claude/hooks/play-tts-piper.sh +82 -24
- package/.claude/hooks/play-tts-ssh-remote.sh +13 -15
- package/.claude/hooks/play-tts.sh +16 -5
- package/.claude/hooks/session-start-tts.sh +26 -56
- package/.claude/hooks/soprano-gradio-synth.py +1 -1
- package/.claude/hooks/verbosity-manager.sh +10 -4
- package/.claude/settings.json +1 -1
- package/CLAUDE.md +129 -104
- package/README.md +418 -10
- package/RELEASE_NOTES.md +60 -1036
- package/bin/agentvibes-voice-browser.js +1827 -0
- package/bin/agentvibes.js +100 -0
- package/mcp-server/server.py +67 -3
- package/package.json +11 -2
- package/src/console/app.js +806 -0
- package/src/console/audio-env.js +123 -0
- package/src/console/brand-colors.js +13 -0
- package/src/console/footer-config.js +42 -0
- package/src/console/modals/.gitkeep +0 -0
- package/src/console/modals/modal-overlay.js +247 -0
- package/src/console/navigation.js +60 -0
- package/src/console/tabs/.gitkeep +0 -0
- package/src/console/tabs/agents-tab.js +369 -0
- package/src/console/tabs/help-tab.js +261 -0
- package/src/console/tabs/install-tab.js +990 -0
- package/src/console/tabs/music-tab.js +997 -0
- package/src/console/tabs/placeholder-tab.js +45 -0
- package/src/console/tabs/readme-tab.js +267 -0
- package/src/console/tabs/settings-tab.js +3949 -0
- package/src/console/tabs/voices-tab.js +1574 -0
- package/src/installer/music-file-input.js +304 -0
- package/src/installer.js +1353 -676
- package/src/services/.gitkeep +0 -0
- package/src/services/agent-voice-store.js +163 -0
- package/src/services/config-service.js +240 -0
- package/src/services/navigation-service.js +123 -0
- package/src/services/provider-service.js +132 -0
- package/src/services/verbosity-service.js +157 -0
- package/src/utils/audio-duration-validator.js +298 -0
- package/src/utils/audio-format-validator.js +277 -0
- package/src/utils/dependency-checker.js +3 -3
- package/src/utils/file-ownership-verifier.js +358 -0
- package/src/utils/music-file-validator.js +275 -0
- package/src/utils/preview-list-prompt.js +136 -0
- package/src/utils/provider-validator.js +144 -132
- package/src/utils/secure-music-storage.js +412 -0
- package/templates/agentvibes-receiver.sh +11 -7
- package/voice-assignments.json +8245 -0
- package/.claude/config/background-music-volume.txt +0 -1
- package/.claude/config/background-music.cfg +0 -1
- package/.claude/config/background-music.txt +0 -1
- package/.claude/config/tts-speech-rate.txt +0 -1
- package/.claude/config/tts-verbosity.txt +0 -1
- package/.claude/hooks/bmad-party-manager.sh +0 -225
- package/.claude/hooks/stop.sh +0 -38
- package/.claude/piper-voices-dir.txt +0 -1
- package/.mcp.json +0 -34
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentVibes TUI Console — Agents Tab
|
|
3
|
+
* Epic 11: Stories 11.1-11.5
|
|
4
|
+
*
|
|
5
|
+
* Implements the Tab Component Contract:
|
|
6
|
+
* createAgentsTab(screen, services) → { box, show, hide, onFocus, onBlur, getFooterText, getFooterColor }
|
|
7
|
+
*
|
|
8
|
+
* Features: BMAD agent list, voice assignments, party mode toggle, single-voice warning.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { AgentVoiceStore, scanBmadAgents, isSingleVoiceProvider } from '../../services/agent-voice-store.js';
|
|
12
|
+
|
|
13
|
+
const IS_TEST = process.env.AGENTVIBES_TEST_MODE === 'true';
|
|
14
|
+
|
|
15
|
+
let blessed;
|
|
16
|
+
if (!IS_TEST) {
|
|
17
|
+
const { default: b } = await import('blessed');
|
|
18
|
+
blessed = b;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
const COLORS = {
|
|
24
|
+
contentBg: '#0a0e1a',
|
|
25
|
+
sectionHdr: '#7b1fa2', // Purple — section headers for Agents tab
|
|
26
|
+
labelFg: '#e3f2fd',
|
|
27
|
+
valueFg: '#ffff00', // Yellow
|
|
28
|
+
activeFg: '#ce93d8', // Light purple — selected agent
|
|
29
|
+
btnDefault: '#6a1b9a', // Purple — Agents tab buttons
|
|
30
|
+
btnFocus: '#9c27b0',
|
|
31
|
+
btnFocusFg: '#ffffff',
|
|
32
|
+
btnPress: '#ff00ff',
|
|
33
|
+
borderFg: '#9c27b0',
|
|
34
|
+
footerBg: '#9c27b0', // Purple — Agents tab footer
|
|
35
|
+
noticeFg: '#90a4ae',
|
|
36
|
+
warnFg: '#ff9800',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const FOOTER_TEXT = '[↑↓/jk] Navigate [Enter] Details [R] Reset Voice [P] Party Mode [S/V/M/A/R] Tab [Q] Quit';
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
function createTestStub() {
|
|
44
|
+
return {
|
|
45
|
+
box: {},
|
|
46
|
+
show: () => {},
|
|
47
|
+
hide: () => {},
|
|
48
|
+
onFocus: () => {},
|
|
49
|
+
onBlur: () => {},
|
|
50
|
+
getFooterText: () => FOOTER_TEXT,
|
|
51
|
+
getFooterColor: () => COLORS.footerBg,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Create the Agents tab component.
|
|
59
|
+
*
|
|
60
|
+
* @param {object} screen - Blessed screen instance (or test stub)
|
|
61
|
+
* @param {object} services
|
|
62
|
+
* @param {import('../../services/config-service.js').ConfigService} services.configService
|
|
63
|
+
* @param {import('../../services/provider-service.js').ProviderService} services.providerService
|
|
64
|
+
* @returns {{ box, show, hide, onFocus, onBlur, getFooterText, getFooterColor }}
|
|
65
|
+
*/
|
|
66
|
+
export function createAgentsTab(screen, services) {
|
|
67
|
+
if (IS_TEST) return createTestStub();
|
|
68
|
+
|
|
69
|
+
const { configService, providerService, focusMainTabBar } = services;
|
|
70
|
+
const voiceStore = new AgentVoiceStore();
|
|
71
|
+
|
|
72
|
+
// -------------------------------------------------------------------------
|
|
73
|
+
// Container
|
|
74
|
+
|
|
75
|
+
const box = blessed.box({
|
|
76
|
+
parent: screen,
|
|
77
|
+
top: 4,
|
|
78
|
+
left: 0,
|
|
79
|
+
width: '100%',
|
|
80
|
+
bottom: 2,
|
|
81
|
+
hidden: true,
|
|
82
|
+
style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
|
|
83
|
+
border: { type: 'line' },
|
|
84
|
+
borderStyle: { fg: COLORS.borderFg },
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// -------------------------------------------------------------------------
|
|
88
|
+
// Section header
|
|
89
|
+
|
|
90
|
+
blessed.text({
|
|
91
|
+
parent: box,
|
|
92
|
+
top: 1,
|
|
93
|
+
left: 2,
|
|
94
|
+
content: `{#7b1fa2-fg}── BMAD Agents ${'─'.repeat(53)}{/#7b1fa2-fg}`,
|
|
95
|
+
tags: true,
|
|
96
|
+
style: { bg: COLORS.contentBg },
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// -------------------------------------------------------------------------
|
|
100
|
+
// Agent list
|
|
101
|
+
|
|
102
|
+
const agentList = blessed.list({
|
|
103
|
+
parent: box,
|
|
104
|
+
top: 3,
|
|
105
|
+
left: 2,
|
|
106
|
+
width: '96%',
|
|
107
|
+
height: '55%',
|
|
108
|
+
keys: true,
|
|
109
|
+
vi: true,
|
|
110
|
+
mouse: true,
|
|
111
|
+
border: { type: 'line' },
|
|
112
|
+
scrollbar: { ch: '│', style: { fg: COLORS.sectionHdr } },
|
|
113
|
+
style: {
|
|
114
|
+
fg: COLORS.labelFg,
|
|
115
|
+
bg: COLORS.contentBg,
|
|
116
|
+
border: { fg: COLORS.borderFg },
|
|
117
|
+
selected: { bg: '#4a148c', fg: COLORS.activeFg, bold: true },
|
|
118
|
+
item: { fg: COLORS.labelFg },
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// -------------------------------------------------------------------------
|
|
123
|
+
// Status panel
|
|
124
|
+
|
|
125
|
+
blessed.text({
|
|
126
|
+
parent: box,
|
|
127
|
+
top: '64%',
|
|
128
|
+
left: 2,
|
|
129
|
+
content: `{#7b1fa2-fg}── Status ${'─'.repeat(58)}{/#7b1fa2-fg}`,
|
|
130
|
+
tags: true,
|
|
131
|
+
style: { bg: COLORS.contentBg },
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const statusLine = blessed.text({
|
|
135
|
+
parent: box,
|
|
136
|
+
top: '69%',
|
|
137
|
+
left: 2,
|
|
138
|
+
tags: true,
|
|
139
|
+
content: '',
|
|
140
|
+
style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const warningLine = blessed.text({
|
|
144
|
+
parent: box,
|
|
145
|
+
top: '74%',
|
|
146
|
+
left: 2,
|
|
147
|
+
tags: true,
|
|
148
|
+
content: '',
|
|
149
|
+
style: { fg: COLORS.warnFg, bg: COLORS.contentBg },
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// -------------------------------------------------------------------------
|
|
153
|
+
// Buttons
|
|
154
|
+
|
|
155
|
+
function _createBtn(label, onClick) {
|
|
156
|
+
const btn = blessed.button({
|
|
157
|
+
parent: box,
|
|
158
|
+
content: label,
|
|
159
|
+
mouse: true,
|
|
160
|
+
keys: true,
|
|
161
|
+
shrink: true,
|
|
162
|
+
padding: { left: 1, right: 1 },
|
|
163
|
+
style: {
|
|
164
|
+
bg: COLORS.btnDefault,
|
|
165
|
+
fg: 'white',
|
|
166
|
+
focus: { bg: COLORS.btnFocus, fg: COLORS.btnFocusFg, bold: true },
|
|
167
|
+
hover: { bg: COLORS.btnFocus, fg: COLORS.btnFocusFg, bold: true },
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
btn.on('focus', () => {
|
|
171
|
+
const raw = btn.content.replace(/[►◄]/g, '').trim();
|
|
172
|
+
btn.setContent(`►${raw}◄`);
|
|
173
|
+
screen.render();
|
|
174
|
+
});
|
|
175
|
+
btn.on('blur', () => {
|
|
176
|
+
const raw = btn.content.replace(/[►◄]/g, '').trim();
|
|
177
|
+
btn.setContent(raw);
|
|
178
|
+
screen.render();
|
|
179
|
+
});
|
|
180
|
+
btn.key(['enter', 'space'], () => {
|
|
181
|
+
btn.style.bg = COLORS.btnPress;
|
|
182
|
+
screen.render();
|
|
183
|
+
setTimeout(() => {
|
|
184
|
+
btn.style.bg = COLORS.btnDefault;
|
|
185
|
+
screen.render();
|
|
186
|
+
onClick();
|
|
187
|
+
}, 150);
|
|
188
|
+
});
|
|
189
|
+
btn.on('click', () => btn.press());
|
|
190
|
+
btn.on('mouseover', () => btn.focus());
|
|
191
|
+
return btn;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const resetBtn = _createBtn('[R] Reset Voice', () => {
|
|
195
|
+
const agents = _agents;
|
|
196
|
+
const agent = agents[agentList.selected];
|
|
197
|
+
if (agent) {
|
|
198
|
+
voiceStore.resetVoice(agent.id);
|
|
199
|
+
refreshDisplay();
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
resetBtn.bottom = 4;
|
|
203
|
+
resetBtn.left = 4;
|
|
204
|
+
|
|
205
|
+
const partyBtn = _createBtn('[P] Party Mode', () => {
|
|
206
|
+
const current = voiceStore.getPartyMode();
|
|
207
|
+
voiceStore.setPartyMode(!current);
|
|
208
|
+
refreshDisplay();
|
|
209
|
+
});
|
|
210
|
+
partyBtn.bottom = 4;
|
|
211
|
+
partyBtn.left = 24;
|
|
212
|
+
|
|
213
|
+
// -------------------------------------------------------------------------
|
|
214
|
+
// State
|
|
215
|
+
|
|
216
|
+
let _agents = [];
|
|
217
|
+
|
|
218
|
+
function _buildListItems(agents, voiceMap) {
|
|
219
|
+
if (agents.length === 0) {
|
|
220
|
+
return [' (no BMAD agents detected — open a project with .bmad/ or _bmad/)'];
|
|
221
|
+
}
|
|
222
|
+
return agents.map(a => {
|
|
223
|
+
const voice = voiceMap[a.id] ?? '(default)';
|
|
224
|
+
return ` ${a.displayName.padEnd(20)} → ${voice}`;
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function refreshDisplay() {
|
|
229
|
+
_agents = scanBmadAgents(process.cwd());
|
|
230
|
+
const voiceMap = voiceStore.getVoiceMap();
|
|
231
|
+
const partyMode = voiceStore.getPartyMode();
|
|
232
|
+
const provider = providerService.getProvider?.() ?? configService.getConfig().provider ?? 'piper';
|
|
233
|
+
const singleVoice = isSingleVoiceProvider(provider);
|
|
234
|
+
|
|
235
|
+
const items = _buildListItems(_agents, voiceMap);
|
|
236
|
+
agentList.setItems(items);
|
|
237
|
+
|
|
238
|
+
statusLine.setContent(
|
|
239
|
+
` Party Mode: ${partyMode ? 'Enabled' : 'Disabled'} | Provider: ${provider} | Agents: ${_agents.length}`
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
warningLine.setContent(
|
|
243
|
+
partyMode && singleVoice
|
|
244
|
+
? ` ⚠ Provider has only 1 voice — all agents sound the same`
|
|
245
|
+
: ''
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
screen.render();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// -------------------------------------------------------------------------
|
|
252
|
+
// Key bindings
|
|
253
|
+
|
|
254
|
+
agentList.key(['r', 'R'], () => {
|
|
255
|
+
const agent = _agents[agentList.selected];
|
|
256
|
+
if (agent) {
|
|
257
|
+
voiceStore.resetVoice(agent.id);
|
|
258
|
+
refreshDisplay();
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
agentList.key(['p', 'P'], () => {
|
|
263
|
+
const current = voiceStore.getPartyMode();
|
|
264
|
+
voiceStore.setPartyMode(!current);
|
|
265
|
+
refreshDisplay();
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// Type-to-jump: press a letter to jump to first agent whose name starts with it
|
|
269
|
+
const _agentJumpBlocked = new Set(['j', 'k', 'g', 'h', 'l', 'd', 'u', 'r', 'p']);
|
|
270
|
+
agentList.on('keypress', (ch, key) => {
|
|
271
|
+
if (!ch || key.ctrl || key.meta) return;
|
|
272
|
+
const lower = ch.toLowerCase();
|
|
273
|
+
if (!/^[a-z]$/.test(lower)) return;
|
|
274
|
+
if (_agentJumpBlocked.has(lower)) return;
|
|
275
|
+
const count = _agents.length;
|
|
276
|
+
if (count === 0) return;
|
|
277
|
+
const start = agentList.selected ?? 0;
|
|
278
|
+
for (let i = 1; i <= count; i++) {
|
|
279
|
+
const idx = (start + i) % count;
|
|
280
|
+
const name = (_agents[idx]?.displayName ?? '').toLowerCase();
|
|
281
|
+
if (name.startsWith(lower)) {
|
|
282
|
+
agentList.select(idx);
|
|
283
|
+
screen.render();
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// Blinking █ on selected row while list is focused
|
|
290
|
+
let _alBlink = { interval: null, on: false, sel: -1 };
|
|
291
|
+
function _alTick() {
|
|
292
|
+
_alBlink.on = !_alBlink.on;
|
|
293
|
+
const items = agentList.items;
|
|
294
|
+
const cur = agentList.selected ?? 0;
|
|
295
|
+
if (_alBlink.sel !== cur && _alBlink.sel >= 0 && items[_alBlink.sel]) {
|
|
296
|
+
items[_alBlink.sel].setContent((items[_alBlink.sel].content ?? '').replace(/ █$/, ''));
|
|
297
|
+
}
|
|
298
|
+
_alBlink.sel = cur;
|
|
299
|
+
if (items[cur]) {
|
|
300
|
+
const base = (items[cur].content ?? '').replace(/ █$/, '');
|
|
301
|
+
items[cur].setContent(_alBlink.on ? `${base} █` : base);
|
|
302
|
+
}
|
|
303
|
+
screen.render();
|
|
304
|
+
}
|
|
305
|
+
agentList.on('focus', () => {
|
|
306
|
+
_alBlink.on = true;
|
|
307
|
+
_alBlink.sel = agentList.selected ?? 0;
|
|
308
|
+
const items = agentList.items;
|
|
309
|
+
if (items[_alBlink.sel]) items[_alBlink.sel].setContent((items[_alBlink.sel].content ?? '') + ' █');
|
|
310
|
+
screen.render();
|
|
311
|
+
_alBlink.interval = setInterval(_alTick, 500);
|
|
312
|
+
});
|
|
313
|
+
agentList.on('blur', () => {
|
|
314
|
+
if (_alBlink.interval) { clearInterval(_alBlink.interval); _alBlink.interval = null; }
|
|
315
|
+
const items = agentList.items;
|
|
316
|
+
const sel = agentList.selected ?? 0;
|
|
317
|
+
if (items[sel]) items[sel].setContent((items[sel].content ?? '').replace(/ █$/, ''));
|
|
318
|
+
screen.render();
|
|
319
|
+
});
|
|
320
|
+
agentList.on('select item', () => {
|
|
321
|
+
if (_alBlink.interval) _alTick();
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// [↑] at top of list → jump to main header tab bar
|
|
325
|
+
agentList.key(['up'], () => {
|
|
326
|
+
if (agentList.selected === 0 && typeof focusMainTabBar === 'function') {
|
|
327
|
+
focusMainTabBar();
|
|
328
|
+
setTimeout(() => { agentList.select(0); screen.render(); }, 0);
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// Escape at the list level → return to header tab bar
|
|
333
|
+
agentList.key(['escape'], () => {
|
|
334
|
+
if (typeof focusMainTabBar === 'function') { focusMainTabBar(); screen.render(); }
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// -------------------------------------------------------------------------
|
|
338
|
+
// Tab Component Contract
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
box,
|
|
342
|
+
|
|
343
|
+
show() {
|
|
344
|
+
box.show();
|
|
345
|
+
refreshDisplay();
|
|
346
|
+
screen.render();
|
|
347
|
+
},
|
|
348
|
+
|
|
349
|
+
hide() {
|
|
350
|
+
box.hide();
|
|
351
|
+
screen.render();
|
|
352
|
+
},
|
|
353
|
+
|
|
354
|
+
onFocus() {
|
|
355
|
+
agentList.focus();
|
|
356
|
+
screen.render();
|
|
357
|
+
},
|
|
358
|
+
|
|
359
|
+
onBlur() {},
|
|
360
|
+
|
|
361
|
+
getFooterText() {
|
|
362
|
+
return FOOTER_TEXT;
|
|
363
|
+
},
|
|
364
|
+
|
|
365
|
+
getFooterColor() {
|
|
366
|
+
return COLORS.footerBg;
|
|
367
|
+
},
|
|
368
|
+
};
|
|
369
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentVibes TUI Console — Help Tab
|
|
3
|
+
* Epic 13: Story 13.1
|
|
4
|
+
*
|
|
5
|
+
* Implements the Tab Component Contract:
|
|
6
|
+
* createHelpTab(screen, services) → { box, show, hide, onFocus, onBlur, getFooterText, getFooterColor }
|
|
7
|
+
*
|
|
8
|
+
* Features: keyboard shortcuts reference, two sections, [/] search.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const IS_TEST = process.env.AGENTVIBES_TEST_MODE === 'true';
|
|
12
|
+
|
|
13
|
+
let blessed;
|
|
14
|
+
if (!IS_TEST) {
|
|
15
|
+
const { default: b } = await import('blessed');
|
|
16
|
+
blessed = b;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
const COLORS = {
|
|
22
|
+
contentBg: '#0a0e1a',
|
|
23
|
+
sectionHdr: '#546e7a', // Blue-gray — Help tab
|
|
24
|
+
labelFg: '#e3f2fd',
|
|
25
|
+
keyFg: '#ffff00', // Yellow — keyboard shortcuts
|
|
26
|
+
descFg: '#90a4ae', // Gray — descriptions
|
|
27
|
+
borderFg: '#607d8b',
|
|
28
|
+
footerBg: '#607d8b', // Gray — Help tab footer
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const FOOTER_TEXT = '[↑↓/jk] Scroll [/] Search [PgUp/PgDn] Page [S/V/M/A/R] Tab [Q] Quit';
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Keyboard shortcuts data
|
|
35
|
+
|
|
36
|
+
const SHORTCUT_SECTIONS = Object.freeze([
|
|
37
|
+
{
|
|
38
|
+
title: 'Global Shortcuts',
|
|
39
|
+
shortcuts: [
|
|
40
|
+
{ key: 'Q', desc: 'Quit the console' },
|
|
41
|
+
{ key: 'Ctrl+C', desc: 'Force quit' },
|
|
42
|
+
{ key: 'S', desc: 'Switch to Settings tab' },
|
|
43
|
+
{ key: 'V', desc: 'Switch to Voices tab' },
|
|
44
|
+
{ key: 'M', desc: 'Switch to Music tab' },
|
|
45
|
+
{ key: 'R', desc: 'Switch to Readme tab' },
|
|
46
|
+
{ key: 'H', desc: 'Switch to Help tab' },
|
|
47
|
+
{ key: 'I', desc: 'Switch to Install tab' },
|
|
48
|
+
{ key: 'Esc', desc: 'Close modal / go back' },
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
title: 'Navigation Shortcuts',
|
|
53
|
+
shortcuts: [
|
|
54
|
+
{ key: '↑↓ / j k', desc: 'Navigate lists' },
|
|
55
|
+
{ key: 'Enter', desc: 'Select / activate' },
|
|
56
|
+
{ key: 'Space', desc: 'Toggle / preview' },
|
|
57
|
+
{ key: 'Tab', desc: 'Next button' },
|
|
58
|
+
{ key: 'Shift+Tab', desc: 'Previous button' },
|
|
59
|
+
{ key: '/', desc: 'Open search/filter' },
|
|
60
|
+
{ key: 'F', desc: 'Toggle favorites filter (Voices/Music)' },
|
|
61
|
+
{ key: '*', desc: 'Toggle favorite (Music tab)' },
|
|
62
|
+
{ key: 'M', desc: 'Toggle music on/off (Music tab)' },
|
|
63
|
+
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
title: 'Tab Color Guide',
|
|
68
|
+
shortcuts: [
|
|
69
|
+
{ key: 'Blue (#2196f3)', desc: 'Settings tab footer' },
|
|
70
|
+
{ key: 'Teal (#00695c)', desc: 'Voices tab footer' },
|
|
71
|
+
{ key: 'Orange (#ff9800)', desc: 'Music tab footer' },
|
|
72
|
+
|
|
73
|
+
{ key: 'Dark (#455a64)', desc: 'Readme tab footer' },
|
|
74
|
+
{ key: 'Gray (#607d8b)', desc: 'Help tab footer' },
|
|
75
|
+
{ key: 'Indigo (#3f51b5)', desc: 'Install tab footer' },
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Return all shortcut sections.
|
|
82
|
+
* @returns {{ title: string, shortcuts: { key: string, desc: string }[] }[]}
|
|
83
|
+
*/
|
|
84
|
+
export function getShortcutSections() {
|
|
85
|
+
return [...SHORTCUT_SECTIONS];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
function createTestStub() {
|
|
91
|
+
return {
|
|
92
|
+
box: {},
|
|
93
|
+
show: () => {},
|
|
94
|
+
hide: () => {},
|
|
95
|
+
onFocus: () => {},
|
|
96
|
+
onBlur: () => {},
|
|
97
|
+
getFooterText: () => FOOTER_TEXT,
|
|
98
|
+
getFooterColor: () => COLORS.footerBg,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Create the Help tab component.
|
|
106
|
+
*
|
|
107
|
+
* @param {object} screen - Blessed screen instance
|
|
108
|
+
* @param {object} services
|
|
109
|
+
* @returns {{ box, show, hide, onFocus, onBlur, getFooterText, getFooterColor }}
|
|
110
|
+
*/
|
|
111
|
+
export function createHelpTab(screen, services) {
|
|
112
|
+
if (IS_TEST) return createTestStub();
|
|
113
|
+
|
|
114
|
+
const { focusMainTabBar } = services;
|
|
115
|
+
|
|
116
|
+
// -------------------------------------------------------------------------
|
|
117
|
+
// Container
|
|
118
|
+
|
|
119
|
+
const box = blessed.box({
|
|
120
|
+
parent: screen,
|
|
121
|
+
top: 4,
|
|
122
|
+
left: 0,
|
|
123
|
+
width: '100%',
|
|
124
|
+
bottom: 2,
|
|
125
|
+
hidden: true,
|
|
126
|
+
style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
|
|
127
|
+
border: { type: 'line' },
|
|
128
|
+
borderStyle: { fg: COLORS.borderFg },
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// -------------------------------------------------------------------------
|
|
132
|
+
// Build content text
|
|
133
|
+
|
|
134
|
+
function _buildContent(filterText) {
|
|
135
|
+
const lines = [];
|
|
136
|
+
for (const section of SHORTCUT_SECTIONS) {
|
|
137
|
+
lines.push(`{bold}{#546e7a-fg}── ${section.title} ${'─'.repeat(Math.max(0, 60 - section.title.length))}{/#546e7a-fg}{/bold}`);
|
|
138
|
+
for (const { key, desc } of section.shortcuts) {
|
|
139
|
+
const displayKey = key.padEnd(20);
|
|
140
|
+
const displayDesc = desc;
|
|
141
|
+
if (filterText && !key.toLowerCase().includes(filterText) && !desc.toLowerCase().includes(filterText)) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
lines.push(` {${COLORS.keyFg}-fg}${displayKey}{/${COLORS.keyFg}-fg} {${COLORS.descFg}-fg}${displayDesc}{/${COLORS.descFg}-fg}`);
|
|
145
|
+
}
|
|
146
|
+
lines.push('');
|
|
147
|
+
}
|
|
148
|
+
return lines.join('\n');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// -------------------------------------------------------------------------
|
|
152
|
+
// Scrollable content
|
|
153
|
+
|
|
154
|
+
const scrollBox = blessed.box({
|
|
155
|
+
parent: box,
|
|
156
|
+
top: 1,
|
|
157
|
+
left: 2,
|
|
158
|
+
width: '96%',
|
|
159
|
+
bottom: 4,
|
|
160
|
+
scrollable: true,
|
|
161
|
+
alwaysScroll: true,
|
|
162
|
+
tags: true,
|
|
163
|
+
keys: true,
|
|
164
|
+
vi: true,
|
|
165
|
+
mouse: true,
|
|
166
|
+
scrollbar: { ch: '│', style: { fg: COLORS.sectionHdr } },
|
|
167
|
+
content: _buildContent(''),
|
|
168
|
+
style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// -------------------------------------------------------------------------
|
|
172
|
+
// Search
|
|
173
|
+
|
|
174
|
+
const searchBox = blessed.textbox({
|
|
175
|
+
parent: box,
|
|
176
|
+
bottom: 2,
|
|
177
|
+
left: 10,
|
|
178
|
+
width: 30,
|
|
179
|
+
height: 1,
|
|
180
|
+
hidden: true,
|
|
181
|
+
inputOnFocus: true,
|
|
182
|
+
keys: true,
|
|
183
|
+
style: { fg: COLORS.keyFg, bg: '#1a237e', focus: { bg: '#283593' } },
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
blessed.text({
|
|
187
|
+
parent: box,
|
|
188
|
+
bottom: 2,
|
|
189
|
+
left: 2,
|
|
190
|
+
content: 'Search:',
|
|
191
|
+
style: { fg: COLORS.descFg, bg: COLORS.contentBg },
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
searchBox.on('keypress', () => {
|
|
195
|
+
setTimeout(() => {
|
|
196
|
+
const filter = searchBox.getValue().toLowerCase().trim();
|
|
197
|
+
scrollBox.setContent(_buildContent(filter));
|
|
198
|
+
screen.render();
|
|
199
|
+
}, 0);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
searchBox.key(['escape'], () => {
|
|
203
|
+
searchBox.clearValue();
|
|
204
|
+
searchBox.hide();
|
|
205
|
+
scrollBox.setContent(_buildContent(''));
|
|
206
|
+
scrollBox.focus();
|
|
207
|
+
screen.render();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
scrollBox.key(['/'], () => {
|
|
211
|
+
searchBox.show();
|
|
212
|
+
searchBox.clearValue();
|
|
213
|
+
searchBox.focus();
|
|
214
|
+
screen.render();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// [↑] at top of content → jump to main header tab bar
|
|
218
|
+
scrollBox.key(['up'], () => {
|
|
219
|
+
if (scrollBox.getScroll() === 0 && typeof focusMainTabBar === 'function') {
|
|
220
|
+
focusMainTabBar();
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Escape → return to header tab bar
|
|
225
|
+
scrollBox.key(['escape'], () => {
|
|
226
|
+
if (typeof focusMainTabBar === 'function') { focusMainTabBar(); screen.render(); }
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// -------------------------------------------------------------------------
|
|
230
|
+
// Tab Component Contract
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
box,
|
|
234
|
+
|
|
235
|
+
show() {
|
|
236
|
+
box.show();
|
|
237
|
+
scrollBox.setContent(_buildContent(''));
|
|
238
|
+
screen.render();
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
hide() {
|
|
242
|
+
box.hide();
|
|
243
|
+
screen.render();
|
|
244
|
+
},
|
|
245
|
+
|
|
246
|
+
onFocus() {
|
|
247
|
+
scrollBox.focus();
|
|
248
|
+
screen.render();
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
onBlur() {},
|
|
252
|
+
|
|
253
|
+
getFooterText() {
|
|
254
|
+
return FOOTER_TEXT;
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
getFooterColor() {
|
|
258
|
+
return COLORS.footerBg;
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
}
|