pi-usereq 0.4.0 → 0.5.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.
Files changed (60) hide show
  1. package/.gitignore +3 -0
  2. package/CHANGELOG.md +31 -0
  3. package/README.md +1 -1
  4. package/package.json +1 -1
  5. package/{req → pi-usereq}/docs/REFERENCES.md +196 -159
  6. package/{req → pi-usereq}/docs/REQUIREMENTS.md +65 -44
  7. package/{req → pi-usereq}/docs/WORKFLOW.md +66 -73
  8. package/src/core/config.ts +39 -12
  9. package/src/core/extension-status.ts +94 -58
  10. package/src/core/pi-notify.ts +198 -11
  11. package/src/core/runtime-project-paths.ts +3 -32
  12. package/src/core/settings-menu.ts +9 -4
  13. package/src/core/static-check.ts +35 -171
  14. package/src/index.ts +281 -86
  15. package/tests/attended-results-scenarios.ts +3 -13
  16. package/tests/cli-command-option-parity.test.ts +21 -12
  17. package/tests/debug-extension-harness.test.ts +11 -7
  18. package/tests/extension-registration.test.ts +371 -71
  19. package/tests/oracle-project.test.ts +8 -3
  20. package/tests/oracle-standalone.test.ts +7 -13
  21. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_c.c.json +0 -5
  22. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_cpp.cpp.json +0 -5
  23. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_csharp.cs.json +0 -5
  24. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_elixir.ex.json +0 -5
  25. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_go.go.json +0 -5
  26. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_haskell.hs.json +0 -5
  27. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_java.java.json +0 -5
  28. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_javascript.js.json +0 -5
  29. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_kotlin.kt.json +0 -5
  30. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_lua.lua.json +0 -5
  31. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_perl.pl.json +0 -5
  32. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_php.php.json +0 -5
  33. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_python.py.json +0 -5
  34. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_ruby.rb.json +0 -5
  35. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_rust.rs.json +0 -5
  36. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_scala.scala.json +0 -5
  37. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_shell.sh.json +0 -5
  38. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_swift.swift.json +0 -5
  39. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_typescript.ts.json +0 -5
  40. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_zig.zig.json +0 -5
  41. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_c.c.json +0 -5
  42. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_cpp.cpp.json +0 -5
  43. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_csharp.cs.json +0 -5
  44. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_elixir.ex.json +0 -5
  45. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_go.go.json +0 -5
  46. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_haskell.hs.json +0 -5
  47. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_java.java.json +0 -5
  48. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_javascript.js.json +0 -5
  49. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_kotlin.kt.json +0 -5
  50. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_lua.lua.json +0 -5
  51. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_perl.pl.json +0 -5
  52. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_php.php.json +0 -5
  53. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_python.py.json +0 -5
  54. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_ruby.rb.json +0 -5
  55. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_rust.rs.json +0 -5
  56. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_scala.scala.json +0 -5
  57. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_shell.sh.json +0 -5
  58. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_swift.swift.json +0 -5
  59. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_typescript.ts.json +0 -5
  60. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_zig.zig.json +0 -5
@@ -19,6 +19,8 @@ import {
19
19
  DEFAULT_PI_NOTIFY_SOUND_MID_CMD,
20
20
  DEFAULT_PI_NOTIFY_SOUND_TOGGLE_SHORTCUT,
21
21
  normalizePiNotifyCommand,
22
+ normalizePiNotifyPushoverCredential,
23
+ normalizePiNotifyPushoverPriority,
22
24
  normalizePiNotifyShortcut,
23
25
  normalizePiNotifySoundLevel,
24
26
  } from "./pi-notify.js";
