@pify/usage 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/README.md CHANGED
@@ -11,11 +11,13 @@ Spend is invisible until the invoice arrives, and by then you cannot tell which
11
11
  ## Live footer
12
12
 
13
13
  ```
14
- 📊 12.3k tok · $0.45
14
+ 📊 12.3k tok · $0.45 · ctx ▰▰▰▱▱▱ 34%
15
15
  ```
16
16
 
17
17
  Folded from each message's `usage.cost`, which pi already computes. It survives `/reload` by replaying the session branch rather than keeping a running total in memory.
18
18
 
19
+ The gauge on the right is how full the context window is — the third mid-session question, next to tokens and cost. It was computed for the `/usage` dashboard but shown only there; now it is live. It appears once there is a window to measure against (so not under `-p`) and turns to `⚠` past 90%, the point where "how full" stops being trivia and becomes a decision. For the breakdown of what filled it, run `/context`.
20
+
19
21
  ## `/usage`
20
22
 
21
23
  ```
@@ -40,7 +42,7 @@ History counts every usage-bearing entry in pi's session JSONL — assistant tur
40
42
 
41
43
  Per-project totals come for free: pi stores sessions one directory per project, so the dashboard can show where the money actually went.
42
44
 
43
- ## `/usage context`
45
+ ## `/context` (or `/usage context`)
44
46
 
45
47
  ```
46
48
  Context window: 22.6k of 200.0k used (11%)
@@ -29,7 +29,7 @@ import { readFileSync } from "node:fs";
29
29
 
30
30
  import { addRecord, aggregate, recordFromEntry, windowTotals } from "../src/aggregate.ts";
31
31
  import { buildBreakdown, formatBreakdown } from "../src/context.ts";
32
- import { footerText, formatCost, formatTokens, historyBlock, sessionBlock } from "../src/format.ts";
32
+ import { contextGauge, footerText, formatCost, formatTokens, historyBlock, sessionBlock } from "../src/format.ts";
33
33
  import { QUOTA_PROVIDERS, fetchQuota, quotaReport, type QuotaResult } from "../src/quota.ts";
34
34
  import { redact } from "../src/redact.ts";
35
35
  import { scanSessions } from "../src/sessions.ts";
@@ -62,7 +62,13 @@ export default function usage(pi: ExtensionAPI) {
62
62
 
63
63
  function updateFooter(ctx: UiContext): void {
64
64
  if (!ctx.hasUI) return;
65
- ctx.ui.setStatus("usage", footerText(session));
65
+ const base = footerText(session);
66
+ if (!base) {
67
+ ctx.ui.setStatus("usage", undefined);
68
+ return;
69
+ }
70
+ const gauge = contextGauge(contextPct(ctx));
71
+ ctx.ui.setStatus("usage", gauge ? `${base} · ${gauge}` : base);
66
72
  }
67
73
 
68
74
  function contextPct(ctx: UiContext): number | null {
@@ -187,9 +193,15 @@ export default function usage(pi: ExtensionAPI) {
187
193
  }
188
194
 
189
195
  pi.registerCommand("usage", {
190
- description: "Token and cost dashboard: /usage [quota]",
196
+ description: "Token and cost dashboard: /usage [context | quota]",
191
197
  handler: async (args, ctx) => {
192
198
  if (!ctx.hasUI) return;
199
+ if ((args ?? "").trim().toLowerCase() === "context") {
200
+ // Documented since the breakdown landed, but never wired — the handler
201
+ // only knew "quota", so `/usage context` quietly showed the dashboard.
202
+ ctx.ui.notify(contextBreakdown(ctx), "info");
203
+ return;
204
+ }
193
205
  if ((args ?? "").trim().toLowerCase() === "quota") {
194
206
  // The one networked path in this package, and only when asked for.
195
207
  // Providers with no key are skipped entirely rather than reported as
@@ -225,6 +237,16 @@ export default function usage(pi: ExtensionAPI) {
225
237
  },
226
238
  });
227
239
 
240
+ pi.registerCommand("context", {
241
+ description: "Where the context window went: /context",
242
+ handler: async (_args, ctx) => {
243
+ // The live footer gauge says how full the window is; this says spent on
244
+ // WHAT — the breakdown was built for the /usage dashboard's one-line note
245
+ // but the full bar view had no command to reach it until now.
246
+ if (ctx.hasUI) ctx.ui.notify(contextBreakdown(ctx), "info");
247
+ },
248
+ });
249
+
228
250
  pi.registerTool({
229
251
  name: "usage_status",
230
252
  label: "Usage status",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pify/usage",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Token and cost reporting for pi sessions: live footer, local-history dashboard with per-project costs, opt-in provider quota",
5
5
  "keywords": [
6
6
  "pi-package",
package/src/format.ts CHANGED
@@ -19,6 +19,25 @@ export function footerText(session: UsageTotals): string | undefined {
19
19
  return `📊 ${formatTokens(session.totalTokens)} tok · ${formatCost(session.cost)}`;
20
20
  }
21
21
 
22
+ const GAUGE_CELLS = 6;
23
+
24
+ /**
25
+ * A compact live context gauge for the footer: a filled/empty bar and the
26
+ * percentage of the window in use. The footer already carries tokens and cost;
27
+ * this answers the third question you have mid-session — how close am I to the
28
+ * wall — which was computed for the /usage dashboard but never shown live. It
29
+ * warns once the window is nearly full, because that is when the number stops
30
+ * being trivia and starts being a decision. Empty when there is no window to
31
+ * measure against (e.g. under `-p`, or before the first response).
32
+ */
33
+ export function contextGauge(pct: number | null): string {
34
+ if (pct === null || !Number.isFinite(pct)) return "";
35
+ const clamped = Math.max(0, Math.min(100, pct));
36
+ const filled = Math.round((clamped / 100) * GAUGE_CELLS);
37
+ const bar = "▰".repeat(filled) + "▱".repeat(GAUGE_CELLS - filled);
38
+ return `${clamped >= 90 ? "⚠ " : ""}ctx ${bar} ${Math.round(clamped)}%`;
39
+ }
40
+
22
41
  export function sessionBlock(session: UsageTotals, contextPct: number | null): string {
23
42
  const lines = [
24
43
  "Session",