opencode-smart-voice-notify 1.0.13 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
@@ -257,10 +258,13 @@ See `example.config.jsonc` for more details.
257
258
  | `permission.asked` | Permission request (SDK v1.1.1+) - alert user |
258
259
  | `permission.updated` | Permission request (SDK v1.0.x) - alert user |
259
260
  | `permission.replied` | User responded - cancel pending reminders |
261
+ | `question.asked` | Agent asks question (SDK v1.1.7+) - notify user |
262
+ | `question.replied` | User answered question - cancel pending reminders |
263
+ | `question.rejected` | User dismissed question - cancel pending reminders |
260
264
  | `message.updated` | New user message - cancel pending reminders |
261
265
  | `session.created` | New session - reset state |
262
266
 
263
- > **Note**: The plugin supports both OpenCode SDK v1.0.x and v1.1.x for backward compatibility.
267
+ > **Note**: The plugin supports OpenCode SDK v1.0.x, v1.1.x, and v1.1.7+ for backward compatibility.
264
268
 
265
269
  ## Development
266
270
 
@@ -194,6 +194,55 @@
194
194
  "Still waiting for authorization on {count} requests! The task is on hold."
195
195
  ],
196
196
 
197
+ // ============================================================
198
+ // QUESTION TOOL MESSAGES (SDK v1.1.7+ - Agent asking user questions)
199
+ // ============================================================
200
+ // The "question" tool allows the LLM to ask users questions during execution.
201
+ // This is useful for gathering preferences, clarifying instructions, or getting
202
+ // decisions on implementation choices.
203
+
204
+ // Messages when agent asks user a question
205
+ "questionTTSMessages": [
206
+ "Hey! I have a question for you. Please check your screen.",
207
+ "Attention! I need your input to continue.",
208
+ "Quick question! Please take a look when you have a moment.",
209
+ "I need some clarification. Could you please respond?",
210
+ "Question time! Your input is needed to proceed."
211
+ ],
212
+
213
+ // Messages for MULTIPLE questions (use {count} placeholder)
214
+ "questionTTSMessagesMultiple": [
215
+ "Hey! I have {count} questions for you. Please check your screen.",
216
+ "Attention! I need your input on {count} items to continue.",
217
+ "{count} questions need your attention. Please take a look!",
218
+ "I need some clarifications. There are {count} questions waiting for you.",
219
+ "Question time! {count} questions need your response to proceed."
220
+ ],
221
+
222
+ // Reminder messages for questions (more urgent - used after delay)
223
+ "questionReminderTTSMessages": [
224
+ "Hey! I am still waiting for your answer. Please check the questions!",
225
+ "Reminder: There is a question waiting for your response.",
226
+ "Hello? I need your input to continue. Please respond when you can.",
227
+ "Still waiting for your answer! The task is on hold.",
228
+ "Your input is needed! Please check the pending question."
229
+ ],
230
+
231
+ // Reminder messages for MULTIPLE questions (use {count} placeholder)
232
+ "questionReminderTTSMessagesMultiple": [
233
+ "Hey! I am still waiting for answers to {count} questions. Please respond!",
234
+ "Reminder: There are {count} questions waiting for your response.",
235
+ "Hello? I need your input on {count} items. Please respond when you can.",
236
+ "Still waiting for your answers on {count} questions! The task is on hold.",
237
+ "Your input is needed! {count} questions are pending your response."
238
+ ],
239
+
240
+ // Delay (in seconds) before question reminder fires
241
+ "questionReminderDelaySeconds": 25,
242
+
243
+ // Question batch window (ms) - how long to wait for more questions before notifying
244
+ "questionBatchWindowMs": 800,
245
+
197
246
  // ============================================================
198
247
  // SOUND FILES (For immediate notifications)
199
248
  // These are played first before TTS reminder kicks in
@@ -204,6 +253,7 @@
204
253
 
205
254
  "idleSound": "assets/Soft-high-tech-notification-sound-effect.mp3",
206
255
  "permissionSound": "assets/Machine-alert-beep-sound-effect.mp3",
256
+ "questionSound": "assets/Machine-alert-beep-sound-effect.mp3",
207
257
 
208
258
  // ============================================================
209
259
  // GENERAL SETTINGS
package/index.js CHANGED
@@ -71,6 +71,25 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
71
71
  // Batch window duration in milliseconds (how long to wait for more permissions)
