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.
@@ -17,7 +17,6 @@ import type {
17
17
  } from "@mariozechner/pi-coding-agent";
18
18
  import type { UseReqConfig } from "./config.js";
19
19
  import { normalizePathSlashes } from "./path-context.js";
20
- import { formatPiNotifyBeepStatus, formatPiNotifyPushoverStatus } from "./pi-notify.js";
21
20
 
22
21
  /**
23
22
  * @brief Enumerates the CLI-supported theme tokens consumed by status rendering.
@@ -32,42 +31,24 @@ type StatusForegroundColor = Extract<
32
31
 
33
32
  /**
34
33
  * @brief Describes the raw theme capabilities required for status rendering.
35
- * @details Accepts the `ctx.ui.theme` foreground renderer plus optional helpers
36
- * that can convert a foreground color into a background-styled fragment for the
37
- * context-usage bar. Compile-time only and introduces no runtime cost.
34
+ * @details Accepts the `ctx.ui.theme` foreground renderer used by the
35
+ * single-line footer. Compile-time only and introduces no runtime cost.
38
36
  */
39
37
  interface RawStatusTheme {
40
38
  fg: (color: StatusForegroundColor, text: string) => string;
41
- bgFromFg?: (color: StatusForegroundColor, text: string) => string;
42
- getFgAnsi?: (color: StatusForegroundColor) => string;
43
39
  }
44
40
 
45
41
  /**
46
42
  * @brief Describes the normalized theme adapter used by status formatters.
47
- * @details Exposes deterministic label, value, foreground, background, and
48
- * context-cell renderers so status text generation stays independent from the
49
- * raw theme API. Compile-time only and introduces no runtime cost.
43
+ * @details Exposes deterministic label, value, foreground, and separator
44
+ * renderers so status text generation stays independent from the raw theme API.
45
+ * Compile-time only and introduces no runtime cost.
50
46
  */
51
47
  interface StatusThemeAdapter {
52
48
  label: (fieldName: string) => string;
53
49
  value: (text: string) => string;
54
50
  colorize: (color: StatusForegroundColor, text: string) => string;
55
- backgroundize: (color: StatusForegroundColor, text: string) => string;
56
51
  separator: string;
57
- filledContextCell: string;
58
- emptyContextCell: string;
59
- }
60
-
61
- /**
62
- * @brief Describes one fixed-width context-bar overlay.
63
- * @details Stores the literal text plus foreground and background color roles
64
- * used when the context bar must render threshold-specific labels instead of
65
- * block glyphs. Compile-time only and introduces no runtime cost.
66
- */
67
- interface ContextUsageOverlaySpec {
68
- backgroundColor: StatusForegroundColor;
69
- foregroundColor: StatusForegroundColor;
70
- text: "◀ CLEAR ▶ " | " ◀ FULL ▶ ";
71
52
  }
72
53
 
73
54
  /**
@@ -149,90 +130,31 @@ export interface PiUsereqStatusController {
149
130
  tickHandle: ReturnType<typeof setInterval> | undefined;
150
131
  }
151
132
 
152
- /**
153
- * @brief Converts a foreground ANSI sequence into the equivalent background ANSI.
154
- * @details Supports the standard `38;` foreground prefix emitted by pi themes.
155
- * Returns `undefined` when the input cannot be transformed deterministically.
156
- * Runtime is O(n) in ANSI sequence length. No external state is mutated.
157
- * @param[in] foregroundAnsi {string} Foreground ANSI sequence.
158
- * @return {string | undefined} Background ANSI sequence when derivable.
159
- */
160
- function convertForegroundAnsiToBackgroundAnsi(
161
- foregroundAnsi: string,
162
- ): string | undefined {
163
- if (!foregroundAnsi.includes("\u001b[")) {
164
- return undefined;
165
- }
166
- const backgroundAnsi = foregroundAnsi.replace("[38;", "[48;");
167
- return backgroundAnsi === foregroundAnsi ? undefined : backgroundAnsi;
168
- }
169
-
170
- /**
171
- * @brief Applies a foreground-derived background style to one text fragment.
172
- * @details Prefers a theme-provided `bgFromFg` encoder for deterministic test
173
- * rendering and falls back to ANSI conversion when the runtime theme exposes
174
- * `getFgAnsi`. Runtime is O(n) in fragment length. No external state is
175
- * mutated.
176
- * @param[in] theme {RawStatusTheme} Raw theme adapter.
177
- * @param[in] color {StatusForegroundColor} Foreground color reused as background.
178
- * @param[in] text {string} Already-colored foreground fragment.
179
- * @return {string} Background-decorated text fragment.
180
- */
181
- function applyForegroundAsBackground(
182
- theme: RawStatusTheme,
183
- color: StatusForegroundColor,
184
- text: string,
185
- ): string {
186
- if (typeof theme.bgFromFg === "function") {
187
- return theme.bgFromFg(color, text);
188
- }
189
- if (typeof theme.getFgAnsi === "function") {
190
- const backgroundAnsi = convertForegroundAnsiToBackgroundAnsi(
191
- theme.getFgAnsi(color),
192
- );
193
- if (backgroundAnsi) {
194
- return `${backgroundAnsi}${text}\u001b[39m\u001b[49m`;
195
- }
196
- }
197
- return text;
198
- }
199
-
200
133
  /**
201
134
  * @brief Builds the normalized theme adapter used by pi-usereq status formatters.
202
- * @details Precomputes label, value, foreground, background, separator, and
203
- * context-cell renderers so status formatting remains stable across real TUI
204
- * themes and deterministic test doubles. Runtime is O(1). No external state
205
- * is mutated.
135
+ * @details Precomputes label, value, foreground, and separator renderers so
136
+ * status formatting remains stable across real TUI themes and deterministic
137
+ * test doubles. Runtime is O(1). No external state is mutated.
206
138
  * @param[in] theme {RawStatusTheme} Raw theme implementation from `ctx.ui.theme`.
207
139
  * @return {StatusThemeAdapter} Normalized status-theme adapter.
208
140
  */
