agentvibes 4.6.7 → 5.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.
Files changed (35) hide show
  1. package/.agentvibes/bmad-voice-map.json +104 -0
  2. package/.agentvibes/config.json +13 -12
  3. package/.agentvibes/copilot-sessions.log +4 -0
  4. package/.claude/audio/tracks/README.md +51 -52
  5. package/.claude/config/audio-effects-bmad.cfg +50 -0
  6. package/.claude/config/audio-effects.cfg +4 -4
  7. package/.claude/config/background-music-enabled.txt +1 -0
  8. package/.claude/config/personality.txt +1 -0
  9. package/.claude/hooks/play-tts-piper.sh +3 -1
  10. package/.claude/hooks/play-tts.sh +373 -301
  11. package/.claude/hooks/session-start-tts.sh +81 -81
  12. package/.claude/hooks-windows/audio-processor.ps1 +181 -0
  13. package/.claude/hooks-windows/play-tts-piper.ps1 +259 -245
  14. package/.claude/hooks-windows/play-tts.ps1 +101 -9
  15. package/.claude/hooks-windows/session-start-tts.ps1 +114 -114
  16. package/README.md +107 -7
  17. package/RELEASE_NOTES.md +54 -0
  18. package/bin/bmad-speak.js +16 -8
  19. package/mcp-server/server.py +15 -8
  20. package/package.json +1 -1
  21. package/src/console/app.js +899 -897
  22. package/src/console/footer-config.js +50 -50
  23. package/src/console/navigation.js +65 -65
  24. package/src/console/tabs/agents-tab.js +1896 -1886
  25. package/src/console/tabs/music-tab.js +1046 -1039
  26. package/src/console/tabs/placeholder-tab.js +81 -80
  27. package/src/console/tabs/settings-tab.js +939 -3988
  28. package/src/console/tabs/setup-tab.js +1811 -0
  29. package/src/console/tabs/voices-tab.js +1720 -1713
  30. package/src/installer.js +6147 -6092
  31. package/src/services/llm-provider-service.js +407 -0
  32. package/src/services/navigation-service.js +123 -123
  33. package/src/services/tts-engine-service.js +69 -0
  34. package/.claude/audio/tracks/dreamy_house_loop.mp3 +0 -0
  35. package/src/console/tabs/install-tab.js +0 -1081