72
72
  const PERMISSION_BATCH_WINDOW_MS = config.permissionBatchWindowMs || 800;
73
73
 
74
+ // ========================================
75
+ // QUESTION BATCHING STATE (SDK v1.1.7+)
76
+ // Batches multiple simultaneous question requests into a single notification
77
+ // ========================================
78
+
79
+ // Array of question request objects waiting to be notified (collected during batch window)
80
+ // Each object contains { id: string, questionCount: number } to track actual question count
81
+ let pendingQuestionBatch = [];
82
+
83
+ // Timeout ID for the question batch window (debounce timer)
84
+ let questionBatchTimeout = null;
85
+
86
+ // Batch window duration in milliseconds (how long to wait for more questions)
87
+ const QUESTION_BATCH_WINDOW_MS = config.questionBatchWindowMs || 800;
88
+
89
+ // Track active question request to prevent race condition where user responds
90
+ // before async notification code runs. Set on question.asked, cleared on question.replied/rejected.
91
+ let activeQuestionId = null;
92
+
74
93
  /**
75
94
  * Write debug message to log file
76
95
  */
@@ -160,9 +179,9 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
160
179
  /**
161
180
  * Schedule a TTS reminder if user doesn't respond within configured delay.
162
181
  * The reminder uses a personalized TTS message.
163
- * @param {string} type - 'idle' or 'permission'
182
+ * @param {string} type - 'idle', 'permission', or 'question'
164
183
  * @param {string} message - The TTS message to speak (used directly, supports count-aware messages)
165
- * @param {object} options - Additional options (fallbackSound, permissionCount)
184
+ * @param {object} options - Additional options (fallbackSound, permissionCount, questionCount)
166
185
  */
167
186
  const scheduleTTSReminder = (type, message, options = {}) => {
168
187
  // Check if TTS reminders are enabled
@@ -172,18 +191,23 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
172
191
  }
173
192
 
174
193
  // Get delay from config (in seconds, convert to ms)
175
- const delaySeconds = type === 'permission'
176
- ? (config.permissionReminderDelaySeconds || config.ttsReminderDelaySeconds || 30)
177
- : (config.idleReminderDelaySeconds || config.ttsReminderDelaySeconds || 30);
194
+ let delaySeconds;
195
+ if (type === 'permission') {
196
+ delaySeconds = config.permissionReminderDelaySeconds || config.ttsReminderDelaySeconds || 30;
197
+ } else if (type === 'question') {
198
+ delaySeconds = config.questionReminderDelaySeconds || config.ttsReminderDelaySeconds || 25;
199
+ } else {
200
+ delaySeconds = config.idleReminderDelaySeconds || config.ttsReminderDelaySeconds || 30;
201
+ }
178
202
  const delayMs = delaySeconds * 1000;
179
203
 
180
204
  // Cancel any existing reminder of this type
181
205
  cancelPendingReminder(type);
182
206
 
183
- // Store permission count for generating count-aware messages in reminders
184
- const permissionCount = options.permissionCount || 1;
207
+ // Store count for generating count-aware messages in reminders
208
+ const itemCount = options.permissionCount || options.questionCount || 1;
185
209
 
186
- debugLog(`scheduleTTSReminder: scheduling ${type} TTS in ${delaySeconds}s (count=${permissionCount})`);
210
+ debugLog(`scheduleTTSReminder: scheduling ${type} TTS in ${delaySeconds}s (count=${itemCount})`);
187
211
 
