pi-usereq 0.6.0 → 0.7.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/src/index.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  * @brief Declares the extension version string.
9
9
  * @details The value is exported for external inspection and packaging metadata alignment. Access complexity is O(1).
10
10
  */
11
- export const VERSION = "0.6.0"
11
+ export const VERSION = "0.7.0"
12
12
 
13
13
  import path from "node:path";
14
14
  import type {
@@ -52,6 +52,7 @@ import {
52
52
  type FindToolScope,
53
53
  } from "./core/find-payload.js";
54
54
  import {
55
+ DEFAULT_SRC_DIRS,
55
56
  getDefaultConfig,
56
57
  getProjectConfigPath,
57
58
  loadConfig,
@@ -61,14 +62,22 @@ import {
61
62
  type UseReqConfig,
62
63
  } from "./core/config.js";
63
64
  import {
65
+ DEFAULT_PI_NOTIFY_CMD,
66
+ DEFAULT_PI_NOTIFY_PUSHOVER_TEXT,
67
+ DEFAULT_PI_NOTIFY_PUSHOVER_TITLE,
68
+ DEFAULT_PI_NOTIFY_SOUND_HIGH_CMD,
69
+ DEFAULT_PI_NOTIFY_SOUND_LOW_CMD,
70
+ DEFAULT_PI_NOTIFY_SOUND_MID_CMD,
64
71
  cyclePiNotifySoundLevel,
65
- formatPiNotifyBeepStatus,
66
72
  formatPiNotifyPushoverStatus,
73
+ formatPiNotifyStatus,
74
+ normalizePiNotifyCommand,
67
75
  normalizePiNotifyPushoverCredential,
68
76
  normalizePiNotifyPushoverPriority,
77
+ normalizePiNotifyTemplateValue,
69
78
  runPiNotifyEffects,
79
+ type PiNotifyEventRequest,
70
80
  type PiNotifyPushoverPriority,
71
- type PiNotifyPushoverRequest,
72
81
  type PiNotifySoundLevel,
73
82
  } from "./core/pi-notify.js";
74
83
  import { formatRuntimePathForDisplay } from "./core/path-context.js";
@@ -104,9 +113,7 @@ import {
104
113
  } from "./core/tool-runner.js";
105
114
  import { LANGUAGE_TAGS } from "./core/find-constructs.js";
106
115
  import {
107
- STATIC_CHECK_MODULES,
108
116
  getSupportedStaticCheckLanguageSupport,
109
- parseEnableStaticCheck,
110
117
  } from "./core/static-check.js";
111
118
  import { makeRelativeIfContainsProject, shellSplit } from "./core/utils.js";
112
119
 
@@ -187,15 +194,17 @@ function saveProjectConfig(cwd: string, config: UseReqConfig): void {
187
194
 
188
195
  /**
189
196
  * @brief Formats the current project config path for top-level menu display.
190
- * @details Resolves `<base-path>/.pi-usereq/config.json` from the cwd-derived
191
- * project base and formats it relative to the user home when possible. Runtime
192
- * is O(p) in path length. No external state is mutated.
197
+ * @details Resolves `<base-path>/.pi-usereq/config.json` from the cwd-derived project base, reuses the shared runtime-path formatter, and rewrites a leading POSIX `$HOME` token to `~` for the `Show configuration` row only. Runtime is O(p) in path length. No external state is mutated.
193
198
  * @param[in] cwd {string} Current working directory.
194
- * @return {string} User-home-relative or absolute config path display value.
199
+ * @return {string} `~`-relative or absolute config path display value.
195
200
  * @satisfies REQ-162
196
201
  */
197
202
  function formatProjectConfigPathForMenu(cwd: string): string {
198
- return formatRuntimePathForDisplay(getProjectConfigPath(getProjectBase(cwd)));
203
+ const displayPath = formatRuntimePathForDisplay(getProjectConfigPath(getProjectBase(cwd)));
204
+ if (displayPath === "$HOME") {
205
+ return "~";
206
+ }
207
+ return displayPath.startsWith("$HOME/") ? `~/${displayPath.slice("$HOME/".length)}` : displayPath;
199
208
  }
200
209
 
201
210
  /**
@@ -278,6 +287,43 @@ function buildTokenToolExecuteResult(
278
287
  return buildStructuredToolExecuteResult(details);
279
288
  }
280
289
 
290
+ /**
291
+ * @brief Builds the fallback execute result returned when token counting fails before payload construction.
292
+ * @details Normalizes one thrown `ReqError` into the stable token-tool response shape so missing runtime dependencies or other pre-count failures do not abort extension tool execution. Runtime is O(1). No external state is mutated.
293
+ * @param[in] error {unknown} Thrown token-tool failure.
294
+ * @return {{ content: Array<{ type: "text"; text: string }>; details: TokenToolPayload & { execution: { code: number; stderr: string } } }} Structured token-tool failure result.
295
+ */
296
+ function buildFailedTokenToolExecuteResult(
297
+ error: unknown,
298
+ ): {
299
+ content: Array<{ type: "text"; text: string }>;
300
+ details: TokenToolPayload & { execution: { code: number; stderr: string } };
301
+ } {
302
+ const result = normalizeToolFailure(error);
303
+ const details = {
304
+ summary: {
305
+ processable_file_count: 0,
306
+ counted_file_count: 0,
307
+ error_file_count: 0,
308
+ skipped_file_count: 0,
309
+ total_token_count: 0,
310
+ total_character_count: 0,
311
+ total_byte_count: 0,
312
+ total_line_count: 0,
313
+ average_token_count_per_counted_file: 0,
314
+ average_character_count_per_counted_file: 0,
315
+ average_byte_count_per_counted_file: 0,
316
+ average_line_count_per_counted_file: 0,
317
+ },
318
+ files: [],
319
+ execution: {
320
+ code: result.code,
321
+ stderr: result.stderr,
322
+ },
323
+ };
324
+ return buildStructuredToolExecuteResult(details);
325
+ }
326
+
281
327
  /**
282
328
  * @brief Builds the agent-oriented execute result returned by references tools.
283
329
  * @details Mirrors the structured references payload into both the text `content` channel and the machine-readable `details` channel while isolating execution metadata under `execution`. Runtime is O(n) in payload size. No side effects occur.
@@ -494,19 +540,6 @@ function getConfiguredEnabledPiUsereqTools(config: UseReqConfig): string[] {
494
540
  return enabledTools;
495
541
  }
496
542
 
497
- /**
498
- * @brief Classifies one configurable tool as embedded or extension-owned.
499
- * @details Uses the runtime `sourceInfo.source` field plus the supported embedded-name subset to produce one stable UI label. Runtime is O(1). No external state is mutated.
500
- * @param[in] tool {ToolInfo} Runtime tool descriptor.
501
- * @return {"builtin" | "extension"} Stable tool-kind label.
502
- */
503
- function getPiUsereqToolKind(tool: ToolInfo): "builtin" | "extension" {
504
- if (tool.sourceInfo?.source === "builtin" && isPiUsereqEmbeddedToolName(tool.name)) {
505
- return "builtin";
506
- }
507
- return "extension";
508
- }
509
-
510
543
  /**
511
544
  * @brief Applies the configured active-tool enablement to the current session.
512
545
  * @details Preserves non-configurable active tools, removes every configurable tool from the active set, then re-adds only configured tools that exist in the current runtime inventory. Runtime is O(t). Side effects include `pi.setActiveTools(...)`.
@@ -539,20 +572,19 @@ function applyConfiguredPiUsereqTools(pi: ExtensionAPI, config: UseReqConfig): v
539
572
  * @details Applies session-start-specific resource validation, project-config
540
573
  * refresh, and startup-tool enablement before forwarding the originating hook
541
574
  * name and payload into the shared `updateExtensionStatus(...)` pipeline.
542
- * On `agent_end`, also dispatches configured pi-notify beep, sound, and
575
+ * On `agent_end`, also dispatches configured command-notify, sound, and
543
576
  * prompt-specific Pushover effects when the current run originates from a
544
577
  * bundled prompt command. Runtime is dominated by configuration loading during
545
578
  * `session_start`; all other hooks are O(1). Side effects include resource
546
579
  * checks, active-tool mutation, status updates, live-ticker disposal on
547
- * shutdown, stdout writes, optional child-process spawning, and outbound
548
- * HTTPS requests.
580
+ * shutdown, optional child-process spawning, and outbound HTTPS requests.
549
581
  * @param[in] pi {ExtensionAPI} Active extension API instance.
550
582
  * @param[in,out] statusController {PiUsereqStatusController} Mutable status controller.
551
583
  * @param[in] hookName {PiUsereqStatusHookName} Intercepted hook name.
552
584
  * @param[in] event {unknown} Hook payload forwarded by pi.
553
585
  * @param[in] ctx {ExtensionContext} Active extension context.
554
586
  * @return {Promise<void>} Promise resolved when hook processing completes.
555
- * @satisfies REQ-117, REQ-118, REQ-119, REQ-129, REQ-130, REQ-131, REQ-132, REQ-133, REQ-166, REQ-167, REQ-168, REQ-169, REQ-172
587
+ * @satisfies REQ-117, REQ-118, REQ-119, REQ-131, REQ-132, REQ-133, REQ-166, REQ-167, REQ-168, REQ-169, REQ-172, REQ-176, REQ-178, REQ-184, REQ-185, REQ-186, REQ-187
556
588
  */
557
589
  async function handleExtensionStatusEvent(
558
590
  pi: ExtensionAPI,
@@ -561,6 +593,16 @@ async function handleExtensionStatusEvent(
561
593
  event: unknown,
562
594
  ctx: ExtensionContext,
563
595
  ): Promise<void> {
596
+ const notifyRequest: PiNotifyEventRequest | undefined = hookName === "agent_end"
597
+ && statusController.state.activePromptRequest !== undefined
598
+ && statusController.state.runStartTimeMs !== undefined
599
+ ? {
600
+ promptName: statusController.state.activePromptRequest.promptName,
601
+ promptArgs: statusController.state.activePromptRequest.promptArgs,
602
+ basePath: path.resolve(ctx.cwd),
603
+ completionTimeMs: Math.max(0, Date.now() - statusController.state.runStartTimeMs),
604
+ }
605
+ : undefined;
564
606
  if (hookName === "session_start") {
565
607
  ensureBundledResourcesAccessible();
566
608
  const config = loadProjectConfig(ctx.cwd);
@@ -570,19 +612,10 @@ async function handleExtensionStatusEvent(
570
612
  updateExtensionStatus(statusController, hookName, event, ctx);
571
613
  if (hookName === "agent_end") {
572
614
  if (statusController.config) {
573
- const pushoverRequest: PiNotifyPushoverRequest | undefined = statusController.state.activePromptRequest
574
- && statusController.state.lastRunDurationMs !== undefined
575
- ? {
576
- promptName: statusController.state.activePromptRequest.promptName,
577
- promptArgs: statusController.state.activePromptRequest.promptArgs,
578
- basePath: path.resolve(ctx.cwd),
579
- completionTimeMs: statusController.state.lastRunDurationMs,
580
- }
581
- : undefined;
582
615
  runPiNotifyEffects(
583
616
  statusController.config,
584
617
  event as { messages: AgentEndEvent["messages"] },
585
- pushoverRequest,
618
+ notifyRequest,
586
619
  );
587
620
  }
588
621
  statusController.state.activePromptRequest = undefined;
@@ -632,131 +665,397 @@ function setConfiguredPiUsereqTools(pi: ExtensionAPI, config: UseReqConfig, enab
632
665
  }
633
666
 
634
667
  /**
635
- * @brief Renders a textual reference for configurable-tool configuration and runtime state.
636
- * @details Lists every configurable tool with configured enablement, runtime activation, builtin-versus-extension classification, source metadata, and optional descriptions. Runtime is O(t). No side effects occur.
637
- * @param[in] pi {ExtensionAPI} Active extension API instance.
638
- * @param[in] config {UseReqConfig} Effective project configuration.
639
- * @return {string} Multiline tool-status report.
668
+ * @brief Represents one persisted boolean notification-setting key.
669
+ * @details Restricts menu toggles to the global enable flags and completed/interrupted/failed event toggles used by command-notify, sound, and Pushover configuration. Compile-time only and introduces no runtime cost.
640
670
  */
641
- function renderPiUsereqToolsReference(pi: ExtensionAPI, config: UseReqConfig): string {
642
- const tools = getPiUsereqStartupTools(pi);
643
- const enabledTools = new Set(getConfiguredEnabledPiUsereqTools(config));
644
- const activeTools = new Set(pi.getActiveTools());
645
- const lines = [
646
- "# configurable active tools",
647
- "",
648
- `Configured enabled tools: ${enabledTools.size}/${tools.length}`,
649
- `Currently active tools: ${tools.filter((tool) => activeTools.has(tool.name)).length}/${tools.length}`,
650
- "",
651
- "Tools:",
652
- ];
671
+ type PiNotifyBooleanConfigKey =
672
+ | "notify-enabled"
673
+ | "notify-on-completed"
674
+ | "notify-on-interrupted"
675
+ | "notify-on-failed"
676
+ | "notify-sound-on-completed"
677
+ | "notify-sound-on-interrupted"
678
+ | "notify-sound-on-failed"
679
+ | "notify-pushover-enabled"
680
+ | "notify-pushover-on-completed"
681
+ | "notify-pushover-on-interrupted"
682
+ | "notify-pushover-on-failed";
653
683
 
654
- for (const tool of tools) {
655
- const configured = enabledTools.has(tool.name) ? "enabled" : "disabled";
656
- const active = activeTools.has(tool.name) ? "active" : "inactive";
657
- const source = tool.sourceInfo ? `${tool.sourceInfo.source}:${tool.sourceInfo.path}` : "unknown";
658
- lines.push(`- ${tool.name}`);
659
- lines.push(` kind: ${getPiUsereqToolKind(tool)}`);
660
- lines.push(` configured: ${configured}`);
661
- lines.push(` runtime: ${active}`);
662
- lines.push(` source: ${source}`);
663
- if (tool.description) {
664
- lines.push(` description: ${tool.description}`);
665
- }
666
- }
684
+ /**
685
+ * @brief Represents one persisted boolean notification event-toggle key.
686
+ * @details Restricts shared event-submenu mutation helpers to completed/interrupted/failed toggles and excludes global enable flags. Compile-time only and introduces no runtime cost.
687
+ */
688
+ type PiNotifyEventBooleanConfigKey = Exclude<
689
+ PiNotifyBooleanConfigKey,
690
+ "notify-enabled" | "notify-pushover-enabled"
691
+ >;
692
+
693
+ /**
694
+ * @brief Represents one shared prompt-end event identifier used by notification menus.
695
+ * @details Restricts event-submenu rendering to the canonical completed/interrupted/failed domain shared by command-notify, sound, and Pushover routing. Compile-time only and introduces no runtime cost.
696
+ */
697
+ type PiNotifyEventId = "completed" | "interrupted" | "failed";
667
698
 
668
- return `${lines.join("\n")}\n`;
699
+ /**
700
+ * @brief Describes one shared prompt-end event row rendered inside notification event submenus.
701
+ * @details Binds one canonical event identifier to the human-readable label and terminal-outcome description reused across command-notify, sound, and Pushover event menus. The interface is compile-time only and introduces no runtime cost.
702
+ */
703
+ interface PiNotifyEventRowDefinition {
704
+ eventId: PiNotifyEventId;
705
+ label: string;
706
+ description: string;
669
707
  }
670
708
 
671
709
  /**
672
- * @brief Represents one persisted pi-notify beep flag key.
673
- * @details Restricts menu toggles to the three independent prompt-end beep
674
- * flags stored in project configuration. Compile-time only and introduces no
675
- * runtime cost.
710
+ * @brief Describes one notification-system event submenu contract.
711
+ * @details Binds the top-level launcher row, submenu title, toast prefix, and completed/interrupted/failed config keys for one notification transport. The interface is compile-time only and introduces no runtime cost.
676
712
  */
677
- type PiNotifyBeepConfigKey =
678
- | "notify-beep-on-end"
679
- | "notify-beep-on-esc"
680
- | "notify-beep-on-error";
713
+ interface PiNotifyEventMenuDefinition {
714
+ topLevelId: string;
715
+ topLevelLabel: string;
716
+ submenuTitle: string;
717
+ systemLabel: string;
718
+ keys: Record<PiNotifyEventId, PiNotifyEventBooleanConfigKey>;
719
+ }
681
720
 
682
721
  /**
683
- * @brief Flips one persisted pi-notify beep flag.
684
- * @details Negates the selected prompt-end beep flag in place and returns the resulting boolean value so callers can emit deterministic UI feedback. Runtime is O(1). Side effect: mutates `config`.
722
+ * @brief Flips one persisted boolean notification setting.
723
+ * @details Negates the selected configuration flag in place and returns the resulting boolean value so callers can emit deterministic UI feedback. Runtime is O(1). Side effect: mutates `config`.
685
724
  * @param[in,out] config {UseReqConfig} Mutable configuration object.
686
- * @param[in] key {PiNotifyBeepConfigKey} Beep flag key to toggle.
725
+ * @param[in] key {PiNotifyBooleanConfigKey} Boolean configuration key to toggle.
687
726
  * @return {boolean} Next enabled state.
688
727
  */
689
- function togglePiNotifyBeepFlag(config: UseReqConfig, key: PiNotifyBeepConfigKey): boolean {
728
+ function togglePiNotifyFlag(config: UseReqConfig, key: PiNotifyBooleanConfigKey): boolean {
690
729
  config[key] = !config[key];
691
730
  return config[key];
692
731
  }
693
732
 
733
+ /**
734
+ * @brief Restores notification-related settings to their documented defaults.
735
+ * @details Copies the command-notify, sound, and Pushover configuration subtree from a fresh default config into the supplied mutable project config. Runtime is O(1). Side effect: mutates `config`.
736
+ * @param[in,out] config {UseReqConfig} Mutable configuration object.
737
+ * @return {void} No return value.
738
+ * @satisfies REQ-174, REQ-178, REQ-184, REQ-195, REQ-196
739
+ */
740
+ function resetPiNotifyConfigToDefaults(config: UseReqConfig): void {
741
+ const defaults = getDefaultConfig("");
742
+ config["notify-enabled"] = defaults["notify-enabled"];
743
+ config["notify-on-completed"] = defaults["notify-on-completed"];
744
+ config["notify-on-interrupted"] = defaults["notify-on-interrupted"];
745
+ config["notify-on-failed"] = defaults["notify-on-failed"];
746
+ config["notify-sound"] = defaults["notify-sound"];
747
+ config["notify-sound-on-completed"] = defaults["notify-sound-on-completed"];
748
+ config["notify-sound-on-interrupted"] = defaults["notify-sound-on-interrupted"];
749
+ config["notify-sound-on-failed"] = defaults["notify-sound-on-failed"];
750
+ config["notify-sound-toggle-shortcut"] = defaults["notify-sound-toggle-shortcut"];
751
+ config["notify-pushover-enabled"] = defaults["notify-pushover-enabled"];
752
+ config["notify-pushover-on-completed"] = defaults["notify-pushover-on-completed"];
753
+ config["notify-pushover-on-interrupted"] = defaults["notify-pushover-on-interrupted"];
754
+ config["notify-pushover-on-failed"] = defaults["notify-pushover-on-failed"];
755
+ config["notify-pushover-user-key"] = defaults["notify-pushover-user-key"];
756
+ config["notify-pushover-api-token"] = defaults["notify-pushover-api-token"];
757
+ config["notify-pushover-priority"] = defaults["notify-pushover-priority"];
758
+ config["notify-pushover-title"] = defaults["notify-pushover-title"];
759
+ config["notify-pushover-text"] = defaults["notify-pushover-text"];
760
+ config.PI_NOTIFY_CMD = defaults.PI_NOTIFY_CMD;
761
+ config.PI_NOTIFY_SOUND_LOW_CMD = defaults.PI_NOTIFY_SOUND_LOW_CMD;
762
+ config.PI_NOTIFY_SOUND_MID_CMD = defaults.PI_NOTIFY_SOUND_MID_CMD;
763
+ config.PI_NOTIFY_SOUND_HIGH_CMD = defaults.PI_NOTIFY_SOUND_HIGH_CMD;
764
+ }
765
+
694
766
  /**
695
767
  * @brief Formats one persisted Pushover priority for menu display.
696
- * @details Maps the canonical `0|1` priority domain to deterministic menu text reused by the Pushover configuration UI. Runtime is O(1). No external state is mutated.
768
+ * @details Maps the canonical `0|1` priority domain to deterministic `Normal|High` labels reused by the Pushover configuration UI. Runtime is O(1). No external state is mutated.
697
769
  * @param[in] priority {PiNotifyPushoverPriority} Persisted Pushover priority.
698
770
  * @return {string} Menu-display label.
699
- * @satisfies REQ-165
771
+ * @satisfies REQ-172
700
772
  */
701
773
  function formatPiNotifyPushoverPriority(priority: PiNotifyPushoverPriority): string {
702
- return priority === 1 ? "1=High Priority" : "0=Normal";
774
+ return priority === 1 ? "High" : "Normal";
703
775
  }
704
776
 
705
777
  /**
706
- * @brief Builds the shared settings-menu choices for Pushover configuration.
707
- * @details Serializes the Pushover global-disable flag, successful-completion enable flag, credential strings, and priority value into right-valued menu rows consumed by the shared settings-menu renderer. Runtime is O(1). No external state is mutated.
778
+ * @brief Defines the shared prompt-end event rows reused across notification submenus.
779
+ * @details Encodes the human-readable completed/interrupted/failed labels and terminal-state descriptions required by the command-notify, sound, and Pushover event menus. Access complexity is O(1).
780
+ * @satisfies REQ-188, REQ-198
781
+ */
782
+ const PI_NOTIFY_EVENT_ROW_DEFINITIONS: PiNotifyEventRowDefinition[] = [
783
+ {
784
+ eventId: "completed",
785
+ label: "Prompt completed",
786
+ description: "Toggle delivery when the prompt finishes without interruption or failure.",
787
+ },
788
+ {
789
+ eventId: "interrupted",
790
+ label: "Prompt interrupted",
791
+ description: "Toggle delivery when the prompt finishes with assistant stopReason `aborted`.",
792
+ },
793
+ {
794
+ eventId: "failed",
795
+ label: "Prompt failed",
796
+ description: "Toggle delivery when the prompt finishes with assistant stopReason `error`.",
797
+ },
798
+ ];
799
+
800
+ /**
801
+ * @brief Defines the shared event-submenu contracts for command-notify, sound, and Pushover.
802
+ * @details Binds each notification transport to its top-level launcher row, submenu title, toast prefix, and completed/interrupted/failed config-key set. Access complexity is O(1).
803
+ * @satisfies REQ-174, REQ-178, REQ-184, REQ-198
804
+ */
805
+ const PI_NOTIFY_EVENT_MENU_DEFINITIONS: Record<
806
+ "notification" | "sound" | "pushover",
807
+ PiNotifyEventMenuDefinition
808
+ > = {
809
+ notification: {
810
+ topLevelId: "notification-events",
811
+ topLevelLabel: "Notification events",
812
+ submenuTitle: "Notification events",
813
+ systemLabel: "Notification",
814
+ keys: {
815
+ completed: "notify-on-completed",
816
+ interrupted: "notify-on-interrupted",
817
+ failed: "notify-on-failed",
818
+ },
819
+ },
820
+ sound: {
821
+ topLevelId: "sound-events",
822
+ topLevelLabel: "Sound events",
823
+ submenuTitle: "Sound events",
824
+ systemLabel: "Sound",
825
+ keys: {
826
+ completed: "notify-sound-on-completed",
827
+ interrupted: "notify-sound-on-interrupted",
828
+ failed: "notify-sound-on-failed",
829
+ },
830
+ },
831
+ pushover: {
832
+ topLevelId: "pushover-events",
833
+ topLevelLabel: "Pushover events",
834
+ submenuTitle: "Pushover events",
835
+ systemLabel: "Pushover",
836
+ keys: {
837
+ completed: "notify-pushover-on-completed",
838
+ interrupted: "notify-pushover-on-interrupted",
839
+ failed: "notify-pushover-on-failed",
840
+ },
841
+ },
842
+ };
843
+
844
+ /**
845
+ * @brief Formats the top-level summary value for one notification event submenu.
846
+ * @details Counts enabled completed/interrupted/failed toggles for the selected transport and renders the result as `n/3 on` for right-aligned menu display. Runtime is O(1). No external state is mutated.
708
847
  * @param[in] config {UseReqConfig} Effective project configuration.
709
- * @return {PiUsereqSettingsMenuChoice[]} Ordered Pushover-menu choice vector.
710
- * @satisfies REQ-163, REQ-165, REQ-166, REQ-172
848
+ * @param[in] eventMenu {PiNotifyEventMenuDefinition} Notification-system event submenu contract.
849
+ * @return {string} Compact enabled-toggle summary.
850
+ * @satisfies REQ-198
711
851
  */
712
- function buildPiNotifyPushoverMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuChoice[] {
852
+ function formatPiNotifyEventMenuSummary(
853
+ config: UseReqConfig,
854
+ eventMenu: PiNotifyEventMenuDefinition,
855
+ ): string {
856
+ const enabledCount = PI_NOTIFY_EVENT_ROW_DEFINITIONS.filter(
857
+ (row) => config[eventMenu.keys[row.eventId]] === true,
858
+ ).length;
859
+ return `${enabledCount}/3 on`;
860
+ }
861
+
862
+ /**
863
+ * @brief Builds the top-level launcher row for one notification event submenu.
864
+ * @details Reuses the shared completed/interrupted/failed summary renderer so the `Notifications` menu can expose dedicated event editors for command-notify, sound, and Pushover in a uniform shape. Runtime is O(1). No external state is mutated.
865
+ * @param[in] config {UseReqConfig} Effective project configuration.
866
+ * @param[in] eventMenu {PiNotifyEventMenuDefinition} Notification-system event submenu contract.
867
+ * @return {PiUsereqSettingsMenuChoice} Launcher row for the selected event submenu.
868
+ * @satisfies REQ-181, REQ-183, REQ-165, REQ-198
869
+ */
870
+ function buildPiNotifyEventLauncherChoice(
871
+ config: UseReqConfig,
872
+ eventMenu: PiNotifyEventMenuDefinition,
873
+ ): PiUsereqSettingsMenuChoice {
874
+ return {
875
+ id: eventMenu.topLevelId,
876
+ label: eventMenu.topLevelLabel,
877
+ value: formatPiNotifyEventMenuSummary(config, eventMenu),
878
+ description: `Open the ${eventMenu.submenuTitle} submenu for shared prompt-end delivery events.`,
879
+ };
880
+ }
881
+
882
+ /**
883
+ * @brief Builds the shared settings-menu choices for one notification event submenu.
884
+ * @details Serializes completed/interrupted/failed rows with right-aligned `on|off` values, then appends `Reset defaults` and `Save and close` for submenu-scoped mutation control. Runtime is O(1). No external state is mutated.
885
+ * @param[in] config {UseReqConfig} Effective project configuration.
886
+ * @param[in] eventMenu {PiNotifyEventMenuDefinition} Notification-system event submenu contract.
887
+ * @return {PiUsereqSettingsMenuChoice[]} Ordered event-submenu choice vector.
888
+ * @satisfies REQ-188, REQ-193, REQ-198
889
+ */
890
+ function buildPiNotifyEventMenuChoices(
891
+ config: UseReqConfig,
892
+ eventMenu: PiNotifyEventMenuDefinition,
893
+ ): PiUsereqSettingsMenuChoice[] {
713
894
  return [
895
+ ...PI_NOTIFY_EVENT_ROW_DEFINITIONS.map((row) => ({
896
+ id: eventMenu.keys[row.eventId],
897
+ label: row.label,
898
+ value: config[eventMenu.keys[row.eventId]] ? "on" : "off",
899
+ description: `${eventMenu.systemLabel}: ${row.description}`,
900
+ })),
901
+ {
902
+ id: "reset-defaults",
903
+ label: "Reset defaults",
904
+ value: "",
905
+ description: `Restore the documented default ${eventMenu.systemLabel.toLowerCase()} event toggles.`,
906
+ },
714
907
  {
715
- id: "pushover-global-disable",
716
- label: "Global disable",
717
- value: config["notify-pushover-global-disable"] ? "on" : "off",
718
- description: "Suppress all Pushover delivery without changing the successful-prompt enable flag.",
908
+ id: "save-and-close",
909
+ label: "Save and close",
910
+ value: "",
911
+ description: "Return to the notifications menu.",
719
912
  },
913
+ ];
914
+ }
915
+
916
+ /**
917
+ * @brief Restores one notification event submenu to its documented defaults.
918
+ * @details Copies only the completed/interrupted/failed toggles referenced by the supplied submenu contract from a fresh default config into the mutable project config. Runtime is O(1). Side effect: mutates `config`.
919
+ * @param[in,out] config {UseReqConfig} Mutable configuration object.
920
+ * @param[in] eventMenu {PiNotifyEventMenuDefinition} Notification-system event submenu contract.
921
+ * @return {void} No return value.
922
+ * @satisfies REQ-174, REQ-178, REQ-184, REQ-195
923
+ */
924
+ function resetPiNotifyEventMenuToDefaults(
925
+ config: UseReqConfig,
926
+ eventMenu: PiNotifyEventMenuDefinition,
927
+ ): void {
928
+ const defaults = getDefaultConfig("");
929
+ for (const key of Object.values(eventMenu.keys) as PiNotifyEventBooleanConfigKey[]) {
930
+ config[key] = defaults[key];
931
+ }
932
+ }
933
+
934
+ /**
935
+ * @brief Resolves the human-readable event label for one event-toggle config key.
936
+ * @details Matches the supplied config key against the submenu contract and returns the corresponding completed/interrupted/failed menu label for deterministic notification toasts. Runtime is O(1). No external state is mutated.
937
+ * @param[in] key {PiNotifyEventBooleanConfigKey} Event-toggle configuration key.
938
+ * @param[in] eventMenu {PiNotifyEventMenuDefinition} Notification-system event submenu contract.
939
+ * @return {string} Human-readable event label.
940
+ * @satisfies REQ-188, REQ-198
941
+ */
942
+ function resolvePiNotifyEventLabel(
943
+ key: PiNotifyEventBooleanConfigKey,
944
+ eventMenu: PiNotifyEventMenuDefinition,
945
+ ): string {
946
+ return PI_NOTIFY_EVENT_ROW_DEFINITIONS.find(
947
+ (row) => eventMenu.keys[row.eventId] === key,
948
+ )?.label ?? key;
949
+ }
950
+
951
+ /**
952
+ * @brief Runs one dedicated notification event submenu.
953
+ * @details Reuses the shared settings-menu renderer to toggle completed/interrupted/failed delivery flags, preserve row focus, and apply submenu-scoped reset semantics for command-notify, sound, or Pushover events. Runtime depends on user interaction count. Side effects include UI updates and config mutation.
954
+ * @param[in] ctx {ExtensionCommandContext} Active command context.
955
+ * @param[in,out] config {UseReqConfig} Mutable configuration object.
956
+ * @param[in] eventMenu {PiNotifyEventMenuDefinition} Notification-system event submenu contract.
957
+ * @return {Promise<void>} Promise resolved when the submenu closes.
958
+ * @satisfies REQ-188, REQ-192, REQ-193, REQ-195, REQ-198
959
+ */
960
+ async function configurePiNotifyEventMenu(
961
+ ctx: ExtensionCommandContext,
962
+ config: UseReqConfig,
963
+ eventMenu: PiNotifyEventMenuDefinition,
964
+ ): Promise<void> {
965
+ let focusedChoiceId: string | undefined;
966
+ while (true) {
967
+ const choice = await showPiUsereqSettingsMenu(
968
+ ctx,
969
+ eventMenu.submenuTitle,
970
+ buildPiNotifyEventMenuChoices(config, eventMenu),
971
+ { initialSelectedId: focusedChoiceId },
972
+ );
973
+ if (!choice || choice === "save-and-close") {
974
+ return;
975
+ }
976
+ focusedChoiceId = choice;
977
+ if (choice === "reset-defaults") {
978
+ resetPiNotifyEventMenuToDefaults(config, eventMenu);
979
+ ctx.ui.notify(
980
+ `Restored default ${eventMenu.systemLabel.toLowerCase()} events`,
981
+ "info",
982
+ );
983
+ continue;
984
+ }
985
+ const enabled = togglePiNotifyFlag(
986
+ config,
987
+ choice as PiNotifyEventBooleanConfigKey,
988
+ );
989
+ const eventLabel = resolvePiNotifyEventLabel(
990
+ choice as PiNotifyEventBooleanConfigKey,
991
+ eventMenu,
992
+ );
993
+ ctx.ui.notify(
994
+ `${eventMenu.systemLabel} ${eventLabel} ${enabled ? "enabled" : "disabled"}`,
995
+ "info",
996
+ );
997
+ }
998
+ }
999
+
1000
+ /**
1001
+ * @brief Builds the direct Pushover rows rendered inside `Notifications`.
1002
+ * @details Serializes the global enable flag, shared-event submenu launcher, priority, title, text, and credential rows into right-valued menu items appended after the sound-command rows. Runtime is O(1). No external state is mutated.
1003
+ * @param[in] config {UseReqConfig} Effective project configuration.
1004
+ * @return {PiUsereqSettingsMenuChoice[]} Ordered direct Pushover rows.
1005
+ * @satisfies REQ-163, REQ-165, REQ-172, REQ-184, REQ-185, REQ-198
1006
+ */
1007
+ function buildPiNotifyPushoverRows(config: UseReqConfig): PiUsereqSettingsMenuChoice[] {
1008
+ return [
720
1009
  {
721
- id: "pushover-on-success",
722
- label: "Notify on success",
1010
+ id: "notify-pushover-enabled",
1011
+ label: "Enable pushover",
723
1012
  value: formatPiNotifyPushoverStatus(config),
724
- description: "Toggle Pushover delivery after successful prompt completion only.",
1013
+ description: "Enable or disable all Pushover delivery globally.",
725
1014
  },
1015
+ buildPiNotifyEventLauncherChoice(
1016
+ config,
1017
+ PI_NOTIFY_EVENT_MENU_DEFINITIONS.pushover,
1018
+ ),
726
1019
  {
727
- id: "pushover-user-key",
728
- label: "User Key/Delivery Group Key",
729
- value: config["notify-pushover-user-key"] || "(empty)",
730
- description: "Edit the Pushover user key or delivery group key sent with successful prompt notifications.",
1020
+ id: "notify-pushover-priority",
1021
+ label: "Pushover priority",
1022
+ value: formatPiNotifyPushoverPriority(config["notify-pushover-priority"]),
1023
+ description: "Select whether outbound Pushover messages use Normal or High priority.",
731
1024
  },
732
1025
  {
733
- id: "pushover-api-token",
734
- label: "Token/API Token Key",
735
- value: config["notify-pushover-api-token"] || "(empty)",
736
- description: "Edit the Pushover application token used for successful prompt notifications.",
1026
+ id: "notify-pushover-title",
1027
+ label: "Pushover title",
1028
+ value: config["notify-pushover-title"],
1029
+ description: "Edit the title template used for outbound Pushover messages.",
737
1030
  },
738
1031
  {
739
- id: "pushover-priority",
740
- label: "Priority",
741
- value: formatPiNotifyPushoverPriority(config["notify-pushover-priority"]),
742
- description: "Select whether successful prompt notifications use normal or high Pushover priority.",
1032
+ id: "notify-pushover-text",
1033
+ label: "Pushover text",
1034
+ value: config["notify-pushover-text"],
1035
+ description: "Edit the text template used for outbound Pushover messages.",
743
1036
  },
744
1037
  {
745
- id: "back",
746
- label: "Back",
747
- value: "",
748
- description: "Return to the notifications menu.",
1038
+ id: "notify-pushover-user-key",
1039
+ label: "Pushover User Key/Delivery Group Key",
1040
+ value: config["notify-pushover-user-key"] || "(empty)",
1041
+ description: "Edit the Pushover user key or delivery group key used for outbound requests.",
1042
+ },
1043
+ {
1044
+ id: "notify-pushover-api-token",
1045
+ label: "Pushover Token/API Token Key",
1046
+ value: config["notify-pushover-api-token"] || "(empty)",
1047
+ description: "Edit the Pushover application token used for outbound requests.",
749
1048
  },
750
1049
  ];
751
1050
  }
752
1051
 
753
1052
  /**
754
1053
  * @brief Opens the shared settings-menu selector for Pushover priority.
755
- * @details Reuses the pi-usereq settings-menu renderer so Pushover priority selection remains stylistically aligned with the existing notification menus and returns the chosen priority or `undefined` on cancel. Runtime depends on user interaction count. Side effects are limited to transient custom-UI rendering.
1054
+ * @details Reuses the pi-usereq settings-menu renderer so Pushover priority selection remains stylistically aligned with the notification menus and returns the chosen priority or `undefined` on cancel. Runtime depends on user interaction count. Side effects are limited to transient custom-UI rendering.
756
1055
  * @param[in] ctx {ExtensionCommandContext} Active command context.
757
1056
  * @param[in] currentPriority {PiNotifyPushoverPriority} Persisted priority value.
758
1057
  * @return {Promise<PiNotifyPushoverPriority | undefined>} Selected priority or `undefined` when cancelled.
759
- * @satisfies REQ-165
1058
+ * @satisfies REQ-172, REQ-192
760
1059
  */
761
1060
  async function selectPiNotifyPushoverPriority(
762
1061
  ctx: ExtensionCommandContext,
@@ -765,148 +1064,92 @@ async function selectPiNotifyPushoverPriority(
765
1064
  const choice = await showPiUsereqSettingsMenu(ctx, "Pushover priority", [
766
1065
  {
767
1066
  id: "0",
768
- label: "0=Normal",
1067
+ label: "Normal",
769
1068
  value: currentPriority === 0 ? "selected" : "",
770
- description: "Send successful prompt notifications with normal Pushover priority.",
1069
+ description: "Send outbound Pushover messages with normal priority `0`.",
771
1070
  },
772
1071
  {
773
1072
  id: "1",
774
- label: "1=High Priority",
1073
+ label: "High",
775
1074
  value: currentPriority === 1 ? "selected" : "",
776
- description: "Send successful prompt notifications with high Pushover priority.",
1075
+ description: "Send outbound Pushover messages with high priority `1`.",
777
1076
  },
778
- ]);
1077
+ ], { initialSelectedId: String(currentPriority) });
779
1078
  if (!choice) {
780
1079
  return undefined;
781
1080
  }
782
1081
  return normalizePiNotifyPushoverPriority(choice);
783
1082
  }
784
1083
 
785
- /**
786
- * @brief Runs the interactive Pushover-configuration menu.
787
- * @details Exposes the Pushover global-disable flag, successful-completion enable flag, user key, API token, and priority selector through the shared settings-menu renderer. Runtime depends on user interaction count. Side effects include UI updates and config mutation.
788
- * @param[in] ctx {ExtensionCommandContext} Active command context.
789
- * @param[in,out] config {UseReqConfig} Mutable configuration object.
790
- * @return {Promise<void>} Promise resolved when the menu closes.
791
- * @satisfies REQ-163, REQ-165, REQ-166, REQ-172
792
- */
793
- async function configurePiNotifyPushoverMenu(
794
- ctx: ExtensionCommandContext,
795
- config: UseReqConfig,
796
- ): Promise<void> {
797
- while (true) {
798
- const choice = await showPiUsereqSettingsMenu(ctx, "Pushover notifications", buildPiNotifyPushoverMenuChoices(config));
799
- if (!choice || choice === "back") {
800
- return;
801
- }
802
- if (choice === "pushover-global-disable") {
803
- config["notify-pushover-global-disable"] = !config["notify-pushover-global-disable"];
804
- ctx.ui.notify(`Pushover global disable ${config["notify-pushover-global-disable"] ? "enabled" : "disabled"}`, "info");
805
- continue;
806
- }
807
- if (choice === "pushover-on-success") {
808
- config["notify-pushover-on-success"] = !config["notify-pushover-on-success"];
809
- ctx.ui.notify(`Pushover on success ${config["notify-pushover-on-success"] ? "enabled" : "disabled"}`, "info");
810
- continue;
811
- }
812
- if (choice === "pushover-user-key") {
813
- const value = await ctx.ui.input(
814
- "User Key/Delivery Group Key",
815
- config["notify-pushover-user-key"] || "gzfjjvp1xxmhibqwzh9m7i1zwvf83j",
816
- );
817
- if (value !== undefined) {
818
- config["notify-pushover-user-key"] = normalizePiNotifyPushoverCredential(value);
819
- ctx.ui.notify("Updated Pushover user key", "info");
820
- }
821
- continue;
822
- }
823
- if (choice === "pushover-api-token") {
824
- const value = await ctx.ui.input(
825
- "Token/API Token Key",
826
- config["notify-pushover-api-token"] || "ah6bf5u2sj63mcvou6qamiabeoubbe",
827
- );
828
- if (value !== undefined) {
829
- config["notify-pushover-api-token"] = normalizePiNotifyPushoverCredential(value);
830
- ctx.ui.notify("Updated Pushover API token", "info");
831
- }
832
- continue;
833
- }
834
- if (choice === "pushover-priority") {
835
- const nextPriority = await selectPiNotifyPushoverPriority(ctx, config["notify-pushover-priority"]);
836
- if (nextPriority !== undefined) {
837
- config["notify-pushover-priority"] = nextPriority;
838
- ctx.ui.notify(`Pushover priority set to ${formatPiNotifyPushoverPriority(nextPriority)}`, "info");
839
- }
840
- }
841
- }
842
- }
843
-
844
1084
  /**
845
1085
  * @brief Builds the shared settings-menu choices for notification configuration.
846
- * @details Serializes the current beep flags, selected notify command, hotkey bind, per-level notify commands, and the Pushover submenu entry into right-valued menu rows consumed by the shared settings-menu renderer. Runtime is O(1) plus command-length formatting. No external state is mutated.
1086
+ * @details Serializes command-notify, sound, and Pushover blocks with dedicated shared-event submenu launchers so the settings-menu renderer can expose one unified but modular configuration surface. Runtime is O(1) plus command-length formatting. No external state is mutated.
847
1087
  * @param[in] config {UseReqConfig} Effective project configuration.
848
1088
  * @return {PiUsereqSettingsMenuChoice[]} Ordered notification-menu choice vector.
849
- * @satisfies REQ-137, REQ-149, REQ-150, REQ-151, REQ-152, REQ-163, REQ-164, REQ-165, REQ-166, REQ-172
1089
+ * @satisfies REQ-137, REQ-149, REQ-150, REQ-151, REQ-152, REQ-163, REQ-164, REQ-165, REQ-172, REQ-179, REQ-181, REQ-183, REQ-188, REQ-193, REQ-198
850
1090
  */
851
1091
  function buildPiNotifyMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuChoice[] {
852
1092
  return [
853
1093
  {
854
- id: "toggle-beep-on-success",
855
- label: "Toggle beep on success",
856
- value: config["notify-beep-on-end"] ? "on" : "off",
857
- description: "Toggle terminal beep delivery for successful prompt completion.",
858
- },
859
- {
860
- id: "toggle-beep-on-escape",
861
- label: "Toggle beep on escape",
862
- value: config["notify-beep-on-esc"] ? "on" : "off",
863
- description: "Toggle terminal beep delivery for escape-triggered prompt abortion.",
1094
+ id: "notify-enabled",
1095
+ label: "Enable notification",
1096
+ value: formatPiNotifyStatus(config),
1097
+ description: "Enable or disable command-notify delivery globally.",
864
1098
  },
1099
+ buildPiNotifyEventLauncherChoice(
1100
+ config,
1101
+ PI_NOTIFY_EVENT_MENU_DEFINITIONS.notification,
1102
+ ),
865
1103
  {
866
- id: "toggle-beep-on-error",
867
- label: "Toggle beep on error",
868
- value: config["notify-beep-on-error"] ? "on" : "off",
869
- description: "Toggle terminal beep delivery for error-terminated prompt completion.",
1104
+ id: "notify-command",
1105
+ label: "Notify command",
1106
+ value: config.PI_NOTIFY_CMD,
1107
+ description: "Edit the shell command used for command-notify delivery.",
870
1108
  },
871
1109
  {
872
- id: "selected-notify-command",
873
- label: "Selected notify command",
1110
+ id: "selected-sound-command",
1111
+ label: "Enable sound",
874
1112
  value: config["notify-sound"],
875
- description: "Select which notify command runs after successful prompt completion.",
1113
+ description: "Select which sound command level is currently active.",
876
1114
  },
1115
+ buildPiNotifyEventLauncherChoice(
1116
+ config,
1117
+ PI_NOTIFY_EVENT_MENU_DEFINITIONS.sound,
1118
+ ),
877
1119
  {
878
1120
  id: "sound-toggle-hotkey-bind",
879
1121
  label: "Sound toggle hotkey bind",
880
1122
  value: config["notify-sound-toggle-shortcut"],
881
- description: "Edit the keyboard shortcut that cycles the selected notify command.",
1123
+ description: "Edit the keyboard shortcut that cycles the selected sound command.",
882
1124
  },
883
1125
  {
884
- id: "notify-command-low",
885
- label: "Notify command (low vol.)",
1126
+ id: "sound-command-low",
1127
+ label: "Sound command (low vol.)",
886
1128
  value: config.PI_NOTIFY_SOUND_LOW_CMD,
887
- description: "Edit the shell command used when the selected notify command is `low`.",
1129
+ description: "Edit the shell command used when the selected sound command is `low`.",
888
1130
  },
889
1131
  {
890
- id: "notify-command-mid",
891
- label: "Notify command (mid vol.)",
1132
+ id: "sound-command-mid",
1133
+ label: "Sound command (mid vol.)",
892
1134
  value: config.PI_NOTIFY_SOUND_MID_CMD,
893
- description: "Edit the shell command used when the selected notify command is `mid`.",
1135
+ description: "Edit the shell command used when the selected sound command is `mid`.",
894
1136
  },
895
1137
  {
896
- id: "notify-command-high",
897
- label: "Notify command (high vol.)",
1138
+ id: "sound-command-high",
1139
+ label: "Sound command (high vol.)",
898
1140
  value: config.PI_NOTIFY_SOUND_HIGH_CMD,
899
- description: "Edit the shell command used when the selected notify command is `high`.",
1141
+ description: "Edit the shell command used when the selected sound command is `high`.",
900
1142
  },
1143
+ ...buildPiNotifyPushoverRows(config),
901
1144
  {
902
- id: "pushover-notifications",
903
- label: "Pushover notifications",
904
- value: formatPiNotifyPushoverStatus(config),
905
- description: "Open the Pushover submenu for successful-completion delivery, credentials, priority, and global disable.",
1145
+ id: "reset-defaults",
1146
+ label: "Reset defaults",
1147
+ value: "",
1148
+ description: "Restore the documented notification defaults for command-notify, sound, and Pushover settings.",
906
1149
  },
907
1150
  {
908
- id: "back",
909
- label: "Back",
1151
+ id: "save-and-close",
1152
+ label: "Save and close",
910
1153
  value: "",
911
1154
  description: "Return to the parent configuration menu.",
912
1155
  },
@@ -914,127 +1157,229 @@ function buildPiNotifyMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuCho
914
1157
  }
915
1158
 
916
1159
  /**
917
- * @brief Opens the shared settings-menu selector for the selected notify command.
918
- * @details Reuses the pi-usereq settings-menu renderer so notify-command selection remains stylistically aligned with the main configuration UI and returns the chosen sound level or `undefined` on cancel. Runtime depends on user interaction count. Side effects are limited to transient custom-UI rendering.
1160
+ * @brief Opens the shared settings-menu selector for the active sound level.
1161
+ * @details Reuses the pi-usereq settings-menu renderer so sound-level selection remains stylistically aligned with the notification menu and returns the chosen sound level or `undefined` on cancel. Runtime depends on user interaction count. Side effects are limited to transient custom-UI rendering.
919
1162
  * @param[in] ctx {ExtensionCommandContext} Active command context.
920
- * @param[in] currentLevel {PiNotifySoundLevel} Currently selected notify command.
1163
+ * @param[in] currentLevel {PiNotifySoundLevel} Currently selected sound level.
921
1164
  * @return {Promise<PiNotifySoundLevel | undefined>} Selected sound level or `undefined` when cancelled.
922
- * @satisfies REQ-131, REQ-137, REQ-149, REQ-151, REQ-152, REQ-153, REQ-154
1165
+ * @satisfies REQ-131, REQ-179, REQ-192
923
1166
  */
924
1167
  async function selectPiNotifySoundLevel(
925
1168
  ctx: ExtensionCommandContext,
926
1169
  currentLevel: PiNotifySoundLevel,
927
1170
  ): Promise<PiNotifySoundLevel | undefined> {
928
- const choice = await showPiUsereqSettingsMenu(ctx, "Selected notify command", [
1171
+ const choice = await showPiUsereqSettingsMenu(ctx, "Enable sound", [
929
1172
  {
930
1173
  id: "none",
931
1174
  label: "none",
932
1175
  value: currentLevel === "none" ? "selected" : "",
933
- description: "Disable external notify-command execution.",
1176
+ description: "Disable sound-command delivery while preserving per-event sound toggles.",
934
1177
  },
935
1178
  {
936
1179
  id: "low",
937
1180
  label: "low",
938
1181
  value: currentLevel === "low" ? "selected" : "",
939
- description: "Run the low-volume notify command after successful completion.",
1182
+ description: "Use the low-volume sound command when sound delivery is enabled for the current event.",
940
1183
  },
941
1184
  {
942
1185
  id: "mid",
943
1186
  label: "mid",
944
1187
  value: currentLevel === "mid" ? "selected" : "",
945
- description: "Run the mid-volume notify command after successful completion.",
1188
+ description: "Use the mid-volume sound command when sound delivery is enabled for the current event.",
946
1189
  },
947
1190
  {
948
1191
  id: "high",
949
1192
  label: "high",
950
1193
  value: currentLevel === "high" ? "selected" : "",
951
- description: "Run the high-volume notify command after successful completion.",
1194
+ description: "Use the high-volume sound command when sound delivery is enabled for the current event.",
952
1195
  },
953
- ]);
1196
+ ], { initialSelectedId: currentLevel });
954
1197
  return choice ? choice as PiNotifySoundLevel : undefined;
955
1198
  }
956
1199
 
957
1200
  /**
958
1201
  * @brief Runs the interactive notification-configuration menu.
959
- * @details Exposes prompt-end beep toggles, selected notify-command selection, hotkey-bind editing, per-level notify-command editors, and the nested Pushover submenu through the shared settings-menu renderer. Runtime depends on user interaction count. Side effects include UI updates and config mutation.
1202
+ * @details Exposes command-notify, sound, and Pushover controls through the shared settings-menu renderer, delegates completed/interrupted/failed toggles to dedicated event submenus, and preserves row focus across menu re-renders. Runtime depends on user interaction count. Side effects include UI updates and config mutation.
960
1203
  * @param[in] ctx {ExtensionCommandContext} Active command context.
961
1204
  * @param[in,out] config {UseReqConfig} Mutable configuration object.
962
1205
  * @return {Promise<boolean>} `true` when the sound-toggle shortcut changed.
963
- * @satisfies REQ-129, REQ-131, REQ-133, REQ-134, REQ-137, REQ-149, REQ-150, REQ-151, REQ-152, REQ-153, REQ-154, REQ-163, REQ-164, REQ-165, REQ-166, REQ-172
1206
+ * @satisfies REQ-131, REQ-133, REQ-134, REQ-137, REQ-163, REQ-164, REQ-165, REQ-172, REQ-179, REQ-181, REQ-183, REQ-184, REQ-188, REQ-192, REQ-193, REQ-195, REQ-196, REQ-198
964
1207
  */
965
1208
  async function configurePiNotifyMenu(
966
1209
  ctx: ExtensionCommandContext,
967
1210
  config: UseReqConfig,
968
1211
  ): Promise<boolean> {
969
1212
  const originalShortcut = config["notify-sound-toggle-shortcut"];
1213
+ let focusedChoiceId: string | undefined;
970
1214
  while (true) {
971
- const choice = await showPiUsereqSettingsMenu(ctx, "notifications", buildPiNotifyMenuChoices(config));
972
- if (!choice || choice === "back") {
1215
+ const choice = await showPiUsereqSettingsMenu(
1216
+ ctx,
1217
+ "Notifications",
1218
+ buildPiNotifyMenuChoices(config),
1219
+ { initialSelectedId: focusedChoiceId },
1220
+ );
1221
+ if (!choice || choice === "save-and-close") {
973
1222
  return config["notify-sound-toggle-shortcut"] !== originalShortcut;
974
1223
  }
975
- if (choice === "toggle-beep-on-success") {
976
- const enabled = togglePiNotifyBeepFlag(config, "notify-beep-on-end");
977
- ctx.ui.notify(`Beep on success ${enabled ? "enabled" : "disabled"}`, "info");
1224
+ focusedChoiceId = choice;
1225
+ if (choice === "notify-enabled" || choice === "notify-pushover-enabled") {
1226
+ const enabled = togglePiNotifyFlag(config, choice as PiNotifyBooleanConfigKey);
1227
+ const labelMap: Record<string, string> = {
1228
+ "notify-enabled": "Notification",
1229
+ "notify-pushover-enabled": "Pushover",
1230
+ };
1231
+ ctx.ui.notify(`${labelMap[choice]} ${enabled ? "enabled" : "disabled"}`, "info");
1232
+ continue;
1233
+ }
1234
+ if (choice === PI_NOTIFY_EVENT_MENU_DEFINITIONS.notification.topLevelId) {
1235
+ await configurePiNotifyEventMenu(
1236
+ ctx,
1237
+ config,
1238
+ PI_NOTIFY_EVENT_MENU_DEFINITIONS.notification,
1239
+ );
978
1240
  continue;
979
1241
  }
980
- if (choice === "toggle-beep-on-escape") {
981
- const enabled = togglePiNotifyBeepFlag(config, "notify-beep-on-esc");
982
- ctx.ui.notify(`Beep on escape ${enabled ? "enabled" : "disabled"}`, "info");
1242
+ if (choice === PI_NOTIFY_EVENT_MENU_DEFINITIONS.sound.topLevelId) {
1243
+ await configurePiNotifyEventMenu(
1244
+ ctx,
1245
+ config,
1246
+ PI_NOTIFY_EVENT_MENU_DEFINITIONS.sound,
1247
+ );
1248
+ continue;
1249
+ }
1250
+ if (choice === PI_NOTIFY_EVENT_MENU_DEFINITIONS.pushover.topLevelId) {
1251
+ await configurePiNotifyEventMenu(
1252
+ ctx,
1253
+ config,
1254
+ PI_NOTIFY_EVENT_MENU_DEFINITIONS.pushover,
1255
+ );
983
1256
  continue;
984
1257
  }
985
- if (choice === "toggle-beep-on-error") {
986
- const enabled = togglePiNotifyBeepFlag(config, "notify-beep-on-error");
987
- ctx.ui.notify(`Beep on error ${enabled ? "enabled" : "disabled"}`, "info");
1258
+ if (choice === "notify-command") {
1259
+ const value = await ctx.ui.input("Notify command", config.PI_NOTIFY_CMD);
1260
+ if (value !== undefined) {
1261
+ config.PI_NOTIFY_CMD = normalizePiNotifyCommand(value, DEFAULT_PI_NOTIFY_CMD);
1262
+ ctx.ui.notify("Updated notify command", "info");
1263
+ }
988
1264
  continue;
989
1265
  }
990
- if (choice === "selected-notify-command") {
1266
+ if (choice === "selected-sound-command") {
991
1267
  const nextLevel = await selectPiNotifySoundLevel(ctx, config["notify-sound"]);
992
- if (nextLevel) {
1268
+ if (nextLevel !== undefined) {
993
1269
  config["notify-sound"] = nextLevel;
994
- ctx.ui.notify(`Selected notify command set to ${nextLevel}`, "info");
1270
+ ctx.ui.notify(`Enable sound set to ${nextLevel}`, "info");
995
1271
  }
996
1272
  continue;
997
1273
  }
998
1274
  if (choice === "sound-toggle-hotkey-bind") {
999
- const value = await ctx.ui.input("Sound toggle hotkey bind", config["notify-sound-toggle-shortcut"]);
1275
+ const value = await ctx.ui.input(
1276
+ "Sound toggle hotkey bind",
1277
+ config["notify-sound-toggle-shortcut"],
1278
+ );
1000
1279
  if (value?.trim()) {
1001
1280
  config["notify-sound-toggle-shortcut"] = value.trim();
1002
- ctx.ui.notify(`Sound toggle hotkey bind set to ${config["notify-sound-toggle-shortcut"]}`, "info");
1281
+ ctx.ui.notify(
1282
+ `Sound toggle hotkey bind set to ${config["notify-sound-toggle-shortcut"]}`,
1283
+ "info",
1284
+ );
1003
1285
  }
1004
1286
  continue;
1005
1287
  }
1006
- if (choice === "notify-command-low") {
1007
- const value = await ctx.ui.input("Notify command (low vol.)", config.PI_NOTIFY_SOUND_LOW_CMD);
1008
- if (value?.trim()) {
1009
- config.PI_NOTIFY_SOUND_LOW_CMD = value.trim();
1010
- ctx.ui.notify("Updated notify command (low vol.)", "info");
1288
+ if (choice === "sound-command-low") {
1289
+ const value = await ctx.ui.input("Sound command (low vol.)", config.PI_NOTIFY_SOUND_LOW_CMD);
1290
+ if (value !== undefined) {
1291
+ config.PI_NOTIFY_SOUND_LOW_CMD = normalizePiNotifyCommand(
1292
+ value,
1293
+ DEFAULT_PI_NOTIFY_SOUND_LOW_CMD,
1294
+ );
1295
+ ctx.ui.notify("Updated sound command (low vol.)", "info");
1011
1296
  }
1012
1297
  continue;
1013
1298
  }
1014
- if (choice === "notify-command-mid") {
1015
- const value = await ctx.ui.input("Notify command (mid vol.)", config.PI_NOTIFY_SOUND_MID_CMD);
1016
- if (value?.trim()) {
1017
- config.PI_NOTIFY_SOUND_MID_CMD = value.trim();
1018
- ctx.ui.notify("Updated notify command (mid vol.)", "info");
1299
+ if (choice === "sound-command-mid") {
1300
+ const value = await ctx.ui.input("Sound command (mid vol.)", config.PI_NOTIFY_SOUND_MID_CMD);
1301
+ if (value !== undefined) {
1302
+ config.PI_NOTIFY_SOUND_MID_CMD = normalizePiNotifyCommand(
1303
+ value,
1304
+ DEFAULT_PI_NOTIFY_SOUND_MID_CMD,
1305
+ );
1306
+ ctx.ui.notify("Updated sound command (mid vol.)", "info");
1019
1307
  }
1020
1308
  continue;
1021
1309
  }
1022
- if (choice === "notify-command-high") {
1023
- const value = await ctx.ui.input("Notify command (high vol.)", config.PI_NOTIFY_SOUND_HIGH_CMD);
1024
- if (value?.trim()) {
1025
- config.PI_NOTIFY_SOUND_HIGH_CMD = value.trim();
1026
- ctx.ui.notify("Updated notify command (high vol.)", "info");
1310
+ if (choice === "sound-command-high") {
1311
+ const value = await ctx.ui.input("Sound command (high vol.)", config.PI_NOTIFY_SOUND_HIGH_CMD);
1312
+ if (value !== undefined) {
1313
+ config.PI_NOTIFY_SOUND_HIGH_CMD = normalizePiNotifyCommand(
1314
+ value,
1315
+ DEFAULT_PI_NOTIFY_SOUND_HIGH_CMD,
1316
+ );
1317
+ ctx.ui.notify("Updated sound command (high vol.)", "info");
1027
1318
  }
1028
1319
  continue;
1029
1320
  }
1030
- if (choice === "pushover-notifications") {
1031
- await configurePiNotifyPushoverMenu(ctx, config);
1321
+ if (choice === "notify-pushover-priority") {
1322
+ const nextPriority = await selectPiNotifyPushoverPriority(ctx, config["notify-pushover-priority"]);
1323
+ if (nextPriority !== undefined) {
1324
+ config["notify-pushover-priority"] = nextPriority;
1325
+ ctx.ui.notify(`Pushover priority set to ${formatPiNotifyPushoverPriority(nextPriority)}`, "info");
1326
+ }
1327
+ continue;
1328
+ }
1329
+ if (choice === "notify-pushover-title") {
1330
+ const value = await ctx.ui.input("Pushover title", config["notify-pushover-title"]);
1331
+ if (value !== undefined) {
1332
+ config["notify-pushover-title"] = normalizePiNotifyTemplateValue(
1333
+ value,
1334
+ DEFAULT_PI_NOTIFY_PUSHOVER_TITLE,
1335
+ );
1336
+ ctx.ui.notify("Updated Pushover title", "info");
1337
+ }
1338
+ continue;
1339
+ }
1340
+ if (choice === "notify-pushover-text") {
1341
+ const value = await ctx.ui.input("Pushover text", config["notify-pushover-text"]);
1342
+ if (value !== undefined) {
1343
+ config["notify-pushover-text"] = normalizePiNotifyTemplateValue(
1344
+ value,
1345
+ DEFAULT_PI_NOTIFY_PUSHOVER_TEXT,
1346
+ );
1347
+ ctx.ui.notify("Updated Pushover text", "info");
1348
+ }
1349
+ continue;
1350
+ }
1351
+ if (choice === "notify-pushover-user-key") {
1352
+ const value = await ctx.ui.input(
1353
+ "Pushover User Key/Delivery Group Key",
1354
+ config["notify-pushover-user-key"],
1355
+ );
1356
+ if (value !== undefined) {
1357
+ config["notify-pushover-user-key"] = normalizePiNotifyPushoverCredential(value);
1358
+ ctx.ui.notify("Updated Pushover user key", "info");
1359
+ }
1360
+ continue;
1361
+ }
1362
+ if (choice === "notify-pushover-api-token") {
1363
+ const value = await ctx.ui.input(
1364
+ "Pushover Token/API Token Key",
1365
+ config["notify-pushover-api-token"],
1366
+ );
1367
+ if (value !== undefined) {
1368
+ config["notify-pushover-api-token"] = normalizePiNotifyPushoverCredential(value);
1369
+ ctx.ui.notify("Updated Pushover API token", "info");
1370
+ }
1371
+ continue;
1372
+ }
1373
+ if (choice === "reset-defaults") {
1374
+ resetPiNotifyConfigToDefaults(config);
1375
+ ctx.ui.notify("Restored notification defaults", "info");
1376
+ continue;
1032
1377
  }
1033
1378
  }
1034
1379
  }
1035
1380
 
1036
1381
  /**
1037
- * @brief Registers the configurable successful-run sound shortcut when supported.
1382
+ * @brief Registers the configurable notification-sound shortcut when supported.
1038
1383
  * @details Loads the current project config, registers one raw pi shortcut when
1039
1384
  * the runtime exposes `registerShortcut(...)`, cycles persisted sound state on
1040
1385
  * invocation, saves the config, refreshes the status bar, and emits one info
@@ -1044,7 +1389,7 @@ async function configurePiNotifyMenu(
1044
1389
  * @param[in] pi {ExtensionAPI} Active extension API instance.
1045
1390
  * @param[in,out] statusController {PiUsereqStatusController} Mutable status controller.
1046
1391
  * @return {void} No return value.
1047
- * @satisfies REQ-131, REQ-134, REQ-136
1392
+ * @satisfies REQ-131, REQ-134, REQ-180
1048
1393
  */
1049
1394
  function registerPiNotifyShortcut(
1050
1395
  pi: ExtensionAPI,
@@ -1056,7 +1401,7 @@ function registerPiNotifyShortcut(
1056
1401
  }
1057
1402
  const config = loadProjectConfig(process.cwd());
1058
1403
  shortcutRegistrar.registerShortcut(config["notify-sound-toggle-shortcut"], {
1059
- description: "Cycle pi-usereq prompt-success sound level",
1404
+ description: "Cycle pi-usereq notification sound level",
1060
1405
  handler: async (ctx) => {
1061
1406
  const nextConfig = loadProjectConfig(ctx.cwd);
1062
1407
  nextConfig["notify-sound"] = cyclePiNotifySoundLevel(nextConfig["notify-sound"]);
@@ -1221,14 +1566,18 @@ function registerAgentTools(pi: ExtensionAPI): void {
1221
1566
  ],
1222
1567
  parameters: filesTokensSchema,
1223
1568
  async execute(_toolCallId, params) {
1224
- const payload = buildTokenToolPayload({
1225
- toolName: "files-tokens",
1226
- scope: "explicit-files",
1227
- baseDir: process.cwd(),
1228
- requestedPaths: params.files,
1229
- encodingName: TOKEN_COUNTER_ENCODING,
1230
- });
1231
- return buildTokenToolExecuteResult(payload);
1569
+ try {
1570
+ const payload = buildTokenToolPayload({
1571
+ toolName: "files-tokens",
1572
+ scope: "explicit-files",
1573
+ baseDir: process.cwd(),
1574
+ requestedPaths: params.files,
1575
+ encodingName: TOKEN_COUNTER_ENCODING,
1576
+ });
1577
+ return buildTokenToolExecuteResult(payload);
1578
+ } catch (error) {
1579
+ return buildFailedTokenToolExecuteResult(error);
1580
+ }
1232
1581
  },
1233
1582
  });
1234
1583
 
@@ -1453,20 +1802,24 @@ function registerAgentTools(pi: ExtensionAPI): void {
1453
1802
  ],
1454
1803
  parameters: tokensSchema,
1455
1804
  async execute() {
1456
- const projectBase = getProjectBase(process.cwd());
1457
- const config = loadProjectConfig(process.cwd());
1458
- const docsDir = config["docs-dir"].replace(/[/\\]+$/, "");
1459
- const canonicalDocNames = ["REQUIREMENTS.md", "WORKFLOW.md", "REFERENCES.md"];
1460
- const payload = buildTokenToolPayload({
1461
- toolName: "tokens",
1462
- scope: "canonical-docs",
1463
- baseDir: projectBase,
1464
- requestedPaths: canonicalDocNames.map((name) => path.join(docsDir, name)),
1465
- docsDir,
1466
- canonicalDocNames,
1467
- encodingName: TOKEN_COUNTER_ENCODING,
1468
- });
1469
- return buildTokenToolExecuteResult(payload);
1805
+ try {
1806
+ const projectBase = getProjectBase(process.cwd());
1807
+ const config = loadProjectConfig(process.cwd());
1808
+ const docsDir = config["docs-dir"].replace(/[/\\]+$/, "");
1809
+ const canonicalDocNames = ["REQUIREMENTS.md", "WORKFLOW.md", "REFERENCES.md"];
1810
+ const payload = buildTokenToolPayload({
1811
+ toolName: "tokens",
1812
+ scope: "canonical-docs",
1813
+ baseDir: projectBase,
1814
+ requestedPaths: canonicalDocNames.map((name) => path.join(docsDir, name)),
1815
+ docsDir,
1816
+ canonicalDocNames,
1817
+ encodingName: TOKEN_COUNTER_ENCODING,
1818
+ });
1819
+ return buildTokenToolExecuteResult(payload);
1820
+ } catch (error) {
1821
+ return buildFailedTokenToolExecuteResult(error);
1822
+ }
1470
1823
  },
1471
1824
  });
1472
1825
 
@@ -1728,26 +2081,20 @@ function registerAgentTools(pi: ExtensionAPI): void {
1728
2081
 
1729
2082
  /**
1730
2083
  * @brief Builds the shared settings-menu choices for startup-tool management.
1731
- * @details Serializes startup-tool actions into right-valued menu rows consumed by the shared settings-menu renderer. Runtime is O(t) in configurable-tool count. No external state is mutated.
2084
+ * @details Serializes startup-tool actions into right-valued menu rows consumed by the shared settings-menu renderer while omitting the removed status-reference action. Runtime is O(t) in configurable-tool count. No external state is mutated.
1732
2085
  * @param[in] pi {ExtensionAPI} Active extension API instance.
1733
2086
  * @param[in] config {UseReqConfig} Effective project configuration.
1734
2087
  * @return {PiUsereqSettingsMenuChoice[]} Ordered startup-tool menu choices.
1735
- * @satisfies REQ-007, REQ-151, REQ-152, REQ-153, REQ-154
2088
+ * @satisfies REQ-007, REQ-150, REQ-151, REQ-152, REQ-153, REQ-154, REQ-193
1736
2089
  */
1737
2090
  function buildPiUsereqToolsMenuChoices(pi: ExtensionAPI, config: UseReqConfig): PiUsereqSettingsMenuChoice[] {
1738
2091
  const tools = getPiUsereqStartupTools(pi);
1739
2092
  return [
1740
2093
  {
1741
- id: "show-tool-status",
1742
- label: "Show tool status",
1743
- value: `${getConfiguredEnabledPiUsereqTools(config).length}/${tools.length} enabled`,
1744
- description: "Open the full startup-tool reference report in the editor.",
1745
- },
1746
- {
1747
- id: "toggle-tool",
1748
- label: "Toggle tool",
2094
+ id: "enable-tools",
2095
+ label: "Enable tools",
1749
2096
  value: `${getConfiguredEnabledPiUsereqTools(config).length} enabled`,
1750
- description: "Open the per-tool toggle menu for configurable startup tools.",
2097
+ description: "Open the per-tool enablement menu for configurable startup tools.",
1751
2098
  },
1752
2099
  {
1753
2100
  id: "enable-all-tools",
@@ -1762,14 +2109,14 @@ function buildPiUsereqToolsMenuChoices(pi: ExtensionAPI, config: UseReqConfig):
1762
2109
  description: "Disable every configurable startup tool for future session starts.",
1763
2110
  },
1764
2111
  {
1765
- id: "reset-tool-defaults",
1766
- label: "Reset configurable-tool defaults",
2112
+ id: "reset-defaults",
2113
+ label: "Reset defaults",
1767
2114
  value: `${normalizeEnabledPiUsereqTools(undefined).length} defaults`,
1768
2115
  description: "Restore the documented default startup-tool selection.",
1769
2116
  },
1770
2117
  {
1771
- id: "back",
1772
- label: "Back",
2118
+ id: "save-and-close",
2119
+ label: "Save and close",
1773
2120
  value: "",
1774
2121
  description: "Return to the parent configuration menu.",
1775
2122
  },
@@ -1797,7 +2144,7 @@ function buildPiUsereqToolToggleChoices(pi: ExtensionAPI, config: UseReqConfig):
1797
2144
  id: "back",
1798
2145
  label: "Back",
1799
2146
  value: "",
1800
- description: "Return to the startup-tools menu.",
2147
+ description: "Return to the Enable tools menu.",
1801
2148
  },
1802
2149
  ];
1803
2150
  }
@@ -1809,23 +2156,22 @@ function buildPiUsereqToolToggleChoices(pi: ExtensionAPI, config: UseReqConfig):
1809
2156
  * @param[in] ctx {ExtensionCommandContext} Active command context.
1810
2157
  * @param[in,out] config {UseReqConfig} Mutable configuration object.
1811
2158
  * @return {Promise<void>} Promise resolved when the menu closes.
1812
- * @satisfies REQ-007, REQ-063, REQ-064, REQ-151, REQ-152, REQ-153, REQ-154
2159
+ * @satisfies REQ-007, REQ-063, REQ-064, REQ-150, REQ-151, REQ-152, REQ-153, REQ-154, REQ-193
1813
2160
  */
1814
2161
  async function configurePiUsereqToolsMenu(pi: ExtensionAPI, ctx: ExtensionCommandContext, config: UseReqConfig): Promise<void> {
1815
2162
  applyConfiguredPiUsereqTools(pi, config);
2163
+ let focusedChoiceId: string | undefined;
1816
2164
  while (true) {
1817
2165
  const tools = getPiUsereqStartupTools(pi);
1818
2166
  const enabledTools = new Set(getConfiguredEnabledPiUsereqTools(config));
1819
- const choice = await showPiUsereqSettingsMenu(ctx, "startup tools", buildPiUsereqToolsMenuChoices(pi, config));
2167
+ const choice = await showPiUsereqSettingsMenu(ctx, "Enable tools", buildPiUsereqToolsMenuChoices(pi, config), {
2168
+ initialSelectedId: focusedChoiceId,
2169
+ });
1820
2170
 
1821
- if (!choice || choice === "back") {
2171
+ if (!choice || choice === "save-and-close") {
1822
2172
  return;
1823
2173
  }
1824
-
1825
- if (choice === "show-tool-status") {
1826
- ctx.ui.setEditorText(renderPiUsereqToolsReference(pi, config));
1827
- continue;
1828
- }
2174
+ focusedChoiceId = choice;
1829
2175
 
1830
2176
  if (choice === "enable-all-tools") {
1831
2177
  setConfiguredPiUsereqTools(pi, config, tools.map((tool) => tool.name));
@@ -1839,14 +2185,14 @@ async function configurePiUsereqToolsMenu(pi: ExtensionAPI, ctx: ExtensionComman
1839
2185
  continue;
1840
2186
  }
1841
2187
 
1842
- if (choice === "reset-tool-defaults") {
2188
+ if (choice === "reset-defaults") {
1843
2189
  setConfiguredPiUsereqTools(pi, config, normalizeEnabledPiUsereqTools(undefined));
1844
2190
  ctx.ui.notify("Restored default configurable active tools", "info");
1845
2191
  continue;
1846
2192
  }
1847
2193
 
1848
- if (choice === "toggle-tool") {
1849
- const selectedToolName = await showPiUsereqSettingsMenu(ctx, "toggle startup tool", buildPiUsereqToolToggleChoices(pi, config));
2194
+ if (choice === "enable-tools") {
2195
+ const selectedToolName = await showPiUsereqSettingsMenu(ctx, "Enable tools", buildPiUsereqToolToggleChoices(pi, config));
1850
2196
  if (!selectedToolName || selectedToolName === "back") {
1851
2197
  continue;
1852
2198
  }
@@ -1864,20 +2210,6 @@ async function configurePiUsereqToolsMenu(pi: ExtensionAPI, ctx: ExtensionComman
1864
2210
  }
1865
2211
  }
1866
2212
 
1867
- /**
1868
- * @brief Formats one static-check configuration entry for UI display.
1869
- * @details Renders command-backed entries as `Command(cmd args...)` and all other modules as `Module(args...)`. Runtime is O(n) in parameter count. No side effects occur.
1870
- * @param[in] entry {StaticCheckEntry} Static-check configuration entry.
1871
- * @return {string} Human-readable entry summary.
1872
- */
1873
- function formatStaticCheckEntry(entry: StaticCheckEntry): string {
1874
- const params = Array.isArray(entry.params) && entry.params.length > 0 ? ` ${entry.params.join(" ")}` : "";
1875
- if (entry.module === "Command") {
1876
- return `${entry.module}(${entry.cmd ?? "?"}${params})`;
1877
- }
1878
- return `${entry.module}${params ? `(${entry.params!.join(" ")})` : ""}`;
1879
- }
1880
-
1881
2213
  /**
1882
2214
  * @brief Summarizes configured static-check languages.
1883
2215
  * @details Keeps only languages with at least one configured checker, sorts them, and emits a compact `Language (count)` list. Runtime is O(l log l). No side effects occur.
@@ -1892,80 +2224,38 @@ function formatStaticCheckLanguagesSummary(config: UseReqConfig): string {
1892
2224
  return languages.join(", ") || "(none)";
1893
2225
  }
1894
2226
 
1895
- /**
1896
- * @brief Renders the static-check configuration reference view.
1897
- * @details Produces a markdown-like summary containing configured entries, supported languages, the Command-only user module surface, and canonical example specifications. Runtime is O(l log l). No side effects occur.
1898
- * @param[in] config {UseReqConfig} Effective project configuration.
1899
- * @return {string} Reference text for the editor view.
1900
- */
1901
- function renderStaticCheckReference(config: UseReqConfig): string {
1902
- const lines = ["# Static-check configuration", "", `Configured languages: ${formatStaticCheckLanguagesSummary(config)}`, ""];
1903
- const configuredLanguages = Object.entries(config["static-check"])
1904
- .filter(([, entries]) => Array.isArray(entries) && entries.length > 0)
1905
- .sort(([left], [right]) => left.localeCompare(right));
1906
-
1907
- if (configuredLanguages.length === 0) {
1908
- lines.push("Configured entries: (none)");
1909
- } else {
1910
- lines.push("Configured entries:");
1911
- for (const [language, entries] of configuredLanguages) {
1912
- lines.push(`- ${language}: ${(entries as StaticCheckEntry[]).map(formatStaticCheckEntry).join(", ")}`);
1913
- }
1914
- }
1915
-
1916
- lines.push("", "Supported languages:");
1917
- for (const { language, extensions } of getSupportedStaticCheckLanguageSupport()) {
1918
- lines.push(`- ${language}: ${extensions.join(", ")}`);
1919
- }
1920
- lines.push(
1921
- "",
1922
- `Supported modules: ${STATIC_CHECK_MODULES.join(", ")}`,
1923
- "",
1924
- "Examples:",
1925
- "- Python=Command,mypy,--strict",
1926
- "- TypeScript=Command,eslint,--max-warnings,0",
1927
- );
1928
- return `${lines.join("\n")}\n`;
1929
- }
1930
-
1931
2227
  /**
1932
2228
  * @brief Builds the shared settings-menu choices for static-check management.
1933
- * @details Serializes Command-oriented static-check actions into right-valued menu rows consumed by the shared settings-menu renderer while omitting user-facing module selection. Runtime is O(1). No external state is mutated.
2229
+ * @details Serializes guided Command-oriented static-check actions into right-valued menu rows consumed by the shared settings-menu renderer while omitting raw-spec and reference-only actions. Runtime is O(1). No external state is mutated.
1934
2230
  * @param[in] config {UseReqConfig} Effective project configuration.
1935
2231
  * @return {PiUsereqSettingsMenuChoice[]} Ordered static-check menu choices.
1936
- * @satisfies REQ-008, REQ-160, REQ-161, REQ-151, REQ-152, REQ-153, REQ-154
2232
+ * @satisfies REQ-008, REQ-150, REQ-160, REQ-161, REQ-151, REQ-152, REQ-153, REQ-154, REQ-193
1937
2233
  */
1938
2234
  function buildStaticCheckMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuChoice[] {
1939
2235
  const supportedLanguageCount = getSupportedStaticCheckLanguageSupport().length;
1940
2236
  const configuredLanguageCount = Object.values(config["static-check"]).filter((entries) => entries.length > 0).length;
1941
2237
  return [
1942
2238
  {
1943
- id: "add-entry-supported-language",
1944
- label: "Add entry for supported language",
2239
+ id: "add-static-check-entry",
2240
+ label: "Add static code checker",
1945
2241
  value: `${supportedLanguageCount} languages`,
1946
- description: "Select a supported language, then configure the Command static-check executable.",
1947
- },
1948
- {
1949
- id: "add-entry-raw-spec",
1950
- label: "Add entry from LANG=MODULE[,CMD[,PARAM...]]",
1951
- value: "raw spec",
1952
- description: "Enter one raw Command-based static-check specification string in canonical CLI format.",
2242
+ description: "Select a supported language, then configure one Command static-check executable.",
1953
2243
  },
1954
2244
  {
1955
- id: "remove-language-entry",
1956
- label: "Remove language entry",
2245
+ id: "remove-static-check-entry",
2246
+ label: "Remove static code checker",
1957
2247
  value: configuredLanguageCount > 0 ? `${configuredLanguageCount} configured` : "(none)",
1958
2248
  description: "Remove every configured static-check entry for one language.",
1959
2249
  },
1960
2250
  {
1961
- id: "show-supported-languages",
1962
- label: "Show supported languages",
1963
- value: `${supportedLanguageCount} languages`,
1964
- description: "Open the static-check reference report in the editor.",
2251
+ id: "reset-defaults",
2252
+ label: "Reset defaults",
2253
+ value: configuredLanguageCount > 0 ? `${configuredLanguageCount} configured` : "(none)",
2254
+ description: "Remove every configured static-check entry and restore the default empty static-check configuration.",
1965
2255
  },
1966
2256
  {
1967
- id: "back",
1968
- label: "Back",
2257
+ id: "save-and-close",
2258
+ label: "Save and close",
1969
2259
  value: "",
1970
2260
  description: "Return to the parent configuration menu.",
1971
2261
  },
@@ -1994,7 +2284,7 @@ function buildSupportedStaticCheckLanguageChoices(config: UseReqConfig): PiUsere
1994
2284
  id: "back",
1995
2285
  label: "Back",
1996
2286
  value: "",
1997
- description: "Return to the static-check menu.",
2287
+ description: "Return to the Language static code checkers menu.",
1998
2288
  },
1999
2289
  ];
2000
2290
  }
@@ -2019,33 +2309,32 @@ function buildConfiguredStaticCheckLanguageChoices(config: UseReqConfig): PiUser
2019
2309
  id: "back",
2020
2310
  label: "Back",
2021
2311
  value: "",
2022
- description: "Return to the static-check menu.",
2312
+ description: "Return to the Language static code checkers menu.",
2023
2313
  },
2024
2314
  ];
2025
2315
  }
2026
2316
 
2027
2317
  /**
2028
2318
  * @brief Runs the interactive static-check configuration menu.
2029
- * @details Lets the user inspect support, add Command entries by guided prompts or raw spec strings, and remove configured language entries through the shared settings-menu renderer until the user exits. Runtime depends on user interaction count. Side effects include UI updates and config mutation.
2319
+ * @details Lets the user add Command entries by guided prompts, remove configured language entries, and reset the static-check configuration through the shared settings-menu renderer until the user exits. Runtime depends on user interaction count. Side effects include UI updates and config mutation.
2030
2320
  * @param[in] ctx {ExtensionCommandContext} Active command context.
2031
2321
  * @param[in,out] config {UseReqConfig} Mutable configuration object.
2032
2322
  * @return {Promise<void>} Promise resolved when the menu closes.
2033
- * @satisfies REQ-008, REQ-160, REQ-161, REQ-151, REQ-152, REQ-153, REQ-154
2323
+ * @satisfies REQ-008, REQ-150, REQ-160, REQ-161, REQ-151, REQ-152, REQ-153, REQ-154, REQ-193, REQ-195
2034
2324
  */
2035
2325
  async function configureStaticCheckMenu(ctx: ExtensionCommandContext, config: UseReqConfig): Promise<void> {
2326
+ let focusedChoiceId: string | undefined;
2036
2327
  while (true) {
2037
- const staticChoice = await showPiUsereqSettingsMenu(ctx, "static-check", buildStaticCheckMenuChoices(config));
2328
+ const staticChoice = await showPiUsereqSettingsMenu(ctx, "Language static code checkers", buildStaticCheckMenuChoices(config), {
2329
+ initialSelectedId: focusedChoiceId,
2330
+ });
2038
2331
 
2039
- if (!staticChoice || staticChoice === "back") {
2332
+ if (!staticChoice || staticChoice === "save-and-close") {
2040
2333
  return;
2041
2334
  }
2335
+ focusedChoiceId = staticChoice;
2042
2336
 
2043
- if (staticChoice === "show-supported-languages") {
2044
- ctx.ui.setEditorText(renderStaticCheckReference(config));
2045
- continue;
2046
- }
2047
-
2048
- if (staticChoice === "add-entry-supported-language") {
2337
+ if (staticChoice === "add-static-check-entry") {
2049
2338
  const selectedLanguage = await showPiUsereqSettingsMenu(ctx, "static-check language", buildSupportedStaticCheckLanguageChoices(config));
2050
2339
  if (!selectedLanguage || selectedLanguage === "back") {
2051
2340
  continue;
@@ -2073,29 +2362,18 @@ async function configureStaticCheckMenu(ctx: ExtensionCommandContext, config: Us
2073
2362
  continue;
2074
2363
  }
2075
2364
 
2076
- if (staticChoice === "add-entry-raw-spec") {
2077
- const spec = await ctx.ui.input("Static-check spec", "Python=Command,true");
2078
- if (!spec?.trim()) {
2079
- continue;
2080
- }
2081
- try {
2082
- const [canonicalLang, entry] = parseEnableStaticCheck(spec.trim());
2083
- config["static-check"][canonicalLang] ??= [];
2084
- config["static-check"][canonicalLang]!.push(entry);
2085
- ctx.ui.notify(`Added ${entry.module} checker for ${canonicalLang}`, "info");
2086
- } catch (error) {
2087
- ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
2088
- }
2089
- continue;
2090
- }
2091
-
2092
- if (staticChoice === "remove-language-entry") {
2093
- const configuredLanguage = await showPiUsereqSettingsMenu(ctx, "remove static-check language", buildConfiguredStaticCheckLanguageChoices(config));
2365
+ if (staticChoice === "remove-static-check-entry") {
2366
+ const configuredLanguage = await showPiUsereqSettingsMenu(ctx, "Remove static code checker", buildConfiguredStaticCheckLanguageChoices(config));
2094
2367
  if (!configuredLanguage || configuredLanguage === "back") {
2095
2368
  continue;
2096
2369
  }
2097
2370
  delete config["static-check"][configuredLanguage];
2098
2371
  ctx.ui.notify(`Removed static-check entries for ${configuredLanguage}`, "info");
2372
+ continue;
2373
+ }
2374
+ if (staticChoice === "reset-defaults") {
2375
+ config["static-check"] = {};
2376
+ ctx.ui.notify("Restored default static code checker configuration", "info");
2099
2377
  }
2100
2378
  }
2101
2379
  }
@@ -2106,7 +2384,7 @@ async function configureStaticCheckMenu(ctx: ExtensionCommandContext, config: Us
2106
2384
  * @param[in] cwd {string} Current working directory.
2107
2385
  * @param[in] config {UseReqConfig} Effective project configuration.
2108
2386
  * @return {PiUsereqSettingsMenuChoice[]} Ordered top-level menu choices.
2109
- * @satisfies REQ-006, REQ-031, REQ-137, REQ-150, REQ-151, REQ-152, REQ-162
2387
+ * @satisfies REQ-006, REQ-031, REQ-137, REQ-150, REQ-151, REQ-152, REQ-162, REQ-190, REQ-191, REQ-197
2110
2388
  */
2111
2389
  function buildPiUsereqMenuChoices(
2112
2390
  cwd: string,
@@ -2115,43 +2393,43 @@ function buildPiUsereqMenuChoices(
2115
2393
  return [
2116
2394
  {
2117
2395
  id: "docs-dir",
2118
- label: "docs-dir",
2396
+ label: "Document directory",
2119
2397
  value: config["docs-dir"],
2120
2398
  description: "Edit the repository-relative directory that stores REQUIREMENTS, WORKFLOW, and REFERENCES documents.",
2121
2399
  },
2122
- {
2123
- id: "tests-dir",
2124
- label: "tests-dir",
2125
- value: config["tests-dir"],
2126
- description: "Edit the repository-relative directory used for project test assets and static-check selection.",
2127
- },
2128
2400
  {
2129
2401
  id: "src-dir",
2130
- label: "src-dir",
2402
+ label: "Source-code directories",
2131
2403
  value: config["src-dir"].join(", "),
2132
2404
  description: "Manage the repository-relative source directories scanned by project-scope tools.",
2133
2405
  },
2406
+ {
2407
+ id: "tests-dir",
2408
+ label: "Unit tests directory",
2409
+ value: config["tests-dir"],
2410
+ description: "Edit the repository-relative directory used for project test assets and static-check selection.",
2411
+ },
2134
2412
  {
2135
2413
  id: "static-check",
2136
- label: "static-check",
2414
+ label: "Language static code checkers",
2137
2415
  value: formatStaticCheckLanguagesSummary(config),
2138
- description: "Manage configured static-check entries and inspect supported languages and modules.",
2416
+ description: "Manage guided Command static-check entries by language.",
2139
2417
  },
2140
2418
  {
2141
2419
  id: "startup-tools",
2142
- label: "startup tools",
2420
+ label: "Enable tools",
2143
2421
  value: `${getConfiguredEnabledPiUsereqTools(config).length} enabled`,
2144
2422
  description: "Manage which configurable tools become active during session_start.",
2145
2423
  },
2146
2424
  {
2147
2425
  id: "notifications",
2148
- label: "notifications",
2149
- value: `beep:${formatPiNotifyBeepStatus(config)} • sound:${config["notify-sound"]}`,
2150
- description: "Manage terminal beep flags, selected notify command, hotkey bind, per-level notify commands, and the nested Pushover submenu.",
2426
+ label: "Notifications",
2427
+ value: `notification:${formatPiNotifyStatus(config)} • sound:${config["notify-sound"]} • pushover:${formatPiNotifyPushoverStatus(config)}`,
2428
+ description: "Manage command-notify, sound, and Pushover settings with dedicated event submenus.",
2151
2429
  },
2152
2430
  {
2153
2431
  id: "show-config",
2154
- label: "show-config",
2432
+ label: "Show configuration",
2155
2433
  value: formatProjectConfigPathForMenu(cwd),
2156
2434
  valueTone: "dim",
2157
2435
  description: "Write the current project configuration JSON into the editor without saving additional changes.",
@@ -2176,25 +2454,31 @@ function buildPiUsereqMenuChoices(
2176
2454
  * @details Exposes add and remove actions for `src-dir` entries through right-valued menu rows consumed by the shared settings-menu renderer. Runtime is O(s) in source-directory count. No external state is mutated.
2177
2455
  * @param[in] config {UseReqConfig} Effective project configuration.
2178
2456
  * @return {PiUsereqSettingsMenuChoice[]} Ordered source-directory management choices.
2179
- * @satisfies REQ-006, REQ-151, REQ-152, REQ-153, REQ-154
2457
+ * @satisfies REQ-006, REQ-151, REQ-152, REQ-153, REQ-154, REQ-193
2180
2458
  */
2181
2459
  function buildSrcDirMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuChoice[] {
2182
2460
  return [
2183
2461
  {
2184
2462
  id: "add-src-dir-entry",
2185
- label: "Add src-dir entry",
2463
+ label: "Add source-code directory",
2186
2464
  value: `${config["src-dir"].length} configured`,
2187
2465
  description: "Append one repository-relative source directory to the current configuration.",
2188
2466
  },
2189
2467
  {
2190
2468
  id: "remove-src-dir-entry",
2191
- label: "Remove src-dir entry",
2469
+ label: "Remove source-code directory",
2192
2470
  value: config["src-dir"].join(", "),
2193
2471
  description: "Select one configured source directory to remove from the current configuration.",
2194
2472
  },
2195
2473
  {
2196
- id: "back",
2197
- label: "Back",
2474
+ id: "reset-defaults",
2475
+ label: "Reset defaults",
2476
+ value: `${DEFAULT_SRC_DIRS.join(", ")}`,
2477
+ description: "Restore the documented default source-directory configuration.",
2478
+ },
2479
+ {
2480
+ id: "save-and-close",
2481
+ label: "Save and close",
2198
2482
  value: "",
2199
2483
  description: "Return to the parent configuration menu.",
2200
2484
  },
@@ -2232,7 +2516,7 @@ function buildSrcDirRemovalChoices(config: UseReqConfig): PiUsereqSettingsMenuCh
2232
2516
  * @param[in] ctx {ExtensionCommandContext} Active command context.
2233
2517
  * @param[in,out] statusController {PiUsereqStatusController} Mutable status controller.
2234
2518
  * @return {Promise<void>} Promise resolved when configuration is saved and the menu closes.
2235
- * @satisfies REQ-006, REQ-031, REQ-137, REQ-150, REQ-151, REQ-152, REQ-153, REQ-154, REQ-162
2519
+ * @satisfies REQ-006, REQ-031, REQ-137, REQ-150, REQ-151, REQ-152, REQ-153, REQ-154, REQ-162, REQ-190, REQ-191, REQ-192, REQ-194, REQ-195
2236
2520
  */
2237
2521
  async function configurePiUsereq(
2238
2522
  pi: ExtensionAPI,
@@ -2248,11 +2532,13 @@ async function configurePiUsereq(
2248
2532
  renderPiUsereqStatus(statusController, ctx);
2249
2533
  };
2250
2534
 
2535
+ let focusedChoiceId: string | undefined;
2251
2536
  while (true) {
2252
2537
  const choice = await showPiUsereqSettingsMenu(
2253
2538
  ctx,
2254
2539
  "pi-usereq",
2255
2540
  buildPiUsereqMenuChoices(ctx.cwd, config),
2541
+ { initialSelectedId: focusedChoiceId },
2256
2542
  );
2257
2543
  if (!choice || choice === "save-and-close") {
2258
2544
  ensureSaved();
@@ -2262,37 +2548,47 @@ async function configurePiUsereq(
2262
2548
  }
2263
2549
  return;
2264
2550
  }
2551
+ focusedChoiceId = choice;
2265
2552
  if (choice === "docs-dir") {
2266
- const value = await ctx.ui.input("docs-dir", config["docs-dir"]);
2553
+ const value = await ctx.ui.input("Document directory", config["docs-dir"]);
2267
2554
  if (value?.trim()) config["docs-dir"] = value.trim();
2268
2555
  continue;
2269
2556
  }
2270
2557
  if (choice === "tests-dir") {
2271
- const value = await ctx.ui.input("tests-dir", config["tests-dir"]);
2558
+ const value = await ctx.ui.input("Unit tests directory", config["tests-dir"]);
2272
2559
  if (value?.trim()) config["tests-dir"] = value.trim();
2273
2560
  continue;
2274
2561
  }
2275
2562
  if (choice === "src-dir") {
2563
+ let srcFocusedChoiceId: string | undefined;
2276
2564
  while (true) {
2277
- const srcAction = await showPiUsereqSettingsMenu(ctx, "src-dir", buildSrcDirMenuChoices(config));
2278
- if (!srcAction || srcAction === "back") {
2565
+ const srcAction = await showPiUsereqSettingsMenu(ctx, "Source-code directories", buildSrcDirMenuChoices(config), {
2566
+ initialSelectedId: srcFocusedChoiceId,
2567
+ });
2568
+ if (!srcAction || srcAction === "save-and-close") {
2279
2569
  break;
2280
2570
  }
2571
+ srcFocusedChoiceId = srcAction;
2281
2572
  if (srcAction === "add-src-dir-entry") {
2282
- const value = await ctx.ui.input("New src-dir entry", "src");
2573
+ const value = await ctx.ui.input("New source-code directory", "src");
2283
2574
  if (value?.trim()) {
2284
2575
  config["src-dir"] = [...config["src-dir"], value.trim()];
2285
2576
  }
2286
2577
  continue;
2287
2578
  }
2288
2579
  if (srcAction === "remove-src-dir-entry") {
2289
- const toRemove = await showPiUsereqSettingsMenu(ctx, "remove src-dir entry", buildSrcDirRemovalChoices(config));
2580
+ const toRemove = await showPiUsereqSettingsMenu(ctx, "Remove source-code directory", buildSrcDirRemovalChoices(config));
2290
2581
  if (toRemove && toRemove !== "back") {
2291
2582
  config["src-dir"] = config["src-dir"].filter((entry) => entry !== toRemove);
2292
2583
  if (config["src-dir"].length === 0) {
2293
2584
  config["src-dir"] = ["src"];
2294
2585
  }
2295
2586
  }
2587
+ continue;
2588
+ }
2589
+ if (srcAction === "reset-defaults") {
2590
+ config["src-dir"] = [...DEFAULT_SRC_DIRS];
2591
+ ctx.ui.notify("Restored default source-code directories", "info");
2296
2592
  }
2297
2593
  }
2298
2594
  continue;
@@ -2312,6 +2608,7 @@ async function configurePiUsereq(
2312
2608
  if (choice === "reset-defaults") {
2313
2609
  config = getDefaultConfig(projectBase);
2314
2610
  applyConfiguredPiUsereqTools(pi, config);
2611
+ ctx.ui.notify("Restored all default configuration values", "info");
2315
2612
  continue;
2316
2613
  }
2317
2614
  if (choice === "show-config") {
@@ -2345,7 +2642,7 @@ function registerConfigCommands(
2345
2642
  * @brief Registers the complete pi-usereq extension.
2346
2643
  * @details Validates installation-owned bundled resources, registers prompt and
2347
2644
  * configuration commands plus agent tools, registers the configurable
2348
- * successful-run sound shortcut when the runtime supports shortcuts, and
2645
+ * notification-sound shortcut when the runtime supports shortcuts, and
2349
2646
  * installs shared wrappers for all supported pi lifecycle hooks so status
2350
2647
  * telemetry, context usage, prompt timing, cumulative runtime, prompt-specific
2351
2648
  * Pushover metadata, and pi-notify effects remain synchronized with runtime
@@ -2355,7 +2652,7 @@ function registerConfigCommands(
2355
2652
  * timer scheduling.
2356
2653
  * @param[in] pi {ExtensionAPI} Active extension API instance.
2357
2654
  * @return {void} No return value.
2358
- * @satisfies DES-002, REQ-004, REQ-005, REQ-009, REQ-044, REQ-045, REQ-067, REQ-068, REQ-109, REQ-111, REQ-112, REQ-113, REQ-114, REQ-115, REQ-116, REQ-117, REQ-118, REQ-119, REQ-120, REQ-121, REQ-122, REQ-123, REQ-124, REQ-125, REQ-126, REQ-129, REQ-130, REQ-131, REQ-132, REQ-133, REQ-134, REQ-135, REQ-136, REQ-137, REQ-148, REQ-159, REQ-163, REQ-164, REQ-165, REQ-166, REQ-167, REQ-168, REQ-169, REQ-170, REQ-171, REQ-172
2655
+ * @satisfies DES-002, REQ-004, REQ-005, REQ-009, REQ-044, REQ-045, REQ-067, REQ-068, REQ-109, REQ-111, REQ-112, REQ-113, REQ-114, REQ-115, REQ-116, REQ-117, REQ-118, REQ-119, REQ-120, REQ-121, REQ-122, REQ-123, REQ-124, REQ-125, REQ-126, REQ-127, REQ-128, REQ-131, REQ-132, REQ-133, REQ-134, REQ-137, REQ-148, REQ-159, REQ-163, REQ-164, REQ-165, REQ-166, REQ-167, REQ-168, REQ-169, REQ-172, REQ-174, REQ-179, REQ-180, REQ-184, REQ-188, REQ-190, REQ-191, REQ-192, REQ-193, REQ-194, REQ-195, REQ-196, REQ-197
2359
2656
  */
2360
2657
  export default function piUsereqExtension(pi: ExtensionAPI): void {
2361
2658
  const statusController = createPiUsereqStatusController();