agentvibes 5.13.1 → 5.14.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/.claude/audio/ui/CREDITS.txt +16 -16
- package/.claude/commands/agent-vibes-bmad-voices.md +117 -117
- package/.claude/commands/agent-vibes-rdp.md +24 -24
- package/.claude/config/audio-effects.cfg.sample +52 -52
- package/.claude/docs/TERMUX_SETUP.md +408 -408
- package/.claude/github-star-reminder.txt +1 -1
- package/.claude/hooks/kokoro-server.py +219 -219
- package/.claude/hooks/kokoro-tts.py +141 -141
- package/.claude/hooks/party-stage-roster.py +328 -328
- package/.claude/hooks/play-tts-kokoro.sh +7 -2
- package/.claude/hooks/play-tts-ssh-remote.sh +38 -3
- package/.claude/hooks/requirements.txt +6 -6
- package/.claude/hooks/soprano-gradio-synth.py +139 -139
- package/.claude/hooks/translator.py +237 -237
- package/.claude/hooks-windows/kokoro-server.py +219 -219
- package/.claude/hooks-windows/kokoro-tts.py +107 -107
- package/.claude/hooks-windows/play-tts.ps1 +17 -0
- package/.claude/hooks-windows/soprano-gradio-synth.py +153 -153
- package/.claude/verbosity.txt +1 -1
- package/.clawdbot/README.md +105 -105
- package/CLAUDE.md +84 -176
- package/README.md +27 -2
- package/RELEASE_NOTES.md +68 -0
- package/WINDOWS-SETUP.md +208 -208
- package/bin/agent-vibes +39 -39
- package/bin/mcp-server.js +121 -121
- package/bin/test-bmad-pr +78 -78
- package/mcp-server/QUICK_START.md +203 -203
- package/mcp-server/README.md +345 -345
- package/mcp-server/examples/claude_desktop_config.json +11 -11
- package/mcp-server/examples/claude_desktop_config_piper.json +9 -9
- package/mcp-server/examples/custom_instructions.md +169 -169
- package/mcp-server/install-deps.js +12 -2
- package/mcp-server/server.py +2111 -2085
- package/mcp-server/test_mcp_correctness.py +504 -504
- package/mcp-server/test_windows_script_parity.py +339 -339
- package/package.json +6 -3
- package/setup-ssh-receiver.ps1 +259 -0
- package/setup-windows.ps1 +39 -29
- package/src/cli/list-personalities.js +110 -110
- package/src/commands/bmad-voices.js +394 -394
- package/src/console/brand-colors.js +13 -13
- package/src/console/constants/personalities.js +44 -44
- package/src/console/tabs/agents-tab.js +14 -4
- package/src/console/tabs/help-tab.js +314 -314
- package/src/console/tabs/readme-tab.js +272 -272
- package/src/console/tabs/settings-tab.js +31 -6
- package/src/console/tabs/setup-tab.js +154 -32
- package/src/console/tabs/voices-tab.js +48 -6
- package/src/console/widgets/destroy-list.js +25 -25
- package/src/console/widgets/notice.js +55 -55
- package/src/installer/language-screen.js +31 -31
- package/src/installer/music-file-input.js +304 -304
- package/src/installer.js +91 -35
- package/src/services/language-service.js +47 -47
- package/src/services/llm-provider-service.js +4 -1
- package/src/services/provider-voice-catalog.js +9 -5
- package/src/utils/audio-format-validator.js +277 -277
- package/src/utils/dependency-checker.js +469 -469
- package/src/utils/file-ownership-verifier.js +358 -358
- package/src/utils/music-file-validator.js +285 -285
- package/src/utils/preview-list-prompt.js +144 -144
- package/src/utils/secure-music-storage.js +412 -412
- package/templates/agentvibes-receiver.ps1 +311 -0
- package/templates/agentvibes-receiver.sh +297 -297
- package/voice-assignments.json +8244 -8244
- package/.agentvibes/config.json +0 -17
- package/.agentvibes/install-manifest.json +0 -362
- package/.claude/config/audio-effects.cfg +0 -7
- package/.claude/config/audio-effects.cfg.bak-kokoro +0 -7
- package/.claude/config/background-music-enabled.txt +0 -1
- package/.claude/config/background-music-position.txt +0 -27
- package/.claude/config/background-music-volume.txt +0 -1
- package/.claude/config/background-music.cfg +0 -1
- package/.claude/config/background-music.txt +0 -1
- package/.claude/config/language.txt +0 -1
- package/.claude/config/personality.txt +0 -1
- package/.claude/config/reverb-level.txt +0 -1
- package/.claude/config/tts-speech-rate.txt +0 -1
- package/.claude/config/tts-verbosity.txt +0 -1
- package/.claude/hooks-windows/audio-cache-utils.ps1.user.bak +0 -119
- package/.claude/hooks-windows/soprano-gradio-synth.py.user.bak +0 -153
|
@@ -20,7 +20,7 @@ import { promisify } from 'node:util';
|
|
|
20
20
|
import fs from 'node:fs';
|
|
21
21
|
import { promises as _fsP } from 'node:fs';
|
|
22
22
|
import { SUPPORTED_LANGUAGES, t } from '../../i18n/strings.js';
|
|
23
|
-
import { ELEVENLABS_VOICES, KOKORO_VOICE_IDS } from '../../services/provider-voice-catalog.js';
|
|
23
|
+
import { ELEVENLABS_VOICES, KOKORO_VOICE_IDS, voicesForProvider } from '../../services/provider-voice-catalog.js';
|
|
24
24
|
import {
|
|
25
25
|
PROVIDERS,
|
|
26
26
|
checkClaudeInstalled, checkCopilotInstalled, checkCodexInstalled,
|
|
@@ -36,14 +36,14 @@ import {
|
|
|
36
36
|
loadLlmConfigSync, saveLlmConfigSync, resolveCfgPath,
|
|
37
37
|
} from '../../services/llm-provider-service.js';
|
|
38
38
|
import {
|
|
39
|
-
getAvailableEngines,
|
|
39
|
+
getAvailableEngines, getAllEngines, checkEngineInstalled, receiverProviderId,
|
|
40
40
|
} from '../../services/tts-engine-service.js';
|
|
41
41
|
import { openReverbPicker, formatEffectLabel } from '../widgets/reverb-picker.js';
|
|
42
42
|
import { openTrackPicker, openVolumeInput } from '../widgets/track-picker.js';
|
|
43
43
|
import { renderHelpBar, selectorTitle } from '../widgets/help-bar.js';
|
|
44
44
|
import { formatTrackName } from '../widgets/format-utils.js';
|
|
45
45
|
import { destroyList } from '../widgets/destroy-list.js';
|
|
46
|
-
import { scanInstalledVoices, getVoiceMeta, genderIconTag, formatVoiceRow, voiceRowHeader, PIPER_VOICES_DIR, SAMPLE_PHRASES, parseMultiSpeaker, getFavorites, getThumbsDown, toggleFavorite, toggleThumbsUp, toggleThumbsDown } from './voices-tab.js';
|
|
46
|
+
import { scanInstalledVoices, getVoiceMeta, previewPhrase, genderIconTag, formatVoiceRow, voiceRowHeader, PIPER_VOICES_DIR, SAMPLE_PHRASES, parseMultiSpeaker, getFavorites, getThumbsDown, toggleFavorite, toggleThumbsUp, toggleThumbsDown } from './voices-tab.js';
|
|
47
47
|
import { attachBtnBlink } from './agents-tab.js';
|
|
48
48
|
import { buildAudioEnv, detectWavPlayer } from '../audio-env.js';
|
|
49
49
|
import { buildBlingCommand, playBlingCue } from '../bling.js';
|
|
@@ -175,6 +175,20 @@ const NATIVE_ENGINE_VOICES = {
|
|
|
175
175
|
elevenlabs: { id: 'EXAVITQu4vr4xnSDxMaL', label: 'ElevenLabs' },
|
|
176
176
|
};
|
|
177
177
|
|
|
178
|
+
// SAPI and macOS Say are NATIVE but MULTI-voice (many built-in system voices),
|
|
179
|
+
// unlike soprano (a genuine single-voice engine). They get the full voice picker
|
|
180
|
+
// and a real default voice, not the engine-id placeholder.
|
|
181
|
+
const MULTI_VOICE_NATIVE = new Set(['sapi', 'macos-say']);
|
|
182
|
+
|
|
183
|
+
/** Default voice to assign when an engine is first selected in a per-LLM draft. */
|
|
184
|
+
function defaultVoiceForEngine(engine) {
|
|
185
|
+
if (MULTI_VOICE_NATIVE.has(engine)) {
|
|
186
|
+
const list = voicesForProvider(engine, { scanInstalledVoices, getVoiceMeta });
|
|
187
|
+
return list[0]?.id || NATIVE_ENGINE_VOICES[engine]?.id || '';
|
|
188
|
+
}
|
|
189
|
+
return NATIVE_ENGINE_VOICES[engine]?.id || '';
|
|
190
|
+
}
|
|
191
|
+
|
|
178
192
|
// Static built-in ElevenLabs premade voices — universal to every ElevenLabs
|
|
179
193
|
// account. The stored config value is the raw ElevenLabs voice_id; the TTS hook
|
|
180
194
|
// accepts raw IDs directly (its ^[a-zA-Z0-9]{10,40}$ path), so there is no
|
|
@@ -810,7 +824,6 @@ export function createSetupTab(screen, services) {
|
|
|
810
824
|
const providerStatusTexts = [];
|
|
811
825
|
|
|
812
826
|
// Transport provider row widgets (Configure-only, no Install/Remove)
|
|
813
|
-
const transportRows = [];
|
|
814
827
|
|
|
815
828
|
// Info box for provider details
|
|
816
829
|
const infoBox = blessed.box({
|
|
@@ -1229,8 +1242,14 @@ export function createSetupTab(screen, services) {
|
|
|
1229
1242
|
style: { bg: COLORS.contentBg },
|
|
1230
1243
|
});
|
|
1231
1244
|
|
|
1245
|
+
// _bgRestoreFn is modal-scoped so _killPreview can restore bg music synchronously,
|
|
1246
|
+
// eliminating the race condition when Preview is clicked twice rapidly.
|
|
1232
1247
|
let _previewModalProc = null;
|
|
1248
|
+
let _bgRestoreFn = null;
|
|
1233
1249
|
function _killPreview() {
|
|
1250
|
+
// Restore bg music immediately (synchronously) before killing the process —
|
|
1251
|
+
// otherwise a second Preview reads bgWas=true before the first's exit fires.
|
|
1252
|
+
if (_bgRestoreFn) { _bgRestoreFn(); _bgRestoreFn = null; }
|
|
1234
1253
|
if (_previewModalProc) { try { _previewModalProc.kill(); } catch {} _previewModalProc = null; }
|
|
1235
1254
|
}
|
|
1236
1255
|
|
|
@@ -1266,6 +1285,24 @@ export function createSetupTab(screen, services) {
|
|
|
1266
1285
|
const hooksSubdir = process.platform === 'win32' ? 'hooks-windows' : 'hooks';
|
|
1267
1286
|
const isWin = process.platform === 'win32' && !process.env.WSL_DISTRO_NAME;
|
|
1268
1287
|
const sampleText = 'This is how your Hermes audio settings sound right now.';
|
|
1288
|
+
|
|
1289
|
+
// Temporarily enable background music for preview if a track is configured.
|
|
1290
|
+
// Preview must play everything combined — voice, effects AND music. Write to
|
|
1291
|
+
// targetDir (project): audio-processor.sh checks CLAUDE_PROJECT_DIR first
|
|
1292
|
+
// (set to targetDir in the subprocess env below).
|
|
1293
|
+
if (!!draft.bgTrack) {
|
|
1294
|
+
const bgEnabledFile = path.join(targetDir, '.claude', 'config', 'background-music-enabled.txt');
|
|
1295
|
+
let bgWas = false;
|
|
1296
|
+
try { bgWas = fs.readFileSync(bgEnabledFile, 'utf8').trim() === 'true'; } catch {}
|
|
1297
|
+
if (!bgWas) {
|
|
1298
|
+
try {
|
|
1299
|
+
fs.mkdirSync(path.dirname(bgEnabledFile), { recursive: true });
|
|
1300
|
+
fs.writeFileSync(bgEnabledFile, 'true', 'utf8');
|
|
1301
|
+
} catch {}
|
|
1302
|
+
_bgRestoreFn = () => { try { fs.writeFileSync(bgEnabledFile, 'false', 'utf8'); } catch {} };
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1269
1306
|
let cmd, args;
|
|
1270
1307
|
if (draft.ttsEngine === 'elevenlabs') {
|
|
1271
1308
|
// ElevenLabs has no Windows provider; route through the bash orchestrator
|
|
@@ -1288,12 +1325,19 @@ export function createSetupTab(screen, services) {
|
|
|
1288
1325
|
stdio: 'ignore', windowsHide: true,
|
|
1289
1326
|
env: {
|
|
1290
1327
|
...process.env, CLAUDE_PROJECT_DIR: targetDir,
|
|
1328
|
+
// audition = keep the draft's EXACT voice (F1). Force the receiver engine
|
|
1329
|
+
// ONLY when this row has an explicit engine — an empty engine means
|
|
1330
|
+
// "use the global/voice-derived engine", and forcing piper here would
|
|
1331
|
+
// shadow a kokoro/SAPI row voice on the receiver (Fable review).
|
|
1332
|
+
AGENTVIBES_VOICE_SOURCE: 'audition',
|
|
1333
|
+
...(draft.ttsEngine ? { AGENTVIBES_RECEIVER_PROVIDER_OVERRIDE: receiverProviderId(draft.ttsEngine) } : {}),
|
|
1291
1334
|
...(draft.ttsEngine === 'elevenlabs' ? { AGENTVIBES_FORCE_PROVIDER: 'elevenlabs' } : {}),
|
|
1292
1335
|
},
|
|
1293
1336
|
});
|
|
1294
1337
|
_previewModalProc = proc;
|
|
1295
1338
|
proc.on('exit', (code) => {
|
|
1296
1339
|
_previewModalProc = null;
|
|
1340
|
+
if (_bgRestoreFn) { _bgRestoreFn(); _bgRestoreFn = null; }
|
|
1297
1341
|
if (!_closed) {
|
|
1298
1342
|
if (code !== 0 && code !== null) {
|
|
1299
1343
|
const engineLabel = NATIVE_ENGINE_VOICES[draft.ttsEngine]?.label || draft.ttsEngine || 'engine';
|
|
@@ -1305,7 +1349,11 @@ export function createSetupTab(screen, services) {
|
|
|
1305
1349
|
}
|
|
1306
1350
|
}
|
|
1307
1351
|
});
|
|
1308
|
-
proc.on('error', () => {
|
|
1352
|
+
proc.on('error', () => {
|
|
1353
|
+
_previewModalProc = null;
|
|
1354
|
+
if (_bgRestoreFn) { _bgRestoreFn(); _bgRestoreFn = null; }
|
|
1355
|
+
if (!_closed) { previewLine.setContent('{red-fg}Preview failed{/red-fg}'); screen.render(); }
|
|
1356
|
+
});
|
|
1309
1357
|
}
|
|
1310
1358
|
|
|
1311
1359
|
const previewBtn = _modalBtn('Preview', 4, _playPreview);
|
|
@@ -1889,14 +1937,9 @@ export function createSetupTab(screen, services) {
|
|
|
1889
1937
|
}
|
|
1890
1938
|
|
|
1891
1939
|
function _autoSave(silent) {
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
if (row && draft.host) {
|
|
1896
|
-
row.statusText.setContent(`{#9e9e9e-fg}→ ${draft.host}:${draft.port}{/#9e9e9e-fg}`);
|
|
1897
|
-
screen.render();
|
|
1898
|
-
}
|
|
1899
|
-
}).catch(() => {});
|
|
1940
|
+
// (Removed a dead status-text refresh keyed off `transportRows`, which was
|
|
1941
|
+
// always empty — Sonar S2583. The config still saves below.)
|
|
1942
|
+
saveTransportConfig(provider.id, draft).catch(() => {});
|
|
1900
1943
|
if (!silent) _showSavedToast(`${provider.name} Config`, `~/.agentvibes/transport-config.json`);
|
|
1901
1944
|
}
|
|
1902
1945
|
|
|
@@ -2225,6 +2268,12 @@ export function createSetupTab(screen, services) {
|
|
|
2225
2268
|
windowsHide: true,
|
|
2226
2269
|
env: {
|
|
2227
2270
|
...process.env, CLAUDE_PROJECT_DIR: targetDir, AGENTVIBES_LLM_KEY: `llm:${llmKey}`,
|
|
2271
|
+
// audition = keep the draft's EXACT voice (F1). Force the receiver
|
|
2272
|
+
// engine ONLY when this row has an explicit engine (empty = use the
|
|
2273
|
+
// global/voice-derived engine; forcing piper would shadow a kokoro/
|
|
2274
|
+
// SAPI row voice on the receiver — Fable review).
|
|
2275
|
+
AGENTVIBES_VOICE_SOURCE: 'audition',
|
|
2276
|
+
...(draft.ttsEngine ? { AGENTVIBES_RECEIVER_PROVIDER_OVERRIDE: receiverProviderId(draft.ttsEngine) } : {}),
|
|
2228
2277
|
...(draft.ttsEngine === 'elevenlabs' ? { AGENTVIBES_FORCE_PROVIDER: 'elevenlabs' } : {}),
|
|
2229
2278
|
},
|
|
2230
2279
|
});
|
|
@@ -2478,8 +2527,24 @@ export function createSetupTab(screen, services) {
|
|
|
2478
2527
|
}
|
|
2479
2528
|
navigationService?.openModal(null, _closePicker);
|
|
2480
2529
|
|
|
2481
|
-
|
|
2530
|
+
// Show ALL engines (like the Settings → Default TTS Engine picker), not just
|
|
2531
|
+
// the platform-native ones — otherwise Windows SAPI / macOS Say vanish on a
|
|
2532
|
+
// Linux/other host even though a remote receiver could synthesize them. When
|
|
2533
|
+
// this LLM (or the global default) is in remote mode the receiver renders the
|
|
2534
|
+
// audio, so off-platform engines are valid, selectable choices tagged
|
|
2535
|
+
// "(Receiver)"; locally they stay greyed "(Not supported)".
|
|
2536
|
+
const _isRemote = draft.mode === 'remote'
|
|
2537
|
+
|| (configService?.getConfig?.()?.audio_destination ?? 'local') === 'remote';
|
|
2538
|
+
const engines = getAllEngines().map(e => ({
|
|
2539
|
+
...e,
|
|
2540
|
+
installed: e.supported ? checkEngineInstalled(e.id) : false,
|
|
2541
|
+
}));
|
|
2482
2542
|
const items = engines.map(e => {
|
|
2543
|
+
if (!e.supported) {
|
|
2544
|
+
return _isRemote
|
|
2545
|
+
? `{cyan-fg} ${e.name.padEnd(20)} (Receiver){/cyan-fg} ${e.desc}`
|
|
2546
|
+
: `{#607d8b-fg} ${e.name.padEnd(20)} (Not supported){/#607d8b-fg}`;
|
|
2547
|
+
}
|
|
2483
2548
|
const status = e.installed ? '{green-fg}[OK]{/green-fg}' : '{yellow-fg}[Not Found]{/yellow-fg}';
|
|
2484
2549
|
return ` ${e.name.padEnd(20)} ${status} ${e.desc}`;
|
|
2485
2550
|
});
|
|
@@ -2533,18 +2598,22 @@ export function createSetupTab(screen, services) {
|
|
|
2533
2598
|
const engine = idx > 0 ? engines[idx - 1] : null;
|
|
2534
2599
|
if (idx > 0 && !engine) { _closePicker(); return; }
|
|
2535
2600
|
const selectedEngine = engine ? engine.id : '';
|
|
2536
|
-
// Guard
|
|
2601
|
+
// Guard selection. In remote mode the receiver synthesizes, so any engine
|
|
2602
|
+
// (incl. off-platform SAPI/macOS) is allowed. Locally, block engines that
|
|
2603
|
+
// can't run here: off-platform ones, and optional engines not installed.
|
|
2537
2604
|
if (selectedEngine) {
|
|
2538
2605
|
const engineStatus = engines.find(e => e.id === selectedEngine);
|
|
2539
|
-
|
|
2540
|
-
box.setLabel(` {red-fg} ${
|
|
2606
|
+
const _flash = (msg) => {
|
|
2607
|
+
box.setLabel(` {red-fg} ${msg} {/red-fg} `);
|
|
2541
2608
|
screen.render();
|
|
2542
|
-
setTimeout(() => {
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2609
|
+
setTimeout(() => { if (!box.destroyed) { box.setLabel(ENGINE_TITLE); screen.render(); } }, 3000);
|
|
2610
|
+
};
|
|
2611
|
+
if (engineStatus && !_isRemote && !engineStatus.supported) {
|
|
2612
|
+
_flash(`${engineStatus.name} runs on the receiver — switch this LLM to a remote receiver to use it`);
|
|
2613
|
+
return;
|
|
2614
|
+
}
|
|
2615
|
+
if (engineStatus && !_isRemote && engineStatus.supported && !engineStatus.installed && !engineStatus.native) {
|
|
2616
|
+
_flash(`${engineStatus.name} is not installed — go to Setup > TTS Engines to install`);
|
|
2548
2617
|
return;
|
|
2549
2618
|
}
|
|
2550
2619
|
}
|
|
@@ -2558,8 +2627,12 @@ export function createSetupTab(screen, services) {
|
|
|
2558
2627
|
const looksLikeElId = /^[A-Za-z0-9]{10,40}$/.test(draft.voice || '');
|
|
2559
2628
|
draft.voice = (elevenLabsVoiceName(draft.voice) || looksLikeElId)
|
|
2560
2629
|
? draft.voice : ELEVENLABS_DEFAULT_VOICE_ID;
|
|
2630
|
+
} else if (MULTI_VOICE_NATIVE.has(selectedEngine)
|
|
2631
|
+
&& voicesForProvider(selectedEngine, { scanInstalledVoices, getVoiceMeta }).some(v => v.id === draft.voice)) {
|
|
2632
|
+
// Re-selecting the SAME multi-voice native engine must NOT discard an
|
|
2633
|
+
// already-valid chosen voice (e.g. Zira -> David) — keep it (Fable review).
|
|
2561
2634
|
} else {
|
|
2562
|
-
draft.voice =
|
|
2635
|
+
draft.voice = defaultVoiceForEngine(selectedEngine);
|
|
2563
2636
|
}
|
|
2564
2637
|
_closePicker();
|
|
2565
2638
|
});
|
|
@@ -3196,7 +3269,7 @@ export function createSetupTab(screen, services) {
|
|
|
3196
3269
|
? '안녕하세요, 코코로 한국어 음성 미리보기입니다.'
|
|
3197
3270
|
: _pfx2 === 'jf' || _pfx2 === 'jm'
|
|
3198
3271
|
? 'こんにちは、これはKokoroの日本語音声プレビューです。'
|
|
3199
|
-
:
|
|
3272
|
+
: previewPhrase(voiceId);
|
|
3200
3273
|
|
|
3201
3274
|
// Bling now — a real preview is committed (past the install-prompt and
|
|
3202
3275
|
// toggle-off early returns). Fires for both local and remote paths.
|
|
@@ -3726,7 +3799,7 @@ export function createSetupTab(screen, services) {
|
|
|
3726
3799
|
const elScript = path.join(packageDir, '.claude', 'hooks', 'play-tts-elevenlabs.sh');
|
|
3727
3800
|
let proc;
|
|
3728
3801
|
try {
|
|
3729
|
-
proc = _spawnAudio(resolveBash(), [elScript, `Hi, I
|
|
3802
|
+
proc = _spawnAudio(resolveBash(), [elScript, `Hi, I'm ${v.name} from ElevenLabs.`, v.id], { // NOSONAR — local hook on user's PATH
|
|
3730
3803
|
stdio: 'ignore',
|
|
3731
3804
|
env: { ...process.env, CLAUDE_PROJECT_DIR: targetDir },
|
|
3732
3805
|
});
|
|
@@ -3827,9 +3900,13 @@ export function createSetupTab(screen, services) {
|
|
|
3827
3900
|
destroyList(vpModal, screen, onDone);
|
|
3828
3901
|
}
|
|
3829
3902
|
|
|
3830
|
-
// AVI-S5.1/5.2: Single-item overlay for
|
|
3831
|
-
//
|
|
3832
|
-
|
|
3903
|
+
// AVI-S5.1/5.2: Single-item overlay for TRUE single-voice engines (soprano).
|
|
3904
|
+
// SAPI / macOS Say are native but MULTI-voice — they fall through to the full
|
|
3905
|
+
// voice picker below (populated from their built-in catalog) so a specific
|
|
3906
|
+
// voice (e.g. Zira) can be chosen and previewed, not just the engine name.
|
|
3907
|
+
const nativeVoice = MULTI_VOICE_NATIVE.has(draft.ttsEngine)
|
|
3908
|
+
? null
|
|
3909
|
+
: NATIVE_ENGINE_VOICES[draft.ttsEngine];
|
|
3833
3910
|
if (nativeVoice) {
|
|
3834
3911
|
draft.voice = nativeVoice.id;
|
|
3835
3912
|
let _nvClosed = false;
|
|
@@ -3890,7 +3967,10 @@ export function createSetupTab(screen, services) {
|
|
|
3890
3967
|
screen.render();
|
|
3891
3968
|
return;
|
|
3892
3969
|
}
|
|
3893
|
-
const
|
|
3970
|
+
const _engineLabel = nativeVoice.id === 'macos' ? 'macOS'
|
|
3971
|
+
: (nativeVoice.id && nativeVoice.id.includes('sapi')) ? 'Windows SAPI'
|
|
3972
|
+
: (nativeVoice.id || 'your system');
|
|
3973
|
+
const phrase = `Hi, I'm ${nativeVoice.label} from ${_engineLabel}.`;
|
|
3894
3974
|
const engine = nativeVoice.id;
|
|
3895
3975
|
|
|
3896
3976
|
function _spawnAndTrack(cmd, args, opts) {
|
|
@@ -4091,7 +4171,10 @@ export function createSetupTab(screen, services) {
|
|
|
4091
4171
|
if (_vpClosed) return;
|
|
4092
4172
|
const savedIdx = vpList.selected ?? 0;
|
|
4093
4173
|
const savedScroll = vpList.childBase ?? 0;
|
|
4094
|
-
|
|
4174
|
+
// SAPI / macOS Say list their built-in catalog voices; Piper scans disk.
|
|
4175
|
+
_allVoices = MULTI_VOICE_NATIVE.has(draft.ttsEngine)
|
|
4176
|
+
? voicesForProvider(draft.ttsEngine, { scanInstalledVoices, getVoiceMeta }).map(v => v.id)
|
|
4177
|
+
: scanInstalledVoices();
|
|
4095
4178
|
// Sort by display name so the first-letter quick jump is intuitive
|
|
4096
4179
|
_allVoices.sort((a, b) => getVoiceMeta(a).displayName.localeCompare(
|
|
4097
4180
|
getVoiceMeta(b).displayName, undefined, { sensitivity: 'base' }));
|
|
@@ -4106,7 +4189,7 @@ export function createSetupTab(screen, services) {
|
|
|
4106
4189
|
if (_previewVoiceId === voiceId) { _killVP(); vpPreviewLine.setContent(''); _refreshVP(); return; }
|
|
4107
4190
|
_killVP();
|
|
4108
4191
|
|
|
4109
|
-
const phrase =
|
|
4192
|
+
const phrase = previewPhrase(voiceId);
|
|
4110
4193
|
|
|
4111
4194
|
// Route through remote provider if active
|
|
4112
4195
|
// Search order: targetDir → cwd → package root → home
|
|
@@ -4133,6 +4216,10 @@ export function createSetupTab(screen, services) {
|
|
|
4133
4216
|
: targetDir;
|
|
4134
4217
|
const _rEnv = {
|
|
4135
4218
|
..._spawnEnv, CLAUDE_PROJECT_DIR: targetDir,
|
|
4219
|
+
// audition = keep the EXACT previewed voice (F1); force the receiver
|
|
4220
|
+
// engine so SAPI/macOS voices render in their own engine, not the default.
|
|
4221
|
+
AGENTVIBES_VOICE_SOURCE: 'audition',
|
|
4222
|
+
AGENTVIBES_RECEIVER_PROVIDER_OVERRIDE: receiverProviderId(draft.ttsEngine || 'piper'),
|
|
4136
4223
|
...(llmKey ? { AGENTVIBES_LLM_KEY: `llm:${llmKey}` } : {}),
|
|
4137
4224
|
};
|
|
4138
4225
|
let rProc;
|
|
@@ -4163,6 +4250,41 @@ export function createSetupTab(screen, services) {
|
|
|
4163
4250
|
return;
|
|
4164
4251
|
}
|
|
4165
4252
|
|
|
4253
|
+
// Local preview of the native MULTI-voice engines (Windows SAPI, macOS Say):
|
|
4254
|
+
// the piper synth below cannot render these. Restores the local preview the
|
|
4255
|
+
// single-item overlay used to provide before SAPI/macOS became multi-voice
|
|
4256
|
+
// (Fable review — otherwise a local SAPI preview mis-ran piper and showed a
|
|
4257
|
+
// bogus "is Piper installed?" error).
|
|
4258
|
+
if (MULTI_VOICE_NATIVE.has(draft.ttsEngine)) {
|
|
4259
|
+
let nProc = null;
|
|
4260
|
+
if (_isWin && draft.ttsEngine === 'sapi') {
|
|
4261
|
+
let _sapiScript = '';
|
|
4262
|
+
for (const n of ['play-tts-sapi.ps1', 'play-tts-windows-sapi.ps1']) {
|
|
4263
|
+
for (const base of [packageDir, targetDir]) {
|
|
4264
|
+
const _p = path.join(base, '.claude', 'hooks-windows', n);
|
|
4265
|
+
if (fs.existsSync(_p)) { _sapiScript = _p; break; }
|
|
4266
|
+
}
|
|
4267
|
+
if (_sapiScript) break;
|
|
4268
|
+
}
|
|
4269
|
+
if (_sapiScript) {
|
|
4270
|
+
nProc = _spawnAudio('powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', _sapiScript, '-Text', phrase, '-VoiceOverride', voiceId], { stdio: 'ignore', windowsHide: true }); // NOSONAR
|
|
4271
|
+
}
|
|
4272
|
+
} else if (process.platform === 'darwin' && draft.ttsEngine === 'macos-say') {
|
|
4273
|
+
nProc = _spawnAudio('say', ['-v', voiceId, phrase], { stdio: 'ignore' }); // NOSONAR
|
|
4274
|
+
}
|
|
4275
|
+
if (!nProc) {
|
|
4276
|
+
// Engine can't run on THIS OS and this LLM isn't remote — nothing to play.
|
|
4277
|
+
if (!_vpClosed) { vpPreviewLine.setContent('{yellow-fg}♪ Runs on the receiver — set this LLM to remote to preview{/yellow-fg}'); _refreshVP(); }
|
|
4278
|
+
return;
|
|
4279
|
+
}
|
|
4280
|
+
_previewProc = nProc;
|
|
4281
|
+
_previewVoiceId = voiceId;
|
|
4282
|
+
if (!_vpClosed) { vpPreviewLine.setContent(`{cyan-fg}♪ Playing: ${voiceId}...{/cyan-fg}`); _refreshVP(); }
|
|
4283
|
+
nProc.on('exit', () => { if (_previewVoiceId === voiceId) { _previewVoiceId = null; _previewProc = null; if (!_vpClosed) { vpPreviewLine.setContent(''); _refreshVP(); } } });
|
|
4284
|
+
nProc.on('error', () => { _previewProc = null; _previewVoiceId = null; });
|
|
4285
|
+
return;
|
|
4286
|
+
}
|
|
4287
|
+
|
|
4166
4288
|
const _ms = parseMultiSpeaker(voiceId);
|
|
4167
4289
|
const voicePath = path.resolve(PIPER_VOICES_DIR, _ms.model + '.onnx');
|
|
4168
4290
|
const safeBase = path.resolve(PIPER_VOICES_DIR);
|
|
@@ -17,6 +17,7 @@ import { buildAudioEnv, detectWavPlayer, getAllWavPlayers } from '../audio-env.j
|
|
|
17
17
|
import { SURNAME_POOL, uniquifyVoiceName } from '../../utils/voice-names.js';
|
|
18
18
|
import { voicesForProvider, ELEVENLABS_VOICES, KOKORO_VOICE_IDS, WINDOWS_SAPI_VOICES, MACOS_VOICES, kokoroGender } from '../../services/provider-voice-catalog.js';
|
|
19
19
|
import { getProvider as _catalogGetProvider } from '../../services/provider-catalog.js';
|
|
20
|
+
import { receiverProviderId } from '../../services/tts-engine-service.js';
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* Resolve display name / gender / provider label for a NON-Piper catalog voice
|
|
@@ -531,7 +532,7 @@ export function scanInstalledVoices() {
|
|
|
531
532
|
} catch { /* fall through to add as single voice */ }
|
|
532
533
|
result.push(voiceId);
|
|
533
534
|
}
|
|
534
|
-
return result.sort();
|
|
535
|
+
return result.sort((a, b) => a.localeCompare(b));
|
|
535
536
|
} catch {
|
|
536
537
|
return [];
|
|
537
538
|
}
|
|
@@ -724,6 +725,27 @@ export function getVoiceMeta(voiceId) {
|
|
|
724
725
|
return result;
|
|
725
726
|
}
|
|
726
727
|
|
|
728
|
+
/**
|
|
729
|
+
* Build the spoken preview phrase for a voice — announces BOTH the voice's name
|
|
730
|
+
* and the engine it renders on, consistently across every voice picker in the
|
|
731
|
+
* app (Piper / Kokoro / Windows SAPI / macOS / ElevenLabs). Centralised so a
|
|
732
|
+
* preview reads the same everywhere: "Hi, I'm Bella from Piper."
|
|
733
|
+
* @param {string} voiceId
|
|
734
|
+
* @returns {string}
|
|
735
|
+
*/
|
|
736
|
+
export function previewPhrase(voiceId) {
|
|
737
|
+
const meta = getVoiceMeta(voiceId);
|
|
738
|
+
// Strip a parenthetical model note ("Piper (libritts)" -> "Piper") for a clean
|
|
739
|
+
// spoken line; fall back to a neutral label if the provider can't be resolved.
|
|
740
|
+
const engine = String(meta.provider || '').replace(/\s*\(.*\)\s*$/, '').trim() || 'AgentVibes';
|
|
741
|
+
// Kokoro ids (af_bella, am_michael) carry a 2-letter lang/gender prefix that the
|
|
742
|
+
// display name keeps ("Af Bella") — awkward aloud, so speak just the name part.
|
|
743
|
+
let name = meta.displayName;
|
|
744
|
+
const kok = voiceId.match(/^[a-z]{2}_([a-z0-9_]+)$/);
|
|
745
|
+
if (kok) name = kok[1].replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
|
746
|
+
return `Hi, I'm ${name} from ${engine}.`;
|
|
747
|
+
}
|
|
748
|
+
|
|
727
749
|
// ---------------------------------------------------------------------------
|
|
728
750
|
|
|
729
751
|
/**
|
|
@@ -1037,7 +1059,27 @@ export function createVoicesTab(screen, services) {
|
|
|
1037
1059
|
if (!_isPiperLocal) {
|
|
1038
1060
|
const _remote = _remoteProviders.includes(activeProvider);
|
|
1039
1061
|
const isWindows = process.platform === 'win32' && !process.env.WSL_DISTRO_NAME;
|
|
1040
|
-
const phrase =
|
|
1062
|
+
const phrase = previewPhrase(voiceId);
|
|
1063
|
+
// A PREVIEW must render in the previewed voice's OWN engine — never the
|
|
1064
|
+
// receiver's persisted default (receiver-provider.txt). Compute the engine
|
|
1065
|
+
// these voices belong to (the Default TTS Engine, same rule the list uses)
|
|
1066
|
+
// and forward it as a ONE-OFF override the remote sender honours for this
|
|
1067
|
+
// invocation only. This changes no settings; it just makes preview preview.
|
|
1068
|
+
let _previewEngine = configService?.getConfig?.()?.ttsEngine
|
|
1069
|
+
|| providerService?.getActiveProvider?.() || 'piper';
|
|
1070
|
+
// Guard: if that resolves to a transport (not a real engine), fall back to
|
|
1071
|
+
// piper so we never tell the receiver to "synthesize with ssh-remote".
|
|
1072
|
+
if (_remoteProviders.includes(_previewEngine)) _previewEngine = 'piper';
|
|
1073
|
+
// AGENTVIBES_VOICE_SOURCE=audition: the resolver must keep the EXACT voice
|
|
1074
|
+
// we're previewing (F1), not demote it to the per-LLM/provider voice —
|
|
1075
|
+
// required for engines whose ids aren't self-identifying (SAPI/macOS).
|
|
1076
|
+
// receiverProviderId maps the engine id to the receiver's provider id the
|
|
1077
|
+
// sender validates (sapi -> windows-sapi, macos-say -> macos).
|
|
1078
|
+
const _previewEnv = {
|
|
1079
|
+
..._spawnEnv,
|
|
1080
|
+
AGENTVIBES_VOICE_SOURCE: 'audition',
|
|
1081
|
+
AGENTVIBES_RECEIVER_PROVIDER_OVERRIDE: receiverProviderId(_previewEngine),
|
|
1082
|
+
};
|
|
1041
1083
|
// Hooks live in the AgentVibes package (projectRoot), not the user's project dir.
|
|
1042
1084
|
// Fall back to CLAUDE_PROJECT_DIR / cwd only if the hook isn't at projectRoot
|
|
1043
1085
|
// (e.g. when running from a published npm package with a different layout).
|
|
@@ -1052,13 +1094,13 @@ export function createVoicesTab(screen, services) {
|
|
|
1052
1094
|
const playTts = path.join(hooksBase, '.claude', 'hooks-windows', 'play-tts.ps1');
|
|
1053
1095
|
const _pdArgs = activeProjectDir ? ['-ProjectDir', activeProjectDir] : [];
|
|
1054
1096
|
proc = spawn('powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', playTts, phrase, voiceId, ..._pdArgs], { // NOSONAR
|
|
1055
|
-
stdio: 'ignore', detached: false, windowsHide: true, env:
|
|
1097
|
+
stdio: 'ignore', detached: false, windowsHide: true, env: _previewEnv,
|
|
1056
1098
|
});
|
|
1057
1099
|
} else {
|
|
1058
1100
|
const playTts = path.join(hooksBase, '.claude', 'hooks', 'play-tts.sh');
|
|
1059
1101
|
const _pdArgs = activeProjectDir ? ['--project-dir', activeProjectDir] : [];
|
|
1060
1102
|
proc = spawn('bash', [playTts, phrase, voiceId, ..._pdArgs], { // NOSONAR
|
|
1061
|
-
stdio: ['ignore', 'ignore', 'pipe'], detached: true, env:
|
|
1103
|
+
stdio: ['ignore', 'ignore', 'pipe'], detached: true, env: _previewEnv,
|
|
1062
1104
|
cwd: process.cwd(),
|
|
1063
1105
|
});
|
|
1064
1106
|
}
|
|
@@ -1103,7 +1145,7 @@ export function createVoicesTab(screen, services) {
|
|
|
1103
1145
|
}
|
|
1104
1146
|
|
|
1105
1147
|
const tempWav = path.join(os.tmpdir(), `agentvibes-preview-${Date.now()}.wav`);
|
|
1106
|
-
const phrase =
|
|
1148
|
+
const phrase = previewPhrase(voiceId);
|
|
1107
1149
|
|
|
1108
1150
|
// Synthesize: spawn piper; on Windows use the exe path directly
|
|
1109
1151
|
const isWindows = process.platform === 'win32' && !process.env.WSL_DISTRO_NAME;
|
|
@@ -1351,7 +1393,7 @@ export function createVoicesTab(screen, services) {
|
|
|
1351
1393
|
try { fs.unlinkSync(path.join(globalClaudeDir, 'tts-piper-speaker-id.txt')); } catch { /* ok */ }
|
|
1352
1394
|
}
|
|
1353
1395
|
// Also write global tts-voice.txt for shell scripts
|
|
1354
|
-
try { fs.writeFileSync(path.join(globalClaudeDir, 'tts-voice.txt'),
|
|
1396
|
+
try { fs.writeFileSync(path.join(globalClaudeDir, 'tts-voice.txt'), voiceId, 'utf8'); } catch { /* ok */ }
|
|
1355
1397
|
}
|
|
1356
1398
|
|
|
1357
1399
|
function _openSelectVoiceModal(voiceId) {
|
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AgentVibes TUI — Shared Widget: Modal Destroy Helper
|
|
3
|
-
*
|
|
4
|
-
* Force-invalidates blessed's olines buffer after destroying a modal widget.
|
|
5
|
-
* Without this, blessed skips repainting cells where lines==olines and the
|
|
6
|
-
* terminal retains stale modal content as ghost artifacts.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Destroy a blessed list/box widget and force full screen repaint.
|
|
11
|
-
*
|
|
12
|
-
* @param {object} widget - blessed widget to destroy
|
|
13
|
-
* @param {object} screen - blessed screen instance
|
|
14
|
-
* @param {Function} [onClose] - optional callback after destruction
|
|
15
|
-
*/
|
|
16
|
-
export function destroyList(widget, screen, onClose) {
|
|
17
|
-
widget.destroy();
|
|
18
|
-
try {
|
|
19
|
-
for (let r = 0; r < screen.height; r++)
|
|
20
|
-
for (let c = 0; c < screen.width; c++)
|
|
21
|
-
if (screen.olines[r]?.[c]) screen.olines[r][c][0] = -1;
|
|
22
|
-
} catch {}
|
|
23
|
-
onClose?.();
|
|
24
|
-
screen.render();
|
|
25
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* AgentVibes TUI — Shared Widget: Modal Destroy Helper
|
|
3
|
+
*
|
|
4
|
+
* Force-invalidates blessed's olines buffer after destroying a modal widget.
|
|
5
|
+
* Without this, blessed skips repainting cells where lines==olines and the
|
|
6
|
+
* terminal retains stale modal content as ghost artifacts.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Destroy a blessed list/box widget and force full screen repaint.
|
|
11
|
+
*
|
|
12
|
+
* @param {object} widget - blessed widget to destroy
|
|
13
|
+
* @param {object} screen - blessed screen instance
|
|
14
|
+
* @param {Function} [onClose] - optional callback after destruction
|
|
15
|
+
*/
|
|
16
|
+
export function destroyList(widget, screen, onClose) {
|
|
17
|
+
widget.destroy();
|
|
18
|
+
try {
|
|
19
|
+
for (let r = 0; r < screen.height; r++)
|
|
20
|
+
for (let c = 0; c < screen.width; c++)
|
|
21
|
+
if (screen.olines[r]?.[c]) screen.olines[r][c][0] = -1;
|
|
22
|
+
} catch {}
|
|
23
|
+
onClose?.();
|
|
24
|
+
screen.render();
|
|
25
|
+
}
|
|
@@ -1,55 +1,55 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AgentVibes TUI — Shared Widget: Notice Toast
|
|
3
|
-
*
|
|
4
|
-
* Displays a short auto-dismissing notice modal centred on screen.
|
|
5
|
-
* Usable from any tab; no settings-specific state required.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { destroyList } from './destroy-list.js';
|
|
9
|
-
|
|
10
|
-
const IS_TEST = process.env.AGENTVIBES_TEST_MODE === 'true';
|
|
11
|
-
let blessed;
|
|
12
|
-
if (!IS_TEST) {
|
|
13
|
-
const { default: b } = await import('blessed');
|
|
14
|
-
blessed = b;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Show a temporary notice that auto-dismisses after 2.5 seconds.
|
|
19
|
-
*
|
|
20
|
-
* @param {object} screen - blessed screen instance
|
|
21
|
-
* @param {string} message - text to display
|
|
22
|
-
* @param {object} [opts]
|
|
23
|
-
* @param {string} [opts.bg='#0a0e1a'] - background colour
|
|
24
|
-
* @param {string} [opts.fg='#e3f2fd'] - foreground colour
|
|
25
|
-
* @param {string} [opts.borderFg='bright-cyan'] - border colour
|
|
26
|
-
* @param {number} [opts.durationMs=2500] - auto-dismiss delay in ms
|
|
27
|
-
*/
|
|
28
|
-
export function showNotice(screen, message, opts = {}) {
|
|
29
|
-
const bg = opts.bg ?? '#0a0e1a';
|
|
30
|
-
const fg = opts.fg ?? '#e3f2fd';
|
|
31
|
-
const borderFg = opts.borderFg ?? 'bright-cyan';
|
|
32
|
-
const durationMs = opts.durationMs ?? 2500;
|
|
33
|
-
|
|
34
|
-
const width = Math.max(28, message.length + 6);
|
|
35
|
-
const modal = blessed.box({
|
|
36
|
-
parent: screen,
|
|
37
|
-
top: 'center',
|
|
38
|
-
left: 'center',
|
|
39
|
-
width,
|
|
40
|
-
height: 3,
|
|
41
|
-
border: { type: 'line' },
|
|
42
|
-
tags: true,
|
|
43
|
-
content: `{center}${message}{/center}`,
|
|
44
|
-
style: {
|
|
45
|
-
fg,
|
|
46
|
-
bg,
|
|
47
|
-
border: { fg: borderFg },
|
|
48
|
-
},
|
|
49
|
-
});
|
|
50
|
-
screen.render();
|
|
51
|
-
|
|
52
|
-
setTimeout(() => {
|
|
53
|
-
destroyList(modal, screen);
|
|
54
|
-
}, durationMs);
|
|
55
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* AgentVibes TUI — Shared Widget: Notice Toast
|
|
3
|
+
*
|
|
4
|
+
* Displays a short auto-dismissing notice modal centred on screen.
|
|
5
|
+
* Usable from any tab; no settings-specific state required.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { destroyList } from './destroy-list.js';
|
|
9
|
+
|
|
10
|
+
const IS_TEST = process.env.AGENTVIBES_TEST_MODE === 'true';
|
|
11
|
+
let blessed;
|
|
12
|
+
if (!IS_TEST) {
|
|
13
|
+
const { default: b } = await import('blessed');
|
|
14
|
+
blessed = b;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Show a temporary notice that auto-dismisses after 2.5 seconds.
|
|
19
|
+
*
|
|
20
|
+
* @param {object} screen - blessed screen instance
|
|
21
|
+
* @param {string} message - text to display
|
|
22
|
+
* @param {object} [opts]
|
|
23
|
+
* @param {string} [opts.bg='#0a0e1a'] - background colour
|
|
24
|
+
* @param {string} [opts.fg='#e3f2fd'] - foreground colour
|
|
25
|
+
* @param {string} [opts.borderFg='bright-cyan'] - border colour
|
|
26
|
+
* @param {number} [opts.durationMs=2500] - auto-dismiss delay in ms
|
|
27
|
+
*/
|
|
28
|
+
export function showNotice(screen, message, opts = {}) {
|
|
29
|
+
const bg = opts.bg ?? '#0a0e1a';
|
|
30
|
+
const fg = opts.fg ?? '#e3f2fd';
|
|
31
|
+
const borderFg = opts.borderFg ?? 'bright-cyan';
|
|
32
|
+
const durationMs = opts.durationMs ?? 2500;
|
|
33
|
+
|
|
34
|
+
const width = Math.max(28, message.length + 6);
|
|
35
|
+
const modal = blessed.box({
|
|
36
|
+
parent: screen,
|
|
37
|
+
top: 'center',
|
|
38
|
+
left: 'center',
|
|
39
|
+
width,
|
|
40
|
+
height: 3,
|
|
41
|
+
border: { type: 'line' },
|
|
42
|
+
tags: true,
|
|
43
|
+
content: `{center}${message}{/center}`,
|
|
44
|
+
style: {
|
|
45
|
+
fg,
|
|
46
|
+
bg,
|
|
47
|
+
border: { fg: borderFg },
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
screen.render();
|
|
51
|
+
|
|
52
|
+
setTimeout(() => {
|
|
53
|
+
destroyList(modal, screen);
|
|
54
|
+
}, durationMs);
|
|
55
|
+
}
|