209
141
  function createStatusThemeAdapter(theme: RawStatusTheme): StatusThemeAdapter {
210
142
  const colorize = (color: StatusForegroundColor, text: string): string =>
211
143
  theme.fg(color, text);
212
- const backgroundize = (color: StatusForegroundColor, text: string): string =>
213
- applyForegroundAsBackground(theme, color, text);
214
144
  return {
215
145
  label: (fieldName: string): string => colorize("accent", `${fieldName}:`),
216
146
  value: (text: string): string => colorize("warning", text),
217
147
  colorize,
218
- backgroundize,
219
148
  separator: colorize("dim", " • "),
220
- filledContextCell: backgroundize(
221
- "accent",
222
- colorize("warning", "▓"),
223
- ),
224
- emptyContextCell: backgroundize(
225
- "accent",
226
- colorize("dim", "▓"),
227
- ),
228
149
  };
229
150
  }
230
151
 
231
152
  /**
232
153
  * @brief Normalizes one raw context-usage snapshot.
233
154
  * @details Preserves the runtime token and context-window counts, derives a
234
- * percentage when the runtime omits it, and clamps percentages into `[0, 100]`.
235
- * Runtime is O(1). No external state is mutated.
155
+ * percentage when the runtime omits it, clamps negative percentages to `0`,
156
+ * and preserves overflow above `100` for footer rendering. Runtime is O(1). No
157
+ * external state is mutated.
236
158
  * @param[in] contextUsage {ContextUsage | undefined} Raw runtime snapshot.
237
159
  * @return {ContextUsage | undefined} Normalized snapshot.
238
160
  */
@@ -251,7 +173,7 @@ function normalizeContextUsage(
251
173
  ...contextUsage,
252
174
  percent: derivedPercent === null
253
175
  ? null
254
- : Math.max(0, Math.min(100, derivedPercent)),
176
+ : Math.max(0, derivedPercent),
255
177
  };
256
178
  }
257
179
 
@@ -273,100 +195,52 @@ function refreshContextUsage(
273
195
  }
274
196
 
275
197
  /**
276
- * @brief Counts the filled cells rendered by the 10-cell context bar.
277
- * @details Uses ceiling semantics for positive percentages so any non-zero
278
- * usage occupies at least one cell and zero usage occupies none. Runtime is
279
- * O(1). No external state is mutated.
280
- * @param[in] contextUsage {ContextUsage | undefined} Normalized context snapshot.
281
- * @return {number} Filled-cell count in the inclusive range `[0, 10]`.
282
- * @satisfies REQ-122
283
- */
284
- function countFilledContextCells(
285
- contextUsage: ContextUsage | undefined,
286
- ): number {
287
- const percent = contextUsage?.percent;
288
- if (percent === null || percent === undefined || percent <= 0) {
289
- return 0;
290
- }
291
- return Math.min(10, Math.ceil((percent * 10) / 100));
292
- }
293
-
294
- /**
295
- * @brief Resolves the threshold-specific context-bar overlay when required.
296
- * @details Returns the empty-state `◀ CLEAR ▶ ` overlay when normalized context
297
- * usage is unavailable or non-positive and returns the centered ` ◀ FULL ▶ `
298
- * overlay with the active theme `error` token when usage exceeds 90 percent.
299
- * Runtime is O(1). No external state is mutated.
198
+ * @brief Resolves the icon text for one normalized context-usage snapshot.
199
+ * @details Maps context usage to one fixed-width icon band so footer rendering
200
+ * remains compact and deterministic. Unavailable usage degrades to the `0%`
201
+ * icon. Runtime is O(1). No external state is mutated.
300
202
  * @param[in] contextUsage {ContextUsage | undefined} Normalized context snapshot.
301
- * @return {ContextUsageOverlaySpec | undefined} Overlay spec when a replacement label is required.
302
- * @satisfies REQ-127, REQ-128
203
+ * @return {string} Fixed-width gauge icon text.
204
+ * @satisfies REQ-121, REQ-122
303
205
  */
