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 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.1",
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",
@@ -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
+ }
@@ -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(getAgentDir(), "extensions", EXTENSION_ID);
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
- writeFileSync(CONFIG_PATH, `${JSON.stringify(validation.config, null, 2)}\n`, "utf-8");
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 { initializeAIMessageService, type AIMessageConfig } from "./ai-messages.ts";
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 { sendDesktopNotification } from "./desktop-notify.ts";
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 { PermissionForwardingWatcher } from "./permission-forwarding-watcher.ts";
39
- import { clearProjectSoundCache } from "./per-project-sound.ts";
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 { SoundThemeService, type SoundThemeConfig } from "./sound-theme.ts";
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 { createWebhookService, type WebhookConfig } from "./webhook.ts";
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 PermissionForwardingWatcherController = Pick<PermissionForwardingWatcher, "start" | "updateConfig" | "stop">;
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?: Parameters<typeof initializeTTSService>[0]) => TTSService;
291
+ initializeTTSService?: (options?: TTSServiceOptions) => TTSService;
288
292
  createPermissionForwardingWatcher?: (
289
- options: ConstructorParameters<typeof PermissionForwardingWatcher>[0],
293
+ options: PermissionForwardingWatcherOptions,
290
294
  ) => PermissionForwardingWatcherController;
291
- isTerminalFocused?: typeof 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 createTTSService = dependencies.initializeTTSService ?? initializeTTSService;
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
- const audioService = new AudioNotificationService({
420
- execRunner: pi,
421
- getConfig: () => config,
422
- debug: logger.debug,
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 detectTerminalFocus = dependencies.isTerminalFocused ?? isTerminalFocused;
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
- let ttsService = createTTSService({
545
- execRunner: pi,
546
- config: buildTTSServiceConfig(),
547
- debug: logger.debug,
548
- });
549
- const aiMessageService = initializeAIMessageService({
550
- config: buildAIMessageConfig(),
551
- debugLog: (message, details = {}) => {
552
- logger.debug(`ai_messages.${message}`, details);
553
- },
554
- });
555
- const webhookService = createWebhookService(buildWebhookConfig());
556
- const linuxSession = detectLinuxSession();
557
- logger.debug("linux.session.detected", {
558
- sessionType: linuxSession.sessionType,
559
- display: linuxSession.display,
560
- waylandDisplay: linuxSession.waylandDisplay,
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 permissionForwardingWatcher = createPermissionForwardingWatcher({
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.stop();
703
+ permissionForwardingWatcher?.stop();
625
704
  return;
626
705
  }
627
- permissionForwardingWatcher.start({
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 = createTTSService({
636
- execRunner: pi,
637
- config: buildTTSServiceConfig(),
638
- debug: logger.debug,
639
- });
640
- aiMessageService.updateConfig(buildAIMessageConfig());
641
- webhookService.updateConfig(buildWebhookConfig());
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
- try {
840
- const generated = await aiMessageService.generateMessage(eventType, {
841
- projectName,
842
- time: new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }),
843
- count: options.followUpCount,
844
- reason: options.reason,
845
- });
846
- if (generated.trim().length > 0) {
847
- return generated;
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 audioService.wakeMonitor();
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 played = await soundThemeService.playEventSound(type, buildSoundThemeConfig(), SOUND_LOOPS[type]);
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 audioService.playWindowsSound(type);
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 spoken = await ttsService.speak(message, config.ttsEngine, {
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 audioService.speakWithSapi(message, signal);
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 dispatchResult = webhookService.dispatch({
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
- clearFocusDetectCache();
1763
- clearProjectSoundCache();
1764
- aiMessageService.clearCache();
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
- clearFocusDetectCache();
1781
- clearProjectSoundCache();
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.clearCache();
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.stop();
1940
+ permissionForwardingWatcher?.stop();
1833
1941
  pendingPermissionToolCallIds.clear();
1834
1942
  processedToolResultToolCallIds.clear();
1835
1943
  resetPermissionBatch("session_shutdown");
1836
1944
  cancelReminderActivity("session_shutdown");
1837
- clearFocusDetectCache();
1838
- clearProjectSoundCache();
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.flush(abortSignal);
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 = getAgentDir();
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;