@monotykamary/pi-better-grok 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.ts CHANGED
@@ -7,7 +7,12 @@
7
7
  * cli-chat-proxy.grok.com identity-first billing surface using pi's native
8
8
  * xAI OAuth credential.
9
9
  */
10
- import { type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
10
+ import {
11
+ type ExtensionAPI,
12
+ type ExtensionContext,
13
+ type Theme,
14
+ type ThemeColor,
15
+ } from "@earendil-works/pi-coding-agent";
11
16
  import { Container, SettingsList, type SettingsListTheme } from "@earendil-works/pi-tui";
12
17
  import { CONFIG_BASENAME, STATUS_KEY } from "./src/identity.ts";
13
18
  import {
@@ -56,6 +61,8 @@ import {
56
61
  parseUsageSnapshot,
57
62
  requestGrokUsage,
58
63
  UsageError,
64
+ type UsageSegment,
65
+ type UsageSeverity,
59
66
  type UsageSnapshot,
60
67
  } from "./src/usage.ts";
61
68
  import {
@@ -85,6 +92,46 @@ const requireSettingsListTheme = (): SettingsListTheme => {
85
92
  return loadedSettingsListTheme;
86
93
  };
87
94
 
95
+ const USAGE_SEVERITY_COLORS: Record<UsageSeverity, ThemeColor> = {
96
+ ok: "success",
97
+ warning: "warning",
98
+ critical: "error",
99
+ muted: "dim",
100
+ };
101
+
102
+ /**
103
+ * Keep a coloured segment on one line. Unlike sanitizeStatusText this leaves the
104
+ * spacing shared by neighbouring segments ("5h: " + "90%") intact.
105
+ */
106
+ function flattenSegment(text: string): string {
107
+ return text.replace(/[\r\n\t]+/g, " ");
108
+ }
109
+
110
+ /**
111
+ * Colour a usage line segment by segment: the percentage carries the severity
112
+ * colour while the label, countdown and banked resets stay dim.
113
+ */
114
+ function colorizeUsageSegments(segments: UsageSegment[], theme: Theme): string {
115
+ return segments
116
+ .map((segment) =>
117
+ theme.fg(USAGE_SEVERITY_COLORS[segment.severity], flattenSegment(segment.text)),
118
+ )
119
+ .join("");
120
+ }
121
+
122
+ /** Pair the fast-mode segment with the usage segments for the status widget. */
123
+ function statusWidgetParts(
124
+ fast: string | undefined,
125
+ usage: UsageSegment[] | undefined,
126
+ ): UsageSegment[] | undefined {
127
+ const parts: UsageSegment[] = fast ? [{ text: fast, severity: "muted" }] : [];
128
+ if (usage?.length) {
129
+ if (parts.length > 0) parts.push({ text: " · ", severity: "muted" });
130
+ parts.push(...usage);
131
+ }
132
+ return parts.length > 0 ? parts : undefined;
133
+ }
134
+
88
135
  class DynamicBorder {
89
136
  readonly #color: (text: string) => string;
90
137
 
@@ -548,8 +595,10 @@ export default function betterGrok(pi: ExtensionAPI): void {
548
595
  parts.push(contextText);
549
596
 
550
597
  const cfg = config(ctx);
551
- const usageStatusLine = usageController.statusLine(ctx, cfg, usingSubscription);
552
- const usageLine = usageStatusLine ? theme.fg("dim", usageStatusLine) : undefined;
598
+ const usageStatusSegments = usageController.statusSegments(ctx, cfg, usingSubscription);
599
+ const usageLine = usageStatusSegments
600
+ ? colorizeUsageSegments(usageStatusSegments, theme)
601
+ : undefined;
553
602
 
554
603
  let statsLeft = parts.join(" ");
555
604
  let statsLeftWidth = visibleWidth(statsLeft);
@@ -630,22 +679,22 @@ export default function betterGrok(pi: ExtensionAPI): void {
630
679
  statusInstalled = text !== undefined;
631
680
  }
632
681
 
633
- function setStatusWidget(ctx: ExtensionContext, text: string | undefined): void {
634
- if (!text && !statusWidgetInstalled) return;
682
+ function setStatusWidget(ctx: ExtensionContext, parts: UsageSegment[] | undefined): void {
683
+ if (!parts && !statusWidgetInstalled) return;
635
684
  ctx.ui.setWidget(
636
685
  STATUS_KEY,
637
- text
686
+ parts
638
687
  ? (_tui, theme) => ({
639
688
  invalidate() {},
640
689
  render(width: number): string[] {
641
- const line = theme.fg("dim", sanitizeStatusText(text));
690
+ const line = colorizeUsageSegments(parts, theme);
642
691
  return [truncateToWidth(line, width, theme.fg("dim", "..."))];
643
692
  },
644
693
  })
645
694
  : undefined,
646
695
  { placement: "belowEditor" },
647
696
  );
648
- statusWidgetInstalled = text !== undefined;
697
+ statusWidgetInstalled = parts !== undefined;
649
698
  }
650
699
 
651
700
  function updateFooter(ctx: ExtensionContext): void {
@@ -678,8 +727,8 @@ export default function betterGrok(pi: ExtensionAPI): void {
678
727
  }
679
728
 
680
729
  const fast = fastController.statusSegment(ctx, cfg);
681
- const usage = usageController.statusLine(ctx, cfg);
682
- setStatusWidget(ctx, [fast, usage].filter(Boolean).join(" · ") || undefined);
730
+ const usage = usageController.statusSegments(ctx, cfg);
731
+ setStatusWidget(ctx, statusWidgetParts(fast, usage));
683
732
  }
684
733
 
685
734
  pi.on("session_start", (_event, ctx) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monotykamary/pi-better-grok",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Improve Grok/xAI in pi with fast mode, subscription usage stats, banked reset redemption, multiprovider pools, footer polish, and settings — mirroring pi-better-openai.",
5
5
  "keywords": [
6
6
  "footer",
@@ -3,7 +3,13 @@ import type { ResolvedConfig } from "./config.ts";
3
3
  import { isXaiProvider, readPiStoredOAuthToken } from "./grok-auth.ts";
4
4
  import { sanitizeDiagnosticError } from "./format.ts";
5
5
  import { currentModelKey } from "./fast-controller.ts";
6
- import { formatUsageDetail, formatUsageSnapshot, type UsageSnapshot } from "./usage.ts";
6
+ import {
7
+ type UsageSegment,
8
+ type UsageSnapshot,
9
+ formatUsageDetail,
10
+ formatUsageSnapshot,
11
+ usageSegments,
12
+ } from "./usage.ts";
7
13
 
8
14
  export function isGrokSubscriptionModel(
9
15
  ctx: Pick<ExtensionContext, "model" | "modelRegistry">,
@@ -77,22 +83,32 @@ export class UsageController {
77
83
  return this.usageSnapshot;
78
84
  }
79
85
 
80
- statusLine(
86
+ statusSegments(
81
87
  ctx: ExtensionContext,
82
88
  cfg = this.getConfig(ctx),
83
89
  isUsingOAuth?: boolean,
84
- ): string | undefined {
90
+ ): UsageSegment[] | undefined {
85
91
  return this.usageSnapshot &&
86
92
  !this.usageError &&
87
93
  cfg.usage.enabled &&
88
94
  isGrokSubscriptionModel(ctx, cfg, isUsingOAuth)
89
- ? formatUsageSnapshot(this.usageSnapshot, {
95
+ ? usageSegments(this.usageSnapshot, {
90
96
  ...cfg.usage,
91
97
  bankedResets: this.getBankedResets?.() ?? null,
92
98
  })
93
99
  : undefined;
94
100
  }
95
101
 
102
+ statusLine(
103
+ ctx: ExtensionContext,
104
+ cfg = this.getConfig(ctx),
105
+ isUsingOAuth?: boolean,
106
+ ): string | undefined {
107
+ return this.statusSegments(ctx, cfg, isUsingOAuth)
108
+ ?.map((segment) => segment.text)
109
+ .join("");
110
+ }
111
+
96
112
  formatStatus(ctx: ExtensionContext): string {
97
113
  const cfg = this.getConfig(ctx);
98
114
  if (!cfg.usage.enabled) return "Usage display is disabled.";
package/src/usage.ts CHANGED
@@ -241,24 +241,59 @@ export function formatBankedResetsSuffix(count: number | null | undefined): stri
241
241
  return `${count} banked reset${count === 1 ? "" : "s"}`;
242
242
  }
243
243
 
244
- export function formatUsageSnapshot(
244
+ export type UsageSeverity = "ok" | "warning" | "critical" | "muted";
245
+
246
+ export type UsageSegment = {
247
+ text: string;
248
+ severity: UsageSeverity;
249
+ };
250
+
251
+ /** Remaining budget at or below these thresholds turns the percentage amber/red. */
252
+ const WARNING_LEFT_PERCENT = 30;
253
+ const CRITICAL_LEFT_PERCENT = 10;
254
+
255
+ export function severityForLeftPercent(percent: number | null): UsageSeverity {
256
+ if (percent === null) return "muted";
257
+ if (percent <= CRITICAL_LEFT_PERCENT) return "critical";
258
+ if (percent <= WARNING_LEFT_PERCENT) return "warning";
259
+ return "ok";
260
+ }
261
+
262
+ export function usageSegments(
245
263
  snapshot: UsageSnapshot,
246
264
  options: UsageStatusOptions,
247
265
  now = Date.now(),
248
- ): string {
266
+ ): UsageSegment[] {
249
267
  const used = snapshot.creditUsagePercent;
250
268
  const left = used === null ? null : clampPercent(100 - used);
251
- const parts = [`Usage: ${formatPercent(left)} left`];
269
+ const segments: UsageSegment[] = [
270
+ { text: "Usage: ", severity: "muted" },
271
+ { text: formatPercent(left), severity: severityForLeftPercent(left) },
272
+ { text: " left", severity: "muted" },
273
+ ];
252
274
  if (options.showResetTimes) {
253
275
  const seconds = periodSecondsLeft(snapshot, now);
254
276
  const countdown = formatResetCountdown(seconds);
255
277
  const clock = formatResetClock(seconds, now);
256
- if (countdown && clock) parts.push(`↺ ${countdown} - ${clock}`);
278
+ if (countdown && clock) {
279
+ segments.push({ text: ` · ↺ ${countdown} - ${clock}`, severity: "muted" });
280
+ }
257
281
  }
258
282
  const banked =
259
283
  options.showBankedResets === false ? null : formatBankedResetsSuffix(options.bankedResets);
260
- if (banked) parts.push(banked);
261
- return parts.join(" · ");
284
+ if (banked) segments.push({ text: ` · ${banked}`, severity: "muted" });
285
+ return segments;
286
+ }
287
+
288
+ /** Flat text form of {@link usageSegments}, used for status lines and notifications. */
289
+ export function formatUsageSnapshot(
290
+ snapshot: UsageSnapshot,
291
+ options: UsageStatusOptions,
292
+ now = Date.now(),
293
+ ): string {
294
+ return usageSegments(snapshot, options, now)
295
+ .map((segment) => segment.text)
296
+ .join("");
262
297
  }
263
298
 
264
299
  export function formatUsageDetail(snapshot: UsageSnapshot): string {