pi-smart-voice-notify 0.5.0 → 0.5.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/CHANGELOG.md +17 -0
- package/package.json +4 -4
- package/src/agent-dir.ts +32 -0
- package/src/config-store.ts +6 -3
- package/src/index.ts +234 -101
- package/src/permission-forwarding-watcher.ts +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.2] - 2026-06-01
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- Deferred notification service initialization until first use to reduce startup work.
|
|
7
|
+
- Replaced shared agent-directory lookup with a local `PI_CODING_AGENT_DIR`-aware resolver for config and permission-forwarding paths.
|
|
8
|
+
- Widened peer dependency ranges to `^0.74.0 || ^0.75.0 || ^0.77.0 || ^0.78.0`.
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Avoid rewriting the config file when normalized content is unchanged.
|
|
12
|
+
|
|
13
|
+
## [0.5.1] - 2026-05-26
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- Suppressed error notifications when the agent has pending continuation messages.
|
|
17
|
+
- Widened peer dependency ranges to `^0.74.0 || ^0.75.0`.
|
|
18
|
+
- Aligned `@types/node` dev dependency to `25.9.1`.
|
|
19
|
+
|
|
3
20
|
## [0.5.0] - 2026-05-22
|
|
4
21
|
|
|
5
22
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-smart-voice-notify",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
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,14 +61,14 @@
|
|
|
61
61
|
]
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
|
-
"@earendil-works/pi-coding-agent": "^0.75.
|
|
65
|
-
"@earendil-works/pi-tui": "^0.75.
|
|
64
|
+
"@earendil-works/pi-coding-agent": "^0.74.0 || ^0.75.0 || ^0.77.0 || ^0.78.0",
|
|
65
|
+
"@earendil-works/pi-tui": "^0.74.0 || ^0.75.0 || ^0.77.0 || ^0.78.0"
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
68
|
"node-notifier": "^10.0.1",
|
|
69
69
|
"undici": "^8.3.0"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
|
-
"@types/node": "
|
|
72
|
+
"@types/node": "25.9.1"
|
|
73
73
|
}
|
|
74
74
|
}
|
package/src/agent-dir.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
const PI_AGENT_DIR_ENV_VAR = "PI_CODING_AGENT_DIR";
|
|
5
|
+
|
|
6
|
+
interface AgentDirEnvironment {
|
|
7
|
+
[name: string]: string | undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function expandHomeDirectory(configuredDir: string, homeDirectory: string): string {
|
|
11
|
+
if (configuredDir === "~") {
|
|
12
|
+
return homeDirectory;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (configuredDir.startsWith("~/") || configuredDir.startsWith("~\\")) {
|
|
16
|
+
return join(homeDirectory, configuredDir.slice(2));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return configuredDir;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function resolvePiAgentDir(
|
|
23
|
+
env: AgentDirEnvironment = process.env,
|
|
24
|
+
homeDirectory = homedir(),
|
|
25
|
+
): string {
|
|
26
|
+
const configuredDir = env[PI_AGENT_DIR_ENV_VAR];
|
|
27
|
+
if (!configuredDir) {
|
|
28
|
+
return join(homeDirectory, ".pi", "agent");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return expandHomeDirectory(configuredDir, homeDirectory);
|
|
32
|
+
}
|
package/src/config-store.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
2
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
2
|
import { isAbsolute, join } from "node:path";
|
|
4
3
|
|
|
4
|
+
import { resolvePiAgentDir } from "./agent-dir.ts";
|
|
5
5
|
import type {
|
|
6
6
|
ConcreteTTSEngine,
|
|
7
7
|
MessageSet,
|
|
@@ -14,7 +14,7 @@ import type {
|
|
|
14
14
|
|
|
15
15
|
export const EXTENSION_ID = "pi-smart-voice-notify";
|
|
16
16
|
export const STATUS_KEY = "smart-voice-notify";
|
|
17
|
-
export const CONFIG_DIR = join(
|
|
17
|
+
export const CONFIG_DIR = join(resolvePiAgentDir(), "extensions", EXTENSION_ID);
|
|
18
18
|
export const CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
19
19
|
export const DEBUG_DIR = join(CONFIG_DIR, "debug");
|
|
20
20
|
export const DEBUG_LOG_PATH = join(DEBUG_DIR, `${EXTENSION_ID}.log`);
|
|
@@ -991,7 +991,10 @@ export function readConfigFromDisk(): VoiceNotifyConfig {
|
|
|
991
991
|
const parsed = JSON.parse(raw) as unknown;
|
|
992
992
|
const normalized = normalizeConfig(parsed);
|
|
993
993
|
const validation = validateConfig(normalized);
|
|
994
|
-
|
|
994
|
+
const serialized = `${JSON.stringify(validation.config, null, 2)}\n`;
|
|
995
|
+
if (raw !== serialized) {
|
|
996
|
+
writeFileSync(CONFIG_PATH, serialized, "utf-8");
|
|
997
|
+
}
|
|
995
998
|
const runtimeConfig = applyEnvironmentOverrides(validation.config);
|
|
996
999
|
return validateConfig(runtimeConfig).config;
|
|
997
1000
|
} catch {
|
package/src/index.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type {
|
|
|
6
6
|
import type { SettingItem } from "@earendil-works/pi-tui";
|
|
7
7
|
import { basename } from "node:path";
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import type { AIMessageConfig, AIMessageService } from "./ai-messages.ts";
|
|
10
10
|
import {
|
|
11
11
|
BOOLEAN_VALUES,
|
|
12
12
|
clampInt,
|
|
@@ -30,25 +30,23 @@ import {
|
|
|
30
30
|
boolValue,
|
|
31
31
|
ensureDebugDirectory,
|
|
32
32
|
} from "./config-store.ts";
|
|
33
|
-
import {
|
|
34
|
-
import { clearFocusDetectCache, isTerminalFocused } from "./focus-detect.ts";
|
|
35
|
-
import { detectLinuxSession, getIdleTime, wakeMonitor as wakeLinuxMonitor } from "./linux.ts";
|
|
33
|
+
import type { FocusDetectOptions } from "./focus-detect.ts";
|
|
36
34
|
import { createExtensionLogger, getErrorMessage } from "./logging.ts";
|
|
37
|
-
import { AudioNotificationService } from "./notify-audio.ts";
|
|
38
|
-
import {
|
|
39
|
-
|
|
35
|
+
import type { AudioNotificationService } from "./notify-audio.ts";
|
|
36
|
+
import type {
|
|
37
|
+
PermissionForwardingWatcherConfig,
|
|
38
|
+
PermissionForwardingWatcherOptions,
|
|
39
|
+
} from "./permission-forwarding-watcher.ts";
|
|
40
40
|
import { ReminderPlaybackController } from "./reminder-playback.ts";
|
|
41
|
-
import {
|
|
42
|
-
import { initializeTTSService } from "./tts.ts";
|
|
41
|
+
import type { SoundThemeConfig, SoundThemeService } from "./sound-theme.ts";
|
|
43
42
|
import type {
|
|
44
43
|
NotificationType,
|
|
45
44
|
NotifyLevel,
|
|
46
45
|
ReminderState,
|
|
47
46
|
VoiceNotifyConfig,
|
|
48
47
|
} from "./types.ts";
|
|
49
|
-
import type { TTSConfig, TTSService } from "./types/tts.ts";
|
|
50
|
-
import {
|
|
51
|
-
import { ZellijModal, ZellijSettingsModal } from "./zellij-modal.ts";
|
|
48
|
+
import type { TTSConfig, TTSService, TTSServiceOptions } from "./types/tts.ts";
|
|
49
|
+
import type { WebhookConfig, WebhookService } from "./webhook.ts";
|
|
52
50
|
|
|
53
51
|
type SessionStartReason = "startup" | "reload" | "new" | "resume" | "fork";
|
|
54
52
|
|
|
@@ -167,6 +165,14 @@ function formatAgentErrorNotification(reason: string | undefined): string {
|
|
|
167
165
|
return `❌ Agent ended with an error: ${normalizedReason.slice(0, 160)}`;
|
|
168
166
|
}
|
|
169
167
|
|
|
168
|
+
function hasPendingAgentMessages(ctx: ExtensionContext): boolean {
|
|
169
|
+
try {
|
|
170
|
+
return ctx.hasPendingMessages();
|
|
171
|
+
} catch {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
170
176
|
function statusLine(config: VoiceNotifyConfig): string | undefined {
|
|
171
177
|
if (!config.enabled) {
|
|
172
178
|
return "voice:off";
|
|
@@ -272,15 +278,21 @@ const REMINDER_EVENT_TYPE: Record<NotificationType, string> = {
|
|
|
272
278
|
};
|
|
273
279
|
|
|
274
280
|
type ReminderKey = string;
|
|
275
|
-
type
|
|
281
|
+
type FocusDetector = (options?: FocusDetectOptions) => Promise<boolean>;
|
|
282
|
+
|
|
283
|
+
type PermissionForwardingWatcherController = {
|
|
284
|
+
start(config: PermissionForwardingWatcherConfig): void;
|
|
285
|
+
updateConfig(config: PermissionForwardingWatcherConfig): void;
|
|
286
|
+
stop(): void;
|
|
287
|
+
};
|
|
276
288
|
|
|
277
289
|
export interface SmartVoiceNotifyDependencies {
|
|
278
290
|
readConfigFromDisk?: typeof readConfigFromDisk;
|
|
279
|
-
initializeTTSService?: (options?:
|
|
291
|
+
initializeTTSService?: (options?: TTSServiceOptions) => TTSService;
|
|
280
292
|
createPermissionForwardingWatcher?: (
|
|
281
|
-
options:
|
|
293
|
+
options: PermissionForwardingWatcherOptions,
|
|
282
294
|
) => PermissionForwardingWatcherController;
|
|
283
|
-
isTerminalFocused?:
|
|
295
|
+
isTerminalFocused?: FocusDetector;
|
|
284
296
|
}
|
|
285
297
|
|
|
286
298
|
function defaultReminderKey(type: NotificationType): ReminderKey {
|
|
@@ -367,12 +379,7 @@ export default function smartVoiceNotifyExtension(
|
|
|
367
379
|
dependencies: SmartVoiceNotifyDependencies = {},
|
|
368
380
|
): void {
|
|
369
381
|
const readConfig = dependencies.readConfigFromDisk ?? readConfigFromDisk;
|
|
370
|
-
const
|
|
371
|
-
const createPermissionForwardingWatcher =
|
|
372
|
-
dependencies.createPermissionForwardingWatcher ??
|
|
373
|
-
((options: ConstructorParameters<typeof PermissionForwardingWatcher>[0]): PermissionForwardingWatcherController => {
|
|
374
|
-
return new PermissionForwardingWatcher(options);
|
|
375
|
-
});
|
|
382
|
+
const createInjectedTTSService = dependencies.initializeTTSService;
|
|
376
383
|
let config = readConfig();
|
|
377
384
|
let lastUserActivityAt = Date.now();
|
|
378
385
|
let hadErrorInTurn = false;
|
|
@@ -408,13 +415,16 @@ export default function smartVoiceNotifyExtension(
|
|
|
408
415
|
ensureDebugDirectory,
|
|
409
416
|
});
|
|
410
417
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
418
|
+
let audioService: AudioNotificationService | null = null;
|
|
419
|
+
let ttsService: TTSService | null = null;
|
|
420
|
+
let aiMessageService: AIMessageService | null = null;
|
|
421
|
+
let webhookService: WebhookService | null = null;
|
|
422
|
+
let soundThemeService: SoundThemeService | null = null;
|
|
423
|
+
let permissionForwardingWatcher: PermissionForwardingWatcherController | null = null;
|
|
424
|
+
let focusModulePromise: Promise<typeof import("./focus-detect.ts")> | null = null;
|
|
425
|
+
|
|
416
426
|
const projectName = basename(process.cwd()) || "project";
|
|
417
|
-
const
|
|
427
|
+
const injectedTerminalFocusDetector = dependencies.isTerminalFocused;
|
|
418
428
|
const focusDetectionEnabled = envBoolean(
|
|
419
429
|
process.platform === "linux" || process.platform === "win32",
|
|
420
430
|
"PI_SMART_NOTIFY_FOCUS_DETECTION",
|
|
@@ -425,11 +435,6 @@ export default function smartVoiceNotifyExtension(
|
|
|
425
435
|
0,
|
|
426
436
|
envInteger(10_000, "PI_SMART_NOTIFY_AGENT_ERROR_GRACE_MS"),
|
|
427
437
|
);
|
|
428
|
-
const soundThemeService = new SoundThemeService({
|
|
429
|
-
debugLog: (message) => {
|
|
430
|
-
logger.debug("sound_theme.debug", { message });
|
|
431
|
-
},
|
|
432
|
-
});
|
|
433
438
|
const buildTTSServiceConfig = (): TTSConfig => {
|
|
434
439
|
return {
|
|
435
440
|
enableTts: config.enableTts,
|
|
@@ -533,26 +538,96 @@ export default function smartVoiceNotifyExtension(
|
|
|
533
538
|
customSoundDirectories: configuredCustomSoundDirectories.length > 0 ? configuredCustomSoundDirectories : undefined,
|
|
534
539
|
};
|
|
535
540
|
};
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
const
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
541
|
+
const getAudioService = async (): Promise<AudioNotificationService> => {
|
|
542
|
+
if (!audioService) {
|
|
543
|
+
const { AudioNotificationService: AudioNotificationServiceCtor } = await import("./notify-audio.ts");
|
|
544
|
+
audioService = new AudioNotificationServiceCtor({
|
|
545
|
+
execRunner: pi,
|
|
546
|
+
getConfig: () => config,
|
|
547
|
+
debug: logger.debug,
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
return audioService;
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
const getTTSService = async (): Promise<TTSService> => {
|
|
554
|
+
if (!ttsService) {
|
|
555
|
+
const createTTSService = createInjectedTTSService
|
|
556
|
+
?? (await import("./tts.ts")).initializeTTSService;
|
|
557
|
+
ttsService = createTTSService({
|
|
558
|
+
execRunner: pi,
|
|
559
|
+
config: buildTTSServiceConfig(),
|
|
560
|
+
debug: logger.debug,
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
return ttsService;
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
const getAIMessageService = async (): Promise<AIMessageService> => {
|
|
567
|
+
if (!aiMessageService) {
|
|
568
|
+
const { initializeAIMessageService } = await import("./ai-messages.ts");
|
|
569
|
+
aiMessageService = initializeAIMessageService({
|
|
570
|
+
config: buildAIMessageConfig(),
|
|
571
|
+
debugLog: (message, details = {}) => {
|
|
572
|
+
logger.debug(`ai_messages.${message}`, details);
|
|
573
|
+
},
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
return aiMessageService;
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
const getWebhookService = async (): Promise<WebhookService> => {
|
|
580
|
+
if (!webhookService) {
|
|
581
|
+
const { createWebhookService } = await import("./webhook.ts");
|
|
582
|
+
webhookService = createWebhookService(buildWebhookConfig());
|
|
583
|
+
}
|
|
584
|
+
return webhookService;
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
const getSoundThemeService = async (): Promise<SoundThemeService> => {
|
|
588
|
+
if (!soundThemeService) {
|
|
589
|
+
const { SoundThemeService: SoundThemeServiceCtor } = await import("./sound-theme.ts");
|
|
590
|
+
soundThemeService = new SoundThemeServiceCtor({
|
|
591
|
+
debugLog: (message) => {
|
|
592
|
+
logger.debug("sound_theme.debug", { message });
|
|
593
|
+
},
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
return soundThemeService;
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
const getTerminalFocusDetector = async (): Promise<FocusDetector> => {
|
|
600
|
+
if (injectedTerminalFocusDetector) {
|
|
601
|
+
return injectedTerminalFocusDetector;
|
|
602
|
+
}
|
|
603
|
+
focusModulePromise ??= import("./focus-detect.ts");
|
|
604
|
+
const module = await focusModulePromise;
|
|
605
|
+
return module.isTerminalFocused;
|
|
606
|
+
};
|
|
554
607
|
|
|
555
|
-
const
|
|
608
|
+
const clearFocusDetectCacheIfLoaded = (): void => {
|
|
609
|
+
if (!focusModulePromise) {
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
void focusModulePromise
|
|
613
|
+
.then((module) => module.clearFocusDetectCache())
|
|
614
|
+
.catch((error) => {
|
|
615
|
+
logger.debug("focus.cache_clear_failed", { error: getErrorMessage(error) });
|
|
616
|
+
});
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
const clearProjectSoundCacheIfLoaded = (): void => {
|
|
620
|
+
if (!soundThemeService) {
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
void import("./per-project-sound.ts")
|
|
624
|
+
.then((module) => module.clearProjectSoundCache())
|
|
625
|
+
.catch((error) => {
|
|
626
|
+
logger.debug("sound_theme.cache_clear_failed", { error: getErrorMessage(error) });
|
|
627
|
+
});
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
const permissionForwardingWatcherOptions: PermissionForwardingWatcherOptions = {
|
|
556
631
|
onRequest: (event) => {
|
|
557
632
|
if (!config.enabled || !config.enablePermissionNotification || !config.enableForwardedPermissionWatcher) {
|
|
558
633
|
return;
|
|
@@ -609,28 +684,52 @@ export default function smartVoiceNotifyExtension(
|
|
|
609
684
|
debugLog: (event, details = {}) => {
|
|
610
685
|
logger.debug(event, details);
|
|
611
686
|
},
|
|
612
|
-
}
|
|
687
|
+
};
|
|
613
688
|
|
|
614
|
-
|
|
689
|
+
if (dependencies.createPermissionForwardingWatcher) {
|
|
690
|
+
permissionForwardingWatcher = dependencies.createPermissionForwardingWatcher(permissionForwardingWatcherOptions);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const getPermissionForwardingWatcher = async (): Promise<PermissionForwardingWatcherController> => {
|
|
694
|
+
if (!permissionForwardingWatcher) {
|
|
695
|
+
const { PermissionForwardingWatcher } = await import("./permission-forwarding-watcher.ts");
|
|
696
|
+
permissionForwardingWatcher = new PermissionForwardingWatcher(permissionForwardingWatcherOptions);
|
|
697
|
+
}
|
|
698
|
+
return permissionForwardingWatcher;
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
const syncPermissionForwardingWatcher = async (): Promise<void> => {
|
|
615
702
|
if (!activeSessionContext) {
|
|
616
|
-
permissionForwardingWatcher
|
|
703
|
+
permissionForwardingWatcher?.stop();
|
|
617
704
|
return;
|
|
618
705
|
}
|
|
619
|
-
|
|
706
|
+
|
|
707
|
+
const watcherConfig: PermissionForwardingWatcherConfig = {
|
|
620
708
|
enabled: config.enabled && config.enablePermissionNotification && config.enableForwardedPermissionWatcher,
|
|
621
709
|
watchLegacyPath: config.watchLegacyForwardedPermissionPath,
|
|
622
710
|
targetSessionId: getPermissionForwardingSessionId(activeSessionContext),
|
|
623
|
-
}
|
|
711
|
+
};
|
|
712
|
+
|
|
713
|
+
if (!watcherConfig.enabled && !permissionForwardingWatcher) {
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
const watcher = watcherConfig.enabled
|
|
718
|
+
? await getPermissionForwardingWatcher()
|
|
719
|
+
: permissionForwardingWatcher;
|
|
720
|
+
watcher?.start(watcherConfig);
|
|
624
721
|
};
|
|
625
722
|
|
|
626
723
|
const refreshIntegratedServiceConfig = (): void => {
|
|
627
|
-
ttsService =
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
724
|
+
ttsService = createInjectedTTSService
|
|
725
|
+
? createInjectedTTSService({
|
|
726
|
+
execRunner: pi,
|
|
727
|
+
config: buildTTSServiceConfig(),
|
|
728
|
+
debug: logger.debug,
|
|
729
|
+
})
|
|
730
|
+
: null;
|
|
731
|
+
aiMessageService?.updateConfig(buildAIMessageConfig());
|
|
732
|
+
webhookService?.updateConfig(buildWebhookConfig());
|
|
634
733
|
};
|
|
635
734
|
|
|
636
735
|
const rememberScopedToolCallId = (toolCallId: string, seenToolCallIds: Set<string>): boolean => {
|
|
@@ -789,6 +888,7 @@ export default function smartVoiceNotifyExtension(
|
|
|
789
888
|
}
|
|
790
889
|
|
|
791
890
|
try {
|
|
891
|
+
const detectTerminalFocus = await getTerminalFocusDetector();
|
|
792
892
|
const focused = await detectTerminalFocus({
|
|
793
893
|
debug: config.debugLog,
|
|
794
894
|
cacheTtlMs: config.focusCacheTtlMs,
|
|
@@ -828,22 +928,25 @@ export default function smartVoiceNotifyExtension(
|
|
|
828
928
|
}
|
|
829
929
|
|
|
830
930
|
const eventType = options.isReminder ? REMINDER_EVENT_TYPE[type] : type;
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
931
|
+
if (config.aiMessages.enabled) {
|
|
932
|
+
try {
|
|
933
|
+
const service = await getAIMessageService();
|
|
934
|
+
const generated = await service.generateMessage(eventType, {
|
|
935
|
+
projectName,
|
|
936
|
+
time: new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }),
|
|
937
|
+
count: options.followUpCount,
|
|
938
|
+
reason: options.reason,
|
|
939
|
+
});
|
|
940
|
+
if (generated.trim().length > 0) {
|
|
941
|
+
return generated;
|
|
942
|
+
}
|
|
943
|
+
} catch (error) {
|
|
944
|
+
logger.debug("message.generate.error", {
|
|
945
|
+
type,
|
|
946
|
+
eventType,
|
|
947
|
+
error: getErrorMessage(error),
|
|
948
|
+
});
|
|
840
949
|
}
|
|
841
|
-
} catch (error) {
|
|
842
|
-
logger.debug("message.generate.error", {
|
|
843
|
-
type,
|
|
844
|
-
eventType,
|
|
845
|
-
error: getErrorMessage(error),
|
|
846
|
-
});
|
|
847
950
|
}
|
|
848
951
|
|
|
849
952
|
return pickRandom(MESSAGE_LIBRARY[type][options.isReminder ? "reminder" : "initial"]);
|
|
@@ -855,6 +958,7 @@ export default function smartVoiceNotifyExtension(
|
|
|
855
958
|
}
|
|
856
959
|
|
|
857
960
|
if (process.platform === "linux") {
|
|
961
|
+
const { getIdleTime, wakeMonitor: wakeLinuxMonitor } = await import("./linux.ts");
|
|
858
962
|
const idleSeconds = await getIdleTime({
|
|
859
963
|
debugLog: (message) => {
|
|
860
964
|
logger.debug("linux.idle", { message });
|
|
@@ -876,7 +980,8 @@ export default function smartVoiceNotifyExtension(
|
|
|
876
980
|
return;
|
|
877
981
|
}
|
|
878
982
|
|
|
879
|
-
await
|
|
983
|
+
const service = await getAudioService();
|
|
984
|
+
await service.wakeMonitor();
|
|
880
985
|
};
|
|
881
986
|
|
|
882
987
|
const playNotificationSound = async (type: NotificationType): Promise<boolean> => {
|
|
@@ -886,7 +991,8 @@ export default function smartVoiceNotifyExtension(
|
|
|
886
991
|
|
|
887
992
|
if (process.platform === "linux") {
|
|
888
993
|
try {
|
|
889
|
-
const
|
|
994
|
+
const service = await getSoundThemeService();
|
|
995
|
+
const played = await service.playEventSound(type, buildSoundThemeConfig(), SOUND_LOOPS[type]);
|
|
890
996
|
if (played) {
|
|
891
997
|
return true;
|
|
892
998
|
}
|
|
@@ -899,7 +1005,8 @@ export default function smartVoiceNotifyExtension(
|
|
|
899
1005
|
}
|
|
900
1006
|
|
|
901
1007
|
try {
|
|
902
|
-
await
|
|
1008
|
+
const service = await getAudioService();
|
|
1009
|
+
await service.playWindowsSound(type);
|
|
903
1010
|
return isWindows();
|
|
904
1011
|
} catch (error) {
|
|
905
1012
|
logger.debug("sound.play.legacy_failed", {
|
|
@@ -915,7 +1022,8 @@ export default function smartVoiceNotifyExtension(
|
|
|
915
1022
|
return false;
|
|
916
1023
|
}
|
|
917
1024
|
|
|
918
|
-
const
|
|
1025
|
+
const service = await getTTSService();
|
|
1026
|
+
const spoken = await service.speak(message, config.ttsEngine, {
|
|
919
1027
|
signal,
|
|
920
1028
|
sapiVoice: config.sapiVoice,
|
|
921
1029
|
sapiRate: config.sapiRate,
|
|
@@ -926,7 +1034,8 @@ export default function smartVoiceNotifyExtension(
|
|
|
926
1034
|
|
|
927
1035
|
if (isWindows()) {
|
|
928
1036
|
try {
|
|
929
|
-
await
|
|
1037
|
+
const audio = await getAudioService();
|
|
1038
|
+
await audio.speakWithSapi(message, signal);
|
|
930
1039
|
return !signal?.aborted;
|
|
931
1040
|
} catch (error) {
|
|
932
1041
|
logger.debug("tts.sapi_fallback_failed", {
|
|
@@ -944,6 +1053,7 @@ export default function smartVoiceNotifyExtension(
|
|
|
944
1053
|
}
|
|
945
1054
|
|
|
946
1055
|
try {
|
|
1056
|
+
const { sendDesktopNotification } = await import("./desktop-notify.ts");
|
|
947
1057
|
const result = await sendDesktopNotification({
|
|
948
1058
|
type,
|
|
949
1059
|
message,
|
|
@@ -977,9 +1087,14 @@ export default function smartVoiceNotifyExtension(
|
|
|
977
1087
|
}
|
|
978
1088
|
};
|
|
979
1089
|
|
|
980
|
-
const dispatchWebhook = (type: NotificationType, message: string): void => {
|
|
1090
|
+
const dispatchWebhook = async (type: NotificationType, message: string): Promise<void> => {
|
|
1091
|
+
if (!config.webhook.enabled) {
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
981
1095
|
try {
|
|
982
|
-
const
|
|
1096
|
+
const service = await getWebhookService();
|
|
1097
|
+
const dispatchResult = service.dispatch({
|
|
983
1098
|
type,
|
|
984
1099
|
title: `Pi Notification - ${type}`,
|
|
985
1100
|
message,
|
|
@@ -1286,7 +1401,7 @@ export default function smartVoiceNotifyExtension(
|
|
|
1286
1401
|
}
|
|
1287
1402
|
|
|
1288
1403
|
if (!shutdownRequested) {
|
|
1289
|
-
dispatchWebhook(type, spokenMessage);
|
|
1404
|
+
await dispatchWebhook(type, spokenMessage);
|
|
1290
1405
|
}
|
|
1291
1406
|
});
|
|
1292
1407
|
if (!shutdownRequested && options.scheduleReminder !== false) {
|
|
@@ -1312,6 +1427,14 @@ export default function smartVoiceNotifyExtension(
|
|
|
1312
1427
|
if (!config.enabled || !config.enableErrorNotification) {
|
|
1313
1428
|
return;
|
|
1314
1429
|
}
|
|
1430
|
+
if (hasPendingAgentMessages(ctx)) {
|
|
1431
|
+
logger.debug("agent.error_notification.skipped", {
|
|
1432
|
+
reason: "pending_messages",
|
|
1433
|
+
errorReason: outcome.reason,
|
|
1434
|
+
stage: "schedule",
|
|
1435
|
+
});
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1315
1438
|
|
|
1316
1439
|
const timeoutId = setTimeout(() => {
|
|
1317
1440
|
if (pendingAgentErrorNotification !== timeoutId) {
|
|
@@ -1326,6 +1449,15 @@ export default function smartVoiceNotifyExtension(
|
|
|
1326
1449
|
return;
|
|
1327
1450
|
}
|
|
1328
1451
|
|
|
1452
|
+
if (hasPendingAgentMessages(ctx)) {
|
|
1453
|
+
logger.debug("agent.error_notification.skipped", {
|
|
1454
|
+
reason: "pending_messages",
|
|
1455
|
+
errorReason: outcome.reason,
|
|
1456
|
+
stage: "fire",
|
|
1457
|
+
});
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1329
1461
|
logger.debug("agent.error_notification.fired", {
|
|
1330
1462
|
reason: outcome.reason,
|
|
1331
1463
|
graceMs: agentErrorNotificationGraceMs,
|
|
@@ -1594,6 +1726,7 @@ export default function smartVoiceNotifyExtension(
|
|
|
1594
1726
|
const overlayOptions = { anchor: "center" as const, width: 92, maxHeight: "85%" as const, margin: 1 };
|
|
1595
1727
|
const advancedConfigPath = CONFIG_PATH;
|
|
1596
1728
|
const description = `Recommended settings only.\nFor advanced settings, manually edit: ${advancedConfigPath}`;
|
|
1729
|
+
const { ZellijModal, ZellijSettingsModal } = await import("./zellij-modal.ts");
|
|
1597
1730
|
|
|
1598
1731
|
await ctx.ui.custom<void>(
|
|
1599
1732
|
(tui, theme, _keybindings, done) => {
|
|
@@ -1607,7 +1740,7 @@ export default function smartVoiceNotifyExtension(
|
|
|
1607
1740
|
applySetting(draft, id, newValue);
|
|
1608
1741
|
config = normalizeConfig(draft);
|
|
1609
1742
|
refreshIntegratedServiceConfig();
|
|
1610
|
-
syncPermissionForwardingWatcher();
|
|
1743
|
+
void syncPermissionForwardingWatcher();
|
|
1611
1744
|
if (config.debugLog && !previousConfig.debugLog) {
|
|
1612
1745
|
logger.debug("debug.enabled", { debugLogPath: DEBUG_LOG_PATH });
|
|
1613
1746
|
}
|
|
@@ -1683,7 +1816,7 @@ export default function smartVoiceNotifyExtension(
|
|
|
1683
1816
|
if (subcommand === "reload") {
|
|
1684
1817
|
config = readConfig();
|
|
1685
1818
|
refreshIntegratedServiceConfig();
|
|
1686
|
-
syncPermissionForwardingWatcher();
|
|
1819
|
+
await syncPermissionForwardingWatcher();
|
|
1687
1820
|
refreshQuestionToolAvailability();
|
|
1688
1821
|
cancelReminderActivity("command_reload");
|
|
1689
1822
|
updateStatus(ctx);
|
|
@@ -1694,7 +1827,7 @@ export default function smartVoiceNotifyExtension(
|
|
|
1694
1827
|
if (subcommand === "on" || subcommand === "off") {
|
|
1695
1828
|
config.enabled = subcommand === "on";
|
|
1696
1829
|
refreshIntegratedServiceConfig();
|
|
1697
|
-
syncPermissionForwardingWatcher();
|
|
1830
|
+
await syncPermissionForwardingWatcher();
|
|
1698
1831
|
persistConfig(ctx);
|
|
1699
1832
|
if (!config.enabled) {
|
|
1700
1833
|
cancelReminderActivity("command_disabled");
|
|
@@ -1734,9 +1867,9 @@ export default function smartVoiceNotifyExtension(
|
|
|
1734
1867
|
pi.on("resources_discover", (event, _ctx) => {
|
|
1735
1868
|
if (event.reason === "reload") {
|
|
1736
1869
|
// Clear caches on reload
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
aiMessageService
|
|
1870
|
+
clearFocusDetectCacheIfLoaded();
|
|
1871
|
+
clearProjectSoundCacheIfLoaded();
|
|
1872
|
+
aiMessageService?.clearCache();
|
|
1740
1873
|
}
|
|
1741
1874
|
});
|
|
1742
1875
|
|
|
@@ -1750,14 +1883,14 @@ export default function smartVoiceNotifyExtension(
|
|
|
1750
1883
|
config = readConfig();
|
|
1751
1884
|
refreshIntegratedServiceConfig();
|
|
1752
1885
|
activeSessionContext = ctx;
|
|
1753
|
-
syncPermissionForwardingWatcher();
|
|
1886
|
+
await syncPermissionForwardingWatcher();
|
|
1754
1887
|
refreshQuestionToolAvailability();
|
|
1755
|
-
|
|
1756
|
-
|
|
1888
|
+
clearFocusDetectCacheIfLoaded();
|
|
1889
|
+
clearProjectSoundCacheIfLoaded();
|
|
1757
1890
|
|
|
1758
1891
|
// Clear AI message cache on reload to pick up config changes
|
|
1759
1892
|
if (reason === "reload") {
|
|
1760
|
-
aiMessageService
|
|
1893
|
+
aiMessageService?.clearCache();
|
|
1761
1894
|
}
|
|
1762
1895
|
lastUserActivityAt = Date.now();
|
|
1763
1896
|
hadErrorInTurn = false;
|
|
@@ -1804,17 +1937,17 @@ export default function smartVoiceNotifyExtension(
|
|
|
1804
1937
|
shutdownPromise = (async () => {
|
|
1805
1938
|
logger.debug("session.shutdown", {});
|
|
1806
1939
|
activeSessionContext = null;
|
|
1807
|
-
permissionForwardingWatcher
|
|
1940
|
+
permissionForwardingWatcher?.stop();
|
|
1808
1941
|
pendingPermissionToolCallIds.clear();
|
|
1809
1942
|
processedToolResultToolCallIds.clear();
|
|
1810
1943
|
resetPermissionBatch("session_shutdown");
|
|
1811
1944
|
cancelReminderActivity("session_shutdown");
|
|
1812
|
-
|
|
1813
|
-
|
|
1945
|
+
clearFocusDetectCacheIfLoaded();
|
|
1946
|
+
clearProjectSoundCacheIfLoaded();
|
|
1814
1947
|
try {
|
|
1815
1948
|
// Forward abort signal to flush if available (added in pi-coding-agent 0.67.x)
|
|
1816
1949
|
const abortSignal = 'signal' in ctx ? (ctx as { signal?: AbortSignal }).signal : undefined;
|
|
1817
|
-
await webhookService
|
|
1950
|
+
await webhookService?.flush(abortSignal);
|
|
1818
1951
|
} catch (error) {
|
|
1819
1952
|
const message = `Failed to flush webhook queue during shutdown: ${getErrorMessage(error)}`;
|
|
1820
1953
|
logger.error(new Error(message));
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
2
1
|
import { existsSync, readdirSync, readFileSync, watch, type FSWatcher } from "node:fs";
|
|
3
2
|
import { basename, join } from "node:path";
|
|
4
3
|
|
|
4
|
+
import { resolvePiAgentDir } from "./agent-dir.ts";
|
|
5
5
|
import { toRecord } from "./config-store.ts";
|
|
6
6
|
import { getErrorMessage } from "./logging.ts";
|
|
7
7
|
|
|
8
8
|
export type PermissionForwardingSource = "primary" | "legacy";
|
|
9
9
|
export type ForwardedPermissionResolutionReason = "request_removed" | "watch_disabled" | "watcher_stopped";
|
|
10
10
|
|
|
11
|
-
const AGENT_DIR =
|
|
11
|
+
const AGENT_DIR = resolvePiAgentDir();
|
|
12
12
|
const PERMISSION_FORWARDING_ROOT_DIR = join(AGENT_DIR, "sessions", "permission-forwarding");
|
|
13
13
|
const SESSION_FORWARDING_ROOT_DIRECTORY_NAME = "sessions";
|
|
14
14
|
const SESSION_FORWARDING_REQUESTS_DIRECTORY_NAME = "requests";
|
|
@@ -60,7 +60,7 @@ export interface PermissionForwardingWatcherConfig {
|
|
|
60
60
|
targetSessionId?: string | null;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
interface PermissionForwardingWatcherOptions {
|
|
63
|
+
export interface PermissionForwardingWatcherOptions {
|
|
64
64
|
onRequest: (event: ForwardedPermissionRequestEvent) => void;
|
|
65
65
|
onResolve: (event: ForwardedPermissionResolutionEvent) => void;
|
|
66
66
|
debugLog: (event: string, details?: Record<string, unknown>) => void;
|