pi-smart-voice-notify 0.3.5 → 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 +9 -0
- package/README.md +4 -4
- package/config/config.example.json +4 -4
- package/package.json +3 -3
- package/src/config-store.ts +37 -9
- package/src/index.ts +70 -1
- 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,14 @@
|
|
|
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
|
+
|
|
3
12
|
## [0.3.5] - 2026-04-27
|
|
4
13
|
|
|
5
14
|
### Fixed
|
package/README.md
CHANGED
|
@@ -166,10 +166,10 @@ Forwarded permission watcher notifications use privacy-safe text, require the re
|
|
|
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",
|
|
@@ -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/config-store.ts
CHANGED
|
@@ -174,10 +174,10 @@ export const DEFAULT_CONFIG: VoiceNotifyConfig = {
|
|
|
174
174
|
openaiTtsFormat: "mp3",
|
|
175
175
|
openaiTtsSpeed: 1,
|
|
176
176
|
|
|
177
|
-
idleSoundFile: "assets/
|
|
178
|
-
permissionSoundFile: "assets/
|
|
179
|
-
questionSoundFile: "assets/
|
|
180
|
-
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",
|
|
181
181
|
themePath: "",
|
|
182
182
|
themeName: "default",
|
|
183
183
|
themesRootPath: "",
|
|
@@ -344,6 +344,34 @@ function stringOrDefault(value: unknown, fallback: string): string {
|
|
|
344
344
|
return fallback;
|
|
345
345
|
}
|
|
346
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
|
+
|
|
347
375
|
function stringOrEmpty(value: unknown): string {
|
|
348
376
|
if (typeof value === "string") {
|
|
349
377
|
return value.trim();
|
|
@@ -746,13 +774,13 @@ export function normalizeConfig(raw: unknown): VoiceNotifyConfig {
|
|
|
746
774
|
openaiTtsFormat: stringOrDefault(record.openaiTtsFormat, DEFAULT_CONFIG.openaiTtsFormat),
|
|
747
775
|
openaiTtsSpeed: clampNumber(record.openaiTtsSpeed, DEFAULT_CONFIG.openaiTtsSpeed, 0.25, 4),
|
|
748
776
|
|
|
749
|
-
idleSoundFile:
|
|
750
|
-
permissionSoundFile:
|
|
777
|
+
idleSoundFile: normalizeSoundFile(record.idleSoundFile ?? record.idleSound, DEFAULT_CONFIG.idleSoundFile),
|
|
778
|
+
permissionSoundFile: normalizeSoundFile(
|
|
751
779
|
record.permissionSoundFile ?? record.permissionSound,
|
|
752
780
|
DEFAULT_CONFIG.permissionSoundFile,
|
|
753
781
|
),
|
|
754
|
-
questionSoundFile:
|
|
755
|
-
errorSoundFile:
|
|
782
|
+
questionSoundFile: normalizeSoundFile(record.questionSoundFile ?? record.questionSound, DEFAULT_CONFIG.questionSoundFile),
|
|
783
|
+
errorSoundFile: normalizeSoundFile(record.errorSoundFile ?? record.errorSound, DEFAULT_CONFIG.errorSoundFile),
|
|
756
784
|
themePath: stringOrEmpty(record.themePath ?? record.soundThemeDir ?? record.themeDirectory),
|
|
757
785
|
themeName: stringOrDefault(record.themeName, DEFAULT_CONFIG.themeName),
|
|
758
786
|
themesRootPath: stringOrEmpty(record.themesRootPath ?? record.themesRootDirectory),
|
|
@@ -989,7 +1017,7 @@ export function isWindows(): boolean {
|
|
|
989
1017
|
|
|
990
1018
|
export function resolveSoundFile(config: VoiceNotifyConfig, type: NotificationType): string | null {
|
|
991
1019
|
const field = SOUND_FILE_FIELD[type];
|
|
992
|
-
const value = config[field];
|
|
1020
|
+
const value = normalizeSoundFile(config[field], DEFAULT_CONFIG[field]);
|
|
993
1021
|
if (!value.trim()) {
|
|
994
1022
|
return null;
|
|
995
1023
|
}
|
package/src/index.ts
CHANGED
|
@@ -127,6 +127,52 @@ function classifyToolResult(
|
|
|
127
127
|
return "error";
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
type AgentEndStatus = "completed" | "error" | "aborted";
|
|
131
|
+
|
|
132
|
+
interface AgentEndOutcome {
|
|
133
|
+
status: AgentEndStatus;
|
|
134
|
+
reason?: string;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function readAgentEndOutcome(event: unknown): AgentEndOutcome {
|
|
138
|
+
const messages = toRecord(event).messages;
|
|
139
|
+
if (!Array.isArray(messages)) {
|
|
140
|
+
return { status: "completed" };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
144
|
+
const message = toRecord(messages[index]);
|
|
145
|
+
if (message.role !== "assistant") {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const stopReason = normalizeOptionalString(message.stopReason);
|
|
150
|
+
const errorMessage = normalizeOptionalString(message.errorMessage);
|
|
151
|
+
if (stopReason === "error") {
|
|
152
|
+
return { status: "error", reason: errorMessage ?? extractTextContent(message.content) };
|
|
153
|
+
}
|
|
154
|
+
if (stopReason === "aborted") {
|
|
155
|
+
return { status: "aborted", reason: errorMessage };
|
|
156
|
+
}
|
|
157
|
+
if (errorMessage) {
|
|
158
|
+
return { status: "error", reason: errorMessage };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return { status: "completed" };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return { status: "completed" };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function formatAgentErrorNotification(reason: string | undefined): string {
|
|
168
|
+
const normalizedReason = reason?.replace(/\s+/g, " ").trim();
|
|
169
|
+
if (!normalizedReason) {
|
|
170
|
+
return "❌ Agent ended with an error. Check the latest output before continuing.";
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return `❌ Agent ended with an error: ${normalizedReason.slice(0, 160)}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
130
176
|
function statusLine(config: VoiceNotifyConfig): string | undefined {
|
|
131
177
|
if (!config.enabled) {
|
|
132
178
|
return "voice:off";
|
|
@@ -1750,6 +1796,7 @@ export default function smartVoiceNotifyExtension(
|
|
|
1750
1796
|
pendingPermissionToolCallIds.clear();
|
|
1751
1797
|
processedToolResultToolCallIds.clear();
|
|
1752
1798
|
resetPermissionBatch("agent_start");
|
|
1799
|
+
cancelReminderActivity("agent_start");
|
|
1753
1800
|
logger.debug("agent.start", {});
|
|
1754
1801
|
});
|
|
1755
1802
|
|
|
@@ -1803,8 +1850,30 @@ export default function smartVoiceNotifyExtension(
|
|
|
1803
1850
|
});
|
|
1804
1851
|
});
|
|
1805
1852
|
|
|
1806
|
-
pi.on("agent_end", async (
|
|
1853
|
+
pi.on("agent_end", async (event, ctx) => {
|
|
1807
1854
|
activeSessionContext = ctx;
|
|
1855
|
+
const outcome = readAgentEndOutcome(event);
|
|
1856
|
+
if (outcome.status === "error") {
|
|
1857
|
+
hadErrorInTurn = true;
|
|
1858
|
+
logger.debug("agent.end.error_detected", {
|
|
1859
|
+
reason: outcome.reason,
|
|
1860
|
+
});
|
|
1861
|
+
if (config.enabled && config.enableErrorNotification) {
|
|
1862
|
+
triggerNotification("error", ctx, {
|
|
1863
|
+
customMessage: formatAgentErrorNotification(outcome.reason),
|
|
1864
|
+
reason: outcome.reason,
|
|
1865
|
+
});
|
|
1866
|
+
}
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1869
|
+
if (outcome.status === "aborted") {
|
|
1870
|
+
hadErrorInTurn = true;
|
|
1871
|
+
logger.debug("agent.end.idle_skipped", {
|
|
1872
|
+
reason: "agent_aborted",
|
|
1873
|
+
errorMessage: outcome.reason,
|
|
1874
|
+
});
|
|
1875
|
+
return;
|
|
1876
|
+
}
|
|
1808
1877
|
if (!config.enabled || !config.enableIdleNotification) {
|
|
1809
1878
|
logger.debug("agent.end.idle_skipped", {
|
|
1810
1879
|
reason: "idle_notification_disabled",
|
|
Binary file
|
|
File without changes
|
|
File without changes
|