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.
- package/.agentvibes/bmad-voice-map.json +104 -0
- package/.agentvibes/config.json +13 -12
- package/.agentvibes/copilot-sessions.log +4 -0
- package/.claude/audio/tracks/README.md +51 -52
- package/.claude/config/audio-effects-bmad.cfg +50 -0
- package/.claude/config/audio-effects.cfg +4 -4
- package/.claude/config/background-music-enabled.txt +1 -0
- package/.claude/config/personality.txt +1 -0
- package/.claude/hooks/play-tts-piper.sh +3 -1
- package/.claude/hooks/play-tts.sh +373 -301
- package/.claude/hooks/session-start-tts.sh +81 -81
- package/.claude/hooks-windows/audio-processor.ps1 +181 -0
- package/.claude/hooks-windows/play-tts-piper.ps1 +259 -245
- package/.claude/hooks-windows/play-tts.ps1 +101 -9
- package/.claude/hooks-windows/session-start-tts.ps1 +114 -114
- package/README.md +107 -7
- package/RELEASE_NOTES.md +54 -0
- package/bin/bmad-speak.js +16 -8
- package/mcp-server/server.py +15 -8
- package/package.json +1 -1
- package/src/console/app.js +899 -897
- package/src/console/footer-config.js +50 -50
- package/src/console/navigation.js +65 -65
- package/src/console/tabs/agents-tab.js +1896 -1886
- package/src/console/tabs/music-tab.js +1046 -1039
- package/src/console/tabs/placeholder-tab.js +81 -80
- package/src/console/tabs/settings-tab.js +939 -3988
- package/src/console/tabs/setup-tab.js +1811 -0
- package/src/console/tabs/voices-tab.js +1720 -1713
- package/src/installer.js +6147 -6092
- package/src/services/llm-provider-service.js +407 -0
- package/src/services/navigation-service.js +123 -123
- package/src/services/tts-engine-service.js +69 -0
- package/.claude/audio/tracks/dreamy_house_loop.mp3 +0 -0
- package/src/console/tabs/install-tab.js +0 -1081
|
@@ -0,0 +1,1811 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentVibes TUI Console — Setup Tab (Unified Setup Wizard)
|
|
3
|
+
*
|
|
4
|
+
* Replaces install-tab.js + llm-providers-tab.js with a single unified tab.
|
|
5
|
+
*
|
|
6
|
+
* Implements the Tab Component Contract:
|
|
7
|
+
* createSetupTab(screen, services) → { box, show, hide, onFocus, onBlur, getFooterText, getFooterColor }
|
|
8
|
+
*
|
|
9
|
+
* 4-screen wizard flow:
|
|
10
|
+
* Screen 0: Language picker
|
|
11
|
+
* Screen 1: Dependency check
|
|
12
|
+
* Screen 2: TTS Engine selection (new)
|
|
13
|
+
* Screen 3: LLM Providers (new — install/remove/configure)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
import { execFile } from 'node:child_process';
|
|
19
|
+
import { promisify } from 'node:util';
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import { promises as _fsP } from 'node:fs';
|
|
22
|
+
import { SUPPORTED_LANGUAGES, t } from '../../i18n/strings.js';
|
|
23
|
+
import {
|
|
24
|
+
PROVIDERS,
|
|
25
|
+
checkClaudeInstalled, checkCopilotInstalled, checkCodexInstalled,
|
|
26
|
+
installClaudeMcp, removeClaudeMcp,
|
|
27
|
+
installCopilotMcp, removeCopilotMcp,
|
|
28
|
+
installCopilotInstructions, removeCopilotInstructions,
|
|
29
|
+
installCodexMcp, removeCodexMcp,
|
|
30
|
+
installCodexInstructions, installCodexHooks,
|
|
31
|
+
removeCodexInstructions, removeCodexHooks,
|
|
32
|
+
loadLlmConfigSync, saveLlmConfigSync,
|
|
33
|
+
} from '../../services/llm-provider-service.js';
|
|
34
|
+
import {
|
|
35
|
+
getAvailableEngines, getEngineStatuses, checkEngineInstalled,
|
|
36
|
+
} from '../../services/tts-engine-service.js';
|
|
37
|
+
import { openReverbPicker, REVERB_PRESETS } from '../widgets/reverb-picker.js';
|
|
38
|
+
import { openTrackPicker, openVolumeInput } from '../widgets/track-picker.js';
|
|
39
|
+
import { formatTrackName } from '../widgets/format-utils.js';
|
|
40
|
+
import { destroyList } from '../widgets/destroy-list.js';
|
|
41
|
+
import { scanInstalledVoices, getVoiceMeta, PIPER_VOICES_DIR, SAMPLE_PHRASES, parseMultiSpeaker } from './voices-tab.js';
|
|
42
|
+
import { attachBtnBlink } from './agents-tab.js';
|
|
43
|
+
import { buildAudioEnv, detectWavPlayer } from '../audio-env.js';
|
|
44
|
+
import { spawn } from 'node:child_process';
|
|
45
|
+
import os from 'node:os';
|
|
46
|
+
import crypto from 'node:crypto';
|
|
47
|
+
|
|
48
|
+
const _execFileAsync = promisify(execFile);
|
|
49
|
+
|
|
50
|
+
const IS_TEST = process.env.AGENTVIBES_TEST_MODE === 'true';
|
|
51
|
+
|
|
52
|
+
let blessed;
|
|
53
|
+
if (!IS_TEST) {
|
|
54
|
+
const { default: b } = await import('blessed');
|
|
55
|
+
blessed = b;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Named ANSI colors only — hex renders as white on Paul's terminal
|
|
60
|
+
|
|
61
|
+
const COLORS = {
|
|
62
|
+
contentBg: 'black',
|
|
63
|
+
sectionHdr: 'bright-cyan',
|
|
64
|
+
labelFg: 'white',
|
|
65
|
+
valueFg: 'yellow',
|
|
66
|
+
brandPink: 'magenta',
|
|
67
|
+
successFg: 'green',
|
|
68
|
+
errorFg: 'red',
|
|
69
|
+
btnDefault: 'blue',
|
|
70
|
+
btnFocus: 'green',
|
|
71
|
+
btnFocusFg: 'white',
|
|
72
|
+
btnPress: 'magenta',
|
|
73
|
+
borderFg: 'bright-cyan',
|
|
74
|
+
footerBg: 'blue',
|
|
75
|
+
noticeFg: 'white',
|
|
76
|
+
btnBg: 'blue',
|
|
77
|
+
btnFg: 'white',
|
|
78
|
+
btnFocusBg: 'cyan',
|
|
79
|
+
removeBg: 'red',
|
|
80
|
+
removeFocusBg: 'magenta',
|
|
81
|
+
cfgBg: 'green',
|
|
82
|
+
cfgFocusBg: 'yellow',
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const FOOTER_TEXT = '[Enter] Continue [Esc] Back [Tab] Next Tab [Q] Quit';
|
|
86
|
+
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// Exported pure helpers (kept from install-tab for backward compat)
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Returns the default intro text suggestion (project folder name).
|
|
92
|
+
* @param {string} projectDir
|
|
93
|
+
* @returns {string}
|
|
94
|
+
*/
|
|
95
|
+
export function getIntroDefault(projectDir) {
|
|
96
|
+
if (!projectDir) return '';
|
|
97
|
+
return path.basename(projectDir);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Format the TTS greeting message.
|
|
102
|
+
* @param {string} introText - User's intro text (may be empty)
|
|
103
|
+
* @param {string} projectName - Project folder name
|
|
104
|
+
* @returns {string}
|
|
105
|
+
*/
|
|
106
|
+
export function formatGreeting(introText, projectName) {
|
|
107
|
+
const name = introText || projectName || 'AgentVibes';
|
|
108
|
+
return `${name} is ready! Welcome to AgentVibes. Love AgentVibes? We'd really appreciate it if you could give us a star on GitHub.`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// Dependency detection helpers
|
|
113
|
+
|
|
114
|
+
async function _commandExistsAsync(cmd) {
|
|
115
|
+
try {
|
|
116
|
+
const opts = { stdio: 'pipe', timeout: 5000 };
|
|
117
|
+
if (process.platform === 'win32') {
|
|
118
|
+
opts.shell = true;
|
|
119
|
+
await _execFileAsync(`${cmd} --version`, [], opts);
|
|
120
|
+
} else {
|
|
121
|
+
await _execFileAsync(cmd, ['--version'], opts);
|
|
122
|
+
}
|
|
123
|
+
return true;
|
|
124
|
+
} catch (err) {
|
|
125
|
+
if (err.code === 'ENOENT') return false;
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function _checkDependenciesAsync() {
|
|
131
|
+
const [node, npm, piperCmd, sopranoTts, sopranoWebui, ffmpeg] = await Promise.all([
|
|
132
|
+
_commandExistsAsync('node'),
|
|
133
|
+
_commandExistsAsync('npm'),
|
|
134
|
+
_commandExistsAsync('piper'),
|
|
135
|
+
_commandExistsAsync('soprano-tts'),
|
|
136
|
+
_commandExistsAsync('soprano-webui'),
|
|
137
|
+
_commandExistsAsync('ffmpeg'),
|
|
138
|
+
]);
|
|
139
|
+
|
|
140
|
+
let piper = piperCmd;
|
|
141
|
+
if (!piper && process.platform === 'win32') {
|
|
142
|
+
const localAppData = process.env.LOCALAPPDATA ||
|
|
143
|
+
(process.env.USERPROFILE ? path.join(process.env.USERPROFILE, 'AppData', 'Local') : null);
|
|
144
|
+
if (localAppData) {
|
|
145
|
+
piper = fs.existsSync(path.join(localAppData, 'Programs', 'Piper', 'piper.exe'));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return { node, npm, piper, soprano: sopranoTts || sopranoWebui, ffmpeg };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
// Test stub
|
|
154
|
+
|
|
155
|
+
function createTestStub() {
|
|
156
|
+
return {
|
|
157
|
+
box: {},
|
|
158
|
+
show: () => {},
|
|
159
|
+
hide: () => {},
|
|
160
|
+
onFocus: () => {},
|
|
161
|
+
onBlur: () => {},
|
|
162
|
+
getFooterText: () => t('en', 'footerText'),
|
|
163
|
+
getFooterColor: () => COLORS.footerBg,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Create the Setup tab component (unified install + provider configuration).
|
|
171
|
+
*
|
|
172
|
+
* @param {object} screen - Blessed screen instance (or test stub)
|
|
173
|
+
* @param {object} services
|
|
174
|
+
* @returns {{ box, show, hide, onFocus, onBlur, getFooterText, getFooterColor }}
|
|
175
|
+
*/
|
|
176
|
+
export function createSetupTab(screen, services) {
|
|
177
|
+
if (IS_TEST) return createTestStub();
|
|
178
|
+
|
|
179
|
+
const { configService, providerService, navigationService, focusMainTabBar, languageService } = services;
|
|
180
|
+
|
|
181
|
+
const targetDir = process.env.INIT_CWD || process.cwd();
|
|
182
|
+
const _thisFile = fileURLToPath(import.meta.url);
|
|
183
|
+
const packageDir = path.resolve(path.dirname(_thisFile), '..', '..', '..');
|
|
184
|
+
|
|
185
|
+
// -------------------------------------------------------------------------
|
|
186
|
+
// Container
|
|
187
|
+
|
|
188
|
+
const box = blessed.box({
|
|
189
|
+
parent: screen,
|
|
190
|
+
top: 5,
|
|
191
|
+
left: 0,
|
|
192
|
+
width: '100%',
|
|
193
|
+
bottom: 2,
|
|
194
|
+
hidden: true,
|
|
195
|
+
style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
|
|
196
|
+
border: { type: 'line' },
|
|
197
|
+
borderStyle: { fg: COLORS.borderFg },
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// -------------------------------------------------------------------------
|
|
201
|
+
// Wizard state
|
|
202
|
+
|
|
203
|
+
let _screen = 0;
|
|
204
|
+
let _lastScreen = -1;
|
|
205
|
+
const _getLang = () => languageService?.getLang() ?? 'en';
|
|
206
|
+
const _tl = (key) => languageService?.t(key) ?? t('en', key);
|
|
207
|
+
let _langIdx = 0;
|
|
208
|
+
let _deps = null;
|
|
209
|
+
let _checking = false;
|
|
210
|
+
|
|
211
|
+
// First-run detection: evaluated at show() time so async config init is complete
|
|
212
|
+
function _isFirstRun() {
|
|
213
|
+
return !(configService?.getConfig?.()?.setupCompleted);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// -------------------------------------------------------------------------
|
|
217
|
+
// Content area
|
|
218
|
+
|
|
219
|
+
const contentBox = blessed.box({
|
|
220
|
+
parent: box,
|
|
221
|
+
top: 1,
|
|
222
|
+
left: 2,
|
|
223
|
+
width: '96%',
|
|
224
|
+
bottom: 5,
|
|
225
|
+
tags: true,
|
|
226
|
+
wrap: false,
|
|
227
|
+
scrollable: false,
|
|
228
|
+
style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const hintLine = blessed.text({
|
|
232
|
+
parent: box,
|
|
233
|
+
bottom: 2,
|
|
234
|
+
left: 2,
|
|
235
|
+
right: 2,
|
|
236
|
+
tags: true,
|
|
237
|
+
content: '',
|
|
238
|
+
style: { fg: COLORS.noticeFg, bg: COLORS.contentBg },
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
function _c(lines) { return lines.join('\n'); }
|
|
242
|
+
|
|
243
|
+
// -------------------------------------------------------------------------
|
|
244
|
+
// Shared button factory
|
|
245
|
+
|
|
246
|
+
function _createBtn(label, bg, onClick, textColor = 'white') {
|
|
247
|
+
const btn = blessed.button({
|
|
248
|
+
parent: box,
|
|
249
|
+
content: label,
|
|
250
|
+
mouse: true,
|
|
251
|
+
keys: true,
|
|
252
|
+
shrink: true,
|
|
253
|
+
hidden: true,
|
|
254
|
+
padding: { left: 1, right: 1 },
|
|
255
|
+
style: {
|
|
256
|
+
bg,
|
|
257
|
+
fg: textColor,
|
|
258
|
+
focus: { bg: COLORS.btnFocus, fg: COLORS.btnFocusFg, bold: true },
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
let _blinkInterval = null;
|
|
263
|
+
const _origLabel = label;
|
|
264
|
+
btn.on('focus', () => {
|
|
265
|
+
btn.style.bg = COLORS.btnFocus;
|
|
266
|
+
btn.style.fg = COLORS.btnFocusFg;
|
|
267
|
+
btn.setContent(`\u25ba ${_origLabel} \u25c4`);
|
|
268
|
+
let _on = true;
|
|
269
|
+
screen.render();
|
|
270
|
+
_blinkInterval = setInterval(() => {
|
|
271
|
+
_on = !_on;
|
|
272
|
+
btn.setContent(_on ? `\u25ba ${_origLabel} \u25c4` : ` ${_origLabel} `);
|
|
273
|
+
screen.render();
|
|
274
|
+
}, 500);
|
|
275
|
+
});
|
|
276
|
+
btn.on('blur', () => {
|
|
277
|
+
if (_blinkInterval) { clearInterval(_blinkInterval); _blinkInterval = null; }
|
|
278
|
+
btn.style.bg = bg;
|
|
279
|
+
btn.style.fg = textColor;
|
|
280
|
+
btn.setContent(_origLabel);
|
|
281
|
+
screen.render();
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
btn.key(['enter', 'space'], () => {
|
|
285
|
+
btn.style.bg = COLORS.btnPress;
|
|
286
|
+
btn.style.fg = 'white';
|
|
287
|
+
screen.render();
|
|
288
|
+
setTimeout(() => {
|
|
289
|
+
btn.style.bg = bg;
|
|
290
|
+
btn.style.fg = textColor;
|
|
291
|
+
screen.render();
|
|
292
|
+
onClick();
|
|
293
|
+
}, 150);
|
|
294
|
+
});
|
|
295
|
+
btn.on('click', () => btn.press());
|
|
296
|
+
return btn;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// =========================================================================
|
|
300
|
+
// SCREEN 0: Language picker (kept as-is)
|
|
301
|
+
// =========================================================================
|
|
302
|
+
|
|
303
|
+
// =========================================================================
|
|
304
|
+
// SCREEN 1: Dependency check (was Screen 2, renumbered)
|
|
305
|
+
// =========================================================================
|
|
306
|
+
|
|
307
|
+
const _s1ContinueBtn = _createBtn('Continue ->', 'blue', () => {
|
|
308
|
+
_screen++;
|
|
309
|
+
_showCurrentScreen();
|
|
310
|
+
});
|
|
311
|
+
_s1ContinueBtn.top = 12; _s1ContinueBtn.left = 4;
|
|
312
|
+
_s1ContinueBtn.key(['right'], () => { _screen++; _showCurrentScreen(); });
|
|
313
|
+
|
|
314
|
+
// =========================================================================
|
|
315
|
+
// SCREEN 2: TTS Engine selection (new)
|
|
316
|
+
// =========================================================================
|
|
317
|
+
|
|
318
|
+
// TTS engine install buttons — created once, shown/hidden per screen
|
|
319
|
+
const _ttsEngineRows = [];
|
|
320
|
+
const _ttsFocusableItems = [];
|
|
321
|
+
let _ttsFocusIndex = 0;
|
|
322
|
+
|
|
323
|
+
const _ttsEngines = getAvailableEngines();
|
|
324
|
+
for (let i = 0; i < _ttsEngines.length; i++) {
|
|
325
|
+
const engine = _ttsEngines[i];
|
|
326
|
+
const yOff = 5 + (i * 3);
|
|
327
|
+
|
|
328
|
+
const nameLabel = blessed.text({
|
|
329
|
+
parent: box, top: yOff, left: 2, tags: true, hidden: true,
|
|
330
|
+
content: '', style: { bg: COLORS.contentBg },
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
const statusLabel = blessed.text({
|
|
334
|
+
parent: box, top: yOff, left: 22, tags: true, hidden: true,
|
|
335
|
+
content: '', style: { bg: COLORS.contentBg },
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
const descLabel = blessed.text({
|
|
339
|
+
parent: box, top: yOff + 1, left: 4, tags: true, hidden: true,
|
|
340
|
+
content: `{cyan-fg}${engine.desc}{/cyan-fg}`,
|
|
341
|
+
style: { bg: COLORS.contentBg },
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
const installBtn = blessed.button({
|
|
345
|
+
parent: box, top: yOff, left: 40, width: 14, height: 1,
|
|
346
|
+
content: ' Install ', tags: true, mouse: true, keys: true, hidden: true,
|
|
347
|
+
style: {
|
|
348
|
+
fg: COLORS.btnFg, bg: COLORS.btnBg,
|
|
349
|
+
focus: { fg: 'black', bg: COLORS.btnFocusBg },
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
installBtn.on('press', () => _handleTtsInstall(engine));
|
|
354
|
+
installBtn.key(['enter', 'space'], () => _handleTtsInstall(engine));
|
|
355
|
+
installBtn.key(['tab', 'down'], () => _cycleTtsFocus(1));
|
|
356
|
+
installBtn.key(['S-tab', 'up'], () => _cycleTtsFocus(-1));
|
|
357
|
+
installBtn.key(['escape'], () => {
|
|
358
|
+
if (typeof focusMainTabBar === 'function') { focusMainTabBar(); screen.render(); }
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
_ttsEngineRows.push({ engine, nameLabel, statusLabel, descLabel, installBtn });
|
|
362
|
+
if (!engine.native) _ttsFocusableItems.push(installBtn);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function _cycleTtsFocus(dir) {
|
|
366
|
+
const items = _ttsFocusableItems.filter(b => !b.hidden);
|
|
367
|
+
if (!items.length) return;
|
|
368
|
+
_ttsFocusIndex = (_ttsFocusIndex + dir + items.length) % items.length;
|
|
369
|
+
items[_ttsFocusIndex].focus();
|
|
370
|
+
screen.render();
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function _showTtsEngineRows() {
|
|
374
|
+
for (const row of _ttsEngineRows) {
|
|
375
|
+
const installed = checkEngineInstalled(row.engine.id);
|
|
376
|
+
row.nameLabel.setContent(`{bold}{white-fg}${row.engine.name}{/white-fg}{/bold}`);
|
|
377
|
+
row.statusLabel.setContent(installed
|
|
378
|
+
? '{green-fg}[Installed]{/green-fg}'
|
|
379
|
+
: '{yellow-fg}[Not Found]{/yellow-fg}');
|
|
380
|
+
row.nameLabel.show();
|
|
381
|
+
row.statusLabel.show();
|
|
382
|
+
row.descLabel.show();
|
|
383
|
+
if (!installed && !row.engine.native) {
|
|
384
|
+
row.installBtn.show();
|
|
385
|
+
} else {
|
|
386
|
+
row.installBtn.hide();
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function _hideTtsEngineRows() {
|
|
392
|
+
for (const row of _ttsEngineRows) {
|
|
393
|
+
row.nameLabel.hide();
|
|
394
|
+
row.statusLabel.hide();
|
|
395
|
+
row.descLabel.hide();
|
|
396
|
+
row.installBtn.hide();
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
let _ttsInstalling = false;
|
|
401
|
+
async function _handleTtsInstall(engine) {
|
|
402
|
+
if (!engine.installCmd || _ttsInstalling) return;
|
|
403
|
+
_ttsInstalling = true;
|
|
404
|
+
|
|
405
|
+
// Show installing status
|
|
406
|
+
const row = _ttsEngineRows.find(r => r.engine.id === engine.id);
|
|
407
|
+
if (row) {
|
|
408
|
+
row.statusLabel.setContent('{yellow-fg}[Installing...]{/yellow-fg}');
|
|
409
|
+
screen.render();
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
try {
|
|
413
|
+
const opts = { stdio: 'pipe', timeout: 120000 };
|
|
414
|
+
if (process.platform === 'win32') {
|
|
415
|
+
opts.shell = true;
|
|
416
|
+
await _execFileAsync(engine.installCmd, [], opts);
|
|
417
|
+
} else {
|
|
418
|
+
const parts = engine.installCmd.split(' ');
|
|
419
|
+
await _execFileAsync(parts[0], parts.slice(1), opts);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Re-check and update status
|
|
423
|
+
const installed = checkEngineInstalled(engine.id);
|
|
424
|
+
if (row) {
|
|
425
|
+
row.statusLabel.setContent(installed
|
|
426
|
+
? '{green-fg}[Installed]{/green-fg}'
|
|
427
|
+
: '{red-fg}[Install Failed]{/red-fg}');
|
|
428
|
+
if (installed) row.installBtn.hide();
|
|
429
|
+
}
|
|
430
|
+
} catch (err) {
|
|
431
|
+
if (row) {
|
|
432
|
+
row.statusLabel.setContent(`{red-fg}[Failed]{/red-fg}`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
_ttsInstalling = false;
|
|
436
|
+
screen.render();
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Continue button for Screen 2
|
|
440
|
+
const _s2ContinueBtn = _createBtn('Continue ->', 'blue', () => {
|
|
441
|
+
if (_screen < 3) { _screen++; _showCurrentScreen(); }
|
|
442
|
+
});
|
|
443
|
+
_s2ContinueBtn.hidden = true;
|
|
444
|
+
|
|
445
|
+
// =========================================================================
|
|
446
|
+
// SCREEN 3: LLM Providers (new — from llm-providers-tab)
|
|
447
|
+
// =========================================================================
|
|
448
|
+
|
|
449
|
+
let installedState = {};
|
|
450
|
+
let providerFocusableItems = [];
|
|
451
|
+
let providerFocusIndex = 0;
|
|
452
|
+
let providerView = 'list'; // 'list' or 'info'
|
|
453
|
+
|
|
454
|
+
// Provider row widgets (created once)
|
|
455
|
+
const providerRows = [];
|
|
456
|
+
const providerStatusTexts = [];
|
|
457
|
+
|
|
458
|
+
// Info box for provider details
|
|
459
|
+
const infoBox = blessed.box({
|
|
460
|
+
parent: box,
|
|
461
|
+
top: 1,
|
|
462
|
+
left: 2,
|
|
463
|
+
width: '96%',
|
|
464
|
+
bottom: 1,
|
|
465
|
+
hidden: true,
|
|
466
|
+
scrollable: true,
|
|
467
|
+
alwaysScroll: true,
|
|
468
|
+
tags: true,
|
|
469
|
+
keys: true,
|
|
470
|
+
vi: true,
|
|
471
|
+
mouse: true,
|
|
472
|
+
scrollbar: { ch: '|', style: { fg: 'cyan' } },
|
|
473
|
+
style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
// Provider header
|
|
477
|
+
const providerHeader = blessed.text({
|
|
478
|
+
parent: box,
|
|
479
|
+
top: 0,
|
|
480
|
+
left: 2,
|
|
481
|
+
tags: true,
|
|
482
|
+
hidden: true,
|
|
483
|
+
content: '{bold}{cyan-fg}LLM Providers{/cyan-fg}{/bold} Configure AgentVibes for your AI assistant:',
|
|
484
|
+
style: { bg: COLORS.contentBg },
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
function createProviderRow(provider, rowIndex) {
|
|
488
|
+
const yOffset = 2 + (rowIndex * 3);
|
|
489
|
+
|
|
490
|
+
const label = blessed.text({
|
|
491
|
+
parent: box,
|
|
492
|
+
top: yOffset,
|
|
493
|
+
left: 2,
|
|
494
|
+
tags: true,
|
|
495
|
+
hidden: true,
|
|
496
|
+
content: `{bold}{white-fg}${provider.name}{/white-fg}{/bold} {cyan-fg}${provider.desc}{/cyan-fg}`,
|
|
497
|
+
style: { bg: COLORS.contentBg },
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
const statusText = blessed.text({
|
|
501
|
+
parent: box,
|
|
502
|
+
top: yOffset + 1,
|
|
503
|
+
left: 4,
|
|
504
|
+
tags: true,
|
|
505
|
+
hidden: true,
|
|
506
|
+
content: '{yellow-fg}Checking...{/yellow-fg}',
|
|
507
|
+
style: { bg: COLORS.contentBg },
|
|
508
|
+
});
|
|
509
|
+
providerStatusTexts.push({ id: provider.id, widget: statusText });
|
|
510
|
+
|
|
511
|
+
const installBtn = blessed.button({
|
|
512
|
+
parent: box,
|
|
513
|
+
top: yOffset + 1,
|
|
514
|
+
left: 30,
|
|
515
|
+
width: 14,
|
|
516
|
+
height: 1,
|
|
517
|
+
content: ' Install ',
|
|
518
|
+
tags: true,
|
|
519
|
+
mouse: true,
|
|
520
|
+
keys: true,
|
|
521
|
+
hidden: true,
|
|
522
|
+
style: {
|
|
523
|
+
fg: COLORS.btnFg,
|
|
524
|
+
bg: COLORS.btnBg,
|
|
525
|
+
focus: { fg: 'black', bg: COLORS.btnFocusBg },
|
|
526
|
+
},
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
const removeBtn = blessed.button({
|
|
530
|
+
parent: box,
|
|
531
|
+
top: yOffset + 1,
|
|
532
|
+
left: 46,
|
|
533
|
+
width: 12,
|
|
534
|
+
height: 1,
|
|
535
|
+
content: ' Remove ',
|
|
536
|
+
tags: true,
|
|
537
|
+
mouse: true,
|
|
538
|
+
keys: true,
|
|
539
|
+
hidden: true,
|
|
540
|
+
style: {
|
|
541
|
+
fg: COLORS.btnFg,
|
|
542
|
+
bg: COLORS.removeBg,
|
|
543
|
+
focus: { fg: 'black', bg: COLORS.removeFocusBg },
|
|
544
|
+
},
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
const configBtn = blessed.button({
|
|
548
|
+
parent: box,
|
|
549
|
+
top: yOffset + 1,
|
|
550
|
+
left: 60,
|
|
551
|
+
width: 14,
|
|
552
|
+
height: 1,
|
|
553
|
+
content: ' Configure ',
|
|
554
|
+
tags: true,
|
|
555
|
+
mouse: true,
|
|
556
|
+
keys: true,
|
|
557
|
+
hidden: true,
|
|
558
|
+
style: {
|
|
559
|
+
fg: 'black',
|
|
560
|
+
bg: COLORS.cfgBg,
|
|
561
|
+
focus: { fg: 'black', bg: COLORS.cfgFocusBg },
|
|
562
|
+
},
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
// Wire actions
|
|
566
|
+
installBtn.on('press', async () => { await handleProviderInstall(provider); });
|
|
567
|
+
installBtn.key(['enter', 'space'], async () => { await handleProviderInstall(provider); });
|
|
568
|
+
|
|
569
|
+
removeBtn.on('press', async () => { await handleProviderRemove(provider); });
|
|
570
|
+
removeBtn.key(['enter', 'space'], async () => { await handleProviderRemove(provider); });
|
|
571
|
+
|
|
572
|
+
configBtn.on('press', async () => { await handleProviderConfigure(provider); });
|
|
573
|
+
configBtn.key(['enter', 'space'], async () => { await handleProviderConfigure(provider); });
|
|
574
|
+
|
|
575
|
+
// Navigation on each button
|
|
576
|
+
for (const btn of [installBtn, removeBtn, configBtn]) {
|
|
577
|
+
btn.key(['tab', 'right'], () => { cycleFocus(1); });
|
|
578
|
+
btn.key(['S-tab', 'left'], () => { cycleFocus(-1); });
|
|
579
|
+
btn.key(['escape'], () => {
|
|
580
|
+
if (typeof focusMainTabBar === 'function') {
|
|
581
|
+
focusMainTabBar();
|
|
582
|
+
screen.render();
|
|
583
|
+
}
|
|
584
|
+
});
|
|
585
|
+
btn.key(['up'], () => {
|
|
586
|
+
const prevIdx = providerFocusIndex - 3;
|
|
587
|
+
if (prevIdx >= 0) {
|
|
588
|
+
providerFocusIndex = prevIdx;
|
|
589
|
+
providerFocusableItems[providerFocusIndex].focus();
|
|
590
|
+
screen.render();
|
|
591
|
+
} else if (typeof focusMainTabBar === 'function') {
|
|
592
|
+
focusMainTabBar();
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
btn.key(['down'], () => {
|
|
596
|
+
const nextIdx = providerFocusIndex + 3;
|
|
597
|
+
if (nextIdx < providerFocusableItems.length) {
|
|
598
|
+
providerFocusIndex = nextIdx;
|
|
599
|
+
providerFocusableItems[providerFocusIndex].focus();
|
|
600
|
+
screen.render();
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
providerRows.push({ id: provider.id, label, statusText, installBtn, removeBtn, configBtn });
|
|
606
|
+
return { installBtn, removeBtn, configBtn };
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Build all provider rows
|
|
610
|
+
for (let i = 0; i < PROVIDERS.length; i++) {
|
|
611
|
+
const { installBtn, removeBtn, configBtn } = createProviderRow(PROVIDERS[i], i);
|
|
612
|
+
providerFocusableItems.push(installBtn, removeBtn, configBtn);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function cycleFocus(dir) {
|
|
616
|
+
providerFocusIndex = (providerFocusIndex + dir + providerFocusableItems.length) % providerFocusableItems.length;
|
|
617
|
+
providerFocusableItems[providerFocusIndex].focus();
|
|
618
|
+
screen.render();
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// ── Provider install/remove handlers ──────────────────────────────────────
|
|
622
|
+
|
|
623
|
+
async function handleProviderInstall(provider) {
|
|
624
|
+
if (provider.id === 'claude-code') {
|
|
625
|
+
const wasInstalled = installedState[provider.id];
|
|
626
|
+
const result = await installClaudeMcp(targetDir);
|
|
627
|
+
await refreshInstalledState();
|
|
628
|
+
showClaudeCodeInfo(result, wasInstalled);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if (provider.id === 'github-copilot') {
|
|
633
|
+
const wasInstalled = installedState[provider.id];
|
|
634
|
+
const result = await installCopilotMcp(targetDir);
|
|
635
|
+
await installCopilotInstructions(targetDir, packageDir);
|
|
636
|
+
await refreshInstalledState();
|
|
637
|
+
showCopilotInfo(result, wasInstalled);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if (provider.id === 'openai-codex') {
|
|
641
|
+
const wasInstalled = installedState[provider.id];
|
|
642
|
+
const result = await installCodexMcp(targetDir);
|
|
643
|
+
await installCopilotMcp(targetDir);
|
|
644
|
+
await installCodexInstructions(targetDir, packageDir);
|
|
645
|
+
await installCodexHooks(targetDir, packageDir);
|
|
646
|
+
await refreshInstalledState();
|
|
647
|
+
showCodexInfo(result, wasInstalled);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
async function handleProviderRemove(provider) {
|
|
652
|
+
if (provider.id === 'claude-code') {
|
|
653
|
+
await removeClaudeMcp(targetDir);
|
|
654
|
+
await refreshInstalledState();
|
|
655
|
+
showRemoveInfo('claude-code');
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (provider.id === 'github-copilot') {
|
|
660
|
+
await removeCopilotMcp(targetDir);
|
|
661
|
+
await removeCopilotInstructions(targetDir);
|
|
662
|
+
await refreshInstalledState();
|
|
663
|
+
showRemoveInfo('github-copilot');
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (provider.id === 'openai-codex') {
|
|
667
|
+
await removeCodexMcp(targetDir);
|
|
668
|
+
await removeCopilotMcp(targetDir);
|
|
669
|
+
await removeCodexInstructions(targetDir);
|
|
670
|
+
await removeCodexHooks(targetDir);
|
|
671
|
+
await refreshInstalledState();
|
|
672
|
+
showRemoveInfo('openai-codex');
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// ── Provider configure handler ────────────────────────────────────────────
|
|
677
|
+
|
|
678
|
+
async function handleProviderConfigure(provider) {
|
|
679
|
+
const llmKeyMap = {
|
|
680
|
+
'claude-code': 'claude-code',
|
|
681
|
+
'github-copilot': 'copilot',
|
|
682
|
+
'openai-codex': 'codex',
|
|
683
|
+
};
|
|
684
|
+
const llmKey = llmKeyMap[provider.id] || provider.id;
|
|
685
|
+
const config = loadLlmConfigSync(llmKey, targetDir);
|
|
686
|
+
_openLlmConfigModal(provider, llmKey, config);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// ── LLM Config Modal ─────────────────────────────────────────────────────
|
|
690
|
+
|
|
691
|
+
function _openLlmConfigModal(provider, llmKey, config) {
|
|
692
|
+
// Guard against double-open (key repeat, double-click)
|
|
693
|
+
if (navigationService?.isModalOpen()) return;
|
|
694
|
+
let _closed = false;
|
|
695
|
+
navigationService?.openModal();
|
|
696
|
+
|
|
697
|
+
const draft = {
|
|
698
|
+
ttsEngine: config.ttsEngine || '',
|
|
699
|
+
voice: config.voice || '',
|
|
700
|
+
pretext: config.pretext || '',
|
|
701
|
+
reverbPreset: config.effects || 'off',
|
|
702
|
+
bgTrack: config.bgTrack || '',
|
|
703
|
+
bgVolume: config.bgVolume || '0.15',
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
const modal = blessed.box({
|
|
707
|
+
parent: screen,
|
|
708
|
+
top: 'center',
|
|
709
|
+
left: 'center',
|
|
710
|
+
width: 72,
|
|
711
|
+
height: 21,
|
|
712
|
+
border: { type: 'line' },
|
|
713
|
+
tags: true,
|
|
714
|
+
label: ` {bold}{cyan-fg} ${provider.name} -- Audio Config {/cyan-fg}{/bold} `,
|
|
715
|
+
style: {
|
|
716
|
+
fg: COLORS.labelFg,
|
|
717
|
+
bg: COLORS.contentBg,
|
|
718
|
+
border: { fg: 'cyan' },
|
|
719
|
+
},
|
|
720
|
+
});
|
|
721
|
+
modal.setFront();
|
|
722
|
+
|
|
723
|
+
// Field definitions
|
|
724
|
+
const FIELDS = [
|
|
725
|
+
{ key: 'ttsEngine', label: 'TTS Engine', getValue: () => draft.ttsEngine || '(global default)' },
|
|
726
|
+
{ key: 'voice', label: 'Voice', getValue: () => draft.voice || '(global default)' },
|
|
727
|
+
{ key: 'pretext', label: 'Pretext', getValue: () => draft.pretext || '(none)' },
|
|
728
|
+
{ key: 'reverb', label: 'Reverb', getValue: () => {
|
|
729
|
+
const p = REVERB_PRESETS.find(r => r.value === draft.reverbPreset);
|
|
730
|
+
return p ? p.label : draft.reverbPreset || 'Off';
|
|
731
|
+
}},
|
|
732
|
+
{ key: 'bgTrack', label: 'Music Track', getValue: () => formatTrackName(draft.bgTrack) || '(default)' },
|
|
733
|
+
{ key: 'bgVolume', label: 'Music Vol', getValue: () => {
|
|
734
|
+
const pct = Math.round(parseFloat(draft.bgVolume) * 100);
|
|
735
|
+
return `${pct}%`;
|
|
736
|
+
}},
|
|
737
|
+
];
|
|
738
|
+
|
|
739
|
+
function _fieldItems() {
|
|
740
|
+
return FIELDS.map(f => {
|
|
741
|
+
const label = f.label.padEnd(14);
|
|
742
|
+
return ` ${label} ${f.getValue()}`;
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const fieldList = blessed.list({
|
|
747
|
+
parent: modal,
|
|
748
|
+
top: 1,
|
|
749
|
+
left: 2,
|
|
750
|
+
right: 2,
|
|
751
|
+
height: FIELDS.length + 2,
|
|
752
|
+
keys: true,
|
|
753
|
+
vi: false,
|
|
754
|
+
mouse: true,
|
|
755
|
+
border: { type: 'line' },
|
|
756
|
+
tags: true,
|
|
757
|
+
style: {
|
|
758
|
+
fg: COLORS.labelFg,
|
|
759
|
+
bg: COLORS.contentBg,
|
|
760
|
+
border: { fg: 'blue' },
|
|
761
|
+
selected: { bg: 'blue', fg: 'yellow' },
|
|
762
|
+
item: { fg: COLORS.labelFg },
|
|
763
|
+
},
|
|
764
|
+
});
|
|
765
|
+
fieldList.setItems(_fieldItems());
|
|
766
|
+
|
|
767
|
+
blessed.text({
|
|
768
|
+
parent: modal,
|
|
769
|
+
bottom: 4,
|
|
770
|
+
left: 2,
|
|
771
|
+
right: 2,
|
|
772
|
+
tags: true,
|
|
773
|
+
content: '{white-fg}[Up/Down] Navigate [Enter] Edit [Tab] Save/Cancel [Esc] Cancel{/white-fg}',
|
|
774
|
+
style: { bg: COLORS.contentBg },
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
// Buttons
|
|
778
|
+
function _modalBtn(label, leftPos, onClick) {
|
|
779
|
+
const btn = blessed.button({
|
|
780
|
+
parent: modal,
|
|
781
|
+
content: label,
|
|
782
|
+
bottom: 2,
|
|
783
|
+
left: leftPos,
|
|
784
|
+
mouse: true,
|
|
785
|
+
keys: true,
|
|
786
|
+
shrink: true,
|
|
787
|
+
padding: { left: 1, right: 1 },
|
|
788
|
+
style: {
|
|
789
|
+
bg: 'blue',
|
|
790
|
+
fg: 'white',
|
|
791
|
+
focus: { bg: 'cyan', fg: 'black', bold: true },
|
|
792
|
+
hover: { bg: 'cyan', fg: 'black', bold: true },
|
|
793
|
+
},
|
|
794
|
+
});
|
|
795
|
+
btn.key(['enter', 'space'], () => onClick());
|
|
796
|
+
btn.on('click', () => onClick());
|
|
797
|
+
return btn;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
const saveBtn = _modalBtn('Save', 4, () => {
|
|
801
|
+
saveLlmConfigSync(llmKey, {
|
|
802
|
+
voice: draft.voice,
|
|
803
|
+
pretext: draft.pretext,
|
|
804
|
+
effects: draft.reverbPreset === 'off' ? '' : draft.reverbPreset,
|
|
805
|
+
bgTrack: draft.bgTrack,
|
|
806
|
+
bgVolume: draft.bgVolume,
|
|
807
|
+
ttsEngine: draft.ttsEngine,
|
|
808
|
+
sourcePath: config.sourcePath,
|
|
809
|
+
}, targetDir);
|
|
810
|
+
_closeModal();
|
|
811
|
+
_showSavedToast(provider.name);
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
const resetBtn = _modalBtn('Reset', 16, () => {
|
|
815
|
+
draft.ttsEngine = '';
|
|
816
|
+
draft.voice = '';
|
|
817
|
+
draft.pretext = '';
|
|
818
|
+
draft.reverbPreset = 'off';
|
|
819
|
+
draft.bgTrack = '';
|
|
820
|
+
draft.bgVolume = '0.15';
|
|
821
|
+
fieldList.setItems(_fieldItems());
|
|
822
|
+
fieldList.focus();
|
|
823
|
+
screen.render();
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
const cancelBtn = _modalBtn('Cancel', 30, _closeModal);
|
|
827
|
+
|
|
828
|
+
const allBtns = [saveBtn, resetBtn, cancelBtn];
|
|
829
|
+
const btnBlink = attachBtnBlink(allBtns, screen);
|
|
830
|
+
|
|
831
|
+
function _closeModal() {
|
|
832
|
+
if (_closed) return;
|
|
833
|
+
_closed = true;
|
|
834
|
+
btnBlink.cleanup();
|
|
835
|
+
navigationService?.closeModal();
|
|
836
|
+
destroyList(modal, screen);
|
|
837
|
+
if (providerFocusableItems.length) providerFocusableItems[providerFocusIndex]?.focus();
|
|
838
|
+
screen.render();
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// Field editing via Enter
|
|
842
|
+
fieldList.key(['enter'], () => {
|
|
843
|
+
const idx = fieldList.selected;
|
|
844
|
+
const field = FIELDS[idx];
|
|
845
|
+
if (!field) return;
|
|
846
|
+
|
|
847
|
+
const _refreshField = () => {
|
|
848
|
+
fieldList.setItems(_fieldItems());
|
|
849
|
+
fieldList.select(idx);
|
|
850
|
+
fieldList.focus();
|
|
851
|
+
screen.render();
|
|
852
|
+
};
|
|
853
|
+
const _cancelField = () => {
|
|
854
|
+
fieldList.focus();
|
|
855
|
+
screen.render();
|
|
856
|
+
};
|
|
857
|
+
|
|
858
|
+
switch (field.key) {
|
|
859
|
+
case 'ttsEngine':
|
|
860
|
+
_openTtsEnginePicker(draft, _refreshField);
|
|
861
|
+
break;
|
|
862
|
+
|
|
863
|
+
case 'voice':
|
|
864
|
+
_openVoicePickerForLlm(draft, _refreshField);
|
|
865
|
+
break;
|
|
866
|
+
|
|
867
|
+
case 'pretext':
|
|
868
|
+
_openPretextEditor(modal, draft, _refreshField);
|
|
869
|
+
break;
|
|
870
|
+
|
|
871
|
+
case 'reverb':
|
|
872
|
+
openReverbPicker(screen, draft.reverbPreset, (val) => {
|
|
873
|
+
draft.reverbPreset = val;
|
|
874
|
+
_refreshField();
|
|
875
|
+
}, _cancelField, { applyToEffectsManager: false });
|
|
876
|
+
break;
|
|
877
|
+
|
|
878
|
+
case 'bgTrack':
|
|
879
|
+
openTrackPicker(screen, draft.bgTrack, Math.round(parseFloat(draft.bgVolume) * 100), (track) => {
|
|
880
|
+
draft.bgTrack = track;
|
|
881
|
+
_refreshField();
|
|
882
|
+
}, _cancelField, { skipVolume: true });
|
|
883
|
+
break;
|
|
884
|
+
|
|
885
|
+
case 'bgVolume':
|
|
886
|
+
openVolumeInput(screen, Math.round(parseFloat(draft.bgVolume) * 100), (volume) => {
|
|
887
|
+
draft.bgVolume = (volume / 100).toFixed(2);
|
|
888
|
+
_refreshField();
|
|
889
|
+
}, _cancelField);
|
|
890
|
+
break;
|
|
891
|
+
}
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
fieldList.key(['escape'], _closeModal);
|
|
895
|
+
|
|
896
|
+
// Remove selection highlight when field list loses focus
|
|
897
|
+
fieldList.on('blur', () => {
|
|
898
|
+
fieldList.style.selected = { bg: COLORS.contentBg, fg: COLORS.labelFg };
|
|
899
|
+
fieldList.setItems(_fieldItems());
|
|
900
|
+
screen.render();
|
|
901
|
+
});
|
|
902
|
+
fieldList.on('focus', () => {
|
|
903
|
+
fieldList.style.selected = { bg: 'blue', fg: 'yellow' };
|
|
904
|
+
fieldList.setItems(_fieldItems());
|
|
905
|
+
screen.render();
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
// Wrap: down on last field → focus Save; up on first field → focus Save
|
|
909
|
+
// One extra arrow press at boundary moves to button row.
|
|
910
|
+
// Track previous selection so arriving at boundary doesn't immediately jump.
|
|
911
|
+
let _prevFieldSel = 0;
|
|
912
|
+
fieldList.key(['down'], () => {
|
|
913
|
+
const cur = fieldList.selected ?? 0;
|
|
914
|
+
if (cur === FIELDS.length - 1 && _prevFieldSel === FIELDS.length - 1) {
|
|
915
|
+
allBtns[0].focus(); screen.render();
|
|
916
|
+
}
|
|
917
|
+
_prevFieldSel = cur;
|
|
918
|
+
});
|
|
919
|
+
fieldList.key(['up'], () => {
|
|
920
|
+
const cur = fieldList.selected ?? 0;
|
|
921
|
+
if (cur === 0 && _prevFieldSel === 0) {
|
|
922
|
+
allBtns[0].focus(); screen.render();
|
|
923
|
+
}
|
|
924
|
+
_prevFieldSel = cur;
|
|
925
|
+
});
|
|
926
|
+
fieldList.key(['tab'], () => {
|
|
927
|
+
allBtns[0].focus();
|
|
928
|
+
screen.render();
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
for (let i = 0; i < allBtns.length; i++) {
|
|
932
|
+
allBtns[i].key(['tab', 'right'], () => {
|
|
933
|
+
allBtns[(i + 1) % allBtns.length].focus();
|
|
934
|
+
screen.render();
|
|
935
|
+
});
|
|
936
|
+
allBtns[i].key(['S-tab', 'left'], () => {
|
|
937
|
+
allBtns[(i - 1 + allBtns.length) % allBtns.length].focus();
|
|
938
|
+
screen.render();
|
|
939
|
+
});
|
|
940
|
+
allBtns[i].key(['escape'], _closeModal);
|
|
941
|
+
allBtns[i].key(['up'], () => {
|
|
942
|
+
fieldList.focus();
|
|
943
|
+
screen.render();
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
modal.key(['escape'], _closeModal);
|
|
948
|
+
fieldList.focus();
|
|
949
|
+
screen.render();
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// ── TTS Engine picker (for config modal) ──────────────────────────────────
|
|
953
|
+
|
|
954
|
+
function _openTtsEnginePicker(draft, onDone) {
|
|
955
|
+
navigationService?.openModal();
|
|
956
|
+
|
|
957
|
+
const engines = getEngineStatuses();
|
|
958
|
+
const items = engines.map(e => {
|
|
959
|
+
const status = e.installed ? '{green-fg}[OK]{/green-fg}' : '{yellow-fg}[Not Found]{/yellow-fg}';
|
|
960
|
+
return ` ${e.name.padEnd(20)} ${status} ${e.desc}`;
|
|
961
|
+
});
|
|
962
|
+
// Add "(global default)" option at top
|
|
963
|
+
items.unshift(' (global default)');
|
|
964
|
+
|
|
965
|
+
const picker = blessed.list({
|
|
966
|
+
parent: screen,
|
|
967
|
+
top: 'center',
|
|
968
|
+
left: 'center',
|
|
969
|
+
width: 70,
|
|
970
|
+
height: Math.min(items.length + 4, 16),
|
|
971
|
+
border: { type: 'line' },
|
|
972
|
+
tags: true,
|
|
973
|
+
label: ' {bold}{cyan-fg} Select TTS Engine {/cyan-fg}{/bold} ',
|
|
974
|
+
keys: true,
|
|
975
|
+
vi: false,
|
|
976
|
+
mouse: true,
|
|
977
|
+
style: {
|
|
978
|
+
fg: COLORS.labelFg,
|
|
979
|
+
bg: COLORS.contentBg,
|
|
980
|
+
border: { fg: 'cyan' },
|
|
981
|
+
selected: { bg: 'blue', fg: 'yellow' },
|
|
982
|
+
item: { fg: COLORS.labelFg },
|
|
983
|
+
},
|
|
984
|
+
});
|
|
985
|
+
picker.setFront();
|
|
986
|
+
picker.setItems(items);
|
|
987
|
+
|
|
988
|
+
picker.key(['enter'], () => {
|
|
989
|
+
const idx = picker.selected;
|
|
990
|
+
if (idx === 0) {
|
|
991
|
+
draft.ttsEngine = '';
|
|
992
|
+
} else {
|
|
993
|
+
draft.ttsEngine = engines[idx - 1].id;
|
|
994
|
+
}
|
|
995
|
+
navigationService?.closeModal();
|
|
996
|
+
destroyList(picker, screen);
|
|
997
|
+
onDone();
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
picker.key(['escape'], () => {
|
|
1001
|
+
navigationService?.closeModal();
|
|
1002
|
+
destroyList(picker, screen);
|
|
1003
|
+
onDone();
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
picker.focus();
|
|
1007
|
+
screen.render();
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// ── Voice picker for LLM config (matches agents-tab pattern) ──────────────
|
|
1011
|
+
|
|
1012
|
+
function _secureTempWav(prefix) {
|
|
1013
|
+
const baseDir = process.env.XDG_RUNTIME_DIR || os.tmpdir();
|
|
1014
|
+
const dir = path.join(baseDir, `agentvibes-${process.getuid?.() ?? 'u'}`);
|
|
1015
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
1016
|
+
try { fs.chmodSync(dir, 0o700); } catch {}
|
|
1017
|
+
return path.join(dir, `${prefix}-${crypto.randomUUID()}.wav`);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
function _openVoicePickerForLlm(draft, onDone) {
|
|
1021
|
+
navigationService?.openModal();
|
|
1022
|
+
|
|
1023
|
+
let _allVoices = [];
|
|
1024
|
+
let _filterText = '';
|
|
1025
|
+
let _previewProc = null;
|
|
1026
|
+
let _previewVoiceId = null;
|
|
1027
|
+
let _vpClosed = false;
|
|
1028
|
+
|
|
1029
|
+
const _spawnEnv = buildAudioEnv();
|
|
1030
|
+
const _isWin = process.platform === 'win32' && !process.env.WSL_DISTRO_NAME;
|
|
1031
|
+
|
|
1032
|
+
function _killVP() {
|
|
1033
|
+
if (_previewProc) {
|
|
1034
|
+
try {
|
|
1035
|
+
if (_isWin) { _previewProc.kill(); } else { process.kill(-_previewProc.pid, 'SIGTERM'); }
|
|
1036
|
+
} catch {}
|
|
1037
|
+
_previewProc = null;
|
|
1038
|
+
}
|
|
1039
|
+
_previewVoiceId = null;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
function _closeVP() {
|
|
1043
|
+
if (_vpClosed) return;
|
|
1044
|
+
_vpClosed = true;
|
|
1045
|
+
_killVP();
|
|
1046
|
+
navigationService?.closeModal();
|
|
1047
|
+
destroyList(vpModal, screen, onDone);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
const vpModal = blessed.box({
|
|
1051
|
+
parent: screen,
|
|
1052
|
+
top: '6%',
|
|
1053
|
+
left: '3%',
|
|
1054
|
+
width: '94%',
|
|
1055
|
+
height: '88%',
|
|
1056
|
+
border: { type: 'line' },
|
|
1057
|
+
tags: true,
|
|
1058
|
+
label: ' {bold}{cyan-fg} Select Voice {/cyan-fg}{/bold} ',
|
|
1059
|
+
style: { fg: COLORS.labelFg, bg: COLORS.contentBg, border: { fg: 'cyan' } },
|
|
1060
|
+
});
|
|
1061
|
+
vpModal.setFront();
|
|
1062
|
+
|
|
1063
|
+
// Search
|
|
1064
|
+
blessed.text({
|
|
1065
|
+
parent: vpModal, top: 1, left: 2,
|
|
1066
|
+
content: 'Search:', style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
|
|
1067
|
+
});
|
|
1068
|
+
const vpSearch = blessed.textbox({
|
|
1069
|
+
parent: vpModal, top: 1, left: 11, width: 40, height: 1,
|
|
1070
|
+
inputOnFocus: true, keys: true,
|
|
1071
|
+
style: { fg: COLORS.valueFg, bg: 'blue', focus: { bg: 'cyan' } },
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
// Column header
|
|
1075
|
+
const COL_N = 28;
|
|
1076
|
+
const COL_G = 10;
|
|
1077
|
+
blessed.text({
|
|
1078
|
+
parent: vpModal, top: 2, left: 6, tags: true,
|
|
1079
|
+
content: `{cyan-fg}${'Name'.padEnd(COL_N)}${'Gender'.padEnd(COL_G)}Provider{/cyan-fg}`,
|
|
1080
|
+
style: { bg: COLORS.contentBg },
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
const vpList = blessed.list({
|
|
1084
|
+
parent: vpModal, top: 3, left: 2, right: 2, bottom: 5,
|
|
1085
|
+
keys: true, vi: true, mouse: true,
|
|
1086
|
+
border: { type: 'line' },
|
|
1087
|
+
scrollbar: { ch: '|', style: { fg: 'cyan' } },
|
|
1088
|
+
tags: true,
|
|
1089
|
+
style: {
|
|
1090
|
+
fg: COLORS.labelFg, bg: COLORS.contentBg,
|
|
1091
|
+
border: { fg: 'blue' },
|
|
1092
|
+
selected: { bg: 'green', fg: 'white', bold: true },
|
|
1093
|
+
item: { fg: COLORS.labelFg },
|
|
1094
|
+
},
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
const vpPreviewLine = blessed.text({
|
|
1098
|
+
parent: vpModal, bottom: 3, left: 2, right: 2, tags: true,
|
|
1099
|
+
content: '', style: { fg: 'cyan', bg: COLORS.contentBg },
|
|
1100
|
+
});
|
|
1101
|
+
|
|
1102
|
+
blessed.text({
|
|
1103
|
+
parent: vpModal, bottom: 2, left: 2, right: 2, tags: true,
|
|
1104
|
+
content: '{white-fg}[↑↓/jk] Navigate [Enter] Select [Space] Preview [/] Search [Esc] Cancel{/white-fg}',
|
|
1105
|
+
style: { bg: COLORS.contentBg },
|
|
1106
|
+
});
|
|
1107
|
+
|
|
1108
|
+
function _getFiltered() {
|
|
1109
|
+
if (!_filterText) return _allVoices;
|
|
1110
|
+
const f = _filterText.toLowerCase();
|
|
1111
|
+
return _allVoices.filter(v => v.toLowerCase().includes(f));
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function _buildVoiceItems(voices) {
|
|
1115
|
+
return voices.map(v => {
|
|
1116
|
+
const isActive = v === draft.voice;
|
|
1117
|
+
const isPrev = v === _previewVoiceId;
|
|
1118
|
+
const dot = isPrev ? '♪' : (isActive ? '●' : ' ');
|
|
1119
|
+
const meta = getVoiceMeta(v);
|
|
1120
|
+
const name = meta.displayName.length > COL_N
|
|
1121
|
+
? meta.displayName.slice(0, COL_N - 1) + '…'
|
|
1122
|
+
: meta.displayName.padEnd(COL_N);
|
|
1123
|
+
return ` ${dot} ${name}${meta.gender.padEnd(COL_G)}${meta.provider}`;
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
function _refreshVP() {
|
|
1128
|
+
if (_vpClosed) return;
|
|
1129
|
+
const savedIdx = vpList.selected ?? 0;
|
|
1130
|
+
const savedScroll = vpList.childBase ?? 0;
|
|
1131
|
+
_allVoices = scanInstalledVoices();
|
|
1132
|
+
const filtered = _getFiltered();
|
|
1133
|
+
const items = _buildVoiceItems(filtered);
|
|
1134
|
+
vpList.setItems(items.length > 0 ? items : [' (no voices found)']);
|
|
1135
|
+
vpList.select(Math.min(savedIdx, items.length - 1));
|
|
1136
|
+
vpList.childBase = Math.min(savedScroll, Math.max(0, items.length - (vpList.height - 2)));
|
|
1137
|
+
screen.render();
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
function _previewVoice(voiceId) {
|
|
1141
|
+
if (_previewVoiceId === voiceId) { _killVP(); vpPreviewLine.setContent(''); _refreshVP(); return; }
|
|
1142
|
+
_killVP();
|
|
1143
|
+
|
|
1144
|
+
const _ms = parseMultiSpeaker(voiceId);
|
|
1145
|
+
const voicePath = path.resolve(PIPER_VOICES_DIR, _ms.model + '.onnx');
|
|
1146
|
+
const safeBase = path.resolve(PIPER_VOICES_DIR);
|
|
1147
|
+
if (!voicePath.startsWith(safeBase + path.sep) && voicePath !== safeBase) return;
|
|
1148
|
+
|
|
1149
|
+
const tempWav = _secureTempWav('vp');
|
|
1150
|
+
const phrase = SAMPLE_PHRASES[Math.floor(Math.random() * SAMPLE_PHRASES.length)];
|
|
1151
|
+
|
|
1152
|
+
let _piperBin = 'piper';
|
|
1153
|
+
if (_isWin) {
|
|
1154
|
+
const _lad = process.env.LOCALAPPDATA ||
|
|
1155
|
+
(process.env.USERPROFILE ? path.join(process.env.USERPROFILE, 'AppData', 'Local') : null);
|
|
1156
|
+
if (_lad) {
|
|
1157
|
+
const _ep = path.join(_lad, 'Programs', 'Piper', 'piper.exe');
|
|
1158
|
+
if (fs.existsSync(_ep)) _piperBin = _ep;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
const args = ['--model', voicePath, '--output_file', tempWav];
|
|
1163
|
+
if (_ms.speakerId != null) args.push('--speaker', String(_ms.speakerId));
|
|
1164
|
+
const piper = spawn(_piperBin, args, {
|
|
1165
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
1166
|
+
detached: !_isWin,
|
|
1167
|
+
windowsHide: true,
|
|
1168
|
+
env: _spawnEnv,
|
|
1169
|
+
});
|
|
1170
|
+
piper.stdin.write(phrase + '\n');
|
|
1171
|
+
piper.stdin.end();
|
|
1172
|
+
_previewProc = piper;
|
|
1173
|
+
_previewVoiceId = voiceId;
|
|
1174
|
+
|
|
1175
|
+
if (!_vpClosed) {
|
|
1176
|
+
vpPreviewLine.setContent(`{cyan-fg}♪ Synthesizing: ${voiceId}...{/cyan-fg}`);
|
|
1177
|
+
_refreshVP();
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
piper.on('exit', (code) => {
|
|
1181
|
+
if (_previewVoiceId !== voiceId) { try { fs.unlinkSync(tempWav); } catch {} return; }
|
|
1182
|
+
if (code !== 0) { _previewProc = null; _previewVoiceId = null; try { fs.unlinkSync(tempWav); } catch {} return; }
|
|
1183
|
+
const wp = detectWavPlayer(_spawnEnv);
|
|
1184
|
+
if (!wp) return;
|
|
1185
|
+
const pp = spawn(wp.bin, wp.args(tempWav), {
|
|
1186
|
+
stdio: 'ignore',
|
|
1187
|
+
detached: !_isWin,
|
|
1188
|
+
windowsHide: true,
|
|
1189
|
+
env: _spawnEnv,
|
|
1190
|
+
});
|
|
1191
|
+
_previewProc = pp;
|
|
1192
|
+
if (!_vpClosed) { vpPreviewLine.setContent(`{cyan-fg}♪ Playing: ${voiceId}{/cyan-fg}`); screen.render(); }
|
|
1193
|
+
pp.on('exit', () => {
|
|
1194
|
+
if (_previewVoiceId === voiceId) { _previewVoiceId = null; _previewProc = null; if (!_vpClosed) { vpPreviewLine.setContent(''); _refreshVP(); } }
|
|
1195
|
+
try { fs.unlinkSync(tempWav); } catch {}
|
|
1196
|
+
});
|
|
1197
|
+
});
|
|
1198
|
+
piper.on('error', () => { _previewProc = null; _previewVoiceId = null; });
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
vpSearch.on('keypress', () => {
|
|
1202
|
+
setTimeout(() => { _filterText = vpSearch.getValue().trim(); _refreshVP(); }, 0);
|
|
1203
|
+
});
|
|
1204
|
+
vpSearch.key(['escape'], () => { vpList.focus(); screen.render(); });
|
|
1205
|
+
vpList.key(['/'], () => { vpSearch.clearValue(); vpSearch.focus(); screen.render(); });
|
|
1206
|
+
vpList.key(['enter'], () => {
|
|
1207
|
+
const filtered = _getFiltered();
|
|
1208
|
+
const sel = filtered[vpList.selected];
|
|
1209
|
+
if (sel) { draft.voice = sel; _closeVP(); }
|
|
1210
|
+
});
|
|
1211
|
+
vpList.key(['space'], () => {
|
|
1212
|
+
const filtered = _getFiltered();
|
|
1213
|
+
const sel = filtered[vpList.selected];
|
|
1214
|
+
if (sel) _previewVoice(sel);
|
|
1215
|
+
});
|
|
1216
|
+
vpList.key(['escape', 'q'], _closeVP);
|
|
1217
|
+
|
|
1218
|
+
_refreshVP();
|
|
1219
|
+
const activeIdx = _getFiltered().indexOf(draft.voice);
|
|
1220
|
+
if (activeIdx >= 0) vpList.select(activeIdx);
|
|
1221
|
+
vpList.focus();
|
|
1222
|
+
screen.render();
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// ── Pretext editor ────────────────────────────────────────────────────────
|
|
1226
|
+
|
|
1227
|
+
function _openPretextEditor(parentModal, draft, onDone) {
|
|
1228
|
+
const editModal = blessed.box({
|
|
1229
|
+
parent: screen, top: 'center', left: 'center',
|
|
1230
|
+
width: 60, height: 8,
|
|
1231
|
+
border: { type: 'line' },
|
|
1232
|
+
tags: true,
|
|
1233
|
+
label: ' {bold}{cyan-fg} Edit Pretext {/cyan-fg}{/bold} ',
|
|
1234
|
+
style: { fg: COLORS.labelFg, bg: COLORS.contentBg, border: { fg: 'cyan' } },
|
|
1235
|
+
});
|
|
1236
|
+
editModal.setFront();
|
|
1237
|
+
|
|
1238
|
+
blessed.text({
|
|
1239
|
+
parent: editModal, top: 1, left: 2, tags: true,
|
|
1240
|
+
content: '{white-fg}Spoken before every TTS message (max 200 chars):{/white-fg}',
|
|
1241
|
+
style: { bg: COLORS.contentBg },
|
|
1242
|
+
});
|
|
1243
|
+
|
|
1244
|
+
const inputBox = blessed.textbox({
|
|
1245
|
+
parent: editModal, top: 3, left: 2, right: 2, height: 3,
|
|
1246
|
+
border: { type: 'line' },
|
|
1247
|
+
inputOnFocus: true,
|
|
1248
|
+
value: draft.pretext,
|
|
1249
|
+
style: {
|
|
1250
|
+
fg: 'white', bg: 'black',
|
|
1251
|
+
border: { fg: 'blue' },
|
|
1252
|
+
focus: { border: { fg: 'cyan' } },
|
|
1253
|
+
},
|
|
1254
|
+
});
|
|
1255
|
+
|
|
1256
|
+
function _closeEdit(save) {
|
|
1257
|
+
if (save) {
|
|
1258
|
+
const val = (inputBox.getValue() || '').trim().slice(0, 200);
|
|
1259
|
+
draft.pretext = val;
|
|
1260
|
+
}
|
|
1261
|
+
destroyList(editModal, screen);
|
|
1262
|
+
onDone();
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
inputBox.key(['enter'], () => _closeEdit(true));
|
|
1266
|
+
inputBox.key(['escape'], () => _closeEdit(false));
|
|
1267
|
+
|
|
1268
|
+
inputBox.focus();
|
|
1269
|
+
inputBox.readInput(() => {});
|
|
1270
|
+
screen.render();
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
// ── Saved toast ───────────────────────────────────────────────────────────
|
|
1274
|
+
|
|
1275
|
+
function _showSavedToast(name) {
|
|
1276
|
+
const toast = blessed.box({
|
|
1277
|
+
parent: screen,
|
|
1278
|
+
top: 'center',
|
|
1279
|
+
left: 'center',
|
|
1280
|
+
width: 30,
|
|
1281
|
+
height: 3,
|
|
1282
|
+
border: { type: 'line' },
|
|
1283
|
+
tags: true,
|
|
1284
|
+
content: `{center}{green-fg}{bold}${name} saved!{/bold}{/green-fg}{/center}`,
|
|
1285
|
+
style: { bg: COLORS.contentBg, border: { fg: 'green' } },
|
|
1286
|
+
});
|
|
1287
|
+
toast.setFront();
|
|
1288
|
+
screen.render();
|
|
1289
|
+
setTimeout(() => {
|
|
1290
|
+
toast.destroy();
|
|
1291
|
+
screen.render();
|
|
1292
|
+
}, 1500);
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// ── Provider info panels ──────────────────────────────────────────────────
|
|
1296
|
+
|
|
1297
|
+
function hideAllProviderRows() {
|
|
1298
|
+
providerHeader.hide();
|
|
1299
|
+
for (const row of providerRows) {
|
|
1300
|
+
row.label.hide();
|
|
1301
|
+
row.statusText.hide();
|
|
1302
|
+
row.installBtn.hide();
|
|
1303
|
+
row.removeBtn.hide();
|
|
1304
|
+
row.configBtn.hide();
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
function showAllProviderRows() {
|
|
1309
|
+
providerHeader.show();
|
|
1310
|
+
for (const row of providerRows) {
|
|
1311
|
+
row.label.show();
|
|
1312
|
+
row.statusText.show();
|
|
1313
|
+
row.installBtn.show();
|
|
1314
|
+
row.removeBtn.show();
|
|
1315
|
+
row.configBtn.show();
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
function showClaudeCodeInfo(result = null, wasInstalled = false) {
|
|
1320
|
+
providerView = 'info';
|
|
1321
|
+
hideAllProviderRows();
|
|
1322
|
+
contentBox.hide();
|
|
1323
|
+
|
|
1324
|
+
const mcpPath = path.join(targetDir, '.mcp.json');
|
|
1325
|
+
const hooksDir = path.join(targetDir, '.claude', process.platform === 'win32' ? 'hooks-windows' : 'hooks');
|
|
1326
|
+
const installed = installedState['claude-code'];
|
|
1327
|
+
const verb = wasInstalled ? 'reinstalled' : 'installed';
|
|
1328
|
+
|
|
1329
|
+
const lines = [];
|
|
1330
|
+
lines.push('{bold}{cyan-fg}Claude Code -- AgentVibes Integration{/cyan-fg}{/bold}');
|
|
1331
|
+
lines.push('');
|
|
1332
|
+
|
|
1333
|
+
if (result) {
|
|
1334
|
+
lines.push(result.success
|
|
1335
|
+
? `{green-fg}AgentVibes for Claude Code ${verb}!{/green-fg}`
|
|
1336
|
+
: `{red-fg}Installation failed{/red-fg}`);
|
|
1337
|
+
} else {
|
|
1338
|
+
lines.push(installed
|
|
1339
|
+
? '{green-fg}Installed{/green-fg}'
|
|
1340
|
+
: '{yellow-fg}Not installed{/yellow-fg}');
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
lines.push('');
|
|
1344
|
+
lines.push(`{bold}{cyan-fg}What ${result ? `got ${verb}` : 'gets installed'}:{/cyan-fg}{/bold}`);
|
|
1345
|
+
lines.push('');
|
|
1346
|
+
lines.push(' {yellow-fg}1.{/yellow-fg} {bold}.mcp.json{/bold} (project root)');
|
|
1347
|
+
lines.push(` Location: ${mcpPath}`);
|
|
1348
|
+
lines.push(' Registers the AgentVibes MCP server for Claude Code.');
|
|
1349
|
+
lines.push('');
|
|
1350
|
+
lines.push(' {yellow-fg}2.{/yellow-fg} {bold}.claude/hooks/{/bold} (session-start + pre-tool hooks)');
|
|
1351
|
+
lines.push(` Location: ${hooksDir}`);
|
|
1352
|
+
lines.push('');
|
|
1353
|
+
lines.push(' {yellow-fg}3.{/yellow-fg} {bold}.claude/commands/{/bold} (slash commands)');
|
|
1354
|
+
lines.push('');
|
|
1355
|
+
lines.push(' {yellow-fg}4.{/yellow-fg} {bold}.claude/config/{/bold} (personality, verbosity, voice settings)');
|
|
1356
|
+
lines.push('');
|
|
1357
|
+
lines.push('{white-fg}Press {bold}Escape{/bold} to return to the provider list.{/white-fg}');
|
|
1358
|
+
|
|
1359
|
+
infoBox.setContent(lines.join('\n'));
|
|
1360
|
+
infoBox.show();
|
|
1361
|
+
infoBox.focus();
|
|
1362
|
+
infoBox.scrollTo(0);
|
|
1363
|
+
screen.render();
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
function showCopilotInfo(result, wasInstalled = false) {
|
|
1367
|
+
providerView = 'info';
|
|
1368
|
+
hideAllProviderRows();
|
|
1369
|
+
contentBox.hide();
|
|
1370
|
+
|
|
1371
|
+
const verb = wasInstalled ? 'reinstalled' : 'installed';
|
|
1372
|
+
|
|
1373
|
+
const lines = [];
|
|
1374
|
+
lines.push('{bold}{cyan-fg}GitHub Copilot -- AgentVibes Integration{/cyan-fg}{/bold}');
|
|
1375
|
+
lines.push('');
|
|
1376
|
+
lines.push(result.success
|
|
1377
|
+
? `{green-fg}AgentVibes for Copilot ${verb}!{/green-fg}`
|
|
1378
|
+
: `{red-fg}Installation failed:{/red-fg} ${result.error || 'Unknown error'}`);
|
|
1379
|
+
lines.push('');
|
|
1380
|
+
lines.push(`{bold}{cyan-fg}What got ${verb}:{/cyan-fg}{/bold}`);
|
|
1381
|
+
lines.push('');
|
|
1382
|
+
lines.push(' {yellow-fg}1.{/yellow-fg} {bold}.vscode/mcp.json{/bold}');
|
|
1383
|
+
lines.push(' {yellow-fg}2.{/yellow-fg} {bold}.github/copilot-instructions.md{/bold}');
|
|
1384
|
+
lines.push('');
|
|
1385
|
+
lines.push('{white-fg}Press {bold}Escape{/bold} to return to the provider list.{/white-fg}');
|
|
1386
|
+
|
|
1387
|
+
infoBox.setContent(lines.join('\n'));
|
|
1388
|
+
infoBox.show();
|
|
1389
|
+
infoBox.focus();
|
|
1390
|
+
infoBox.scrollTo(0);
|
|
1391
|
+
screen.render();
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
function showCodexInfo(result, wasInstalled = false) {
|
|
1395
|
+
providerView = 'info';
|
|
1396
|
+
hideAllProviderRows();
|
|
1397
|
+
contentBox.hide();
|
|
1398
|
+
|
|
1399
|
+
const verb = wasInstalled ? 'reinstalled' : 'installed';
|
|
1400
|
+
|
|
1401
|
+
const lines = [];
|
|
1402
|
+
lines.push('{bold}{cyan-fg}OpenAI Codex -- AgentVibes Integration{/cyan-fg}{/bold}');
|
|
1403
|
+
lines.push('');
|
|
1404
|
+
lines.push(result.success
|
|
1405
|
+
? `{green-fg}AgentVibes for Codex ${verb}!{/green-fg}`
|
|
1406
|
+
: `{red-fg}Installation failed:{/red-fg} ${result.error || 'Unknown error'}`);
|
|
1407
|
+
lines.push('');
|
|
1408
|
+
lines.push(`{bold}{cyan-fg}What got ${verb}:{/cyan-fg}{/bold}`);
|
|
1409
|
+
lines.push('');
|
|
1410
|
+
lines.push(' {yellow-fg}1.{/yellow-fg} {bold}.codex/config.toml{/bold}');
|
|
1411
|
+
lines.push(' {yellow-fg}2.{/yellow-fg} {bold}.vscode/mcp.json{/bold}');
|
|
1412
|
+
lines.push(' {yellow-fg}3.{/yellow-fg} {bold}AGENTS.md{/bold}');
|
|
1413
|
+
lines.push(' {yellow-fg}4.{/yellow-fg} {bold}.codex/hooks/{/bold}');
|
|
1414
|
+
lines.push('');
|
|
1415
|
+
lines.push('{white-fg}Press {bold}Escape{/bold} to return to the provider list.{/white-fg}');
|
|
1416
|
+
|
|
1417
|
+
infoBox.setContent(lines.join('\n'));
|
|
1418
|
+
infoBox.show();
|
|
1419
|
+
infoBox.focus();
|
|
1420
|
+
infoBox.scrollTo(0);
|
|
1421
|
+
screen.render();
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
function showRemoveInfo(providerId) {
|
|
1425
|
+
providerView = 'info';
|
|
1426
|
+
hideAllProviderRows();
|
|
1427
|
+
contentBox.hide();
|
|
1428
|
+
|
|
1429
|
+
const lines = [];
|
|
1430
|
+
if (providerId === 'claude-code') {
|
|
1431
|
+
lines.push('{bold}{cyan-fg}Remove Claude Code Integration{/cyan-fg}{/bold}');
|
|
1432
|
+
lines.push('');
|
|
1433
|
+
lines.push('To remove, run: {yellow-fg}npx agentvibes uninstall{/yellow-fg}');
|
|
1434
|
+
} else if (providerId === 'github-copilot') {
|
|
1435
|
+
lines.push('{bold}{cyan-fg}GitHub Copilot -- Removed{/cyan-fg}{/bold}');
|
|
1436
|
+
lines.push('');
|
|
1437
|
+
lines.push('{green-fg}Successfully removed!{/green-fg}');
|
|
1438
|
+
} else if (providerId === 'openai-codex') {
|
|
1439
|
+
lines.push('{bold}{cyan-fg}OpenAI Codex -- Removed{/cyan-fg}{/bold}');
|
|
1440
|
+
lines.push('');
|
|
1441
|
+
lines.push('{green-fg}Successfully removed!{/green-fg}');
|
|
1442
|
+
}
|
|
1443
|
+
lines.push('');
|
|
1444
|
+
lines.push('{white-fg}Press {bold}Escape{/bold} to return to the provider list.{/white-fg}');
|
|
1445
|
+
|
|
1446
|
+
infoBox.setContent(lines.join('\n'));
|
|
1447
|
+
infoBox.show();
|
|
1448
|
+
infoBox.focus();
|
|
1449
|
+
infoBox.scrollTo(0);
|
|
1450
|
+
screen.render();
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
function showProviderListView() {
|
|
1454
|
+
providerView = 'list';
|
|
1455
|
+
infoBox.hide();
|
|
1456
|
+
contentBox.hide();
|
|
1457
|
+
showAllProviderRows();
|
|
1458
|
+
providerFocusIndex = 0;
|
|
1459
|
+
if (providerFocusableItems.length) providerFocusableItems[0].focus();
|
|
1460
|
+
screen.render();
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
infoBox.key(['escape'], () => {
|
|
1464
|
+
showProviderListView();
|
|
1465
|
+
});
|
|
1466
|
+
|
|
1467
|
+
async function refreshInstalledState() {
|
|
1468
|
+
for (const p of PROVIDERS) {
|
|
1469
|
+
const checkFn = p.id === 'claude-code' ? checkClaudeInstalled
|
|
1470
|
+
: p.id === 'github-copilot' ? checkCopilotInstalled
|
|
1471
|
+
: checkCodexInstalled;
|
|
1472
|
+
installedState[p.id] = await checkFn(targetDir);
|
|
1473
|
+
}
|
|
1474
|
+
for (const row of providerRows) {
|
|
1475
|
+
const installed = installedState[row.id];
|
|
1476
|
+
row.statusText.setContent(
|
|
1477
|
+
installed
|
|
1478
|
+
? '{green-fg}[Installed]{/green-fg}'
|
|
1479
|
+
: '{yellow-fg}[Not Installed]{/yellow-fg}'
|
|
1480
|
+
);
|
|
1481
|
+
row.installBtn.setContent(installed ? ' Re-install ' : ' Install ');
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
// =========================================================================
|
|
1486
|
+
// Screen renderers
|
|
1487
|
+
// =========================================================================
|
|
1488
|
+
|
|
1489
|
+
const _HDR = (emoji, label) =>
|
|
1490
|
+
`{${COLORS.sectionHdr}-fg}${emoji} ${label} ${'--'.repeat(50)}{/${COLORS.sectionHdr}-fg}`;
|
|
1491
|
+
|
|
1492
|
+
function _renderScreen0() {
|
|
1493
|
+
const lines = [
|
|
1494
|
+
_HDR('', 'Language / Idioma / Langue / Sprache'),
|
|
1495
|
+
'',
|
|
1496
|
+
' Select your language:',
|
|
1497
|
+
'',
|
|
1498
|
+
...SUPPORTED_LANGUAGES.map((l, i) =>
|
|
1499
|
+
i === _langIdx
|
|
1500
|
+
? ` {green-fg}> ${l.name}{/green-fg}`
|
|
1501
|
+
: ` ${l.name}`
|
|
1502
|
+
),
|
|
1503
|
+
];
|
|
1504
|
+
contentBox.setContent(_c(lines));
|
|
1505
|
+
hintLine.setContent(' Screen 0: Language | [Up/Down] Select | [Enter] Apply & Continue | [->] Skip (English)');
|
|
1506
|
+
screen.render();
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
async function _renderScreen1() {
|
|
1510
|
+
const frames = ['|','/','-','\\'];
|
|
1511
|
+
let frameIdx = 0;
|
|
1512
|
+
_checking = true;
|
|
1513
|
+
_s1ContinueBtn.hide();
|
|
1514
|
+
|
|
1515
|
+
contentBox.setContent(_c([
|
|
1516
|
+
_HDR('', t(_getLang(), 'dependencyCheck')),
|
|
1517
|
+
'',
|
|
1518
|
+
` {white-fg}${frames[0]} ${t(_getLang(), 'checkingDependencies')}{/white-fg}`,
|
|
1519
|
+
]));
|
|
1520
|
+
hintLine.setContent(` ${t(_getLang(), 'screen2Hint')}`);
|
|
1521
|
+
screen.render();
|
|
1522
|
+
|
|
1523
|
+
const spinInterval = setInterval(() => {
|
|
1524
|
+
frameIdx = (frameIdx + 1) % frames.length;
|
|
1525
|
+
contentBox.setContent(_c([
|
|
1526
|
+
_HDR('', t(_getLang(), 'dependencyCheck')),
|
|
1527
|
+
'',
|
|
1528
|
+
` {white-fg}${frames[frameIdx]} ${t(_getLang(), 'checkingDependencies')}{/white-fg}`,
|
|
1529
|
+
]));
|
|
1530
|
+
screen.render();
|
|
1531
|
+
}, 100);
|
|
1532
|
+
|
|
1533
|
+
try {
|
|
1534
|
+
_deps = await _checkDependenciesAsync();
|
|
1535
|
+
} finally {
|
|
1536
|
+
clearInterval(spinInterval);
|
|
1537
|
+
_checking = false;
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
const ok = () => `{green-fg}OK ${t(_getLang(), 'installed')}{/green-fg}`;
|
|
1541
|
+
const bad = () => `{red-fg}X ${t(_getLang(), 'notFound')}{/red-fg}`;
|
|
1542
|
+
|
|
1543
|
+
const ttsOk = _deps.piper || _deps.soprano;
|
|
1544
|
+
contentBox.setContent(_c([
|
|
1545
|
+
_HDR('', t(_getLang(), 'dependencyCheck')),
|
|
1546
|
+
'',
|
|
1547
|
+
` {white-fg}${'Dependency'.padEnd(14)}${'Status'}{/white-fg}`,
|
|
1548
|
+
` {white-fg}${'---'.repeat(26)}{/white-fg}`,
|
|
1549
|
+
` {white-fg}${'Node.js'.padEnd(14)}{/white-fg}${_deps.node ? ok() : bad()}`,
|
|
1550
|
+
` {white-fg}${'npm'.padEnd(14)}{/white-fg}${_deps.npm ? ok() : bad()}`,
|
|
1551
|
+
` {white-fg}${'Piper TTS'.padEnd(14)}{/white-fg}${_deps.piper ? ok() : bad()}`,
|
|
1552
|
+
` {white-fg}${'Soprano TTS'.padEnd(14)}{/white-fg}${_deps.soprano ? ok() : bad()}`,
|
|
1553
|
+
` {white-fg}${'ffmpeg'.padEnd(14)}{/white-fg}${_deps.ffmpeg ? ok() : `{red-fg}! ${t(_getLang(), 'ffmpegMissing')}{/red-fg}`}`,
|
|
1554
|
+
'',
|
|
1555
|
+
ttsOk
|
|
1556
|
+
? ` {green-fg}OK ${t(_getLang(), 'ttsDetected')}{/green-fg}`
|
|
1557
|
+
: ` {red-fg}! ${t(_getLang(), 'noTtsFound')}{/red-fg}`,
|
|
1558
|
+
'',
|
|
1559
|
+
'',
|
|
1560
|
+
]));
|
|
1561
|
+
if (ttsOk) {
|
|
1562
|
+
_s1ContinueBtn.setContent(_tl('continueArrowBtn'));
|
|
1563
|
+
_s1ContinueBtn.show();
|
|
1564
|
+
_s1ContinueBtn.focus();
|
|
1565
|
+
}
|
|
1566
|
+
screen.render();
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
function _renderScreen2() {
|
|
1570
|
+
const lines = [
|
|
1571
|
+
_HDR('', 'TTS Engine Selection'),
|
|
1572
|
+
'',
|
|
1573
|
+
' {white-fg}Select which TTS engines to use with AgentVibes:{/white-fg}',
|
|
1574
|
+
];
|
|
1575
|
+
|
|
1576
|
+
contentBox.setContent(_c(lines));
|
|
1577
|
+
|
|
1578
|
+
_showTtsEngineRows();
|
|
1579
|
+
|
|
1580
|
+
// Position continue button below engine rows
|
|
1581
|
+
const btnY = 5 + (_ttsEngines.length * 3) + 1;
|
|
1582
|
+
_s2ContinueBtn.top = btnY;
|
|
1583
|
+
_s2ContinueBtn.left = 4;
|
|
1584
|
+
_s2ContinueBtn.show();
|
|
1585
|
+
|
|
1586
|
+
hintLine.setContent(' Screen 2: TTS Engines | [Tab] Install | [Enter/->] Continue | [Esc/<-] Back');
|
|
1587
|
+
|
|
1588
|
+
// Focus first visible install button or continue button
|
|
1589
|
+
const visibleBtns = _ttsFocusableItems.filter(b => !b.hidden);
|
|
1590
|
+
if (visibleBtns.length) {
|
|
1591
|
+
_ttsFocusIndex = 0;
|
|
1592
|
+
visibleBtns[0].focus();
|
|
1593
|
+
} else {
|
|
1594
|
+
_s2ContinueBtn.focus();
|
|
1595
|
+
}
|
|
1596
|
+
screen.render();
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
function _renderScreen3() {
|
|
1600
|
+
// Mark setup as completed once user reaches the providers screen
|
|
1601
|
+
try { configService.set('setupCompleted', true); } catch {}
|
|
1602
|
+
|
|
1603
|
+
// Show provider rows instead of contentBox
|
|
1604
|
+
contentBox.hide();
|
|
1605
|
+
hintLine.setContent(' Screen 3: LLM Providers | [Enter] Action | [Tab] Next button | [Esc] Tab bar');
|
|
1606
|
+
showAllProviderRows();
|
|
1607
|
+
refreshInstalledState().then(() => {
|
|
1608
|
+
if (providerFocusableItems.length) {
|
|
1609
|
+
providerFocusIndex = 0;
|
|
1610
|
+
providerFocusableItems[0].focus();
|
|
1611
|
+
}
|
|
1612
|
+
screen.render();
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
function _showCurrentScreen() {
|
|
1617
|
+
// Hide Screen 1 continue button on other screens
|
|
1618
|
+
if (_screen !== 1) _s1ContinueBtn.hide();
|
|
1619
|
+
|
|
1620
|
+
// Hide Screen 2 TTS engine rows on other screens
|
|
1621
|
+
if (_screen !== 2) {
|
|
1622
|
+
_hideTtsEngineRows();
|
|
1623
|
+
_s2ContinueBtn.hide();
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
// Hide provider rows on non-provider screens
|
|
1627
|
+
if (_screen !== 3) {
|
|
1628
|
+
hideAllProviderRows();
|
|
1629
|
+
infoBox.hide();
|
|
1630
|
+
providerView = 'list';
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
// Show contentBox on screens 0-2
|
|
1634
|
+
if (_screen <= 2) {
|
|
1635
|
+
contentBox.show();
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
if (_screen !== _lastScreen) {
|
|
1639
|
+
// Nuclear clear
|
|
1640
|
+
try {
|
|
1641
|
+
for (let r = 0; r < screen.height; r++) {
|
|
1642
|
+
const orow = screen.olines?.[r];
|
|
1643
|
+
if (!orow) continue;
|
|
1644
|
+
for (let c = 0; c < screen.width; c++) {
|
|
1645
|
+
if (orow[c]) orow[c][0] = -1;
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
if (screen.lines?.[2]) screen.lines[2].dirty = true;
|
|
1649
|
+
} catch {}
|
|
1650
|
+
|
|
1651
|
+
const _clearLine = ' '.repeat(150);
|
|
1652
|
+
const _clearPage = Array(25).fill(_clearLine).join('\n');
|
|
1653
|
+
contentBox.setContent(_clearPage);
|
|
1654
|
+
hintLine.setContent(_clearLine);
|
|
1655
|
+
screen.render();
|
|
1656
|
+
|
|
1657
|
+
const targetScreen = _screen;
|
|
1658
|
+
_lastScreen = _screen;
|
|
1659
|
+
setTimeout(() => {
|
|
1660
|
+
if (_screen !== targetScreen) return;
|
|
1661
|
+
switch (_screen) {
|
|
1662
|
+
case 0: _renderScreen0(); break;
|
|
1663
|
+
case 1: _renderScreen1(); break;
|
|
1664
|
+
case 2: _renderScreen2(); break;
|
|
1665
|
+
case 3: _renderScreen3(); break;
|
|
1666
|
+
}
|
|
1667
|
+
}, 50);
|
|
1668
|
+
return;
|
|
1669
|
+
}
|
|
1670
|
+
switch (_screen) {
|
|
1671
|
+
case 0: _renderScreen0(); break;
|
|
1672
|
+
case 1: _renderScreen1(); break;
|
|
1673
|
+
case 2: _renderScreen2(); break;
|
|
1674
|
+
case 3: _renderScreen3(); break;
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
// =========================================================================
|
|
1679
|
+
// Navigation (key handlers)
|
|
1680
|
+
// =========================================================================
|
|
1681
|
+
|
|
1682
|
+
screen.key(['enter'], () => {
|
|
1683
|
+
if (box.hidden || _checking) return;
|
|
1684
|
+
if (_screen === 0) {
|
|
1685
|
+
if (languageService) languageService.setLang(SUPPORTED_LANGUAGES[_langIdx].value);
|
|
1686
|
+
_screen = 1;
|
|
1687
|
+
_showCurrentScreen();
|
|
1688
|
+
return;
|
|
1689
|
+
}
|
|
1690
|
+
if (_screen === 1) return; // Enter handled by Continue button
|
|
1691
|
+
if (_screen === 2) return; // Enter handled by Continue button and install buttons
|
|
1692
|
+
if (_screen === 3) return; // Enter handled by provider buttons
|
|
1693
|
+
});
|
|
1694
|
+
|
|
1695
|
+
screen.key(['escape'], () => {
|
|
1696
|
+
if (box.hidden || _checking) return;
|
|
1697
|
+
if (_screen === 3 && providerView === 'info') {
|
|
1698
|
+
showProviderListView();
|
|
1699
|
+
return;
|
|
1700
|
+
}
|
|
1701
|
+
if (_screen > 0) {
|
|
1702
|
+
_screen--;
|
|
1703
|
+
_showCurrentScreen();
|
|
1704
|
+
} else {
|
|
1705
|
+
setTimeout(() => navigationService?.switchTab('settings'), 0);
|
|
1706
|
+
}
|
|
1707
|
+
});
|
|
1708
|
+
|
|
1709
|
+
screen.key(['up'], () => {
|
|
1710
|
+
if (box.hidden) return;
|
|
1711
|
+
if (_screen === 0) {
|
|
1712
|
+
_langIdx = Math.max(0, _langIdx - 1);
|
|
1713
|
+
_renderScreen0();
|
|
1714
|
+
return;
|
|
1715
|
+
}
|
|
1716
|
+
});
|
|
1717
|
+
|
|
1718
|
+
screen.key(['left'], () => {
|
|
1719
|
+
if (box.hidden || _checking) return;
|
|
1720
|
+
if (_screen === 3) return; // Left handled by button nav
|
|
1721
|
+
if (_screen > 0) {
|
|
1722
|
+
_screen--;
|
|
1723
|
+
_showCurrentScreen();
|
|
1724
|
+
}
|
|
1725
|
+
});
|
|
1726
|
+
|
|
1727
|
+
screen.key(['right'], () => {
|
|
1728
|
+
if (box.hidden || _checking) return;
|
|
1729
|
+
if (_screen === 0) {
|
|
1730
|
+
if (languageService) languageService.setLang(SUPPORTED_LANGUAGES[_langIdx].value);
|
|
1731
|
+
_screen = 1;
|
|
1732
|
+
_showCurrentScreen();
|
|
1733
|
+
return;
|
|
1734
|
+
}
|
|
1735
|
+
if (_screen === 1) return; // Right handled by Continue button
|
|
1736
|
+
if (_screen === 2) { if (_screen < 3) { _screen++; _showCurrentScreen(); } return; }
|
|
1737
|
+
if (_screen === 3) return; // Right handled by button nav
|
|
1738
|
+
});
|
|
1739
|
+
|
|
1740
|
+
screen.key(['down'], () => {
|
|
1741
|
+
if (box.hidden) return;
|
|
1742
|
+
if (_screen === 0) {
|
|
1743
|
+
_langIdx = Math.min(SUPPORTED_LANGUAGES.length - 1, _langIdx + 1);
|
|
1744
|
+
_renderScreen0();
|
|
1745
|
+
return;
|
|
1746
|
+
}
|
|
1747
|
+
});
|
|
1748
|
+
|
|
1749
|
+
// =========================================================================
|
|
1750
|
+
// Tab Component Contract
|
|
1751
|
+
// =========================================================================
|
|
1752
|
+
|
|
1753
|
+
return {
|
|
1754
|
+
box,
|
|
1755
|
+
|
|
1756
|
+
show() {
|
|
1757
|
+
// If not first run, skip directly to Screen 3 (providers)
|
|
1758
|
+
if (!_isFirstRun()) {
|
|
1759
|
+
_screen = 3;
|
|
1760
|
+
} else {
|
|
1761
|
+
_screen = 0;
|
|
1762
|
+
_langIdx = 0;
|
|
1763
|
+
}
|
|
1764
|
+
_lastScreen = -1;
|
|
1765
|
+
providerView = 'list';
|
|
1766
|
+
box.show();
|
|
1767
|
+
_showCurrentScreen();
|
|
1768
|
+
screen.render();
|
|
1769
|
+
},
|
|
1770
|
+
|
|
1771
|
+
hide() {
|
|
1772
|
+
box.hide();
|
|
1773
|
+
hideAllProviderRows();
|
|
1774
|
+
infoBox.hide();
|
|
1775
|
+
providerView = 'list';
|
|
1776
|
+
screen.render();
|
|
1777
|
+
},
|
|
1778
|
+
|
|
1779
|
+
onFocus() {
|
|
1780
|
+
if (_screen === 0) {
|
|
1781
|
+
box.focus();
|
|
1782
|
+
} else if (_screen === 3) {
|
|
1783
|
+
if (providerView === 'list') {
|
|
1784
|
+
providerFocusIndex = 0;
|
|
1785
|
+
if (providerFocusableItems.length) providerFocusableItems[0].focus();
|
|
1786
|
+
} else {
|
|
1787
|
+
infoBox.focus();
|
|
1788
|
+
}
|
|
1789
|
+
} else {
|
|
1790
|
+
box.focus();
|
|
1791
|
+
}
|
|
1792
|
+
screen.render();
|
|
1793
|
+
},
|
|
1794
|
+
|
|
1795
|
+
onBlur() {},
|
|
1796
|
+
|
|
1797
|
+
getFooterText() {
|
|
1798
|
+
if (_screen === 3) {
|
|
1799
|
+
if (providerView === 'info') {
|
|
1800
|
+
return '[Esc] Back to list [Up/Down] Scroll';
|
|
1801
|
+
}
|
|
1802
|
+
return '[Enter] Action [Tab] Next button [Esc] Tab bar';
|
|
1803
|
+
}
|
|
1804
|
+
return _tl('footerText');
|
|
1805
|
+
},
|
|
1806
|
+
|
|
1807
|
+
getFooterColor() {
|
|
1808
|
+
return COLORS.footerBg;
|
|
1809
|
+
},
|
|
1810
|
+
};
|
|
1811
|
+
}
|