@tmustier/pi-usage-extension 0.7.1 → 0.7.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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.7.2] - 2026-07-17
4
+
5
+ ### Changed
6
+ - **Insights view polish.** Section headers with rules (“Worth attention” in warning colour, “Where it went” in accent), alarm markers and stats in warning colour, the trailing “(x% of this period)” de-emphasised, content capped at 100 columns for readability on wide terminals, and an explicit all-clear line (✓) when no alarms fire. The stat column fits 6-character values (e.g. `$58.79`).
7
+ - **Plain-language copy.** Insight headlines now state what happened rather than the cache mechanism (“spent re-sending conversations after a break” instead of “re-warming caches that expired during idle gaps”), advice avoids jargon, and vendor-specific cache details are gone.
8
+ - The period · “independent lenses” note is folded into the subtitle: “Approximate, based on local sessions on this machine (these are independent and don't sum to 100%).”
9
+
3
10
  ## [0.7.1] - 2026-07-17
4
11
 
5
12
  ### Added
package/README.md CHANGED
@@ -69,12 +69,12 @@ In Pi, run:
69
69
 
70
70
  The **Insights** view has two sections:
71
71
 
72
- **Worth attention** — alarms that only appear when a wasteful pattern is material for the period. A quiet panel means nothing notable was detected.
72
+ **Worth attention** — alarms that only appear when a wasteful pattern is material for the period. When nothing is flagged, the section shows an explicit all-clear.
73
73
 
74
74
  | Alarm | Fires when |
75
75
  |---|---|
76
- | TTL re-warm tax | ≥ 2% of the period's cost (and ≥ $1) went to messages that re-wrote a large context from scratch after a > 5 min idle gap — provider caches expire after a few minutes idle (Anthropic ~5 min) |
77
- | Prefix-change misses | ≥ 2% of cost (and ≥ $1) went to large-context cache misses with **no** idle gap — the request prefix changed mid-session. Messages right after a pi compaction are excluded, since compaction legitimately rewrites the prefix |
76
+ | Re-sends after a break (cache TTL) | ≥ 2% of the period's cost (and ≥ $1) went to messages that re-sent a large conversation from scratch after a > 5 min idle gap — provider caches expire after a few minutes idle |
77
+ | Mid-session re-sends (prefix change) | ≥ 2% of cost (and ≥ $1) went to large-context cache misses with **no** idle gap — the request prefix changed mid-session. Messages right after a pi compaction are excluded, since compaction legitimately rewrites the prefix |
78
78
  | Session concentration | the top 5 sessions account for ≥ 35% of the period's cost |
79
79
  | Upfront tax | ≥ 8% of cost was the first message of a session (session starts pay for their whole prompt uncached) |
80
80
  | Cache leverage floor | fewer than 5 cached tokens served per fresh token paid (shown only above $5 / 1M fresh tokens, to avoid noise) |
package/data.ts CHANGED
@@ -1045,9 +1045,9 @@ function computeInsights(raw: PeriodRawData, trend: TrendInfo | null): PeriodIns
1045
1045
  insights.push({
1046
1046
  kind: "alarm",
1047
1047
  stat: fmtMoney(raw.ttlMissCost),
1048
- headline: `spent re-warming caches that expired during idle gaps (${fmtPercent(ttlPct)} of this period)`,
1048
+ headline: `spent re-sending conversations after a break (${fmtPercent(ttlPct)} of this period)`,
1049
1049
  advice:
1050
- "Provider caches expire after a few minutes idle (Anthropic ~5min); the next message re-writes the whole context at premium rates. Batching replies instead of trickling them keeps caches warm.",
1050
+ "Sent context is only reusable for a few minutes. After a longer pause, the next message pays to send the whole conversation again. Replying while a session is fresh avoids this.",
1051
1051
  });
1052
1052
  }
1053
1053
 
@@ -1056,9 +1056,9 @@ function computeInsights(raw: PeriodRawData, trend: TrendInfo | null): PeriodIns
1056
1056
  insights.push({
1057
1057
  kind: "alarm",
1058
1058
  stat: fmtMoney(raw.prefixMissCost),
1059
- headline: `spent on cache misses with no idle gap (${fmtPercent(prefixPct)} of this period)`,
1059
+ headline: `spent re-sending conversations mid-session (${fmtPercent(prefixPct)} of this period)`,
1060
1060
  advice:
1061
- "The request prefix changed mid-session (compaction excluded), so cached context was re-sent at full price. If this stays high, something in the workflow is defeating provider caching.",
1061
+ "These messages paid full price for context that had already been sent, without a pause to explain it. Usually a tool or workflow is restarting or rewriting conversations. Worth a look if it stays high.",
1062
1062
  });
