agentvibes 4.6.8 → 5.1.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/Drifting Down the Hall.mp3 +0 -0
- package/.claude/audio/tracks/Late Night Hip Hop Groove.mp3 +0 -0
- package/.claude/audio/tracks/Midnight Charleston Stomp.mp3 +0 -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 +380 -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 +28 -6
- package/.claude/hooks-windows/session-start-tts.ps1 +114 -114
- package/README.md +112 -6
- package/RELEASE_NOTES.md +83 -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 +1899 -1886
- package/src/console/tabs/music-tab.js +1076 -1039
- package/src/console/tabs/placeholder-tab.js +81 -80
- package/src/console/tabs/settings-tab.js +941 -3988
- package/src/console/tabs/setup-tab.js +2071 -0
- package/src/console/tabs/voices-tab.js +1843 -1714
- package/src/console/widgets/format-utils.js +92 -89
- package/src/console/widgets/track-picker.js +325 -322
- package/src/installer.js +6147 -6092
- package/src/services/llm-provider-service.js +486 -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,2071 @@
|
|
|
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, uninstallClaude,
|
|
27
|
+
installCopilotMcp, removeCopilotMcp,
|
|
28
|
+
installCopilotInstructions, removeCopilotInstructions,
|
|
29
|
+
installCodexMcp, removeCodexMcp,
|
|
30
|
+
installCodexInstructions, installCodexHooks,
|
|
31
|
+
removeCodexInstructions, removeCodexHooks,
|
|
32
|
+
loadLlmConfigSync, saveLlmConfigSync, resolveCfgPath,
|
|
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, genderIconTag, PIPER_VOICES_DIR, SAMPLE_PHRASES, parseMultiSpeaker, getFavorites, toggleFavorite } 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 = -2;
|
|
205
|
+
let _pendingGlobalCfg = null; // Set when global config detected on first run
|
|
206
|
+
let _globalChoiceIdx = 0; // 0 = Load Global, 1 = Start Fresh
|
|
207
|
+
const _getLang = () => languageService?.getLang() ?? 'en';
|
|
208
|
+
const _tl = (key) => languageService?.t(key) ?? t('en', key);
|
|
209
|
+
let _langIdx = 0;
|
|
210
|
+
let _deps = null;
|
|
211
|
+
let _checking = false;
|
|
212
|
+
|
|
213
|
+
// First-run detection: evaluated at show() time so async config init is complete
|
|
214
|
+
function _isFirstRun() {
|
|
215
|
+
return !(configService?.getConfig?.()?.setupCompleted);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// -------------------------------------------------------------------------
|
|
219
|
+
// Content area
|
|
220
|
+
|
|
221
|
+
const contentBox = blessed.box({
|
|
222
|
+
parent: box,
|
|
223
|
+
top: 1,
|
|
224
|
+
left: 2,
|
|
225
|
+
width: '96%',
|
|
226
|
+
bottom: 5,
|
|
227
|
+
tags: true,
|
|
228
|
+
wrap: false,
|
|
229
|
+
scrollable: false,
|
|
230
|
+
style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const hintLine = blessed.text({
|
|
234
|
+
parent: box,
|
|
235
|
+
bottom: 2,
|
|
236
|
+
left: 2,
|
|
237
|
+
right: 2,
|
|
238
|
+
tags: true,
|
|
239
|
+
content: '',
|
|
240
|
+
style: { fg: COLORS.noticeFg, bg: COLORS.contentBg },
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
function _c(lines) { return lines.join('\n'); }
|
|
244
|
+
|
|
245
|
+
// -------------------------------------------------------------------------
|
|
246
|
+
// Shared button factory
|
|
247
|
+
|
|
248
|
+
function _createBtn(label, bg, onClick, textColor = 'white') {
|
|
249
|
+
const btn = blessed.button({
|
|
250
|
+
parent: box,
|
|
251
|
+
content: label,
|
|
252
|
+
mouse: true,
|
|
253
|
+
keys: true,
|
|
254
|
+
shrink: true,
|
|
255
|
+
hidden: true,
|
|
256
|
+
padding: { left: 1, right: 1 },
|
|
257
|
+
style: {
|
|
258
|
+
bg,
|
|
259
|
+
fg: textColor,
|
|
260
|
+
focus: { bg: COLORS.btnFocus, fg: COLORS.btnFocusFg, bold: true },
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
let _blinkInterval = null;
|
|
265
|
+
const _origLabel = label;
|
|
266
|
+
btn.on('focus', () => {
|
|
267
|
+
btn.style.bg = COLORS.btnFocus;
|
|
268
|
+
btn.style.fg = COLORS.btnFocusFg;
|
|
269
|
+
btn.setContent(`\u25ba ${_origLabel} \u25c4`);
|
|
270
|
+
let _on = true;
|
|
271
|
+
screen.render();
|
|
272
|
+
_blinkInterval = setInterval(() => {
|
|
273
|
+
_on = !_on;
|
|
274
|
+
btn.setContent(_on ? `\u25ba ${_origLabel} \u25c4` : ` ${_origLabel} `);
|
|
275
|
+
screen.render();
|
|
276
|
+
}, 500);
|
|
277
|
+
});
|
|
278
|
+
btn.on('blur', () => {
|
|
279
|
+
if (_blinkInterval) { clearInterval(_blinkInterval); _blinkInterval = null; }
|
|
280
|
+
btn.style.bg = bg;
|
|
281
|
+
btn.style.fg = textColor;
|
|
282
|
+
btn.setContent(_origLabel);
|
|
283
|
+
screen.render();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
btn.key(['enter', 'space'], () => {
|
|
287
|
+
btn.style.bg = COLORS.btnPress;
|
|
288
|
+
btn.style.fg = 'white';
|
|
289
|
+
screen.render();
|
|
290
|
+
setTimeout(() => {
|
|
291
|
+
btn.style.bg = bg;
|
|
292
|
+
btn.style.fg = textColor;
|
|
293
|
+
screen.render();
|
|
294
|
+
onClick();
|
|
295
|
+
}, 150);
|
|
296
|
+
});
|
|
297
|
+
btn.on('click', () => btn.press());
|
|
298
|
+
return btn;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// =========================================================================
|
|
302
|
+
// SCREEN 0: Language picker (kept as-is)
|
|
303
|
+
// =========================================================================
|
|
304
|
+
|
|
305
|
+
// =========================================================================
|
|
306
|
+
// SCREEN 1: Dependency check (was Screen 2, renumbered)
|
|
307
|
+
// =========================================================================
|
|
308
|
+
|
|
309
|
+
const _s1ContinueBtn = _createBtn('Continue ->', 'blue', () => {
|
|
310
|
+
_screen++;
|
|
311
|
+
_showCurrentScreen();
|
|
312
|
+
});
|
|
313
|
+
_s1ContinueBtn.top = 12; _s1ContinueBtn.left = 4;
|
|
314
|
+
_s1ContinueBtn.key(['right'], () => { _screen++; _showCurrentScreen(); });
|
|
315
|
+
|
|
316
|
+
// =========================================================================
|
|
317
|
+
// SCREEN 2: TTS Engine selection (new)
|
|
318
|
+
// =========================================================================
|
|
319
|
+
|
|
320
|
+
// TTS engine install buttons — created once, shown/hidden per screen
|
|
321
|
+
const _ttsEngineRows = [];
|
|
322
|
+
const _ttsFocusableItems = [];
|
|
323
|
+
let _ttsFocusIndex = 0;
|
|
324
|
+
|
|
325
|
+
const _ttsEngines = getAvailableEngines();
|
|
326
|
+
for (let i = 0; i < _ttsEngines.length; i++) {
|
|
327
|
+
const engine = _ttsEngines[i];
|
|
328
|
+
const yOff = 5 + (i * 3);
|
|
329
|
+
|
|
330
|
+
const nameLabel = blessed.text({
|
|
331
|
+
parent: box, top: yOff, left: 2, tags: true, hidden: true,
|
|
332
|
+
content: '', style: { bg: COLORS.contentBg },
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
const statusLabel = blessed.text({
|
|
336
|
+
parent: box, top: yOff, left: 22, tags: true, hidden: true,
|
|
337
|
+
content: '', style: { bg: COLORS.contentBg },
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
const descLabel = blessed.text({
|
|
341
|
+
parent: box, top: yOff + 1, left: 4, tags: true, hidden: true,
|
|
342
|
+
content: `{cyan-fg}${engine.desc}{/cyan-fg}`,
|
|
343
|
+
style: { bg: COLORS.contentBg },
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
const installBtn = blessed.button({
|
|
347
|
+
parent: box, top: yOff, left: 40, width: 14, height: 1,
|
|
348
|
+
content: ' Install ', tags: true, mouse: true, keys: true, hidden: true,
|
|
349
|
+
style: {
|
|
350
|
+
fg: COLORS.btnFg, bg: COLORS.btnBg,
|
|
351
|
+
focus: { fg: 'black', bg: COLORS.btnFocusBg },
|
|
352
|
+
},
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
installBtn.on('press', () => _handleTtsInstall(engine));
|
|
356
|
+
installBtn.key(['enter', 'space'], () => _handleTtsInstall(engine));
|
|
357
|
+
installBtn.key(['tab', 'down'], () => _cycleTtsFocus(1));
|
|
358
|
+
installBtn.key(['S-tab', 'up'], () => _cycleTtsFocus(-1));
|
|
359
|
+
installBtn.key(['escape'], () => {
|
|
360
|
+
if (typeof focusMainTabBar === 'function') { focusMainTabBar(); screen.render(); }
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
_ttsEngineRows.push({ engine, nameLabel, statusLabel, descLabel, installBtn });
|
|
364
|
+
if (!engine.native) _ttsFocusableItems.push(installBtn);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function _cycleTtsFocus(dir) {
|
|
368
|
+
const items = _ttsFocusableItems.filter(b => !b.hidden);
|
|
369
|
+
if (!items.length) return;
|
|
370
|
+
_ttsFocusIndex = (_ttsFocusIndex + dir + items.length) % items.length;
|
|
371
|
+
items[_ttsFocusIndex].focus();
|
|
372
|
+
screen.render();
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function _showTtsEngineRows() {
|
|
376
|
+
for (const row of _ttsEngineRows) {
|
|
377
|
+
const installed = checkEngineInstalled(row.engine.id);
|
|
378
|
+
row.nameLabel.setContent(`{bold}{white-fg}${row.engine.name}{/white-fg}{/bold}`);
|
|
379
|
+
row.statusLabel.setContent(installed
|
|
380
|
+
? '{green-fg}[Installed]{/green-fg}'
|
|
381
|
+
: '{yellow-fg}[Not Found]{/yellow-fg}');
|
|
382
|
+
row.nameLabel.show();
|
|
383
|
+
row.statusLabel.show();
|
|
384
|
+
row.descLabel.show();
|
|
385
|
+
if (!installed && !row.engine.native) {
|
|
386
|
+
row.installBtn.show();
|
|
387
|
+
} else {
|
|
388
|
+
row.installBtn.hide();
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function _hideTtsEngineRows() {
|
|
394
|
+
for (const row of _ttsEngineRows) {
|
|
395
|
+
row.nameLabel.hide();
|
|
396
|
+
row.statusLabel.hide();
|
|
397
|
+
row.descLabel.hide();
|
|
398
|
+
row.installBtn.hide();
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
let _ttsInstalling = false;
|
|
403
|
+
async function _handleTtsInstall(engine) {
|
|
404
|
+
if (!engine.installCmd || _ttsInstalling) return;
|
|
405
|
+
_ttsInstalling = true;
|
|
406
|
+
|
|
407
|
+
// Show installing status
|
|
408
|
+
const row = _ttsEngineRows.find(r => r.engine.id === engine.id);
|
|
409
|
+
if (row) {
|
|
410
|
+
row.statusLabel.setContent('{yellow-fg}[Installing...]{/yellow-fg}');
|
|
411
|
+
screen.render();
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
try {
|
|
415
|
+
const opts = { stdio: 'pipe', timeout: 120000 };
|
|
416
|
+
if (process.platform === 'win32') {
|
|
417
|
+
opts.shell = true;
|
|
418
|
+
await _execFileAsync(engine.installCmd, [], opts);
|
|
419
|
+
} else {
|
|
420
|
+
const parts = engine.installCmd.split(' ');
|
|
421
|
+
await _execFileAsync(parts[0], parts.slice(1), opts);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Re-check and update status
|
|
425
|
+
const installed = checkEngineInstalled(engine.id);
|
|
426
|
+
if (row) {
|
|
427
|
+
row.statusLabel.setContent(installed
|
|
428
|
+
? '{green-fg}[Installed]{/green-fg}'
|
|
429
|
+
: '{red-fg}[Install Failed]{/red-fg}');
|
|
430
|
+
if (installed) row.installBtn.hide();
|
|
431
|
+
}
|
|
432
|
+
} catch (err) {
|
|
433
|
+
if (row) {
|
|
434
|
+
row.statusLabel.setContent(`{red-fg}[Failed]{/red-fg}`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
_ttsInstalling = false;
|
|
438
|
+
screen.render();
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Continue button for Screen 2
|
|
442
|
+
const _s2ContinueBtn = _createBtn('Continue ->', 'blue', () => {
|
|
443
|
+
if (_screen < 3) { _screen++; _showCurrentScreen(); }
|
|
444
|
+
});
|
|
445
|
+
_s2ContinueBtn.hidden = true;
|
|
446
|
+
|
|
447
|
+
// =========================================================================
|
|
448
|
+
// SCREEN 3: LLM Providers (new — from llm-providers-tab)
|
|
449
|
+
// =========================================================================
|
|
450
|
+
|
|
451
|
+
let installedState = {};
|
|
452
|
+
let providerFocusableItems = [];
|
|
453
|
+
let providerFocusIndex = 0;
|
|
454
|
+
let providerView = 'list'; // 'list' or 'info'
|
|
455
|
+
|
|
456
|
+
// Provider row widgets (created once)
|
|
457
|
+
const providerRows = [];
|
|
458
|
+
const providerStatusTexts = [];
|
|
459
|
+
|
|
460
|
+
// Info box for provider details
|
|
461
|
+
const infoBox = blessed.box({
|
|
462
|
+
parent: box,
|
|
463
|
+
top: 0,
|
|
464
|
+
left: 0,
|
|
465
|
+
width: '100%',
|
|
466
|
+
bottom: 0,
|
|
467
|
+
hidden: true,
|
|
468
|
+
scrollable: true,
|
|
469
|
+
alwaysScroll: true,
|
|
470
|
+
tags: true,
|
|
471
|
+
keys: true,
|
|
472
|
+
vi: true,
|
|
473
|
+
mouse: true,
|
|
474
|
+
valign: 'top',
|
|
475
|
+
scrollbar: { ch: '|', style: { fg: 'cyan' } },
|
|
476
|
+
style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
// Provider header
|
|
480
|
+
const providerHeader = blessed.text({
|
|
481
|
+
parent: box,
|
|
482
|
+
top: 0,
|
|
483
|
+
left: 2,
|
|
484
|
+
tags: true,
|
|
485
|
+
hidden: true,
|
|
486
|
+
content: '{bold}{cyan-fg}LLM Providers{/cyan-fg}{/bold} Configure AgentVibes for your AI assistant:',
|
|
487
|
+
style: { bg: COLORS.contentBg },
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
function createProviderRow(provider, rowIndex) {
|
|
491
|
+
const yOffset = 2 + (rowIndex * 3);
|
|
492
|
+
|
|
493
|
+
const label = blessed.text({
|
|
494
|
+
parent: box,
|
|
495
|
+
top: yOffset,
|
|
496
|
+
left: 2,
|
|
497
|
+
tags: true,
|
|
498
|
+
hidden: true,
|
|
499
|
+
content: `{bold}{white-fg}${provider.name}{/white-fg}{/bold} {cyan-fg}${provider.desc}{/cyan-fg}`,
|
|
500
|
+
style: { bg: COLORS.contentBg },
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
const statusText = blessed.text({
|
|
504
|
+
parent: box,
|
|
505
|
+
top: yOffset + 1,
|
|
506
|
+
left: 4,
|
|
507
|
+
tags: true,
|
|
508
|
+
hidden: true,
|
|
509
|
+
content: '{yellow-fg}Checking...{/yellow-fg}',
|
|
510
|
+
style: { bg: COLORS.contentBg },
|
|
511
|
+
});
|
|
512
|
+
providerStatusTexts.push({ id: provider.id, widget: statusText });
|
|
513
|
+
|
|
514
|
+
const installBtn = blessed.button({
|
|
515
|
+
parent: box,
|
|
516
|
+
top: yOffset + 1,
|
|
517
|
+
left: 30,
|
|
518
|
+
width: 14,
|
|
519
|
+
height: 1,
|
|
520
|
+
content: ' Install ',
|
|
521
|
+
tags: true,
|
|
522
|
+
mouse: true,
|
|
523
|
+
keys: true,
|
|
524
|
+
hidden: true,
|
|
525
|
+
style: {
|
|
526
|
+
fg: COLORS.btnFg,
|
|
527
|
+
bg: COLORS.btnBg,
|
|
528
|
+
focus: { fg: 'black', bg: COLORS.btnFocusBg },
|
|
529
|
+
},
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
const removeBtn = blessed.button({
|
|
533
|
+
parent: box,
|
|
534
|
+
top: yOffset + 1,
|
|
535
|
+
left: 46,
|
|
536
|
+
width: 12,
|
|
537
|
+
height: 1,
|
|
538
|
+
content: ' Remove ',
|
|
539
|
+
tags: true,
|
|
540
|
+
mouse: true,
|
|
541
|
+
keys: true,
|
|
542
|
+
hidden: true,
|
|
543
|
+
style: {
|
|
544
|
+
fg: COLORS.btnFg,
|
|
545
|
+
bg: COLORS.removeBg,
|
|
546
|
+
focus: { fg: 'black', bg: COLORS.removeFocusBg },
|
|
547
|
+
},
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
const configBtn = blessed.button({
|
|
551
|
+
parent: box,
|
|
552
|
+
top: yOffset + 1,
|
|
553
|
+
left: 60,
|
|
554
|
+
width: 14,
|
|
555
|
+
height: 1,
|
|
556
|
+
content: ' Configure ',
|
|
557
|
+
tags: true,
|
|
558
|
+
mouse: true,
|
|
559
|
+
keys: true,
|
|
560
|
+
hidden: true,
|
|
561
|
+
style: {
|
|
562
|
+
fg: 'black',
|
|
563
|
+
bg: COLORS.cfgBg,
|
|
564
|
+
focus: { fg: 'black', bg: COLORS.cfgFocusBg },
|
|
565
|
+
},
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
// Wire actions
|
|
569
|
+
installBtn.on('press', async () => { await handleProviderInstall(provider); });
|
|
570
|
+
installBtn.key(['enter', 'space'], async () => { await handleProviderInstall(provider); });
|
|
571
|
+
|
|
572
|
+
removeBtn.on('press', async () => { await handleProviderRemove(provider); });
|
|
573
|
+
removeBtn.key(['enter', 'space'], async () => { await handleProviderRemove(provider); });
|
|
574
|
+
|
|
575
|
+
configBtn.on('press', async () => { await handleProviderConfigure(provider); });
|
|
576
|
+
configBtn.key(['enter', 'space'], async () => { await handleProviderConfigure(provider); });
|
|
577
|
+
|
|
578
|
+
// Navigation on each button
|
|
579
|
+
for (const btn of [installBtn, removeBtn, configBtn]) {
|
|
580
|
+
btn.key(['tab', 'right'], () => { cycleFocus(1); });
|
|
581
|
+
btn.key(['S-tab', 'left'], () => { cycleFocus(-1); });
|
|
582
|
+
btn.key(['escape'], () => {
|
|
583
|
+
if (typeof focusMainTabBar === 'function') {
|
|
584
|
+
focusMainTabBar();
|
|
585
|
+
screen.render();
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
btn.key(['up'], () => {
|
|
589
|
+
const prevIdx = providerFocusIndex - 3;
|
|
590
|
+
if (prevIdx >= 0) {
|
|
591
|
+
providerFocusIndex = prevIdx;
|
|
592
|
+
providerFocusableItems[providerFocusIndex].focus();
|
|
593
|
+
screen.render();
|
|
594
|
+
} else if (typeof focusMainTabBar === 'function') {
|
|
595
|
+
focusMainTabBar();
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
btn.key(['down'], () => {
|
|
599
|
+
const nextIdx = providerFocusIndex + 3;
|
|
600
|
+
if (nextIdx < providerFocusableItems.length) {
|
|
601
|
+
providerFocusIndex = nextIdx;
|
|
602
|
+
providerFocusableItems[providerFocusIndex].focus();
|
|
603
|
+
screen.render();
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
providerRows.push({ id: provider.id, label, statusText, installBtn, removeBtn, configBtn });
|
|
609
|
+
return { installBtn, removeBtn, configBtn };
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Build all provider rows
|
|
613
|
+
for (let i = 0; i < PROVIDERS.length; i++) {
|
|
614
|
+
const { installBtn, removeBtn, configBtn } = createProviderRow(PROVIDERS[i], i);
|
|
615
|
+
providerFocusableItems.push(installBtn, removeBtn, configBtn);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function cycleFocus(dir) {
|
|
619
|
+
providerFocusIndex = (providerFocusIndex + dir + providerFocusableItems.length) % providerFocusableItems.length;
|
|
620
|
+
providerFocusableItems[providerFocusIndex].focus();
|
|
621
|
+
screen.render();
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// ── Provider install/remove handlers ──────────────────────────────────────
|
|
625
|
+
|
|
626
|
+
async function handleProviderInstall(provider) {
|
|
627
|
+
if (provider.id === 'claude-code') {
|
|
628
|
+
const wasInstalled = installedState[provider.id];
|
|
629
|
+
const result = await installClaudeMcp(targetDir);
|
|
630
|
+
await refreshInstalledState();
|
|
631
|
+
showClaudeCodeInfo(result, wasInstalled);
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (provider.id === 'github-copilot') {
|
|
636
|
+
const wasInstalled = installedState[provider.id];
|
|
637
|
+
const result = await installCopilotMcp(targetDir);
|
|
638
|
+
await installCopilotInstructions(targetDir, packageDir);
|
|
639
|
+
await refreshInstalledState();
|
|
640
|
+
showCopilotInfo(result, wasInstalled);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (provider.id === 'openai-codex') {
|
|
644
|
+
const wasInstalled = installedState[provider.id];
|
|
645
|
+
const result = await installCodexMcp(targetDir);
|
|
646
|
+
await installCopilotMcp(targetDir);
|
|
647
|
+
await installCodexInstructions(targetDir, packageDir);
|
|
648
|
+
await installCodexHooks(targetDir, packageDir);
|
|
649
|
+
await refreshInstalledState();
|
|
650
|
+
showCodexInfo(result, wasInstalled);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
async function handleProviderRemove(provider) {
|
|
655
|
+
if (provider.id === 'claude-code') {
|
|
656
|
+
const result = await uninstallClaude(targetDir);
|
|
657
|
+
await refreshInstalledState();
|
|
658
|
+
showRemoveInfo('claude-code', result.removed || []);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (provider.id === 'github-copilot') {
|
|
663
|
+
await removeCopilotMcp(targetDir);
|
|
664
|
+
await removeCopilotInstructions(targetDir);
|
|
665
|
+
await refreshInstalledState();
|
|
666
|
+
showRemoveInfo('github-copilot');
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (provider.id === 'openai-codex') {
|
|
670
|
+
await removeCodexMcp(targetDir);
|
|
671
|
+
await removeCopilotMcp(targetDir);
|
|
672
|
+
await removeCodexInstructions(targetDir);
|
|
673
|
+
await removeCodexHooks(targetDir);
|
|
674
|
+
await refreshInstalledState();
|
|
675
|
+
showRemoveInfo('openai-codex');
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// ── Provider configure handler ────────────────────────────────────────────
|
|
680
|
+
|
|
681
|
+
async function handleProviderConfigure(provider) {
|
|
682
|
+
const llmKeyMap = {
|
|
683
|
+
'claude-code': 'claude-code',
|
|
684
|
+
'github-copilot': 'copilot',
|
|
685
|
+
'openai-codex': 'codex',
|
|
686
|
+
};
|
|
687
|
+
const llmKey = llmKeyMap[provider.id] || provider.id;
|
|
688
|
+
const config = loadLlmConfigSync(llmKey, targetDir);
|
|
689
|
+
_openLlmConfigModal(provider, llmKey, config);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// ── LLM Config Modal ─────────────────────────────────────────────────────
|
|
693
|
+
|
|
694
|
+
function _openLlmConfigModal(provider, llmKey, config) {
|
|
695
|
+
// Guard against double-open (key repeat, double-click)
|
|
696
|
+
if (navigationService?.isModalOpen()) return;
|
|
697
|
+
let _closed = false;
|
|
698
|
+
navigationService?.openModal();
|
|
699
|
+
|
|
700
|
+
const defaultPretext = {
|
|
701
|
+
'claude-code': 'Claude Code here',
|
|
702
|
+
'copilot': 'Copilot here',
|
|
703
|
+
'codex': 'Codex here',
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
// Read global defaults for display
|
|
707
|
+
const globalEngine = providerService?.getActiveProvider?.() || 'piper';
|
|
708
|
+
const globalVoice = providerService?.getActiveVoiceId?.() || 'none';
|
|
709
|
+
|
|
710
|
+
const draft = {
|
|
711
|
+
ttsEngine: config.ttsEngine || '',
|
|
712
|
+
voice: config.voice || '',
|
|
713
|
+
pretext: config.pretext || defaultPretext[llmKey] || '',
|
|
714
|
+
reverbPreset: config.effects || 'off',
|
|
715
|
+
bgTrack: config.bgTrack || '',
|
|
716
|
+
bgVolume: config.bgVolume || '0.15',
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
const modal = blessed.box({
|
|
720
|
+
parent: screen,
|
|
721
|
+
top: 'center',
|
|
722
|
+
left: 'center',
|
|
723
|
+
width: 72,
|
|
724
|
+
height: 22,
|
|
725
|
+
border: { type: 'line' },
|
|
726
|
+
tags: true,
|
|
727
|
+
label: ` {bold}{cyan-fg} ${provider.name} -- Audio Config {/cyan-fg}{/bold} `,
|
|
728
|
+
style: {
|
|
729
|
+
fg: COLORS.labelFg,
|
|
730
|
+
bg: COLORS.contentBg,
|
|
731
|
+
border: { fg: 'cyan' },
|
|
732
|
+
},
|
|
733
|
+
});
|
|
734
|
+
modal.setFront();
|
|
735
|
+
|
|
736
|
+
// Field definitions
|
|
737
|
+
const FIELDS = [
|
|
738
|
+
{ key: 'ttsEngine', label: 'TTS Engine', getValue: () => draft.ttsEngine || `(global: ${globalEngine})` },
|
|
739
|
+
{ key: 'voice', label: 'Voice', getValue: () => draft.voice || `(global: ${globalVoice})` },
|
|
740
|
+
{ key: 'pretext', label: 'Pretext', getValue: () => draft.pretext || '(none)' },
|
|
741
|
+
{ key: 'reverb', label: 'Reverb', getValue: () => {
|
|
742
|
+
const p = REVERB_PRESETS.find(r => r.value === draft.reverbPreset);
|
|
743
|
+
return p ? p.label : draft.reverbPreset || 'Off';
|
|
744
|
+
}},
|
|
745
|
+
{ key: 'bgTrack', label: 'Music Track', getValue: () => formatTrackName(draft.bgTrack) || '(default)' },
|
|
746
|
+
{ key: 'bgVolume', label: 'Music Vol', getValue: () => {
|
|
747
|
+
const pct = Math.round(parseFloat(draft.bgVolume) * 100);
|
|
748
|
+
return `${pct}%`;
|
|
749
|
+
}},
|
|
750
|
+
];
|
|
751
|
+
|
|
752
|
+
function _fieldItems() {
|
|
753
|
+
return FIELDS.map(f => {
|
|
754
|
+
const label = f.label.padEnd(14);
|
|
755
|
+
return ` ${label} ${f.getValue()}`;
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
const fieldList = blessed.list({
|
|
760
|
+
parent: modal,
|
|
761
|
+
top: 1,
|
|
762
|
+
left: 2,
|
|
763
|
+
right: 2,
|
|
764
|
+
height: FIELDS.length + 2,
|
|
765
|
+
keys: true,
|
|
766
|
+
vi: false,
|
|
767
|
+
mouse: true,
|
|
768
|
+
border: { type: 'line' },
|
|
769
|
+
tags: true,
|
|
770
|
+
style: {
|
|
771
|
+
fg: COLORS.labelFg,
|
|
772
|
+
bg: COLORS.contentBg,
|
|
773
|
+
border: { fg: 'blue' },
|
|
774
|
+
selected: { bg: 'blue', fg: 'yellow' },
|
|
775
|
+
item: { fg: COLORS.labelFg },
|
|
776
|
+
},
|
|
777
|
+
});
|
|
778
|
+
fieldList.setItems(_fieldItems());
|
|
779
|
+
|
|
780
|
+
blessed.text({
|
|
781
|
+
parent: modal,
|
|
782
|
+
bottom: 4,
|
|
783
|
+
left: 2,
|
|
784
|
+
right: 2,
|
|
785
|
+
tags: true,
|
|
786
|
+
content: '{white-fg}[Up/Down] Navigate [Enter] Edit [Tab] Buttons [Esc] Close{/white-fg}',
|
|
787
|
+
style: { bg: COLORS.contentBg },
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
// Buttons
|
|
791
|
+
function _modalBtn(label, leftPos, onClick) {
|
|
792
|
+
const btn = blessed.button({
|
|
793
|
+
parent: modal,
|
|
794
|
+
content: label,
|
|
795
|
+
bottom: 2,
|
|
796
|
+
left: leftPos,
|
|
797
|
+
mouse: true,
|
|
798
|
+
keys: true,
|
|
799
|
+
shrink: true,
|
|
800
|
+
padding: { left: 1, right: 1 },
|
|
801
|
+
style: {
|
|
802
|
+
bg: 'blue',
|
|
803
|
+
fg: 'white',
|
|
804
|
+
focus: { bg: 'cyan', fg: 'black', bold: true },
|
|
805
|
+
hover: { bg: 'cyan', fg: 'black', bold: true },
|
|
806
|
+
},
|
|
807
|
+
});
|
|
808
|
+
btn.key(['enter', 'space'], () => onClick());
|
|
809
|
+
btn.on('click', () => onClick());
|
|
810
|
+
return btn;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// Preview status line
|
|
814
|
+
const previewLine = blessed.text({
|
|
815
|
+
parent: modal,
|
|
816
|
+
bottom: 1,
|
|
817
|
+
left: 2,
|
|
818
|
+
right: 2,
|
|
819
|
+
tags: true,
|
|
820
|
+
content: '',
|
|
821
|
+
style: { bg: COLORS.contentBg },
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
let _previewModalProc = null;
|
|
825
|
+
function _killPreview() {
|
|
826
|
+
if (_previewModalProc) {
|
|
827
|
+
try { _previewModalProc.kill(); } catch {}
|
|
828
|
+
_previewModalProc = null;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function _playPreview() {
|
|
833
|
+
_killPreview();
|
|
834
|
+
previewLine.setContent('{cyan-fg}♪ Previewing...{/cyan-fg}');
|
|
835
|
+
screen.render();
|
|
836
|
+
|
|
837
|
+
// Save first so play-tts picks up current settings
|
|
838
|
+
_autoSave(true);
|
|
839
|
+
|
|
840
|
+
// Temporarily enable background music for preview if a track is configured
|
|
841
|
+
const hasBgTrack = !!draft.bgTrack;
|
|
842
|
+
let _bgRestore = null;
|
|
843
|
+
if (hasBgTrack) {
|
|
844
|
+
const avCfgPath = path.join(targetDir, '.agentvibes', 'config.json');
|
|
845
|
+
try {
|
|
846
|
+
const raw = fs.readFileSync(avCfgPath, 'utf8');
|
|
847
|
+
const cfg = JSON.parse(raw);
|
|
848
|
+
if (cfg.backgroundMusic && !cfg.backgroundMusic.enabled) {
|
|
849
|
+
cfg.backgroundMusic.enabled = true;
|
|
850
|
+
fs.writeFileSync(avCfgPath, JSON.stringify(cfg, null, 2) + '\n');
|
|
851
|
+
_bgRestore = () => { cfg.backgroundMusic.enabled = false; fs.writeFileSync(avCfgPath, JSON.stringify(cfg, null, 2) + '\n'); };
|
|
852
|
+
}
|
|
853
|
+
} catch {
|
|
854
|
+
// No .agentvibes/config.json — use legacy txt fallback
|
|
855
|
+
const bgEnabledFile = path.join(targetDir, '.claude', 'config', 'background-music-enabled.txt');
|
|
856
|
+
let bgWas = false;
|
|
857
|
+
try { bgWas = fs.readFileSync(bgEnabledFile, 'utf8').trim() === 'true'; } catch {}
|
|
858
|
+
if (!bgWas) {
|
|
859
|
+
try { fs.writeFileSync(bgEnabledFile, 'true', 'utf8'); } catch {}
|
|
860
|
+
_bgRestore = () => { try { fs.writeFileSync(bgEnabledFile, 'false', 'utf8'); } catch {} };
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
const hooksSubdir = process.platform === 'win32' ? 'hooks-windows' : 'hooks';
|
|
866
|
+
const isWin = process.platform === 'win32' && !process.env.WSL_DISTRO_NAME;
|
|
867
|
+
// Don't include pretext — play-tts already prepends it from the config
|
|
868
|
+
const sampleText = 'Here is a preview of your audio settings.';
|
|
869
|
+
|
|
870
|
+
let cmd, args;
|
|
871
|
+
if (isWin) {
|
|
872
|
+
const script = path.join(targetDir, '.claude', hooksSubdir, 'play-tts.ps1');
|
|
873
|
+
cmd = 'powershell';
|
|
874
|
+
args = ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', script, sampleText, '', '-llm', llmKey];
|
|
875
|
+
} else {
|
|
876
|
+
const script = path.join(targetDir, '.claude', hooksSubdir, 'play-tts.sh');
|
|
877
|
+
cmd = 'bash';
|
|
878
|
+
args = [script, sampleText, '', '--llm', llmKey];
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
const proc = spawn(cmd, args, {
|
|
882
|
+
stdio: 'ignore',
|
|
883
|
+
windowsHide: true,
|
|
884
|
+
env: { ...process.env, CLAUDE_PROJECT_DIR: targetDir },
|
|
885
|
+
});
|
|
886
|
+
_previewModalProc = proc;
|
|
887
|
+
|
|
888
|
+
const _restoreBg = () => { if (_bgRestore) _bgRestore(); };
|
|
889
|
+
|
|
890
|
+
proc.on('exit', () => {
|
|
891
|
+
_previewModalProc = null;
|
|
892
|
+
_restoreBg();
|
|
893
|
+
if (!_closed) { previewLine.setContent(''); screen.render(); }
|
|
894
|
+
});
|
|
895
|
+
proc.on('error', () => {
|
|
896
|
+
_previewModalProc = null;
|
|
897
|
+
_restoreBg();
|
|
898
|
+
if (!_closed) { previewLine.setContent('{red-fg}Preview failed{/red-fg}'); screen.render(); }
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// Auto-save: persist draft to config immediately on any change
|
|
903
|
+
function _autoSave(silent) {
|
|
904
|
+
// Infer engine from voice — voice picker only shows Piper voices,
|
|
905
|
+
// so if a voice is set but no engine chosen, default to piper
|
|
906
|
+
const engine = draft.ttsEngine || (draft.voice ? 'piper' : '');
|
|
907
|
+
saveLlmConfigSync(llmKey, {
|
|
908
|
+
voice: draft.voice,
|
|
909
|
+
pretext: draft.pretext,
|
|
910
|
+
effects: draft.reverbPreset === 'off' ? '' : draft.reverbPreset,
|
|
911
|
+
bgTrack: draft.bgTrack,
|
|
912
|
+
bgVolume: draft.bgVolume,
|
|
913
|
+
ttsEngine: engine,
|
|
914
|
+
sourcePath: config.sourcePath,
|
|
915
|
+
}, targetDir);
|
|
916
|
+
if (!silent) {
|
|
917
|
+
const cfgPath = config.sourcePath || resolveCfgPath(targetDir);
|
|
918
|
+
_showSavedToast('Settings', cfgPath);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
const previewBtn = _modalBtn('Preview', 4, _playPreview);
|
|
923
|
+
|
|
924
|
+
const resetBtn = _modalBtn('Reset', 18, () => {
|
|
925
|
+
draft.ttsEngine = '';
|
|
926
|
+
draft.voice = '';
|
|
927
|
+
draft.pretext = defaultPretext[llmKey] || '';
|
|
928
|
+
draft.reverbPreset = 'off';
|
|
929
|
+
draft.bgTrack = '';
|
|
930
|
+
draft.bgVolume = '0.15';
|
|
931
|
+
_autoSave();
|
|
932
|
+
fieldList.setItems(_fieldItems());
|
|
933
|
+
fieldList.focus();
|
|
934
|
+
screen.render();
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
const closeBtn = _modalBtn('Close', 30, _closeModal);
|
|
938
|
+
|
|
939
|
+
const allBtns = [previewBtn, resetBtn, closeBtn];
|
|
940
|
+
const btnBlink = attachBtnBlink(allBtns, screen);
|
|
941
|
+
|
|
942
|
+
function _closeModal() {
|
|
943
|
+
if (_closed) return;
|
|
944
|
+
_closed = true;
|
|
945
|
+
_killPreview();
|
|
946
|
+
btnBlink.cleanup();
|
|
947
|
+
navigationService?.closeModal();
|
|
948
|
+
destroyList(modal, screen);
|
|
949
|
+
if (providerFocusableItems.length) providerFocusableItems[providerFocusIndex]?.focus();
|
|
950
|
+
screen.render();
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// Field editing via Enter
|
|
954
|
+
fieldList.key(['enter'], () => {
|
|
955
|
+
const idx = fieldList.selected;
|
|
956
|
+
const field = FIELDS[idx];
|
|
957
|
+
if (!field) return;
|
|
958
|
+
|
|
959
|
+
const _refreshField = () => {
|
|
960
|
+
_autoSave();
|
|
961
|
+
fieldList.setItems(_fieldItems());
|
|
962
|
+
fieldList.select(idx);
|
|
963
|
+
fieldList.focus();
|
|
964
|
+
screen.render();
|
|
965
|
+
};
|
|
966
|
+
const _cancelField = () => {
|
|
967
|
+
fieldList.focus();
|
|
968
|
+
screen.render();
|
|
969
|
+
};
|
|
970
|
+
|
|
971
|
+
switch (field.key) {
|
|
972
|
+
case 'ttsEngine':
|
|
973
|
+
_openTtsEnginePicker(draft, _refreshField);
|
|
974
|
+
break;
|
|
975
|
+
|
|
976
|
+
case 'voice':
|
|
977
|
+
_openVoicePickerForLlm(draft, _refreshField);
|
|
978
|
+
break;
|
|
979
|
+
|
|
980
|
+
case 'pretext':
|
|
981
|
+
_openPretextEditor(modal, draft, _refreshField);
|
|
982
|
+
break;
|
|
983
|
+
|
|
984
|
+
case 'reverb':
|
|
985
|
+
openReverbPicker(screen, draft.reverbPreset, (val) => {
|
|
986
|
+
draft.reverbPreset = val;
|
|
987
|
+
_refreshField();
|
|
988
|
+
}, _cancelField, { applyToEffectsManager: false });
|
|
989
|
+
break;
|
|
990
|
+
|
|
991
|
+
case 'bgTrack':
|
|
992
|
+
openTrackPicker(screen, draft.bgTrack, Math.round(parseFloat(draft.bgVolume) * 100), (track) => {
|
|
993
|
+
draft.bgTrack = track;
|
|
994
|
+
_refreshField();
|
|
995
|
+
}, _cancelField, { skipVolume: true });
|
|
996
|
+
break;
|
|
997
|
+
|
|
998
|
+
case 'bgVolume':
|
|
999
|
+
openVolumeInput(screen, Math.round(parseFloat(draft.bgVolume) * 100), (volume) => {
|
|
1000
|
+
draft.bgVolume = (volume / 100).toFixed(2);
|
|
1001
|
+
_refreshField();
|
|
1002
|
+
}, _cancelField);
|
|
1003
|
+
break;
|
|
1004
|
+
}
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
fieldList.key(['escape'], _closeModal);
|
|
1008
|
+
|
|
1009
|
+
// Remove selection highlight when field list loses focus
|
|
1010
|
+
fieldList.on('blur', () => {
|
|
1011
|
+
fieldList.style.selected = { bg: COLORS.contentBg, fg: COLORS.labelFg };
|
|
1012
|
+
fieldList.setItems(_fieldItems());
|
|
1013
|
+
screen.render();
|
|
1014
|
+
});
|
|
1015
|
+
fieldList.on('focus', () => {
|
|
1016
|
+
fieldList.style.selected = { bg: 'blue', fg: 'yellow' };
|
|
1017
|
+
fieldList.setItems(_fieldItems());
|
|
1018
|
+
screen.render();
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
// Wrap: down on last field → focus Save; up on first field → focus Save
|
|
1022
|
+
// One extra arrow press at boundary moves to button row.
|
|
1023
|
+
// Track previous selection so arriving at boundary doesn't immediately jump.
|
|
1024
|
+
let _prevFieldSel = 0;
|
|
1025
|
+
fieldList.key(['down'], () => {
|
|
1026
|
+
const cur = fieldList.selected ?? 0;
|
|
1027
|
+
if (cur === FIELDS.length - 1 && _prevFieldSel === FIELDS.length - 1) {
|
|
1028
|
+
allBtns[0].focus(); screen.render();
|
|
1029
|
+
}
|
|
1030
|
+
_prevFieldSel = cur;
|
|
1031
|
+
});
|
|
1032
|
+
fieldList.key(['up'], () => {
|
|
1033
|
+
const cur = fieldList.selected ?? 0;
|
|
1034
|
+
if (cur === 0 && _prevFieldSel === 0) {
|
|
1035
|
+
allBtns[0].focus(); screen.render();
|
|
1036
|
+
}
|
|
1037
|
+
_prevFieldSel = cur;
|
|
1038
|
+
});
|
|
1039
|
+
fieldList.key(['tab'], () => {
|
|
1040
|
+
allBtns[0].focus();
|
|
1041
|
+
screen.render();
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
for (let i = 0; i < allBtns.length; i++) {
|
|
1045
|
+
allBtns[i].key(['tab', 'right'], () => {
|
|
1046
|
+
allBtns[(i + 1) % allBtns.length].focus();
|
|
1047
|
+
screen.render();
|
|
1048
|
+
});
|
|
1049
|
+
allBtns[i].key(['S-tab', 'left'], () => {
|
|
1050
|
+
allBtns[(i - 1 + allBtns.length) % allBtns.length].focus();
|
|
1051
|
+
screen.render();
|
|
1052
|
+
});
|
|
1053
|
+
allBtns[i].key(['escape'], _closeModal);
|
|
1054
|
+
allBtns[i].key(['up'], () => {
|
|
1055
|
+
fieldList.focus();
|
|
1056
|
+
screen.render();
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
modal.key(['escape'], _closeModal);
|
|
1061
|
+
fieldList.focus();
|
|
1062
|
+
screen.render();
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// ── TTS Engine picker (for config modal) ──────────────────────────────────
|
|
1066
|
+
|
|
1067
|
+
function _openTtsEnginePicker(draft, onDone) {
|
|
1068
|
+
navigationService?.openModal();
|
|
1069
|
+
|
|
1070
|
+
const engines = getEngineStatuses();
|
|
1071
|
+
const items = engines.map(e => {
|
|
1072
|
+
const status = e.installed ? '{green-fg}[OK]{/green-fg}' : '{yellow-fg}[Not Found]{/yellow-fg}';
|
|
1073
|
+
return ` ${e.name.padEnd(20)} ${status} ${e.desc}`;
|
|
1074
|
+
});
|
|
1075
|
+
// Add "(global default)" option at top
|
|
1076
|
+
items.unshift(' (global default)');
|
|
1077
|
+
|
|
1078
|
+
const picker = blessed.list({
|
|
1079
|
+
parent: screen,
|
|
1080
|
+
top: 'center',
|
|
1081
|
+
left: 'center',
|
|
1082
|
+
width: 70,
|
|
1083
|
+
height: Math.min(items.length + 4, 16),
|
|
1084
|
+
border: { type: 'line' },
|
|
1085
|
+
tags: true,
|
|
1086
|
+
label: ' {bold}{cyan-fg} Select TTS Engine {/cyan-fg}{/bold} ',
|
|
1087
|
+
keys: true,
|
|
1088
|
+
vi: false,
|
|
1089
|
+
mouse: true,
|
|
1090
|
+
style: {
|
|
1091
|
+
fg: COLORS.labelFg,
|
|
1092
|
+
bg: COLORS.contentBg,
|
|
1093
|
+
border: { fg: 'cyan' },
|
|
1094
|
+
selected: { bg: 'blue', fg: 'yellow' },
|
|
1095
|
+
item: { fg: COLORS.labelFg },
|
|
1096
|
+
},
|
|
1097
|
+
});
|
|
1098
|
+
picker.setFront();
|
|
1099
|
+
picker.setItems(items);
|
|
1100
|
+
|
|
1101
|
+
picker.key(['enter'], () => {
|
|
1102
|
+
const idx = picker.selected;
|
|
1103
|
+
if (idx === 0) {
|
|
1104
|
+
draft.ttsEngine = '';
|
|
1105
|
+
} else {
|
|
1106
|
+
draft.ttsEngine = engines[idx - 1].id;
|
|
1107
|
+
}
|
|
1108
|
+
navigationService?.closeModal();
|
|
1109
|
+
destroyList(picker, screen);
|
|
1110
|
+
onDone();
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
picker.key(['escape'], () => {
|
|
1114
|
+
navigationService?.closeModal();
|
|
1115
|
+
destroyList(picker, screen);
|
|
1116
|
+
onDone();
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
picker.focus();
|
|
1120
|
+
screen.render();
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// ── Voice picker for LLM config (matches agents-tab pattern) ──────────────
|
|
1124
|
+
|
|
1125
|
+
function _secureTempWav(prefix) {
|
|
1126
|
+
const baseDir = process.env.XDG_RUNTIME_DIR || os.tmpdir();
|
|
1127
|
+
const dir = path.join(baseDir, `agentvibes-${process.getuid?.() ?? 'u'}`);
|
|
1128
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
1129
|
+
try { fs.chmodSync(dir, 0o700); } catch {}
|
|
1130
|
+
return path.join(dir, `${prefix}-${crypto.randomUUID()}.wav`);
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
function _openVoicePickerForLlm(draft, onDone) {
|
|
1134
|
+
navigationService?.openModal();
|
|
1135
|
+
|
|
1136
|
+
let _allVoices = [];
|
|
1137
|
+
let _previewProc = null;
|
|
1138
|
+
let _previewVoiceId = null;
|
|
1139
|
+
let _vpClosed = false;
|
|
1140
|
+
|
|
1141
|
+
const _spawnEnv = buildAudioEnv();
|
|
1142
|
+
const _isWin = process.platform === 'win32' && !process.env.WSL_DISTRO_NAME;
|
|
1143
|
+
|
|
1144
|
+
function _killVP() {
|
|
1145
|
+
if (_previewProc) {
|
|
1146
|
+
try {
|
|
1147
|
+
if (_isWin) { _previewProc.kill(); } else { process.kill(-_previewProc.pid, 'SIGTERM'); }
|
|
1148
|
+
} catch {}
|
|
1149
|
+
_previewProc = null;
|
|
1150
|
+
}
|
|
1151
|
+
_previewVoiceId = null;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
function _closeVP() {
|
|
1155
|
+
if (_vpClosed) return;
|
|
1156
|
+
_vpClosed = true;
|
|
1157
|
+
_killVP();
|
|
1158
|
+
navigationService?.closeModal();
|
|
1159
|
+
destroyList(vpModal, screen, onDone);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
const vpModal = blessed.box({
|
|
1163
|
+
parent: screen,
|
|
1164
|
+
top: '6%',
|
|
1165
|
+
left: '3%',
|
|
1166
|
+
width: '94%',
|
|
1167
|
+
height: '88%',
|
|
1168
|
+
border: { type: 'line' },
|
|
1169
|
+
tags: true,
|
|
1170
|
+
label: ' {bold}{cyan-fg} Select Voice {/cyan-fg}{/bold} ',
|
|
1171
|
+
style: { fg: COLORS.labelFg, bg: COLORS.contentBg, border: { fg: 'cyan' } },
|
|
1172
|
+
});
|
|
1173
|
+
vpModal.setFront();
|
|
1174
|
+
|
|
1175
|
+
// Column header
|
|
1176
|
+
const COL_N = 30;
|
|
1177
|
+
const COL_G = 4;
|
|
1178
|
+
blessed.text({
|
|
1179
|
+
parent: vpModal, top: 1, left: 6, tags: true,
|
|
1180
|
+
content: `{cyan-fg}${'Name'.padEnd(COL_N)}{/cyan-fg}{magenta-fg}♀{/magenta-fg}/{bright-cyan-fg}♂{/bright-cyan-fg} {cyan-fg}Provider{/cyan-fg}`,
|
|
1181
|
+
style: { bg: COLORS.contentBg },
|
|
1182
|
+
});
|
|
1183
|
+
|
|
1184
|
+
const vpList = blessed.list({
|
|
1185
|
+
parent: vpModal, top: 2, left: 2, right: 2, bottom: 5,
|
|
1186
|
+
keys: true, vi: true, mouse: true,
|
|
1187
|
+
border: { type: 'line' },
|
|
1188
|
+
scrollbar: { ch: '|', style: { fg: 'cyan' } },
|
|
1189
|
+
tags: true,
|
|
1190
|
+
style: {
|
|
1191
|
+
fg: COLORS.labelFg, bg: COLORS.contentBg,
|
|
1192
|
+
border: { fg: 'blue' },
|
|
1193
|
+
selected: { bg: 'green', fg: 'white', bold: true },
|
|
1194
|
+
item: { fg: COLORS.labelFg },
|
|
1195
|
+
},
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
const vpPreviewLine = blessed.text({
|
|
1199
|
+
parent: vpModal, bottom: 3, left: 2, right: 2, tags: true,
|
|
1200
|
+
content: '', style: { fg: 'cyan', bg: COLORS.contentBg },
|
|
1201
|
+
});
|
|
1202
|
+
|
|
1203
|
+
blessed.text({
|
|
1204
|
+
parent: vpModal, bottom: 2, left: 2, right: 2, tags: true,
|
|
1205
|
+
content: '{white-fg}[↑↓] Nav [PgUp/PgDn] Page [Home/End] [a-z] Jump [Enter] Select [Space] Preview [*] Fav [Esc] Cancel{/white-fg}',
|
|
1206
|
+
style: { bg: COLORS.contentBg },
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
function _buildVoiceItems(voices) {
|
|
1210
|
+
const favs = getFavorites(configService);
|
|
1211
|
+
return voices.map(v => {
|
|
1212
|
+
const isActive = v === draft.voice;
|
|
1213
|
+
const isPrev = v === _previewVoiceId;
|
|
1214
|
+
const isFav = favs.includes(v);
|
|
1215
|
+
const dot = isPrev ? '♪' : (isActive ? '●' : ' ');
|
|
1216
|
+
const star = isFav ? '{yellow-fg}★{/yellow-fg}' : ' ';
|
|
1217
|
+
const meta = getVoiceMeta(v);
|
|
1218
|
+
const name = meta.displayName.length > COL_N
|
|
1219
|
+
? meta.displayName.slice(0, COL_N - 1) + '…'
|
|
1220
|
+
: meta.displayName.padEnd(COL_N);
|
|
1221
|
+
// genderIconTag has invisible color tags — pad with literal spaces (1 visible char + 3 spaces = 4)
|
|
1222
|
+
return ` ${dot}${star} ${name}${genderIconTag(meta.gender)} ${meta.provider}`;
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
function _refreshVP() {
|
|
1227
|
+
if (_vpClosed) return;
|
|
1228
|
+
const savedIdx = vpList.selected ?? 0;
|
|
1229
|
+
const savedScroll = vpList.childBase ?? 0;
|
|
1230
|
+
_allVoices = scanInstalledVoices();
|
|
1231
|
+
// Sort by display name so the first-letter quick jump is intuitive
|
|
1232
|
+
_allVoices.sort((a, b) => getVoiceMeta(a).displayName.localeCompare(
|
|
1233
|
+
getVoiceMeta(b).displayName, undefined, { sensitivity: 'base' }));
|
|
1234
|
+
const items = _buildVoiceItems(_allVoices);
|
|
1235
|
+
vpList.setItems(items.length > 0 ? items : [' (no voices found)']);
|
|
1236
|
+
vpList.select(Math.min(savedIdx, items.length - 1));
|
|
1237
|
+
vpList.childBase = Math.min(savedScroll, Math.max(0, items.length - (vpList.height - 2)));
|
|
1238
|
+
screen.render();
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
function _previewVoice(voiceId) {
|
|
1242
|
+
if (_previewVoiceId === voiceId) { _killVP(); vpPreviewLine.setContent(''); _refreshVP(); return; }
|
|
1243
|
+
_killVP();
|
|
1244
|
+
|
|
1245
|
+
const _ms = parseMultiSpeaker(voiceId);
|
|
1246
|
+
const voicePath = path.resolve(PIPER_VOICES_DIR, _ms.model + '.onnx');
|
|
1247
|
+
const safeBase = path.resolve(PIPER_VOICES_DIR);
|
|
1248
|
+
if (!voicePath.startsWith(safeBase + path.sep) && voicePath !== safeBase) return;
|
|
1249
|
+
|
|
1250
|
+
const tempWav = _secureTempWav('vp');
|
|
1251
|
+
const phrase = SAMPLE_PHRASES[Math.floor(Math.random() * SAMPLE_PHRASES.length)];
|
|
1252
|
+
|
|
1253
|
+
let _piperBin = 'piper';
|
|
1254
|
+
if (_isWin) {
|
|
1255
|
+
const _lad = process.env.LOCALAPPDATA ||
|
|
1256
|
+
(process.env.USERPROFILE ? path.join(process.env.USERPROFILE, 'AppData', 'Local') : null);
|
|
1257
|
+
if (_lad) {
|
|
1258
|
+
const _ep = path.join(_lad, 'Programs', 'Piper', 'piper.exe');
|
|
1259
|
+
if (fs.existsSync(_ep)) _piperBin = _ep;
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
const args = ['--model', voicePath, '--output_file', tempWav];
|
|
1264
|
+
if (_ms.speakerId != null) args.push('--speaker', String(_ms.speakerId));
|
|
1265
|
+
const piper = spawn(_piperBin, args, {
|
|
1266
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
1267
|
+
detached: !_isWin,
|
|
1268
|
+
windowsHide: true,
|
|
1269
|
+
env: _spawnEnv,
|
|
1270
|
+
});
|
|
1271
|
+
piper.stdin.write(phrase + '\n');
|
|
1272
|
+
piper.stdin.end();
|
|
1273
|
+
_previewProc = piper;
|
|
1274
|
+
_previewVoiceId = voiceId;
|
|
1275
|
+
|
|
1276
|
+
if (!_vpClosed) {
|
|
1277
|
+
vpPreviewLine.setContent(`{cyan-fg}♪ Synthesizing: ${voiceId}...{/cyan-fg}`);
|
|
1278
|
+
_refreshVP();
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
piper.on('exit', (code) => {
|
|
1282
|
+
if (_previewVoiceId !== voiceId) { try { fs.unlinkSync(tempWav); } catch {} return; }
|
|
1283
|
+
if (code !== 0) { _previewProc = null; _previewVoiceId = null; try { fs.unlinkSync(tempWav); } catch {} return; }
|
|
1284
|
+
const wp = detectWavPlayer(_spawnEnv);
|
|
1285
|
+
if (!wp) return;
|
|
1286
|
+
const pp = spawn(wp.bin, wp.args(tempWav), {
|
|
1287
|
+
stdio: 'ignore',
|
|
1288
|
+
detached: !_isWin,
|
|
1289
|
+
windowsHide: true,
|
|
1290
|
+
env: _spawnEnv,
|
|
1291
|
+
});
|
|
1292
|
+
_previewProc = pp;
|
|
1293
|
+
if (!_vpClosed) { vpPreviewLine.setContent(`{cyan-fg}♪ Playing: ${voiceId}{/cyan-fg}`); screen.render(); }
|
|
1294
|
+
pp.on('exit', () => {
|
|
1295
|
+
if (_previewVoiceId === voiceId) { _previewVoiceId = null; _previewProc = null; if (!_vpClosed) { vpPreviewLine.setContent(''); _refreshVP(); } }
|
|
1296
|
+
try { fs.unlinkSync(tempWav); } catch {}
|
|
1297
|
+
});
|
|
1298
|
+
});
|
|
1299
|
+
piper.on('error', () => { _previewProc = null; _previewVoiceId = null; });
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
vpList.key(['enter'], () => {
|
|
1303
|
+
const sel = _allVoices[vpList.selected];
|
|
1304
|
+
if (sel) { draft.voice = sel; _closeVP(); }
|
|
1305
|
+
});
|
|
1306
|
+
vpList.key(['space'], () => {
|
|
1307
|
+
const sel = _allVoices[vpList.selected];
|
|
1308
|
+
if (sel) _previewVoice(sel);
|
|
1309
|
+
});
|
|
1310
|
+
vpList.key(['*'], () => {
|
|
1311
|
+
const sel = _allVoices[vpList.selected];
|
|
1312
|
+
if (sel) { toggleFavorite(configService, sel); _refreshVP(); }
|
|
1313
|
+
});
|
|
1314
|
+
vpList.key(['escape', 'q'], _closeVP);
|
|
1315
|
+
|
|
1316
|
+
// PageUp / PageDown / Home / End navigation
|
|
1317
|
+
const _pageSize = () => Math.max(1, (vpList.height ?? 10) - 2);
|
|
1318
|
+
vpList.key(['pageup'], () => { vpList.up(_pageSize()); screen.render(); });
|
|
1319
|
+
vpList.key(['pagedown'], () => { vpList.down(_pageSize()); screen.render(); });
|
|
1320
|
+
vpList.key(['home'], () => { vpList.select(0); screen.render(); });
|
|
1321
|
+
vpList.key(['end'], () => { vpList.select(Math.max(0, _allVoices.length - 1)); screen.render(); });
|
|
1322
|
+
|
|
1323
|
+
// First-letter quick jump: typing 'a' jumps to the first voice starting
|
|
1324
|
+
// with A. Block keys reserved by the list widget (vi nav, cancel) so
|
|
1325
|
+
// they don't get swallowed: q (cancel), j/k/g/h/l (vi navigation).
|
|
1326
|
+
const _vpJumpBlocked = new Set(['j', 'k', 'g', 'h', 'l', 'q']);
|
|
1327
|
+
vpList.on('keypress', (ch, key) => {
|
|
1328
|
+
if (!ch || key?.ctrl || key?.meta) return;
|
|
1329
|
+
if (!/^[a-zA-Z]$/.test(ch)) return;
|
|
1330
|
+
const target = ch.toLowerCase();
|
|
1331
|
+
if (_vpJumpBlocked.has(target)) return;
|
|
1332
|
+
const idx = _allVoices.findIndex(v => {
|
|
1333
|
+
const name = getVoiceMeta(v).displayName.toLowerCase();
|
|
1334
|
+
return name.startsWith(target);
|
|
1335
|
+
});
|
|
1336
|
+
if (idx >= 0) { vpList.select(idx); screen.render(); }
|
|
1337
|
+
});
|
|
1338
|
+
|
|
1339
|
+
_refreshVP();
|
|
1340
|
+
const activeIdx = _allVoices.indexOf(draft.voice);
|
|
1341
|
+
if (activeIdx >= 0) vpList.select(activeIdx);
|
|
1342
|
+
vpList.focus();
|
|
1343
|
+
screen.render();
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
// ── Pretext editor ────────────────────────────────────────────────────────
|
|
1347
|
+
|
|
1348
|
+
function _openPretextEditor(parentModal, draft, onDone) {
|
|
1349
|
+
const editModal = blessed.box({
|
|
1350
|
+
parent: screen, top: 'center', left: 'center',
|
|
1351
|
+
width: 60, height: 8,
|
|
1352
|
+
border: { type: 'line' },
|
|
1353
|
+
tags: true,
|
|
1354
|
+
label: ' {bold}{cyan-fg} Edit Pretext {/cyan-fg}{/bold} ',
|
|
1355
|
+
style: { fg: COLORS.labelFg, bg: COLORS.contentBg, border: { fg: 'cyan' } },
|
|
1356
|
+
});
|
|
1357
|
+
editModal.setFront();
|
|
1358
|
+
|
|
1359
|
+
blessed.text({
|
|
1360
|
+
parent: editModal, top: 1, left: 2, tags: true,
|
|
1361
|
+
content: '{white-fg}Spoken before every TTS message (max 200 chars):{/white-fg}',
|
|
1362
|
+
style: { bg: COLORS.contentBg },
|
|
1363
|
+
});
|
|
1364
|
+
|
|
1365
|
+
const inputBox = blessed.textbox({
|
|
1366
|
+
parent: editModal, top: 3, left: 2, right: 2, height: 3,
|
|
1367
|
+
border: { type: 'line' },
|
|
1368
|
+
inputOnFocus: true,
|
|
1369
|
+
value: draft.pretext,
|
|
1370
|
+
style: {
|
|
1371
|
+
fg: 'white', bg: 'black',
|
|
1372
|
+
border: { fg: 'blue' },
|
|
1373
|
+
focus: { border: { fg: 'cyan' } },
|
|
1374
|
+
},
|
|
1375
|
+
});
|
|
1376
|
+
|
|
1377
|
+
function _closeEdit(save) {
|
|
1378
|
+
if (save) {
|
|
1379
|
+
const val = (inputBox.getValue() || '').trim().slice(0, 200);
|
|
1380
|
+
draft.pretext = val;
|
|
1381
|
+
}
|
|
1382
|
+
destroyList(editModal, screen);
|
|
1383
|
+
onDone();
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
inputBox.key(['enter'], () => _closeEdit(true));
|
|
1387
|
+
inputBox.key(['escape'], () => _closeEdit(false));
|
|
1388
|
+
|
|
1389
|
+
inputBox.focus();
|
|
1390
|
+
inputBox.readInput(() => {});
|
|
1391
|
+
screen.render();
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
// ── Saved toast ───────────────────────────────────────────────────────────
|
|
1395
|
+
|
|
1396
|
+
function _showSavedToast(name, filePath) {
|
|
1397
|
+
const lines = [`{center}{green-fg}{bold}${name} saved!{/bold}{/green-fg}{/center}`];
|
|
1398
|
+
if (filePath) lines.push(`{center}{white-fg}${filePath}{/white-fg}{/center}`);
|
|
1399
|
+
const w = filePath ? Math.min(Math.max(filePath.length + 6, 30), 70) : 30;
|
|
1400
|
+
const toast = blessed.box({
|
|
1401
|
+
parent: screen,
|
|
1402
|
+
top: 'center',
|
|
1403
|
+
left: 'center',
|
|
1404
|
+
width: w,
|
|
1405
|
+
height: filePath ? 4 : 3,
|
|
1406
|
+
border: { type: 'line' },
|
|
1407
|
+
tags: true,
|
|
1408
|
+
content: lines.join('\n'),
|
|
1409
|
+
style: { bg: COLORS.contentBg, border: { fg: 'green' } },
|
|
1410
|
+
});
|
|
1411
|
+
toast.setFront();
|
|
1412
|
+
screen.render();
|
|
1413
|
+
setTimeout(() => {
|
|
1414
|
+
toast.destroy();
|
|
1415
|
+
screen.render();
|
|
1416
|
+
}, 1500);
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
// ── Provider info panels ──────────────────────────────────────────────────
|
|
1420
|
+
|
|
1421
|
+
function hideAllProviderRows() {
|
|
1422
|
+
providerHeader.hide();
|
|
1423
|
+
for (const row of providerRows) {
|
|
1424
|
+
row.label.hide();
|
|
1425
|
+
row.statusText.hide();
|
|
1426
|
+
row.installBtn.hide();
|
|
1427
|
+
row.removeBtn.hide();
|
|
1428
|
+
row.configBtn.hide();
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
function showAllProviderRows() {
|
|
1433
|
+
providerHeader.show();
|
|
1434
|
+
for (const row of providerRows) {
|
|
1435
|
+
row.label.show();
|
|
1436
|
+
row.statusText.show();
|
|
1437
|
+
row.installBtn.show();
|
|
1438
|
+
row.removeBtn.show();
|
|
1439
|
+
row.configBtn.show();
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
function showClaudeCodeInfo(result = null, wasInstalled = false) {
|
|
1444
|
+
providerView = 'info';
|
|
1445
|
+
hideAllProviderRows();
|
|
1446
|
+
contentBox.hide();
|
|
1447
|
+
|
|
1448
|
+
const mcpPath = path.join(targetDir, '.mcp.json');
|
|
1449
|
+
const hooksDir = path.join(targetDir, '.claude', process.platform === 'win32' ? 'hooks-windows' : 'hooks');
|
|
1450
|
+
const installed = installedState['claude-code'];
|
|
1451
|
+
const verb = wasInstalled ? 'reinstalled' : 'installed';
|
|
1452
|
+
|
|
1453
|
+
const lines = [];
|
|
1454
|
+
lines.push('{bold}{cyan-fg}Claude Code -- AgentVibes Integration{/cyan-fg}{/bold}');
|
|
1455
|
+
lines.push('');
|
|
1456
|
+
|
|
1457
|
+
if (result) {
|
|
1458
|
+
lines.push(result.success
|
|
1459
|
+
? `{green-fg}AgentVibes for Claude Code ${verb}!{/green-fg}`
|
|
1460
|
+
: `{red-fg}Installation failed{/red-fg}`);
|
|
1461
|
+
} else {
|
|
1462
|
+
lines.push(installed
|
|
1463
|
+
? '{green-fg}Installed{/green-fg}'
|
|
1464
|
+
: '{yellow-fg}Not installed{/yellow-fg}');
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
lines.push('');
|
|
1468
|
+
lines.push(`{bold}{cyan-fg}What ${result ? `got ${verb}` : 'gets installed'}:{/cyan-fg}{/bold}`);
|
|
1469
|
+
lines.push('');
|
|
1470
|
+
lines.push(' {yellow-fg}1.{/yellow-fg} {bold}.mcp.json{/bold} (project root)');
|
|
1471
|
+
lines.push(` Location: ${mcpPath}`);
|
|
1472
|
+
lines.push(' Registers the AgentVibes MCP server for Claude Code.');
|
|
1473
|
+
lines.push('');
|
|
1474
|
+
lines.push(' {yellow-fg}2.{/yellow-fg} {bold}.claude/hooks/{/bold} (session-start + pre-tool hooks)');
|
|
1475
|
+
lines.push(` Location: ${hooksDir}`);
|
|
1476
|
+
lines.push('');
|
|
1477
|
+
lines.push(' {yellow-fg}3.{/yellow-fg} {bold}.claude/commands/{/bold} (slash commands)');
|
|
1478
|
+
lines.push('');
|
|
1479
|
+
lines.push(' {yellow-fg}4.{/yellow-fg} {bold}.claude/config/{/bold} (personality, verbosity, voice settings)');
|
|
1480
|
+
lines.push('');
|
|
1481
|
+
lines.push('{white-fg}Press {bold}Enter{/bold} or {bold}Escape{/bold} to return to the provider list.{/white-fg}');
|
|
1482
|
+
|
|
1483
|
+
infoBox.setContent(lines.join('\n'));
|
|
1484
|
+
infoBox.show();
|
|
1485
|
+
infoBox.setFront();
|
|
1486
|
+
infoBox.focus();
|
|
1487
|
+
infoBox.scrollTo(0);
|
|
1488
|
+
screen.render();
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
function showCopilotInfo(result, wasInstalled = false) {
|
|
1492
|
+
providerView = 'info';
|
|
1493
|
+
hideAllProviderRows();
|
|
1494
|
+
contentBox.hide();
|
|
1495
|
+
|
|
1496
|
+
const verb = wasInstalled ? 'reinstalled' : 'installed';
|
|
1497
|
+
|
|
1498
|
+
const lines = [];
|
|
1499
|
+
lines.push('{bold}{cyan-fg}GitHub Copilot -- AgentVibes Integration{/cyan-fg}{/bold}');
|
|
1500
|
+
lines.push('');
|
|
1501
|
+
lines.push(result.success
|
|
1502
|
+
? `{green-fg}AgentVibes for Copilot ${verb}!{/green-fg}`
|
|
1503
|
+
: `{red-fg}Installation failed:{/red-fg} ${result.error || 'Unknown error'}`);
|
|
1504
|
+
lines.push('');
|
|
1505
|
+
lines.push(`{bold}{cyan-fg}What got ${verb}:{/cyan-fg}{/bold}`);
|
|
1506
|
+
lines.push('');
|
|
1507
|
+
lines.push(' {yellow-fg}1.{/yellow-fg} {bold}.vscode/mcp.json{/bold}');
|
|
1508
|
+
lines.push(' {yellow-fg}2.{/yellow-fg} {bold}.github/copilot-instructions.md{/bold}');
|
|
1509
|
+
lines.push('');
|
|
1510
|
+
lines.push('{white-fg}Press {bold}Enter{/bold} or {bold}Escape{/bold} to return to the provider list.{/white-fg}');
|
|
1511
|
+
|
|
1512
|
+
infoBox.setContent(lines.join('\n'));
|
|
1513
|
+
infoBox.show();
|
|
1514
|
+
infoBox.setFront();
|
|
1515
|
+
infoBox.focus();
|
|
1516
|
+
infoBox.scrollTo(0);
|
|
1517
|
+
screen.render();
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
function showCodexInfo(result, wasInstalled = false) {
|
|
1521
|
+
providerView = 'info';
|
|
1522
|
+
hideAllProviderRows();
|
|
1523
|
+
contentBox.hide();
|
|
1524
|
+
|
|
1525
|
+
const verb = wasInstalled ? 'reinstalled' : 'installed';
|
|
1526
|
+
|
|
1527
|
+
const lines = [];
|
|
1528
|
+
lines.push('{bold}{cyan-fg}OpenAI Codex -- AgentVibes Integration{/cyan-fg}{/bold}');
|
|
1529
|
+
lines.push('');
|
|
1530
|
+
lines.push(result.success
|
|
1531
|
+
? `{green-fg}AgentVibes for Codex ${verb}!{/green-fg}`
|
|
1532
|
+
: `{red-fg}Installation failed:{/red-fg} ${result.error || 'Unknown error'}`);
|
|
1533
|
+
lines.push('');
|
|
1534
|
+
lines.push(`{bold}{cyan-fg}What got ${verb}:{/cyan-fg}{/bold}`);
|
|
1535
|
+
lines.push('');
|
|
1536
|
+
lines.push(' {yellow-fg}1.{/yellow-fg} {bold}.codex/config.toml{/bold}');
|
|
1537
|
+
lines.push(' {yellow-fg}2.{/yellow-fg} {bold}.vscode/mcp.json{/bold}');
|
|
1538
|
+
lines.push(' {yellow-fg}3.{/yellow-fg} {bold}AGENTS.md{/bold}');
|
|
1539
|
+
lines.push(' {yellow-fg}4.{/yellow-fg} {bold}.codex/hooks/{/bold}');
|
|
1540
|
+
lines.push('');
|
|
1541
|
+
lines.push('{white-fg}Press {bold}Enter{/bold} or {bold}Escape{/bold} to return to the provider list.{/white-fg}');
|
|
1542
|
+
|
|
1543
|
+
infoBox.setContent(lines.join('\n'));
|
|
1544
|
+
infoBox.show();
|
|
1545
|
+
infoBox.setFront();
|
|
1546
|
+
infoBox.focus();
|
|
1547
|
+
infoBox.scrollTo(0);
|
|
1548
|
+
screen.render();
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
function showRemoveInfo(providerId, removedItems) {
|
|
1552
|
+
providerView = 'info';
|
|
1553
|
+
hideAllProviderRows();
|
|
1554
|
+
contentBox.hide();
|
|
1555
|
+
|
|
1556
|
+
const lines = [];
|
|
1557
|
+
if (providerId === 'claude-code') {
|
|
1558
|
+
lines.push('{bold}{cyan-fg}Claude Code -- Uninstalled{/cyan-fg}{/bold}');
|
|
1559
|
+
lines.push('');
|
|
1560
|
+
lines.push('{green-fg}AgentVibes fully removed from this project!{/green-fg}');
|
|
1561
|
+
lines.push('');
|
|
1562
|
+
if (removedItems && removedItems.length > 0) {
|
|
1563
|
+
lines.push('{bold}{cyan-fg}Removed:{/cyan-fg}{/bold}');
|
|
1564
|
+
for (const item of removedItems) {
|
|
1565
|
+
lines.push(` {yellow-fg}•{/yellow-fg} ${item}`);
|
|
1566
|
+
}
|
|
1567
|
+
lines.push('');
|
|
1568
|
+
}
|
|
1569
|
+
lines.push('{white-fg}Re-install anytime with the Install button.{/white-fg}');
|
|
1570
|
+
} else if (providerId === 'github-copilot') {
|
|
1571
|
+
lines.push('{bold}{cyan-fg}GitHub Copilot -- Removed{/cyan-fg}{/bold}');
|
|
1572
|
+
lines.push('');
|
|
1573
|
+
lines.push('{green-fg}Successfully removed!{/green-fg}');
|
|
1574
|
+
} else if (providerId === 'openai-codex') {
|
|
1575
|
+
lines.push('{bold}{cyan-fg}OpenAI Codex -- Removed{/cyan-fg}{/bold}');
|
|
1576
|
+
lines.push('');
|
|
1577
|
+
lines.push('{green-fg}Successfully removed!{/green-fg}');
|
|
1578
|
+
}
|
|
1579
|
+
lines.push('');
|
|
1580
|
+
lines.push('{white-fg}Press {bold}Enter{/bold} or {bold}Escape{/bold} to return to the provider list.{/white-fg}');
|
|
1581
|
+
|
|
1582
|
+
infoBox.setContent(lines.join('\n'));
|
|
1583
|
+
infoBox.show();
|
|
1584
|
+
infoBox.setFront();
|
|
1585
|
+
infoBox.focus();
|
|
1586
|
+
infoBox.scrollTo(0);
|
|
1587
|
+
screen.render();
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
function showProviderListView() {
|
|
1591
|
+
providerView = 'list';
|
|
1592
|
+
infoBox.hide();
|
|
1593
|
+
contentBox.hide();
|
|
1594
|
+
showAllProviderRows();
|
|
1595
|
+
providerFocusIndex = 0;
|
|
1596
|
+
if (providerFocusableItems.length) providerFocusableItems[0].focus();
|
|
1597
|
+
screen.render();
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
infoBox.key(['escape', 'enter'], () => {
|
|
1601
|
+
showProviderListView();
|
|
1602
|
+
});
|
|
1603
|
+
|
|
1604
|
+
async function refreshInstalledState() {
|
|
1605
|
+
for (const p of PROVIDERS) {
|
|
1606
|
+
const checkFn = p.id === 'claude-code' ? checkClaudeInstalled
|
|
1607
|
+
: p.id === 'github-copilot' ? checkCopilotInstalled
|
|
1608
|
+
: checkCodexInstalled;
|
|
1609
|
+
installedState[p.id] = await checkFn(targetDir);
|
|
1610
|
+
}
|
|
1611
|
+
for (const row of providerRows) {
|
|
1612
|
+
const installed = installedState[row.id];
|
|
1613
|
+
row.statusText.setContent(
|
|
1614
|
+
installed
|
|
1615
|
+
? '{green-fg}[Installed]{/green-fg}'
|
|
1616
|
+
: '{yellow-fg}[Not Installed]{/yellow-fg}'
|
|
1617
|
+
);
|
|
1618
|
+
row.installBtn.setContent(installed ? ' Re-install ' : ' Install ');
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
// =========================================================================
|
|
1623
|
+
// Screen renderers
|
|
1624
|
+
// =========================================================================
|
|
1625
|
+
|
|
1626
|
+
const _HDR = (emoji, label) =>
|
|
1627
|
+
`{${COLORS.sectionHdr}-fg}${emoji} ${label} ${'--'.repeat(50)}{/${COLORS.sectionHdr}-fg}`;
|
|
1628
|
+
|
|
1629
|
+
function _renderScreen0() {
|
|
1630
|
+
const lines = [
|
|
1631
|
+
_HDR('', 'Language / Idioma / Langue / Sprache'),
|
|
1632
|
+
'',
|
|
1633
|
+
' Select your language:',
|
|
1634
|
+
'',
|
|
1635
|
+
...SUPPORTED_LANGUAGES.map((l, i) =>
|
|
1636
|
+
i === _langIdx
|
|
1637
|
+
? ` {green-fg}> ${l.name}{/green-fg}`
|
|
1638
|
+
: ` ${l.name}`
|
|
1639
|
+
),
|
|
1640
|
+
];
|
|
1641
|
+
contentBox.setContent(_c(lines));
|
|
1642
|
+
hintLine.setContent(' Screen 0: Language | [Up/Down] Select | [Enter] Apply & Continue | [->] Skip (English)');
|
|
1643
|
+
screen.render();
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
async function _renderScreen1() {
|
|
1647
|
+
const frames = ['|','/','-','\\'];
|
|
1648
|
+
let frameIdx = 0;
|
|
1649
|
+
_checking = true;
|
|
1650
|
+
_s1ContinueBtn.hide();
|
|
1651
|
+
|
|
1652
|
+
contentBox.setContent(_c([
|
|
1653
|
+
_HDR('', t(_getLang(), 'dependencyCheck')),
|
|
1654
|
+
'',
|
|
1655
|
+
` {white-fg}${frames[0]} ${t(_getLang(), 'checkingDependencies')}{/white-fg}`,
|
|
1656
|
+
]));
|
|
1657
|
+
hintLine.setContent(` ${t(_getLang(), 'screen2Hint')}`);
|
|
1658
|
+
screen.render();
|
|
1659
|
+
|
|
1660
|
+
const spinInterval = setInterval(() => {
|
|
1661
|
+
frameIdx = (frameIdx + 1) % frames.length;
|
|
1662
|
+
contentBox.setContent(_c([
|
|
1663
|
+
_HDR('', t(_getLang(), 'dependencyCheck')),
|
|
1664
|
+
'',
|
|
1665
|
+
` {white-fg}${frames[frameIdx]} ${t(_getLang(), 'checkingDependencies')}{/white-fg}`,
|
|
1666
|
+
]));
|
|
1667
|
+
screen.render();
|
|
1668
|
+
}, 100);
|
|
1669
|
+
|
|
1670
|
+
try {
|
|
1671
|
+
_deps = await _checkDependenciesAsync();
|
|
1672
|
+
} finally {
|
|
1673
|
+
clearInterval(spinInterval);
|
|
1674
|
+
_checking = false;
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
const ok = () => `{green-fg}OK ${t(_getLang(), 'installed')}{/green-fg}`;
|
|
1678
|
+
const bad = () => `{red-fg}X ${t(_getLang(), 'notFound')}{/red-fg}`;
|
|
1679
|
+
|
|
1680
|
+
const ttsOk = _deps.piper || _deps.soprano;
|
|
1681
|
+
contentBox.setContent(_c([
|
|
1682
|
+
_HDR('', t(_getLang(), 'dependencyCheck')),
|
|
1683
|
+
'',
|
|
1684
|
+
` {white-fg}${'Dependency'.padEnd(14)}${'Status'}{/white-fg}`,
|
|
1685
|
+
` {white-fg}${'---'.repeat(26)}{/white-fg}`,
|
|
1686
|
+
` {white-fg}${'Node.js'.padEnd(14)}{/white-fg}${_deps.node ? ok() : bad()}`,
|
|
1687
|
+
` {white-fg}${'npm'.padEnd(14)}{/white-fg}${_deps.npm ? ok() : bad()}`,
|
|
1688
|
+
` {white-fg}${'Piper TTS'.padEnd(14)}{/white-fg}${_deps.piper ? ok() : bad()}`,
|
|
1689
|
+
` {white-fg}${'Soprano TTS'.padEnd(14)}{/white-fg}${_deps.soprano ? ok() : bad()}`,
|
|
1690
|
+
` {white-fg}${'ffmpeg'.padEnd(14)}{/white-fg}${_deps.ffmpeg ? ok() : `{red-fg}! ${t(_getLang(), 'ffmpegMissing')}{/red-fg}`}`,
|
|
1691
|
+
'',
|
|
1692
|
+
ttsOk
|
|
1693
|
+
? ` {green-fg}OK ${t(_getLang(), 'ttsDetected')}{/green-fg}`
|
|
1694
|
+
: ` {red-fg}! ${t(_getLang(), 'noTtsFound')}{/red-fg}`,
|
|
1695
|
+
'',
|
|
1696
|
+
'',
|
|
1697
|
+
]));
|
|
1698
|
+
if (ttsOk) {
|
|
1699
|
+
_s1ContinueBtn.setContent(_tl('continueArrowBtn'));
|
|
1700
|
+
_s1ContinueBtn.show();
|
|
1701
|
+
_s1ContinueBtn.focus();
|
|
1702
|
+
}
|
|
1703
|
+
screen.render();
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
function _renderScreen2() {
|
|
1707
|
+
const lines = [
|
|
1708
|
+
_HDR('', 'TTS Engine Selection'),
|
|
1709
|
+
'',
|
|
1710
|
+
' {white-fg}Select which TTS engines to use with AgentVibes:{/white-fg}',
|
|
1711
|
+
];
|
|
1712
|
+
|
|
1713
|
+
contentBox.setContent(_c(lines));
|
|
1714
|
+
|
|
1715
|
+
_showTtsEngineRows();
|
|
1716
|
+
|
|
1717
|
+
// Position continue button below engine rows
|
|
1718
|
+
const btnY = 5 + (_ttsEngines.length * 3) + 1;
|
|
1719
|
+
_s2ContinueBtn.top = btnY;
|
|
1720
|
+
_s2ContinueBtn.left = 4;
|
|
1721
|
+
_s2ContinueBtn.show();
|
|
1722
|
+
|
|
1723
|
+
hintLine.setContent(' Screen 2: TTS Engines | [Tab] Install | [Enter/->] Continue | [Esc/<-] Back');
|
|
1724
|
+
|
|
1725
|
+
// Focus first visible install button or continue button
|
|
1726
|
+
const visibleBtns = _ttsFocusableItems.filter(b => !b.hidden);
|
|
1727
|
+
if (visibleBtns.length) {
|
|
1728
|
+
_ttsFocusIndex = 0;
|
|
1729
|
+
visibleBtns[0].focus();
|
|
1730
|
+
} else {
|
|
1731
|
+
_s2ContinueBtn.focus();
|
|
1732
|
+
}
|
|
1733
|
+
screen.render();
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
function _renderScreen3() {
|
|
1737
|
+
// Mark setup as completed — write to targetDir in case configService
|
|
1738
|
+
// has a different projectRoot (e.g. npm link resolves differently).
|
|
1739
|
+
// Each step is wrapped individually so a partial failure (e.g. corrupt
|
|
1740
|
+
// local config file) does not block the others — and errors are logged
|
|
1741
|
+
// to stderr so the user can see why setup keeps re-running.
|
|
1742
|
+
try { configService.set('setupCompleted', true); }
|
|
1743
|
+
catch (e) { console.error('setupCompleted (project): ' + e.message); }
|
|
1744
|
+
try { configService.setGlobal?.('setupCompleted', true); }
|
|
1745
|
+
catch (e) { console.error('setupCompleted (global): ' + e.message); }
|
|
1746
|
+
|
|
1747
|
+
try {
|
|
1748
|
+
const localCfgDir = path.join(targetDir, '.agentvibes');
|
|
1749
|
+
const localCfgPath = path.join(localCfgDir, 'config.json');
|
|
1750
|
+
if (!fs.existsSync(localCfgPath)) {
|
|
1751
|
+
fs.mkdirSync(localCfgDir, { recursive: true });
|
|
1752
|
+
fs.writeFileSync(localCfgPath, JSON.stringify({ setupCompleted: true }, null, 2));
|
|
1753
|
+
} else {
|
|
1754
|
+
let existing = {};
|
|
1755
|
+
try {
|
|
1756
|
+
existing = JSON.parse(fs.readFileSync(localCfgPath, 'utf8'));
|
|
1757
|
+
} catch (e) {
|
|
1758
|
+
// Corrupt JSON — back up the old file and start fresh so the user
|
|
1759
|
+
// doesn't get stuck in an endless setup loop.
|
|
1760
|
+
console.error(`setupCompleted: ${localCfgPath} is corrupt (${e.message}); rewriting`);
|
|
1761
|
+
try { fs.renameSync(localCfgPath, localCfgPath + '.bak'); } catch {}
|
|
1762
|
+
existing = {};
|
|
1763
|
+
}
|
|
1764
|
+
if (!existing.setupCompleted) {
|
|
1765
|
+
existing.setupCompleted = true;
|
|
1766
|
+
fs.writeFileSync(localCfgPath, JSON.stringify(existing, null, 2));
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
} catch (e) {
|
|
1770
|
+
console.error('setupCompleted (local file): ' + e.message);
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
// Show provider rows instead of contentBox
|
|
1774
|
+
contentBox.hide();
|
|
1775
|
+
hintLine.setContent(' Screen 3: LLM Providers | [Enter] Action | [Tab] Next button | [Esc] Tab bar');
|
|
1776
|
+
showAllProviderRows();
|
|
1777
|
+
refreshInstalledState().then(() => {
|
|
1778
|
+
if (providerFocusableItems.length) {
|
|
1779
|
+
providerFocusIndex = 0;
|
|
1780
|
+
providerFocusableItems[0].focus();
|
|
1781
|
+
}
|
|
1782
|
+
screen.render();
|
|
1783
|
+
});
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
function _showCurrentScreen() {
|
|
1787
|
+
// Hide Screen 1 continue button on other screens
|
|
1788
|
+
if (_screen !== 1) _s1ContinueBtn.hide();
|
|
1789
|
+
|
|
1790
|
+
// Hide Screen 2 TTS engine rows on other screens
|
|
1791
|
+
if (_screen !== 2) {
|
|
1792
|
+
_hideTtsEngineRows();
|
|
1793
|
+
_s2ContinueBtn.hide();
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
// Hide provider rows on non-provider screens
|
|
1797
|
+
if (_screen !== 3) {
|
|
1798
|
+
hideAllProviderRows();
|
|
1799
|
+
infoBox.hide();
|
|
1800
|
+
providerView = 'list';
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
// Show contentBox on screens 0-2
|
|
1804
|
+
if (_screen <= 2) {
|
|
1805
|
+
contentBox.show();
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
if (_screen !== _lastScreen) {
|
|
1809
|
+
// Nuclear clear
|
|
1810
|
+
try {
|
|
1811
|
+
for (let r = 0; r < screen.height; r++) {
|
|
1812
|
+
const orow = screen.olines?.[r];
|
|
1813
|
+
if (!orow) continue;
|
|
1814
|
+
for (let c = 0; c < screen.width; c++) {
|
|
1815
|
+
if (orow[c]) orow[c][0] = -1;
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
if (screen.lines?.[2]) screen.lines[2].dirty = true;
|
|
1819
|
+
} catch {}
|
|
1820
|
+
|
|
1821
|
+
const _clearLine = ' '.repeat(150);
|
|
1822
|
+
const _clearPage = Array(25).fill(_clearLine).join('\n');
|
|
1823
|
+
contentBox.setContent(_clearPage);
|
|
1824
|
+
hintLine.setContent(_clearLine);
|
|
1825
|
+
screen.render();
|
|
1826
|
+
|
|
1827
|
+
const targetScreen = _screen;
|
|
1828
|
+
_lastScreen = _screen;
|
|
1829
|
+
setTimeout(() => {
|
|
1830
|
+
if (_screen !== targetScreen) return;
|
|
1831
|
+
switch (_screen) {
|
|
1832
|
+
case -1: _renderScreenGlobal(); break;
|
|
1833
|
+
case 0: _renderScreen0(); break;
|
|
1834
|
+
case 1: _renderScreen1(); break;
|
|
1835
|
+
case 2: _renderScreen2(); break;
|
|
1836
|
+
case 3: _renderScreen3(); break;
|
|
1837
|
+
}
|
|
1838
|
+
}, 50);
|
|
1839
|
+
return;
|
|
1840
|
+
}
|
|
1841
|
+
switch (_screen) {
|
|
1842
|
+
case -1: _renderScreenGlobal(); break;
|
|
1843
|
+
case 0: _renderScreen0(); break;
|
|
1844
|
+
case 1: _renderScreen1(); break;
|
|
1845
|
+
case 2: _renderScreen2(); break;
|
|
1846
|
+
case 3: _renderScreen3(); break;
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
// =========================================================================
|
|
1851
|
+
// Navigation (key handlers)
|
|
1852
|
+
// =========================================================================
|
|
1853
|
+
|
|
1854
|
+
screen.key(['enter'], () => {
|
|
1855
|
+
if (box.hidden || _checking || navigationService?.isModalOpen()) return;
|
|
1856
|
+
if (_screen === -1) {
|
|
1857
|
+
// Global config choice screen
|
|
1858
|
+
if (_globalChoiceIdx === 0) {
|
|
1859
|
+
try { configService.saveAllToLocal(_pendingGlobalCfg); } catch {}
|
|
1860
|
+
_screen = 3;
|
|
1861
|
+
} else {
|
|
1862
|
+
_screen = 0;
|
|
1863
|
+
_langIdx = 0;
|
|
1864
|
+
}
|
|
1865
|
+
_pendingGlobalCfg = null;
|
|
1866
|
+
_showCurrentScreen();
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1869
|
+
if (_screen === 0) {
|
|
1870
|
+
if (languageService) languageService.setLang(SUPPORTED_LANGUAGES[_langIdx].value);
|
|
1871
|
+
_screen = 1;
|
|
1872
|
+
_showCurrentScreen();
|
|
1873
|
+
return;
|
|
1874
|
+
}
|
|
1875
|
+
if (_screen === 1) return; // Enter handled by Continue button
|
|
1876
|
+
if (_screen === 2) return; // Enter handled by Continue button and install buttons
|
|
1877
|
+
if (_screen === 3) return; // Enter handled by provider buttons
|
|
1878
|
+
});
|
|
1879
|
+
|
|
1880
|
+
screen.key(['escape'], () => {
|
|
1881
|
+
if (box.hidden || _checking || navigationService?.isModalOpen()) return;
|
|
1882
|
+
if (_screen === -1) {
|
|
1883
|
+
setTimeout(() => navigationService?.switchTab('settings'), 0);
|
|
1884
|
+
return;
|
|
1885
|
+
}
|
|
1886
|
+
if (_screen === 3 && providerView === 'info') {
|
|
1887
|
+
showProviderListView();
|
|
1888
|
+
return;
|
|
1889
|
+
}
|
|
1890
|
+
if (_screen > 0) {
|
|
1891
|
+
_screen--;
|
|
1892
|
+
_showCurrentScreen();
|
|
1893
|
+
} else {
|
|
1894
|
+
setTimeout(() => navigationService?.switchTab('settings'), 0);
|
|
1895
|
+
}
|
|
1896
|
+
});
|
|
1897
|
+
|
|
1898
|
+
screen.key(['up'], () => {
|
|
1899
|
+
if (box.hidden || navigationService?.isModalOpen()) return;
|
|
1900
|
+
if (_screen === -1) {
|
|
1901
|
+
_globalChoiceIdx = 0;
|
|
1902
|
+
_renderScreenGlobal();
|
|
1903
|
+
return;
|
|
1904
|
+
}
|
|
1905
|
+
if (_screen === 0) {
|
|
1906
|
+
_langIdx = Math.max(0, _langIdx - 1);
|
|
1907
|
+
_renderScreen0();
|
|
1908
|
+
return;
|
|
1909
|
+
}
|
|
1910
|
+
});
|
|
1911
|
+
|
|
1912
|
+
screen.key(['left'], () => {
|
|
1913
|
+
if (box.hidden || _checking || navigationService?.isModalOpen()) return;
|
|
1914
|
+
if (_screen === -1) return;
|
|
1915
|
+
if (_screen === 3) return; // Left handled by button nav
|
|
1916
|
+
if (_screen > 0) {
|
|
1917
|
+
_screen--;
|
|
1918
|
+
_showCurrentScreen();
|
|
1919
|
+
}
|
|
1920
|
+
});
|
|
1921
|
+
|
|
1922
|
+
screen.key(['right'], () => {
|
|
1923
|
+
if (box.hidden || _checking || navigationService?.isModalOpen()) return;
|
|
1924
|
+
if (_screen === -1) return;
|
|
1925
|
+
if (_screen === 0) {
|
|
1926
|
+
if (languageService) languageService.setLang(SUPPORTED_LANGUAGES[_langIdx].value);
|
|
1927
|
+
_screen = 1;
|
|
1928
|
+
_showCurrentScreen();
|
|
1929
|
+
return;
|
|
1930
|
+
}
|
|
1931
|
+
if (_screen === 1) return; // Right handled by Continue button
|
|
1932
|
+
if (_screen === 2) { if (_screen < 3) { _screen++; _showCurrentScreen(); } return; }
|
|
1933
|
+
if (_screen === 3) return; // Right handled by button nav
|
|
1934
|
+
});
|
|
1935
|
+
|
|
1936
|
+
screen.key(['down'], () => {
|
|
1937
|
+
if (box.hidden || navigationService?.isModalOpen()) return;
|
|
1938
|
+
if (_screen === -1) {
|
|
1939
|
+
_globalChoiceIdx = 1;
|
|
1940
|
+
_renderScreenGlobal();
|
|
1941
|
+
return;
|
|
1942
|
+
}
|
|
1943
|
+
if (_screen === 0) {
|
|
1944
|
+
_langIdx = Math.min(SUPPORTED_LANGUAGES.length - 1, _langIdx + 1);
|
|
1945
|
+
_renderScreen0();
|
|
1946
|
+
return;
|
|
1947
|
+
}
|
|
1948
|
+
});
|
|
1949
|
+
|
|
1950
|
+
// =========================================================================
|
|
1951
|
+
// Screen -1: Global Config Detection (pre-wizard)
|
|
1952
|
+
// =========================================================================
|
|
1953
|
+
|
|
1954
|
+
function _renderScreenGlobal() {
|
|
1955
|
+
const cfg = _pendingGlobalCfg || {};
|
|
1956
|
+
const cfgPath = configService?.getGlobalConfigPath?.() || '~/.agentvibes/config.json';
|
|
1957
|
+
|
|
1958
|
+
// Build settings preview
|
|
1959
|
+
const voice = cfg.voice || '(default)';
|
|
1960
|
+
const lang = cfg.language || 'en';
|
|
1961
|
+
const ttsEngine = cfg.ttsEngine || '(auto)';
|
|
1962
|
+
const verbosity = cfg.verbosity || 'high';
|
|
1963
|
+
const personality = cfg.personality || 'none';
|
|
1964
|
+
|
|
1965
|
+
const sel0 = _globalChoiceIdx === 0;
|
|
1966
|
+
const sel1 = _globalChoiceIdx === 1;
|
|
1967
|
+
const hi = '{magenta-bg}{white-fg}';
|
|
1968
|
+
const lo = '{/white-fg}{/magenta-bg}';
|
|
1969
|
+
|
|
1970
|
+
const lines = [
|
|
1971
|
+
_HDR('', 'Global Settings Found'),
|
|
1972
|
+
'',
|
|
1973
|
+
` {white-fg}Location:{/white-fg} {yellow-fg}${cfgPath}{/yellow-fg}`,
|
|
1974
|
+
'',
|
|
1975
|
+
` {cyan-fg}Voice:{/cyan-fg} {yellow-fg}${voice}{/yellow-fg}`,
|
|
1976
|
+
` {cyan-fg}Language:{/cyan-fg} {yellow-fg}${lang}{/yellow-fg}`,
|
|
1977
|
+
` {cyan-fg}TTS Engine:{/cyan-fg} {yellow-fg}${ttsEngine}{/yellow-fg}`,
|
|
1978
|
+
` {cyan-fg}Verbosity:{/cyan-fg} {yellow-fg}${verbosity}{/yellow-fg}`,
|
|
1979
|
+
` {cyan-fg}Personality:{/cyan-fg}{yellow-fg} ${personality}{/yellow-fg}`,
|
|
1980
|
+
'',
|
|
1981
|
+
' {white-fg}What would you like to do for this project?{/white-fg}',
|
|
1982
|
+
'',
|
|
1983
|
+
` ${sel0 ? hi : ''}> Load Global Settings${sel0 ? lo : ''} {white-fg}— use these settings for this project{/white-fg}`,
|
|
1984
|
+
` ${sel1 ? hi : ''}> Start Fresh${sel1 ? lo : ''} {white-fg}— run the full setup wizard from scratch{/white-fg}`,
|
|
1985
|
+
'',
|
|
1986
|
+
];
|
|
1987
|
+
contentBox.setContent(_c(lines));
|
|
1988
|
+
hintLine.setContent(' [Up/Down] Choose | [Enter] Select');
|
|
1989
|
+
box.focus();
|
|
1990
|
+
screen.render();
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
// =========================================================================
|
|
1994
|
+
// Tab Component Contract
|
|
1995
|
+
// =========================================================================
|
|
1996
|
+
|
|
1997
|
+
return {
|
|
1998
|
+
box,
|
|
1999
|
+
|
|
2000
|
+
show() {
|
|
2001
|
+
_lastScreen = -1;
|
|
2002
|
+
providerView = 'list';
|
|
2003
|
+
box.show();
|
|
2004
|
+
|
|
2005
|
+
// Detect if AgentVibes is already installed in the target directory
|
|
2006
|
+
// (e.g. user ran install, closed TUI, came back)
|
|
2007
|
+
const alreadyInstalled = fs.existsSync(path.join(targetDir, '.claude', 'commands', 'agent-vibes'));
|
|
2008
|
+
|
|
2009
|
+
// Check: no local config but global exists with setupCompleted
|
|
2010
|
+
const hasLocal = configService?.hasLocalConfig?.();
|
|
2011
|
+
const globalCfg = configService?.getGlobalConfig?.() ?? {};
|
|
2012
|
+
if (!alreadyInstalled && !hasLocal && globalCfg.setupCompleted) {
|
|
2013
|
+
_pendingGlobalCfg = globalCfg;
|
|
2014
|
+
_screen = -1;
|
|
2015
|
+
_showCurrentScreen();
|
|
2016
|
+
screen.render();
|
|
2017
|
+
return;
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
// If already installed or not first run, skip directly to Screen 3 (providers)
|
|
2021
|
+
if (alreadyInstalled || !_isFirstRun()) {
|
|
2022
|
+
_screen = 3;
|
|
2023
|
+
} else {
|
|
2024
|
+
_screen = 0;
|
|
2025
|
+
_langIdx = 0;
|
|
2026
|
+
}
|
|
2027
|
+
_showCurrentScreen();
|
|
2028
|
+
screen.render();
|
|
2029
|
+
},
|
|
2030
|
+
|
|
2031
|
+
hide() {
|
|
2032
|
+
box.hide();
|
|
2033
|
+
hideAllProviderRows();
|
|
2034
|
+
infoBox.hide();
|
|
2035
|
+
providerView = 'list';
|
|
2036
|
+
screen.render();
|
|
2037
|
+
},
|
|
2038
|
+
|
|
2039
|
+
onFocus() {
|
|
2040
|
+
if (_screen === 0) {
|
|
2041
|
+
box.focus();
|
|
2042
|
+
} else if (_screen === 3) {
|
|
2043
|
+
if (providerView === 'list') {
|
|
2044
|
+
providerFocusIndex = 0;
|
|
2045
|
+
if (providerFocusableItems.length) providerFocusableItems[0].focus();
|
|
2046
|
+
} else {
|
|
2047
|
+
infoBox.focus();
|
|
2048
|
+
}
|
|
2049
|
+
} else {
|
|
2050
|
+
box.focus();
|
|
2051
|
+
}
|
|
2052
|
+
screen.render();
|
|
2053
|
+
},
|
|
2054
|
+
|
|
2055
|
+
onBlur() {},
|
|
2056
|
+
|
|
2057
|
+
getFooterText() {
|
|
2058
|
+
if (_screen === 3) {
|
|
2059
|
+
if (providerView === 'info') {
|
|
2060
|
+
return '[Esc] Back to list [Up/Down] Scroll';
|
|
2061
|
+
}
|
|
2062
|
+
return '[Enter] Action [Tab] Next button [Esc] Tab bar';
|
|
2063
|
+
}
|
|
2064
|
+
return _tl('footerText');
|
|
2065
|
+
},
|
|
2066
|
+
|
|
2067
|
+
getFooterColor() {
|
|
2068
|
+
return COLORS.footerBg;
|
|
2069
|
+
},
|
|
2070
|
+
};
|
|
2071
|
+
}
|