agentvibes 4.2.0 → 4.4.1
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/bmad-voices.md +69 -69
- package/.agentvibes/config.json +12 -0
- package/.claude/activation-instructions +54 -54
- package/.claude/audio/tracks/README.md +52 -52
- package/.claude/commands/agent-vibes/add.md +21 -21
- package/.claude/commands/agent-vibes/agent-vibes.md +101 -101
- package/.claude/commands/agent-vibes/agent.md +79 -79
- package/.claude/commands/agent-vibes/background-music.md +111 -111
- package/.claude/commands/agent-vibes/bmad.md +198 -198
- package/.claude/commands/agent-vibes/clean.md +18 -18
- package/.claude/commands/agent-vibes/cleanup.md +18 -18
- package/.claude/commands/agent-vibes/commands.json +145 -145
- package/.claude/commands/agent-vibes/effects.md +97 -97
- package/.claude/commands/agent-vibes/get.md +9 -9
- package/.claude/commands/agent-vibes/hide.md +91 -91
- package/.claude/commands/agent-vibes/language.md +23 -23
- package/.claude/commands/agent-vibes/learn.md +67 -67
- package/.claude/commands/agent-vibes/list.md +13 -13
- package/.claude/commands/agent-vibes/mute.md +37 -37
- package/.claude/commands/agent-vibes/preview.md +17 -17
- package/.claude/commands/agent-vibes/provider.md +68 -68
- package/.claude/commands/agent-vibes/replay-target.md +14 -14
- package/.claude/commands/agent-vibes/sample.md +12 -12
- package/.claude/commands/agent-vibes/set-favorite-voice.md +84 -84
- package/.claude/commands/agent-vibes/set-pretext.md +65 -65
- package/.claude/commands/agent-vibes/set-speed.md +41 -41
- package/.claude/commands/agent-vibes/show.md +84 -84
- package/.claude/commands/agent-vibes/switch.md +87 -87
- package/.claude/commands/agent-vibes/target-voice.md +26 -26
- package/.claude/commands/agent-vibes/target.md +30 -30
- package/.claude/commands/agent-vibes/translate.md +68 -68
- package/.claude/commands/agent-vibes/unmute.md +45 -45
- package/.claude/commands/agent-vibes/verbosity.md +89 -89
- package/.claude/commands/agent-vibes/whoami.md +7 -7
- package/.claude/commands/agent-vibes-bmad-voices.md +117 -117
- package/.claude/commands/agent-vibes-rdp.md +24 -24
- package/.claude/config/agentvibes.json +1 -0
- package/.claude/config/audio-effects.cfg +2 -2
- package/.claude/config/audio-effects.cfg.sample +52 -52
- package/.claude/config/background-music-volume.txt +1 -0
- package/.claude/config/intro-text.txt +1 -0
- package/.claude/config/piper-speech-rate.txt +4 -0
- package/.claude/config/piper-target-speech-rate.txt +1 -0
- package/.claude/config/reverb-level.txt +1 -0
- package/.claude/config/tts-speech-rate.txt +4 -0
- package/.claude/config/tts-target-speech-rate.txt +1 -0
- package/.claude/docs/TERMUX_SETUP.md +408 -408
- package/.claude/github-star-reminder.txt +1 -1
- package/.claude/hooks/README-TTS-QUEUE.md +135 -135
- package/.claude/hooks/audio-cache-utils.sh +246 -246
- package/.claude/hooks/audio-processor.sh +433 -433
- package/.claude/hooks/background-music-manager.sh +404 -404
- package/.claude/hooks/bmad-speak-enhanced.sh +165 -165
- package/.claude/hooks/bmad-speak.sh +269 -269
- package/.claude/hooks/bmad-tts-injector.sh +568 -568
- package/.claude/hooks/bmad-voice-manager.sh +928 -928
- package/.claude/hooks/clawdbot-receiver-SECURE.sh +129 -129
- package/.claude/hooks/clawdbot-receiver.sh +107 -107
- package/.claude/hooks/clean-audio-cache.sh +22 -22
- package/.claude/hooks/cleanup-cache.sh +106 -106
- package/.claude/hooks/configure-rdp-mode.sh +137 -137
- package/.claude/hooks/download-extra-voices.sh +244 -244
- package/.claude/hooks/effects-manager.sh +268 -268
- package/.claude/hooks/github-star-reminder.sh +154 -154
- package/.claude/hooks/language-manager.sh +362 -362
- package/.claude/hooks/learn-manager.sh +492 -492
- package/.claude/hooks/macos-voice-manager.sh +205 -205
- package/.claude/hooks/migrate-background-music.sh +125 -125
- package/.claude/hooks/migrate-to-agentvibes.sh +161 -161
- package/.claude/hooks/optimize-background-music.sh +87 -87
- package/.claude/hooks/path-resolver.sh +60 -60
- package/.claude/hooks/personality-manager.sh +448 -448
- package/.claude/hooks/piper-download-voices.sh +225 -225
- package/.claude/hooks/piper-installer.sh +292 -292
- package/.claude/hooks/piper-multispeaker-registry.sh +171 -171
- package/.claude/hooks/piper-voice-manager.sh +24 -3
- package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +90 -90
- package/.claude/hooks/play-tts-enhanced.sh +105 -105
- package/.claude/hooks/play-tts-macos.sh +368 -368
- package/.claude/hooks/play-tts-piper.sh +679 -679
- package/.claude/hooks/play-tts-soprano.sh +356 -356
- package/.claude/hooks/play-tts-ssh-remote.sh +167 -167
- package/.claude/hooks/play-tts-termux-ssh.sh +169 -169
- package/.claude/hooks/play-tts.sh +301 -301
- package/.claude/hooks/prepare-release.sh +54 -54
- package/.claude/hooks/provider-commands.sh +617 -617
- package/.claude/hooks/provider-manager.sh +399 -399
- package/.claude/hooks/replay-target-audio.sh +95 -95
- package/.claude/hooks/requirements.txt +6 -6
- package/.claude/hooks/sentiment-manager.sh +201 -201
- package/.claude/hooks/session-start-tts.sh +81 -81
- package/.claude/hooks/soprano-gradio-synth.py +139 -139
- package/.claude/hooks/speed-manager.sh +291 -291
- package/.claude/hooks/stop-tts.sh +84 -84
- package/.claude/hooks/termux-installer.sh +261 -261
- package/.claude/hooks/translate-manager.sh +341 -341
- package/.claude/hooks/translator.py +237 -237
- package/.claude/hooks/tts-queue-worker.sh +145 -145
- package/.claude/hooks/tts-queue.sh +165 -165
- package/.claude/hooks/verbosity-manager.sh +178 -178
- package/.claude/hooks/voice-manager.sh +548 -548
- package/.claude/hooks-windows/audio-cache-utils.ps1 +119 -119
- package/.claude/hooks-windows/background-music-manager.ps1 +348 -0
- package/.claude/hooks-windows/clean-audio-cache.ps1 +53 -0
- package/.claude/hooks-windows/download-extra-voices.ps1 +185 -0
- package/.claude/hooks-windows/effects-manager.ps1 +294 -0
- package/.claude/hooks-windows/language-manager.ps1 +193 -0
- package/.claude/hooks-windows/learn-manager.ps1 +241 -0
- package/.claude/hooks-windows/personality-manager.ps1 +266 -0
- package/.claude/hooks-windows/play-tts-piper.ps1 +209 -0
- package/.claude/hooks-windows/play-tts-sapi.ps1 +108 -0
- package/.claude/hooks-windows/play-tts-soprano.ps1 +159 -158
- package/.claude/hooks-windows/play-tts-windows-piper.ps1 +50 -5
- package/.claude/hooks-windows/play-tts-windows-sapi.ps1 +108 -108
- package/.claude/hooks-windows/play-tts.ps1 +344 -266
- package/.claude/hooks-windows/provider-manager.ps1 +29 -10
- package/.claude/hooks-windows/session-start-tts.ps1 +124 -124
- package/.claude/hooks-windows/soprano-gradio-synth.py +153 -153
- package/.claude/hooks-windows/speed-manager.ps1 +166 -0
- package/.claude/hooks-windows/verbosity-manager.ps1 +119 -0
- package/.claude/hooks-windows/voice-manager-windows.ps1 +92 -8
- package/.claude/output-styles/agent-vibes.md +202 -202
- package/.claude/personalities/angry.md +14 -14
- package/.claude/personalities/annoying.md +14 -14
- package/.claude/personalities/crass.md +14 -14
- package/.claude/personalities/dramatic.md +14 -14
- package/.claude/personalities/dry-humor.md +50 -50
- package/.claude/personalities/flirty.md +20 -20
- package/.claude/personalities/funny.md +14 -14
- package/.claude/personalities/grandpa.md +32 -32
- package/.claude/personalities/millennial.md +14 -14
- package/.claude/personalities/moody.md +14 -14
- package/.claude/personalities/normal.md +16 -16
- package/.claude/personalities/pirate.md +14 -14
- package/.claude/personalities/poetic.md +14 -14
- package/.claude/personalities/professional.md +14 -14
- package/.claude/personalities/rapper.md +55 -55
- package/.claude/personalities/robot.md +14 -14
- package/.claude/personalities/sarcastic.md +38 -38
- package/.claude/personalities/sassy.md +14 -14
- package/.claude/personalities/surfer-dude.md +14 -14
- package/.claude/personalities/zen.md +14 -14
- package/.claude/settings.json +15 -15
- package/.claude/verbosity.txt +1 -1
- package/.clawdbot/README.md +105 -105
- package/.clawdbot/skill/SKILL.md +241 -241
- package/.mcp.json +12 -0
- package/CLAUDE.md +170 -170
- package/README.md +2029 -2007
- package/RELEASE_NOTES.md +1310 -1203
- package/WINDOWS-SETUP.md +208 -208
- package/bin/agent-vibes +39 -39
- package/bin/agentvibes-voice-browser.js +1840 -1840
- package/bin/agentvibes.js +48 -2
- package/bin/mcp-server.js +121 -121
- package/bin/mcp-server.sh +206 -206
- 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/WINDOWS_SETUP.md +260 -260
- package/mcp-server/docs/troubleshooting-audio.md +313 -313
- 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 +130 -130
- package/mcp-server/pyproject.toml +52 -52
- package/mcp-server/requirements.txt +2 -2
- package/mcp-server/server.py +1465 -1453
- package/mcp-server/test_server.py +395 -395
- package/mcp-server/test_windows_script_parity.py +336 -0
- package/package.json +110 -110
- package/setup-windows.ps1 +815 -815
- package/src/bmad-detector.js +71 -71
- package/src/cli/list-personalities.js +110 -110
- package/src/cli/list-voices.js +114 -114
- package/src/commands/bmad-voices.js +394 -394
- package/src/commands/install-mcp.js +476 -476
- package/src/console/app.js +824 -824
- package/src/console/audio-env.js +20 -1
- package/src/console/brand-colors.js +13 -13
- package/src/console/constants/personalities.js +44 -44
- package/src/console/footer-config.js +50 -50
- package/src/console/modals/modal-overlay.js +247 -247
- package/src/console/navigation.js +62 -62
- package/src/console/tabs/agents-tab.js +1684 -1516
- package/src/console/tabs/help-tab.js +261 -261
- package/src/console/tabs/install-tab.js +1007 -991
- package/src/console/tabs/music-tab.js +22 -8
- package/src/console/tabs/placeholder-tab.js +53 -53
- package/src/console/tabs/readme-tab.js +267 -267
- package/src/console/tabs/receiver-tab.js +1472 -1212
- package/src/console/tabs/settings-tab.js +208 -84
- package/src/console/tabs/voices-tab.js +100 -21
- package/src/console/widgets/destroy-list.js +25 -25
- package/src/console/widgets/format-utils.js +89 -89
- package/src/console/widgets/notice.js +55 -55
- package/src/console/widgets/personality-picker.js +185 -185
- package/src/console/widgets/reverb-picker.js +94 -94
- package/src/console/widgets/track-picker.js +285 -285
- package/src/installer/music-file-input.js +304 -304
- package/src/installer.js +5895 -5829
- package/src/services/agent-voice-store.js +423 -423
- package/src/services/config-service.js +264 -264
- package/src/services/navigation-service.js +123 -123
- package/src/services/provider-service.js +143 -132
- package/src/services/verbosity-service.js +157 -157
- package/src/utils/audio-duration-validator.js +298 -298
- package/src/utils/audio-format-validator.js +277 -277
- package/src/utils/dependency-checker.js +469 -466
- package/src/utils/file-ownership-verifier.js +358 -358
- package/src/utils/list-formatter.js +194 -194
- package/src/utils/music-file-validator.js +285 -285
- package/src/utils/preview-list-prompt.js +136 -136
- package/src/utils/provider-validator.js +96 -12
- package/src/utils/secure-music-storage.js +412 -412
- package/templates/agentvibes-receiver.sh +482 -482
- package/templates/audio/welcome-music.mp3 +0 -0
- package/voice-assignments.json +8244 -8244
- package/.claude/config/background-music-position.txt +0 -1
|
@@ -1,247 +1,247 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AgentVibes TUI Console — Modal Overlay Component
|
|
3
|
-
* Story 6.4: Modal Overlay Component
|
|
4
|
-
*
|
|
5
|
-
* Provides a reusable dim-overlay + centered-container modal for the TUI.
|
|
6
|
-
* Used by stories 7.7 (Personality), 7.8 (Voice), 7.9 (Music) selector modals.
|
|
7
|
-
*
|
|
8
|
-
* Integration with NavigationService:
|
|
9
|
-
* - openModal() sets isModalOpen() = true (blocks S/V/M/A/R/H/I/T shortcuts)
|
|
10
|
-
* - closeModal() restores modal-closed state
|
|
11
|
-
* - pushFocus/popFocus manage keyboard focus across open/close
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import blessed from 'blessed';
|
|
15
|
-
|
|
16
|
-
// Brand colours consistent with app.js and UX design plan
|
|
17
|
-
const COLORS = {
|
|
18
|
-
headerBg: '#1a237e',
|
|
19
|
-
contentBg: '#0a0e1a',
|
|
20
|
-
activeTab: '#3949ab',
|
|
21
|
-
textWhite: 'white',
|
|
22
|
-
overlayBg: '#000000',
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
export class ModalOverlay {
|
|
26
|
-
/**
|
|
27
|
-
* @param {object} screen - Blessed screen instance (or stub in tests)
|
|
28
|
-
* @param {NavigationService} nav - Navigation service for modal state management
|
|
29
|
-
* @param {object} [opts={}]
|
|
30
|
-
* @param {string} [opts.title='Modal'] - Default modal title
|
|
31
|
-
*/
|
|
32
|
-
constructor(screen, nav, opts = {}) {
|
|
33
|
-
this._screen = screen;
|
|
34
|
-
this._nav = nav;
|
|
35
|
-
this._onApply = null;
|
|
36
|
-
this._focusedButton = 'apply'; // M1: tracks which footer button has focus ('apply'|'cancel')
|
|
37
|
-
|
|
38
|
-
const title = opts.title ?? 'Modal';
|
|
39
|
-
this._title = title; // L1: stored for external access
|
|
40
|
-
|
|
41
|
-
// Detect test/stub environment: real blessed screens have a .program property.
|
|
42
|
-
// When using a stub screen (no .program), create lightweight object stubs
|
|
43
|
-
// instead of real blessed widgets to avoid "No active screen." errors.
|
|
44
|
-
const isTestMode = !screen.program;
|
|
45
|
-
|
|
46
|
-
if (isTestMode) {
|
|
47
|
-
this._overlay = { hidden: true };
|
|
48
|
-
this._container = { hidden: true, key: () => {}, focus: () => {} };
|
|
49
|
-
this._titleBar = { setContent: () => {} };
|
|
50
|
-
this._contentBox = { hidden: true };
|
|
51
|
-
this._footerBar = { hidden: true, setContent: () => {} }; // setContent needed by _updateFooter
|
|
52
|
-
} else {
|
|
53
|
-
// Full-screen dim overlay — sits beneath the modal container
|
|
54
|
-
this._overlay = blessed.box({
|
|
55
|
-
top: 0,
|
|
56
|
-
left: 0,
|
|
57
|
-
width: '100%',
|
|
58
|
-
height: '100%',
|
|
59
|
-
style: { bg: COLORS.overlayBg },
|
|
60
|
-
hidden: true,
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
// Centered modal container with border
|
|
64
|
-
this._container = blessed.box({
|
|
65
|
-
top: 'center',
|
|
66
|
-
left: 'center',
|
|
67
|
-
width: '60%',
|
|
68
|
-
height: '60%',
|
|
69
|
-
border: { type: 'line' },
|
|
70
|
-
style: {
|
|
71
|
-
fg: COLORS.textWhite,
|
|
72
|
-
bg: COLORS.contentBg,
|
|
73
|
-
border: { fg: COLORS.activeTab },
|
|
74
|
-
},
|
|
75
|
-
hidden: true,
|
|
76
|
-
keys: true,
|
|
77
|
-
mouse: true,
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
// Title bar — top row of container
|
|
81
|
-
this._titleBar = blessed.box({
|
|
82
|
-
parent: this._container,
|
|
83
|
-
top: 0,
|
|
84
|
-
left: 0,
|
|
85
|
-
width: '100%',
|
|
86
|
-
height: 1,
|
|
87
|
-
content: ` ${title}`,
|
|
88
|
-
tags: true,
|
|
89
|
-
style: {
|
|
90
|
-
fg: COLORS.textWhite,
|
|
91
|
-
bg: COLORS.headerBg,
|
|
92
|
-
},
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
// Scrollable content area — callers append their UI here via getContentBox()
|
|
96
|
-
this._contentBox = blessed.box({
|
|
97
|
-
parent: this._container,
|
|
98
|
-
top: 1,
|
|
99
|
-
left: 0,
|
|
100
|
-
width: '100%',
|
|
101
|
-
bottom: 2,
|
|
102
|
-
scrollable: true,
|
|
103
|
-
alwaysScroll: true,
|
|
104
|
-
keys: true,
|
|
105
|
-
style: {
|
|
106
|
-
fg: COLORS.textWhite,
|
|
107
|
-
bg: COLORS.contentBg,
|
|
108
|
-
},
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
// Footer strip with focusable Apply / Cancel buttons
|
|
112
|
-
this._footerBar = blessed.box({
|
|
113
|
-
parent: this._container,
|
|
114
|
-
bottom: 0,
|
|
115
|
-
left: 0,
|
|
116
|
-
width: '100%',
|
|
117
|
-
height: 2,
|
|
118
|
-
content: ' ► {bold}[Apply]{/bold} [Cancel]',
|
|
119
|
-
tags: true,
|
|
120
|
-
style: {
|
|
121
|
-
fg: COLORS.textWhite,
|
|
122
|
-
bg: COLORS.headerBg,
|
|
123
|
-
},
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
// Append to screen in z-order: overlay first (bottom), container on top
|
|
127
|
-
this._screen.append(this._overlay);
|
|
128
|
-
this._screen.append(this._container);
|
|
129
|
-
|
|
130
|
-
// M1: Tab cycles focus indicator between Apply and Cancel
|
|
131
|
-
this._container.key(['tab'], () => {
|
|
132
|
-
this._focusedButton = this._focusedButton === 'apply' ? 'cancel' : 'apply';
|
|
133
|
-
this._updateFooter();
|
|
134
|
-
this._screen.render();
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
// Enter activates the currently focused footer button (M1: checks _focusedButton)
|
|
138
|
-
this._container.key(['enter'], () => {
|
|
139
|
-
if (this._focusedButton === 'apply') {
|
|
140
|
-
this._handleApply();
|
|
141
|
-
} else {
|
|
142
|
-
this.close();
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
this._container.key(['escape'], () => this.close());
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// ---------------------------------------------------------------------------
|
|
151
|
-
// Public API
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Open the modal overlay.
|
|
155
|
-
* Shows the dim overlay + container, updates NavigationService modal state,
|
|
156
|
-
* pushes current focus to the focus stack, and renders the screen.
|
|
157
|
-
* @param {Function|null} [onApply] - Called when user activates Apply
|
|
158
|
-
*/
|
|
159
|
-
open(onApply = null) {
|
|
160
|
-
// H2: Guard against double-open — prevents stacked pushFocus calls
|
|
161
|
-
if (this._nav.isModalOpen()) return;
|
|
162
|
-
|
|
163
|
-
this._onApply = onApply;
|
|
164
|
-
this._focusedButton = 'apply'; // Reset focus to Apply on each open
|
|
165
|
-
this._overlay.hidden = false;
|
|
166
|
-
this._container.hidden = false;
|
|
167
|
-
|
|
168
|
-
this._nav.openModal(() => {
|
|
169
|
-
// Push current focus so we can restore on close
|
|
170
|
-
this._nav.pushFocus(this._screen.focused);
|
|
171
|
-
this._container.focus();
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
this._updateFooter();
|
|
175
|
-
this._screen.render();
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Close the modal overlay.
|
|
180
|
-
* Hides overlay + container, restores NavigationService modal state,
|
|
181
|
-
* pops saved focus, and renders the screen.
|
|
182
|
-
*
|
|
183
|
-
* Guard: uses widget-state (overlay.hidden) rather than isModalOpen().
|
|
184
|
-
* Reason: Blessed.js fires multiple Esc handlers in registration order.
|
|
185
|
-
* navigation.js registers its handler first and calls nav.closeModal()
|
|
186
|
-
* before app.js calls modal.close() — so isModalOpen() is already false
|
|
187
|
-
* when we arrive here, causing the old guard to return early and leave
|
|
188
|
-
* the overlay/container visible on screen.
|
|
189
|
-
*/
|
|
190
|
-
close() {
|
|
191
|
-
// H1: Widget-state guard — safe against dual-handler Esc ordering
|
|
192
|
-
if (this._overlay.hidden) return;
|
|
193
|
-
this._overlay.hidden = true;
|
|
194
|
-
this._container.hidden = true;
|
|
195
|
-
// H1: navigation.js may have already called closeModal() — only call if still open
|
|
196
|
-
if (this._nav.isModalOpen()) this._nav.closeModal();
|
|
197
|
-
const prev = this._nav.popFocus();
|
|
198
|
-
if (prev && typeof prev.focus === 'function') prev.focus();
|
|
199
|
-
this._screen.render();
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Returns the inner content box so callers (7.7-7.9) can append their UI.
|
|
204
|
-
* @returns {object} Blessed box (or stub in tests)
|
|
205
|
-
*/
|
|
206
|
-
getContentBox() {
|
|
207
|
-
return this._contentBox;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Update the modal title bar text.
|
|
212
|
-
* @param {string} text
|
|
213
|
-
*/
|
|
214
|
-
setTitle(text) {
|
|
215
|
-
this._titleBar.setContent(` ${text}`);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// ---------------------------------------------------------------------------
|
|
219
|
-
// Private
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* M1: Render the footer with a ► cursor on the currently focused button.
|
|
223
|
-
*/
|
|
224
|
-
_updateFooter() {
|
|
225
|
-
const applyLabel = this._focusedButton === 'apply' ? '► {bold}[Apply]{/bold}' : ' [Apply]';
|
|
226
|
-
const cancelLabel = this._focusedButton === 'cancel' ? '► {bold}[Cancel]{/bold}' : ' [Cancel]';
|
|
227
|
-
this._footerBar.setContent(` ${applyLabel} ${cancelLabel}`);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
_handleApply() {
|
|
231
|
-
if (typeof this._onApply === 'function') {
|
|
232
|
-
this._onApply();
|
|
233
|
-
}
|
|
234
|
-
this.close();
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Factory function for creating a ModalOverlay.
|
|
240
|
-
* @param {object} screen - Blessed screen
|
|
241
|
-
* @param {NavigationService} nav - Navigation service
|
|
242
|
-
* @param {object} [opts={}]
|
|
243
|
-
* @returns {ModalOverlay}
|
|
244
|
-
*/
|
|
245
|
-
export function createModalOverlay(screen, nav, opts = {}) {
|
|
246
|
-
return new ModalOverlay(screen, nav, opts);
|
|
247
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* AgentVibes TUI Console — Modal Overlay Component
|
|
3
|
+
* Story 6.4: Modal Overlay Component
|
|
4
|
+
*
|
|
5
|
+
* Provides a reusable dim-overlay + centered-container modal for the TUI.
|
|
6
|
+
* Used by stories 7.7 (Personality), 7.8 (Voice), 7.9 (Music) selector modals.
|
|
7
|
+
*
|
|
8
|
+
* Integration with NavigationService:
|
|
9
|
+
* - openModal() sets isModalOpen() = true (blocks S/V/M/A/R/H/I/T shortcuts)
|
|
10
|
+
* - closeModal() restores modal-closed state
|
|
11
|
+
* - pushFocus/popFocus manage keyboard focus across open/close
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import blessed from 'blessed';
|
|
15
|
+
|
|
16
|
+
// Brand colours consistent with app.js and UX design plan
|
|
17
|
+
const COLORS = {
|
|
18
|
+
headerBg: '#1a237e',
|
|
19
|
+
contentBg: '#0a0e1a',
|
|
20
|
+
activeTab: '#3949ab',
|
|
21
|
+
textWhite: 'white',
|
|
22
|
+
overlayBg: '#000000',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export class ModalOverlay {
|
|
26
|
+
/**
|
|
27
|
+
* @param {object} screen - Blessed screen instance (or stub in tests)
|
|
28
|
+
* @param {NavigationService} nav - Navigation service for modal state management
|
|
29
|
+
* @param {object} [opts={}]
|
|
30
|
+
* @param {string} [opts.title='Modal'] - Default modal title
|
|
31
|
+
*/
|
|
32
|
+
constructor(screen, nav, opts = {}) {
|
|
33
|
+
this._screen = screen;
|
|
34
|
+
this._nav = nav;
|
|
35
|
+
this._onApply = null;
|
|
36
|
+
this._focusedButton = 'apply'; // M1: tracks which footer button has focus ('apply'|'cancel')
|
|
37
|
+
|
|
38
|
+
const title = opts.title ?? 'Modal';
|
|
39
|
+
this._title = title; // L1: stored for external access
|
|
40
|
+
|
|
41
|
+
// Detect test/stub environment: real blessed screens have a .program property.
|
|
42
|
+
// When using a stub screen (no .program), create lightweight object stubs
|
|
43
|
+
// instead of real blessed widgets to avoid "No active screen." errors.
|
|
44
|
+
const isTestMode = !screen.program;
|
|
45
|
+
|
|
46
|
+
if (isTestMode) {
|
|
47
|
+
this._overlay = { hidden: true };
|
|
48
|
+
this._container = { hidden: true, key: () => {}, focus: () => {} };
|
|
49
|
+
this._titleBar = { setContent: () => {} };
|
|
50
|
+
this._contentBox = { hidden: true };
|
|
51
|
+
this._footerBar = { hidden: true, setContent: () => {} }; // setContent needed by _updateFooter
|
|
52
|
+
} else {
|
|
53
|
+
// Full-screen dim overlay — sits beneath the modal container
|
|
54
|
+
this._overlay = blessed.box({
|
|
55
|
+
top: 0,
|
|
56
|
+
left: 0,
|
|
57
|
+
width: '100%',
|
|
58
|
+
height: '100%',
|
|
59
|
+
style: { bg: COLORS.overlayBg },
|
|
60
|
+
hidden: true,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Centered modal container with border
|
|
64
|
+
this._container = blessed.box({
|
|
65
|
+
top: 'center',
|
|
66
|
+
left: 'center',
|
|
67
|
+
width: '60%',
|
|
68
|
+
height: '60%',
|
|
69
|
+
border: { type: 'line' },
|
|
70
|
+
style: {
|
|
71
|
+
fg: COLORS.textWhite,
|
|
72
|
+
bg: COLORS.contentBg,
|
|
73
|
+
border: { fg: COLORS.activeTab },
|
|
74
|
+
},
|
|
75
|
+
hidden: true,
|
|
76
|
+
keys: true,
|
|
77
|
+
mouse: true,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Title bar — top row of container
|
|
81
|
+
this._titleBar = blessed.box({
|
|
82
|
+
parent: this._container,
|
|
83
|
+
top: 0,
|
|
84
|
+
left: 0,
|
|
85
|
+
width: '100%',
|
|
86
|
+
height: 1,
|
|
87
|
+
content: ` ${title}`,
|
|
88
|
+
tags: true,
|
|
89
|
+
style: {
|
|
90
|
+
fg: COLORS.textWhite,
|
|
91
|
+
bg: COLORS.headerBg,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Scrollable content area — callers append their UI here via getContentBox()
|
|
96
|
+
this._contentBox = blessed.box({
|
|
97
|
+
parent: this._container,
|
|
98
|
+
top: 1,
|
|
99
|
+
left: 0,
|
|
100
|
+
width: '100%',
|
|
101
|
+
bottom: 2,
|
|
102
|
+
scrollable: true,
|
|
103
|
+
alwaysScroll: true,
|
|
104
|
+
keys: true,
|
|
105
|
+
style: {
|
|
106
|
+
fg: COLORS.textWhite,
|
|
107
|
+
bg: COLORS.contentBg,
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Footer strip with focusable Apply / Cancel buttons
|
|
112
|
+
this._footerBar = blessed.box({
|
|
113
|
+
parent: this._container,
|
|
114
|
+
bottom: 0,
|
|
115
|
+
left: 0,
|
|
116
|
+
width: '100%',
|
|
117
|
+
height: 2,
|
|
118
|
+
content: ' ► {bold}[Apply]{/bold} [Cancel]',
|
|
119
|
+
tags: true,
|
|
120
|
+
style: {
|
|
121
|
+
fg: COLORS.textWhite,
|
|
122
|
+
bg: COLORS.headerBg,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Append to screen in z-order: overlay first (bottom), container on top
|
|
127
|
+
this._screen.append(this._overlay);
|
|
128
|
+
this._screen.append(this._container);
|
|
129
|
+
|
|
130
|
+
// M1: Tab cycles focus indicator between Apply and Cancel
|
|
131
|
+
this._container.key(['tab'], () => {
|
|
132
|
+
this._focusedButton = this._focusedButton === 'apply' ? 'cancel' : 'apply';
|
|
133
|
+
this._updateFooter();
|
|
134
|
+
this._screen.render();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Enter activates the currently focused footer button (M1: checks _focusedButton)
|
|
138
|
+
this._container.key(['enter'], () => {
|
|
139
|
+
if (this._focusedButton === 'apply') {
|
|
140
|
+
this._handleApply();
|
|
141
|
+
} else {
|
|
142
|
+
this.close();
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
this._container.key(['escape'], () => this.close());
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
// Public API
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Open the modal overlay.
|
|
155
|
+
* Shows the dim overlay + container, updates NavigationService modal state,
|
|
156
|
+
* pushes current focus to the focus stack, and renders the screen.
|
|
157
|
+
* @param {Function|null} [onApply] - Called when user activates Apply
|
|
158
|
+
*/
|
|
159
|
+
open(onApply = null) {
|
|
160
|
+
// H2: Guard against double-open — prevents stacked pushFocus calls
|
|
161
|
+
if (this._nav.isModalOpen()) return;
|
|
162
|
+
|
|
163
|
+
this._onApply = onApply;
|
|
164
|
+
this._focusedButton = 'apply'; // Reset focus to Apply on each open
|
|
165
|
+
this._overlay.hidden = false;
|
|
166
|
+
this._container.hidden = false;
|
|
167
|
+
|
|
168
|
+
this._nav.openModal(() => {
|
|
169
|
+
// Push current focus so we can restore on close
|
|
170
|
+
this._nav.pushFocus(this._screen.focused);
|
|
171
|
+
this._container.focus();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
this._updateFooter();
|
|
175
|
+
this._screen.render();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Close the modal overlay.
|
|
180
|
+
* Hides overlay + container, restores NavigationService modal state,
|
|
181
|
+
* pops saved focus, and renders the screen.
|
|
182
|
+
*
|
|
183
|
+
* Guard: uses widget-state (overlay.hidden) rather than isModalOpen().
|
|
184
|
+
* Reason: Blessed.js fires multiple Esc handlers in registration order.
|
|
185
|
+
* navigation.js registers its handler first and calls nav.closeModal()
|
|
186
|
+
* before app.js calls modal.close() — so isModalOpen() is already false
|
|
187
|
+
* when we arrive here, causing the old guard to return early and leave
|
|
188
|
+
* the overlay/container visible on screen.
|
|
189
|
+
*/
|
|
190
|
+
close() {
|
|
191
|
+
// H1: Widget-state guard — safe against dual-handler Esc ordering
|
|
192
|
+
if (this._overlay.hidden) return;
|
|
193
|
+
this._overlay.hidden = true;
|
|
194
|
+
this._container.hidden = true;
|
|
195
|
+
// H1: navigation.js may have already called closeModal() — only call if still open
|
|
196
|
+
if (this._nav.isModalOpen()) this._nav.closeModal();
|
|
197
|
+
const prev = this._nav.popFocus();
|
|
198
|
+
if (prev && typeof prev.focus === 'function') prev.focus();
|
|
199
|
+
this._screen.render();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Returns the inner content box so callers (7.7-7.9) can append their UI.
|
|
204
|
+
* @returns {object} Blessed box (or stub in tests)
|
|
205
|
+
*/
|
|
206
|
+
getContentBox() {
|
|
207
|
+
return this._contentBox;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Update the modal title bar text.
|
|
212
|
+
* @param {string} text
|
|
213
|
+
*/
|
|
214
|
+
setTitle(text) {
|
|
215
|
+
this._titleBar.setContent(` ${text}`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
// Private
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* M1: Render the footer with a ► cursor on the currently focused button.
|
|
223
|
+
*/
|
|
224
|
+
_updateFooter() {
|
|
225
|
+
const applyLabel = this._focusedButton === 'apply' ? '► {bold}[Apply]{/bold}' : ' [Apply]';
|
|
226
|
+
const cancelLabel = this._focusedButton === 'cancel' ? '► {bold}[Cancel]{/bold}' : ' [Cancel]';
|
|
227
|
+
this._footerBar.setContent(` ${applyLabel} ${cancelLabel}`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
_handleApply() {
|
|
231
|
+
if (typeof this._onApply === 'function') {
|
|
232
|
+
this._onApply();
|
|
233
|
+
}
|
|
234
|
+
this.close();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Factory function for creating a ModalOverlay.
|
|
240
|
+
* @param {object} screen - Blessed screen
|
|
241
|
+
* @param {NavigationService} nav - Navigation service
|
|
242
|
+
* @param {object} [opts={}]
|
|
243
|
+
* @returns {ModalOverlay}
|
|
244
|
+
*/
|
|
245
|
+
export function createModalOverlay(screen, nav, opts = {}) {
|
|
246
|
+
return new ModalOverlay(screen, nav, opts);
|
|
247
|
+
}
|
|
@@ -1,62 +1,62 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AgentVibes TUI Console — Global Keyboard Navigation
|
|
3
|
-
* Story 6.2: Tab Bar & Global Keyboard Navigation
|
|
4
|
-
*
|
|
5
|
-
* Registers all global key bindings on the Blessed screen.
|
|
6
|
-
* Tab shortcuts (S/V/M/X/R/H/I) are blocked when a modal is open.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/** Map of key → tab ID for global tab shortcut keys */
|
|
10
|
-
const KEY_TO_TAB = {
|
|
11
|
-
's': 'settings', 'S': 'settings',
|
|
12
|
-
'v': 'voices', 'V': 'voices',
|
|
13
|
-
'm': 'music', 'M': 'music',
|
|
14
|
-
'b': 'agents', 'B': 'agents',
|
|
15
|
-
'x': 'receiver', 'X': 'receiver',
|
|
16
|
-
'r': 'readme', 'R': 'readme',
|
|
17
|
-
'h': 'help', 'H': 'help',
|
|
18
|
-
'i': 'install', 'I': 'install',
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Register all global keyboard navigation handlers on the Blessed screen.
|
|
23
|
-
*
|
|
24
|
-
* Handlers registered:
|
|
25
|
-
* S/V/M/A/R/H/I → switchTab (blocked when modal is open)
|
|
26
|
-
* Tab / T/t → cycleTab forward (blocked when modal is open)
|
|
27
|
-
* Shift+Tab → cycleTab backward (blocked when modal is open)
|
|
28
|
-
* Escape → closeModal (only when modal is open)
|
|
29
|
-
*
|
|
30
|
-
* Arrow keys (left/right) are intentionally NOT used for tab cycling —
|
|
31
|
-
* individual tabs use left/right for in-element navigation (e.g. row siblings).
|
|
32
|
-
*
|
|
33
|
-
* NOTE: Q / Ctrl+C are already registered in app.js (_registerHandlers).
|
|
34
|
-
* Do NOT re-register them here — that would stack duplicate quit handlers.
|
|
35
|
-
*
|
|
36
|
-
* @param {object} screen - Blessed screen instance (or stub in tests)
|
|
37
|
-
* @param {import('../services/navigation-service.js').NavigationService} navigationService
|
|
38
|
-
*/
|
|
39
|
-
export function setupNavigation(screen, navigationService) {
|
|
40
|
-
// Tab switching shortcuts — one handler per key (both cases)
|
|
41
|
-
for (const [key, tabId] of Object.entries(KEY_TO_TAB)) {
|
|
42
|
-
screen.key([key], () => {
|
|
43
|
-
if (!navigationService.isModalOpen()) {
|
|
44
|
-
navigationService.switchTab(tabId);
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// T → cycle to next tab (Tab itself is handled by the tab bar and footer only)
|
|
50
|
-
screen.key(['t', 'T'], () => {
|
|
51
|
-
if (!navigationService.isModalOpen()) {
|
|
52
|
-
navigationService.cycleTab();
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
// Escape — close modal (story 6.4 will expand modal handling)
|
|
57
|
-
screen.key(['escape'], () => {
|
|
58
|
-
if (navigationService.isModalOpen()) {
|
|
59
|
-
navigationService.closeModal();
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* AgentVibes TUI Console — Global Keyboard Navigation
|
|
3
|
+
* Story 6.2: Tab Bar & Global Keyboard Navigation
|
|
4
|
+
*
|
|
5
|
+
* Registers all global key bindings on the Blessed screen.
|
|
6
|
+
* Tab shortcuts (S/V/M/X/R/H/I) are blocked when a modal is open.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Map of key → tab ID for global tab shortcut keys */
|
|
10
|
+
const KEY_TO_TAB = {
|
|
11
|
+
's': 'settings', 'S': 'settings',
|
|
12
|
+
'v': 'voices', 'V': 'voices',
|
|
13
|
+
'm': 'music', 'M': 'music',
|
|
14
|
+
'b': 'agents', 'B': 'agents',
|
|
15
|
+
'x': 'receiver', 'X': 'receiver',
|
|
16
|
+
'r': 'readme', 'R': 'readme',
|
|
17
|
+
'h': 'help', 'H': 'help',
|
|
18
|
+
'i': 'install', 'I': 'install',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Register all global keyboard navigation handlers on the Blessed screen.
|
|
23
|
+
*
|
|
24
|
+
* Handlers registered:
|
|
25
|
+
* S/V/M/A/R/H/I → switchTab (blocked when modal is open)
|
|
26
|
+
* Tab / T/t → cycleTab forward (blocked when modal is open)
|
|
27
|
+
* Shift+Tab → cycleTab backward (blocked when modal is open)
|
|
28
|
+
* Escape → closeModal (only when modal is open)
|
|
29
|
+
*
|
|
30
|
+
* Arrow keys (left/right) are intentionally NOT used for tab cycling —
|
|
31
|
+
* individual tabs use left/right for in-element navigation (e.g. row siblings).
|
|
32
|
+
*
|
|
33
|
+
* NOTE: Q / Ctrl+C are already registered in app.js (_registerHandlers).
|
|
34
|
+
* Do NOT re-register them here — that would stack duplicate quit handlers.
|
|
35
|
+
*
|
|
36
|
+
* @param {object} screen - Blessed screen instance (or stub in tests)
|
|
37
|
+
* @param {import('../services/navigation-service.js').NavigationService} navigationService
|
|
38
|
+
*/
|
|
39
|
+
export function setupNavigation(screen, navigationService) {
|
|
40
|
+
// Tab switching shortcuts — one handler per key (both cases)
|
|
41
|
+
for (const [key, tabId] of Object.entries(KEY_TO_TAB)) {
|
|
42
|
+
screen.key([key], () => {
|
|
43
|
+
if (!navigationService.isModalOpen()) {
|
|
44
|
+
navigationService.switchTab(tabId);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// T → cycle to next tab (Tab itself is handled by the tab bar and footer only)
|
|
50
|
+
screen.key(['t', 'T'], () => {
|
|
51
|
+
if (!navigationService.isModalOpen()) {
|
|
52
|
+
navigationService.cycleTab();
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Escape — close modal (story 6.4 will expand modal handling)
|
|
57
|
+
screen.key(['escape'], () => {
|
|
58
|
+
if (navigationService.isModalOpen()) {
|
|
59
|
+
navigationService.closeModal();
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|