1063
1063
  }
1064
1064
 
@@ -1071,7 +1071,7 @@ function computeInsights(raw: PeriodRawData, trend: TrendInfo | null): PeriodIns
1071
1071
  kind: "alarm",
1072
1072
  stat: fmtPercent(topPct),
1073
1073
  headline: `of this period's cost came from just ${TOP_SESSION_COUNT} sessions (of ${raw.sessionCosts.size})`,
1074
- advice: "Spend is unusually concentrated. The graph view's filters can pinpoint what those sessions were doing.",
1074
+ advice: "A handful of sessions drove most of the spend. The graph view can show what they were doing.",
1075
1075
  });
1076
1076
  }
1077
1077
  }
@@ -1081,9 +1081,8 @@ function computeInsights(raw: PeriodRawData, trend: TrendInfo | null): PeriodIns
1081
1081
  insights.push({
1082
1082
  kind: "alarm",
1083
1083
  stat: fmtPercent(upfrontPct),
1084
- headline: "of cost was the first message of a session",
1085
- advice:
1086
- "Session starts pay for their whole prompt uncached. Fewer, longer-lived sessions amortize that setup cost.",
1084
+ headline: "of cost went on the opening message of new sessions",
1085
+ advice: "A session's first message sends everything from scratch. Fewer, longer sessions cut this overhead.",
1087
1086
  });
1088
1087
  }
1089
1088
 
@@ -1093,9 +1092,9 @@ function computeInsights(raw: PeriodRawData, trend: TrendInfo | null): PeriodIns
1093
1092
  insights.push({
1094
1093
  kind: "alarm",
1095
1094
  stat: `${leverage.toFixed(1)}×`,
1096
- headline: "cache leverage — cached tokens served per fresh token paid",
1095
+ headline: "tokens reused from history for every token paid at full price",
1097
1096
  advice:
1098
- "Healthy interactive use typically sees 10×+. Low leverage means context is being re-sent uncached — look for workflows that restart conversations.",
1097
+ "Typical interactive use reuses 10× or more. A low number means conversations keep being sent from scratch — look for workflows that restart sessions.",
1099
1098
  });
1100
1099
  }
1101
1100
  }
@@ -1109,13 +1108,13 @@ function computeInsights(raw: PeriodRawData, trend: TrendInfo | null): PeriodIns
1109
1108
  const avgLow = raw.ctxLow.messages > 0 ? raw.ctxLow.cost / raw.ctxLow.messages : 0;
1110
1109
  const cmp =
1111
1110
  avgLow > 0
1112
- ? ` — averaging ${fmtMoney(avgHigh)}/msg vs ${fmtMoney(avgLow)} under ${formatThresholdTokens(CTX_LOW_THRESHOLD)}`
1111
+ ? ` — ${fmtMoney(avgHigh)}/msg vs ${fmtMoney(avgLow)} under ${formatThresholdTokens(CTX_LOW_THRESHOLD)}`
1113
1112
  : "";
1114
1113
  insights.push({
1115
1114
  kind: "structure",
1116
1115
  stat: fmtPercent(pct),
1117
- headline: `of your cost was at ≥${formatThresholdTokens(CTX_TAX_THRESHOLD)} context${cmp}`,
1118
- advice: "Context size is the main cost driver. /compact mid-task and /clear between tasks to reset it.",
1116
+ headline: `of your cost came from messages with ≥${formatThresholdTokens(CTX_TAX_THRESHOLD)} tokens loaded${cmp}`,
1117
+ advice: "Long conversations cost more per message. /compact mid-task and /clear between tasks keep them lean.",
1119
1118
  });
1120
1119
  }
1121
1120
  }
@@ -1143,9 +1142,9 @@ function computeInsights(raw: PeriodRawData, trend: TrendInfo | null): PeriodIns
1143
1142
  insights.push({
1144
1143
  kind: "structure",
1145
1144
  stat: fmtPercent(reasoningPct),
1146
- headline: "of your output tokens were invisible reasoning",
1145
+ headline: "of your output tokens were hidden reasoning",
1147
1146
  advice:
1148
- "Reasoning is billed as output but never displayed. Recorded by pi 0.80.3+ (June 2026) only, so older periods understate it.",
1147
+ "Models charge for their behind-the-scenes thinking as output tokens. pi records this only from 0.80.3 (June 2026), so older periods understate it.",
1149
1148
  });
1150
1149
  }
1151
1150
  }
@@ -1154,9 +1153,9 @@ function computeInsights(raw: PeriodRawData, trend: TrendInfo | null): PeriodIns
1154
1153
  const ratio = trend.last7Cost / trend.priorWeeklyPace;
1155
1154
  const advice =
1156
1155
  ratio >= TREND_HIGH_RATIO
