@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 +7 -0
- package/README.md +3 -3
- package/data.ts +16 -17
- package/index.ts +29 -12
- package/package.json +1 -1
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.
|
|
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
|
-
|
|
|
77
|
-
|
|
|
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-
|
|
1048
|
+
headline: `spent re-sending conversations after a break (${fmtPercent(ttlPct)} of this period)`,
|
|
1049
1049
|
advice:
|
|
1050
|
-
"
|
|
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
|
|
1059
|
+
headline: `spent re-sending conversations mid-session (${fmtPercent(prefixPct)} of this period)`,
|
|
1060
1060
|
advice:
|
|
1061
|
-
"
|
|
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: "
|
|
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
|
|
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: "
|
|
1095
|
+
headline: "tokens reused from history for every token paid at full price",
|
|
1097
1096
|
advice:
|
|
1098
|
-
"
|
|
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
|
-
? ` —
|
|
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
|
|
1118
|
-
advice: "
|
|
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
|
|
1145
|
+
headline: "of your output tokens were hidden reasoning",
|
|
1147
1146
|
advice:
|
|
1148
|
-
"
|
|
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
|
-
? "
|
|
1156
|
+
? "Spending is up against your own baseline — the graph view shows what changed."
|
|
1158
1157
|
: ratio <= TREND_LOW_RATIO
|
|
1159
|
-
? "
|
|
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
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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
|
-
|
|
526
|
-
const
|
|
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
|
|
530
|
-
const
|
|
531
|
-
|
|
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
|
-
|
|
564
|
+
lines.push(sectionHeader("Where it went", "accent"));
|
|
548
565
|
for (const insight of structure) renderOne(insight);
|
|
549
566
|
}
|
|
550
567
|
|