304
- function resolveContextUsageOverlay(
206
+ function resolveContextUsageIconText(
305
207
  contextUsage: ContextUsage | undefined,
306
- ): ContextUsageOverlaySpec | undefined {
208
+ ): string {
307
209
  const percent = contextUsage?.percent;
308
210
  if (percent === undefined || percent === null || percent <= 0) {
309
- return {
310
- backgroundColor: "accent",
311
- foregroundColor: "warning",
312
- text: "◀ CLEAR ▶ ",
313
- };
211
+ return "▕_▏";
314
212
  }
315
- if (percent > 90) {
316
- return {
317
- backgroundColor: "warning",
318
- foregroundColor: "error",
319
- text: " ◀ FULL ▶ ",
320
- };
213
+ if (percent <= 25) {
214
+ return "▕▂▏";
321
215
  }
322
- return undefined;
323
- }
324
-
325
- /**
326
- * @brief Formats one threshold-specific context-bar overlay.
327
- * @details Renders the fixed-width overlay text with the requested foreground
328
- * color and reuses the selected bar color as the background so the bar width
329
- * and state-specific backdrop remain preserved. Runtime is O(n) in overlay
330
- * width. No external state is mutated.
331
- * @param[in] theme {StatusThemeAdapter} Normalized status theme.
332
- * @param[in] overlay {ContextUsageOverlaySpec} Overlay specification.
333
- * @return {string} Rendered overlay text.
334
- * @satisfies REQ-127, REQ-128
335
- */
336
- function formatContextUsageOverlay(
337
- theme: StatusThemeAdapter,
338
- overlay: ContextUsageOverlaySpec,
339
- ): string {
340
- return theme.backgroundize(
341
- overlay.backgroundColor,
342
- theme.colorize(overlay.foregroundColor, overlay.text),
343
- );
216
+ if (percent <= 50) {
217
+ return "▕▄▏";
218
+ }
219
+ if (percent <= 90) {
220
+ return "▕▆▏";
221
+ }
222
+ return "▕█▏";
344
223
  }
345
224
 
346
225
  /**
347
- * @brief Formats one 10-cell context-usage bar.
348
- * @details Renders threshold-specific overlays for empty and high-water states;
349
- * otherwise renders filled cells with the theme `warning` token on an
350
- * accent-derived background and unfilled cells in `dim` on the same background
351
- * to preserve constant bar width. Runtime is O(1). No external state is
352
- * mutated.
226
+ * @brief Formats one icon-based context-usage gauge.
227
+ * @details Renders the documented `warning`-colored icon bands for `0-100%` and
228
+ * applies terminal blink control to the `error`-colored overflow icon so
229
+ * unsupported terminals degrade to non-blinking red automatically. Runtime is
230
+ * O(1). No external state is mutated.
353
231
  * @param[in] theme {StatusThemeAdapter} Normalized status theme.
354
232
  * @param[in] contextUsage {ContextUsage | undefined} Normalized context snapshot.
355
- * @return {string} Rendered 10-cell bar or overlay.
233
+ * @return {string} Rendered fixed-width gauge icon.
356
234
  * @satisfies REQ-121, REQ-122, REQ-126, REQ-127, REQ-128
357
235
  */
358
236
  function formatContextUsageBar(
359
237
  theme: StatusThemeAdapter,
360
238
  contextUsage: ContextUsage | undefined,
361
239
  ): string {
362
- const overlay = resolveContextUsageOverlay(contextUsage);
363
- if (overlay) {
364
- return formatContextUsageOverlay(theme, overlay);
240
+ if ((contextUsage?.percent ?? 0) > 100) {
241
+ return theme.colorize("error", "\u001b[5m▕█▏\u001b[25m");
365
242
  }
366
- const filledCells = countFilledContextCells(contextUsage);
367
- return Array.from({ length: 10 }, (_value, index) =>
368
- index < filledCells ? theme.filledContextCell : theme.emptyContextCell,
369
- ).join("");
243
+ return theme.value(resolveContextUsageIconText(contextUsage));
370
244
  }
371
245
 
372
246
  /**
@@ -404,7 +278,7 @@ function formatCompletedStatusDuration(
404
278
  * @brief Formats the consolidated `elapsed` status-bar value.
405
279
  * @details Emits the active prompt segment `⏱︎ <active>`, the latest normally
406
280
  * completed segment `⚑ <last>`, and the accumulated successful-runtime segment
407
- * `⌛︎ <total>` with fixed spacing. Runtime is O(1). No external state is
281
+ * `⌛︎<total>` with fixed spacing. Runtime is O(1). No external state is
408
282
  * mutated.
409
283
  * @param[in] state {PiUsereqStatusState} Mutable status state snapshot.
410
284
  * @param[in] nowMs {number} Current wall-clock time in milliseconds.
@@ -420,7 +294,7 @@ function formatElapsedStatusValue(
420
294
  : formatStatusDuration(nowMs - state.runStartTimeMs);
421
295
  const lastText = formatCompletedStatusDuration(state.lastRunDurationMs);
422
296
  const totalText = formatCompletedStatusDuration(state.totalRunDurationMs);
423
- return ` ⏱︎ ${activeText} ⚑ ${lastText} ⌛︎ ${totalText}`;
297
+ return `⏱︎ ${activeText} ⚑ ${lastText} ⌛︎${totalText}`;
424
298
  }
425
299
 
426
300
  /**
@@ -478,14 +352,16 @@ function didAgentEndAbort(messages: AgentEndEvent["messages"]): boolean {
478
352
 
479
353
  /**
480
354
  * @brief Builds the full single-line pi-usereq status-bar payload.
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.
355
+ * @details Renders base, context, elapsed, and sound fields in the canonical
356
+ * order with dim bullet separators and the documented icon-based context gauge.
357
+ * Runtime is O(1). No external state is mutated.
482
358
  * @param[in] cwd {string} Runtime working directory used for base-path derivation.
483
359
  * @param[in] config {UseReqConfig} Effective project configuration.
484
360
  * @param[in] theme {StatusThemeAdapter} Normalized status theme.
485
361
  * @param[in] state {PiUsereqStatusState} Mutable status state snapshot.
486
362
  * @param[in] nowMs {number} Current wall-clock time in milliseconds.
487
363
  * @return {string} Single-line status-bar text.
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
364
+ * @satisfies REQ-109, REQ-112, REQ-120, REQ-121, REQ-123, REQ-124, REQ-125, REQ-126, REQ-127, REQ-128, REQ-148, REQ-156, REQ-159, REQ-180
489
365
  */
490
366
  function buildPiUsereqStatusText(
491
367
  cwd: string,
@@ -496,9 +372,7 @@ function buildPiUsereqStatusText(
496
372
  ): string {
497
373
  const baseText = normalizePathSlashes(path.resolve(cwd));
498
374
  const elapsedText = formatElapsedStatusValue(state, nowMs);
499
- const beepText = formatPiNotifyBeepStatus(config);
500
375
  const soundText = config["notify-sound"];
501
- const pushoverText = formatPiNotifyPushoverStatus(config);
502
376
  return [
503
377
  formatStatusField(theme, "base", baseText),
504
378
  formatRenderedStatusField(
@@ -507,9 +381,7 @@ function buildPiUsereqStatusText(
507
381
  formatContextUsageBar(theme, state.contextUsage),
508
382
  ),
509
383
  formatStatusField(theme, "elapsed", elapsedText),
510
- formatStatusField(theme, "beep", beepText),
511
384
  formatStatusField(theme, "sound", soundText),
512
- formatStatusField(theme, "pushover", pushoverText),
513
385
  ].join(theme.separator);
514
386
  }
515
387
 
@@ -598,13 +470,13 @@ export function setPiUsereqStatusConfig(
598
470
  /**
599
471
  * @brief Renders the current pi-usereq status bar into the active UI context.
600
472
  * @details Updates the controller's latest context pointer and writes the
601
- * single-line status text only when configuration is available, including any
602
- * threshold-specific context-bar overlays. Runtime is O(s) in configured
603
- * source-path count. Side effect: mutates `ctx.ui` status.
473
+ * single-line status text only when configuration is available, including the
474
+ * documented icon-based context gauge. Runtime is O(s) in configured source-
475
+ * path count. Side effect: mutates `ctx.ui` status.
604
476
  * @param[in,out] controller {PiUsereqStatusController} Mutable status controller.
605
477
  * @param[in] ctx {ExtensionContext} Active extension context.
606
478
  * @return {void} No return value.
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
479
+ * @satisfies REQ-120, REQ-121, REQ-123, REQ-124, REQ-125, REQ-126, REQ-127, REQ-128, REQ-148, REQ-159, REQ-180
608
480
  */
609
481
  export function renderPiUsereqStatus(
610
482
  controller: PiUsereqStatusController,