opencode-smart-voice-notify 1.0.13 → 1.1.2
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/README.md +257 -101
- package/example.config.jsonc +57 -0
- package/index.js +304 -22
- package/package.json +1 -1
- package/util/config.js +57 -0
- package/util/tts.js +41 -0
package/README.md
CHANGED
|
@@ -33,6 +33,7 @@ The plugin automatically tries multiple TTS engines in order, falling back if on
|
|
|
33
33
|
- Per-notification type delays (permission requests are more urgent)
|
|
34
34
|
- **Smart Quota Handling**: Automatically falls back to free Edge TTS if ElevenLabs quota is exceeded
|
|
35
35
|
- **Permission Batching**: Multiple simultaneous permission requests are batched into a single notification (e.g., "5 permission requests require your attention")
|
|
36
|
+
- **Question Tool Support** (SDK v1.1.7+): Notifies when the agent asks questions and needs user input
|
|
36
37
|
|
|
37
38
|
### System Integration
|
|
38
39
|
- **Native Edge TTS**: No external dependencies (Python/pip) required
|
|
@@ -95,86 +96,153 @@ The auto-generated configuration includes all advanced settings, message arrays,
|
|
|
95
96
|
|
|
96
97
|
If you prefer to create the config manually, add a `smart-voice-notify.jsonc` file in your OpenCode config directory (`~/.config/opencode/`):
|
|
97
98
|
|
|
98
|
-
```jsonc
|
|
99
|
-
{
|
|
100
|
-
// ============================================================
|
|
101
|
-
//
|
|
102
|
-
// ============================================================
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
99
|
+
```jsonc
|
|
100
|
+
{
|
|
101
|
+
// ============================================================
|
|
102
|
+
// OpenCode Smart Voice Notify - Full Configuration Reference
|
|
103
|
+
// ============================================================
|
|
104
|
+
//
|
|
105
|
+
// IMPORTANT: This is a REFERENCE file showing ALL available options.
|
|
106
|
+
//
|
|
107
|
+
// To use this plugin:
|
|
108
|
+
// 1. Copy this file to: ~/.config/opencode/smart-voice-notify.jsonc
|
|
109
|
+
// (On Windows: C:\Users\<YourUser>\.config\opencode\smart-voice-notify.jsonc)
|
|
110
|
+
// 2. Customize the settings below to your preference
|
|
111
|
+
// 3. The plugin auto-creates a minimal config if none exists
|
|
112
|
+
//
|
|
113
|
+
// Sound files are automatically copied to ~/.config/opencode/assets/
|
|
114
|
+
// on first run. You can also use your own custom sound files.
|
|
115
|
+
//
|
|
116
|
+
// ============================================================
|
|
117
|
+
|
|
118
|
+
// ============================================================
|
|
119
|
+
// NOTIFICATION MODE SETTINGS (Smart Notification System)
|
|
120
|
+
// ============================================================
|
|
121
|
+
// Controls how notifications are delivered:
|
|
122
|
+
// 'sound-first' - Play sound immediately, TTS reminder after delay (RECOMMENDED)
|
|
123
|
+
// 'tts-first' - Speak TTS immediately, no sound
|
|
124
|
+
// 'both' - Play sound AND speak TTS immediately
|
|
125
|
+
// 'sound-only' - Only play sound, no TTS at all
|
|
126
|
+
"notificationMode": "sound-first",
|
|
127
|
+
|
|
128
|
+
// ============================================================
|
|
129
|
+
// TTS REMINDER SETTINGS (When user doesn't respond to sound)
|
|
130
|
+
// ============================================================
|
|
131
|
+
|
|
132
|
+
// Enable TTS reminder if user doesn't respond after sound notification
|
|
133
|
+
"enableTTSReminder": true,
|
|
134
|
+
|
|
135
|
+
// Delay (in seconds) before TTS reminder fires
|
|
136
|
+
// Set globally or per-notification type
|
|
137
|
+
"ttsReminderDelaySeconds": 30, // Global default
|
|
138
|
+
"idleReminderDelaySeconds": 30, // For task completion notifications
|
|
139
|
+
"permissionReminderDelaySeconds": 20, // For permission requests (more urgent)
|
|
140
|
+
|
|
122
141
|
// Follow-up reminders if user STILL doesn't respond after first TTS
|
|
123
142
|
"enableFollowUpReminders": true,
|
|
124
143
|
"maxFollowUpReminders": 3, // Max number of follow-up TTS reminders
|
|
125
144
|
"reminderBackoffMultiplier": 1.5, // Each follow-up waits longer (30s, 45s, 67s...)
|
|
126
|
-
|
|
145
|
+
|
|
127
146
|
// ============================================================
|
|
128
147
|
// PERMISSION BATCHING (Multiple permissions at once)
|
|
129
148
|
// ============================================================
|
|
130
|
-
// When multiple permissions arrive simultaneously
|
|
131
|
-
|
|
132
|
-
|
|
149
|
+
// When multiple permissions arrive simultaneously (e.g., 5 at once),
|
|
150
|
+
// batch them into a single notification instead of playing 5 overlapping sounds.
|
|
151
|
+
// The notification will say "X permission requests require your attention".
|
|
152
|
+
|
|
153
|
+
// Batch window (ms) - how long to wait for more permissions before notifying
|
|
154
|
+
"permissionBatchWindowMs": 800,
|
|
155
|
+
|
|
133
156
|
// ============================================================
|
|
134
157
|
// TTS ENGINE SELECTION
|
|
135
158
|
// ============================================================
|
|
136
|
-
// 'elevenlabs' - Best quality, anime-like voices (requires API key)
|
|
159
|
+
// 'elevenlabs' - Best quality, anime-like voices (requires API key, free tier: 10k chars/month)
|
|
137
160
|
// 'edge' - Good quality neural voices (Free, Native Node.js implementation)
|
|
138
|
-
// 'sapi' - Windows built-in voices (free, offline)
|
|
139
|
-
"ttsEngine": "
|
|
161
|
+
// 'sapi' - Windows built-in voices (free, offline, robotic)
|
|
162
|
+
"ttsEngine": "elevenlabs",
|
|
163
|
+
|
|
164
|
+
// Enable TTS for notifications (falls back to sound files if TTS fails)
|
|
140
165
|
"enableTTS": true,
|
|
141
|
-
|
|
142
|
-
// ============================================================
|
|
143
|
-
// ELEVENLABS SETTINGS (Best Quality - Anime-like Voices)
|
|
144
|
-
// ============================================================
|
|
145
|
-
// Get your API key from: https://elevenlabs.io/app/settings/api-keys
|
|
146
|
-
//
|
|
147
|
-
"
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
"
|
|
159
|
-
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
"
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
166
|
+
|
|
167
|
+
// ============================================================
|
|
168
|
+
// ELEVENLABS SETTINGS (Best Quality - Anime-like Voices)
|
|
169
|
+
// ============================================================
|
|
170
|
+
// Get your API key from: https://elevenlabs.io/app/settings/api-keys
|
|
171
|
+
// Free tier: 10,000 characters/month
|
|
172
|
+
"elevenLabsApiKey": "YOUR_API_KEY_HERE",
|
|
173
|
+
|
|
174
|
+
// Voice ID - Recommended cute/anime-like voices:
|
|
175
|
+
// 'cgSgspJ2msm6clMCkdW9' - Jessica (Playful, Bright, Warm) - RECOMMENDED
|
|
176
|
+
// 'FGY2WhTYpPnrIDTdsKH5' - Laura (Enthusiast, Quirky)
|
|
177
|
+
// 'jsCqWAovK2LkecY7zXl4' - Freya (Expressive, Confident)
|
|
178
|
+
// 'EXAVITQu4vr4xnSDxMaL' - Sarah (Soft, Warm)
|
|
179
|
+
// Browse more at: https://elevenlabs.io/voice-library
|
|
180
|
+
"elevenLabsVoiceId": "cgSgspJ2msm6clMCkdW9",
|
|
181
|
+
|
|
182
|
+
// Model: 'eleven_turbo_v2_5' (fast, good), 'eleven_multilingual_v2' (highest quality)
|
|
183
|
+
"elevenLabsModel": "eleven_turbo_v2_5",
|
|
184
|
+
|
|
185
|
+
// Voice tuning (0.0 to 1.0)
|
|
186
|
+
"elevenLabsStability": 0.5, // Lower = more expressive, Higher = more consistent
|
|
187
|
+
"elevenLabsSimilarity": 0.75, // How closely to match the original voice
|
|
188
|
+
"elevenLabsStyle": 0.5, // Style exaggeration (higher = more expressive)
|
|
189
|
+
|
|
190
|
+
// ============================================================
|
|
191
|
+
// EDGE TTS SETTINGS (Free Neural Voices - Fallback)
|
|
192
|
+
// ============================================================
|
|
193
|
+
// Native Node.js implementation (No external dependencies)
|
|
194
|
+
|
|
195
|
+
// Voice options (run 'edge-tts --list-voices' to see all):
|
|
196
|
+
// 'en-US-AnaNeural' - Young, cute, cartoon-like (RECOMMENDED)
|
|
197
|
+
// 'en-US-JennyNeural' - Friendly, warm
|
|
198
|
+
// 'en-US-AriaNeural' - Confident, clear
|
|
199
|
+
// 'en-GB-SoniaNeural' - British, friendly
|
|
200
|
+
// 'en-AU-NatashaNeural' - Australian, warm
|
|
201
|
+
"edgeVoice": "en-US-AnaNeural",
|
|
202
|
+
|
|
203
|
+
// Pitch adjustment: +0Hz to +100Hz (higher = more anime-like)
|
|
204
|
+
"edgePitch": "+50Hz",
|
|
205
|
+
|
|
206
|
+
// Speech rate: -50% to +100%
|
|
207
|
+
"edgeRate": "+10%",
|
|
208
|
+
|
|
209
|
+
// ============================================================
|
|
210
|
+
// SAPI SETTINGS (Windows Built-in - Last Resort Fallback)
|
|
211
|
+
// ============================================================
|
|
212
|
+
|
|
213
|
+
// Voice (run PowerShell to list all installed voices):
|
|
214
|
+
// Add-Type -AssemblyName System.Speech; (New-Object System.Speech.Synthesis.SpeechSynthesizer).GetInstalledVoices() | % { $_.VoiceInfo.Name }
|
|
215
|
+
//
|
|
216
|
+
// Common Windows voices:
|
|
217
|
+
// 'Microsoft Zira Desktop' - Female, US English
|
|
218
|
+
// 'Microsoft David Desktop' - Male, US English
|
|
219
|
+
// 'Microsoft Hazel Desktop' - Female, UK English
|
|
220
|
+
"sapiVoice": "Microsoft Zira Desktop",
|
|
221
|
+
|
|
222
|
+
// Speech rate: -10 (slowest) to +10 (fastest), 0 is normal
|
|
223
|
+
"sapiRate": -1,
|
|
224
|
+
|
|
225
|
+
// Pitch: 'x-low', 'low', 'medium', 'high', 'x-high'
|
|
226
|
+
"sapiPitch": "medium",
|
|
227
|
+
|
|
228
|
+
// Volume: 'silent', 'x-soft', 'soft', 'medium', 'loud', 'x-loud'
|
|
229
|
+
"sapiVolume": "loud",
|
|
230
|
+
|
|
231
|
+
// ============================================================
|
|
232
|
+
// INITIAL TTS MESSAGES (Used immediately or after sound)
|
|
233
|
+
// These are randomly selected each time for variety
|
|
234
|
+
// ============================================================
|
|
235
|
+
|
|
236
|
+
// Messages when agent finishes work (task completion)
|
|
237
|
+
"idleTTSMessages": [
|
|
238
|
+
"All done! Your task has been completed successfully.",
|
|
239
|
+
"Hey there! I finished working on your request.",
|
|
240
|
+
"Task complete! Ready for your review whenever you are.",
|
|
241
|
+
"Good news! Everything is done and ready for you.",
|
|
242
|
+
"Finished! Let me know if you need anything else."
|
|
243
|
+
],
|
|
244
|
+
|
|
245
|
+
// Messages for permission requests
|
|
178
246
|
"permissionTTSMessages": [
|
|
179
247
|
"Attention please! I need your permission to continue.",
|
|
180
248
|
"Hey! Quick approval needed to proceed with the task.",
|
|
@@ -182,22 +250,32 @@ If you prefer to create the config manually, add a `smart-voice-notify.jsonc` fi
|
|
|
182
250
|
"Excuse me! I need your authorization before I can continue.",
|
|
183
251
|
"Permission required! Please review and approve when ready."
|
|
184
252
|
],
|
|
253
|
+
|
|
185
254
|
// Messages for MULTIPLE permission requests (use {count} placeholder)
|
|
255
|
+
// Used when several permissions arrive simultaneously
|
|
186
256
|
"permissionTTSMessagesMultiple": [
|
|
187
257
|
"Attention please! There are {count} permission requests waiting for your approval.",
|
|
188
|
-
"Hey! {count} permissions need your approval to continue."
|
|
258
|
+
"Hey! {count} permissions need your approval to continue.",
|
|
259
|
+
"Heads up! You have {count} pending permission requests.",
|
|
260
|
+
"Excuse me! I need your authorization for {count} different actions.",
|
|
261
|
+
"{count} permissions required! Please review and approve when ready."
|
|
189
262
|
],
|
|
190
|
-
|
|
191
|
-
// ============================================================
|
|
192
|
-
// TTS REMINDER MESSAGES (
|
|
193
|
-
//
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
"
|
|
199
|
-
"
|
|
200
|
-
|
|
263
|
+
|
|
264
|
+
// ============================================================
|
|
265
|
+
// TTS REMINDER MESSAGES (More urgent - used after delay if no response)
|
|
266
|
+
// These are more personalized and urgent to get user attention
|
|
267
|
+
// ============================================================
|
|
268
|
+
|
|
269
|
+
// Reminder messages when agent finished but user hasn't responded
|
|
270
|
+
"idleReminderTTSMessages": [
|
|
271
|
+
"Hey, are you still there? Your task has been waiting for review.",
|
|
272
|
+
"Just a gentle reminder - I finished your request a while ago!",
|
|
273
|
+
"Hello? I completed your task. Please take a look when you can.",
|
|
274
|
+
"Still waiting for you! The work is done and ready for review.",
|
|
275
|
+
"Knock knock! Your completed task is patiently waiting for you."
|
|
276
|
+
],
|
|
277
|
+
|
|
278
|
+
// Reminder messages when permission still needed
|
|
201
279
|
"permissionReminderTTSMessages": [
|
|
202
280
|
"Hey! I still need your permission to continue. Please respond!",
|
|
203
281
|
"Reminder: There is a pending permission request. I cannot proceed without you.",
|
|
@@ -205,30 +283,105 @@ If you prefer to create the config manually, add a `smart-voice-notify.jsonc` fi
|
|
|
205
283
|
"Please check your screen! I really need your permission to move forward.",
|
|
206
284
|
"Still waiting for authorization! The task is on hold until you respond."
|
|
207
285
|
],
|
|
286
|
+
|
|
208
287
|
// Reminder messages for MULTIPLE permissions (use {count} placeholder)
|
|
209
288
|
"permissionReminderTTSMessagesMultiple": [
|
|
210
289
|
"Hey! I still need your approval for {count} permissions. Please respond!",
|
|
211
|
-
"Reminder: There are {count} pending permission requests."
|
|
290
|
+
"Reminder: There are {count} pending permission requests. I cannot proceed without you.",
|
|
291
|
+
"Hello? I am waiting for your approval on {count} items. This is getting urgent!",
|
|
292
|
+
"Please check your screen! {count} permissions are waiting for your response.",
|
|
293
|
+
"Still waiting for authorization on {count} requests! The task is on hold."
|
|
212
294
|
],
|
|
213
|
-
|
|
214
|
-
// ============================================================
|
|
215
|
-
//
|
|
216
|
-
// ============================================================
|
|
217
|
-
"
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
//
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
|
|
295
|
+
|
|
296
|
+
// ============================================================
|
|
297
|
+
// QUESTION TOOL MESSAGES (SDK v1.1.7+ - Agent asking user questions)
|
|
298
|
+
// ============================================================
|
|
299
|
+
// The "question" tool allows the LLM to ask users questions during execution.
|
|
300
|
+
// This is useful for gathering preferences, clarifying instructions, or getting
|
|
301
|
+
// decisions on implementation choices.
|
|
302
|
+
|
|
303
|
+
// Messages when agent asks user a question
|
|
304
|
+
"questionTTSMessages": [
|
|
305
|
+
"Hey! I have a question for you. Please check your screen.",
|
|
306
|
+
"Attention! I need your input to continue.",
|
|
307
|
+
"Quick question! Please take a look when you have a moment.",
|
|
308
|
+
"I need some clarification. Could you please respond?",
|
|
309
|
+
"Question time! Your input is needed to proceed."
|
|
310
|
+
],
|
|
311
|
+
|
|
312
|
+
// Messages for MULTIPLE questions (use {count} placeholder)
|
|
313
|
+
"questionTTSMessagesMultiple": [
|
|
314
|
+
"Hey! I have {count} questions for you. Please check your screen.",
|
|
315
|
+
"Attention! I need your input on {count} items to continue.",
|
|
316
|
+
"{count} questions need your attention. Please take a look!",
|
|
317
|
+
"I need some clarifications. There are {count} questions waiting for you.",
|
|
318
|
+
"Question time! {count} questions need your response to proceed."
|
|
319
|
+
],
|
|
320
|
+
|
|
321
|
+
// Reminder messages for questions (more urgent - used after delay)
|
|
322
|
+
"questionReminderTTSMessages": [
|
|
323
|
+
"Hey! I am still waiting for your answer. Please check the questions!",
|
|
324
|
+
"Reminder: There is a question waiting for your response.",
|
|
325
|
+
"Hello? I need your input to continue. Please respond when you can.",
|
|
326
|
+
"Still waiting for your answer! The task is on hold.",
|
|
327
|
+
"Your input is needed! Please check the pending question."
|
|
328
|
+
],
|
|
329
|
+
|
|
330
|
+
// Reminder messages for MULTIPLE questions (use {count} placeholder)
|
|
331
|
+
"questionReminderTTSMessagesMultiple": [
|
|
332
|
+
"Hey! I am still waiting for answers to {count} questions. Please respond!",
|
|
333
|
+
"Reminder: There are {count} questions waiting for your response.",
|
|
334
|
+
"Hello? I need your input on {count} items. Please respond when you can.",
|
|
335
|
+
"Still waiting for your answers on {count} questions! The task is on hold.",
|
|
336
|
+
"Your input is needed! {count} questions are pending your response."
|
|
337
|
+
],
|
|
338
|
+
|
|
339
|
+
// Delay (in seconds) before question reminder fires
|
|
340
|
+
"questionReminderDelaySeconds": 25,
|
|
341
|
+
|
|
342
|
+
// Question batch window (ms) - how long to wait for more questions before notifying
|
|
343
|
+
"questionBatchWindowMs": 800,
|
|
344
|
+
|
|
345
|
+
// ============================================================
|
|
346
|
+
// SOUND FILES (For immediate notifications)
|
|
347
|
+
// These are played first before TTS reminder kicks in
|
|
348
|
+
// ============================================================
|
|
349
|
+
// Paths are relative to ~/.config/opencode/ directory
|
|
350
|
+
// The plugin automatically copies bundled sounds to assets/ on first run
|
|
351
|
+
// You can replace with your own custom MP3/WAV files
|
|
352
|
+
|
|
353
|
+
"idleSound": "assets/Soft-high-tech-notification-sound-effect.mp3",
|
|
354
|
+
"permissionSound": "assets/Machine-alert-beep-sound-effect.mp3",
|
|
355
|
+
"questionSound": "assets/Machine-alert-beep-sound-effect.mp3",
|
|
356
|
+
|
|
357
|
+
// ============================================================
|
|
358
|
+
// GENERAL SETTINGS
|
|
359
|
+
// ============================================================
|
|
360
|
+
|
|
361
|
+
// Wake monitor from sleep when notifying (Windows/macOS)
|
|
362
|
+
"wakeMonitor": true,
|
|
363
|
+
|
|
364
|
+
// Force system volume up if below threshold
|
|
365
|
+
"forceVolume": true,
|
|
366
|
+
|
|
367
|
+
// Volume threshold (0-100): force volume if current level is below this
|
|
368
|
+
"volumeThreshold": 50,
|
|
369
|
+
|
|
370
|
+
// Show TUI toast notifications in OpenCode terminal
|
|
371
|
+
"enableToast": true,
|
|
372
|
+
|
|
373
|
+
// Enable audio notifications (sound files and TTS)
|
|
374
|
+
"enableSound": true,
|
|
375
|
+
|
|
376
|
+
// Consider monitor asleep after this many seconds of inactivity (Windows only)
|
|
377
|
+
"idleThresholdSeconds": 60,
|
|
378
|
+
|
|
379
|
+
// Enable debug logging to ~/.config/opencode/logs/smart-voice-notify-debug.log
|
|
380
|
+
// The logs folder is created automatically when debug logging is enabled
|
|
381
|
+
// Useful for troubleshooting notification issues
|
|
382
|
+
"debugLog": false
|
|
383
|
+
}
|
|
384
|
+
```
|
|
232
385
|
|
|
233
386
|
See `example.config.jsonc` for more details.
|
|
234
387
|
|
|
@@ -257,10 +410,13 @@ See `example.config.jsonc` for more details.
|
|
|
257
410
|
| `permission.asked` | Permission request (SDK v1.1.1+) - alert user |
|
|
258
411
|
| `permission.updated` | Permission request (SDK v1.0.x) - alert user |
|
|
259
412
|
| `permission.replied` | User responded - cancel pending reminders |
|
|
413
|
+
| `question.asked` | Agent asks question (SDK v1.1.7+) - notify user |
|
|
414
|
+
| `question.replied` | User answered question - cancel pending reminders |
|
|
415
|
+
| `question.rejected` | User dismissed question - cancel pending reminders |
|
|
260
416
|
| `message.updated` | New user message - cancel pending reminders |
|
|
261
417
|
| `session.created` | New session - reset state |
|
|
262
418
|
|
|
263
|
-
> **Note**: The plugin supports
|
|
419
|
+
> **Note**: The plugin supports OpenCode SDK v1.0.x, v1.1.x, and v1.1.7+ for backward compatibility.
|
|
264
420
|
|
|
265
421
|
## Development
|
|
266
422
|
|
package/example.config.jsonc
CHANGED
|
@@ -16,6 +16,13 @@
|
|
|
16
16
|
//
|
|
17
17
|
// ============================================================
|
|
18
18
|
|
|
19
|
+
// ============================================================
|
|
20
|
+
// PLUGIN ENABLE/DISABLE
|
|
21
|
+
// ============================================================
|
|
22
|
+
// Master switch to enable or disable the entire plugin.
|
|
23
|
+
// Set to false to disable all notifications without uninstalling.
|
|
24
|
+
"enabled": true,
|
|
25
|
+
|
|
19
26
|
// ============================================================
|
|
20
27
|
// NOTIFICATION MODE SETTINGS (Smart Notification System)
|
|
21
28
|
// ============================================================
|
|
@@ -194,6 +201,55 @@
|
|
|
194
201
|
"Still waiting for authorization on {count} requests! The task is on hold."
|
|
195
202
|
],
|
|
196
203
|
|
|
204
|
+
// ============================================================
|
|
205
|
+
// QUESTION TOOL MESSAGES (SDK v1.1.7+ - Agent asking user questions)
|
|
206
|
+
// ============================================================
|
|
207
|
+
// The "question" tool allows the LLM to ask users questions during execution.
|
|
208
|
+
// This is useful for gathering preferences, clarifying instructions, or getting
|
|
209
|
+
// decisions on implementation choices.
|
|
210
|
+
|
|
211
|
+
// Messages when agent asks user a question
|
|
212
|
+
"questionTTSMessages": [
|
|
213
|
+
"Hey! I have a question for you. Please check your screen.",
|
|
214
|
+
"Attention! I need your input to continue.",
|
|
215
|
+
"Quick question! Please take a look when you have a moment.",
|
|
216
|
+
"I need some clarification. Could you please respond?",
|
|
217
|
+
"Question time! Your input is needed to proceed."
|
|
218
|
+
],
|
|
219
|
+
|
|
220
|
+
// Messages for MULTIPLE questions (use {count} placeholder)
|
|
221
|
+
"questionTTSMessagesMultiple": [
|
|
222
|
+
"Hey! I have {count} questions for you. Please check your screen.",
|
|
223
|
+
"Attention! I need your input on {count} items to continue.",
|
|
224
|
+
"{count} questions need your attention. Please take a look!",
|
|
225
|
+
"I need some clarifications. There are {count} questions waiting for you.",
|
|
226
|
+
"Question time! {count} questions need your response to proceed."
|
|
227
|
+
],
|
|
228
|
+
|
|
229
|
+
// Reminder messages for questions (more urgent - used after delay)
|
|
230
|
+
"questionReminderTTSMessages": [
|
|
231
|
+
"Hey! I am still waiting for your answer. Please check the questions!",
|
|
232
|
+
"Reminder: There is a question waiting for your response.",
|
|
233
|
+
"Hello? I need your input to continue. Please respond when you can.",
|
|
234
|
+
"Still waiting for your answer! The task is on hold.",
|
|
235
|
+
"Your input is needed! Please check the pending question."
|
|
236
|
+
],
|
|
237
|
+
|
|
238
|
+
// Reminder messages for MULTIPLE questions (use {count} placeholder)
|
|
239
|
+
"questionReminderTTSMessagesMultiple": [
|
|
240
|
+
"Hey! I am still waiting for answers to {count} questions. Please respond!",
|
|
241
|
+
"Reminder: There are {count} questions waiting for your response.",
|
|
242
|
+
"Hello? I need your input on {count} items. Please respond when you can.",
|
|
243
|
+
"Still waiting for your answers on {count} questions! The task is on hold.",
|
|
244
|
+
"Your input is needed! {count} questions are pending your response."
|
|
245
|
+
],
|
|
246
|
+
|
|
247
|
+
// Delay (in seconds) before question reminder fires
|
|
248
|
+
"questionReminderDelaySeconds": 25,
|
|
249
|
+
|
|
250
|
+
// Question batch window (ms) - how long to wait for more questions before notifying
|
|
251
|
+
"questionBatchWindowMs": 800,
|
|
252
|
+
|
|
197
253
|
// ============================================================
|
|
198
254
|
// SOUND FILES (For immediate notifications)
|
|
199
255
|
// These are played first before TTS reminder kicks in
|
|
@@ -204,6 +260,7 @@
|
|
|
204
260
|
|
|
205
261
|
"idleSound": "assets/Soft-high-tech-notification-sound-effect.mp3",
|
|
206
262
|
"permissionSound": "assets/Machine-alert-beep-sound-effect.mp3",
|
|
263
|
+
"questionSound": "assets/Machine-alert-beep-sound-effect.mp3",
|
|
207
264
|
|
|
208
265
|
// ============================================================
|
|
209
266
|
// GENERAL SETTINGS
|
package/index.js
CHANGED
|
@@ -23,9 +23,28 @@ import { createTTS, getTTSConfig } from './util/tts.js';
|
|
|
23
23
|
*/
|
|
24
24
|
export default async function SmartVoiceNotifyPlugin({ project, client, $, directory, worktree }) {
|
|
25
25
|
const config = getTTSConfig();
|
|
26
|
+
|
|
27
|
+
// Master switch: if plugin is disabled, return empty handlers immediately
|
|
28
|
+
if (config.enabled === false) {
|
|
29
|
+
const configDir = process.env.OPENCODE_CONFIG_DIR || path.join(os.homedir(), '.config', 'opencode');
|
|
30
|
+
const logsDir = path.join(configDir, 'logs');
|
|
31
|
+
const logFile = path.join(logsDir, 'smart-voice-notify-debug.log');
|
|
32
|
+
if (config.debugLog) {
|
|
33
|
+
try {
|
|
34
|
+
if (!fs.existsSync(logsDir)) {
|
|
35
|
+
fs.mkdirSync(logsDir, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
const timestamp = new Date().toISOString();
|
|
38
|
+
fs.appendFileSync(logFile, `[${timestamp}] Plugin disabled via config (enabled: false) - no event handlers registered\n`);
|
|
39
|
+
} catch (e) {}
|
|
40
|
+
}
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
|
|
26
44
|
const tts = createTTS({ $, client });
|
|
27
45
|
|
|
28
46
|
const platform = os.platform();
|
|
47
|
+
|
|
29
48
|
const configDir = process.env.OPENCODE_CONFIG_DIR || path.join(os.homedir(), '.config', 'opencode');
|
|
30
49
|
const logsDir = path.join(configDir, 'logs');
|
|
31
50
|
const logFile = path.join(logsDir, 'smart-voice-notify-debug.log');
|
|
@@ -71,6 +90,25 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
|
|
|
71
90
|
// Batch window duration in milliseconds (how long to wait for more permissions)
|
|
72
91
|
const PERMISSION_BATCH_WINDOW_MS = config.permissionBatchWindowMs || 800;
|
|
73
92
|
|
|
93
|
+
// ========================================
|
|
94
|
+
// QUESTION BATCHING STATE (SDK v1.1.7+)
|
|
95
|
+
// Batches multiple simultaneous question requests into a single notification
|
|
96
|
+
// ========================================
|
|
97
|
+
|
|
98
|
+
// Array of question request objects waiting to be notified (collected during batch window)
|
|
99
|
+
// Each object contains { id: string, questionCount: number } to track actual question count
|
|
100
|
+
let pendingQuestionBatch = [];
|
|
101
|
+
|
|
102
|
+
// Timeout ID for the question batch window (debounce timer)
|
|
103
|
+
let questionBatchTimeout = null;
|
|
104
|
+
|
|
105
|
+
// Batch window duration in milliseconds (how long to wait for more questions)
|
|
106
|
+
const QUESTION_BATCH_WINDOW_MS = config.questionBatchWindowMs || 800;
|
|
107
|
+
|
|
108
|
+
// Track active question request to prevent race condition where user responds
|
|
109
|
+
// before async notification code runs. Set on question.asked, cleared on question.replied/rejected.
|
|
110
|
+
let activeQuestionId = null;
|
|
111
|
+
|
|
74
112
|
/**
|
|
75
113
|
* Write debug message to log file
|
|
76
114
|
*/
|
|
@@ -160,9 +198,9 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
|
|
|
160
198
|
/**
|
|
161
199
|
* Schedule a TTS reminder if user doesn't respond within configured delay.
|
|
162
200
|
* The reminder uses a personalized TTS message.
|
|
163
|
-
* @param {string} type - 'idle' or '
|
|
201
|
+
* @param {string} type - 'idle', 'permission', or 'question'
|
|
164
202
|
* @param {string} message - The TTS message to speak (used directly, supports count-aware messages)
|
|
165
|
-
* @param {object} options - Additional options (fallbackSound, permissionCount)
|
|
203
|
+
* @param {object} options - Additional options (fallbackSound, permissionCount, questionCount)
|
|
166
204
|
*/
|
|
167
205
|
const scheduleTTSReminder = (type, message, options = {}) => {
|
|
168
206
|
// Check if TTS reminders are enabled
|
|
@@ -172,18 +210,23 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
|
|
|
172
210
|
}
|
|
173
211
|
|
|
174
212
|
// Get delay from config (in seconds, convert to ms)
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
213
|
+
let delaySeconds;
|
|
214
|
+
if (type === 'permission') {
|
|
215
|
+
delaySeconds = config.permissionReminderDelaySeconds || config.ttsReminderDelaySeconds || 30;
|
|
216
|
+
} else if (type === 'question') {
|
|
217
|
+
delaySeconds = config.questionReminderDelaySeconds || config.ttsReminderDelaySeconds || 25;
|
|
218
|
+
} else {
|
|
219
|
+
delaySeconds = config.idleReminderDelaySeconds || config.ttsReminderDelaySeconds || 30;
|
|
220
|
+
}
|
|
178
221
|
const delayMs = delaySeconds * 1000;
|
|
179
222
|
|
|
180
223
|
// Cancel any existing reminder of this type
|
|
181
224
|
cancelPendingReminder(type);
|
|
182
225
|
|
|
183
|
-
// Store
|
|
184
|
-
const
|
|
226
|
+
// Store count for generating count-aware messages in reminders
|
|
227
|
+
const itemCount = options.permissionCount || options.questionCount || 1;
|
|
185
228
|
|
|
186
|
-
debugLog(`scheduleTTSReminder: scheduling ${type} TTS in ${delaySeconds}s (count=${
|
|
229
|
+
debugLog(`scheduleTTSReminder: scheduling ${type} TTS in ${delaySeconds}s (count=${itemCount})`);
|
|
187
230
|
|
|
188
231
|
const timeoutId = setTimeout(async () => {
|
|
189
232
|
try {
|
|
@@ -201,14 +244,16 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
|
|
|
201
244
|
return;
|
|
202
245
|
}
|
|
203
246
|
|
|
204
|
-
debugLog(`scheduleTTSReminder: firing ${type} TTS reminder (count=${reminder?.
|
|
247
|
+
debugLog(`scheduleTTSReminder: firing ${type} TTS reminder (count=${reminder?.itemCount || 1})`);
|
|
205
248
|
|
|
206
249
|
// Get the appropriate reminder message
|
|
207
|
-
// For permissions with count > 1, use the count-aware message generator
|
|
208
|
-
const storedCount = reminder?.
|
|
250
|
+
// For permissions/questions with count > 1, use the count-aware message generator
|
|
251
|
+
const storedCount = reminder?.itemCount || 1;
|
|
209
252
|
let reminderMessage;
|
|
210
253
|
if (type === 'permission') {
|
|
211
254
|
reminderMessage = getPermissionMessage(storedCount, true);
|
|
255
|
+
} else if (type === 'question') {
|
|
256
|
+
reminderMessage = getQuestionMessage(storedCount, true);
|
|
212
257
|
} else {
|
|
213
258
|
reminderMessage = getRandomMessage(config.idleReminderTTSMessages);
|
|
214
259
|
}
|
|
@@ -257,10 +302,12 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
|
|
|
257
302
|
}
|
|
258
303
|
|
|
259
304
|
// Use count-aware message for follow-ups too
|
|
260
|
-
const followUpStoredCount = followUpReminder?.
|
|
305
|
+
const followUpStoredCount = followUpReminder?.itemCount || 1;
|
|
261
306
|
let followUpMessage;
|
|
262
307
|
if (type === 'permission') {
|
|
263
308
|
followUpMessage = getPermissionMessage(followUpStoredCount, true);
|
|
309
|
+
} else if (type === 'question') {
|
|
310
|
+
followUpMessage = getQuestionMessage(followUpStoredCount, true);
|
|
264
311
|
} else {
|
|
265
312
|
followUpMessage = getRandomMessage(config.idleReminderTTSMessages);
|
|
266
313
|
}
|
|
@@ -279,7 +326,7 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
|
|
|
279
326
|
timeoutId: followUpTimeoutId,
|
|
280
327
|
scheduledAt: Date.now(),
|
|
281
328
|
followUpCount,
|
|
282
|
-
|
|
329
|
+
itemCount: storedCount // Preserve the count for follow-ups
|
|
283
330
|
});
|
|
284
331
|
}
|
|
285
332
|
}
|
|
@@ -289,18 +336,18 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
|
|
|
289
336
|
}
|
|
290
337
|
}, delayMs);
|
|
291
338
|
|
|
292
|
-
// Store the pending reminder with
|
|
339
|
+
// Store the pending reminder with item count
|
|
293
340
|
pendingReminders.set(type, {
|
|
294
341
|
timeoutId,
|
|
295
342
|
scheduledAt: Date.now(),
|
|
296
343
|
followUpCount: 0,
|
|
297
|
-
|
|
344
|
+
itemCount // Store count for later use
|
|
298
345
|
});
|
|
299
346
|
};
|
|
300
347
|
|
|
301
348
|
/**
|
|
302
349
|
* Smart notification: play sound first, then schedule TTS reminder
|
|
303
|
-
* @param {string} type - 'idle' or '
|
|
350
|
+
* @param {string} type - 'idle', 'permission', or 'question'
|
|
304
351
|
* @param {object} options - Notification options
|
|
305
352
|
*/
|
|
306
353
|
const smartNotify = async (type, options = {}) => {
|
|
@@ -309,7 +356,8 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
|
|
|
309
356
|
soundLoops = 1,
|
|
310
357
|
ttsMessage,
|
|
311
358
|
fallbackSound,
|
|
312
|
-
permissionCount = 1 // Support permission count for batched notifications
|
|
359
|
+
permissionCount = 1, // Support permission count for batched notifications
|
|
360
|
+
questionCount = 1 // Support question count for batched notifications
|
|
313
361
|
} = options;
|
|
314
362
|
|
|
315
363
|
// Step 1: Play the immediate sound notification
|
|
@@ -328,17 +376,27 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
|
|
|
328
376
|
debugLog(`smartNotify: permission handled during sound - aborting reminder`);
|
|
329
377
|
return;
|
|
330
378
|
}
|
|
379
|
+
// For question notifications: check if the question was already answered/rejected
|
|
380
|
+
if (type === 'question' && !activeQuestionId) {
|
|
381
|
+
debugLog(`smartNotify: question handled during sound - aborting reminder`);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
331
384
|
|
|
332
385
|
// Step 2: Schedule TTS reminder if user doesn't respond
|
|
333
386
|
if (config.enableTTSReminder && ttsMessage) {
|
|
334
|
-
scheduleTTSReminder(type, ttsMessage, { fallbackSound, permissionCount });
|
|
387
|
+
scheduleTTSReminder(type, ttsMessage, { fallbackSound, permissionCount, questionCount });
|
|
335
388
|
}
|
|
336
389
|
|
|
337
390
|
// Step 3: If TTS-first mode is enabled, also speak immediately
|
|
338
391
|
if (config.notificationMode === 'tts-first' || config.notificationMode === 'both') {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
392
|
+
let immediateMessage;
|
|
393
|
+
if (type === 'permission') {
|
|
394
|
+
immediateMessage = getRandomMessage(config.permissionTTSMessages);
|
|
395
|
+
} else if (type === 'question') {
|
|
396
|
+
immediateMessage = getRandomMessage(config.questionTTSMessages);
|
|
397
|
+
} else {
|
|
398
|
+
immediateMessage = getRandomMessage(config.idleTTSMessages);
|
|
399
|
+
}
|
|
342
400
|
|
|
343
401
|
await tts.speak(immediateMessage, {
|
|
344
402
|
enableTTS: true,
|
|
@@ -378,6 +436,37 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
|
|
|
378
436
|
}
|
|
379
437
|
};
|
|
380
438
|
|
|
439
|
+
/**
|
|
440
|
+
* Get a count-aware TTS message for question requests (SDK v1.1.7+)
|
|
441
|
+
* @param {number} count - Number of question requests
|
|
442
|
+
* @param {boolean} isReminder - Whether this is a reminder message
|
|
443
|
+
* @returns {string} The formatted message
|
|
444
|
+
*/
|
|
445
|
+
const getQuestionMessage = (count, isReminder = false) => {
|
|
446
|
+
const messages = isReminder
|
|
447
|
+
? config.questionReminderTTSMessages
|
|
448
|
+
: config.questionTTSMessages;
|
|
449
|
+
|
|
450
|
+
if (count === 1) {
|
|
451
|
+
// Single question - use regular message
|
|
452
|
+
return getRandomMessage(messages);
|
|
453
|
+
} else {
|
|
454
|
+
// Multiple questions - use count-aware messages if available, or format dynamically
|
|
455
|
+
const countMessages = isReminder
|
|
456
|
+
? config.questionReminderTTSMessagesMultiple
|
|
457
|
+
: config.questionTTSMessagesMultiple;
|
|
458
|
+
|
|
459
|
+
if (countMessages && countMessages.length > 0) {
|
|
460
|
+
// Use configured multi-question messages (replace {count} placeholder)
|
|
461
|
+
const template = getRandomMessage(countMessages);
|
|
462
|
+
return template.replace('{count}', count.toString());
|
|
463
|
+
} else {
|
|
464
|
+
// Fallback: generate a dynamic message
|
|
465
|
+
return `Hey! I have ${count} questions for you. Please check your screen.`;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
|
|
381
470
|
/**
|
|
382
471
|
* Process the batched permission requests as a single notification
|
|
383
472
|
* Called after the batch window expires
|
|
@@ -449,6 +538,81 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
|
|
|
449
538
|
}
|
|
450
539
|
};
|
|
451
540
|
|
|
541
|
+
/**
|
|
542
|
+
* Process the batched question requests as a single notification (SDK v1.1.7+)
|
|
543
|
+
* Called after the batch window expires
|
|
544
|
+
*/
|
|
545
|
+
const processQuestionBatch = async () => {
|
|
546
|
+
// Capture and clear the batch
|
|
547
|
+
const batch = [...pendingQuestionBatch];
|
|
548
|
+
pendingQuestionBatch = [];
|
|
549
|
+
questionBatchTimeout = null;
|
|
550
|
+
|
|
551
|
+
if (batch.length === 0) {
|
|
552
|
+
debugLog('processQuestionBatch: empty batch, skipping');
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Calculate total number of questions across all batched requests
|
|
557
|
+
// Each batch item is { id, questionCount } where questionCount is the number of questions in that request
|
|
558
|
+
const totalQuestionCount = batch.reduce((sum, item) => sum + (item.questionCount || 1), 0);
|
|
559
|
+
|
|
560
|
+
debugLog(`processQuestionBatch: processing ${batch.length} request(s) with ${totalQuestionCount} total question(s)`);
|
|
561
|
+
|
|
562
|
+
// Set activeQuestionId to the first one (for race condition checks)
|
|
563
|
+
// We track all IDs in the batch for proper cleanup
|
|
564
|
+
activeQuestionId = batch[0]?.id;
|
|
565
|
+
|
|
566
|
+
// Show toast with count
|
|
567
|
+
const toastMessage = totalQuestionCount === 1
|
|
568
|
+
? "❓ The agent has a question for you"
|
|
569
|
+
: `❓ The agent has ${totalQuestionCount} questions for you`;
|
|
570
|
+
await showToast(toastMessage, "info", 8000);
|
|
571
|
+
|
|
572
|
+
// CHECK: Did user already respond while we were showing toast?
|
|
573
|
+
if (pendingQuestionBatch.length > 0) {
|
|
574
|
+
// New questions arrived during toast - they'll be handled in next batch
|
|
575
|
+
debugLog('processQuestionBatch: new questions arrived during toast');
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Check if any question was already replied to or rejected
|
|
579
|
+
if (activeQuestionId === null) {
|
|
580
|
+
debugLog('processQuestionBatch: aborted - user already responded');
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Get count-aware TTS message (uses total question count, not request count)
|
|
585
|
+
const ttsMessage = getQuestionMessage(totalQuestionCount, false);
|
|
586
|
+
const reminderMessage = getQuestionMessage(totalQuestionCount, true);
|
|
587
|
+
|
|
588
|
+
// Smart notification: sound first, TTS reminder later
|
|
589
|
+
// Sound plays 2 times by default (matching permission behavior)
|
|
590
|
+
await smartNotify('question', {
|
|
591
|
+
soundFile: config.questionSound,
|
|
592
|
+
soundLoops: 2, // Fixed at 2 loops to match permission sound behavior
|
|
593
|
+
ttsMessage: reminderMessage,
|
|
594
|
+
fallbackSound: config.questionSound,
|
|
595
|
+
// Pass count for use in reminders
|
|
596
|
+
questionCount: totalQuestionCount
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
// Speak immediately if in TTS-first or both mode (with count-aware message)
|
|
600
|
+
if (config.notificationMode === 'tts-first' || config.notificationMode === 'both') {
|
|
601
|
+
await tts.wakeMonitor();
|
|
602
|
+
await tts.forceVolume();
|
|
603
|
+
await tts.speak(ttsMessage, {
|
|
604
|
+
enableTTS: true,
|
|
605
|
+
fallbackSound: config.questionSound
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Final check: if user responded during notification, cancel scheduled reminder
|
|
610
|
+
if (activeQuestionId === null) {
|
|
611
|
+
debugLog('processQuestionBatch: user responded during notification - cancelling reminder');
|
|
612
|
+
cancelPendingReminder('question');
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
|
|
452
616
|
return {
|
|
453
617
|
event: async ({ event }) => {
|
|
454
618
|
try {
|
|
@@ -456,13 +620,16 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
|
|
|
456
620
|
// USER ACTIVITY DETECTION
|
|
457
621
|
// Cancels pending TTS reminders when user responds
|
|
458
622
|
// ========================================
|
|
459
|
-
// NOTE: OpenCode event types (supporting SDK v1.0.x and v1.1.
|
|
623
|
+
// NOTE: OpenCode event types (supporting SDK v1.0.x, v1.1.x, and v1.1.7+):
|
|
460
624
|
// - message.updated: fires when a message is added/updated (use properties.info.role to check user vs assistant)
|
|
461
625
|
// - permission.updated (SDK v1.0.x): fires when a permission request is created
|
|
462
626
|
// - permission.asked (SDK v1.1.1+): fires when a permission request is created (replaces permission.updated)
|
|
463
627
|
// - permission.replied: fires when user responds to a permission request
|
|
464
628
|
// - SDK v1.0.x: uses permissionID, response
|
|
465
629
|
// - SDK v1.1.1+: uses requestID, reply
|
|
630
|
+
// - question.asked (SDK v1.1.7+): fires when agent asks user a question
|
|
631
|
+
// - question.replied (SDK v1.1.7+): fires when user answers a question
|
|
632
|
+
// - question.rejected (SDK v1.1.7+): fires when user dismisses a question
|
|
466
633
|
// - session.created: fires when a new session starts
|
|
467
634
|
//
|
|
468
635
|
// CRITICAL: message.updated fires for EVERY modification to a message (not just creation).
|
|
@@ -546,6 +713,7 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
|
|
|
546
713
|
lastUserActivityTime = Date.now();
|
|
547
714
|
lastSessionIdleTime = 0;
|
|
548
715
|
activePermissionId = null;
|
|
716
|
+
activeQuestionId = null;
|
|
549
717
|
seenUserMessageIds.clear();
|
|
550
718
|
cancelAllPendingReminders();
|
|
551
719
|
|
|
@@ -556,6 +724,13 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
|
|
|
556
724
|
permissionBatchTimeout = null;
|
|
557
725
|
}
|
|
558
726
|
|
|
727
|
+
// Reset question batch state
|
|
728
|
+
pendingQuestionBatch = [];
|
|
729
|
+
if (questionBatchTimeout) {
|
|
730
|
+
clearTimeout(questionBatchTimeout);
|
|
731
|
+
questionBatchTimeout = null;
|
|
732
|
+
}
|
|
733
|
+
|
|
559
734
|
debugLog(`Session created: ${event.type} - reset all tracking state`);
|
|
560
735
|
}
|
|
561
736
|
|
|
@@ -633,6 +808,113 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
|
|
|
633
808
|
|
|
634
809
|
debugLog(`${event.type}: batch window reset (will process in ${PERMISSION_BATCH_WINDOW_MS}ms if no more arrive)`);
|
|
635
810
|
}
|
|
811
|
+
|
|
812
|
+
// ========================================
|
|
813
|
+
// NOTIFICATION 3: Question Request (BATCHED) - SDK v1.1.7+
|
|
814
|
+
// ========================================
|
|
815
|
+
// The "question" tool allows the LLM to ask users questions during execution.
|
|
816
|
+
// Events: question.asked, question.replied, question.rejected
|
|
817
|
+
//
|
|
818
|
+
// BATCHING: When multiple question requests arrive simultaneously,
|
|
819
|
+
// we batch them into a single notification instead of playing overlapping sounds.
|
|
820
|
+
// NOTE: Each question.asked event can contain multiple questions in its questions array.
|
|
821
|
+
if (event.type === "question.asked") {
|
|
822
|
+
// Capture question request ID and count of questions in this request
|
|
823
|
+
const questionId = event.properties?.id;
|
|
824
|
+
const questionsArray = event.properties?.questions;
|
|
825
|
+
const questionCount = Array.isArray(questionsArray) ? questionsArray.length : 1;
|
|
826
|
+
|
|
827
|
+
if (!questionId) {
|
|
828
|
+
debugLog(`${event.type}: question ID missing. properties keys: ` + Object.keys(event.properties || {}).join(', '));
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// Add to the pending batch (avoid duplicates by checking ID)
|
|
832
|
+
// Store as object with id and questionCount for proper counting
|
|
833
|
+
const existingIndex = pendingQuestionBatch.findIndex(item => item.id === questionId);
|
|
834
|
+
if (questionId && existingIndex === -1) {
|
|
835
|
+
pendingQuestionBatch.push({ id: questionId, questionCount });
|
|
836
|
+
debugLog(`${event.type}: added ${questionId} with ${questionCount} question(s) to batch (now ${pendingQuestionBatch.length} request(s) pending)`);
|
|
837
|
+
} else if (!questionId) {
|
|
838
|
+
// If no ID, still count it (use a placeholder)
|
|
839
|
+
pendingQuestionBatch.push({ id: `unknown-${Date.now()}`, questionCount });
|
|
840
|
+
debugLog(`${event.type}: added unknown question request with ${questionCount} question(s) to batch (now ${pendingQuestionBatch.length} request(s) pending)`);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Reset the batch window timer (debounce)
|
|
844
|
+
// This gives more questions a chance to arrive before we notify
|
|
845
|
+
if (questionBatchTimeout) {
|
|
846
|
+
clearTimeout(questionBatchTimeout);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
questionBatchTimeout = setTimeout(async () => {
|
|
850
|
+
try {
|
|
851
|
+
await processQuestionBatch();
|
|
852
|
+
} catch (e) {
|
|
853
|
+
debugLog(`processQuestionBatch error: ${e.message}`);
|
|
854
|
+
}
|
|
855
|
+
}, QUESTION_BATCH_WINDOW_MS);
|
|
856
|
+
|
|
857
|
+
debugLog(`${event.type}: batch window reset (will process in ${QUESTION_BATCH_WINDOW_MS}ms if no more arrive)`);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Handle question.replied - user answered the question(s)
|
|
861
|
+
if (event.type === "question.replied") {
|
|
862
|
+
const repliedQuestionId = event.properties?.requestID;
|
|
863
|
+
const answers = event.properties?.answers;
|
|
864
|
+
|
|
865
|
+
// Remove this question from the pending batch (if still waiting)
|
|
866
|
+
// pendingQuestionBatch is now an array of { id, questionCount } objects
|
|
867
|
+
const existingIndex = pendingQuestionBatch.findIndex(item => item.id === repliedQuestionId);
|
|
868
|
+
if (repliedQuestionId && existingIndex !== -1) {
|
|
869
|
+
pendingQuestionBatch.splice(existingIndex, 1);
|
|
870
|
+
debugLog(`Question replied: removed ${repliedQuestionId} from pending batch (${pendingQuestionBatch.length} remaining)`);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// If batch is now empty and we have a pending batch timeout, we can cancel it
|
|
874
|
+
if (pendingQuestionBatch.length === 0 && questionBatchTimeout) {
|
|
875
|
+
clearTimeout(questionBatchTimeout);
|
|
876
|
+
questionBatchTimeout = null;
|
|
877
|
+
debugLog('Question replied: cancelled batch timeout (all questions handled)');
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// Clear active question ID
|
|
881
|
+
if (activeQuestionId === repliedQuestionId || activeQuestionId === undefined) {
|
|
882
|
+
activeQuestionId = null;
|
|
883
|
+
debugLog(`Question replied: cleared activeQuestionId ${repliedQuestionId || '(unknown)'}`);
|
|
884
|
+
}
|
|
885
|
+
lastUserActivityTime = Date.now();
|
|
886
|
+
cancelPendingReminder('question'); // Cancel question-specific reminder
|
|
887
|
+
debugLog(`Question replied: ${event.type} (answers=${JSON.stringify(answers)}) - cancelled question reminder`);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// Handle question.rejected - user dismissed the question
|
|
891
|
+
if (event.type === "question.rejected") {
|
|
892
|
+
const rejectedQuestionId = event.properties?.requestID;
|
|
893
|
+
|
|
894
|
+
// Remove this question from the pending batch (if still waiting)
|
|
895
|
+
// pendingQuestionBatch is now an array of { id, questionCount } objects
|
|
896
|
+
const existingIndex = pendingQuestionBatch.findIndex(item => item.id === rejectedQuestionId);
|
|
897
|
+
if (rejectedQuestionId && existingIndex !== -1) {
|
|
898
|
+
pendingQuestionBatch.splice(existingIndex, 1);
|
|
899
|
+
debugLog(`Question rejected: removed ${rejectedQuestionId} from pending batch (${pendingQuestionBatch.length} remaining)`);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// If batch is now empty and we have a pending batch timeout, we can cancel it
|
|
903
|
+
if (pendingQuestionBatch.length === 0 && questionBatchTimeout) {
|
|
904
|
+
clearTimeout(questionBatchTimeout);
|
|
905
|
+
questionBatchTimeout = null;
|
|
906
|
+
debugLog('Question rejected: cancelled batch timeout (all questions handled)');
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// Clear active question ID
|
|
910
|
+
if (activeQuestionId === rejectedQuestionId || activeQuestionId === undefined) {
|
|
911
|
+
activeQuestionId = null;
|
|
912
|
+
debugLog(`Question rejected: cleared activeQuestionId ${rejectedQuestionId || '(unknown)'}`);
|
|
913
|
+
}
|
|
914
|
+
lastUserActivityTime = Date.now();
|
|
915
|
+
cancelPendingReminder('question'); // Cancel question-specific reminder
|
|
916
|
+
debugLog(`Question rejected: ${event.type} - cancelled question reminder`);
|
|
917
|
+
}
|
|
636
918
|
} catch (e) {
|
|
637
919
|
debugLog(`event handler error: ${e.message}`);
|
|
638
920
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-smart-voice-notify",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "Smart voice notification plugin for OpenCode with multiple TTS engines (ElevenLabs, Edge TTS, Windows SAPI) and intelligent reminder system",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
package/util/config.js
CHANGED
|
@@ -59,6 +59,13 @@ const generateDefaultConfig = (overrides = {}, version = '1.0.0') => {
|
|
|
59
59
|
// Internal version tracking - DO NOT REMOVE
|
|
60
60
|
"_configVersion": "${version}",
|
|
61
61
|
|
|
62
|
+
// ============================================================
|
|
63
|
+
// PLUGIN ENABLE/DISABLE
|
|
64
|
+
// ============================================================
|
|
65
|
+
// Master switch to enable or disable the entire plugin.
|
|
66
|
+
// Set to false to disable all notifications without uninstalling.
|
|
67
|
+
"enabled": ${overrides.enabled !== undefined ? overrides.enabled : true},
|
|
68
|
+
|
|
62
69
|
// ============================================================
|
|
63
70
|
// NOTIFICATION MODE SETTINGS (Smart Notification System)
|
|
64
71
|
// ============================================================
|
|
@@ -241,6 +248,55 @@ const generateDefaultConfig = (overrides = {}, version = '1.0.0') => {
|
|
|
241
248
|
// Batch window (ms) - how long to wait for more permissions before notifying
|
|
242
249
|
"permissionBatchWindowMs": ${overrides.permissionBatchWindowMs !== undefined ? overrides.permissionBatchWindowMs : 800},
|
|
243
250
|
|
|
251
|
+
// ============================================================
|
|
252
|
+
// QUESTION TOOL SETTINGS (SDK v1.1.7+ - Agent asking user questions)
|
|
253
|
+
// ============================================================
|
|
254
|
+
// The "question" tool allows the LLM to ask users questions during execution.
|
|
255
|
+
// This is useful for gathering preferences, clarifying instructions, or getting
|
|
256
|
+
// decisions on implementation choices.
|
|
257
|
+
|
|
258
|
+
// Messages when agent asks user a question
|
|
259
|
+
"questionTTSMessages": ${formatJSON(overrides.questionTTSMessages || [
|
|
260
|
+
"Hey! I have a question for you. Please check your screen.",
|
|
261
|
+
"Attention! I need your input to continue.",
|
|
262
|
+
"Quick question! Please take a look when you have a moment.",
|
|
263
|
+
"I need some clarification. Could you please respond?",
|
|
264
|
+
"Question time! Your input is needed to proceed."
|
|
265
|
+
], 4)},
|
|
266
|
+
|
|
267
|
+
// Messages for MULTIPLE questions (use {count} placeholder)
|
|
268
|
+
"questionTTSMessagesMultiple": ${formatJSON(overrides.questionTTSMessagesMultiple || [
|
|
269
|
+
"Hey! I have {count} questions for you. Please check your screen.",
|
|
270
|
+
"Attention! I need your input on {count} items to continue.",
|
|
271
|
+
"{count} questions need your attention. Please take a look!",
|
|
272
|
+
"I need some clarifications. There are {count} questions waiting for you.",
|
|
273
|
+
"Question time! {count} questions need your response to proceed."
|
|
274
|
+
], 4)},
|
|
275
|
+
|
|
276
|
+
// Reminder messages for questions (more urgent - used after delay)
|
|
277
|
+
"questionReminderTTSMessages": ${formatJSON(overrides.questionReminderTTSMessages || [
|
|
278
|
+
"Hey! I am still waiting for your answer. Please check the questions!",
|
|
279
|
+
"Reminder: There is a question waiting for your response.",
|
|
280
|
+
"Hello? I need your input to continue. Please respond when you can.",
|
|
281
|
+
"Still waiting for your answer! The task is on hold.",
|
|
282
|
+
"Your input is needed! Please check the pending question."
|
|
283
|
+
], 4)},
|
|
284
|
+
|
|
285
|
+
// Reminder messages for MULTIPLE questions (use {count} placeholder)
|
|
286
|
+
"questionReminderTTSMessagesMultiple": ${formatJSON(overrides.questionReminderTTSMessagesMultiple || [
|
|
287
|
+
"Hey! I am still waiting for answers to {count} questions. Please respond!",
|
|
288
|
+
"Reminder: There are {count} questions waiting for your response.",
|
|
289
|
+
"Hello? I need your input on {count} items. Please respond when you can.",
|
|
290
|
+
"Still waiting for your answers on {count} questions! The task is on hold.",
|
|
291
|
+
"Your input is needed! {count} questions are pending your response."
|
|
292
|
+
], 4)},
|
|
293
|
+
|
|
294
|
+
// Delay (in seconds) before question reminder fires
|
|
295
|
+
"questionReminderDelaySeconds": ${overrides.questionReminderDelaySeconds !== undefined ? overrides.questionReminderDelaySeconds : 25},
|
|
296
|
+
|
|
297
|
+
// Question batch window (ms) - how long to wait for more questions before notifying
|
|
298
|
+
"questionBatchWindowMs": ${overrides.questionBatchWindowMs !== undefined ? overrides.questionBatchWindowMs : 800},
|
|
299
|
+
|
|
244
300
|
// ============================================================
|
|
245
301
|
// SOUND FILES (For immediate notifications)
|
|
246
302
|
// These are played first before TTS reminder kicks in
|
|
@@ -251,6 +307,7 @@ const generateDefaultConfig = (overrides = {}, version = '1.0.0') => {
|
|
|
251
307
|
|
|
252
308
|
"idleSound": "${overrides.idleSound || 'assets/Soft-high-tech-notification-sound-effect.mp3'}",
|
|
253
309
|
"permissionSound": "${overrides.permissionSound || 'assets/Machine-alert-beep-sound-effect.mp3'}",
|
|
310
|
+
"questionSound": "${overrides.questionSound || 'assets/Machine-alert-beep-sound-effect.mp3'}",
|
|
254
311
|
|
|
255
312
|
// ============================================================
|
|
256
313
|
// GENERAL SETTINGS
|
package/util/tts.js
CHANGED
|
@@ -111,11 +111,52 @@ export const getTTSConfig = () => {
|
|
|
111
111
|
// Permission batch window (ms) - how long to wait for more permissions before notifying
|
|
112
112
|
permissionBatchWindowMs: 800,
|
|
113
113
|
|
|
114
|
+
// ============================================================
|
|
115
|
+
// QUESTION TOOL MESSAGES (SDK v1.1.7+ - Agent asking user questions)
|
|
116
|
+
// ============================================================
|
|
117
|
+
// Messages when agent asks user a question
|
|
118
|
+
questionTTSMessages: [
|
|
119
|
+
'Hey! I have a question for you. Please check your screen.',
|
|
120
|
+
'Attention! I need your input to continue.',
|
|
121
|
+
'Quick question! Please take a look when you have a moment.',
|
|
122
|
+
'I need some clarification. Could you please respond?',
|
|
123
|
+
'Question time! Your input is needed to proceed.'
|
|
124
|
+
],
|
|
125
|
+
// Messages for MULTIPLE questions (use {count} placeholder)
|
|
126
|
+
questionTTSMessagesMultiple: [
|
|
127
|
+
'Hey! I have {count} questions for you. Please check your screen.',
|
|
128
|
+
'Attention! I need your input on {count} items to continue.',
|
|
129
|
+
'{count} questions need your attention. Please take a look!',
|
|
130
|
+
'I need some clarifications. There are {count} questions waiting for you.',
|
|
131
|
+
'Question time! {count} questions need your response to proceed.'
|
|
132
|
+
],
|
|
133
|
+
// Reminder messages for questions
|
|
134
|
+
questionReminderTTSMessages: [
|
|
135
|
+
'Hey! I am still waiting for your answer. Please check the questions!',
|
|
136
|
+
'Reminder: There is a question waiting for your response.',
|
|
137
|
+
'Hello? I need your input to continue. Please respond when you can.',
|
|
138
|
+
'Still waiting for your answer! The task is on hold.',
|
|
139
|
+
'Your input is needed! Please check the pending question.'
|
|
140
|
+
],
|
|
141
|
+
// Reminder messages for MULTIPLE questions (use {count} placeholder)
|
|
142
|
+
questionReminderTTSMessagesMultiple: [
|
|
143
|
+
'Hey! I am still waiting for answers to {count} questions. Please respond!',
|
|
144
|
+
'Reminder: There are {count} questions waiting for your response.',
|
|
145
|
+
'Hello? I need your input on {count} items. Please respond when you can.',
|
|
146
|
+
'Still waiting for your answers on {count} questions! The task is on hold.',
|
|
147
|
+
'Your input is needed! {count} questions are pending your response.'
|
|
148
|
+
],
|
|
149
|
+
// Question reminder delay (seconds) - slightly less urgent than permissions
|
|
150
|
+
questionReminderDelaySeconds: 25,
|
|
151
|
+
// Question batch window (ms) - how long to wait for more questions before notifying
|
|
152
|
+
questionBatchWindowMs: 800,
|
|
153
|
+
|
|
114
154
|
// ============================================================
|
|
115
155
|
// SOUND FILES (Used for immediate notifications)
|
|
116
156
|
// ============================================================
|
|
117
157
|
idleSound: 'assets/Soft-high-tech-notification-sound-effect.mp3',
|
|
118
158
|
permissionSound: 'assets/Machine-alert-beep-sound-effect.mp3',
|
|
159
|
+
questionSound: 'assets/Machine-alert-beep-sound-effect.mp3',
|
|
119
160
|
|
|
120
161
|
// ============================================================
|
|
121
162
|
// GENERAL SETTINGS
|