agentvibes 4.2.0 → 4.4.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.
Files changed (219) hide show
  1. package/.agentvibes/bmad/bmad-voices.md +69 -69
  2. package/.agentvibes/config.json +12 -0
  3. package/.claude/activation-instructions +54 -54
  4. package/.claude/audio/tracks/README.md +52 -52
  5. package/.claude/commands/agent-vibes/add.md +21 -21
  6. package/.claude/commands/agent-vibes/agent-vibes.md +101 -101
  7. package/.claude/commands/agent-vibes/agent.md +79 -79
  8. package/.claude/commands/agent-vibes/background-music.md +111 -111
  9. package/.claude/commands/agent-vibes/bmad.md +198 -198
  10. package/.claude/commands/agent-vibes/clean.md +18 -18
  11. package/.claude/commands/agent-vibes/cleanup.md +18 -18
  12. package/.claude/commands/agent-vibes/commands.json +145 -145
  13. package/.claude/commands/agent-vibes/effects.md +97 -97
  14. package/.claude/commands/agent-vibes/get.md +9 -9
  15. package/.claude/commands/agent-vibes/hide.md +91 -91
  16. package/.claude/commands/agent-vibes/language.md +23 -23
  17. package/.claude/commands/agent-vibes/learn.md +67 -67
  18. package/.claude/commands/agent-vibes/list.md +13 -13
  19. package/.claude/commands/agent-vibes/mute.md +37 -37
  20. package/.claude/commands/agent-vibes/preview.md +17 -17
  21. package/.claude/commands/agent-vibes/provider.md +68 -68
  22. package/.claude/commands/agent-vibes/replay-target.md +14 -14
  23. package/.claude/commands/agent-vibes/sample.md +12 -12
  24. package/.claude/commands/agent-vibes/set-favorite-voice.md +84 -84
  25. package/.claude/commands/agent-vibes/set-pretext.md +65 -65
  26. package/.claude/commands/agent-vibes/set-speed.md +41 -41
  27. package/.claude/commands/agent-vibes/show.md +84 -84
  28. package/.claude/commands/agent-vibes/switch.md +87 -87
  29. package/.claude/commands/agent-vibes/target-voice.md +26 -26
  30. package/.claude/commands/agent-vibes/target.md +30 -30
  31. package/.claude/commands/agent-vibes/translate.md +68 -68
  32. package/.claude/commands/agent-vibes/unmute.md +45 -45
  33. package/.claude/commands/agent-vibes/verbosity.md +89 -89
  34. package/.claude/commands/agent-vibes/whoami.md +7 -7
  35. package/.claude/commands/agent-vibes-bmad-voices.md +117 -117
  36. package/.claude/commands/agent-vibes-rdp.md +24 -24
  37. package/.claude/config/agentvibes.json +1 -0
  38. package/.claude/config/audio-effects.cfg +2 -2
  39. package/.claude/config/audio-effects.cfg.sample +52 -52
  40. package/.claude/config/background-music-volume.txt +1 -0
  41. package/.claude/config/intro-text.txt +1 -0
  42. package/.claude/config/piper-speech-rate.txt +4 -0
  43. package/.claude/config/piper-target-speech-rate.txt +1 -0
  44. package/.claude/config/reverb-level.txt +1 -0
  45. package/.claude/config/tts-speech-rate.txt +4 -0
  46. package/.claude/config/tts-target-speech-rate.txt +1 -0
  47. package/.claude/docs/TERMUX_SETUP.md +408 -408
  48. package/.claude/github-star-reminder.txt +1 -1
  49. package/.claude/hooks/README-TTS-QUEUE.md +135 -135
  50. package/.claude/hooks/audio-cache-utils.sh +246 -246
  51. package/.claude/hooks/audio-processor.sh +433 -433
  52. package/.claude/hooks/background-music-manager.sh +404 -404
  53. package/.claude/hooks/bmad-speak-enhanced.sh +165 -165
  54. package/.claude/hooks/bmad-speak.sh +269 -269
  55. package/.claude/hooks/bmad-tts-injector.sh +568 -568
  56. package/.claude/hooks/bmad-voice-manager.sh +928 -928
  57. package/.claude/hooks/clawdbot-receiver-SECURE.sh +129 -129
  58. package/.claude/hooks/clawdbot-receiver.sh +107 -107
  59. package/.claude/hooks/clean-audio-cache.sh +22 -22
  60. package/.claude/hooks/cleanup-cache.sh +106 -106
  61. package/.claude/hooks/configure-rdp-mode.sh +137 -137
  62. package/.claude/hooks/download-extra-voices.sh +244 -244
  63. package/.claude/hooks/effects-manager.sh +268 -268
  64. package/.claude/hooks/github-star-reminder.sh +154 -154
  65. package/.claude/hooks/language-manager.sh +362 -362
  66. package/.claude/hooks/learn-manager.sh +492 -492
  67. package/.claude/hooks/macos-voice-manager.sh +205 -205
  68. package/.claude/hooks/migrate-background-music.sh +125 -125
  69. package/.claude/hooks/migrate-to-agentvibes.sh +161 -161
  70. package/.claude/hooks/optimize-background-music.sh +87 -87
  71. package/.claude/hooks/path-resolver.sh +60 -60
  72. package/.claude/hooks/personality-manager.sh +448 -448
  73. package/.claude/hooks/piper-download-voices.sh +225 -225
  74. package/.claude/hooks/piper-installer.sh +292 -292
  75. package/.claude/hooks/piper-multispeaker-registry.sh +171 -171
  76. package/.claude/hooks/piper-voice-manager.sh +24 -3
  77. package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +90 -90
  78. package/.claude/hooks/play-tts-enhanced.sh +105 -105
  79. package/.claude/hooks/play-tts-macos.sh +368 -368
  80. package/.claude/hooks/play-tts-piper.sh +679 -679
  81. package/.claude/hooks/play-tts-soprano.sh +356 -356
  82. package/.claude/hooks/play-tts-ssh-remote.sh +167 -167
  83. package/.claude/hooks/play-tts-termux-ssh.sh +169 -169
  84. package/.claude/hooks/play-tts.sh +301 -301
  85. package/.claude/hooks/prepare-release.sh +54 -54
  86. package/.claude/hooks/provider-commands.sh +617 -617
  87. package/.claude/hooks/provider-manager.sh +399 -399
  88. package/.claude/hooks/replay-target-audio.sh +95 -95
  89. package/.claude/hooks/requirements.txt +6 -6
  90. package/.claude/hooks/sentiment-manager.sh +201 -201
  91. package/.claude/hooks/session-start-tts.sh +81 -81
  92. package/.claude/hooks/soprano-gradio-synth.py +139 -139
  93. package/.claude/hooks/speed-manager.sh +291 -291
  94. package/.claude/hooks/stop-tts.sh +84 -84
  95. package/.claude/hooks/termux-installer.sh +261 -261
  96. package/.claude/hooks/translate-manager.sh +341 -341
  97. package/.claude/hooks/translator.py +237 -237
  98. package/.claude/hooks/tts-queue-worker.sh +145 -145
  99. package/.claude/hooks/tts-queue.sh +165 -165
  100. package/.claude/hooks/verbosity-manager.sh +178 -178
  101. package/.claude/hooks/voice-manager.sh +548 -548
  102. package/.claude/hooks-windows/audio-cache-utils.ps1 +119 -119
  103. package/.claude/hooks-windows/background-music-manager.ps1 +348 -0
  104. package/.claude/hooks-windows/clean-audio-cache.ps1 +53 -0
  105. package/.claude/hooks-windows/download-extra-voices.ps1 +185 -0
  106. package/.claude/hooks-windows/effects-manager.ps1 +294 -0
  107. package/.claude/hooks-windows/language-manager.ps1 +193 -0
  108. package/.claude/hooks-windows/learn-manager.ps1 +241 -0
  109. package/.claude/hooks-windows/personality-manager.ps1 +266 -0
  110. package/.claude/hooks-windows/play-tts-piper.ps1 +209 -0
  111. package/.claude/hooks-windows/play-tts-sapi.ps1 +108 -0
  112. package/.claude/hooks-windows/play-tts-soprano.ps1 +159 -158
  113. package/.claude/hooks-windows/play-tts-windows-piper.ps1 +50 -5
  114. package/.claude/hooks-windows/play-tts-windows-sapi.ps1 +108 -108
  115. package/.claude/hooks-windows/play-tts.ps1 +344 -266
  116. package/.claude/hooks-windows/provider-manager.ps1 +29 -10
  117. package/.claude/hooks-windows/session-start-tts.ps1 +124 -124
  118. package/.claude/hooks-windows/soprano-gradio-synth.py +153 -153
  119. package/.claude/hooks-windows/speed-manager.ps1 +166 -0
  120. package/.claude/hooks-windows/verbosity-manager.ps1 +119 -0
  121. package/.claude/hooks-windows/voice-manager-windows.ps1 +92 -8
  122. package/.claude/output-styles/agent-vibes.md +202 -202
  123. package/.claude/personalities/angry.md +14 -14
  124. package/.claude/personalities/annoying.md +14 -14
  125. package/.claude/personalities/crass.md +14 -14
  126. package/.claude/personalities/dramatic.md +14 -14
  127. package/.claude/personalities/dry-humor.md +50 -50
  128. package/.claude/personalities/flirty.md +20 -20
  129. package/.claude/personalities/funny.md +14 -14
  130. package/.claude/personalities/grandpa.md +32 -32
  131. package/.claude/personalities/millennial.md +14 -14
  132. package/.claude/personalities/moody.md +14 -14
  133. package/.claude/personalities/normal.md +16 -16
  134. package/.claude/personalities/pirate.md +14 -14
  135. package/.claude/personalities/poetic.md +14 -14
  136. package/.claude/personalities/professional.md +14 -14
  137. package/.claude/personalities/rapper.md +55 -55
  138. package/.claude/personalities/robot.md +14 -14
  139. package/.claude/personalities/sarcastic.md +38 -38
  140. package/.claude/personalities/sassy.md +14 -14
  141. package/.claude/personalities/surfer-dude.md +14 -14
  142. package/.claude/personalities/zen.md +14 -14
  143. package/.claude/settings.json +15 -15
  144. package/.claude/verbosity.txt +1 -1
  145. package/.clawdbot/README.md +105 -105
  146. package/.clawdbot/skill/SKILL.md +241 -241
  147. package/.mcp.json +12 -0
  148. package/CLAUDE.md +170 -170
  149. package/README.md +2029 -2007
  150. package/RELEASE_NOTES.md +1310 -1203
  151. package/WINDOWS-SETUP.md +208 -208
  152. package/bin/agent-vibes +39 -39
  153. package/bin/agentvibes-voice-browser.js +1840 -1840
  154. package/bin/agentvibes.js +48 -2
  155. package/bin/mcp-server.js +121 -121
  156. package/bin/mcp-server.sh +206 -206
  157. package/bin/test-bmad-pr +78 -78
  158. package/mcp-server/QUICK_START.md +203 -203
  159. package/mcp-server/README.md +345 -345
  160. package/mcp-server/WINDOWS_SETUP.md +260 -260
  161. package/mcp-server/docs/troubleshooting-audio.md +313 -313
  162. package/mcp-server/examples/claude_desktop_config.json +11 -11
  163. package/mcp-server/examples/claude_desktop_config_piper.json +9 -9
  164. package/mcp-server/examples/custom_instructions.md +169 -169
  165. package/mcp-server/install-deps.js +130 -130
  166. package/mcp-server/pyproject.toml +52 -52
  167. package/mcp-server/requirements.txt +2 -2
  168. package/mcp-server/server.py +1465 -1453
  169. package/mcp-server/test_server.py +395 -395
  170. package/mcp-server/test_windows_script_parity.py +336 -0
  171. package/package.json +110 -110
  172. package/setup-windows.ps1 +815 -815
  173. package/src/bmad-detector.js +71 -71
  174. package/src/cli/list-personalities.js +110 -110
  175. package/src/cli/list-voices.js +114 -114
  176. package/src/commands/bmad-voices.js +394 -394
  177. package/src/commands/install-mcp.js +476 -476
  178. package/src/console/app.js +824 -824
  179. package/src/console/audio-env.js +20 -1
  180. package/src/console/brand-colors.js +13 -13
  181. package/src/console/constants/personalities.js +44 -44
  182. package/src/console/footer-config.js +50 -50
  183. package/src/console/modals/modal-overlay.js +247 -247
  184. package/src/console/navigation.js +62 -62
  185. package/src/console/tabs/agents-tab.js +1684 -1516
  186. package/src/console/tabs/help-tab.js +261 -261
  187. package/src/console/tabs/install-tab.js +1007 -991
  188. package/src/console/tabs/music-tab.js +22 -8
  189. package/src/console/tabs/placeholder-tab.js +53 -53
  190. package/src/console/tabs/readme-tab.js +267 -267
  191. package/src/console/tabs/receiver-tab.js +1472 -1212
  192. package/src/console/tabs/settings-tab.js +152 -79
  193. package/src/console/tabs/voices-tab.js +100 -21
  194. package/src/console/widgets/destroy-list.js +25 -25
  195. package/src/console/widgets/format-utils.js +89 -89
  196. package/src/console/widgets/notice.js +55 -55
  197. package/src/console/widgets/personality-picker.js +185 -185
  198. package/src/console/widgets/reverb-picker.js +94 -94
  199. package/src/console/widgets/track-picker.js +285 -285
  200. package/src/installer/music-file-input.js +304 -304
  201. package/src/installer.js +5882 -5829
  202. package/src/services/agent-voice-store.js +423 -423
  203. package/src/services/config-service.js +264 -264
  204. package/src/services/navigation-service.js +123 -123
  205. package/src/services/provider-service.js +132 -132
  206. package/src/services/verbosity-service.js +157 -157
  207. package/src/utils/audio-duration-validator.js +298 -298
  208. package/src/utils/audio-format-validator.js +277 -277
  209. package/src/utils/dependency-checker.js +469 -466
  210. package/src/utils/file-ownership-verifier.js +358 -358
  211. package/src/utils/list-formatter.js +194 -194
  212. package/src/utils/music-file-validator.js +285 -285
  213. package/src/utils/preview-list-prompt.js +136 -136
  214. package/src/utils/provider-validator.js +96 -12
  215. package/src/utils/secure-music-storage.js +412 -412
  216. package/templates/agentvibes-receiver.sh +482 -482
  217. package/templates/audio/welcome-music.mp3 +0 -0
  218. package/voice-assignments.json +8244 -8244
  219. 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
+ }