@@ -1,1081 +0,0 @@
1
- /**
2
- * AgentVibes TUI Console — Install Tab (Installer Wizard)
3
- * Epic 12: Stories 12.1-12.5
4
- *
5
- * Implements the Tab Component Contract:
6
- * createInstallTab(screen, services) → { box, show, hide, onFocus, onBlur, getFooterText, getFooterColor }
7
- *
8
- * 5-screen wizard flow:
9
- * Screen 1: Welcome & Purpose
10
- * Screen 2: Auto Dependency Check
11
- * Screen 3: Provider Selection
12
- * Screen 4: Voice Config & Intro Text
13
- * Screen 5: Complete & TTS Greeting
14
- */
15
-
16
- import path from 'node:path';
17
- import { execFile } from 'node:child_process';
18
- import { promisify } from 'node:util';
19
- import fs from 'node:fs';
20
- import { promises as _fsP } from 'node:fs';
21
- import { buildAudioEnv } from '../audio-env.js';
22
- import { SUPPORTED_LANGUAGES, t } from '../../i18n/strings.js';
23
- import {
24
- copyCommandFiles, copyHookFiles, copyPersonalityFiles,
25
- copyPluginFiles, copyBmadConfigFiles, copyBackgroundMusicFiles,
26
- copyConfigFiles, configureSessionStartHook,
27
- installPluginManifest, checkAndInstallPiper, ensureGitRepo,
28
- } from '../../installer.js';
29
-
30
- const _execFileAsync = promisify(execFile);
31
-
32
- const IS_TEST = process.env.AGENTVIBES_TEST_MODE === 'true';
33
-
34
- let blessed;
35
- if (!IS_TEST) {
36
- const { default: b } = await import('blessed');
37
- blessed = b;
38
- }
39
-
40
- // ---------------------------------------------------------------------------
41
-
42
- const COLORS = {
43
- contentBg: '#0a0e1a',
44
- sectionHdr: 'bright-cyan', // Matches header "Agent" color
45
- labelFg: '#e3f2fd',
46
- valueFg: '#ffff00', // Yellow
47
- brandPink: '#f06292', // Light magenta — AgentVibes logotype
48
- successFg: '#69f0ae', // Green — success
49
- errorFg: '#ef9a9a', // Red — error/missing
50
- btnDefault: '#283593',
51
- btnFocus: '#2e7d32', // Green — focused/selected
52
- btnFocusFg: '#ffffff', // White text on green
53
- btnPress: '#ff00ff',
54
- borderFg: 'bright-cyan',
55
- footerBg: '#5c6bc0', // Lighter indigo — Install tab footer
56
- noticeFg: '#90a4ae',
57
- };
58
-
59
- const FOOTER_TEXT = '[Enter] Continue/Finish [Esc] Back/Exit [C] Open Console [S/V/M/A/R] Tab [Q] Quit';
60
-
61
- // ---------------------------------------------------------------------------
62
- // Exported pure helpers (stories 12.1, 12.5)
63
-
64
- /**
65
- * Returns the default intro text suggestion (project folder name).
66
- * @param {string} projectDir
67
- * @returns {string}
68
- */
69
- export function getIntroDefault(projectDir) {
70
- if (!projectDir) return '';
71
- return path.basename(projectDir);
72
- }
73
-
74
- /**
75
- * Format the TTS greeting message for Screen 5.
76
- * @param {string} introText - User's intro text (may be empty)
77
- * @param {string} projectName - Project folder name
78
- * @returns {string}
79
- */
80
- export function formatGreeting(introText, projectName) {
81
- const name = introText || projectName || 'AgentVibes';
82
- return `${name} is ready! Welcome to AgentVibes. Love AgentVibes? We'd really appreciate it if you could give us a star on GitHub.`;
83
- }
84
-
85
- // ---------------------------------------------------------------------------
86
- // Dependency detection helpers (story 12.2)
87
-
88
- /**
89
- * Check if a command exists on the system (async).
90
- * Only ENOENT means "not installed" — non-zero exit code still means the binary exists.
91
- * @param {string} cmd
92
- * @returns {Promise<boolean>}
93
- */
94
- async function _commandExistsAsync(cmd) {
95
- try {
96
- // On Windows, commands like 'npm' are .cmd batch files that require shell: true
97
- const opts = { stdio: 'pipe', timeout: 5000 };
98
- if (process.platform === 'win32') opts.shell = true;
99
- await _execFileAsync(cmd, ['--version'], opts);
100
- return true;
101
- } catch (err) {
102
- if (err.code === 'ENOENT') return false;
103
- return true; // binary exists but --version returned non-zero
104
- }
105
- }
106
-
107
- /**
108
- * Run dependency checks asynchronously. Returns results map.
109
- * @returns {Promise<{ node: boolean, npm: boolean, piper: boolean, soprano: boolean }>}
110
- */
111
- async function _checkDependenciesAsync() {
112
- const [node, npm, piperCmd, sopranoTts, sopranoWebui, ffmpeg] = await Promise.all([
113
- _commandExistsAsync('node'),
114
- _commandExistsAsync('npm'),
115
- _commandExistsAsync('piper'),
116
- _commandExistsAsync('soprano-tts'),
117
- _commandExistsAsync('soprano-webui'),
118
- _commandExistsAsync('ffmpeg'),
119
- ]);
120
-
121
- // On Windows, Piper is a standalone exe at %LOCALAPPDATA%\Programs\Piper\piper.exe
122
- let piper = piperCmd;
123
- if (!piper && process.platform === 'win32') {
124
- const localAppData = process.env.LOCALAPPDATA ||
125
- (process.env.USERPROFILE ? path.join(process.env.USERPROFILE, 'AppData', 'Local') : null);
126
- if (localAppData) {
127
- piper = fs.existsSync(path.join(localAppData, 'Programs', 'Piper', 'piper.exe'));
128
- }
129
- }
130
-
131
- return { node, npm, piper, soprano: sopranoTts || sopranoWebui, ffmpeg };
132
- }
133
-
134
- // ---------------------------------------------------------------------------
135
- // Test stub
136
-
137
- function createTestStub() {
138
- return {
139
- box: {},
140
- show: () => {},
141
- hide: () => {},
142
- onFocus: () => {},
143
- onBlur: () => {},
144
- getFooterText: () => t('en', 'footerText'),
145
- getFooterColor: () => COLORS.footerBg,
146
- };
147
- }
148
-
149
- // ---------------------------------------------------------------------------
150
-
151
- /**
152
- * Create the Install tab component.
153
- *
154
- * @param {object} screen - Blessed screen instance (or test stub)
155
- * @param {object} services
156
- * @param {import('../../services/config-service.js').ConfigService} services.configService
157
- * @returns {{ box, show, hide, onFocus, onBlur, getFooterText, getFooterColor }}
158
- */
159
- export function createInstallTab(screen, services) {
160
- if (IS_TEST) return createTestStub();
161
-
162
- const { configService, providerService, navigationService, focusMainTabBar, languageService } = services;
163
-
164
- // -------------------------------------------------------------------------
165
- // Container
166
-
167
- const box = blessed.box({
168
- parent: screen,
169
- top: 5,
170
- left: 0,
171
- width: '100%',
172
- bottom: 2,
173
- hidden: true,
174
- style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
175
- border: { type: 'line' },
176
- borderStyle: { fg: COLORS.borderFg },
177
- });
178
-
179
- // -------------------------------------------------------------------------
180
- // Wizard state
181
-
182
- let _screen = 0;
183
- let _lastScreen = -1;
184
- // _lang is now owned by languageService; keep a local helper for convenience
185
- // and a local _langIdx for the language-picker UI (Screen 0).
186
- const _getLang = () => languageService?.getLang() ?? 'en';
187
- const _tl = (key) => languageService?.t(key) ?? t('en', key);
188
- let _langIdx = 0;
189
- let _deps = null;
190
- let _checking = false;
191
- let _selectedProvider = null;
192
- let _introText = getIntroDefault(process.cwd());
193
- let _screen5Announced = false; // TTS greeting fires once per wizard run
194
- let _completionModalOpen = false;
195
- let _completionModalBox = null;
196
-
197
- // Install state (populated during screen 5)
198
- let _installLog = []; // array of blessed-tagged strings
199
- let _installRunning = false;
200
- let _installComplete = false;
201
- let _installError = null;
202
- let _lastSpinnerIdx = -1; // index of last ⟳ entry, replaced by ✓ on succeed
203
-
204
- // -------------------------------------------------------------------------
205
- // Content area — single persistent box, never detached.
206
- //
207
- // KEY INSIGHT: detach+recreate fails because the new widget has no previous
208
- // cell state, so blessed's diff renderer doesn't know which cells to clear.
209
- // Keeping the SAME element and calling setContent('') lets blessed diff
210
- // old-content → empty and write spaces over every character that was there.
211
-
212
- const contentBox = blessed.box({
213
- parent: box,
214
- top: 1,
215
- left: 2,
216
- width: '96%',
217
- bottom: 5,
218
- tags: true,
219
- wrap: false,
220
- scrollable: false,
221
- style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
222
- });
223
-
224
- // Footer hint
225
- const hintLine = blessed.text({
226
- parent: box,
227
- bottom: 2,
228
- left: 2,
229
- right: 2, // explicit right bound — prevents blessed auto-shrink which leaves stale chars
230
- tags: true,
231
- content: '',
232
- style: { fg: COLORS.noticeFg, bg: COLORS.contentBg },
233
- });
234
-
235
- function _c(lines) { return lines.join('\n'); }
236
-
237
- // -------------------------------------------------------------------------
238
- // Screen 4 action button callbacks
239
-
240
- function _doEdit() {
241
- if (box.hidden || _screen !== 4) return;
242
- const prompt = blessed.prompt({
243
- parent: screen,
244
- top: 'center',
245
- left: 'center',
246
- height: 'shrink',
247
- width: '60%',
248
- border: 'line',
249
- tags: true,
250
- style: {
251
- fg: COLORS.labelFg,
252
- bg: COLORS.contentBg,
253
- border: { fg: COLORS.sectionHdr },
254
- label: { fg: COLORS.sectionHdr },
255
- },
256
- });
257
- prompt.input('Intro text (prefix spoken before every TTS message):', _introText, (err, val) => {
258
- prompt.destroy();
259
- if (!err && val !== null) {
260
- _introText = val.trim();
261
- _renderScreen4();
262
- }
263
- screen.render();
264
- });
265
- screen.render();
266
- }
267
-
268
- // -------------------------------------------------------------------------
269
- // TUI spinner adapter — captures copy-function progress into _installLog
270
-
271
- function _makeSpinner() {
272
- return {
273
- start(msg) {
274
- _installLog.push(`{${COLORS.noticeFg}-fg} ⟳ ${msg}{/${COLORS.noticeFg}-fg}`);
275
- _lastSpinnerIdx = _installLog.length - 1;
276
- _renderScreen5();
277
- },
278
- succeed(msg) {
279
- const line = `{${COLORS.successFg}-fg} ✓ ${msg || ''}{/${COLORS.successFg}-fg}`;
280
- if (_lastSpinnerIdx >= 0) {
281
- _installLog[_lastSpinnerIdx] = line;
282
- } else {
283
- _installLog.push(line);
284
- }
285
- _lastSpinnerIdx = -1;
286
- _renderScreen5();
287
- },
288
- info(msg) {
289
- _installLog.push(`{${COLORS.noticeFg}-fg} ℹ ${msg}{/${COLORS.noticeFg}-fg}`);
290
- _renderScreen5();
291
- },
292
- warn(msg) {
293
- _installLog.push(`{#ffcc00-fg} ⚠ ${msg}{/#ffcc00-fg}`);
294
- _renderScreen5();
295
- },
296
- stop() {},
297
- };
298
- }
299
-
300
- // -------------------------------------------------------------------------
301
- // Write AgentVibes config files into targetDir/.claude/
302
-
303
- async function _writeInstallConfig(targetDir, provider) {
304
- const claudeDir = path.join(targetDir, '.claude');
305
- const configDir = path.join(claudeDir, 'config');
306
- await _fsP.mkdir(configDir, { recursive: true });
307
-
308
- const defaultVoices = {
309
- piper: 'en_US-ryan-high',
310
- macos: 'Samantha',
311
- soprano: 'soprano-default',
312
- sapi: 'Microsoft David Desktop',
313
- };
314
- // Use voice from Settings if configured, otherwise fall back to provider default
315
- const configuredVoice = configService?.getConfig?.()?.voice;
316
- const voice = configuredVoice ?? (defaultVoices[provider] ?? 'en_US-ryan-high');
317
-
318
- await _fsP.writeFile(path.join(claudeDir, 'tts-provider.txt'), provider);
319
- await _fsP.writeFile(path.join(claudeDir, 'tts-voice.txt'), voice);
320
- await _fsP.writeFile(path.join(claudeDir, 'tts-verbosity.txt'), 'medium');
321
-
322
- const pretext = _introText?.trim() ?? '';
323
- if (pretext) {
324
- await _fsP.writeFile(path.join(configDir, 'tts-pretext.txt'), pretext, { mode: 0o600 });
325
- } else {
326
- try { await _fsP.unlink(path.join(configDir, 'tts-pretext.txt')); } catch { /* ok */ }
327
- }
328
-
329
- // Apply background music settings from Settings tab.
330
- // play-tts-piper.sh reads background-music-enabled.txt (not background-music.txt),
331
- // so we must write that file explicitly when music is enabled.
332
- const bgMusic = configService?.getConfig?.()?.backgroundMusic;
333
- if (bgMusic?.enabled) {
334
- await _fsP.writeFile(path.join(configDir, 'background-music-enabled.txt'), 'true');
335
- // Update the track in audio-effects.cfg (copied from package defaults a moment ago).
336
- // Only apply if the track name is a safe filename (no pipe characters or path separators).
337
- const track = bgMusic.track;
338
- if (track && !/[|/\\]/.test(track)) {
339
- try {
340
- const audioEffectsPath = path.join(configDir, 'audio-effects.cfg');
341
- let content = await _fsP.readFile(audioEffectsPath, 'utf-8');
342
- content = content.replace(
343
- /^default\|([^|]*)\|([^|]*)\|(.*)$/m,
344
- `default|$1|${track}|$3`,
345
- );
346
- await _fsP.writeFile(audioEffectsPath, content);
347
- } catch { /* audio-effects.cfg not yet present — non-fatal */ }
348
- }
349
- }
350
- }
351
-
352
- // -------------------------------------------------------------------------
353
- // Full installation sequence (runs on screen 5)
354
-
355
- async function _runInstall() {
356
- _installLog = [];
357
- _installRunning = true;
358
- _installComplete = false;
359
- _installError = null;
360
- _lastSpinnerIdx = -1;
361
-
362
- const targetDir = process.cwd();
363
- const provider = _selectedProvider ?? 'piper';
364
- const spinner = _makeSpinner();
365
-
366
- // Suppress console output from installer.js copy functions — they use
367
- // chalk+console.log which would corrupt the blessed display.
368
- const _origLog = console.log;
369
- const _origWarn = console.warn;
370
- const _origErr = console.error;
371
- console.log = () => {};
372
- console.warn = () => {};
373
- console.error = () => {};
374
-
375
- try {
376
- // Create directory structure
377
- spinner.start('Preparing .claude directory...');
378
- await _fsP.mkdir(path.join(targetDir, '.claude', 'commands'), { recursive: true });
379
- await _fsP.mkdir(path.join(targetDir, '.claude', 'hooks'), { recursive: true });
380
- await _fsP.mkdir(path.join(targetDir, '.claude', 'audio', 'tracks'), { recursive: true });
381
- spinner.succeed('Directory structure ready');
382
-
383
- await copyCommandFiles(targetDir, spinner);
384
- await copyHookFiles(targetDir, spinner);
385
- await copyPersonalityFiles(targetDir, spinner);
386
- await copyPluginFiles(targetDir, spinner);
387
- await copyBmadConfigFiles(targetDir, spinner);
388
- await copyBackgroundMusicFiles(targetDir, spinner);
389
- await copyConfigFiles(targetDir, spinner);
390
- await configureSessionStartHook(targetDir, spinner);
391
- await installPluginManifest(targetDir, spinner);
392
- await ensureGitRepo(targetDir, spinner);
393
-
394
- spinner.start('Writing configuration...');
395
- await _writeInstallConfig(targetDir, provider);
396
- spinner.succeed('Configuration saved');
397
-
398
- // Create .mcp.json if it doesn't already exist
399
- const mcpConfigPath = path.join(targetDir, '.mcp.json');
400
- let _mcpCreated = false;
401
- try {
402
- await _fsP.access(mcpConfigPath);
403
- // Already exists — skip to avoid overwriting user's config
404
- } catch {
405
- const mcpConfig = {
406
- mcpServers: {
407
- agentvibes: {
408
- command: 'npx',
409
- args: ['-y', '--package=agentvibes', 'agentvibes-mcp-server'],
410
- },
411
- },
412
- };
413
- spinner.start('Creating .mcp.json...');
414
- await _fsP.writeFile(mcpConfigPath, JSON.stringify(mcpConfig, null, 2) + '\n');
415
- spinner.succeed('.mcp.json created');
416
- _mcpCreated = true;
417
- }
418
-
419
- if (provider === 'piper') {
420
- spinner.start('Checking Piper TTS voices...');
421
- await checkAndInstallPiper(targetDir, { yes: true, silent: true });
422
- spinner.succeed('Piper TTS ready');
423
- }
424
-
425
- _installComplete = true;
426
- _installRunning = false;
427
- _installLog.push('');
428
- _installLog.push(`{${COLORS.successFg}-fg} ✅ AgentVibes installed successfully!{/${COLORS.successFg}-fg}`);
429
- if (_mcpCreated) {
430
- _installLog.push(`{${COLORS.successFg}-fg} 📡 .mcp.json created — run: claude --mcp-config .mcp.json{/${COLORS.successFg}-fg}`);
431
- }
432
- _installLog.push(`{${COLORS.noticeFg}-fg} ⭐ Star us on GitHub: github.com/preibisch/agentvibes{/${COLORS.noticeFg}-fg}`);
433
-
434
- } catch (err) {
435
- _installRunning = false;
436
- _installError = err.message;
437
- _installLog.push(`{${COLORS.errorFg}-fg} ✗ Installation failed: ${err.message}{/${COLORS.errorFg}-fg}`);
438
- } finally {
439
- console.log = _origLog;
440
- console.warn = _origWarn;
441
- console.error = _origErr;
442
- }
443
-
444
- _renderScreen5();
445
-
446
- // Show buttons now that install is done (success or error)
447
- _s5QuitBtn.show();
448
- _s5CustomizeBtn.show();
449
- _s5QuitBtn.focus();
450
- screen.render();
451
-
452
- // Play TTS greeting on success
453
- if (_installComplete && !_screen5Announced) {
454
- _screen5Announced = true;
455
- const greeting = formatGreeting(_introText, getIntroDefault(process.cwd()));
456
- const ttsScript = path.resolve(targetDir, '.claude/hooks/play-tts.sh');
457
- execFile('bash', [ttsScript, greeting], {
458
- env: buildAudioEnv(),
459
- timeout: 30000,
460
- }, () => {});
461
- }
462
- }
463
-
464
- function _doAccept() {
465
- if (_screen !== 4 || _installRunning) return;
466
- _screen++;
467
- _showCurrentScreen();
468
- // Start install after screen transition renders (50ms delay in _showCurrentScreen)
469
- setTimeout(() => _runInstall().catch(() => {}), 100);
470
- }
471
-
472
- // -------------------------------------------------------------------------
473
- // Screen 4 action buttons — real blessed widgets for keyboard focus + ←/→ nav
474
-
475
- function _createInstallBtn(label, bg, onClick, textColor = '#ffffff') {
476
- const btn = blessed.button({
477
- parent: box,
478
- content: label,
479
- mouse: true,
480
- keys: true,
481
- shrink: true,
482
- hidden: true,
483
- padding: { left: 1, right: 1 },
484
- style: {
485
- bg,
486
- fg: textColor,
487
- focus: { bg: COLORS.btnFocus, fg: COLORS.btnFocusFg, bold: true },
488
- },
489
- });
490
-
491
- // Focus indicator: ►label◄ with blinking █ — matches settings-tab standard
492
- let _blinkInterval = null;
493
- btn.on('focus', () => {
494
- btn.style.bg = COLORS.btnFocus;
495
- btn.style.fg = COLORS.btnFocusFg;
496
- const raw = btn.content.replace(/[►◄█]/g, '').trim();
497
- btn.setContent(`►${raw}◄ █`);
498
- let _on = true;
499
- screen.render();
500
- _blinkInterval = setInterval(() => {
501
- _on = !_on;
502
- if (!btn.content.includes('►')) return;
503
- const r = btn.content.replace(/[►◄█]/g, '').trim();
504
- btn.setContent(_on ? `►${r}◄ █` : `►${r}◄`);
505
- screen.render();
506
- }, 500);
507
- });
508
- btn.on('blur', () => {
509
- if (_blinkInterval) { clearInterval(_blinkInterval); _blinkInterval = null; }
510
- btn.style.bg = bg;
511
- btn.style.fg = textColor;
512
- const raw = btn.content.replace(/[►◄█]/g, '').trim();
513
- btn.setContent(raw);
514
- screen.render();
515
- });
516
-
517
- // Press: magenta flash then invoke onClick
518
- // Guard: don't fire onClick when the completion modal is open — Enter should dismiss it.
519
- btn.key(['enter', 'space'], () => {
520
- if (_completionModalOpen) return;
521
- btn.style.bg = COLORS.btnPress;
522
- btn.style.fg = 'white';
523
- screen.render();
524
- setTimeout(() => {
525
- btn.style.bg = bg;
526
- btn.style.fg = textColor;
527
- screen.render();
528
- onClick();
529
- }, 150);
530
- });
531
- btn.on('click', () => btn.press());
532
- return btn;
533
- }
534
-
535
- const _editBtn = _createInstallBtn(_tl('editInstallBtn'), '#1565c0', _doEdit);
536
- const _acceptBtn = _createInstallBtn(_tl('acceptInstallBtn'), COLORS.btnDefault, _doAccept);
537
-
538
- // Edit sits inline with the intro text row; Accept & Install is below
539
- _editBtn.top = 8; _editBtn.left = 36;
540
- _acceptBtn.top = 13; _acceptBtn.left = 4;
541
-
542
- // ↓/↑ navigate between Edit and Accept & Install
543
- // Note: Tab is NOT used here — 'tab' is registered globally by navigation.js (cycles tabs)
544
- _editBtn.key(['down', 'right'], () => { _acceptBtn.focus(); screen.render(); });
545
- _acceptBtn.key(['up', 'left'], () => { _editBtn.focus(); screen.render(); });
546
-
547
- // -------------------------------------------------------------------------
548
- // Screen 1 buttons — Begin (cyan) and Exit (grey)
549
-
550
- const _s1BeginBtn = _createInstallBtn(_tl('beginBtn'), '#00838f', () => {
551
- _screen++;
552
- _showCurrentScreen();
553
- });
554
- const _s1ExitBtn = _createInstallBtn(_tl('exitBtn'), '#546e7a', () => {
555
- box.hide();
556
- screen.render();
557
- if (typeof focusMainTabBar === 'function') focusMainTabBar();
558
- });
559
-
560
- _s1BeginBtn.top = 5; _s1BeginBtn.left = 4;
561
- _s1ExitBtn.top = 5; _s1ExitBtn.left = 20;
562
-
563
- // ←/→ horizontal and ↑/↓ vertical — both navigate between the two buttons
564
- _s1BeginBtn.key(['right', 'down'], () => { _s1ExitBtn.focus(); screen.render(); });
565
- _s1ExitBtn.key(['right', 'down'], () => { _s1BeginBtn.focus(); screen.render(); });
566
- _s1ExitBtn.key(['left', 'up', 'S-tab'], () => { _s1BeginBtn.focus(); screen.render(); });
567
- _s1BeginBtn.key(['left', 'up', 'S-tab'], () => { _s1ExitBtn.focus(); screen.render(); });
568
-
569
- // -------------------------------------------------------------------------
570
- // Screen 2 button — Continue (shown after deps check passes)
571
-
572
- const _s2ContinueBtn = _createInstallBtn(_tl('continueArrowBtn'), '#1565c0', () => {
573
- _screen++;
574
- _showCurrentScreen();
575
- });
576
- _s2ContinueBtn.top = 12; _s2ContinueBtn.left = 4;
577
- // → also advances without the flash delay
578
- _s2ContinueBtn.key(['right'], () => { _screen++; _showCurrentScreen(); });
579
-
580
- // Screen 3: no Continue button — Enter/→ on the list confirms selection and advances
581
-
582
- // -------------------------------------------------------------------------
583
- // Screen 5 buttons — Customize More (left) + Done - Quit (right, default focused)
584
- // Layout: [ Customize More ] [ Done - Quit ]
585
- // Button width = label + 2 padding chars; gap between buttons = 3 chars
586
-
587
- const _s5CustomizeBtn = _createInstallBtn(_tl('doneCustomizeBtn'), '#1565c0', () => {
588
- _dismissCompletionModal();
589
- });
590
- _s5CustomizeBtn.bottom = 3; _s5CustomizeBtn.left = 4;
591
-
592
- const _s5QuitBtn = _createInstallBtn(_tl('doneQuitBtn'), '#b71c1c', () => {
593
- screen.destroy();
594
- process.exit(0);
595
- });
596
- // left = start(4) + customizeLabel + 2 padding + 3 gap
597
- _s5QuitBtn.bottom = 3; _s5QuitBtn.left = 4 + _tl('doneCustomizeBtn').length + 2 + 3;
598
-
599
- // Arrow/Tab navigation between the two buttons
600
- _s5CustomizeBtn.key(['tab', 'right'], () => { _s5QuitBtn.focus(); screen.render(); });
601
- _s5QuitBtn.key(['tab', 'left', 'S-tab'], () => { _s5CustomizeBtn.focus(); screen.render(); });
602
-
603
- // -------------------------------------------------------------------------
604
- // Screen renderers
605
-
606
- const _HDR = (emoji, label) =>
607
- `{${COLORS.sectionHdr}-fg}${emoji} ${label} ${'─'.repeat(100)}{/${COLORS.sectionHdr}-fg}`;
608
-
609
- function _renderScreen0() {
610
- const lines = [
611
- _HDR('🌐', 'Language / Idioma / Langue / Sprache / 言語 / भाषा / 语言 / 언어'),
612
- '',
613
- ' Select your language:',
614
- '',
615
- ...SUPPORTED_LANGUAGES.map((l, i) =>
616
- i === _langIdx
617
- ? ` {green-fg}► ${l.name}{/green-fg}`
618
- : ` ${l.name}`
619
- ),
620
- ];
621
- contentBox.setContent(_c(lines));
622
- hintLine.setContent(' Screen 0: Language | [↑/↓] Select | [Enter] Apply & Continue | [→] Skip (English)');
623
- screen.render();
624
- }
625
-
626
- function _renderScreen1() {
627
- // Update button labels to current language before focus triggers the decorator
628
- _s1BeginBtn.setContent(t(_getLang(), 'beginBtn'));
629
- _s1ExitBtn.setContent(t(_getLang(), 'exitBtn'));
630
- contentBox.setContent(_c([
631
- _HDR('🔧', t(_getLang(), 'setupWizard')),
632
- '',
633
- ` {${COLORS.noticeFg}-fg}${t(_getLang(), 'setupWizardSubtitle')}{/${COLORS.noticeFg}-fg}`,
634
- '',
635
- '', // ← [▶ Begin] [✗ Exit] buttons here (box row 5)
636
- ]));
637
- hintLine.setContent(` ${t(_getLang(), 'screen1Hint')}`);
638
- _s1BeginBtn.focus();
639
- screen.render();
640
- }
641
-
642
- async function _renderScreen2() {
643
- const frames = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
644
- let frameIdx = 0;
645
- _checking = true;
646
- _s2ContinueBtn.hide(); // hidden during spinner
647
-
648
- contentBox.setContent(_c([
649
- _HDR('🔍', t(_getLang(), 'dependencyCheck')),
650
- '',
651
- ` {${COLORS.noticeFg}-fg}${frames[0]} ${t(_getLang(), 'checkingDependencies')}{/${COLORS.noticeFg}-fg}`,
652
- ]));
653
- hintLine.setContent(` ${t(_getLang(), 'screen2Hint')}`);
654
- screen.render();
655
-
656
- const spinInterval = setInterval(() => {
657
- frameIdx = (frameIdx + 1) % frames.length;
658
- contentBox.setContent(_c([
659
- _HDR('🔍', t(_getLang(), 'dependencyCheck')),
660
- '',
661
- ` {${COLORS.noticeFg}-fg}${frames[frameIdx]} ${t(_getLang(), 'checkingDependencies')}{/${COLORS.noticeFg}-fg}`,
662
- ]));
663
- screen.render();
664
- }, 100);
665
-
666
- try {
667
- _deps = await _checkDependenciesAsync();
668
- } finally {
669
- clearInterval(spinInterval);
670
- _checking = false;
671
- }
672
-
673
- const ok = () => `{${COLORS.successFg}-fg}✅ ${t(_getLang(), 'installed')}{/${COLORS.successFg}-fg}`;
674
- const bad = () => `{${COLORS.errorFg}-fg}❌ ${t(_getLang(), 'notFound')}{/${COLORS.errorFg}-fg}`;
675
-
676
- const ttsOk = _deps.piper || _deps.soprano;
677
- contentBox.setContent(_c([
678
- _HDR('🔍', t(_getLang(), 'dependencyCheck')),
679
- '',
680
- ` {${COLORS.noticeFg}-fg}${t(_getLang(), 'depColumn').padEnd(14)}${t(_getLang(), 'statusColumn')}{/${COLORS.noticeFg}-fg}`,
681
- ` {${COLORS.noticeFg}-fg}${'─'.repeat(78)}{/${COLORS.noticeFg}-fg}`,
682
- ` {${COLORS.labelFg}-fg}${'Node.js'.padEnd(14)}{/${COLORS.labelFg}-fg}${_deps.node ? ok() : bad()}`,
683
- ` {${COLORS.labelFg}-fg}${'npm'.padEnd(14)}{/${COLORS.labelFg}-fg}${_deps.npm ? ok() : bad()}`,
684
- ` {${COLORS.labelFg}-fg}${'Piper TTS'.padEnd(14)}{/${COLORS.labelFg}-fg}${_deps.piper ? ok() : bad()}`,
685
- ` {${COLORS.labelFg}-fg}${'Soprano TTS'.padEnd(14)}{/${COLORS.labelFg}-fg}${_deps.soprano ? ok() : bad()}`,
686
- ` {${COLORS.labelFg}-fg}${'ffmpeg'.padEnd(14)}{/${COLORS.labelFg}-fg}${_deps.ffmpeg ? ok() : `{${COLORS.errorFg}-fg}⚠ ${t(_getLang(), 'ffmpegMissing')}{/${COLORS.errorFg}-fg}`}`,
687
- '',
688
- ttsOk
689
- ? ` {${COLORS.successFg}-fg}✅ ${t(_getLang(), 'ttsDetected')}{/${COLORS.successFg}-fg}`
690
- : ` {${COLORS.errorFg}-fg}⚠ ${t(_getLang(), 'noTtsFound')}{/${COLORS.errorFg}-fg}`,
691
- '', // blank separator
692
- '', // ← [Continue →] button here (box row 12) when TTS detected
693
- ]));
694
- if (ttsOk) {
695
- _s2ContinueBtn.setContent(_tl('continueArrowBtn'));
696
- _s2ContinueBtn.show();
697
- _s2ContinueBtn.focus();
698
- }
699
- screen.render();
700
- }
701
-
702
- function _renderScreen3() {
703
- const providers = [];
704
- if (_deps?.piper) providers.push('piper');
705
- if (_deps?.soprano) providers.push('soprano');
706
-
707
- if (providers.length === 0) providers.push('piper'); // fallback
708
- if (!_selectedProvider) _selectedProvider = providers[0];
709
-
710
- // Pad items to 96 visible chars so they fully overwrite any stale cells from Screen 2.
711
- // Selected row uses cyan bg + black text (matches button focus standard).
712
- const items = providers.map(p =>
713
- p === _selectedProvider
714
- ? `{#00e5ff-bg}{#000000-fg}{bold} ● ${p.padEnd(92)}{/bold}{/#000000-fg}{/#00e5ff-bg}`
715
- : `{${COLORS.labelFg}-fg} ${p.padEnd(93)}{/${COLORS.labelFg}-fg}`
716
- );
717
-
718
- // Pad item list to 3 entries so the Continue button sits at a fixed row
719
- // and all stale lines from Screen 2 (which has ~10 lines) are overwritten.
720
- const paddedItems = [...items];
721
- while (paddedItems.length < 3) paddedItems.push(` ${''.padEnd(93)}`);
722
-
723
- // Append trailing blank rows (space-padded) so blessed rewrites every cell that
724
- // screen 2 used. Two screen.render() calls in the same tick are batched, so the
725
- // intermediate "clear" render never fires — trailing spaces here fix that in one pass.
726
- const _blank = ' '.repeat(120);
727
- const _trail = Array(12).fill(_blank);
728
- contentBox.setContent(_c([
729
- _HDR('🎤', t(_getLang(), 'providerSelection')),
730
- '',
731
- ` {${COLORS.noticeFg}-fg}${t(_getLang(), 'availableProviders').padEnd(94)}{/${COLORS.noticeFg}-fg}`,
732
- '',
733
- ...paddedItems.map(i => ` ${i}`),
734
- ..._trail,
735
- ]));
736
- hintLine.setContent(` ${t(_getLang(), 'screen3Hint')}`);
737
- box.focus();
738
- screen.render();
739
- }
740
-
741
- function _renderScreen4() {
742
- const provider = _selectedProvider ?? 'piper';
743
- const intro = _introText || '';
744
- const folderName = getIntroDefault(process.cwd()) || 'AgentVibes';
745
- const example = `${folderName}: Here`;
746
- const voiceId = providerService?.getActiveVoiceId?.() ?? 'en_US-amy-medium';
747
-
748
- contentBox.setContent(_c([
749
- _HDR('🎤', t(_getLang(), 'providerAndVoice')),
750
- '',
751
- ` {${COLORS.labelFg}-fg}${`${t(_getLang(), 'providerLabel')}:`.padEnd(14)}{/${COLORS.labelFg}-fg}{${COLORS.valueFg}-fg}${provider}{/${COLORS.valueFg}-fg}`,
752
- ` {${COLORS.labelFg}-fg}${`${t(_getLang(), 'voiceLabel')}:`.padEnd(14)}{/${COLORS.labelFg}-fg}{${COLORS.valueFg}-fg}${voiceId}{/${COLORS.valueFg}-fg} {${COLORS.noticeFg}-fg}${t(_getLang(), 'voiceChangeHint')}{/${COLORS.noticeFg}-fg}`,
753
- '',
754
- _HDR('✍️', t(_getLang(), 'introText')),
755
- '',
756
- ` {${COLORS.labelFg}-fg}${`${t(_getLang(), 'introTextLabel')}:`.padEnd(14)}{/${COLORS.labelFg}-fg}{${COLORS.valueFg}-fg}${intro || `(${t(_getLang(), 'none')})`}{/${COLORS.valueFg}-fg}`,
757
- // ↑ [Edit] button rendered inline at box row 8, left=36
758
- '',
759
- ` {${COLORS.noticeFg}-fg}${t(_getLang(), 'example')}:{/${COLORS.noticeFg}-fg} {${COLORS.valueFg}-fg}"${example}"{/${COLORS.valueFg}-fg}`,
760
- '',
761
- '',
762
- '', // ← [✓ Accept & Install] button rendered as real widget here (box row 13)
763
- ]));
764
- hintLine.setContent(` ${t(_getLang(), 'screen4Hint')}`);
765
- _editBtn.setContent(_tl('editInstallBtn'));
766
- _acceptBtn.setContent(_tl('acceptInstallBtn'));
767
- _acceptBtn.focus();
768
- screen.render();
769
- }
770
-
771
- function _renderScreen5() {
772
- const header = _installError
773
- ? _HDR('❌', t(_getLang(), 'installationFailed'))
774
- : _installComplete
775
- ? _HDR('✅', t(_getLang(), 'installComplete'))
776
- : _HDR('⚙️', t(_getLang(), 'installing'));
777
-
778
- const hint = (_installComplete || _installError)
779
- ? ` ${t(_getLang(), 'screen5HintDone')}`
780
- : ` ${t(_getLang(), 'screen5HintWait')}`;
781
-
782
- // Show last 18 log lines so content fits in the box
783
- const MAX_LINES = 18;
784
- const visibleLog = _installLog.length > MAX_LINES
785
- ? _installLog.slice(-MAX_LINES)
786
- : _installLog;
787
-
788
- contentBox.setContent(_c([
789
- header,
790
- '',
791
- ...visibleLog,
792
- ]));
793
- hintLine.setContent(hint);
794
- screen.render();
795
- }
796
-
797
- function _showInstallNotice(message) {
798
- const width = Math.max(28, message.length + 6);
799
- const notice = blessed.box({
800
- parent: screen,
801
- top: 'center',
802
- left: 'center',
803
- width,
804
- height: 3,
805
- border: { type: 'line' },
806
- tags: true,
807
- content: `{center}${message}{/center}`,
808
- style: {
809
- fg: '#e3f2fd',
810
- bg: COLORS.contentBg,
811
- border: { fg: 'bright-cyan' },
812
- },
813
- });
814
- screen.render();
815
- setTimeout(() => { try { notice.destroy(); screen.render(); } catch {} }, 2500);
816
- }
817
-
818
- function _dismissCompletionModal() {
819
- if (_completionModalBox) {
820
- _completionModalBox.destroy();
821
- _completionModalBox = null;
822
- }
823
- _completionModalOpen = false;
824
- _screen = 0;
825
- box.hide();
826
- _showInstallNotice('Installation Complete — Settings Saved');
827
- screen.render();
828
- navigationService?.switchTab('settings');
829
- }
830
-
831
- function _showCurrentScreen() {
832
- // Show Screen 1 buttons only on screen 1
833
- if (_screen === 1) {
834
- _s1BeginBtn.show(); _s1ExitBtn.show();
835
- } else {
836
- _s1BeginBtn.hide(); _s1ExitBtn.hide();
837
- }
838
-
839
- // Screen 0 has no button widgets — nav is handled via key handlers
840
-
841
- // Screen 2 continue button: hidden on other screens; _renderScreen2 manages show/focus
842
- if (_screen !== 2) _s2ContinueBtn.hide();
843
-
844
- // Screen 5 buttons: hidden during active install, shown by _runInstall() on completion
845
- if (_screen === 5 && (_installComplete || _installError)) {
846
- _s5QuitBtn.show();
847
- _s5CustomizeBtn.show();
848
- } else {
849
- _s5QuitBtn.hide();
850
- _s5CustomizeBtn.hide();
851
- }
852
-
853
- // Show Screen 4 action buttons only on screen 4
854
- if (_screen === 4) {
855
- _editBtn.show(); _acceptBtn.show();
856
- } else {
857
- _editBtn.hide(); _acceptBtn.hide();
858
- }
859
-
860
- if (_screen !== _lastScreen) {
861
- // Nuclear clear: force-invalidate every olines cell so blessed's diff renderer
862
- // actually writes blanks to the terminal (blessed skips cells it thinks are
863
- // unchanged — setting attr=-1 is impossible for any real cell so draw() is
864
- // forced to physically rewrite every character).
865
- try {
866
- for (let r = 0; r < screen.height; r++) {
867
- const orow = screen.olines?.[r];
868
- if (!orow) continue;
869
- for (let c = 0; c < screen.width; c++) {
870
- if (orow[c]) orow[c][0] = -1;
871
- }
872
- }
873
- // Row 2 (header bottom) never becomes dirty on its own — force it so
874
- // draw() writes headerBg+spaces and overwrites any ghost terminal content.
875
- if (screen.lines?.[2]) screen.lines[2].dirty = true;
876
- } catch {}
877
-
878
- const _clearLine = ' '.repeat(150);
879
- const _clearPage = Array(25).fill(_clearLine).join('\n');
880
- contentBox.setContent(_clearPage);
881
- hintLine.setContent(_clearLine);
882
- screen.render();
883
-
884
- const targetScreen = _screen;
885
- _lastScreen = _screen;
886
- // 50 ms delay: enough for the terminal to display the blank frame before
887
- // the new screen content overwrites it. setTimeout(0) is too fast —
888
- // both renders land in the same display frame.
889
- setTimeout(() => {
890
- if (_screen !== targetScreen) return;
891
- switch (_screen) {
892
- case 0: _renderScreen0(); break;
893
- case 1: _renderScreen1(); break;
894
- case 2: _renderScreen2(); break;
895
- case 3: _renderScreen3(); break;
896
- case 4: _renderScreen4(); break;
897
- case 5: _renderScreen5(); break;
898
- }
899
- }, 50);
900
- return;
901
- }
902
- switch (_screen) {
903
- case 0: _renderScreen0(); break;
904
- case 1: _renderScreen1(); break;
905
- case 2: _renderScreen2(); break;
906
- case 3: _renderScreen3(); break;
907
- case 4: _renderScreen4(); break;
908
- case 5: _renderScreen5(); break;
909
- }
910
- }
911
-
912
- // -------------------------------------------------------------------------
913
- // Navigation
914
-
915
- // Use screen.key() instead of box.key() so handlers fire regardless of which
916
- // blessed element currently holds focus. Guard with `box.hidden` so they are
917
- // no-ops when another tab is active.
918
-
919
- screen.key(['enter'], () => {
920
- if (box.hidden || _checking) return;
921
- if (_completionModalOpen) { _dismissCompletionModal(); return; } // always first
922
- if (_screen === 0) { // Screen 0: apply selected language and advance
923
- if (languageService) languageService.setLang(SUPPORTED_LANGUAGES[_langIdx].value);
924
- _screen = 1;
925
- _showCurrentScreen();
926
- return;
927
- }
928
- if (_screen === 1) return; // Screen 1: Enter handled by Begin/Exit buttons
929
- if (_screen === 2) return; // Screen 2: Enter handled by Continue button
930
- if (_screen === 4) return; // Screen 4: Enter handled by the focused button
931
- if (_screen === 5) return; // Screen 5: Enter handled by OK button
932
- if (_screen < 5) {
933
- _screen++;
934
- _showCurrentScreen();
935
- }
936
- });
937
-
938
- screen.key(['escape'], () => {
939
- if (box.hidden || _checking) return;
940
- if (_completionModalOpen) { _dismissCompletionModal(); return; }
941
- if (_screen > 0) {
942
- _screen--;
943
- _showCurrentScreen();
944
- } else {
945
- // User pressed Escape at the first screen — they want out of the installer.
946
- // Switch to Settings so they can configure without being stuck on the install tab.
947
- // (focusMainTabBar would re-focus the install tab item, which loops back here.)
948
- setTimeout(() => navigationService?.switchTab('settings'), 0);
949
- }
950
- });
951
-
952
- screen.key(['up'], () => {
953
- if (box.hidden) return;
954
- if (_screen === 0) {
955
- _langIdx = Math.max(0, _langIdx - 1);
956
- _renderScreen0();
957
- return;
958
- }
959
- if (_screen === 3 && _deps) {
960
- const providers = [];
961
- if (_deps.piper) providers.push('piper');
962
- if (_deps.soprano) providers.push('soprano');
963
- const idx = providers.indexOf(_selectedProvider ?? providers[0]);
964
- _selectedProvider = providers[Math.max(0, idx - 1)];
965
- _renderScreen3();
966
- }
967
- });
968
-
969
- // Left arrow = go back (same logic as Escape)
970
- // Screen 4: left arrow is handled by button ←/→ navigation; use Escape to go back
971
- screen.key(['left'], () => {
972
- if (box.hidden || _checking) return;
973
- if (_screen === 4) return;
974
- if (_screen === 5) return; // Screen 5: ← handled by button nav
975
- if (_screen > 0) {
976
- _screen--;
977
- _showCurrentScreen();
978
- }
979
- });
980
-
981
- // Right arrow = go forward (same logic as Enter, without save/finish side-effects)
982
- // Screen 0: → skips language selection (keeps English)
983
- // Screen 1: right arrow handled by button ←/→ navigation
984
- screen.key(['right'], () => {
985
- if (box.hidden || _checking) return;
986
- if (_screen === 0) { // → skips: keep current _lang (default 'en') and advance
987
- if (languageService) languageService.setLang(SUPPORTED_LANGUAGES[_langIdx].value);
988
- _screen = 1;
989
- _showCurrentScreen();
990
- return;
991
- }
992
- if (_screen === 1) return;
993
- if (_screen === 2) return; // Screen 2: → handled by Continue button
994
- if (_screen === 3) { _screen++; _showCurrentScreen(); return; } // → confirms provider and advances
995
- if (_screen === 4) return; // Screen 4: → handled by button nav
996
- if (_screen === 5) return; // Screen 5: → handled by button nav
997
- });
998
-
999
- // Down arrow: Screen 0 language nav; Screen 3 provider nav; Screen 1 ↓ is handled by button key handlers
1000
- // (tab bar's el.key(['down']) → onFocus() focuses Begin, then button ↓ → Exit)
1001
- screen.key(['down'], () => {
1002
- if (box.hidden) return;
1003
- if (_screen === 0) {
1004
- _langIdx = Math.min(SUPPORTED_LANGUAGES.length - 1, _langIdx + 1);
1005
- _renderScreen0();
1006
- return;
1007
- }
1008
- if (_screen === 3 && _deps) {
1009
- const providers = [];
1010
- if (_deps.piper) providers.push('piper');
1011
- if (_deps.soprano) providers.push('soprano');
1012
- const idx = providers.indexOf(_selectedProvider ?? providers[0]);
1013
- _selectedProvider = providers[Math.min(providers.length - 1, idx + 1)];
1014
- _renderScreen3();
1015
- }
1016
- });
1017
-
1018
- // [E] on Screen 4: edit intro text inline
1019
- screen.key(['e', 'E'], () => { _doEdit(); });
1020
-
1021
- // [O] anywhere: dismiss the completion modal (OK button)
1022
- screen.key(['o', 'O'], () => {
1023
- if (box.hidden || !_completionModalOpen) return;
1024
- _dismissCompletionModal();
1025
- });
1026
-
1027
- // -------------------------------------------------------------------------
1028
- // Tab Component Contract
1029
-
1030
- return {
1031
- box,
1032
-
1033
- show() {
1034
- _screen = 0;
1035
- _langIdx = 0;
1036
- // _lang now lives in languageService — don't reset it here
1037
- _screen5Announced = false;
1038
- _installLog = [];
1039
- _installRunning = false;
1040
- _installComplete = false;
1041
- _installError = null;
1042
- _lastSpinnerIdx = -1;
1043
- if (_completionModalBox) { _completionModalBox.destroy(); _completionModalBox = null; }
1044
- _completionModalOpen = false;
1045
- box.show();
1046
- _showCurrentScreen();
1047
- screen.render();
1048
- },
1049
-
1050
- hide() {
1051
- box.hide();
1052
- screen.render();
1053
- },
1054
-
1055
- onFocus() {
1056
- // Focus the active interactive element, not just the box container
1057
- if (_screen === 0) {
1058
- box.focus(); // Screen 0 uses key handlers, no button widgets
1059
- } else if (_screen === 1) {
1060
- _s1BeginBtn.focus();
1061
- } else if (_screen === 4) {
1062
- _editBtn.focus();
1063
- } else if (_screen === 5 && (_installComplete || _installError)) {
1064
- _s5QuitBtn.focus();
1065
- } else {
1066
- box.focus();
1067
- }
1068
- screen.render();
1069
- },
1070
-
1071
- onBlur() {},
1072
-
1073
- getFooterText() {
1074
- return _tl('footerText');
1075
- },
1076
-
1077
- getFooterColor() {
1078
- return COLORS.footerBg;
1079
- },
1080
- };
1081
- }