@@ -50,6 +52,11 @@ export interface UseReqConfig {
50
52
  "notify-beep-on-error": boolean;
51
53
  "notify-sound": "none" | "low" | "mid" | "high";
52
54
  "notify-sound-toggle-shortcut": string;
55
+ "notify-pushover-global-disable": boolean;
56
+ "notify-pushover-on-success": boolean;
57
+ "notify-pushover-user-key": string;
58
+ "notify-pushover-api-token": string;
59
+ "notify-pushover-priority": 0 | 1;
53
60
  PI_NOTIFY_SOUND_LOW_CMD: string;
54
61
  PI_NOTIFY_SOUND_MID_CMD: string;
55
62
  PI_NOTIFY_SOUND_HIGH_CMD: string;
@@ -83,10 +90,10 @@ export function getProjectConfigPath(projectBase: string): string {
83
90
 
84
91
  /**
85
92
  * @brief Builds the default project configuration.
86
- * @details Populates canonical docs/test/source directories, the default startup tool set, and default pi-notify fields while excluding runtime-derived path metadata. Time complexity is O(n) in default tool count. No filesystem side effects occur.
93
+ * @details Populates canonical docs/test/source directories, the default startup tool set, default pi-notify fields, and default Pushover settings while excluding runtime-derived path metadata. Time complexity is O(n) in default tool count. No filesystem side effects occur.
87
94
  * @param[in] projectBase {string} Absolute project root path.
88
95
  * @return {UseReqConfig} Fresh default configuration object.
89
- * @satisfies CTN-001, CTN-012, REQ-066, REQ-146
96
+ * @satisfies CTN-001, CTN-012, REQ-066, REQ-129, REQ-146, REQ-163
90
97
  */
91
98
  export function getDefaultConfig(_projectBase: string): UseReqConfig {
92
99
  return {
@@ -95,11 +102,16 @@ export function getDefaultConfig(_projectBase: string): UseReqConfig {
95
102
  "src-dir": [...DEFAULT_SRC_DIRS],
96
103
  "static-check": {},
97
104
  "enabled-tools": normalizeEnabledPiUsereqTools(undefined),
98
- "notify-beep-on-end": false,
99
- "notify-beep-on-esc": false,
100
- "notify-beep-on-error": false,
105
+ "notify-beep-on-end": true,
106
+ "notify-beep-on-esc": true,
107
+ "notify-beep-on-error": true,
101
108
  "notify-sound": "none",
102
109
  "notify-sound-toggle-shortcut": DEFAULT_PI_NOTIFY_SOUND_TOGGLE_SHORTCUT,
110
+ "notify-pushover-global-disable": false,
111
+ "notify-pushover-on-success": false,
112
+ "notify-pushover-user-key": "",
113
+ "notify-pushover-api-token": "",
114
+ "notify-pushover-priority": 0,
103
115
  PI_NOTIFY_SOUND_LOW_CMD: DEFAULT_PI_NOTIFY_SOUND_LOW_CMD,
104
116
  PI_NOTIFY_SOUND_MID_CMD: DEFAULT_PI_NOTIFY_SOUND_MID_CMD,
105
117
  PI_NOTIFY_SOUND_HIGH_CMD: DEFAULT_PI_NOTIFY_SOUND_HIGH_CMD,
@@ -108,11 +120,11 @@ export function getDefaultConfig(_projectBase: string): UseReqConfig {
108
120
 
109
121
  /**
110
122
  * @brief Loads and sanitizes the persisted project configuration.
111
- * @details Returns defaults when the config file does not exist. Otherwise parses JSON, validates directory and static-check field shapes, normalizes enabled tool names and pi-notify fields, and ignores removed or runtime-derived path metadata. Runtime is O(n) in config size. Side effects are limited to filesystem reads.
123
+ * @details Returns defaults when the config file does not exist. Otherwise parses JSON, validates directory and static-check field shapes, normalizes enabled tool names plus pi-notify and Pushover fields, applies enabled beep defaults for missing flag payloads, and ignores removed or runtime-derived path metadata. Runtime is O(n) in config size. Side effects are limited to filesystem reads.
112
124
  * @param[in] projectBase {string} Absolute project root path.
113
125
  * @return {UseReqConfig} Sanitized effective configuration.
114
126
  * @throws {ReqError} Throws with exit code `11` when the config file contains invalid JSON or a non-object payload.
115
- * @satisfies CTN-012, REQ-066, REQ-146
127
+ * @satisfies CTN-012, REQ-066, REQ-129, REQ-146, REQ-163
116
128
  */
117
129
  export function loadConfig(projectBase: string): UseReqConfig {
118
130
  const configPath = getProjectConfigPath(projectBase);
@@ -140,11 +152,16 @@ export function loadConfig(projectBase: string): UseReqConfig {
140
152
  ? (data["static-check"] as Record<string, StaticCheckEntry[]>)
141
153
  : {};
142
154
  const enabledTools = normalizeEnabledPiUsereqTools(data["enabled-tools"]);
143
- const notifyBeepOnEnd = data["notify-beep-on-end"] === true;
144
- const notifyBeepOnEsc = data["notify-beep-on-esc"] === true;
145
- const notifyBeepOnError = data["notify-beep-on-error"] === true;
155
+ const notifyBeepOnEnd = data["notify-beep-on-end"] !== false;
156
+ const notifyBeepOnEsc = data["notify-beep-on-esc"] !== false;
157
+ const notifyBeepOnError = data["notify-beep-on-error"] !== false;
146
158
  const notifySound = normalizePiNotifySoundLevel(data["notify-sound"]);
147
159
  const notifySoundToggleShortcut = normalizePiNotifyShortcut(data["notify-sound-toggle-shortcut"]);
160
+ const pushoverGlobalDisable = data["notify-pushover-global-disable"] === true;
161
+ const pushoverOnSuccess = data["notify-pushover-on-success"] === true;
162
+ const pushoverUserKey = normalizePiNotifyPushoverCredential(data["notify-pushover-user-key"]);
163
+ const pushoverApiToken = normalizePiNotifyPushoverCredential(data["notify-pushover-api-token"]);
164
+ const pushoverPriority = normalizePiNotifyPushoverPriority(data["notify-pushover-priority"]);
148
165
  const lowSoundCommand = normalizePiNotifyCommand(data.PI_NOTIFY_SOUND_LOW_CMD, DEFAULT_PI_NOTIFY_SOUND_LOW_CMD);
149
166
  const midSoundCommand = normalizePiNotifyCommand(data.PI_NOTIFY_SOUND_MID_CMD, DEFAULT_PI_NOTIFY_SOUND_MID_CMD);
150
167
  const highSoundCommand = normalizePiNotifyCommand(data.PI_NOTIFY_SOUND_HIGH_CMD, DEFAULT_PI_NOTIFY_SOUND_HIGH_CMD);
@@ -160,6 +177,11 @@ export function loadConfig(projectBase: string): UseReqConfig {
160
177
  "notify-beep-on-error": notifyBeepOnError,
161
178
  "notify-sound": notifySound,
162
179
  "notify-sound-toggle-shortcut": notifySoundToggleShortcut,
180
+ "notify-pushover-global-disable": pushoverGlobalDisable,
181
+ "notify-pushover-on-success": pushoverOnSuccess,
182
+ "notify-pushover-user-key": pushoverUserKey,
183
+ "notify-pushover-api-token": pushoverApiToken,
184
+ "notify-pushover-priority": pushoverPriority,
163
185
  PI_NOTIFY_SOUND_LOW_CMD: lowSoundCommand,
164
186
  PI_NOTIFY_SOUND_MID_CMD: midSoundCommand,
165
187
  PI_NOTIFY_SOUND_HIGH_CMD: highSoundCommand,
@@ -168,10 +190,10 @@ export function loadConfig(projectBase: string): UseReqConfig {
168
190
 
169
191
  /**
170
192
  * @brief Builds the persisted configuration payload that excludes runtime-derived fields.
171
- * @details Copies only the canonical persisted configuration keys into a fresh object so runtime-derived metadata such as `base-path` and `git-path` can never be written to disk. Runtime is O(n) in config size. No external state is mutated.
193
+ * @details Copies only the canonical persisted configuration keys into a fresh object so runtime-derived metadata such as `base-path` and `git-path` can never be written to disk while preserving notification and Pushover settings verbatim. Runtime is O(n) in config size. No external state is mutated.
172
194
  * @param[in] config {UseReqConfig} Effective configuration object.
173
195
  * @return {UseReqConfig} Persistable configuration payload.
174
- * @satisfies CTN-012, REQ-146
196
+ * @satisfies CTN-012, REQ-146, REQ-163
175
197
  */
176
198
  function buildPersistedConfig(config: UseReqConfig): UseReqConfig {
177
199
  return {
@@ -194,6 +216,11 @@ function buildPersistedConfig(config: UseReqConfig): UseReqConfig {
194
216
  "notify-beep-on-error": config["notify-beep-on-error"],
195
217
  "notify-sound": config["notify-sound"],
196
218
  "notify-sound-toggle-shortcut": config["notify-sound-toggle-shortcut"],
219
+ "notify-pushover-global-disable": config["notify-pushover-global-disable"],
220
+ "notify-pushover-on-success": config["notify-pushover-on-success"],
221
+ "notify-pushover-user-key": config["notify-pushover-user-key"],
222
+ "notify-pushover-api-token": config["notify-pushover-api-token"],
223
+ "notify-pushover-priority": config["notify-pushover-priority"],
197
224
  PI_NOTIFY_SOUND_LOW_CMD: config.PI_NOTIFY_SOUND_LOW_CMD,
198
225
  PI_NOTIFY_SOUND_MID_CMD: config.PI_NOTIFY_SOUND_MID_CMD,
199
226
  PI_NOTIFY_SOUND_HIGH_CMD: config.PI_NOTIFY_SOUND_HIGH_CMD,
@@ -8,6 +8,7 @@
8
8
  * scheduling through exported controller helpers.
9
9
  */
10
10
 
11
+ import path from "node:path";
11
12
  import type {
12
13
  AgentEndEvent,
13
14
  ContextUsage,
@@ -15,8 +16,8 @@ import type {
15
16
  ThemeColor,
16
17
  } from "@mariozechner/pi-coding-agent";
17
18
  import type { UseReqConfig } from "./config.js";
18
- import { formatPiNotifyBeepStatus } from "./pi-notify.js";
19
- import { formatAbsoluteGitPath, formatBasePathRelativeToGitPath, resolveRuntimeGitPath } from "./runtime-project-paths.js";
19
+ import { normalizePathSlashes } from "./path-context.js";
20
+ import { formatPiNotifyBeepStatus, formatPiNotifyPushoverStatus } from "./pi-notify.js";
20
21
 
21
22
  /**
22
23
  * @brief Enumerates the CLI-supported theme tokens consumed by status rendering.
@@ -66,7 +67,7 @@ interface StatusThemeAdapter {
66
67
  interface ContextUsageOverlaySpec {
67
68
  backgroundColor: StatusForegroundColor;
68
69
  foregroundColor: StatusForegroundColor;
69
- text: "CLEAR" | "FULL!";
70
+ text: "◀ CLEAR ▶ " | " ◀ FULL ▶ ";
70
71
  }
71
72
 
72
73
  /**
@@ -112,28 +113,36 @@ export const PI_USEREQ_STATUS_HOOK_NAMES = [
112
113
  */
113
114
  export type PiUsereqStatusHookName = (typeof PI_USEREQ_STATUS_HOOK_NAMES)[number];
114
115
 
116
+ /**
117
+ * @brief Describes one prompt request tracked across extension command delivery and runtime execution.
118
+ * @details Stores the bundled prompt command name plus the raw argument string substituted into `%%ARGS%%` so later successful-run side effects can reconstruct prompt-specific completion notifications. The interface is compile-time only and introduces no runtime cost.
119
+ */
120
+ export interface PiUsereqPromptRequest {
121
+ promptName: string;
122
+ promptArgs: string;
123
+ }
124
+
115
125
  /**
116
126
  * @brief Stores the mutable runtime facts displayed by the status bar.
117
- * @details Persists the latest context-usage snapshot, the active run start
118
- * timestamp, and the most recent normally completed run duration. Runtime state
119
- * is mutated in-place by controller helpers. Compile-time only and introduces
120
- * no runtime cost.
127
+ * @details Persists the latest context-usage snapshot, the active run start timestamp, the most recent normally completed run duration, the accumulated duration of all normally completed runs, and prompt-request metadata carried from command dispatch into the next runtime execution. Runtime state is mutated in-place by controller helpers. Compile-time only and introduces no runtime cost.
121
128
  */
122
129
  export interface PiUsereqStatusState {
123
130
  contextUsage: ContextUsage | undefined;
124
131
  runStartTimeMs: number | undefined;
125
132
  lastRunDurationMs: number | undefined;
133
+ totalRunDurationMs: number | undefined;
134
+ pendingPromptRequest: PiUsereqPromptRequest | undefined;
135
+ activePromptRequest: PiUsereqPromptRequest | undefined;
126
136
  }
127
137
 
128
138
  /**
129
139
  * @brief Stores the controller state required for event-driven status updates.
130
140
  * @details Keeps the mutable status snapshot, the current configuration, the
131
- * latest extension context used for rendering, the active-tools provider, and
132
- * the interval handle used for live elapsed-time refreshes. Compile-time only
133
- * and introduces no runtime cost.
141
+ * latest extension context used for rendering, and the interval handle used
142
+ * for live elapsed-time refreshes. Compile-time only and introduces no runtime
143
+ * cost.
134
144
  */
135
145
  export interface PiUsereqStatusController {
136
- readonly getActiveTools: () => readonly string[];
137
146
  config: UseReqConfig | undefined;
138
147
  latestContext: ExtensionContext | undefined;
139
148
  state: PiUsereqStatusState;
@@ -264,12 +273,12 @@ function refreshContextUsage(
264
273
  }
265
274
 
266
275
  /**
267
- * @brief Counts the filled cells rendered by the 5-cell context bar.
276
+ * @brief Counts the filled cells rendered by the 10-cell context bar.
268
277
  * @details Uses ceiling semantics for positive percentages so any non-zero
269
278
  * usage occupies at least one cell and zero usage occupies none. Runtime is
270
279
  * O(1). No external state is mutated.
271
280
  * @param[in] contextUsage {ContextUsage | undefined} Normalized context snapshot.
272
- * @return {number} Filled-cell count in the inclusive range `[0, 5]`.
281
+ * @return {number} Filled-cell count in the inclusive range `[0, 10]`.
273
282
  * @satisfies REQ-122
274
283
  */
275
284
  function countFilledContextCells(
@@ -279,13 +288,13 @@ function countFilledContextCells(
279
288
  if (percent === null || percent === undefined || percent <= 0) {
280
289
  return 0;
281
290
  }
282
- return Math.min(5, Math.ceil((percent * 5) / 100));
291
+ return Math.min(10, Math.ceil((percent * 10) / 100));
283
292
  }
284
293
 
285
294
  /**
286
295
  * @brief Resolves the threshold-specific context-bar overlay when required.
287
- * @details Returns the empty-state `CLEAR` overlay when normalized context
288
- * usage is unavailable or non-positive and returns the high-water `FULL!`
296
+ * @details Returns the empty-state `◀ CLEAR ▶ ` overlay when normalized context
297
+ * usage is unavailable or non-positive and returns the centered ` ◀ FULL ▶ `
289
298
  * overlay with the active theme `error` token when usage exceeds 90 percent.
290
299
  * Runtime is O(1). No external state is mutated.
291
300
  * @param[in] contextUsage {ContextUsage | undefined} Normalized context snapshot.
@@ -300,14 +309,14 @@ function resolveContextUsageOverlay(
300
309
  return {
301
310
  backgroundColor: "accent",
302
311
  foregroundColor: "warning",
303
- text: "CLEAR",
312
+ text: "◀ CLEAR ▶ ",
304
313
  };
305
314
  }
306
315
  if (percent > 90) {
307
316
  return {
308
317
  backgroundColor: "warning",
309
318
  foregroundColor: "error",
310
- text: "FULL!",
319
+ text: " ◀ FULL ▶ ",
311
320
  };
312
321
  }
313
322
  return undefined;
@@ -335,7 +344,7 @@ function formatContextUsageOverlay(
335
344
  }
336
345
 
337
346
  /**
338
- * @brief Formats one 5-cell context-usage bar.
347
+ * @brief Formats one 10-cell context-usage bar.
339
348
  * @details Renders threshold-specific overlays for empty and high-water states;
340
349
  * otherwise renders filled cells with the theme `warning` token on an
341
350
  * accent-derived background and unfilled cells in `dim` on the same background
@@ -343,7 +352,7 @@ function formatContextUsageOverlay(
343
352
  * mutated.
344
353
  * @param[in] theme {StatusThemeAdapter} Normalized status theme.
345
354
  * @param[in] contextUsage {ContextUsage | undefined} Normalized context snapshot.
346
- * @return {string} Rendered 5-cell bar or overlay.
355
+ * @return {string} Rendered 10-cell bar or overlay.
347
356
  * @satisfies REQ-121, REQ-122, REQ-126, REQ-127, REQ-128
348
357
  */
349
358
  function formatContextUsageBar(
@@ -355,7 +364,7 @@ function formatContextUsageBar(
355
364
  return formatContextUsageOverlay(theme, overlay);
356
365
  }
357
366
  const filledCells = countFilledContextCells(contextUsage);
358
- return Array.from({ length: 5 }, (_value, index) =>
367
+ return Array.from({ length: 10 }, (_value, index) =>
359
368
  index < filledCells ? theme.filledContextCell : theme.emptyContextCell,
360
369
  ).join("");
361
370
  }
@@ -376,6 +385,44 @@ function formatStatusDuration(durationMs: number): string {
376
385
  return `${minutes}:${String(seconds).padStart(2, "0")}`;
377
386
  }
378
387
 
388
+ /**
389
+ * @brief Formats one optional completed-duration value.
390
+ * @details Returns the canonical unset placeholder `--:--` until the supplied
391
+ * timer receives a normally completed prompt duration, then delegates to
392
+ * `formatStatusDuration(...)`. Runtime is O(1). No external state is mutated.
393
+ * @param[in] durationMs {number | undefined} Optional completed-duration value.
394
+ * @return {string} Rendered duration or unset placeholder.
395
+ * @satisfies REQ-124
396
+ */
397
+ function formatCompletedStatusDuration(
398
+ durationMs: number | undefined,
399
+ ): string {
400
+ return durationMs === undefined ? "--:--" : formatStatusDuration(durationMs);
401
+ }
402
+
403
+ /**
404
+ * @brief Formats the consolidated `elapsed` status-bar value.
405
+ * @details Emits the active prompt segment `⏱︎ <active>`, the latest normally
406
+ * completed segment `⚑ <last>`, and the accumulated successful-runtime segment
407
+ * `⌛︎ <total>` with fixed spacing. Runtime is O(1). No external state is
408
+ * mutated.
409
+ * @param[in] state {PiUsereqStatusState} Mutable status state snapshot.
410
+ * @param[in] nowMs {number} Current wall-clock time in milliseconds.
411
+ * @return {string} Consolidated `elapsed` field value.
412
+ * @satisfies REQ-123, REQ-124, REQ-125, REQ-159
413
+ */
414
+ function formatElapsedStatusValue(
415
+ state: PiUsereqStatusState,
416
+ nowMs: number,
417
+ ): string {
418
+ const activeText = state.runStartTimeMs === undefined
419
+ ? "--:--"
420
+ : formatStatusDuration(nowMs - state.runStartTimeMs);
421
+ const lastText = formatCompletedStatusDuration(state.lastRunDurationMs);
422
+ const totalText = formatCompletedStatusDuration(state.totalRunDurationMs);
423
+ return ` ⏱︎ ${activeText} ⚑ ${lastText} ⌛︎ ${totalText}`;
424
+ }
425
+
379
426
  /**
380
427
  * @brief Formats one standard status-bar field.
381
428
  * @details Renders the field label in accent color and the value in warning
@@ -431,52 +478,38 @@ function didAgentEndAbort(messages: AgentEndEvent["messages"]): boolean {
431
478
 
432
479
  /**
433
480
  * @brief Builds the full single-line pi-usereq status-bar payload.
434
- * @details Renders git, base, docs, tests, src, tools, context, elapsed, last, beep, and sound fields in the canonical order with dim bullet separators and threshold-specific context-bar overlays. Runtime is O(s) in configured source-path count plus runtime git probing. No external state is mutated.
435
- * @param[in] cwd {string} Runtime working directory used for git/base path derivation.
481
+ * @details Renders base, context, elapsed, beep, sound, and pushover fields in the canonical order with dim bullet separators and threshold-specific context-bar overlays. Runtime is O(1). No external state is mutated.
482
+ * @param[in] cwd {string} Runtime working directory used for base-path derivation.
436
483
  * @param[in] config {UseReqConfig} Effective project configuration.
437
- * @param[in] activeTools {readonly string[]} Active runtime tool names.
438
484
  * @param[in] theme {StatusThemeAdapter} Normalized status theme.
439
485
  * @param[in] state {PiUsereqStatusState} Mutable status state snapshot.
440
486
  * @param[in] nowMs {number} Current wall-clock time in milliseconds.
441
487
  * @return {string} Single-line status-bar text.
442
- * @satisfies REQ-109, REQ-112, REQ-120, REQ-121, REQ-123, REQ-124, REQ-125, REQ-126, REQ-127, REQ-128, REQ-135, REQ-136, REQ-147, REQ-148, REQ-156
488
+ * @satisfies REQ-109, REQ-112, REQ-120, REQ-121, REQ-123, REQ-124, REQ-125, REQ-126, REQ-127, REQ-128, REQ-135, REQ-136, REQ-148, REQ-156, REQ-159, REQ-170, REQ-171
443
489
  */
444
490
  function buildPiUsereqStatusText(
445
491
  cwd: string,
446
492
  config: UseReqConfig,
447
- activeTools: readonly string[],
448
493
  theme: StatusThemeAdapter,
449
494
  state: PiUsereqStatusState,
450
495
  nowMs: number,
451
496
  ): string {
452
- const gitPath = resolveRuntimeGitPath(cwd);
453
- const gitText = formatAbsoluteGitPath(gitPath);
454
- const baseText = formatBasePathRelativeToGitPath(cwd, gitPath);
455
- const sourcePaths = config["src-dir"].join(",");
456
- const elapsedText = state.runStartTimeMs === undefined
457
- ? "idle"
458
- : formatStatusDuration(nowMs - state.runStartTimeMs);
459
- const lastText = state.lastRunDurationMs === undefined
460
- ? "N/A"
461
- : formatStatusDuration(state.lastRunDurationMs);
497
+ const baseText = normalizePathSlashes(path.resolve(cwd));
498
+ const elapsedText = formatElapsedStatusValue(state, nowMs);
462
499
  const beepText = formatPiNotifyBeepStatus(config);
463
500
  const soundText = config["notify-sound"];
501
+ const pushoverText = formatPiNotifyPushoverStatus(config);
464
502
  return [
465
- formatStatusField(theme, "git", gitText),
466
503
  formatStatusField(theme, "base", baseText),
467
- formatStatusField(theme, "docs", config["docs-dir"]),
468
- formatStatusField(theme, "tests", config["tests-dir"]),
469
- formatStatusField(theme, "src", sourcePaths),
470
- formatStatusField(theme, "tools", String(activeTools.length)),
471
504
  formatRenderedStatusField(
472
505
  theme,
473
506
  "context",
474
507
  formatContextUsageBar(theme, state.contextUsage),
475
508
  ),
476
509
  formatStatusField(theme, "elapsed", elapsedText),
477
- formatStatusField(theme, "last", lastText),
478
510
  formatStatusField(theme, "beep", beepText),
479
511
  formatStatusField(theme, "sound", soundText),
512
+ formatStatusField(theme, "pushover", pushoverText),
480
513
  ].join(theme.separator);
481
514
  }
482
515
 
@@ -525,24 +558,21 @@ function syncPiUsereqStatusTicker(
525
558
 
526
559
  /**
527
560
  * @brief Creates an empty pi-usereq status controller.
528
- * @details Initializes the mutable status snapshot, stores the active-tools
529
- * provider used by render-time tool counting, and starts with no config, no
530
- * context, and no live ticker. Runtime is O(1). No external state is mutated.
531
- * @param[in] getActiveTools {() => readonly string[]} Provider for active tools.
561
+ * @details Initializes the mutable status snapshot, including empty prompt-request tracking, and starts with no config, no context, and no live ticker. Runtime is O(1). No external state is mutated.
532
562
  * @return {PiUsereqStatusController} New status controller.
533
563
  * @satisfies DES-010
534
564
  */
535
- export function createPiUsereqStatusController(
536
- getActiveTools: () => readonly string[],
537
- ): PiUsereqStatusController {
565
+ export function createPiUsereqStatusController(): PiUsereqStatusController {
538
566
  return {
539
- getActiveTools,
540
567
  config: undefined,
541
568
  latestContext: undefined,
542
569
  state: {
543
570
  contextUsage: undefined,
544
571
  runStartTimeMs: undefined,
545
572
  lastRunDurationMs: undefined,
573
+ totalRunDurationMs: undefined,
574
+ pendingPromptRequest: undefined,
575
+ activePromptRequest: undefined,
546
576
  },
547
577
  tickHandle: undefined,
548
578
  };
@@ -574,7 +604,7 @@ export function setPiUsereqStatusConfig(
574
604
  * @param[in,out] controller {PiUsereqStatusController} Mutable status controller.
575
605
  * @param[in] ctx {ExtensionContext} Active extension context.
576
606
  * @return {void} No return value.
577
- * @satisfies REQ-120, REQ-121, REQ-123, REQ-124, REQ-125, REQ-126, REQ-127, REQ-128, REQ-135, REQ-136
607
+ * @satisfies REQ-120, REQ-121, REQ-123, REQ-124, REQ-125, REQ-126, REQ-127, REQ-128, REQ-135, REQ-136, REQ-148, REQ-159, REQ-170, REQ-171
578
608
  */
579
609
  export function renderPiUsereqStatus(
580
610
  controller: PiUsereqStatusController,
@@ -590,7 +620,6 @@ export function renderPiUsereqStatus(
590
620
  buildPiUsereqStatusText(
591
621
  ctx.cwd,
592
622
  controller.config,
593
- controller.getActiveTools(),
594
623
  theme,
595
624
  controller.state,
596
625
  Date.now(),
@@ -601,17 +630,17 @@ export function renderPiUsereqStatus(
601
630
  /**
602
631
  * @brief Updates mutable status state for one intercepted lifecycle hook.
603
632
  * @details Refreshes stored context usage on every hook, starts run timing on
604
- * `agent_start`, captures non-aborted run duration on `agent_end`, clears live
605
- * timing on shutdown, synchronizes the live ticker, and re-renders the status
606
- * bar when configuration is available. Runtime is O(n) in `agent_end` message
607
- * count and otherwise O(1). Side effects include in-memory state mutation,
608
- * interval scheduling, and footer-status updates.
633
+ * `agent_start`, promotes pending prompt-request metadata into the active run, captures non-aborted run duration on `agent_end`, accumulates successful runtime into `Σ`, clears live timing on shutdown, synchronizes
634
+ * the live ticker, and re-renders the status bar when configuration is
635
+ * available. Runtime is O(n) in `agent_end` message count and otherwise O(1).
636
+ * Side effects include in-memory state mutation, interval scheduling, and
637
+ * footer-status updates.
609
638
  * @param[in,out] controller {PiUsereqStatusController} Mutable status controller.
610
639
  * @param[in] hookName {PiUsereqStatusHookName} Intercepted hook name.
611
640
  * @param[in] event {unknown} Hook payload forwarded from the wrapper.
612
641
  * @param[in] ctx {ExtensionContext} Active extension context.
613
642
  * @return {void} No return value.
614
- * @satisfies REQ-117, REQ-118, REQ-119, REQ-123, REQ-124, REQ-125
643
+ * @satisfies REQ-117, REQ-118, REQ-119, REQ-123, REQ-124, REQ-125, REQ-159, REQ-169
615
644
  */
616
645
  export function updateExtensionStatus(
617
646
  controller: PiUsereqStatusController,
@@ -625,18 +654,25 @@ export function updateExtensionStatus(
625
654
 
626
655
  if (hookName === "agent_start") {
627
656
  controller.state.runStartTimeMs = nowMs;
657
+ controller.state.activePromptRequest = controller.state.pendingPromptRequest;
658
+ controller.state.pendingPromptRequest = undefined;
628
659
  }
629
660
 
630
661
  if (hookName === "agent_end" && controller.state.runStartTimeMs !== undefined) {
631
662
  const durationMs = nowMs - controller.state.runStartTimeMs;
632
663
  if (!didAgentEndAbort((event as AgentEndEvent).messages ?? [])) {
633
664
  controller.state.lastRunDurationMs = durationMs;
665
+ controller.state.totalRunDurationMs = controller.state.totalRunDurationMs === undefined
666
+ ? durationMs
667
+ : controller.state.totalRunDurationMs + durationMs;
634
668
  }
635
669
  controller.state.runStartTimeMs = undefined;
636
670
  }
637
671
 
638
672
  if (hookName === "session_shutdown") {
639
673
  controller.state.runStartTimeMs = undefined;
674
+ controller.state.pendingPromptRequest = undefined;
675
+ controller.state.activePromptRequest = undefined;
640
676
  }
641
677
 
642
678
  syncPiUsereqStatusTicker(controller);