pi-smart-voice-notify 0.5.1 → 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 +10 -0
- package/package.json +3 -3
- package/src/agent-dir.ts +32 -0
- package/src/config-store.ts +6 -3
- package/src/index.ts +209 -101
- package/src/permission-forwarding-watcher.ts +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
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
|
+
|
|
3
13
|
## [0.5.1] - 2026-05-26
|
|
4
14
|
|
|
5
15
|
### Changed
|
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,8 +61,8 @@
|
|
|
61
61
|
]
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
|
-
"@earendil-works/pi-coding-agent": "^0.74.0 || ^0.75.0",
|
|
65
|
-
"@earendil-works/pi-tui": "^0.74.0 || ^0.75.0"
|
|
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",
|
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
|
|
|
@@ -280,15 +278,21 @@ const REMINDER_EVENT_TYPE: Record<NotificationType, string> = {
|
|
|
280
278
|
};
|
|
281
279
|
|
|
282
280
|
type ReminderKey = string;
|
|
283
|
-
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
|
+
};
|
|
284
288
|
|
|
285
289
|
export interface SmartVoiceNotifyDependencies {
|
|
286
290
|
readConfigFromDisk?: typeof readConfigFromDisk;
|
|
287
|
-
initializeTTSService?: (options?:
|
|
291
|
+
initializeTTSService?: (options?: TTSServiceOptions) => TTSService;
|
|
288
292
|
createPermissionForwardingWatcher?: (
|
|
289
|
-
options:
|
|
293
|
+
options: PermissionForwardingWatcherOptions,
|
|
290
294
|
) => PermissionForwardingWatcherController;
|
|
291
|
-
isTerminalFocused?:
|
|
295
|
+
isTerminalFocused?: FocusDetector;
|
|
292
296
|
}
|
|
293
297
|
|
|
294
298
|
function defaultReminderKey(type: NotificationType): ReminderKey {
|
|
@@ -375,12 +379,7 @@ export default function smartVoiceNotifyExtension(
|
|
|
375
379
|
dependencies: SmartVoiceNotifyDependencies = {},
|
|
376
380
|
): void {
|
|
377
381
|
const readConfig = dependencies.readConfigFromDisk ?? readConfigFromDisk;
|
|
378
|
-
const
|
|
379
|
-
const createPermissionForwardingWatcher =
|
|
380
|
-
dependencies.createPermissionForwardingWatcher ??
|
|
381
|
-
((options: ConstructorParameters<typeof PermissionForwardingWatcher>[0]): PermissionForwardingWatcherController => {
|
|
382
|
-
return new PermissionForwardingWatcher(options);
|
|
383
|
-
});
|
|
382
|
+
const createInjectedTTSService = dependencies.initializeTTSService;
|
|
384
383
|
let config = readConfig();
|
|
385
384
|
let lastUserActivityAt = Date.now();
|
|
386
385
|
let hadErrorInTurn = false;
|
|
@@ -416,13 +415,16 @@ export default function smartVoiceNotifyExtension(
|
|
|
416
415
|
ensureDebugDirectory,
|
|
417
416
|
});
|
|
418
417
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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
|
+
|
|
424
426
|
const projectName = basename(process.cwd()) || "project";
|
|
425
|
-
const
|
|
427
|
+
const injectedTerminalFocusDetector = dependencies.isTerminalFocused;
|
|
426
428
|
const focusDetectionEnabled = envBoolean(
|
|
427
429
|
process.platform === "linux" || process.platform === "win32",
|
|
428
430
|
"PI_SMART_NOTIFY_FOCUS_DETECTION",
|
|
@@ -433,11 +435,6 @@ export default function smartVoiceNotifyExtension(
|
|
|
433
435
|
0,
|
|
434
436
|
envInteger(10_000, "PI_SMART_NOTIFY_AGENT_ERROR_GRACE_MS"),
|
|
435
437
|
);
|
|
436
|
-
const soundThemeService = new SoundThemeService({
|
|
437
|
-
debugLog: (message) => {
|
|
438
|
-
logger.debug("sound_theme.debug", { message });
|
|
439
|
-
},
|
|
440
|
-
});
|
|
441
438
|
const buildTTSServiceConfig = (): TTSConfig => {
|
|
442
439
|
return {
|
|
443
440
|
enableTts: config.enableTts,
|
|
@@ -541,26 +538,96 @@ export default function smartVoiceNotifyExtension(
|
|
|
541
538
|
customSoundDirectories: configuredCustomSoundDirectories.length > 0 ? configuredCustomSoundDirectories : undefined,
|
|
542
539
|
};
|
|
543
540
|
};
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
const
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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
|
+
};
|
|
607
|
+
|
|
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
|
+
};
|
|
562
618
|
|
|
563
|
-
const
|
|
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 = {
|
|
564
631
|
onRequest: (event) => {
|
|
565
632
|
if (!config.enabled || !config.enablePermissionNotification || !config.enableForwardedPermissionWatcher) {
|
|
566
633
|
return;
|
|
@@ -617,28 +684,52 @@ export default function smartVoiceNotifyExtension(
|
|
|
617
684
|
debugLog: (event, details = {}) => {
|
|
618
685
|
logger.debug(event, details);
|
|
619
686
|
},
|
|
620
|
-
}
|
|
687
|
+
};
|
|
688
|
+
|
|
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
|
+
};
|
|
621
700
|
|
|
622
|
-
const syncPermissionForwardingWatcher = (): void => {
|
|
701
|
+
const syncPermissionForwardingWatcher = async (): Promise<void> => {
|
|
623
702
|
if (!activeSessionContext) {
|
|
624
|
-
permissionForwardingWatcher
|
|
703
|
+
permissionForwardingWatcher?.stop();
|
|
625
704
|
return;
|
|
626
705
|
}
|
|
627
|
-
|
|
706
|
+
|
|
707
|
+
const watcherConfig: PermissionForwardingWatcherConfig = {
|
|
628
708
|
enabled: config.enabled && config.enablePermissionNotification && config.enableForwardedPermissionWatcher,
|
|
629
709
|
watchLegacyPath: config.watchLegacyForwardedPermissionPath,
|
|
630
710
|
targetSessionId: getPermissionForwardingSessionId(activeSessionContext),
|
|
631
|
-
}
|
|
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);
|
|
632
721
|
};
|
|
633
722
|
|
|
634
723
|
const refreshIntegratedServiceConfig = (): void => {
|
|
635
|
-
ttsService =
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
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());
|
|
642
733
|
};
|
|
643
734
|
|
|
644
735
|
const rememberScopedToolCallId = (toolCallId: string, seenToolCallIds: Set<string>): boolean => {
|
|
@@ -797,6 +888,7 @@ export default function smartVoiceNotifyExtension(
|
|
|
797
888
|
}
|
|
798
889
|
|
|
799
890
|
try {
|
|
891
|
+
const detectTerminalFocus = await getTerminalFocusDetector();
|
|
800
892
|
const focused = await detectTerminalFocus({
|
|
801
893
|
debug: config.debugLog,
|
|
802
894
|
cacheTtlMs: config.focusCacheTtlMs,
|
|
@@ -836,22 +928,25 @@ export default function smartVoiceNotifyExtension(
|
|
|
836
928
|
}
|
|
837
929
|
|
|
838
930
|
const eventType = options.isReminder ? REMINDER_EVENT_TYPE[type] : type;
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
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
|
+
});
|
|
848
949
|
}
|
|
849
|
-
} catch (error) {
|
|
850
|
-
logger.debug("message.generate.error", {
|
|
851
|
-
type,
|
|
852
|
-
eventType,
|
|
853
|
-
error: getErrorMessage(error),
|
|
854
|
-
});
|
|
855
950
|
}
|
|
856
951
|
|
|
857
952
|
return pickRandom(MESSAGE_LIBRARY[type][options.isReminder ? "reminder" : "initial"]);
|
|
@@ -863,6 +958,7 @@ export default function smartVoiceNotifyExtension(
|
|
|
863
958
|
}
|
|
864
959
|
|
|
865
960
|
if (process.platform === "linux") {
|
|
961
|
+
const { getIdleTime, wakeMonitor: wakeLinuxMonitor } = await import("./linux.ts");
|
|
866
962
|
const idleSeconds = await getIdleTime({
|
|
867
963
|
debugLog: (message) => {
|
|
868
964
|
logger.debug("linux.idle", { message });
|
|
@@ -884,7 +980,8 @@ export default function smartVoiceNotifyExtension(
|
|
|
884
980
|
return;
|
|
885
981
|
}
|
|
886
982
|
|
|
887
|
-
await
|
|
983
|
+
const service = await getAudioService();
|
|
984
|
+
await service.wakeMonitor();
|
|
888
985
|
};
|
|
889
986
|
|
|
890
987
|
const playNotificationSound = async (type: NotificationType): Promise<boolean> => {
|
|
@@ -894,7 +991,8 @@ export default function smartVoiceNotifyExtension(
|
|
|
894
991
|
|
|
895
992
|
if (process.platform === "linux") {
|
|
896
993
|
try {
|
|
897
|
-
const
|
|
994
|
+
const service = await getSoundThemeService();
|
|
995
|
+
const played = await service.playEventSound(type, buildSoundThemeConfig(), SOUND_LOOPS[type]);
|
|
898
996
|
if (played) {
|
|
899
997
|
return true;
|
|
900
998
|
}
|
|
@@ -907,7 +1005,8 @@ export default function smartVoiceNotifyExtension(
|
|
|
907
1005
|
}
|
|
908
1006
|
|
|
909
1007
|
try {
|
|
910
|
-
await
|
|
1008
|
+
const service = await getAudioService();
|
|
1009
|
+
await service.playWindowsSound(type);
|
|
911
1010
|
return isWindows();
|
|
912
1011
|
} catch (error) {
|
|
913
1012
|
logger.debug("sound.play.legacy_failed", {
|
|
@@ -923,7 +1022,8 @@ export default function smartVoiceNotifyExtension(
|
|
|
923
1022
|
return false;
|
|
924
1023
|
}
|
|
925
1024
|
|
|
926
|
-
const
|
|
1025
|
+
const service = await getTTSService();
|
|
1026
|
+
const spoken = await service.speak(message, config.ttsEngine, {
|
|
927
1027
|
signal,
|
|
928
1028
|
sapiVoice: config.sapiVoice,
|
|
929
1029
|
sapiRate: config.sapiRate,
|
|
@@ -934,7 +1034,8 @@ export default function smartVoiceNotifyExtension(
|
|
|
934
1034
|
|
|
935
1035
|
if (isWindows()) {
|
|
936
1036
|
try {
|
|
937
|
-
await
|
|
1037
|
+
const audio = await getAudioService();
|
|
1038
|
+
await audio.speakWithSapi(message, signal);
|
|
938
1039
|
return !signal?.aborted;
|
|
939
1040
|
} catch (error) {
|
|
940
1041
|
logger.debug("tts.sapi_fallback_failed", {
|
|
@@ -952,6 +1053,7 @@ export default function smartVoiceNotifyExtension(
|
|
|
952
1053
|
}
|
|
953
1054
|
|
|
954
1055
|
try {
|
|
1056
|
+
const { sendDesktopNotification } = await import("./desktop-notify.ts");
|
|
955
1057
|
const result = await sendDesktopNotification({
|
|
956
1058
|
type,
|
|
957
1059
|
message,
|
|
@@ -985,9 +1087,14 @@ export default function smartVoiceNotifyExtension(
|
|
|
985
1087
|
}
|
|
986
1088
|
};
|
|
987
1089
|
|
|
988
|
-
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
|
+
|
|
989
1095
|
try {
|
|
990
|
-
const
|
|
1096
|
+
const service = await getWebhookService();
|
|
1097
|
+
const dispatchResult = service.dispatch({
|
|
991
1098
|
type,
|
|
992
1099
|
title: `Pi Notification - ${type}`,
|
|
993
1100
|
message,
|
|
@@ -1294,7 +1401,7 @@ export default function smartVoiceNotifyExtension(
|
|
|
1294
1401
|
}
|
|
1295
1402
|
|
|
1296
1403
|
if (!shutdownRequested) {
|
|
1297
|
-
dispatchWebhook(type, spokenMessage);
|
|
1404
|
+
await dispatchWebhook(type, spokenMessage);
|
|
1298
1405
|
}
|
|
1299
1406
|
});
|
|
1300
1407
|
if (!shutdownRequested && options.scheduleReminder !== false) {
|
|
@@ -1619,6 +1726,7 @@ export default function smartVoiceNotifyExtension(
|
|
|
1619
1726
|
const overlayOptions = { anchor: "center" as const, width: 92, maxHeight: "85%" as const, margin: 1 };
|
|
1620
1727
|
const advancedConfigPath = CONFIG_PATH;
|
|
1621
1728
|
const description = `Recommended settings only.\nFor advanced settings, manually edit: ${advancedConfigPath}`;
|
|
1729
|
+
const { ZellijModal, ZellijSettingsModal } = await import("./zellij-modal.ts");
|
|
1622
1730
|
|
|
1623
1731
|
await ctx.ui.custom<void>(
|
|
1624
1732
|
(tui, theme, _keybindings, done) => {
|
|
@@ -1632,7 +1740,7 @@ export default function smartVoiceNotifyExtension(
|
|
|
1632
1740
|
applySetting(draft, id, newValue);
|
|
1633
1741
|
config = normalizeConfig(draft);
|
|
1634
1742
|
refreshIntegratedServiceConfig();
|
|
1635
|
-
syncPermissionForwardingWatcher();
|
|
1743
|
+
void syncPermissionForwardingWatcher();
|
|
1636
1744
|
if (config.debugLog && !previousConfig.debugLog) {
|
|
1637
1745
|
logger.debug("debug.enabled", { debugLogPath: DEBUG_LOG_PATH });
|
|
1638
1746
|
}
|
|
@@ -1708,7 +1816,7 @@ export default function smartVoiceNotifyExtension(
|
|
|
1708
1816
|
if (subcommand === "reload") {
|
|
1709
1817
|
config = readConfig();
|
|
1710
1818
|
refreshIntegratedServiceConfig();
|
|
1711
|
-
syncPermissionForwardingWatcher();
|
|
1819
|
+
await syncPermissionForwardingWatcher();
|
|
1712
1820
|
refreshQuestionToolAvailability();
|
|
1713
1821
|
cancelReminderActivity("command_reload");
|
|
1714
1822
|
updateStatus(ctx);
|
|
@@ -1719,7 +1827,7 @@ export default function smartVoiceNotifyExtension(
|
|
|
1719
1827
|
if (subcommand === "on" || subcommand === "off") {
|
|
1720
1828
|
config.enabled = subcommand === "on";
|
|
1721
1829
|
refreshIntegratedServiceConfig();
|
|
1722
|
-
syncPermissionForwardingWatcher();
|
|
1830
|
+
await syncPermissionForwardingWatcher();
|
|
1723
1831
|
persistConfig(ctx);
|
|
1724
1832
|
if (!config.enabled) {
|
|
1725
1833
|
cancelReminderActivity("command_disabled");
|
|
@@ -1759,9 +1867,9 @@ export default function smartVoiceNotifyExtension(
|
|
|
1759
1867
|
pi.on("resources_discover", (event, _ctx) => {
|
|
1760
1868
|
if (event.reason === "reload") {
|
|
1761
1869
|
// Clear caches on reload
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
aiMessageService
|
|
1870
|
+
clearFocusDetectCacheIfLoaded();
|
|
1871
|
+
clearProjectSoundCacheIfLoaded();
|
|
1872
|
+
aiMessageService?.clearCache();
|
|
1765
1873
|
}
|
|
1766
1874
|
});
|
|
1767
1875
|
|
|
@@ -1775,14 +1883,14 @@ export default function smartVoiceNotifyExtension(
|
|
|
1775
1883
|
config = readConfig();
|
|
1776
1884
|
refreshIntegratedServiceConfig();
|
|
1777
1885
|
activeSessionContext = ctx;
|
|
1778
|
-
syncPermissionForwardingWatcher();
|
|
1886
|
+
await syncPermissionForwardingWatcher();
|
|
1779
1887
|
refreshQuestionToolAvailability();
|
|
1780
|
-
|
|
1781
|
-
|
|
1888
|
+
clearFocusDetectCacheIfLoaded();
|
|
1889
|
+
clearProjectSoundCacheIfLoaded();
|
|
1782
1890
|
|
|
1783
1891
|
// Clear AI message cache on reload to pick up config changes
|
|
1784
1892
|
if (reason === "reload") {
|
|
1785
|
-
aiMessageService
|
|
1893
|
+
aiMessageService?.clearCache();
|
|
1786
1894
|
}
|
|
1787
1895
|
lastUserActivityAt = Date.now();
|
|
1788
1896
|
hadErrorInTurn = false;
|
|
@@ -1829,17 +1937,17 @@ export default function smartVoiceNotifyExtension(
|
|
|
1829
1937
|
shutdownPromise = (async () => {
|
|
1830
1938
|
logger.debug("session.shutdown", {});
|
|
1831
1939
|
activeSessionContext = null;
|
|
1832
|
-
permissionForwardingWatcher
|
|
1940
|
+
permissionForwardingWatcher?.stop();
|
|
1833
1941
|
pendingPermissionToolCallIds.clear();
|
|
1834
1942
|
processedToolResultToolCallIds.clear();
|
|
1835
1943
|
resetPermissionBatch("session_shutdown");
|
|
1836
1944
|
cancelReminderActivity("session_shutdown");
|
|
1837
|
-
|
|
1838
|
-
|
|
1945
|
+
clearFocusDetectCacheIfLoaded();
|
|
1946
|
+
clearProjectSoundCacheIfLoaded();
|
|
1839
1947
|
try {
|
|
1840
1948
|
// Forward abort signal to flush if available (added in pi-coding-agent 0.67.x)
|
|
1841
1949
|
const abortSignal = 'signal' in ctx ? (ctx as { signal?: AbortSignal }).signal : undefined;
|
|
1842
|
-
await webhookService
|
|
1950
|
+
await webhookService?.flush(abortSignal);
|
|
1843
1951
|
} catch (error) {
|
|
1844
1952
|
const message = `Failed to flush webhook queue during shutdown: ${getErrorMessage(error)}`;
|
|
1845
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;
|