agentvibes 4.4.1 → 4.5.7
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/config.json +4 -4
- package/.claude/config/audio-effects.cfg +1 -0
- package/.claude/config/background-music-enabled.txt +1 -0
- package/.claude/config/reverb-level.txt +1 -1
- package/.claude/github-star-reminder.txt +1 -1
- package/.claude/hooks/audio-processor.sh +1 -1
- package/.claude/hooks/bmad-speak.sh +16 -2
- package/.claude/hooks-windows/bmad-speak.ps1 +200 -0
- package/.claude/hooks-windows/play-tts-piper.ps1 +3 -4
- package/.claude/hooks-windows/play-tts-sapi.ps1 +3 -4
- package/.claude/hooks-windows/play-tts-soprano.ps1 +2 -3
- package/.claude/hooks-windows/play-tts-termux-ssh.ps1 +138 -0
- package/.claude/hooks-windows/play-tts.ps1 +14 -6
- package/.claude/hooks-windows/provider-manager.ps1 +16 -1
- package/CLAUDE.md +4 -0
- package/README.md +39 -9
- package/RELEASE_NOTES.md +78 -0
- package/bin/agent-vibes +1 -1
- package/bin/agentvibes-voice-browser.js +1 -1
- package/bin/bmad-speak.js +52 -0
- package/bin/mcp-server.js +1 -1
- package/bin/test-bmad-pr +1 -1
- package/package.json +1 -1
- package/setup-windows.ps1 +4 -4
- package/src/console/app.js +63 -12
- package/src/console/navigation.js +5 -2
- package/src/console/tabs/agents-tab.js +72 -76
- package/src/console/tabs/help-tab.js +107 -54
- package/src/console/tabs/install-tab.js +132 -56
- package/src/console/tabs/music-tab.js +1039 -1011
- package/src/console/tabs/placeholder-tab.js +27 -0
- package/src/console/tabs/readme-tab.js +9 -7
- package/src/console/tabs/receiver-tab.js +23 -12
- package/src/console/tabs/settings-tab.js +4001 -3783
- package/src/console/tabs/voices-tab.js +1680 -1653
- package/src/console/widgets/personality-picker.js +35 -7
- package/src/console/widgets/reverb-picker.js +9 -6
- package/src/console/widgets/track-picker.js +7 -2
- package/src/i18n/de.js +203 -0
- package/src/i18n/en.js +203 -0
- package/src/i18n/es.js +203 -0
- package/src/i18n/fr.js +203 -0
- package/src/i18n/hi.js +203 -0
- package/src/i18n/ja.js +203 -0
- package/src/i18n/ko.js +203 -0
- package/src/i18n/pt.js +203 -0
- package/src/i18n/strings.js +54 -0
- package/src/i18n/zh-CN.js +203 -0
- package/src/installer/language-screen.js +31 -0
- package/src/installer.js +79 -25
- package/src/services/language-service.js +47 -0
- package/src/utils/file-ownership-verifier.js +2 -2
- package/src/utils/provider-validator.js +9 -13
- package/.claude/hooks-windows/play-tts-windows-piper.ps1 +0 -209
- package/.claude/hooks-windows/play-tts-windows-sapi.ps1 +0 -108
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,5 +1,83 @@
|
|
|
1
1
|
# AgentVibes Release Notes
|
|
2
2
|
|
|
3
|
+
## 🐛 v4.5.7 — Patch Release
|
|
4
|
+
|
|
5
|
+
**Release Date:** April 2026
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
- **Background music volume default** — All volume defaults lowered from 70% to 20% across the UI (settings tab, agents tab, music tab, track picker). New installs and newly configured agents will default to a much more reasonable background music level.
|
|
10
|
+
- **bmad-speak volume inheritance** — `bmad-speak.sh` and `bmad-speak.ps1` now read the global `background-music-volume.txt` config file as the fallback volume instead of a hardcoded value. Per-agent background music volume now correctly inherits the global setting when no explicit per-agent override is saved.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 🐛 v4.5.1 — Patch Release
|
|
15
|
+
|
|
16
|
+
**Release Date:** April 2026
|
|
17
|
+
|
|
18
|
+
### Bug Fix
|
|
19
|
+
|
|
20
|
+
- **Music tab preview** — Pressing Space on a track in the Music tab now plays correctly
|
|
21
|
+
when running `npx agentvibes` from a fresh directory. Previously, if `.claude/audio/tracks/`
|
|
22
|
+
didn't exist in the current working directory, the track list showed built-in tracks but
|
|
23
|
+
Space did nothing (the player was spawned against a non-existent path). Now falls back to
|
|
24
|
+
the package-bundled tracks directory automatically.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 🌍 v4.5.0 — "Speak Every Language" Release
|
|
29
|
+
|
|
30
|
+
**Release Date:** April 2026
|
|
31
|
+
|
|
32
|
+
Full multilingual TUI support across all 9 languages, complete Windows security hardening, and zero failing tests.
|
|
33
|
+
|
|
34
|
+
### 🌍 Multilingual TUI — 9 Languages
|
|
35
|
+
|
|
36
|
+
Every screen, tab, button, and label in the `npx agentvibes` TUI is now fully translated:
|
|
37
|
+
|
|
38
|
+
- **English, Spanish, French, German, Portuguese, Japanese, Korean, Chinese (Simplified), Italian**
|
|
39
|
+
- Language selection on first launch (Screen 0 of the installer wizard)
|
|
40
|
+
- Language sub-tab in Settings — switch language live without restarting
|
|
41
|
+
- All tab bar labels, button text, footer hints, and status messages translated
|
|
42
|
+
- BMAD tab and SSH Receiver tab fully localized
|
|
43
|
+
- Per-language i18n files (`src/i18n/en.js`, `es.js`, `fr.js`, ...) with English fallback
|
|
44
|
+
|
|
45
|
+
### 🪟 Windows Security & Bug Fixes
|
|
46
|
+
|
|
47
|
+
- **Temp filenames** — All `Date.now()` temp filenames replaced with `randomUUID()` across JS and PowerShell (unpredictable, prevents temp file hijacking)
|
|
48
|
+
- **Shell injection** — `execSync('which ...', { shell: true })` replaced with `spawnSync` (no shell expansion)
|
|
49
|
+
- **Music player** — Hardcoded `ffplay` on Windows replaced with `detectMp3Player()` (respects user's installed player)
|
|
50
|
+
- **Boolean coercion** — `isWindowsTerminal` now correctly returns `true/false` instead of leaking `WT_SESSION` UUID string
|
|
51
|
+
- **Network mount detection** — `.match()` result properly coerced to boolean
|
|
52
|
+
|
|
53
|
+
### 🎙️ Cross-Platform BMAD Speak
|
|
54
|
+
|
|
55
|
+
BMAD (Build More Architect Dreams) is an AI multi-agent framework where specialized agents — Architect, PM, Developer, QA, and Analyst — collaborate to build software. With this release, every agent in a BMAD party mode session now speaks aloud with their own unique voice, personality, and music on Windows — making each role instantly recognizable.
|
|
56
|
+
|
|
57
|
+
## 🐛 v4.5.1 — Patch Release
|
|
58
|
+
|
|
59
|
+
**Release Date:** April 2026
|
|
60
|
+
|
|
61
|
+
### Bug Fix
|
|
62
|
+
|
|
63
|
+
- **Music tab preview** — Pressing Space on a track in the Music tab now plays correctly
|
|
64
|
+
when running `npx agentvibes` from a fresh directory. Previously, if `.claude/audio/tracks/`
|
|
65
|
+
didn't exist in the current working directory, the track list showed built-in tracks but
|
|
66
|
+
Space did nothing (the player was spawned against a non-existent path). Now falls back to
|
|
67
|
+
the package-bundled tracks directory automatically.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
- `bin/bmad-speak.js` — cross-platform entry point for BMAD agent speech
|
|
72
|
+
- `.claude/hooks-windows/bmad-speak.ps1` — native Windows BMAD speak with per-agent personality routing
|
|
73
|
+
|
|
74
|
+
### 🧪 Test Suite
|
|
75
|
+
|
|
76
|
+
- 600 tests, 0 failures
|
|
77
|
+
- Full cross-platform coverage (Windows path separators, chmod skip, provider file restore)
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
3
81
|
## 🎉 v4.4.0 — "Full Platform Parity" Release
|
|
4
82
|
|
|
5
83
|
**Release Date:** March 2026
|
package/bin/agent-vibes
CHANGED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AgentVibes - Cross-platform BMAD agent TTS entry point
|
|
5
|
+
*
|
|
6
|
+
* Delegates to the correct platform script:
|
|
7
|
+
* Windows (non-WSL) → .claude/hooks-windows/bmad-speak.ps1
|
|
8
|
+
* Linux/Mac/WSL → .claude/hooks/bmad-speak.sh
|
|
9
|
+
*
|
|
10
|
+
* Usage: node bin/bmad-speak.js "Agent Name" "dialogue text"
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { spawnSync } from 'node:child_process';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import os from 'node:os';
|
|
17
|
+
|
|
18
|
+
const [, , agentName, dialogue] = process.argv;
|
|
19
|
+
|
|
20
|
+
if (!agentName || !dialogue) {
|
|
21
|
+
process.stderr.write('Usage: bmad-speak.js "Agent Name" "dialogue text"\n');
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const IS_WINDOWS = process.platform === 'win32' && !process.env.WSL_DISTRO_NAME;
|
|
26
|
+
|
|
27
|
+
// Resolve script path — prefer project-local, fall back to global ~/.claude install
|
|
28
|
+
function resolveScript(relPath) {
|
|
29
|
+
const cwdPath = path.join(process.cwd(), relPath);
|
|
30
|
+
const homePath = path.join(os.homedir(), relPath.replace(/^\.claude[\\/]/, '.claude/'));
|
|
31
|
+
if (fs.existsSync(cwdPath)) return cwdPath;
|
|
32
|
+
if (fs.existsSync(homePath)) return homePath;
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let result;
|
|
37
|
+
|
|
38
|
+
if (IS_WINDOWS) {
|
|
39
|
+
const script = resolveScript('.claude/hooks-windows/bmad-speak.ps1');
|
|
40
|
+
if (!script) process.exit(0);
|
|
41
|
+
result = spawnSync(
|
|
42
|
+
'powershell',
|
|
43
|
+
['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', script, agentName, dialogue],
|
|
44
|
+
{ stdio: 'inherit' }
|
|
45
|
+
);
|
|
46
|
+
} else {
|
|
47
|
+
const script = resolveScript('.claude/hooks/bmad-speak.sh');
|
|
48
|
+
if (!script) process.exit(0);
|
|
49
|
+
result = spawnSync('bash', [script, agentName, dialogue], { stdio: 'inherit' });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
process.exit(result.status ?? 0);
|
package/bin/mcp-server.js
CHANGED
package/bin/test-bmad-pr
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "agentvibes",
|
|
4
|
-
"version": "4.
|
|
4
|
+
"version": "4.5.7",
|
|
5
5
|
"description": "Now your AI Agents can finally talk back! Professional TTS voice for Claude Code, Claude Desktop (via MCP), and Clawdbot with multi-provider support.",
|
|
6
6
|
"homepage": "https://agentvibes.org",
|
|
7
7
|
"keywords": [
|
package/setup-windows.ps1
CHANGED
|
@@ -283,8 +283,8 @@ if (-not (Test-Path $HooksDir)) {
|
|
|
283
283
|
$HookScriptInfo = @(
|
|
284
284
|
@{ Name = "play-tts.ps1"; Desc = "Main TTS router - dispatches to active provider" },
|
|
285
285
|
@{ Name = "play-tts-soprano.ps1"; Desc = "Soprano neural voice provider (fastest)" },
|
|
286
|
-
@{ Name = "play-tts-
|
|
287
|
-
@{ Name = "play-tts-
|
|
286
|
+
@{ Name = "play-tts-piper.ps1"; Desc = "Piper offline neural voice provider" },
|
|
287
|
+
@{ Name = "play-tts-sapi.ps1"; Desc = "Windows built-in SAPI voice provider" },
|
|
288
288
|
@{ Name = "provider-manager.ps1"; Desc = "Switch between TTS providers" },
|
|
289
289
|
@{ Name = "voice-manager-windows.ps1"; Desc = "Browse and select voice models" },
|
|
290
290
|
@{ Name = "audio-cache-utils.ps1"; Desc = "Manage TTS audio file cache" },
|
|
@@ -765,11 +765,11 @@ try {
|
|
|
765
765
|
}
|
|
766
766
|
}
|
|
767
767
|
"piper" {
|
|
768
|
-
& "$HooksDir\play-tts-
|
|
768
|
+
& "$HooksDir\play-tts-piper.ps1" $TestMessage | Out-Null
|
|
769
769
|
Write-Ok "Piper TTS is working"
|
|
770
770
|
}
|
|
771
771
|
"sapi" {
|
|
772
|
-
& "$HooksDir\play-tts-
|
|
772
|
+
& "$HooksDir\play-tts-sapi.ps1" $TestMessage | Out-Null
|
|
773
773
|
Write-Ok "Windows SAPI is working"
|
|
774
774
|
}
|
|
775
775
|
}
|
package/src/console/app.js
CHANGED
|
@@ -14,7 +14,9 @@ import { fileURLToPath } from 'node:url';
|
|
|
14
14
|
import { spawnSync, execFileSync } from 'node:child_process';
|
|
15
15
|
import { NavigationService, TAB_ORDER } from '../services/navigation-service.js';
|
|
16
16
|
import { setupNavigation } from './navigation.js';
|
|
17
|
-
import { createPlaceholderTab, TAB_DISPLAY_LABELS, TAB_SHORTCUT_KEYS } from './tabs/placeholder-tab.js';
|
|
17
|
+
import { createPlaceholderTab, TAB_DISPLAY_LABELS, TAB_SHORTCUT_KEYS, getTabLabel } from './tabs/placeholder-tab.js';
|
|
18
|
+
import { LanguageService } from '../services/language-service.js';
|
|
19
|
+
import { t } from '../i18n/strings.js';
|
|
18
20
|
import { FOOTER_CONFIG, DEFAULT_FOOTER_COLOR } from './footer-config.js';
|
|
19
21
|
import { createModalOverlay } from './modals/modal-overlay.js';
|
|
20
22
|
import { BRAND_PINK } from './brand-colors.js';
|
|
@@ -169,7 +171,7 @@ export class AgentVibesConsole {
|
|
|
169
171
|
});
|
|
170
172
|
|
|
171
173
|
// Row 1: subtitle
|
|
172
|
-
blessed.text({
|
|
174
|
+
this._headerSubtitleText = blessed.text({
|
|
173
175
|
parent: this.headerBox,
|
|
174
176
|
top: 1,
|
|
175
177
|
left: 2,
|
|
@@ -180,7 +182,7 @@ export class AgentVibesConsole {
|
|
|
180
182
|
});
|
|
181
183
|
|
|
182
184
|
// Row 1: Quit shortcut — left-anchored after "Customization Tool" (18 chars at left:2)
|
|
183
|
-
blessed.text({
|
|
185
|
+
this._headerQuitText = blessed.text({
|
|
184
186
|
parent: this.headerBox,
|
|
185
187
|
top: 1,
|
|
186
188
|
left: 22,
|
|
@@ -291,9 +293,11 @@ export class AgentVibesConsole {
|
|
|
291
293
|
|
|
292
294
|
// One box per tab — direct screen children at absolute top:3. No tag parsing, no wrapping.
|
|
293
295
|
this._tabItems = {};
|
|
296
|
+
this._tabItemXOffsets = {}; // track x positions for label refresh
|
|
294
297
|
let xOffset = 1;
|
|
295
298
|
for (const id of TAB_ORDER) {
|
|
296
|
-
const
|
|
299
|
+
const lang = this._languageService?.getLang() ?? 'en';
|
|
300
|
+
const label = getTabLabel(id, lang);
|
|
297
301
|
const shortcutKey = TAB_SHORTCUT_KEYS[id] || label[0];
|
|
298
302
|
const text = ` [${shortcutKey}] ${label} `;
|
|
299
303
|
const el = blessed.box({
|
|
@@ -304,11 +308,13 @@ export class AgentVibesConsole {
|
|
|
304
308
|
height: 1,
|
|
305
309
|
content: text,
|
|
306
310
|
tags: false,
|
|
311
|
+
wrap: false,
|
|
307
312
|
keys: true,
|
|
308
313
|
focusable: true,
|
|
309
314
|
style: { fg: COLORS.focusCyan, bg: COLORS.tabBarBg },
|
|
310
315
|
});
|
|
311
316
|
this._tabItems[id] = el;
|
|
317
|
+
this._tabItemXOffsets[id] = xOffset;
|
|
312
318
|
xOffset += text.length + 1; // 1-space gap between tabs
|
|
313
319
|
}
|
|
314
320
|
|
|
@@ -359,9 +365,8 @@ export class AgentVibesConsole {
|
|
|
359
365
|
const el = this._tabItems[tabIds[i]];
|
|
360
366
|
|
|
361
367
|
// Blinking block cursor: replace trailing space with █, toggle at 500ms
|
|
362
|
-
|
|
363
|
-
const
|
|
364
|
-
const _blockContent = _baseContent.slice(0, -1) + '█';
|
|
368
|
+
// Always derive from current el.content so language changes are preserved.
|
|
369
|
+
const _getBaseContent = () => el.content.replace(/█$/, ' ');
|
|
365
370
|
let _cursorInterval = null;
|
|
366
371
|
let _cursorOn = false;
|
|
367
372
|
|
|
@@ -369,18 +374,21 @@ export class AgentVibesConsole {
|
|
|
369
374
|
el.style.fg = 'white';
|
|
370
375
|
el.style.bg = '#9c27b0'; // purple — cursor on this tab item
|
|
371
376
|
_cursorOn = true;
|
|
372
|
-
|
|
377
|
+
const _base = _getBaseContent();
|
|
378
|
+
const _block = _base.slice(0, -1) + '█';
|
|
379
|
+
el.setContent(_block);
|
|
373
380
|
this.screen.render();
|
|
374
381
|
if (_cursorInterval) { clearInterval(_cursorInterval); _cursorInterval = null; }
|
|
375
382
|
_cursorInterval = setInterval(() => {
|
|
376
383
|
_cursorOn = !_cursorOn;
|
|
377
|
-
|
|
384
|
+
const b = _getBaseContent();
|
|
385
|
+
el.setContent(_cursorOn ? b.slice(0, -1) + '█' : b);
|
|
378
386
|
this.screen.render();
|
|
379
387
|
}, 500);
|
|
380
388
|
});
|
|
381
389
|
el.on('blur', () => {
|
|
382
390
|
if (_cursorInterval) { clearInterval(_cursorInterval); _cursorInterval = null; }
|
|
383
|
-
el.setContent(
|
|
391
|
+
el.setContent(_getBaseContent());
|
|
384
392
|
// navigationService set up after _createTabBar, but blur fires lazily — safe
|
|
385
393
|
this._updateTabBar(this.navigationService?.getActiveTab() ?? tabIds[0]);
|
|
386
394
|
this.screen.render();
|
|
@@ -474,13 +482,47 @@ export class AgentVibesConsole {
|
|
|
474
482
|
}
|
|
475
483
|
}
|
|
476
484
|
|
|
485
|
+
// ---------------------------------------------------------------------------
|
|
486
|
+
// Private: Refresh all chrome strings (header subtitle, tab bar labels) when lang changes
|
|
487
|
+
|
|
488
|
+
_refreshChrome(lang) {
|
|
489
|
+
// Update header subtitle "Customization Tool"
|
|
490
|
+
if (this._headerSubtitleText) {
|
|
491
|
+
this._headerSubtitleText.setContent(`{green-fg}${t(lang, 'customizationTool')}{/green-fg}`);
|
|
492
|
+
}
|
|
493
|
+
if (this._headerQuitText) {
|
|
494
|
+
this._headerQuitText.setContent(`{#ef9a9a-fg}${t(lang, 'quitLabel')}{/#ef9a9a-fg}`);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Update tab bar item labels — resize and reposition to fit translated labels
|
|
498
|
+
let xOffset = 1;
|
|
499
|
+
for (const id of TAB_ORDER) {
|
|
500
|
+
const el = this._tabItems?.[id];
|
|
501
|
+
if (!el) continue;
|
|
502
|
+
const label = getTabLabel(id, lang);
|
|
503
|
+
const shortcutKey = TAB_SHORTCUT_KEYS[id] || label[0];
|
|
504
|
+
const text = ` [${shortcutKey}] ${label} `;
|
|
505
|
+
el.left = xOffset;
|
|
506
|
+
el.width = text.length;
|
|
507
|
+
el.setContent(text);
|
|
508
|
+
xOffset += text.length + 1;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Update active tab's footer text if it supports language-aware footer
|
|
512
|
+
const activeId = this.navigationService?.getActiveTab();
|
|
513
|
+
if (activeId) this._updateContextFooter(activeId);
|
|
514
|
+
|
|
515
|
+
this.screen.render();
|
|
516
|
+
}
|
|
517
|
+
|
|
477
518
|
// ---------------------------------------------------------------------------
|
|
478
519
|
// Private: Render tab bar content string for given active tab
|
|
479
520
|
// (kept as a pure helper for unit tests; real rendering uses _updateTabBar)
|
|
480
521
|
|
|
481
522
|
_renderTabBarContent(activeTabId) {
|
|
523
|
+
const lang = this._languageService?.getLang() ?? 'en';
|
|
482
524
|
return TAB_ORDER.map(id => {
|
|
483
|
-
const label =
|
|
525
|
+
const label = getTabLabel(id, lang);
|
|
484
526
|
const shortcutKey = TAB_SHORTCUT_KEYS[id] || label[0];
|
|
485
527
|
if (id === activeTabId) {
|
|
486
528
|
return `{bold}{white-fg}[${shortcutKey}] ${label}{/white-fg}{/bold}`;
|
|
@@ -614,9 +656,14 @@ export class AgentVibesConsole {
|
|
|
614
656
|
const providerService = new ProviderService(configService);
|
|
615
657
|
this._configService = configService;
|
|
616
658
|
this._providerService = providerService;
|
|
659
|
+
const languageService = new LanguageService();
|
|
660
|
+
this._languageService = languageService;
|
|
661
|
+
// Refresh UI chrome when language changes
|
|
662
|
+
languageService.onChange(lang => this._refreshChrome(lang));
|
|
617
663
|
const services = {
|
|
618
664
|
configService,
|
|
619
665
|
providerService,
|
|
666
|
+
languageService,
|
|
620
667
|
navigationService: this.navigationService,
|
|
621
668
|
updateHeaderStatus: () => this._updateHeaderStatus(),
|
|
622
669
|
focusMainTabBar: () => {
|
|
@@ -778,7 +825,11 @@ export class AgentVibesConsole {
|
|
|
778
825
|
});
|
|
779
826
|
|
|
780
827
|
// Register global key bindings (S/V/M/A/R/H/I/T/Esc)
|
|
781
|
-
setupNavigation(this.screen, this.navigationService)
|
|
828
|
+
setupNavigation(this.screen, this.navigationService, () => {
|
|
829
|
+
const id = this.navigationService.getActiveTab();
|
|
830
|
+
const item = this._tabItems?.[id];
|
|
831
|
+
if (item) item.focus();
|
|
832
|
+
});
|
|
782
833
|
}
|
|
783
834
|
|
|
784
835
|
// ---------------------------------------------------------------------------
|
|
@@ -35,8 +35,9 @@ const KEY_TO_TAB = {
|
|
|
35
35
|
*
|
|
36
36
|
* @param {object} screen - Blessed screen instance (or stub in tests)
|
|
37
37
|
* @param {import('../services/navigation-service.js').NavigationService} navigationService
|
|
38
|
+
* @param {function} [focusMainTabBar] - Optional callback to return focus to the tab bar
|
|
38
39
|
*/
|
|
39
|
-
export function setupNavigation(screen, navigationService) {
|
|
40
|
+
export function setupNavigation(screen, navigationService, focusMainTabBar) {
|
|
40
41
|
// Tab switching shortcuts — one handler per key (both cases)
|
|
41
42
|
for (const [key, tabId] of Object.entries(KEY_TO_TAB)) {
|
|
42
43
|
screen.key([key], () => {
|
|
@@ -53,10 +54,12 @@ export function setupNavigation(screen, navigationService) {
|
|
|
53
54
|
}
|
|
54
55
|
});
|
|
55
56
|
|
|
56
|
-
// Escape — close modal
|
|
57
|
+
// Escape — close modal if open, otherwise return focus to tab bar
|
|
57
58
|
screen.key(['escape'], () => {
|
|
58
59
|
if (navigationService.isModalOpen()) {
|
|
59
60
|
navigationService.closeModal();
|
|
61
|
+
} else if (typeof focusMainTabBar === 'function') {
|
|
62
|
+
focusMainTabBar();
|
|
60
63
|
}
|
|
61
64
|
});
|
|
62
65
|
}
|