pi-smart-voice-notify 0.3.4 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/README.md +9 -9
- package/config/config.example.json +4 -4
- package/package.json +4 -4
- package/src/ai-messages.ts +1 -1
- package/src/config-store.ts +38 -19
- package/src/index.test.ts +177 -10
- package/src/index.ts +84 -57
- package/src/logging.ts +2 -3
- package/src/permission-forwarding-watcher.test.ts +180 -0
- package/src/permission-forwarding-watcher.ts +577 -378
- package/src/webhook.ts +2 -2
- package/assets/pi-smart-voice-notify.png +0 -0
- /package/assets/{Machine-alert-beep-sound-effect.mp3 → attention-alert.mp3} +0 -0
- /package/assets/{Soft-high-tech-notification-sound-effect.mp3 → soft-notification.mp3} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.0] - 2026-04-30
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Detect agent turn failures reported at `agent_end` and send error notifications instead of completion alerts.
|
|
7
|
+
|
|
8
|
+
### Changed
|
|
9
|
+
- Renamed bundled notification sound assets to stable lowercase filenames and migrate legacy bundled sound paths automatically.
|
|
10
|
+
- Updated `@mariozechner/pi-*` peer dependency ranges to `^0.70.6` and synchronized package lock metadata.
|
|
11
|
+
|
|
12
|
+
## [0.3.5] - 2026-04-27
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- Permission notifications now require an authoritative `pi-permission-system:permission-request` waiting event before alerts or reminders are queued, preventing permission-looking `tool_call` and `tool_result` payloads from producing false permission alerts.
|
|
16
|
+
- Forwarded permission notifications now watch only the active session's scoped request/response directories, require matching `targetSessionId`, and ignore unscoped legacy paths, stale requests, resolved requests, malformed files, and mismatched request filenames.
|
|
17
|
+
|
|
3
18
|
## [0.3.4] - 2026-04-25
|
|
4
19
|
|
|
5
20
|
### Added
|
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ Windows-optimized smart notification extension for the Pi coding agent.
|
|
|
19
19
|
|
|
20
20
|
- **Intelligent event detection**
|
|
21
21
|
- Task completion (idle)
|
|
22
|
-
-
|
|
22
|
+
- Authoritative permission-system wait events plus current-session forwarded subagent permission requests
|
|
23
23
|
- Questions requiring input (when custom `question` tool is loaded)
|
|
24
24
|
- Errors
|
|
25
25
|
|
|
@@ -125,17 +125,17 @@ A starter template is provided in `config/config.example.json`. On startup, the
|
|
|
125
125
|
| Option | Type | Default | Description |
|
|
126
126
|
|--------|------|---------|-------------|
|
|
127
127
|
| `enableIdleNotification` | boolean | `true` | Notify when agent finishes a task |
|
|
128
|
-
| `enablePermissionNotification` | boolean | `true` | Notify
|
|
129
|
-
| `enableForwardedPermissionWatcher` | boolean | `true` | Watch forwarded permission request files and notify when new requests arrive |
|
|
128
|
+
| `enablePermissionNotification` | boolean | `true` | Notify when the permission system reports requests waiting for approval |
|
|
129
|
+
| `enableForwardedPermissionWatcher` | boolean | `true` | Watch current-session forwarded permission request/response files and notify when new requests arrive |
|
|
130
130
|
| `includeForwardedPermissionAgentName` | boolean | `true` | Include sanitized requester agent name in forwarded permission notification text |
|
|
131
|
-
| `watchLegacyForwardedPermissionPath` | boolean | `true` |
|
|
131
|
+
| `watchLegacyForwardedPermissionPath` | boolean | `true` | Retained for config compatibility; unscoped legacy forwarded-permission paths are ignored because they cannot prove the current target session |
|
|
132
132
|
| `enableQuestionNotification` | boolean | `true` | Notify when agent asks a question* |
|
|
133
133
|
| `enableErrorNotification` | boolean | `true` | Notify on errors |
|
|
134
134
|
| `suppressIdleAfterError` | boolean | `true` | Skip idle notification if turn had errors |
|
|
135
135
|
|
|
136
136
|
*Question notifications only work when a custom `question` tool is loaded.
|
|
137
137
|
|
|
138
|
-
Forwarded permission watcher notifications use privacy-safe text and never include raw forwarded `message` content.
|
|
138
|
+
Forwarded permission watcher notifications use privacy-safe text, require the request `targetSessionId` to match the active Pi session, and never include raw forwarded `message` content.
|
|
139
139
|
|
|
140
140
|
### Reminder Settings
|
|
141
141
|
|
|
@@ -166,10 +166,10 @@ Forwarded permission watcher notifications use privacy-safe text and never inclu
|
|
|
166
166
|
|
|
167
167
|
| Option | Type | Default |
|
|
168
168
|
|--------|------|---------|
|
|
169
|
-
| `idleSoundFile` | string | `"assets/
|
|
170
|
-
| `permissionSoundFile` | string | `"assets/
|
|
171
|
-
| `questionSoundFile` | string | `"assets/
|
|
172
|
-
| `errorSoundFile` | string | `"assets/
|
|
169
|
+
| `idleSoundFile` | string | `"assets/soft-notification.mp3"` |
|
|
170
|
+
| `permissionSoundFile` | string | `"assets/attention-alert.mp3"` |
|
|
171
|
+
| `questionSoundFile` | string | `"assets/attention-alert.mp3"` |
|
|
172
|
+
| `errorSoundFile` | string | `"assets/attention-alert.mp3"` |
|
|
173
173
|
|
|
174
174
|
Paths can be absolute or relative to the extension directory.
|
|
175
175
|
|
|
@@ -94,10 +94,10 @@
|
|
|
94
94
|
"openaiTtsSpeed": 1,
|
|
95
95
|
|
|
96
96
|
"_comment_sounds": "Direct sound file fallbacks.",
|
|
97
|
-
"idleSoundFile": "assets/
|
|
98
|
-
"permissionSoundFile": "assets/
|
|
99
|
-
"questionSoundFile": "assets/
|
|
100
|
-
"errorSoundFile": "assets/
|
|
97
|
+
"idleSoundFile": "assets/soft-notification.mp3",
|
|
98
|
+
"permissionSoundFile": "assets/attention-alert.mp3",
|
|
99
|
+
"questionSoundFile": "assets/attention-alert.mp3",
|
|
100
|
+
"errorSoundFile": "assets/attention-alert.mp3",
|
|
101
101
|
|
|
102
102
|
"_comment_sound_theme": "Sound theme settings.",
|
|
103
103
|
"themePath": "",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-smart-voice-notify",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Windows-optimized smart voice, sound, and desktop notifications for Pi coding agent.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.ts",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"scripts": {
|
|
20
20
|
"build": "npx --yes -p typescript@5.7.3 -p @types/node@20.17.57 tsc -p tsconfig.json --noEmit",
|
|
21
21
|
"lint": "npm run build",
|
|
22
|
-
"test": "node --experimental-strip-types --test src/abortable-command.test.ts src/reminder-playback.test.ts src/index.test.ts",
|
|
22
|
+
"test": "node --experimental-strip-types --test src/abortable-command.test.ts src/reminder-playback.test.ts src/permission-forwarding-watcher.test.ts src/index.test.ts",
|
|
23
23
|
"check": "npm run build && npm run test"
|
|
24
24
|
},
|
|
25
25
|
"keywords": [
|
|
@@ -61,8 +61,8 @@
|
|
|
61
61
|
]
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
|
-
"@mariozechner/pi-coding-agent": "^0.70.
|
|
65
|
-
"@mariozechner/pi-tui": "^0.70.
|
|
64
|
+
"@mariozechner/pi-coding-agent": "^0.70.6",
|
|
65
|
+
"@mariozechner/pi-tui": "^0.70.6"
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
68
|
"node-notifier": "^10.0.1"
|
package/src/ai-messages.ts
CHANGED
|
@@ -70,7 +70,7 @@ const DEFAULT_TEMPLATES: Record<CoreAIEventType, string[]> = {
|
|
|
70
70
|
permission: [
|
|
71
71
|
"Permission needed for {projectName}. Please approve in the terminal.",
|
|
72
72
|
"I need your approval to continue {projectName}.",
|
|
73
|
-
"
|
|
73
|
+
"Permission approval is pending. Please confirm the request.",
|
|
74
74
|
],
|
|
75
75
|
question: [
|
|
76
76
|
"I need your input for {projectName}. Please check the terminal.",
|
package/src/config-store.ts
CHANGED
|
@@ -45,7 +45,7 @@ const DEFAULT_WEBHOOK_EVENTS: NotificationType[] = [...NOTIFICATION_TYPE_VALUES]
|
|
|
45
45
|
|
|
46
46
|
export const INLINE_NOTIFY_TEXT: Record<NotificationType, string> = {
|
|
47
47
|
idle: "✅ Agent finished its current task.",
|
|
48
|
-
permission: "⚠️
|
|
48
|
+
permission: "⚠️ Permission approval is pending.",
|
|
49
49
|
question: "❓ Agent needs your input.",
|
|
50
50
|
error: "❌ Agent encountered an error.",
|
|
51
51
|
};
|
|
@@ -108,15 +108,6 @@ export const MESSAGE_LIBRARY: Record<NotificationType, MessageSet> = {
|
|
|
108
108
|
},
|
|
109
109
|
};
|
|
110
110
|
|
|
111
|
-
export const PERMISSION_HINTS = [
|
|
112
|
-
"permission",
|
|
113
|
-
"not permitted",
|
|
114
|
-
"requires approval",
|
|
115
|
-
"approval",
|
|
116
|
-
"user denied",
|
|
117
|
-
"blocked by",
|
|
118
|
-
];
|
|
119
|
-
|
|
120
111
|
export const QUESTION_HINTS = ["question", "need your input", "please answer", "requires your input"];
|
|
121
112
|
|
|
122
113
|
export const DEFAULT_CONFIG: VoiceNotifyConfig = {
|
|
@@ -183,10 +174,10 @@ export const DEFAULT_CONFIG: VoiceNotifyConfig = {
|
|
|
183
174
|
openaiTtsFormat: "mp3",
|
|
184
175
|
openaiTtsSpeed: 1,
|
|
185
176
|
|
|
186
|
-
idleSoundFile: "assets/
|
|
187
|
-
permissionSoundFile: "assets/
|
|
188
|
-
questionSoundFile: "assets/
|
|
189
|
-
errorSoundFile: "assets/
|
|
177
|
+
idleSoundFile: "assets/soft-notification.mp3",
|
|
178
|
+
permissionSoundFile: "assets/attention-alert.mp3",
|
|
179
|
+
questionSoundFile: "assets/attention-alert.mp3",
|
|
180
|
+
errorSoundFile: "assets/attention-alert.mp3",
|
|
190
181
|
themePath: "",
|
|
191
182
|
themeName: "default",
|
|
192
183
|
themesRootPath: "",
|
|
@@ -353,6 +344,34 @@ function stringOrDefault(value: unknown, fallback: string): string {
|
|
|
353
344
|
return fallback;
|
|
354
345
|
}
|
|
355
346
|
|
|
347
|
+
const LEGACY_BUNDLED_SOUND_FILES: Record<string, string> = {
|
|
348
|
+
"assets/machine-alert-beep-sound-effect.mp3": "assets/attention-alert.mp3",
|
|
349
|
+
"assets/soft-high-tech-notification-sound-effect.mp3": "assets/soft-notification.mp3",
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
function normalizeSoundFileLookup(value: string): string {
|
|
353
|
+
return value.replaceAll("\\", "/").replace(/^\.\//, "").toLowerCase();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function soundFileExists(value: string): boolean {
|
|
357
|
+
return existsSync(isAbsolute(value) ? value : join(CONFIG_DIR, value));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function normalizeSoundFile(value: unknown, fallback: string): string {
|
|
361
|
+
const selected = stringOrDefault(value, fallback);
|
|
362
|
+
const lookup = normalizeSoundFileLookup(selected);
|
|
363
|
+
const migrated = LEGACY_BUNDLED_SOUND_FILES[lookup];
|
|
364
|
+
if (migrated) {
|
|
365
|
+
return migrated;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (lookup.startsWith("assets/") && !soundFileExists(selected) && soundFileExists(fallback)) {
|
|
369
|
+
return fallback;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return selected;
|
|
373
|
+
}
|
|
374
|
+
|
|
356
375
|
function stringOrEmpty(value: unknown): string {
|
|
357
376
|
if (typeof value === "string") {
|
|
358
377
|
return value.trim();
|
|
@@ -755,13 +774,13 @@ export function normalizeConfig(raw: unknown): VoiceNotifyConfig {
|
|
|
755
774
|
openaiTtsFormat: stringOrDefault(record.openaiTtsFormat, DEFAULT_CONFIG.openaiTtsFormat),
|
|
756
775
|
openaiTtsSpeed: clampNumber(record.openaiTtsSpeed, DEFAULT_CONFIG.openaiTtsSpeed, 0.25, 4),
|
|
757
776
|
|
|
758
|
-
idleSoundFile:
|
|
759
|
-
permissionSoundFile:
|
|
777
|
+
idleSoundFile: normalizeSoundFile(record.idleSoundFile ?? record.idleSound, DEFAULT_CONFIG.idleSoundFile),
|
|
778
|
+
permissionSoundFile: normalizeSoundFile(
|
|
760
779
|
record.permissionSoundFile ?? record.permissionSound,
|
|
761
780
|
DEFAULT_CONFIG.permissionSoundFile,
|
|
762
781
|
),
|
|
763
|
-
questionSoundFile:
|
|
764
|
-
errorSoundFile:
|
|
782
|
+
questionSoundFile: normalizeSoundFile(record.questionSoundFile ?? record.questionSound, DEFAULT_CONFIG.questionSoundFile),
|
|
783
|
+
errorSoundFile: normalizeSoundFile(record.errorSoundFile ?? record.errorSound, DEFAULT_CONFIG.errorSoundFile),
|
|
765
784
|
themePath: stringOrEmpty(record.themePath ?? record.soundThemeDir ?? record.themeDirectory),
|
|
766
785
|
themeName: stringOrDefault(record.themeName, DEFAULT_CONFIG.themeName),
|
|
767
786
|
themesRootPath: stringOrEmpty(record.themesRootPath ?? record.themesRootDirectory),
|
|
@@ -998,7 +1017,7 @@ export function isWindows(): boolean {
|
|
|
998
1017
|
|
|
999
1018
|
export function resolveSoundFile(config: VoiceNotifyConfig, type: NotificationType): string | null {
|
|
1000
1019
|
const field = SOUND_FILE_FIELD[type];
|
|
1001
|
-
const value = config[field];
|
|
1020
|
+
const value = normalizeSoundFile(config[field], DEFAULT_CONFIG[field]);
|
|
1002
1021
|
if (!value.trim()) {
|
|
1003
1022
|
return null;
|
|
1004
1023
|
}
|
package/src/index.test.ts
CHANGED
|
@@ -61,6 +61,7 @@ class FakeEventBus {
|
|
|
61
61
|
|
|
62
62
|
class FakePi {
|
|
63
63
|
private readonly handlers = new Map<string, EventHandler[]>();
|
|
64
|
+
private tools: Array<{ name: string }> = [];
|
|
64
65
|
|
|
65
66
|
public readonly events = new FakeEventBus();
|
|
66
67
|
|
|
@@ -73,8 +74,12 @@ class FakePi {
|
|
|
73
74
|
public registerCommand(): void {
|
|
74
75
|
}
|
|
75
76
|
|
|
77
|
+
public setAvailableTools(tools: Array<{ name: string }>): void {
|
|
78
|
+
this.tools = [...tools];
|
|
79
|
+
}
|
|
80
|
+
|
|
76
81
|
public getAllTools(): Array<{ name: string }> {
|
|
77
|
-
return [];
|
|
82
|
+
return [...this.tools];
|
|
78
83
|
}
|
|
79
84
|
|
|
80
85
|
public sendMessage(): void {
|
|
@@ -397,7 +402,13 @@ test("skipWhenFocused=false still notifies even when the terminal is focused", a
|
|
|
397
402
|
|
|
398
403
|
await pi.emit("session_start", {}, ctx);
|
|
399
404
|
await flushAsyncWork();
|
|
400
|
-
|
|
405
|
+
pi.events.emit(
|
|
406
|
+
PERMISSION_SYSTEM_EVENT_CHANNEL,
|
|
407
|
+
permissionSystemEvent("waiting", "permission-focus-disabled", {
|
|
408
|
+
toolCallId: "call-focus-disabled",
|
|
409
|
+
toolName: "write_file",
|
|
410
|
+
}),
|
|
411
|
+
);
|
|
401
412
|
await flushAsyncWork();
|
|
402
413
|
await tickAndFlush(PERMISSION_BATCH_WINDOW_MS);
|
|
403
414
|
|
|
@@ -425,7 +436,13 @@ test("skipWhenFocused=true suppresses focused notifications and uses config focu
|
|
|
425
436
|
|
|
426
437
|
await pi.emit("session_start", {}, ctx);
|
|
427
438
|
await flushAsyncWork();
|
|
428
|
-
|
|
439
|
+
pi.events.emit(
|
|
440
|
+
PERMISSION_SYSTEM_EVENT_CHANNEL,
|
|
441
|
+
permissionSystemEvent("waiting", "permission-focus-enabled", {
|
|
442
|
+
toolCallId: "call-focus-enabled",
|
|
443
|
+
toolName: "write_file",
|
|
444
|
+
}),
|
|
445
|
+
);
|
|
429
446
|
await flushAsyncWork();
|
|
430
447
|
await tickAndFlush(PERMISSION_BATCH_WINDOW_MS);
|
|
431
448
|
|
|
@@ -525,7 +542,13 @@ test("permission reminders use the permission-specific reminder interval", async
|
|
|
525
542
|
|
|
526
543
|
await pi.emit("session_start", {}, ctx);
|
|
527
544
|
await flushAsyncWork();
|
|
528
|
-
|
|
545
|
+
pi.events.emit(
|
|
546
|
+
PERMISSION_SYSTEM_EVENT_CHANNEL,
|
|
547
|
+
permissionSystemEvent("waiting", "permission-reminder-delay", {
|
|
548
|
+
toolCallId: "call-permission-reminder-delay",
|
|
549
|
+
toolName: "write_file",
|
|
550
|
+
}),
|
|
551
|
+
);
|
|
529
552
|
await flushAsyncWork();
|
|
530
553
|
await tickAndFlush(PERMISSION_BATCH_WINDOW_MS);
|
|
531
554
|
|
|
@@ -539,6 +562,47 @@ test("permission reminders use the permission-specific reminder interval", async
|
|
|
539
562
|
assert.equal(countReminderCalls(ttsCalls), 1);
|
|
540
563
|
});
|
|
541
564
|
|
|
565
|
+
test("blocked tool_call events do not trigger permission notifications without permission-system waiting state", async (t) => {
|
|
566
|
+
disableFocusDetection(t);
|
|
567
|
+
useMockClock(t);
|
|
568
|
+
|
|
569
|
+
const { ctx, pi, ttsCalls } = createHarness();
|
|
570
|
+
|
|
571
|
+
await pi.emit("session_start", {}, ctx);
|
|
572
|
+
await flushAsyncWork();
|
|
573
|
+
await pi.emit("tool_call", permissionEvent("call-no-authoritative-wait"), ctx);
|
|
574
|
+
await flushAsyncWork();
|
|
575
|
+
await tickAndFlush(PERMISSION_BATCH_WINDOW_MS);
|
|
576
|
+
|
|
577
|
+
assert.equal(immediateNotificationCalls(ttsCalls).length, 0);
|
|
578
|
+
assert.equal(countReminderCalls(ttsCalls), 0);
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
test("permission-looking tool_result errors do not trigger permission notifications without permission-system waiting state", async (t) => {
|
|
582
|
+
disableFocusDetection(t);
|
|
583
|
+
useMockClock(t);
|
|
584
|
+
|
|
585
|
+
const { ctx, pi, ttsCalls } = createHarness();
|
|
586
|
+
|
|
587
|
+
await pi.emit("session_start", {}, ctx);
|
|
588
|
+
await flushAsyncWork();
|
|
589
|
+
await pi.emit(
|
|
590
|
+
"tool_result",
|
|
591
|
+
{
|
|
592
|
+
toolCallId: "result-no-authoritative-wait",
|
|
593
|
+
toolName: "permission_guard",
|
|
594
|
+
isError: true,
|
|
595
|
+
content: [{ type: "text", text: "Requires approval from the user before continuing." }],
|
|
596
|
+
},
|
|
597
|
+
ctx,
|
|
598
|
+
);
|
|
599
|
+
await flushAsyncWork();
|
|
600
|
+
await tickAndFlush(PERMISSION_BATCH_WINDOW_MS);
|
|
601
|
+
|
|
602
|
+
assert.equal(immediateNotificationCalls(ttsCalls).length, 0);
|
|
603
|
+
assert.equal(countReminderCalls(ttsCalls), 0);
|
|
604
|
+
});
|
|
605
|
+
|
|
542
606
|
test("permission-system waiting events trigger a permission notification and cancel on resolution", async (t) => {
|
|
543
607
|
disableFocusDetection(t);
|
|
544
608
|
useMockClock(t);
|
|
@@ -605,9 +669,21 @@ test("tool_execution_start only cancels the resolved permission reminder flow",
|
|
|
605
669
|
|
|
606
670
|
await pi.emit("session_start", {}, ctx);
|
|
607
671
|
await flushAsyncWork();
|
|
608
|
-
|
|
672
|
+
pi.events.emit(
|
|
673
|
+
PERMISSION_SYSTEM_EVENT_CHANNEL,
|
|
674
|
+
permissionSystemEvent("waiting", "permission-call-a", {
|
|
675
|
+
toolCallId: "call-a",
|
|
676
|
+
toolName: "write_file",
|
|
677
|
+
}),
|
|
678
|
+
);
|
|
609
679
|
await flushAsyncWork();
|
|
610
|
-
|
|
680
|
+
pi.events.emit(
|
|
681
|
+
PERMISSION_SYSTEM_EVENT_CHANNEL,
|
|
682
|
+
permissionSystemEvent("waiting", "permission-call-b", {
|
|
683
|
+
toolCallId: "call-b",
|
|
684
|
+
toolName: "write_file",
|
|
685
|
+
}),
|
|
686
|
+
);
|
|
611
687
|
await flushAsyncWork();
|
|
612
688
|
|
|
613
689
|
assert.equal(countReminderCalls(ttsCalls), 0);
|
|
@@ -640,9 +716,21 @@ test("tool_result resolution keeps another permission reminder active while drop
|
|
|
640
716
|
|
|
641
717
|
await pi.emit("session_start", {}, ctx);
|
|
642
718
|
await flushAsyncWork();
|
|
643
|
-
|
|
719
|
+
pi.events.emit(
|
|
720
|
+
PERMISSION_SYSTEM_EVENT_CHANNEL,
|
|
721
|
+
permissionSystemEvent("waiting", "permission-result-call-a", {
|
|
722
|
+
toolCallId: "call-a",
|
|
723
|
+
toolName: "write_file",
|
|
724
|
+
}),
|
|
725
|
+
);
|
|
644
726
|
await flushAsyncWork();
|
|
645
|
-
|
|
727
|
+
pi.events.emit(
|
|
728
|
+
PERMISSION_SYSTEM_EVENT_CHANNEL,
|
|
729
|
+
permissionSystemEvent("waiting", "permission-result-call-b", {
|
|
730
|
+
toolCallId: "call-b",
|
|
731
|
+
toolName: "write_file",
|
|
732
|
+
}),
|
|
733
|
+
);
|
|
646
734
|
await flushAsyncWork();
|
|
647
735
|
await tickAndFlush(PERMISSION_BATCH_WINDOW_MS);
|
|
648
736
|
await tickAndFlush(1_000);
|
|
@@ -662,7 +750,7 @@ test("tool_result resolution keeps another permission reminder active while drop
|
|
|
662
750
|
assert.equal(countReminderCalls(ttsCalls), 1);
|
|
663
751
|
});
|
|
664
752
|
|
|
665
|
-
test("tool_result with the same toolCallId still classifies after a permission-
|
|
753
|
+
test("tool_result with the same toolCallId still classifies after a permission-system waiting event", async (t) => {
|
|
666
754
|
disableFocusDetection(t);
|
|
667
755
|
useMockClock(t);
|
|
668
756
|
|
|
@@ -672,7 +760,13 @@ test("tool_result with the same toolCallId still classifies after a permission-b
|
|
|
672
760
|
|
|
673
761
|
await pi.emit("session_start", {}, ctx);
|
|
674
762
|
await flushAsyncWork();
|
|
675
|
-
|
|
763
|
+
pi.events.emit(
|
|
764
|
+
PERMISSION_SYSTEM_EVENT_CHANNEL,
|
|
765
|
+
permissionSystemEvent("waiting", "permission-shared", {
|
|
766
|
+
toolCallId: "call-shared",
|
|
767
|
+
toolName: "write_file",
|
|
768
|
+
}),
|
|
769
|
+
);
|
|
676
770
|
await flushAsyncWork();
|
|
677
771
|
await tickAndFlush(PERMISSION_BATCH_WINDOW_MS);
|
|
678
772
|
assert.equal(immediateNotificationCalls(ttsCalls).length, 1);
|
|
@@ -692,6 +786,79 @@ test("tool_result with the same toolCallId still classifies after a permission-b
|
|
|
692
786
|
assert.equal(immediateNotificationCalls(ttsCalls).length, 2);
|
|
693
787
|
});
|
|
694
788
|
|
|
789
|
+
test("agent_end triggers an idle notification when idle notifications are enabled", async (t) => {
|
|
790
|
+
disableFocusDetection(t);
|
|
791
|
+
useMockClock(t);
|
|
792
|
+
|
|
793
|
+
const { ctx, pi, ttsCalls } = createHarness({
|
|
794
|
+
enableIdleNotification: true,
|
|
795
|
+
reminderEnabled: false,
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
await pi.emit("session_start", {}, ctx);
|
|
799
|
+
await flushAsyncWork();
|
|
800
|
+
await pi.emit("agent_end", {}, ctx);
|
|
801
|
+
await flushAsyncWork();
|
|
802
|
+
|
|
803
|
+
assert.equal(immediateNotificationCalls(ttsCalls).length, 1);
|
|
804
|
+
assert.equal(countReminderCalls(ttsCalls), 0);
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
test("question-classified tool_result triggers a question notification when the question tool is available", async (t) => {
|
|
808
|
+
disableFocusDetection(t);
|
|
809
|
+
useMockClock(t);
|
|
810
|
+
|
|
811
|
+
const { ctx, pi, ttsCalls } = createHarness({
|
|
812
|
+
enableQuestionNotification: true,
|
|
813
|
+
reminderEnabled: false,
|
|
814
|
+
});
|
|
815
|
+
pi.setAvailableTools([{ name: "question" }]);
|
|
816
|
+
|
|
817
|
+
await pi.emit("session_start", {}, ctx);
|
|
818
|
+
await flushAsyncWork();
|
|
819
|
+
await pi.emit(
|
|
820
|
+
"tool_result",
|
|
821
|
+
{
|
|
822
|
+
toolCallId: "call-question-available",
|
|
823
|
+
toolName: "custom_tool",
|
|
824
|
+
isError: false,
|
|
825
|
+
content: [{ type: "text", text: "This request requires your input before continuing." }],
|
|
826
|
+
},
|
|
827
|
+
ctx,
|
|
828
|
+
);
|
|
829
|
+
await flushAsyncWork();
|
|
830
|
+
|
|
831
|
+
assert.equal(immediateNotificationCalls(ttsCalls).length, 1);
|
|
832
|
+
assert.equal(countReminderCalls(ttsCalls), 0);
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
test("question-classified tool_result does not notify when the question tool is unavailable", async (t) => {
|
|
836
|
+
disableFocusDetection(t);
|
|
837
|
+
useMockClock(t);
|
|
838
|
+
|
|
839
|
+
const { ctx, pi, ttsCalls } = createHarness({
|
|
840
|
+
enableQuestionNotification: true,
|
|
841
|
+
reminderEnabled: false,
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
await pi.emit("session_start", {}, ctx);
|
|
845
|
+
await flushAsyncWork();
|
|
846
|
+
await pi.emit(
|
|
847
|
+
"tool_result",
|
|
848
|
+
{
|
|
849
|
+
toolCallId: "call-question-unavailable",
|
|
850
|
+
toolName: "custom_tool",
|
|
851
|
+
isError: false,
|
|
852
|
+
content: [{ type: "text", text: "This request requires your input before continuing." }],
|
|
853
|
+
},
|
|
854
|
+
ctx,
|
|
855
|
+
);
|
|
856
|
+
await flushAsyncWork();
|
|
857
|
+
|
|
858
|
+
assert.equal(immediateNotificationCalls(ttsCalls).length, 0);
|
|
859
|
+
assert.equal(countReminderCalls(ttsCalls), 0);
|
|
860
|
+
});
|
|
861
|
+
|
|
695
862
|
test("forwarded permission resolution cancels a queued reminder before it fires", async (t) => {
|
|
696
863
|
disableFocusDetection(t);
|
|
697
864
|
useMockClock(t);
|