188
212
  const timeoutId = setTimeout(async () => {
189
213
  try {
@@ -201,14 +225,16 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
201
225
  return;
202
226
  }
203
227
 
204
- debugLog(`scheduleTTSReminder: firing ${type} TTS reminder (count=${reminder?.permissionCount || 1})`);
228
+ debugLog(`scheduleTTSReminder: firing ${type} TTS reminder (count=${reminder?.itemCount || 1})`);
205
229
 
206
230
  // Get the appropriate reminder message
207
- // For permissions with count > 1, use the count-aware message generator
208
- const storedCount = reminder?.permissionCount || 1;
231
+ // For permissions/questions with count > 1, use the count-aware message generator
232
+ const storedCount = reminder?.itemCount || 1;
209
233
  let reminderMessage;
210
234
  if (type === 'permission') {
211
235
  reminderMessage = getPermissionMessage(storedCount, true);
236
+ } else if (type === 'question') {
237
+ reminderMessage = getQuestionMessage(storedCount, true);
212
238
  } else {
213
239
  reminderMessage = getRandomMessage(config.idleReminderTTSMessages);
214
240
  }
@@ -257,10 +283,12 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
257
283
  }
258
284
 
259
285
  // Use count-aware message for follow-ups too
260
- const followUpStoredCount = followUpReminder?.permissionCount || 1;
286
+ const followUpStoredCount = followUpReminder?.itemCount || 1;
261
287
  let followUpMessage;
262
288
  if (type === 'permission') {
263
289
  followUpMessage = getPermissionMessage(followUpStoredCount, true);
290
+ } else if (type === 'question') {
291
+ followUpMessage = getQuestionMessage(followUpStoredCount, true);
264
292
  } else {
265
293
  followUpMessage = getRandomMessage(config.idleReminderTTSMessages);
266
294
  }
@@ -279,7 +307,7 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
279
307
  timeoutId: followUpTimeoutId,
280
308
  scheduledAt: Date.now(),
281
309
  followUpCount,
282
- permissionCount: storedCount // Preserve the count for follow-ups
310
+ itemCount: storedCount // Preserve the count for follow-ups
283
311
  });
284
312
  }
285
313
  }
@@ -289,18 +317,18 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
289
317
  }
290
318
  }, delayMs);
291
319
 
292
- // Store the pending reminder with permission count
320
+ // Store the pending reminder with item count
293
321
  pendingReminders.set(type, {
294
322
  timeoutId,
295
323
  scheduledAt: Date.now(),
296
324
  followUpCount: 0,
297
- permissionCount // Store count for later use
325
+ itemCount // Store count for later use
298
326
  });
299
327
  };
300
328
 
301
329
  /**
302
330
  * Smart notification: play sound first, then schedule TTS reminder
303
- * @param {string} type - 'idle' or 'permission'
331
+ * @param {string} type - 'idle', 'permission', or 'question'
304
332
  * @param {object} options - Notification options
305
333
  */
306
334
  const smartNotify = async (type, options = {}) => {
@@ -309,7 +337,8 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
309
337
  soundLoops = 1,
310
338
  ttsMessage,
311
339
  fallbackSound,
312
- permissionCount = 1 // Support permission count for batched notifications
340
+ permissionCount = 1, // Support permission count for batched notifications
341
+ questionCount = 1 // Support question count for batched notifications
313
342
  } = options;
314
343
 
315
344
  // Step 1: Play the immediate sound notification
@@ -328,17 +357,27 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
328
357
  debugLog(`smartNotify: permission handled during sound - aborting reminder`);
329
358
  return;
330
359
  }
360
+ // For question notifications: check if the question was already answered/rejected
361
+ if (type === 'question' && !activeQuestionId) {
362
+ debugLog(`smartNotify: question handled during sound - aborting reminder`);
363
+ return;
364
+ }
331
365
 
332
366
  // Step 2: Schedule TTS reminder if user doesn't respond
333
367
  if (config.enableTTSReminder && ttsMessage) {
334
- scheduleTTSReminder(type, ttsMessage, { fallbackSound, permissionCount });
368
+ scheduleTTSReminder(type, ttsMessage, { fallbackSound, permissionCount, questionCount });
335
369
  }
336
370
 
337
371
  // Step 3: If TTS-first mode is enabled, also speak immediately
