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/CHANGELOG.md +38 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/pi-usereq/docs/REFERENCES.md +24470 -4561
- package/pi-usereq/docs/REQUIREMENTS.md +70 -50
- package/pi-usereq/docs/WORKFLOW.md +112 -77
- package/src/core/config.ts +84 -29
- package/src/core/extension-status.ts +49 -177
- package/src/core/pi-notify.ts +473 -313
- package/src/core/settings-menu.ts +21 -1
- package/src/core/token-counter.ts +55 -3
- package/src/index.ts +735 -438
- package/src/resources/images/favicon.svg +21 -0
- package/src/resources/images/pi.dev.png +0 -0
- package/tests/debug-extension-harness.test.ts +6 -6
- package/tests/extension-registration.test.ts +609 -129
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.
|
|
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}
|
|
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
|
-
|
|
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
|
|
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,
|
|
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-
|
|
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
|
-
|
|
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
|
|
636
|
-
* @details
|
|
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
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
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
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
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
|
-
|
|
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
|
|
673
|
-
* @details
|
|
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
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
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
|
|
684
|
-
* @details Negates the selected
|
|
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 {
|
|
725
|
+
* @param[in] key {PiNotifyBooleanConfigKey} Boolean configuration key to toggle.
|
|
687
726
|
* @return {boolean} Next enabled state.
|
|
688
727
|
*/
|
|
689
|
-
function
|
|
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
|
|
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-
|
|
771
|
+
* @satisfies REQ-172
|
|
700
772
|
*/
|
|
701
773
|
function formatPiNotifyPushoverPriority(priority: PiNotifyPushoverPriority): string {
|
|
702
|
-
return priority === 1 ? "
|
|
774
|
+
return priority === 1 ? "High" : "Normal";
|
|
703
775
|
}
|
|
704
776
|
|
|
705
777
|
/**
|
|
706
|
-
* @brief
|
|
707
|
-
* @details
|
|
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
|
-
* @
|
|
710
|
-
* @
|
|
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
|
|
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: "
|
|
716
|
-
label: "
|
|
717
|
-
value:
|
|
718
|
-
description: "
|
|
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-
|
|
722
|
-
label: "
|
|
1010
|
+
id: "notify-pushover-enabled",
|
|
1011
|
+
label: "Enable pushover",
|
|
723
1012
|
value: formatPiNotifyPushoverStatus(config),
|
|
724
|
-
description: "
|
|
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-
|
|
728
|
-
label: "
|
|
729
|
-
value: config["notify-pushover-
|
|
730
|
-
description: "
|
|
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-
|
|
734
|
-
label: "
|
|
735
|
-
value: config["notify-pushover-
|
|
736
|
-
description: "Edit the
|
|
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-
|
|
740
|
-
label: "
|
|
741
|
-
value:
|
|
742
|
-
description: "
|
|
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: "
|
|
746
|
-
label: "
|
|
747
|
-
value: "",
|
|
748
|
-
description: "
|
|
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
|
|
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-
|
|
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: "
|
|
1067
|
+
label: "Normal",
|
|
769
1068
|
value: currentPriority === 0 ? "selected" : "",
|
|
770
|
-
description: "Send
|
|
1069
|
+
description: "Send outbound Pushover messages with normal priority `0`.",
|
|
771
1070
|
},
|
|
772
1071
|
{
|
|
773
1072
|
id: "1",
|
|
774
|
-
label: "
|
|
1073
|
+
label: "High",
|
|
775
1074
|
value: currentPriority === 1 ? "selected" : "",
|
|
776
|
-
description: "Send
|
|
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
|
|
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-
|
|
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: "
|
|
855
|
-
label: "
|
|
856
|
-
value: config
|
|
857
|
-
description: "
|
|
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: "
|
|
867
|
-
label: "
|
|
868
|
-
value: config
|
|
869
|
-
description: "
|
|
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-
|
|
873
|
-
label: "
|
|
1110
|
+
id: "selected-sound-command",
|
|
1111
|
+
label: "Enable sound",
|
|
874
1112
|
value: config["notify-sound"],
|
|
875
|
-
description: "Select which
|
|
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
|
|
1123
|
+
description: "Edit the keyboard shortcut that cycles the selected sound command.",
|
|
882
1124
|
},
|
|
883
1125
|
{
|
|
884
|
-
id: "
|
|
885
|
-
label: "
|
|
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
|
|
1129
|
+
description: "Edit the shell command used when the selected sound command is `low`.",
|
|
888
1130
|
},
|
|
889
1131
|
{
|
|
890
|
-
id: "
|
|
891
|
-
label: "
|
|
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
|
|
1135
|
+
description: "Edit the shell command used when the selected sound command is `mid`.",
|
|
894
1136
|
},
|
|
895
1137
|
{
|
|
896
|
-
id: "
|
|
897
|
-
label: "
|
|
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
|
|
1141
|
+
description: "Edit the shell command used when the selected sound command is `high`.",
|
|
900
1142
|
},
|
|
1143
|
+
...buildPiNotifyPushoverRows(config),
|
|
901
1144
|
{
|
|
902
|
-
id: "
|
|
903
|
-
label: "
|
|
904
|
-
value:
|
|
905
|
-
description: "
|
|
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: "
|
|
909
|
-
label: "
|
|
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
|
|
918
|
-
* @details Reuses the pi-usereq settings-menu renderer so
|
|
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
|
|
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-
|
|
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, "
|
|
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
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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
|
|
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-
|
|
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(
|
|
972
|
-
|
|
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
|
-
|
|
976
|
-
|
|
977
|
-
|
|
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 ===
|
|
981
|
-
|
|
982
|
-
|
|
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 === "
|
|
986
|
-
const
|
|
987
|
-
|
|
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-
|
|
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(`
|
|
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(
|
|
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(
|
|
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 === "
|
|
1007
|
-
const value = await ctx.ui.input("
|
|
1008
|
-
if (value
|
|
1009
|
-
config.PI_NOTIFY_SOUND_LOW_CMD =
|
|
1010
|
-
|
|
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 === "
|
|
1015
|
-
const value = await ctx.ui.input("
|
|
1016
|
-
if (value
|
|
1017
|
-
config.PI_NOTIFY_SOUND_MID_CMD =
|
|
1018
|
-
|
|
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 === "
|
|
1023
|
-
const value = await ctx.ui.input("
|
|
1024
|
-
if (value
|
|
1025
|
-
config.PI_NOTIFY_SOUND_HIGH_CMD =
|
|
1026
|
-
|
|
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-
|
|
1031
|
-
await
|
|
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
|
|
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-
|
|
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
|
|
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
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
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
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
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: "
|
|
1742
|
-
label: "
|
|
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
|
|
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-
|
|
1766
|
-
label: "Reset
|
|
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: "
|
|
1772
|
-
label: "
|
|
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
|
|
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, "
|
|
2167
|
+
const choice = await showPiUsereqSettingsMenu(ctx, "Enable tools", buildPiUsereqToolsMenuChoices(pi, config), {
|
|
2168
|
+
initialSelectedId: focusedChoiceId,
|
|
2169
|
+
});
|
|
1820
2170
|
|
|
1821
|
-
if (!choice || choice === "
|
|
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-
|
|
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 === "
|
|
1849
|
-
const selectedToolName = await showPiUsereqSettingsMenu(ctx, "
|
|
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
|
|
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-
|
|
1944
|
-
label: "Add
|
|
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
|
|
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-
|
|
1956
|
-
label: "Remove
|
|
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: "
|
|
1962
|
-
label: "
|
|
1963
|
-
value: `${
|
|
1964
|
-
description: "
|
|
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: "
|
|
1968
|
-
label: "
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2328
|
+
const staticChoice = await showPiUsereqSettingsMenu(ctx, "Language static code checkers", buildStaticCheckMenuChoices(config), {
|
|
2329
|
+
initialSelectedId: focusedChoiceId,
|
|
2330
|
+
});
|
|
2038
2331
|
|
|
2039
|
-
if (!staticChoice || staticChoice === "
|
|
2332
|
+
if (!staticChoice || staticChoice === "save-and-close") {
|
|
2040
2333
|
return;
|
|
2041
2334
|
}
|
|
2335
|
+
focusedChoiceId = staticChoice;
|
|
2042
2336
|
|
|
2043
|
-
if (staticChoice === "
|
|
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 === "
|
|
2077
|
-
const
|
|
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: "
|
|
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: "
|
|
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
|
|
2414
|
+
label: "Language static code checkers",
|
|
2137
2415
|
value: formatStaticCheckLanguagesSummary(config),
|
|
2138
|
-
description: "Manage
|
|
2416
|
+
description: "Manage guided Command static-check entries by language.",
|
|
2139
2417
|
},
|
|
2140
2418
|
{
|
|
2141
2419
|
id: "startup-tools",
|
|
2142
|
-
label: "
|
|
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: "
|
|
2149
|
-
value: `
|
|
2150
|
-
description: "Manage
|
|
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: "
|
|
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
|
|
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
|
|
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: "
|
|
2197
|
-
label: "
|
|
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("
|
|
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
|
|
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, "
|
|
2278
|
-
|
|
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
|
|
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, "
|
|
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
|
-
*
|
|
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-
|
|
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();
|