1157
- ? "Spend is accelerating vs your own baseline — the graph view shows what changed."
1156
+ ? "Spending is up against your own baseline — the graph view shows what changed."
1158
1157
  : ratio <= TREND_LOW_RATIO
1159
- ? "Spend is well below your recent baseline."
1158
+ ? "Spending is well below your recent baseline."
1160
1159
  : "";
1161
1160
  insights.push({
1162
1161
  kind: "structure",
package/index.ts CHANGED
@@ -499,11 +499,14 @@ class UsageComponent {
499
499
  const hasCost = stats.totals.cost > 0;
500
500
  const lines: string[] = [];
501
501
 
502
- lines.push("What's contributing to your cost?");
503
- lines.push(th.fg("dim", "Approximate, based on local sessions on this machine."));
504
- lines.push("");
505
- const note = `${TAB_LABELS[this.activeTab]} · independent lenses — they overlap and don't sum to 100%`;
506
- lines.push(th.fg("dim", note));
502
+ // Cap the content column so advice stays readable on very wide terminals.
503
+ const contentWidth = Math.max(Math.min(width, 100), 40);
504
+
505
+ lines.push(th.bold("What's contributing to your cost?"));
506
+ const subtitle = "Approximate, based on local sessions on this machine (these are independent and don't sum to 100%).";
507
+ for (const wrapped of wrapTextWithAnsi(subtitle, contentWidth)) {
508
+ lines.push(th.fg("dim", wrapped));
509
+ }
507
510
  lines.push("");
508
511
 
509
512
  if (!hasMessages) {
@@ -522,13 +525,24 @@ class UsageComponent {
522
525
  return lines;
523
526
  }
524
527
 
525
- const indent = " ";
526
- const adviceWidth = Math.max(width - indent.length, 30);
528
+ // Columns: marker(2) + stat(6) + gap(1); advice aligns under the headline.
529
+ const indent = " ";
530
+ const adviceWidth = Math.max(contentWidth - indent.length, 30);
531
+
532
+ const sectionHeader = (label: string, color: "warning" | "accent"): string => {
533
+ const rule = "─".repeat(Math.max(contentWidth - label.length - 1, 4));
534
+ return `${th.fg(color, th.bold(label))} ${th.fg("border", rule)}`;
535
+ };
527
536
 
528
537
  const renderOne = (insight: (typeof insights)[number]): void => {
529
- const marker = insight.kind === "alarm" ? "⚠ " : " ";
530
- const stat = th.fg("accent", th.bold(padLeft(insight.stat, 5)));
531
- lines.push(`${marker}${stat} ${insight.headline}`);
538
+ const isAlarm = insight.kind === "alarm";
539
+ const marker = isAlarm ? th.fg("warning", "⚠ ") : " ";
540
+ const statText = padLeft(insight.stat, 6);
541
+ const stat = isAlarm ? th.fg("warning", th.bold(statText)) : th.fg("accent", th.bold(statText));
542
+ // De-emphasise the trailing period-share parenthetical on alarm headlines.
543
+ const match = insight.headline.match(/^(.*?)\s*(\(\d[\d.,]*% of this period\))$/);
544
+ const headline = match ? `${match[1]} ${th.fg("dim", match[2]!)}` : insight.headline;
545
+ lines.push(`${marker}${stat} ${headline}`);
532
546
  if (insight.advice) {
533
547
  for (const wrapped of wrapTextWithAnsi(insight.advice, adviceWidth)) {
534
548
  lines.push(`${indent}${th.fg("dim", wrapped)}`);
@@ -539,12 +553,15 @@ class UsageComponent {
539
553
 
540
554
  const alarms = insights.filter((i) => i.kind === "alarm");
541
555
  const structure = insights.filter((i) => i.kind === "structure");
556
+ lines.push(sectionHeader("Worth attention", "warning"));
542
557
  if (alarms.length > 0) {
543
- lines.push(th.fg("accent", "Worth attention"));
544
558
  for (const insight of alarms) renderOne(insight);
559
+ } else {
560
+ lines.push(` ${th.fg("success", padLeft("✓", 6))} ${th.fg("dim", "no waste patterns flagged for this period")}`);
561
+ lines.push("");
545
562
  }
546
563
  if (structure.length > 0) {
547
- if (alarms.length > 0) lines.push(th.fg("dim", "Where it went"));
564
+ lines.push(sectionHeader("Where it went", "accent"));
548
565
  for (const insight of structure) renderOne(insight);
549
566
  }
550
567
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tmustier/pi-usage-extension",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
4
4
  "description": "Usage statistics dashboard for Pi sessions.",
5
5
  "license": "MIT",
6
6
  "author": "Thomas Mustier",