opencode-smart-voice-notify 1.2.0 → 1.2.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/index.js +117 -76
- package/package.json +1 -1
- package/util/ai-messages.js +14 -16
package/index.js
CHANGED
|
@@ -357,8 +357,8 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
|
|
|
357
357
|
soundLoops = 1,
|
|
358
358
|
ttsMessage,
|
|
359
359
|
fallbackSound,
|
|
360
|
-
permissionCount
|
|
361
|
-
questionCount
|
|
360
|
+
permissionCount, // Support permission count for batched notifications
|
|
361
|
+
questionCount // Support question count for batched notifications
|
|
362
362
|
} = options;
|
|
363
363
|
|
|
364
364
|
// Step 1: Play the immediate sound notification
|
|
@@ -418,27 +418,30 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
|
|
|
418
418
|
? config.permissionReminderTTSMessages
|
|
419
419
|
: config.permissionTTSMessages;
|
|
420
420
|
|
|
421
|
+
// If AI messages are enabled, ALWAYS try AI first (regardless of count)
|
|
422
|
+
if (config.enableAIMessages) {
|
|
423
|
+
const aiMessage = await getSmartMessage('permission', isReminder, messages, { count, type: 'permission' });
|
|
424
|
+
// getSmartMessage returns static message as fallback, so if AI was attempted
|
|
425
|
+
// and succeeded, we'll get the AI message. If it failed, we get static.
|
|
426
|
+
// Check if we got a valid message (not the generic fallback)
|
|
427
|
+
if (aiMessage && aiMessage !== 'Notification') {
|
|
428
|
+
return aiMessage;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Fallback to static messages (AI disabled or failed with generic fallback)
|
|
421
433
|
if (count === 1) {
|
|
422
|
-
|
|
423
|
-
return await getSmartMessage('permission', isReminder, messages, { count });
|
|
434
|
+
return getRandomMessage(messages);
|
|
424
435
|
} else {
|
|
425
|
-
// Multiple permissions - use count-aware messages if available, or format dynamically
|
|
426
436
|
const countMessages = isReminder
|
|
427
437
|
? config.permissionReminderTTSMessagesMultiple
|
|
428
438
|
: config.permissionTTSMessagesMultiple;
|
|
429
439
|
|
|
430
440
|
if (countMessages && countMessages.length > 0) {
|
|
431
|
-
// Use configured multi-permission messages (replace {count} placeholder)
|
|
432
441
|
const template = getRandomMessage(countMessages);
|
|
433
442
|
return template.replace('{count}', count.toString());
|
|
434
|
-
} else {
|
|
435
|
-
// Try AI message with count context, fallback to dynamic message
|
|
436
|
-
const aiMessage = await getSmartMessage('permission', isReminder, [], { count });
|
|
437
|
-
if (aiMessage !== 'Notification') {
|
|
438
|
-
return aiMessage;
|
|
439
|
-
}
|
|
440
|
-
return `Attention! There are ${count} permission requests waiting for your approval.`;
|
|
441
443
|
}
|
|
444
|
+
return `Attention! There are ${count} permission requests waiting for your approval.`;
|
|
442
445
|
}
|
|
443
446
|
};
|
|
444
447
|
|
|
@@ -454,33 +457,39 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
|
|
|
454
457
|
? config.questionReminderTTSMessages
|
|
455
458
|
: config.questionTTSMessages;
|
|
456
459
|
|
|
460
|
+
// If AI messages are enabled, ALWAYS try AI first (regardless of count)
|
|
461
|
+
if (config.enableAIMessages) {
|
|
462
|
+
const aiMessage = await getSmartMessage('question', isReminder, messages, { count, type: 'question' });
|
|
463
|
+
// getSmartMessage returns static message as fallback, so if AI was attempted
|
|
464
|
+
// and succeeded, we'll get the AI message. If it failed, we get static.
|
|
465
|
+
// Check if we got a valid message (not the generic fallback)
|
|
466
|
+
if (aiMessage && aiMessage !== 'Notification') {
|
|
467
|
+
return aiMessage;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Fallback to static messages (AI disabled or failed with generic fallback)
|
|
457
472
|
if (count === 1) {
|
|
458
|
-
|
|
459
|
-
return await getSmartMessage('question', isReminder, messages, { count });
|
|
473
|
+
return getRandomMessage(messages);
|
|
460
474
|
} else {
|
|
461
|
-
// Multiple questions - use count-aware messages if available, or format dynamically
|
|
462
475
|
const countMessages = isReminder
|
|
463
476
|
? config.questionReminderTTSMessagesMultiple
|
|
464
477
|
: config.questionTTSMessagesMultiple;
|
|
465
478
|
|
|
466
479
|
if (countMessages && countMessages.length > 0) {
|
|
467
|
-
// Use configured multi-question messages (replace {count} placeholder)
|
|
468
480
|
const template = getRandomMessage(countMessages);
|
|
469
481
|
return template.replace('{count}', count.toString());
|
|
470
|
-
} else {
|
|
471
|
-
// Try AI message with count context, fallback to dynamic message
|
|
472
|
-
const aiMessage = await getSmartMessage('question', isReminder, [], { count });
|
|
473
|
-
if (aiMessage !== 'Notification') {
|
|
474
|
-
return aiMessage;
|
|
475
|
-
}
|
|
476
|
-
return `Hey! I have ${count} questions for you. Please check your screen.`;
|
|
477
482
|
}
|
|
483
|
+
return `Hey! I have ${count} questions for you. Please check your screen.`;
|
|
478
484
|
}
|
|
479
485
|
};
|
|
480
486
|
|
|
481
487
|
/**
|
|
482
488
|
* Process the batched permission requests as a single notification
|
|
483
489
|
* Called after the batch window expires
|
|
490
|
+
*
|
|
491
|
+
* FIX: Play sound IMMEDIATELY before any AI generation to avoid delay.
|
|
492
|
+
* AI message generation can take 3-15+ seconds, which was delaying sound playback.
|
|
484
493
|
*/
|
|
485
494
|
const processPermissionBatch = async () => {
|
|
486
495
|
// Capture and clear the batch
|
|
@@ -500,40 +509,42 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
|
|
|
500
509
|
// We track all IDs in the batch for proper cleanup
|
|
501
510
|
activePermissionId = batch[0];
|
|
502
511
|
|
|
503
|
-
// Show toast
|
|
512
|
+
// Step 1: Show toast IMMEDIATELY (fire and forget - no await)
|
|
504
513
|
const toastMessage = batchCount === 1
|
|
505
514
|
? "⚠️ Permission request requires your attention"
|
|
506
515
|
: `⚠️ ${batchCount} permission requests require your attention`;
|
|
507
|
-
|
|
516
|
+
showToast(toastMessage, "warning", 8000); // No await - instant display
|
|
517
|
+
|
|
518
|
+
// Step 2: Play sound (after toast is triggered)
|
|
519
|
+
const soundLoops = batchCount === 1 ? 2 : Math.min(3, batchCount);
|
|
520
|
+
await playSound(config.permissionSound, soundLoops);
|
|
508
521
|
|
|
509
|
-
// CHECK: Did user already respond while
|
|
522
|
+
// CHECK: Did user already respond while sound was playing?
|
|
510
523
|
if (pendingPermissionBatch.length > 0) {
|
|
511
|
-
// New permissions arrived during
|
|
512
|
-
debugLog('processPermissionBatch: new permissions arrived during
|
|
524
|
+
// New permissions arrived during sound - they'll be handled in next batch
|
|
525
|
+
debugLog('processPermissionBatch: new permissions arrived during sound');
|
|
513
526
|
}
|
|
514
527
|
|
|
515
|
-
// Check
|
|
528
|
+
// Step 3: Check race condition - did user respond during sound?
|
|
516
529
|
if (activePermissionId === null) {
|
|
517
|
-
debugLog('processPermissionBatch:
|
|
530
|
+
debugLog('processPermissionBatch: user responded during sound - aborting');
|
|
518
531
|
return;
|
|
519
532
|
}
|
|
520
533
|
|
|
521
|
-
//
|
|
522
|
-
const ttsMessage = await getPermissionMessage(batchCount, false);
|
|
534
|
+
// Step 4: Generate AI message for reminder AFTER sound played
|
|
523
535
|
const reminderMessage = await getPermissionMessage(batchCount, true);
|
|
524
|
-
|
|
525
|
-
// Smart notification: sound first, TTS reminder later
|
|
526
|
-
await smartNotify('permission', {
|
|
527
|
-
soundFile: config.permissionSound,
|
|
528
|
-
soundLoops: batchCount === 1 ? 2 : Math.min(3, batchCount), // More loops for more permissions
|
|
529
|
-
ttsMessage: reminderMessage,
|
|
530
|
-
fallbackSound: config.permissionSound,
|
|
531
|
-
// Pass count for potential use in notification
|
|
532
|
-
permissionCount: batchCount
|
|
533
|
-
});
|
|
534
536
|
|
|
535
|
-
//
|
|
537
|
+
// Step 5: Schedule TTS reminder if enabled
|
|
538
|
+
if (config.enableTTSReminder && reminderMessage) {
|
|
539
|
+
scheduleTTSReminder('permission', reminderMessage, {
|
|
540
|
+
fallbackSound: config.permissionSound,
|
|
541
|
+
permissionCount: batchCount
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Step 6: If TTS-first or both mode, generate and speak immediate message
|
|
536
546
|
if (config.notificationMode === 'tts-first' || config.notificationMode === 'both') {
|
|
547
|
+
const ttsMessage = await getPermissionMessage(batchCount, false);
|
|
537
548
|
await tts.wakeMonitor();
|
|
538
549
|
await tts.forceVolume();
|
|
539
550
|
await tts.speak(ttsMessage, {
|
|
@@ -552,6 +563,9 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
|
|
|
552
563
|
/**
|
|
553
564
|
* Process the batched question requests as a single notification (SDK v1.1.7+)
|
|
554
565
|
* Called after the batch window expires
|
|
566
|
+
*
|
|
567
|
+
* FIX: Play sound IMMEDIATELY before any AI generation to avoid delay.
|
|
568
|
+
* AI message generation can take 3-15+ seconds, which was delaying sound playback.
|
|
555
569
|
*/
|
|
556
570
|
const processQuestionBatch = async () => {
|
|
557
571
|
// Capture and clear the batch
|
|
@@ -574,41 +588,41 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
|
|
|
574
588
|
// We track all IDs in the batch for proper cleanup
|
|
575
589
|
activeQuestionId = batch[0]?.id;
|
|
576
590
|
|
|
577
|
-
// Show toast
|
|
591
|
+
// Step 1: Show toast IMMEDIATELY (fire and forget - no await)
|
|
578
592
|
const toastMessage = totalQuestionCount === 1
|
|
579
593
|
? "❓ The agent has a question for you"
|
|
580
594
|
: `❓ The agent has ${totalQuestionCount} questions for you`;
|
|
581
|
-
|
|
595
|
+
showToast(toastMessage, "info", 8000); // No await - instant display
|
|
596
|
+
|
|
597
|
+
// Step 2: Play sound (after toast is triggered)
|
|
598
|
+
await playSound(config.questionSound, 2);
|
|
582
599
|
|
|
583
|
-
// CHECK: Did user already respond while
|
|
600
|
+
// CHECK: Did user already respond while sound was playing?
|
|
584
601
|
if (pendingQuestionBatch.length > 0) {
|
|
585
|
-
// New questions arrived during
|
|
586
|
-
debugLog('processQuestionBatch: new questions arrived during
|
|
602
|
+
// New questions arrived during sound - they'll be handled in next batch
|
|
603
|
+
debugLog('processQuestionBatch: new questions arrived during sound');
|
|
587
604
|
}
|
|
588
605
|
|
|
589
|
-
// Check
|
|
606
|
+
// Step 3: Check race condition - did user respond during sound?
|
|
590
607
|
if (activeQuestionId === null) {
|
|
591
|
-
debugLog('processQuestionBatch:
|
|
608
|
+
debugLog('processQuestionBatch: user responded during sound - aborting');
|
|
592
609
|
return;
|
|
593
610
|
}
|
|
594
611
|
|
|
595
|
-
//
|
|
596
|
-
const ttsMessage = await getQuestionMessage(totalQuestionCount, false);
|
|
612
|
+
// Step 4: Generate AI message for reminder AFTER sound played
|
|
597
613
|
const reminderMessage = await getQuestionMessage(totalQuestionCount, true);
|
|
598
614
|
|
|
599
|
-
//
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
// Pass count for use in reminders
|
|
607
|
-
questionCount: totalQuestionCount
|
|
608
|
-
});
|
|
615
|
+
// Step 5: Schedule TTS reminder if enabled
|
|
616
|
+
if (config.enableTTSReminder && reminderMessage) {
|
|
617
|
+
scheduleTTSReminder('question', reminderMessage, {
|
|
618
|
+
fallbackSound: config.questionSound,
|
|
619
|
+
questionCount: totalQuestionCount
|
|
620
|
+
});
|
|
621
|
+
}
|
|
609
622
|
|
|
610
|
-
//
|
|
623
|
+
// Step 6: If TTS-first or both mode, generate and speak immediate message
|
|
611
624
|
if (config.notificationMode === 'tts-first' || config.notificationMode === 'both') {
|
|
625
|
+
const ttsMessage = await getQuestionMessage(totalQuestionCount, false);
|
|
612
626
|
await tts.wakeMonitor();
|
|
613
627
|
await tts.forceVolume();
|
|
614
628
|
await tts.speak(ttsMessage, {
|
|
@@ -747,6 +761,9 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
|
|
|
747
761
|
|
|
748
762
|
// ========================================
|
|
749
763
|
// NOTIFICATION 1: Session Idle (Agent Finished)
|
|
764
|
+
//
|
|
765
|
+
// FIX: Play sound IMMEDIATELY before any AI generation to avoid delay.
|
|
766
|
+
// AI message generation can take 3-15+ seconds, which was delaying sound playback.
|
|
750
767
|
// ========================================
|
|
751
768
|
if (event.type === "session.idle") {
|
|
752
769
|
const sessionID = event.properties?.sessionID;
|
|
@@ -764,18 +781,42 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
|
|
|
764
781
|
lastSessionIdleTime = Date.now();
|
|
765
782
|
|
|
766
783
|
debugLog(`session.idle: notifying for session ${sessionID} (idleTime=${lastSessionIdleTime})`);
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
//
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
784
|
+
|
|
785
|
+
// Step 1: Show toast IMMEDIATELY (fire and forget - no await)
|
|
786
|
+
showToast("✅ Agent has finished working", "success", 5000); // No await - instant display
|
|
787
|
+
|
|
788
|
+
// Step 2: Play sound (after toast is triggered)
|
|
789
|
+
// Only play sound in sound-first, sound-only, or both mode
|
|
790
|
+
if (config.notificationMode !== 'tts-first') {
|
|
791
|
+
await playSound(config.idleSound, 1);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// Step 3: Check race condition - did user respond during sound?
|
|
795
|
+
if (lastUserActivityTime > lastSessionIdleTime) {
|
|
796
|
+
debugLog(`session.idle: user active during sound - aborting`);
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// Step 4: Generate AI message for reminder AFTER sound played
|
|
801
|
+
const reminderMessage = await getSmartMessage('idle', true, config.idleReminderTTSMessages);
|
|
802
|
+
|
|
803
|
+
// Step 5: Schedule TTS reminder if enabled
|
|
804
|
+
if (config.enableTTSReminder && reminderMessage) {
|
|
805
|
+
scheduleTTSReminder('idle', reminderMessage, {
|
|
806
|
+
fallbackSound: config.idleSound
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// Step 6: If TTS-first or both mode, generate and speak immediate message
|
|
811
|
+
if (config.notificationMode === 'tts-first' || config.notificationMode === 'both') {
|
|
812
|
+
const ttsMessage = await getSmartMessage('idle', false, config.idleTTSMessages);
|
|
813
|
+
await tts.wakeMonitor();
|
|
814
|
+
await tts.forceVolume();
|
|
815
|
+
await tts.speak(ttsMessage, {
|
|
816
|
+
enableTTS: true,
|
|
817
|
+
fallbackSound: config.idleSound
|
|
818
|
+
});
|
|
819
|
+
}
|
|
779
820
|
}
|
|
780
821
|
|
|
781
822
|
// ========================================
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-smart-voice-notify",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.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/ai-messages.js
CHANGED
|
@@ -24,12 +24,23 @@ export async function generateAIMessage(promptType, context = {}) {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
// Get the prompt for this type
|
|
27
|
-
|
|
27
|
+
let prompt = config.aiPrompts?.[promptType];
|
|
28
28
|
if (!prompt) {
|
|
29
|
-
console.error(`[AI Messages] No prompt configured for type: ${promptType}`);
|
|
30
29
|
return null;
|
|
31
30
|
}
|
|
32
31
|
|
|
32
|
+
// Inject count context if multiple items
|
|
33
|
+
if (context.count && context.count > 1) {
|
|
34
|
+
// Use type-specific terminology
|
|
35
|
+
let itemType = 'items';
|
|
36
|
+
if (context.type === 'question') {
|
|
37
|
+
itemType = 'questions';
|
|
38
|
+
} else if (context.type === 'permission') {
|
|
39
|
+
itemType = 'permission requests';
|
|
40
|
+
}
|
|
41
|
+
prompt = `${prompt} Important: There are ${context.count} ${itemType} (not just one) waiting for the user's attention. Mention the count in your message.`;
|
|
42
|
+
}
|
|
43
|
+
|
|
33
44
|
try {
|
|
34
45
|
// Build headers
|
|
35
46
|
const headers = { 'Content-Type': 'application/json' };
|
|
@@ -72,8 +83,6 @@ export async function generateAIMessage(promptType, context = {}) {
|
|
|
72
83
|
clearTimeout(timeout);
|
|
73
84
|
|
|
74
85
|
if (!response.ok) {
|
|
75
|
-
const errorText = await response.text().catch(() => 'Unknown error');
|
|
76
|
-
console.error(`[AI Messages] API error ${response.status}: ${errorText}`);
|
|
77
86
|
return null;
|
|
78
87
|
}
|
|
79
88
|
|
|
@@ -83,7 +92,6 @@ export async function generateAIMessage(promptType, context = {}) {
|
|
|
83
92
|
const message = data.choices?.[0]?.message?.content?.trim();
|
|
84
93
|
|
|
85
94
|
if (!message) {
|
|
86
|
-
console.error('[AI Messages] Empty response from AI');
|
|
87
95
|
return null;
|
|
88
96
|
}
|
|
89
97
|
|
|
@@ -92,18 +100,12 @@ export async function generateAIMessage(promptType, context = {}) {
|
|
|
92
100
|
|
|
93
101
|
// Validate message length (sanity check)
|
|
94
102
|
if (cleanMessage.length < 5 || cleanMessage.length > 200) {
|
|
95
|
-
console.error(`[AI Messages] Message length invalid: ${cleanMessage.length} chars`);
|
|
96
103
|
return null;
|
|
97
104
|
}
|
|
98
105
|
|
|
99
106
|
return cleanMessage;
|
|
100
107
|
|
|
101
108
|
} catch (error) {
|
|
102
|
-
if (error.name === 'AbortError') {
|
|
103
|
-
console.error(`[AI Messages] Request timed out after ${config.aiTimeout || 15000}ms`);
|
|
104
|
-
} else {
|
|
105
|
-
console.error(`[AI Messages] Error: ${error.message}`);
|
|
106
|
-
}
|
|
107
109
|
return null;
|
|
108
110
|
}
|
|
109
111
|
}
|
|
@@ -127,14 +129,10 @@ export async function getSmartMessage(eventType, isReminder, staticMessages, con
|
|
|
127
129
|
try {
|
|
128
130
|
const aiMessage = await generateAIMessage(promptType, context);
|
|
129
131
|
if (aiMessage) {
|
|
130
|
-
// Log success for debugging
|
|
131
|
-
if (config.debugLog) {
|
|
132
|
-
console.log(`[AI Messages] Generated: ${aiMessage}`);
|
|
133
|
-
}
|
|
134
132
|
return aiMessage;
|
|
135
133
|
}
|
|
136
134
|
} catch (error) {
|
|
137
|
-
|
|
135
|
+
// Silently fall through to fallback
|
|
138
136
|
}
|
|
139
137
|
|
|
140
138
|
// Check if fallback is disabled
|