@tmustier/pi-usage-extension 0.9.4 → 0.10.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/CHANGELOG.md +19 -0
- package/README.md +3 -2
- package/data.ts +68 -28
- package/graph.ts +2 -0
- package/index.ts +2 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.10.0] - 2026-10-26
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **This Month period.** New tab between Last Week and Last 30 Days covering the current calendar month from local midnight on the 1st, in the graph, table, insights, and exports. Useful when a usage budget resets on the 1st of each month.
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
- Collapse Windows home directories in project labels. Backslash paths are normalised, `C:\Users\<name>\...` collapses to `~/...` like `/Users/<name>` and `/home/<name>`, and other drive paths drop the drive letter (`D:\work\repo` becomes `work/repo`). Previously the project mix insight showed raw Windows paths.
|
|
10
|
+
- Make the usage tests pass on Windows.
|
|
11
|
+
|
|
12
|
+
Thanks to @mturley for This Month ([#114](https://github.com/tmustier/pi-extensions/pull/114), requested in [#107](https://github.com/tmustier/pi-extensions/issues/107)) and @dawidmachon for the Windows fix ([#110](https://github.com/tmustier/pi-extensions/pull/110)).
|
|
13
|
+
|
|
14
|
+
## [0.9.5] - 2026-09-22
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- Treat Pi 0.87 `context_edit` entries as intentional request-prefix changes so they do not count as cache misses.
|
|
18
|
+
|
|
19
|
+
### Internal
|
|
20
|
+
- Cache format bumped to **v6**. The first `/usage` open after upgrading performs a one-off rebuild.
|
|
21
|
+
|
|
3
22
|
## [0.9.4] - 2026-07-22
|
|
4
23
|
|
|
5
24
|
### Changed
|
package/README.md
CHANGED
|
@@ -97,7 +97,7 @@ The **Insights** view has two sections, facts first:
|
|
|
97
97
|
|---|---|
|
|
98
98
|
| Resuming after a break | ≥ 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 |
|
|
99
99
|
| Switching models mid-conversation | ≥ 2% of cost (and ≥ $1) went to large-context misses right after the provider/model changed mid-session — the previous model's cache doesn't transfer |
|
|
100
|
-
| Mid-session re-sends (prefix change) | ≥ 2% of cost (and ≥ $1) went to large-context misses with **no** idle gap, compaction, or model switch to explain them — something rewrote the request prefix |
|
|
100
|
+
| Mid-session re-sends (prefix change) | ≥ 2% of cost (and ≥ $1) went to large-context misses with **no** idle gap, compaction, context edit, or model switch to explain them — something unexpectedly rewrote the request prefix |
|
|
101
101
|
| Session concentration | the top 5 sessions account for ≥ 35% of the period's cost |
|
|
102
102
|
| Upfront tax | ≥ 8% of cost was the first message of a session (session starts pay for their whole prompt uncached) |
|
|
103
103
|
| Cache leverage floor | fewer than 5 cached tokens served per fresh token paid (shown only above $5 / 1M fresh tokens, to avoid noise) |
|
|
@@ -136,7 +136,7 @@ The **Graphs** view plots usage over time for the active period as a braille lin
|
|
|
136
136
|
- **Grouping** (`g` to cycle): by provider, by model, by thinking level, or total only. The top 6 series are shown individually; the rest merge into an `other` series. A bold **Total** line is always drawn.
|
|
137
137
|
- **Cumulative vs per-bucket** (`c` to toggle): running total across the period (default), or the raw per-bucket rate.
|
|
138
138
|
- **Filtering**: move the legend cursor with `↑`/`↓` and toggle series visibility with `Enter`/`Space` (`a` shows all again). The y-axis rescales to the visible series — hide the big lines to zoom into the small ones.
|
|
139
|
-
- **Buckets**: hourly for Today / This Week / Last Week, daily for Last 30 Days / All Time.
|
|
139
|
+
- **Buckets**: hourly for Today / This Week / Last Week (and This Month during its first 8 days), daily for This Month after its first 8 days / Last 30 Days / All Time.
|
|
140
140
|
- **Line clipping**: every series (provider, model, thinking level, `other`, Total) is drawn only between its first and last bucket with usage, so late-starting or retired series don't drag a flat zero/flat tail across the whole period.
|
|
141
141
|
|
|
142
142
|
Thinking levels are replayed from `thinking_level_change` entries in each session file; messages before the first recorded change appear as `unknown`. Auxiliary usage has no reliable thinking-level attribution and appears as `Tools/summaries` in that grouping. Reasoning token counts come from `usage.reasoning` where providers report them; pi only records this field since **pi 0.80.3 (30 June 2026)**, so earlier sessions show zero reasoning tokens even though thinking models were in use.
|
|
@@ -148,6 +148,7 @@ Thinking levels are replayed from `thinking_level_change` entries in each sessio
|
|
|
148
148
|
| **Today** | From midnight (00:00) today |
|
|
149
149
|
| **This Week** | From Monday 00:00 of the current week |
|
|
150
150
|
| **Last Week** | Previous week (Monday 00:00 → this Monday 00:00) |
|
|
151
|
+
| **This Month** | From local midnight on the first day of the current month |
|
|
151
152
|
| **Last 30 Days** | Rolling window: the last 30 calendar days including today (from midnight 29 days back) |
|
|
152
153
|
| **All Time** | All recorded sessions |
|
|
153
154
|
|
package/data.ts
CHANGED
|
@@ -149,6 +149,7 @@ export interface PeriodBounds {
|
|
|
149
149
|
todayMs: number;
|
|
150
150
|
weekStartMs: number;
|
|
151
151
|
lastWeekStartMs: number;
|
|
152
|
+
thisMonthStartMs: number;
|
|
152
153
|
last30DaysStartMs: number;
|
|
153
154
|
nowMs: number;
|
|
154
155
|
}
|
|
@@ -157,6 +158,7 @@ export interface UsageData {
|
|
|
157
158
|
today: TimeFilteredStats;
|
|
158
159
|
thisWeek: TimeFilteredStats;
|
|
159
160
|
lastWeek: TimeFilteredStats;
|
|
161
|
+
thisMonth: TimeFilteredStats;
|
|
160
162
|
last30Days: TimeFilteredStats;
|
|
161
163
|
allTime: TimeFilteredStats;
|
|
162
164
|
/** Deduped usage bucketed by hour start (ms) → series key → metrics. */
|
|
@@ -164,9 +166,9 @@ export interface UsageData {
|
|
|
164
166
|
bounds: PeriodBounds;
|
|
165
167
|
}
|
|
166
168
|
|
|
167
|
-
export type TabName = "today" | "thisWeek" | "lastWeek" | "last30Days" | "allTime";
|
|
169
|
+
export type TabName = "today" | "thisWeek" | "lastWeek" | "thisMonth" | "last30Days" | "allTime";
|
|
168
170
|
|
|
169
|
-
export const TAB_ORDER: TabName[] = ["today", "thisWeek", "lastWeek", "last30Days", "allTime"];
|
|
171
|
+
export const TAB_ORDER: TabName[] = ["today", "thisWeek", "lastWeek", "thisMonth", "last30Days", "allTime"];
|
|
170
172
|
|
|
171
173
|
export type UsageSource = "assistant" | "auxiliary";
|
|
172
174
|
|
|
@@ -194,12 +196,8 @@ export interface SessionMessage extends UsageAmount {
|
|
|
194
196
|
/** Session entry id used to dedupe copied auxiliary entries; empty for assistant messages. */
|
|
195
197
|
sourceId: string;
|
|
196
198
|
timestamp: number;
|
|
197
|
-
/**
|
|
198
|
-
|
|
199
|
-
* message and this one. Compaction legitimately changes the request prefix,
|
|
200
|
-
* so such messages are excluded from prefix-change cache-miss accounting.
|
|
201
|
-
*/
|
|
202
|
-
afterCompaction: boolean;
|
|
199
|
+
/** The request followed a compaction or context edit that intentionally changed its prefix. */
|
|
200
|
+
afterContextChange: boolean;
|
|
203
201
|
}
|
|
204
202
|
|
|
205
203
|
export interface ChildToolUsage {
|
|
@@ -300,6 +298,8 @@ const PATTERN_THINKING_COMPACT = Buffer.from('"type":"thinking_level_change"');
|
|
|
300
298
|
const PATTERN_THINKING_SPACED = Buffer.from('"type": "thinking_level_change"');
|
|
301
299
|
const PATTERN_COMPACTION_COMPACT = Buffer.from('"type":"compaction"');
|
|
302
300
|
const PATTERN_COMPACTION_SPACED = Buffer.from('"type": "compaction"');
|
|
301
|
+
const PATTERN_CONTEXT_EDIT_COMPACT = Buffer.from('"type":"context_edit"');
|
|
302
|
+
const PATTERN_CONTEXT_EDIT_SPACED = Buffer.from('"type": "context_edit"');
|
|
303
303
|
const PATTERN_BRANCH_SUMMARY_COMPACT = Buffer.from('"type":"branch_summary"');
|
|
304
304
|
const PATTERN_BRANCH_SUMMARY_SPACED = Buffer.from('"type": "branch_summary"');
|
|
305
305
|
// pi-subagents versions predating Pi 0.81 persisted child usage in details but
|
|
@@ -357,7 +357,7 @@ function auxiliaryMessage(usage: UsageAmount, timestamp: number, sourceId: strin
|
|
|
357
357
|
sourceId,
|
|
358
358
|
...usage,
|
|
359
359
|
timestamp,
|
|
360
|
-
|
|
360
|
+
afterContextChange: false,
|
|
361
361
|
};
|
|
362
362
|
}
|
|
363
363
|
|
|
@@ -642,10 +642,12 @@ function lineMightBeRelevant(line: Buffer): boolean {
|
|
|
642
642
|
head.includes(PATTERN_SESSION_COMPACT) ||
|
|
643
643
|
head.includes(PATTERN_THINKING_COMPACT) ||
|
|
644
644
|
head.includes(PATTERN_COMPACTION_COMPACT) ||
|
|
645
|
+
head.includes(PATTERN_CONTEXT_EDIT_COMPACT) ||
|
|
645
646
|
head.includes(PATTERN_BRANCH_SUMMARY_COMPACT) ||
|
|
646
647
|
head.includes(PATTERN_SESSION_SPACED) ||
|
|
647
648
|
head.includes(PATTERN_THINKING_SPACED) ||
|
|
648
649
|
head.includes(PATTERN_COMPACTION_SPACED) ||
|
|
650
|
+
head.includes(PATTERN_CONTEXT_EDIT_SPACED) ||
|
|
649
651
|
head.includes(PATTERN_BRANCH_SUMMARY_SPACED)
|
|
650
652
|
);
|
|
651
653
|
}
|
|
@@ -665,7 +667,7 @@ export async function parseSessionBuffer(buffer: Buffer, signal?: AbortSignal):
|
|
|
665
667
|
// message of a session. Replaying them in append order attributes each message
|
|
666
668
|
// to the level active when it was produced.
|
|
667
669
|
let thinkingLevel = "";
|
|
668
|
-
let
|
|
670
|
+
let contextChangePending = false;
|
|
669
671
|
|
|
670
672
|
let start = 0;
|
|
671
673
|
let lineNumber = 0;
|
|
@@ -700,7 +702,9 @@ export async function parseSessionBuffer(buffer: Buffer, signal?: AbortSignal):
|
|
|
700
702
|
} else if (entry.type === "compaction") {
|
|
701
703
|
const usage = parseUsageAmount(entry.usage);
|
|
702
704
|
if (usage) messages.push(auxiliaryMessage(usage, parsedTimestamp(undefined, entry.timestamp), typeof entry.id === "string" ? entry.id : ""));
|
|
703
|
-
|
|
705
|
+
contextChangePending = true;
|
|
706
|
+
} else if (entry.type === "context_edit") {
|
|
707
|
+
contextChangePending = true;
|
|
704
708
|
} else if (entry.type === "branch_summary") {
|
|
705
709
|
const usage = parseUsageAmount(entry.usage);
|
|
706
710
|
if (usage) messages.push(auxiliaryMessage(usage, parsedTimestamp(undefined, entry.timestamp), typeof entry.id === "string" ? entry.id : ""));
|
|
@@ -721,9 +725,9 @@ export async function parseSessionBuffer(buffer: Buffer, signal?: AbortSignal):
|
|
|
721
725
|
cacheWrite: msg.usage.cacheWrite || 0,
|
|
722
726
|
reasoning: msg.usage.reasoning || 0,
|
|
723
727
|
timestamp: msg.timestamp || (Number.isNaN(fallbackTs) ? 0 : fallbackTs),
|
|
724
|
-
|
|
728
|
+
afterContextChange: contextChangePending,
|
|
725
729
|
});
|
|
726
|
-
|
|
730
|
+
contextChangePending = false;
|
|
727
731
|
}
|
|
728
732
|
} else if (entry.type === "message" && entry.message?.role === "toolResult") {
|
|
729
733
|
const msg = entry.message;
|
|
@@ -745,7 +749,7 @@ export async function parseSessionBuffer(buffer: Buffer, signal?: AbortSignal):
|
|
|
745
749
|
// On-disk cache
|
|
746
750
|
// =============================================================================
|
|
747
751
|
|
|
748
|
-
const CACHE_VERSION =
|
|
752
|
+
const CACHE_VERSION = 6;
|
|
749
753
|
|
|
750
754
|
type CachedMessageTuple = [
|
|
751
755
|
providerIdx: number,
|
|
@@ -758,7 +762,7 @@ type CachedMessageTuple = [
|
|
|
758
762
|
timestamp: number,
|
|
759
763
|
thinkingLevelIdx: number,
|
|
760
764
|
reasoning: number,
|
|
761
|
-
|
|
765
|
+
afterContextChange: 0 | 1,
|
|
762
766
|
auxiliary: 0 | 1,
|
|
763
767
|
sourceIdIdx: number,
|
|
764
768
|
];
|
|
@@ -860,7 +864,7 @@ export async function loadUsageCache(cachePath: string): Promise<Map<string, Cac
|
|
|
860
864
|
cacheWrite: Number(tuple[6]) || 0,
|
|
861
865
|
timestamp: Number(tuple[7]) || 0,
|
|
862
866
|
reasoning: Number(tuple[9]) || 0,
|
|
863
|
-
|
|
867
|
+
afterContextChange: tuple[10] === 1,
|
|
864
868
|
});
|
|
865
869
|
}
|
|
866
870
|
if (!valid) continue;
|
|
@@ -959,7 +963,7 @@ export async function saveUsageCache(cachePath: string, states: Map<string, Cach
|
|
|
959
963
|
m.timestamp,
|
|
960
964
|
intern(m.thinkingLevel),
|
|
961
965
|
m.reasoning,
|
|
962
|
-
m.
|
|
966
|
+
m.afterContextChange ? 1 : 0,
|
|
963
967
|
m.source === "auxiliary" ? 1 : 0,
|
|
964
968
|
intern(m.source === "auxiliary" ? m.sourceId : ""),
|
|
965
969
|
]),
|
|
@@ -1036,23 +1040,32 @@ function emptyPeriodRawData(): PeriodRawData {
|
|
|
1036
1040
|
*/
|
|
1037
1041
|
export function projectLabelFromCwd(cwd: string): string {
|
|
1038
1042
|
if (!cwd) return "(unknown)";
|
|
1043
|
+
// Normalize Windows separators so backslash paths (C:\Users\me\...) match the
|
|
1044
|
+
// forward-slash forms compared below. Path text after the home prefix keeps
|
|
1045
|
+
// its normalized forward slashes, which is what the label displays.
|
|
1046
|
+
const normalized = cwd.replaceAll("\\", "/");
|
|
1039
1047
|
// Collapse any home-directory prefix to "~", not just the current user's —
|
|
1040
1048
|
// session stores merged from other machines can carry a different username.
|
|
1041
|
-
const home = homedir();
|
|
1049
|
+
const home = homedir().replaceAll("\\", "/");
|
|
1042
1050
|
let homePrefix: string | null = null;
|
|
1043
|
-
if (
|
|
1051
|
+
if (normalized === home || normalized.startsWith(home + "/")) {
|
|
1044
1052
|
homePrefix = home;
|
|
1045
1053
|
} else {
|
|
1046
|
-
const m = /^(\/Users\/[^/]+|\/home\/[^/]+)(?=\/|$)/.exec(
|
|
1054
|
+
const m = /^(\/Users\/[^/]+|\/home\/[^/]+|[A-Za-z]:\/Users\/[^/]+)(?=\/|$)/.exec(normalized);
|
|
1047
1055
|
if (m) homePrefix = m[1]!;
|
|
1048
1056
|
}
|
|
1049
|
-
if (homePrefix !== null &&
|
|
1050
|
-
let rel = homePrefix !== null ?
|
|
1057
|
+
if (homePrefix !== null && normalized.length <= homePrefix.length) return "~";
|
|
1058
|
+
let rel = homePrefix !== null ? normalized.slice(homePrefix.length + 1) : normalized;
|
|
1051
1059
|
const wt = rel.indexOf("/.worktrees/");
|
|
1052
1060
|
if (wt !== -1) rel = rel.slice(0, wt);
|
|
1053
1061
|
const parts = rel.split("/").filter(Boolean);
|
|
1054
|
-
|
|
1055
|
-
|
|
1062
|
+
if (homePrefix !== null) return `~/${parts.slice(0, 2).join("/")}`;
|
|
1063
|
+
// Windows drive paths outside a home (D:/work/repo/sub): drop the drive segment
|
|
1064
|
+
// so the label keeps meaningful segments (work/repo) and stays slash-separated.
|
|
1065
|
+
const segments = /^[A-Za-z]:$/.test(parts[0] ?? "") ? parts.slice(1) : parts;
|
|
1066
|
+
const label = segments.slice(0, 2).join("/");
|
|
1067
|
+
if (/^[A-Za-z]:\//.test(normalized)) return label;
|
|
1068
|
+
return `/${label}`;
|
|
1056
1069
|
}
|
|
1057
1070
|
|
|
1058
1071
|
function emptyUsageData(bounds: PeriodBounds): UsageData {
|
|
@@ -1060,6 +1073,7 @@ function emptyUsageData(bounds: PeriodBounds): UsageData {
|
|
|
1060
1073
|
today: emptyTimeFilteredStats(),
|
|
1061
1074
|
thisWeek: emptyTimeFilteredStats(),
|
|
1062
1075
|
lastWeek: emptyTimeFilteredStats(),
|
|
1076
|
+
thisMonth: emptyTimeFilteredStats(),
|
|
1063
1077
|
last30Days: emptyTimeFilteredStats(),
|
|
1064
1078
|
allTime: emptyTimeFilteredStats(),
|
|
1065
1079
|
hourly: new Map(),
|
|
@@ -1113,6 +1127,7 @@ function getPeriodsForTimestamp(
|
|
|
1113
1127
|
todayMs: number,
|
|
1114
1128
|
weekStartMs: number,
|
|
1115
1129
|
lastWeekStartMs: number,
|
|
1130
|
+
thisMonthStartMs: number,
|
|
1116
1131
|
last30DaysStartMs: number
|
|
1117
1132
|
): TabName[] {
|
|
1118
1133
|
const periods: TabName[] = ["allTime"];
|
|
@@ -1122,6 +1137,7 @@ function getPeriodsForTimestamp(
|
|
|
1122
1137
|
} else if (timestamp >= lastWeekStartMs) {
|
|
1123
1138
|
periods.push("lastWeek");
|
|
1124
1139
|
}
|
|
1140
|
+
if (timestamp >= thisMonthStartMs) periods.push("thisMonth");
|
|
1125
1141
|
if (timestamp >= last30DaysStartMs) periods.push("last30Days");
|
|
1126
1142
|
return periods;
|
|
1127
1143
|
}
|
|
@@ -1138,11 +1154,12 @@ function addMessagesToUsageData(
|
|
|
1138
1154
|
todayMs: number,
|
|
1139
1155
|
weekStartMs: number,
|
|
1140
1156
|
lastWeekStartMs: number,
|
|
1157
|
+
thisMonthStartMs: number,
|
|
1141
1158
|
last30DaysStartMs: number,
|
|
1142
1159
|
rawByPeriod: Record<TabName, PeriodRawData>,
|
|
1143
1160
|
costByDayIdx: Map<number, number>
|
|
1144
1161
|
): void {
|
|
1145
|
-
const sessionContributed = { today: false, thisWeek: false, lastWeek: false, last30Days: false, allTime: false };
|
|
1162
|
+
const sessionContributed = { today: false, thisWeek: false, lastWeek: false, thisMonth: false, last30Days: false, allTime: false };
|
|
1146
1163
|
|
|
1147
1164
|
for (let mi = 0; mi < messages.length; mi++) {
|
|
1148
1165
|
const msg = messages[mi]!;
|
|
@@ -1159,7 +1176,14 @@ function addMessagesToUsageData(
|
|
|
1159
1176
|
|
|
1160
1177
|
addToHourlyBuckets(data.hourly, msg);
|
|
1161
1178
|
|
|
1162
|
-
const periods = getPeriodsForTimestamp(
|
|
1179
|
+
const periods = getPeriodsForTimestamp(
|
|
1180
|
+
msg.timestamp,
|
|
1181
|
+
todayMs,
|
|
1182
|
+
weekStartMs,
|
|
1183
|
+
lastWeekStartMs,
|
|
1184
|
+
thisMonthStartMs,
|
|
1185
|
+
last30DaysStartMs
|
|
1186
|
+
);
|
|
1163
1187
|
const tokens = {
|
|
1164
1188
|
// Count fresh tokens processed this turn.
|
|
1165
1189
|
// Include cacheWrite because those prompt tokens were newly written and billed.
|
|
@@ -1220,7 +1244,7 @@ function addMessagesToUsageData(
|
|
|
1220
1244
|
}
|
|
1221
1245
|
if (mm.isSessionStart) raw.upfrontCost += msg.cost;
|
|
1222
1246
|
if (
|
|
1223
|
-
!msg.
|
|
1247
|
+
!msg.afterContextChange &&
|
|
1224
1248
|
mm.prevCtx >= MISS_MIN_PREV_CONTEXT &&
|
|
1225
1249
|
msg.cacheRead < Math.min(MISS_MAX_CACHE_READ, 0.3 * mm.prevCtx)
|
|
1226
1250
|
) {
|
|
@@ -1238,6 +1262,7 @@ function addMessagesToUsageData(
|
|
|
1238
1262
|
if (sessionContributed.today) data.today.totals.sessions++;
|
|
1239
1263
|
if (sessionContributed.thisWeek) data.thisWeek.totals.sessions++;
|
|
1240
1264
|
if (sessionContributed.lastWeek) data.lastWeek.totals.sessions++;
|
|
1265
|
+
if (sessionContributed.thisMonth) data.thisMonth.totals.sessions++;
|
|
1241
1266
|
if (sessionContributed.last30Days) data.last30Days.totals.sessions++;
|
|
1242
1267
|
if (sessionContributed.allTime) data.allTime.totals.sessions++;
|
|
1243
1268
|
}
|
|
@@ -1395,6 +1420,12 @@ export async function collectUsageData(options: CollectUsageOptions = {}): Promi
|
|
|
1395
1420
|
startOfLastWeek.setDate(startOfLastWeek.getDate() - 7);
|
|
1396
1421
|
const lastWeekStartMs = startOfLastWeek.getTime();
|
|
1397
1422
|
|
|
1423
|
+
// Calendar month to date, from local midnight on the first.
|
|
1424
|
+
const startOfThisMonth = new Date(now);
|
|
1425
|
+
startOfThisMonth.setDate(1);
|
|
1426
|
+
startOfThisMonth.setHours(0, 0, 0, 0);
|
|
1427
|
+
const thisMonthStartMs = startOfThisMonth.getTime();
|
|
1428
|
+
|
|
1398
1429
|
// Rolling 30-day window: the last 30 calendar days including today,
|
|
1399
1430
|
// i.e. from midnight 29 days before today. setDate handles DST correctly.
|
|
1400
1431
|
const startOfLast30Days = new Date(startOfToday);
|
|
@@ -1524,11 +1555,19 @@ export async function collectUsageData(options: CollectUsageOptions = {}): Promi
|
|
|
1524
1555
|
}
|
|
1525
1556
|
|
|
1526
1557
|
// 6. Aggregate in sorted path order with cross-file dedupe.
|
|
1527
|
-
const data = emptyUsageData({
|
|
1558
|
+
const data = emptyUsageData({
|
|
1559
|
+
todayMs,
|
|
1560
|
+
weekStartMs,
|
|
1561
|
+
lastWeekStartMs,
|
|
1562
|
+
thisMonthStartMs,
|
|
1563
|
+
last30DaysStartMs,
|
|
1564
|
+
nowMs: now.getTime(),
|
|
1565
|
+
});
|
|
1528
1566
|
const rawByPeriod: Record<TabName, PeriodRawData> = {
|
|
1529
1567
|
today: emptyPeriodRawData(),
|
|
1530
1568
|
thisWeek: emptyPeriodRawData(),
|
|
1531
1569
|
lastWeek: emptyPeriodRawData(),
|
|
1570
|
+
thisMonth: emptyPeriodRawData(),
|
|
1532
1571
|
last30Days: emptyPeriodRawData(),
|
|
1533
1572
|
allTime: emptyPeriodRawData(),
|
|
1534
1573
|
};
|
|
@@ -1595,6 +1634,7 @@ export async function collectUsageData(options: CollectUsageOptions = {}): Promi
|
|
|
1595
1634
|
todayMs,
|
|
1596
1635
|
weekStartMs,
|
|
1597
1636
|
lastWeekStartMs,
|
|
1637
|
+
thisMonthStartMs,
|
|
1598
1638
|
last30DaysStartMs,
|
|
1599
1639
|
rawByPeriod,
|
|
1600
1640
|
costByDayIdx
|
package/graph.ts
CHANGED
|
@@ -118,6 +118,8 @@ function domainFor(period: TabName, bounds: PeriodBounds, hourly: Map<number, Ma
|
|
|
118
118
|
return { startMs: bounds.weekStartMs, endMs: bounds.nowMs };
|
|
119
119
|
case "lastWeek":
|
|
120
120
|
return { startMs: bounds.lastWeekStartMs, endMs: bounds.weekStartMs };
|
|
121
|
+
case "thisMonth":
|
|
122
|
+
return { startMs: bounds.thisMonthStartMs, endMs: bounds.nowMs };
|
|
121
123
|
case "last30Days":
|
|
122
124
|
return { startMs: bounds.last30DaysStartMs, endMs: bounds.nowMs };
|
|
123
125
|
case "allTime": {
|
package/index.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* /usage - Usage statistics dashboard
|
|
3
3
|
*
|
|
4
4
|
* Shows an inline view with usage stats grouped by provider.
|
|
5
|
-
* - Tab cycles: Today → This Week → Last Week → All Time
|
|
5
|
+
* - Tab cycles: Today → This Week → Last Week → This Month → All Time
|
|
6
6
|
* - Arrow keys navigate providers
|
|
7
7
|
* - Enter expands/collapses to show models
|
|
8
8
|
*
|
|
@@ -273,6 +273,7 @@ const TAB_LABELS: Record<TabName, string> = {
|
|
|
273
273
|
today: "Today",
|
|
274
274
|
thisWeek: "This Week",
|
|
275
275
|
lastWeek: "Last Week",
|
|
276
|
+
thisMonth: "This Month",
|
|
276
277
|
last30Days: "Last 30 Days",
|
|
277
278
|
allTime: "All Time",
|
|
278
279
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tmustier/pi-usage-extension",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Usage statistics dashboard for Pi sessions.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Thomas Mustier",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"@earendil-works/pi-tui": "*"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@earendil-works/pi-coding-agent": "
|
|
29
|
-
"@earendil-works/pi-tui": "
|
|
28
|
+
"@earendil-works/pi-coding-agent": "0.87.0",
|
|
29
|
+
"@earendil-works/pi-tui": "0.87.0"
|
|
30
30
|
}
|
|
31
31
|
}
|