338
372
  if (config.notificationMode === 'tts-first' || config.notificationMode === 'both') {
339
- const immediateMessage = type === 'permission'
340
- ? getRandomMessage(config.permissionTTSMessages)
341
- : getRandomMessage(config.idleTTSMessages);
373
+ let immediateMessage;
374
+ if (type === 'permission') {
375
+ immediateMessage = getRandomMessage(config.permissionTTSMessages);
376
+ } else if (type === 'question') {
377
+ immediateMessage = getRandomMessage(config.questionTTSMessages);
378
+ } else {
379
+ immediateMessage = getRandomMessage(config.idleTTSMessages);
380
+ }
342
381
 
343
382
  await tts.speak(immediateMessage, {
344
383
  enableTTS: true,
@@ -378,6 +417,37 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
378
417
  }
379
418
  };
380
419
 
420
+ /**
421
+ * Get a count-aware TTS message for question requests (SDK v1.1.7+)
422
+ * @param {number} count - Number of question requests
423
+ * @param {boolean} isReminder - Whether this is a reminder message
424
+ * @returns {string} The formatted message
425
+ */
426
+ const getQuestionMessage = (count, isReminder = false) => {
427
+ const messages = isReminder
428
+ ? config.questionReminderTTSMessages
429
+ : config.questionTTSMessages;
430
+
431
+ if (count === 1) {
432
+ // Single question - use regular message
433
+ return getRandomMessage(messages);
434
+ } else {
435
+ // Multiple questions - use count-aware messages if available, or format dynamically
436
+ const countMessages = isReminder
437
+ ? config.questionReminderTTSMessagesMultiple
438
+ : config.questionTTSMessagesMultiple;
439
+
440
+ if (countMessages && countMessages.length > 0) {
441
+ // Use configured multi-question messages (replace {count} placeholder)
442
+ const template = getRandomMessage(countMessages);
443
+ return template.replace('{count}', count.toString());
444
+ } else {
445
+ // Fallback: generate a dynamic message
446
+ return `Hey! I have ${count} questions for you. Please check your screen.`;
447
+ }
448
+ }
449
+ };
450
+
381
451
  /**
382
452
  * Process the batched permission requests as a single notification
383
453
  * Called after the batch window expires
@@ -449,6 +519,81 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
449
519
  }
450
520
  };
451
521
 
522
+ /**
523
+ * Process the batched question requests as a single notification (SDK v1.1.7+)
524
+ * Called after the batch window expires
525
+ */
526
+ const processQuestionBatch = async () => {
527
+ // Capture and clear the batch
528
+ const batch = [...pendingQuestionBatch];
529
+ pendingQuestionBatch = [];
530
+ questionBatchTimeout = null;
531
+
532
+ if (batch.length === 0) {
533
+ debugLog('processQuestionBatch: empty batch, skipping');
534
+ return;
535
+ }
536
+
537
+ // Calculate total number of questions across all batched requests
538
+ // Each batch item is { id, questionCount } where questionCount is the number of questions in that request
539
+ const totalQuestionCount = batch.reduce((sum, item) => sum + (item.questionCount || 1), 0);
540
+
541
+ debugLog(`processQuestionBatch: processing ${batch.length} request(s) with ${totalQuestionCount} total question(s)`);
542
+
543
+ // Set activeQuestionId to the first one (for race condition checks)
544
+ // We track all IDs in the batch for proper cleanup
545
+ activeQuestionId = batch[0]?.id;
546
+
547
+ // Show toast with count
548
+ const toastMessage = totalQuestionCount === 1
549
+ ? "❓ The agent has a question for you"
550
+ : `❓ The agent has ${totalQuestionCount} questions for you`;
551
+ await showToast(toastMessage, "info", 8000);
552
+
553
+ // CHECK: Did user already respond while we were showing toast?
554
+ if (pendingQuestionBatch.length > 0) {
555
+ // New questions arrived during toast - they'll be handled in next batch
556
+ debugLog('processQuestionBatch: new questions arrived during toast');
557
+ }
558
+
559
+ // Check if any question was already replied to or rejected
560
+ if (activeQuestionId === null) {
561
+ debugLog('processQuestionBatch: aborted - user already responded');
562
+ return;
563
+ }
564
+
565
+ // Get count-aware TTS message (uses total question count, not request count)
566
+ const ttsMessage = getQuestionMessage(totalQuestionCount, false);
567
+ const reminderMessage = getQuestionMessage(totalQuestionCount, true);
568
+
569
+ // Smart notification: sound first, TTS reminder later
570
+ // Sound plays 2 times by default (matching permission behavior)
571
+ await smartNotify('question', {
572
+ soundFile: config.questionSound,
573
+ soundLoops: 2, // Fixed at 2 loops to match permission sound behavior
574
+ ttsMessage: reminderMessage,
575
+ fallbackSound: config.questionSound,
576
+ // Pass count for use in reminders
577
+ questionCount: totalQuestionCount
578
+ });
579
+
580
+ // Speak immediately if in TTS-first or both mode (with count-aware message)
581
+ if (config.notificationMode === 'tts-first' || config.notificationMode === 'both') {
582
+ await tts.wakeMonitor();
583
+ await tts.forceVolume();
584
+ await tts.speak(ttsMessage, {
585
+ enableTTS: true,
586
+ fallbackSound: config.questionSound
587
+ });
588
+ }
589
+
590
+ // Final check: if user responded during notification, cancel scheduled reminder
591
+ if (activeQuestionId === null) {
592
+ debugLog('processQuestionBatch: user responded during notification - cancelling reminder');
593
+ cancelPendingReminder('question');
594
+ }
595
+ };
596
+
452
597
  return {
453
598
  event: async ({ event }) => {
454
599
  try {
@@ -456,13 +601,16 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
456
601
  // USER ACTIVITY DETECTION
457
602
  // Cancels pending TTS reminders when user responds
458
603
  // ========================================
459
- // NOTE: OpenCode event types (supporting SDK v1.0.x and v1.1.x):
604
+ // NOTE: OpenCode event types (supporting SDK v1.0.x, v1.1.x, and v1.1.7+):
460
605
  // - message.updated: fires when a message is added/updated (use properties.info.role to check user vs assistant)
461
606
  // - permission.updated (SDK v1.0.x): fires when a permission request is created
462
607
  // - permission.asked (SDK v1.1.1+): fires when a permission request is created (replaces permission.updated)
463
608
  // - permission.replied: fires when user responds to a permission request
464
609
  // - SDK v1.0.x: uses permissionID, response
465
610
  // - SDK v1.1.1+: uses requestID, reply
611
+ // - question.asked (SDK v1.1.7+): fires when agent asks user a question
612
+ // - question.replied (SDK v1.1.7+): fires when user answers a question
613
+ // - question.rejected (SDK v1.1.7+): fires when user dismisses a question
466
614
  // - session.created: fires when a new session starts
467
615
  //
468
616
  // CRITICAL: message.updated fires for EVERY modification to a message (not just creation).
@@ -546,6 +694,7 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
546
694
  lastUserActivityTime = Date.now();
547
695
  lastSessionIdleTime = 0;
548
696
  activePermissionId = null;
697
+ activeQuestionId = null;
549
698
  seenUserMessageIds.clear();
550
699
  cancelAllPendingReminders();
551
700
 
@@ -556,6 +705,13 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
556
705
  permissionBatchTimeout = null;
557
706
  }
558
707
 
708
+ // Reset question batch state
709
+ pendingQuestionBatch = [];
710
+ if (questionBatchTimeout) {
711
+ clearTimeout(questionBatchTimeout);
712
+ questionBatchTimeout = null;
713
+ }
714
+
559
715
  debugLog(`Session created: ${event.type} - reset all tracking state`);
560
716
  }
561
717
 
@@ -633,6 +789,113 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
633
789
 
634
790
  debugLog(`${event.type}: batch window reset (will process in ${PERMISSION_BATCH_WINDOW_MS}ms if no more arrive)`);
635
791
  }
792
+
793
+ // ========================================
794
+ // NOTIFICATION 3: Question Request (BATCHED) - SDK v1.1.7+
795
+ // ========================================
796
+ // The "question" tool allows the LLM to ask users questions during execution.
797
+ // Events: question.asked, question.replied, question.rejected
798
+ //
799
+ // BATCHING: When multiple question requests arrive simultaneously,
800
+ // we batch them into a single notification instead of playing overlapping sounds.
801
+ // NOTE: Each question.asked event can contain multiple questions in its questions array.
802
+ if (event.type === "question.asked") {
803
+ // Capture question request ID and count of questions in this request
804
+ const questionId = event.properties?.id;
805
+ const questionsArray = event.properties?.questions;
806
+ const questionCount = Array.isArray(questionsArray) ? questionsArray.length : 1;
807
+
808
+ if (!questionId) {
809
+ debugLog(`${event.type}: question ID missing. properties keys: ` + Object.keys(event.properties || {}).join(', '));
810
+ }
811
+
812
+ // Add to the pending batch (avoid duplicates by checking ID)
813
+ // Store as object with id and questionCount for proper counting
814
+ const existingIndex = pendingQuestionBatch.findIndex(item => item.id === questionId);
815
+ if (questionId && existingIndex === -1) {
816
+ pendingQuestionBatch.push({ id: questionId, questionCount });
817
+ debugLog(`${event.type}: added ${questionId} with ${questionCount} question(s) to batch (now ${pendingQuestionBatch.length} request(s) pending)`);
818
+ } else if (!questionId) {
819
+ // If no ID, still count it (use a placeholder)
820
+ pendingQuestionBatch.push({ id: `unknown-${Date.now()}`, questionCount });
821
+ debugLog(`${event.type}: added unknown question request with ${questionCount} question(s) to batch (now ${pendingQuestionBatch.length} request(s) pending)`);
822
+ }
823
+
824
+ // Reset the batch window timer (debounce)
825
+ // This gives more questions a chance to arrive before we notify
826
+ if (questionBatchTimeout) {
827
+ clearTimeout(questionBatchTimeout);
828
+ }
829
+
830
+ questionBatchTimeout = setTimeout(async () => {
831
+ try {
832
+ await processQuestionBatch();
833
+ } catch (e) {
834
+ debugLog(`processQuestionBatch error: ${e.message}`);
835
+ }
836
+ }, QUESTION_BATCH_WINDOW_MS);
837
+
838
+ debugLog(`${event.type}: batch window reset (will process in ${QUESTION_BATCH_WINDOW_MS}ms if no more arrive)`);
839
+ }
840
+
841
+ // Handle question.replied - user answered the question(s)
842
+ if (event.type === "question.replied") {
843
+ const repliedQuestionId = event.properties?.requestID;
844
+ const answers = event.properties?.answers;
845
+
846
+ // Remove this question from the pending batch (if still waiting)
847
+ // pendingQuestionBatch is now an array of { id, questionCount } objects
848
+ const existingIndex = pendingQuestionBatch.findIndex(item => item.id === repliedQuestionId);
849
+ if (repliedQuestionId && existingIndex !== -1) {
850
+ pendingQuestionBatch.splice(existingIndex, 1);
851
+ debugLog(`Question replied: removed ${repliedQuestionId} from pending batch (${pendingQuestionBatch.length} remaining)`);
852
+ }
853
+
854
+ // If batch is now empty and we have a pending batch timeout, we can cancel it
855
+ if (pendingQuestionBatch.length === 0 && questionBatchTimeout) {
856
+ clearTimeout(questionBatchTimeout);
857
+ questionBatchTimeout = null;
858
+ debugLog('Question replied: cancelled batch timeout (all questions handled)');
859
+ }
860
+
861
+ // Clear active question ID
862
+ if (activeQuestionId === repliedQuestionId || activeQuestionId === undefined) {
863
+ activeQuestionId = null;
864
+ debugLog(`Question replied: cleared activeQuestionId ${repliedQuestionId || '(unknown)'}`);
865
+ }
866
+ lastUserActivityTime = Date.now();
867
+ cancelPendingReminder('question'); // Cancel question-specific reminder
868
+ debugLog(`Question replied: ${event.type} (answers=${JSON.stringify(answers)}) - cancelled question reminder`);
869
+ }
870
+
871
+ // Handle question.rejected - user dismissed the question
872
+ if (event.type === "question.rejected") {
873
+ const rejectedQuestionId = event.properties?.requestID;
874
+
875
+ // Remove this question from the pending batch (if still waiting)
876
+ // pendingQuestionBatch is now an array of { id, questionCount } objects
877
+ const existingIndex = pendingQuestionBatch.findIndex(item => item.id === rejectedQuestionId);
878
+ if (rejectedQuestionId && existingIndex !== -1) {
879
+ pendingQuestionBatch.splice(existingIndex, 1);
880
+ debugLog(`Question rejected: removed ${rejectedQuestionId} from pending batch (${pendingQuestionBatch.length} remaining)`);
881
+ }
882
+
883
+ // If batch is now empty and we have a pending batch timeout, we can cancel it
884
+ if (pendingQuestionBatch.length === 0 && questionBatchTimeout) {
885
+ clearTimeout(questionBatchTimeout);
886
+ questionBatchTimeout = null;
887
+ debugLog('Question rejected: cancelled batch timeout (all questions handled)');
888
+ }
889
+
890
+ // Clear active question ID
891
+ if (activeQuestionId === rejectedQuestionId || activeQuestionId === undefined) {
892
+ activeQuestionId = null;
893
+ debugLog(`Question rejected: cleared activeQuestionId ${rejectedQuestionId || '(unknown)'}`);
894
+ }
895
+ lastUserActivityTime = Date.now();
896
+ cancelPendingReminder('question'); // Cancel question-specific reminder
897
+ debugLog(`Question rejected: ${event.type} - cancelled question reminder`);
898
+ }
636
899
  } catch (e) {
637
900
  debugLog(`event handler error: ${e.message}`);
638
901
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-smart-voice-notify",
3
- "version": "1.0.13",
3
+ "version": "1.1.1",
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
@@ -241,6 +241,55 @@ const generateDefaultConfig = (overrides = {}, version = '1.0.0') => {
241
241
  // Batch window (ms) - how long to wait for more permissions before notifying
242
242
  "permissionBatchWindowMs": ${overrides.permissionBatchWindowMs !== undefined ? overrides.permissionBatchWindowMs : 800},
243
243
 
244
+ // ============================================================
245
+ // QUESTION TOOL SETTINGS (SDK v1.1.7+ - Agent asking user questions)
246
+ // ============================================================
247
+ // The "question" tool allows the LLM to ask users questions during execution.
248
+ // This is useful for gathering preferences, clarifying instructions, or getting
249
+ // decisions on implementation choices.
250
+
251
+ // Messages when agent asks user a question
252
+ "questionTTSMessages": ${formatJSON(overrides.questionTTSMessages || [
253
+ "Hey! I have a question for you. Please check your screen.",
254
+ "Attention! I need your input to continue.",
255
+ "Quick question! Please take a look when you have a moment.",
256
+ "I need some clarification. Could you please respond?",
257
+ "Question time! Your input is needed to proceed."
258
+ ], 4)},
259
+
260
+ // Messages for MULTIPLE questions (use {count} placeholder)
261
+ "questionTTSMessagesMultiple": ${formatJSON(overrides.questionTTSMessagesMultiple || [
262
+ "Hey! I have {count} questions for you. Please check your screen.",
263
+ "Attention! I need your input on {count} items to continue.",
264
+ "{count} questions need your attention. Please take a look!",
265
+ "I need some clarifications. There are {count} questions waiting for you.",
266
+ "Question time! {count} questions need your response to proceed."
267
+ ], 4)},
268
+
269
+ // Reminder messages for questions (more urgent - used after delay)
270
+ "questionReminderTTSMessages": ${formatJSON(overrides.questionReminderTTSMessages || [
271
+ "Hey! I am still waiting for your answer. Please check the questions!",
272
+ "Reminder: There is a question waiting for your response.",
273
+ "Hello? I need your input to continue. Please respond when you can.",
274
+ "Still waiting for your answer! The task is on hold.",
275
+ "Your input is needed! Please check the pending question."
276
+ ], 4)},
277
+
278
+ // Reminder messages for MULTIPLE questions (use {count} placeholder)
279
+ "questionReminderTTSMessagesMultiple": ${formatJSON(overrides.questionReminderTTSMessagesMultiple || [
280
+ "Hey! I am still waiting for answers to {count} questions. Please respond!",
281
+ "Reminder: There are {count} questions waiting for your response.",
282
+ "Hello? I need your input on {count} items. Please respond when you can.",
283
+ "Still waiting for your answers on {count} questions! The task is on hold.",
284
+ "Your input is needed! {count} questions are pending your response."
285
+ ], 4)},
286
+
287
+ // Delay (in seconds) before question reminder fires
288
+ "questionReminderDelaySeconds": ${overrides.questionReminderDelaySeconds !== undefined ? overrides.questionReminderDelaySeconds : 25},
289
+
290
+ // Question batch window (ms) - how long to wait for more questions before notifying
291
+ "questionBatchWindowMs": ${overrides.questionBatchWindowMs !== undefined ? overrides.questionBatchWindowMs : 800},
292
+
244
293
  // ============================================================
245
294
  // SOUND FILES (For immediate notifications)
246
295
  // These are played first before TTS reminder kicks in
@@ -251,6 +300,7 @@ const generateDefaultConfig = (overrides = {}, version = '1.0.0') => {
251
300
 
252
301
  "idleSound": "${overrides.idleSound || 'assets/Soft-high-tech-notification-sound-effect.mp3'}",
253
302
  "permissionSound": "${overrides.permissionSound || 'assets/Machine-alert-beep-sound-effect.mp3'}",
303
+ "questionSound": "${overrides.questionSound || 'assets/Machine-alert-beep-sound-effect.mp3'}",
254
304
 
255
305
  // ============================================================
256
306
  // 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