@monotykamary/pi-better-openai 0.1.43 → 0.1.44

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
@@ -4,7 +4,12 @@
4
4
  * Adds `service_tier: "priority"` to OpenAI provider payloads while fast mode is
5
5
  * enabled and the selected model is in the configured allow-list.
6
6
  */
7
- import { type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
7
+ import {
8
+ type ExtensionAPI,
9
+ type ExtensionContext,
10
+ type Theme,
11
+ type ThemeColor,
12
+ } from "@earendil-works/pi-coding-agent";
8
13
  import {
9
14
  Container,
10
15
  Key,
@@ -58,6 +63,8 @@ import {
58
63
  applySettingToRawConfig,
59
64
  } from "./src/config.ts";
60
65
  import {
66
+ type UsageSegment,
67
+ type UsageSeverity,
61
68
  formatPercent,
62
69
  formatUsageSnapshot,
63
70
  parseUsageSnapshot,
@@ -114,6 +121,46 @@ const requireSettingsListTheme = (): SettingsListTheme => {
114
121
  return loadedSettingsListTheme;
115
122
  };
116
123
 
124
+ const USAGE_SEVERITY_COLORS: Record<UsageSeverity, ThemeColor> = {
125
+ ok: "success",
126
+ warning: "warning",
127
+ critical: "error",
128
+ muted: "dim",
129
+ };
130
+
131
+ /**
132
+ * Keep a coloured segment on one line. Unlike sanitizeStatusText this leaves the
133
+ * spacing shared by neighbouring segments ("Usage: " + "5h: " + "90%") intact.
134
+ */
135
+ function flattenSegment(text: string): string {
136
+ return text.replace(/[\r\n\t]+/g, " ");
137
+ }
138
+
139
+ /**
140
+ * Colour a usage line segment by segment: the percentages carry the severity
141
+ * colour while the labels, countdowns and banked resets stay dim.
142
+ */
143
+ function colorizeUsageSegments(segments: UsageSegment[], theme: Theme): string {
144
+ return segments
145
+ .map((segment) =>
146
+ theme.fg(USAGE_SEVERITY_COLORS[segment.severity], flattenSegment(segment.text)),
147
+ )
148
+ .join("");
149
+ }
150
+
151
+ /** Pair the fast-mode segment with the usage segments for the status widget. */
152
+ function statusWidgetParts(
153
+ fast: string | undefined,
154
+ usage: UsageSegment[] | undefined,
155
+ ): UsageSegment[] | undefined {
156
+ const parts: UsageSegment[] = fast ? [{ text: fast, severity: "muted" }] : [];
157
+ if (usage?.length) {
158
+ if (parts.length > 0) parts.push({ text: " · ", severity: "muted" });
159
+ parts.push(...usage);
160
+ }
161
+ return parts.length > 0 ? parts : undefined;
162
+ }
163
+
117
164
  class DynamicBorder {
118
165
  readonly #color: (text: string) => string;
119
166
 
@@ -1188,8 +1235,10 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
1188
1235
  const petColumnWidth = Math.min(petRenderSizeCells, Math.max(1, width - 1));
1189
1236
  const footerTextWidth = inlinePet ? Math.max(1, width - petColumnWidth - 2) : width;
1190
1237
 
1191
- const usageStatusLine = usageController.statusLine(ctx, cfg, usingSubscription);
1192
- const usageLine = usageStatusLine ? theme.fg("dim", usageStatusLine) : undefined;
1238
+ const usageStatusSegments = usageController.statusSegments(ctx, cfg, usingSubscription);
1239
+ const usageLine = usageStatusSegments
1240
+ ? colorizeUsageSegments(usageStatusSegments, theme)
1241
+ : undefined;
1193
1242
 
1194
1243
  let statsLeft = parts.join(" ");
1195
1244
  let statsLeftWidth = visibleWidth(statsLeft);
@@ -1315,22 +1364,22 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
1315
1364
  statusInstalled = text !== undefined;
1316
1365
  }
1317
1366
 
1318
- function setStatusWidget(ctx: ExtensionContext, text: string | undefined): void {
1319
- if (!text && !statusWidgetInstalled) return;
1367
+ function setStatusWidget(ctx: ExtensionContext, parts: UsageSegment[] | undefined): void {
1368
+ if (!parts && !statusWidgetInstalled) return;
1320
1369
  ctx.ui.setWidget(
1321
1370
  STATUS_KEY,
1322
- text
1371
+ parts
1323
1372
  ? (_tui, theme) => ({
1324
1373
  invalidate() {},
1325
1374
  render(width: number): string[] {
1326
- const line = theme.fg("dim", sanitizeStatusText(text));
1375
+ const line = colorizeUsageSegments(parts, theme);
1327
1376
  return [truncateToWidth(line, width, theme.fg("dim", "..."))];
1328
1377
  },
1329
1378
  })
1330
1379
  : undefined,
1331
1380
  { placement: "belowEditor" },
1332
1381
  );
1333
- statusWidgetInstalled = text !== undefined;
1382
+ statusWidgetInstalled = parts !== undefined;
1334
1383
  }
1335
1384
 
1336
1385
  function updateFooter(ctx: ExtensionContext): void {
@@ -1366,8 +1415,8 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
1366
1415
  }
1367
1416
 
1368
1417
  const fast = fastController.statusSegment(ctx, cfg);
1369
- const usage = usageController.statusLine(ctx, cfg);
1370
- setStatusWidget(ctx, [fast, usage].filter(Boolean).join(" · ") || undefined);
1418
+ const usage = usageController.statusSegments(ctx, cfg);
1419
+ setStatusWidget(ctx, statusWidgetParts(fast, usage));
1371
1420
  }
1372
1421
 
1373
1422
  pi.on("session_start", (_event, ctx) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monotykamary/pi-better-openai",
3
- "version": "0.1.43",
3
+ "version": "0.1.44",
4
4
  "description": "Improve OpenAI in pi with fast mode, usage stats, realtime voice, image generation, and footer polish.",
5
5
  "keywords": [
6
6
  "fast",
@@ -3,6 +3,7 @@ import type { ResolvedConfig } from "./config.ts";
3
3
  import { maskIdentifier, sanitizeDiagnosticError } from "./format.ts";
4
4
  import {
5
5
  AUTH_FILE,
6
+ type UsageSegment,
6
7
  type UsageSnapshot,
7
8
  formatResetCountdown,
8
9
  formatUsageSnapshot,
@@ -10,6 +11,7 @@ import {
10
11
  readCodexAuth,
11
12
  requestCodexUsage,
12
13
  usageScopeForModel,
14
+ usageSegments,
13
15
  } from "./usage.ts";
14
16
  import { currentModelKey } from "./fast-controller.ts";
15
17
 
@@ -70,20 +72,30 @@ export class UsageController {
70
72
  return this.usageSnapshot;
71
73
  }
72
74
 
73
- statusLine(
75
+ statusSegments(
74
76
  ctx: ExtensionContext,
75
77
  cfg = this.getConfig(ctx),
76
78
  isUsingOAuth?: boolean,
77
- ): string | undefined {
79
+ ): UsageSegment[] | undefined {
78
80
  return this.usageSnapshot &&
79
81
  !this.usageError &&
80
82
  this.usageSnapshot.scope === usageScopeForModel(ctx.model?.id) &&
81
83
  cfg.usage.enabled &&
82
84
  isOpenAISubscriptionModel(ctx, cfg, isUsingOAuth)
83
- ? formatUsageSnapshot(this.usageSnapshot, cfg.usage)
85
+ ? usageSegments(this.usageSnapshot, cfg.usage)
84
86
  : undefined;
85
87
  }
86
88
 
89
+ statusLine(
90
+ ctx: ExtensionContext,
91
+ cfg = this.getConfig(ctx),
92
+ isUsingOAuth?: boolean,
93
+ ): string | undefined {
94
+ return this.statusSegments(ctx, cfg, isUsingOAuth)
95
+ ?.map((segment) => segment.text)
96
+ .join("");
97
+ }
98
+
87
99
  formatStatus(ctx: ExtensionContext): string {
88
100
  const cfg = this.getConfig(ctx);
89
101
  if (!cfg.usage.enabled) return "Usage display is disabled.";
package/src/usage.ts CHANGED
@@ -253,11 +253,29 @@ export function formatPercent(value: number | null): string {
253
253
  : "--";
254
254
  }
255
255
 
256
- export function formatUsageSnapshot(
256
+ export type UsageSeverity = "ok" | "warning" | "critical" | "muted";
257
+
258
+ export type UsageSegment = {
259
+ text: string;
260
+ severity: UsageSeverity;
261
+ };
262
+
263
+ /** Remaining budget at or below these thresholds turns the percentage amber/red. */
264
+ const WARNING_LEFT_PERCENT = 30;
265
+ const CRITICAL_LEFT_PERCENT = 10;
266
+
267
+ export function severityForLeftPercent(percent: number | null): UsageSeverity {
268
+ if (percent === null) return "muted";
269
+ if (percent <= CRITICAL_LEFT_PERCENT) return "critical";
270
+ if (percent <= WARNING_LEFT_PERCENT) return "warning";
271
+ return "ok";
272
+ }
273
+
274
+ export function usageSegments(
257
275
  snapshot: UsageSnapshot,
258
276
  options: { showResetTimes: boolean; showBankedResets?: boolean },
259
277
  now = Date.now(),
260
- ): string {
278
+ ): UsageSegment[] {
261
279
  const windows = [
262
280
  {
263
281
  label: "5h",
@@ -276,24 +294,41 @@ export function formatUsageSnapshot(
276
294
  (window) => window.percent !== null || window.resetSeconds !== null,
277
295
  );
278
296
  const displayedWindows = availableWindows.length > 0 ? availableWindows : windows;
279
- const usage = displayedWindows
280
- .map((window) => `${window.label}: ${formatPercent(window.percent)}`)
281
- .join(" · ");
282
- const resets = options.showResetTimes
283
- ? displayedWindows
284
- .map((window) =>
285
- formatCompactReset(
286
- displayedWindows.length > 1 ? window.label : undefined,
287
- remainingResetSeconds(window.resetSeconds, snapshot.capturedAt, now),
288
- window.includeDate ? { includeDate: true } : undefined,
289
- now,
290
- ),
291
- )
292
- .filter((value): value is string => value !== null)
293
- : [];
297
+ const segments: UsageSegment[] = [{ text: "Usage: ", severity: "muted" }];
298
+ displayedWindows.forEach((window, index) => {
299
+ if (index > 0) segments.push({ text: " · ", severity: "muted" });
300
+ segments.push({ text: `${window.label}: `, severity: "muted" });
301
+ segments.push({
302
+ text: formatPercent(window.percent),
303
+ severity: severityForLeftPercent(window.percent),
304
+ });
305
+ });
306
+ if (options.showResetTimes) {
307
+ for (const window of displayedWindows) {
308
+ const reset = formatCompactReset(
309
+ displayedWindows.length > 1 ? window.label : undefined,
310
+ remainingResetSeconds(window.resetSeconds, snapshot.capturedAt, now),
311
+ window.includeDate ? { includeDate: true } : undefined,
312
+ now,
313
+ );
314
+ if (reset) segments.push({ text: ` · ${reset}`, severity: "muted" });
315
+ }
316
+ }
294
317
  const banked =
295
318
  options.showBankedResets === false ? null : formatBankedResetsSuffix(snapshot.bankedResets);
296
- return `Usage: ${usage}${resets.length ? ` · ${resets.join(" · ")}` : ""}${banked ? ` · ${banked}` : ""}`;
319
+ if (banked) segments.push({ text: ` · ${banked}`, severity: "muted" });
320
+ return segments;
321
+ }
322
+
323
+ /** Flat text form of {@link usageSegments}, used for status lines and notifications. */
324
+ export function formatUsageSnapshot(
325
+ snapshot: UsageSnapshot,
326
+ options: { showResetTimes: boolean; showBankedResets?: boolean },
327
+ now = Date.now(),
328
+ ): string {
329
+ return usageSegments(snapshot, options, now)
330
+ .map((segment) => segment.text)
331
+ .join("");
297
332
  }
298
333
 
299
334
  export function formatBankedResetsSuffix(count: number | null): string | null {