@po.dev/pi-usage-dashboard 0.1.2 → 0.1.4
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 +80 -27
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import type { ExtensionAPI, ExtensionCommandContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
13
13
|
import { DynamicBorder } from "@earendil-works/pi-coding-agent";
|
|
14
14
|
import { CancellableLoader, Container, Spacer, matchesKey, visibleWidth, truncateToWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
15
|
+
import type { TuiMouseEvent, TuiMouseEventResult } from "@earendil-works/pi-tui";
|
|
15
16
|
|
|
16
17
|
import { AUXILIARY_PROVIDER, collectUsageData, getAgentDir, splitHourlyKey, TAB_ORDER } from "./data";
|
|
17
18
|
import type { CollectProgress } from "./data";
|
|
@@ -378,6 +379,7 @@ class UsageComponent {
|
|
|
378
379
|
private tableFilterEditing = false;
|
|
379
380
|
private graphHidden = new Set<string>();
|
|
380
381
|
private graphLegendIndex = 0;
|
|
382
|
+
private graphDetailProvider: string | null = null;
|
|
381
383
|
private promptSections: PromptSection[];
|
|
382
384
|
private insightIndex = 0;
|
|
383
385
|
private insightDetail: PromptSection | null = null;
|
|
@@ -599,7 +601,10 @@ class UsageComponent {
|
|
|
599
601
|
} else if (matchesKey(data, "down")) {
|
|
600
602
|
const count = this.buildDailyProviderModel().providers.length;
|
|
601
603
|
this.graphLegendIndex = Math.min(Math.max(count - 1, 0), this.graphLegendIndex + 1);
|
|
602
|
-
} else if (matchesKey(data, "enter")
|
|
604
|
+
} else if (matchesKey(data, "enter")) {
|
|
605
|
+
const target = this.buildDailyProviderModel().providers[this.graphLegendIndex];
|
|
606
|
+
if (target) this.graphDetailProvider = this.graphDetailProvider === target.name ? null : target.name;
|
|
607
|
+
} else if (matchesKey(data, "space")) {
|
|
603
608
|
const target = this.buildDailyProviderModel().providers[this.graphLegendIndex];
|
|
604
609
|
if (target) {
|
|
605
610
|
if (this.graphHidden.has(target.name)) this.graphHidden.delete(target.name);
|
|
@@ -656,7 +661,7 @@ class UsageComponent {
|
|
|
656
661
|
let content: string;
|
|
657
662
|
const stats = this.data[this.activeTab];
|
|
658
663
|
if (this.viewMode === "graph") {
|
|
659
|
-
const slice = `${this.graphCumulative ? "cumulative" : "per-bucket"}-${this.graphMetric}-by-${this.graphGroupBy}`;
|
|
664
|
+
const slice = `${this.viewMode}-${this.graphCumulative ? "cumulative" : "per-bucket"}-${this.graphMetric}-by-${this.graphGroupBy}`;
|
|
660
665
|
name = exportFileName("graph", this.activeTab, slice, "csv", now);
|
|
661
666
|
content = buildGraphCsv(this.buildGraphModelForView());
|
|
662
667
|
} else if (this.viewMode === "insights") {
|
|
@@ -750,45 +755,68 @@ class UsageComponent {
|
|
|
750
755
|
return [truncateToWidth(summary, width), truncateToWidth(stats, width), ""];
|
|
751
756
|
}
|
|
752
757
|
|
|
753
|
-
private buildDailyProviderModel(): { days: { label: string; total: number; providers: Map<string, number> }[]; providers: { name: string; total: number }[]; max: number; total: number } {
|
|
758
|
+
private buildDailyProviderModel(): { days: { label: string; total: number; providers: Map<string, number> }[]; providers: { name: string; total: number; models: { name: string; cost: number; tokens: number; value: number }[] }[]; max: number; total: number } {
|
|
754
759
|
const now = this.data.bounds.nowMs;
|
|
755
760
|
const start = this.activeTab === "today" ? this.data.bounds.todayMs : this.activeTab === "thisWeek" ? this.data.bounds.weekStartMs : this.activeTab === "lastWeek" ? this.data.bounds.lastWeekStartMs : this.activeTab === "last30Days" ? this.data.bounds.last30DaysStartMs : Math.min(...this.data.hourly.keys(), this.data.bounds.todayMs);
|
|
756
761
|
const end = this.activeTab === "lastWeek" ? this.data.bounds.weekStartMs : now;
|
|
757
762
|
const dayMs = 24 * 3_600_000;
|
|
763
|
+
const valueOf = (cell: { cost: number; input: number; output: number; cacheRead: number; cacheWrite: number }) => cell.cost;
|
|
758
764
|
const days: { label: string; total: number; providers: Map<string, number> }[] = [];
|
|
759
|
-
for (let t = start; t < end; t += dayMs)
|
|
765
|
+
for (let t = start; t < end; t += dayMs) {
|
|
766
|
+
const date = new Date(t);
|
|
767
|
+
days.push({ label: `${date.getMonth() + 1}/${date.getDate()}`, total: 0, providers: new Map() });
|
|
768
|
+
}
|
|
760
769
|
const providerTotals = new Map<string, number>();
|
|
770
|
+
const modelTotals = new Map<string, Map<string, { cost: number; tokens: number; value: number }>>();
|
|
761
771
|
for (const [hour, cells] of this.data.hourly) {
|
|
762
772
|
if (hour < start || hour >= end) continue;
|
|
763
773
|
const day = days[Math.min(days.length - 1, Math.floor((hour - start) / dayMs))];
|
|
764
774
|
if (!day) continue;
|
|
765
775
|
for (const [key, cell] of cells) {
|
|
766
|
-
const provider = splitHourlyKey(key)
|
|
776
|
+
const { provider, model } = splitHourlyKey(key);
|
|
767
777
|
if (provider === AUXILIARY_PROVIDER) continue;
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
778
|
+
const tokens = cell.input + cell.output + cell.cacheRead + cell.cacheWrite;
|
|
779
|
+
const value = valueOf(cell);
|
|
780
|
+
day.providers.set(provider, (day.providers.get(provider) ?? 0) + value);
|
|
781
|
+
day.total += value;
|
|
782
|
+
providerTotals.set(provider, (providerTotals.get(provider) ?? 0) + value);
|
|
783
|
+
let byModel = modelTotals.get(provider);
|
|
784
|
+
if (!byModel) {
|
|
785
|
+
byModel = new Map();
|
|
786
|
+
modelTotals.set(provider, byModel);
|
|
787
|
+
}
|
|
788
|
+
const modelTotal = byModel.get(model) ?? { cost: 0, tokens: 0, value: 0 };
|
|
789
|
+
modelTotal.cost += cell.cost;
|
|
790
|
+
modelTotal.tokens += tokens;
|
|
791
|
+
modelTotal.value += value;
|
|
792
|
+
byModel.set(model, modelTotal);
|
|
771
793
|
}
|
|
772
794
|
}
|
|
773
|
-
const providers = [...providerTotals.entries()].sort((a, b) => b[1] - a[1]).slice(0, 8).map(([name, total]) => ({
|
|
795
|
+
const providers = [...providerTotals.entries()].sort((a, b) => b[1] - a[1]).slice(0, 8).map(([name, total]) => ({
|
|
796
|
+
name,
|
|
797
|
+
total,
|
|
798
|
+
models: [...(modelTotals.get(name) ?? new Map()).entries()].sort((a, b) => b[1].value - a[1].value).map(([modelName, stats]) => ({ name: modelName, ...stats })),
|
|
799
|
+
}));
|
|
774
800
|
return { days, providers, max: Math.max(0, ...days.map((d) => d.total)), total: days.reduce((sum, d) => sum + d.total, 0) };
|
|
775
801
|
}
|
|
776
802
|
|
|
777
803
|
private renderGraph(width: number): string[] {
|
|
778
804
|
const th = this.theme;
|
|
779
805
|
const model = this.buildDailyProviderModel();
|
|
806
|
+
const formatValue = formatAxisCost;
|
|
780
807
|
const lines: string[] = [...this.renderOverviewSummary(width), th.fg("muted", "Daily total cost · by provider"), ""];
|
|
781
808
|
if (model.total === 0) return [...lines, th.fg("dim", " No usage data for this period"), ""];
|
|
782
809
|
|
|
783
|
-
const labelW = Math.max(
|
|
784
|
-
const
|
|
810
|
+
const labelW = Math.max(formatValue(model.max).length, 3);
|
|
811
|
+
const slotW = 6;
|
|
812
|
+
const plotW = Math.max(1, Math.min(Math.floor((width - labelW - 2) / slotW), model.days.length));
|
|
785
813
|
const start = Math.max(0, model.days.length - plotW);
|
|
786
814
|
const days = model.days.slice(start);
|
|
787
|
-
const axisW = Math.max(0, days.length *
|
|
815
|
+
const axisW = Math.max(0, days.length * slotW - 1);
|
|
788
816
|
const height = 8;
|
|
789
817
|
for (let row = height; row >= 1; row--) {
|
|
790
818
|
const threshold = (model.max * row) / height;
|
|
791
|
-
let line = th.fg("dim", `${row === height ?
|
|
819
|
+
let line = th.fg("dim", `${row === height ? formatValue(model.max) : row === 1 ? formatValue(0) : ""}`.padStart(labelW) + " │");
|
|
792
820
|
for (let d = 0; d < days.length; d++) {
|
|
793
821
|
const day = days[d]!;
|
|
794
822
|
let acc = 0, owner = -1;
|
|
@@ -798,18 +826,29 @@ class UsageComponent {
|
|
|
798
826
|
acc += day.providers.get(p.name) ?? 0;
|
|
799
827
|
if (acc >= threshold) { owner = i; break; }
|
|
800
828
|
}
|
|
801
|
-
line += (owner < 0 ? " " : seriesColor(owner) + "█" + COLOR_RESET) + (d === days.length - 1 ?
|
|
829
|
+
line += (owner < 0 ? " " : seriesColor(owner) + "█" + COLOR_RESET) + " ".repeat(d === days.length - 1 ? 0 : slotW - 1);
|
|
802
830
|
}
|
|
803
831
|
lines.push(line);
|
|
804
832
|
}
|
|
805
|
-
lines.push(th.fg("dim", " ".repeat(labelW) + " └" + "─".
|
|
806
|
-
lines.push(th.fg("dim", " ".repeat(labelW + 2) + (
|
|
833
|
+
lines.push(th.fg("dim", " ".repeat(labelW) + " └" + Array.from({ length: axisW }, (_, i) => i % slotW === 0 ? "┬" : "─").join("")));
|
|
834
|
+
lines.push(th.fg("dim", " ".repeat(labelW + 2) + days.map((day) => day.label.padEnd(slotW)).join("").trimEnd()));
|
|
807
835
|
lines.push("");
|
|
836
|
+
const detail = model.providers.find((p) => p.name === this.graphDetailProvider);
|
|
837
|
+
const rows = detail?.models.slice(0, 8) ?? [];
|
|
838
|
+
const prefix = " ";
|
|
839
|
+
const costWidth = Math.max(visibleWidth("Cost ($)"), ...rows.map((m) => visibleWidth(formatAxisCost(m.cost))));
|
|
840
|
+
const tokenWidth = Math.max(visibleWidth("Token usage"), ...rows.map((m) => visibleWidth(formatTokens(m.tokens))));
|
|
841
|
+
const modelWidth = Math.max(visibleWidth("Model"), Math.min(Math.max(visibleWidth("Model"), ...rows.map((m) => visibleWidth(m.name))), width - visibleWidth(prefix) - costWidth - tokenWidth - 3));
|
|
808
842
|
for (let i = 0; i < model.providers.length; i++) {
|
|
809
843
|
const p = model.providers[i]!;
|
|
810
844
|
const cursor = i === this.graphLegendIndex ? th.fg("accent", "▸ ") : " ";
|
|
811
845
|
const marker = this.graphHidden.has(p.name) ? th.fg("dim", "·") : seriesColor(i) + "•" + COLOR_RESET;
|
|
812
|
-
lines.push(`${cursor}${marker} ${padRight(this.graphHidden.has(p.name) ? th.fg("dim", p.name) : p.name, 24)} ${padLeft(
|
|
846
|
+
lines.push(`${cursor}${marker} ${padRight(this.graphHidden.has(p.name) ? th.fg("dim", p.name) : p.name, 24)} ${padLeft(formatValue(p.total), 8)}`);
|
|
847
|
+
if (p !== detail) continue;
|
|
848
|
+
lines.push(this.theme.fg("muted", `${prefix}${padRight("Model", modelWidth)} ${padLeft("Cost ($)", costWidth)} ${padLeft("Token usage", tokenWidth)}`));
|
|
849
|
+
for (const m of rows) {
|
|
850
|
+
lines.push(`${prefix}${padRight(truncateToWidth(m.name, modelWidth), modelWidth)} ${padLeft(formatAxisCost(m.cost), costWidth)} ${padLeft(formatTokens(m.tokens), tokenWidth)}`);
|
|
851
|
+
}
|
|
813
852
|
}
|
|
814
853
|
lines.push("");
|
|
815
854
|
return lines;
|
|
@@ -1100,23 +1139,23 @@ class UsageComponent {
|
|
|
1100
1139
|
const variants =
|
|
1101
1140
|
this.viewMode === "graph"
|
|
1102
1141
|
? [
|
|
1103
|
-
"[Tab
|
|
1104
|
-
"[
|
|
1105
|
-
"[
|
|
1142
|
+
"[Tab] view [←→] period [↑↓] select [Enter/click] models [Space] hide [e] export [q] close",
|
|
1143
|
+
"[←→] period [↑↓] select [Enter] models [q] close",
|
|
1144
|
+
"[Enter] models [q] close",
|
|
1106
1145
|
"[q] close",
|
|
1107
1146
|
]
|
|
1108
1147
|
: this.viewMode === "insights"
|
|
1109
1148
|
? [
|
|
1110
|
-
"[Tab
|
|
1111
|
-
"[
|
|
1112
|
-
"[↑↓] select [
|
|
1149
|
+
"[Tab] view [←→] period [↑↓] select [e] export [q] close",
|
|
1150
|
+
"[←→] period [↑↓] select [e] export [q] close",
|
|
1151
|
+
"[↑↓] select [q] close",
|
|
1113
1152
|
"[q] close",
|
|
1114
1153
|
]
|
|
1115
1154
|
: [
|
|
1116
|
-
"[Tab
|
|
1117
|
-
"[
|
|
1118
|
-
"[↑↓] select [Enter] expand [/] filter [x] hide [
|
|
1119
|
-
"[↑↓] select [/] [x] [
|
|
1155
|
+
"[Tab] view [←→] period [↑↓] select [Enter] expand [/] filter [x] hide [a] all [e] export [q] close",
|
|
1156
|
+
"[←→] period [↑↓] select [Enter] expand [/] filter [x] hide [e] export [q] close",
|
|
1157
|
+
"[↑↓] select [Enter] expand [/] filter [x] hide [q] close",
|
|
1158
|
+
"[↑↓] select [/] [x] [q]",
|
|
1120
1159
|
"[↑↓] select [q] close",
|
|
1121
1160
|
"[q] close",
|
|
1122
1161
|
];
|
|
@@ -1124,6 +1163,19 @@ class UsageComponent {
|
|
|
1124
1163
|
return [...noteLines, this.theme.fg("dim", line)];
|
|
1125
1164
|
}
|
|
1126
1165
|
|
|
1166
|
+
handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined {
|
|
1167
|
+
if (this.viewMode !== "graph" || event.type !== "click" || event.button !== "left") return undefined;
|
|
1168
|
+
const legendStart = 20;
|
|
1169
|
+
const idx = event.y - legendStart;
|
|
1170
|
+
const providers = this.buildDailyProviderModel().providers;
|
|
1171
|
+
if (idx < 0 || idx >= providers.length) return undefined;
|
|
1172
|
+
const target = providers[idx]!;
|
|
1173
|
+
this.graphLegendIndex = idx;
|
|
1174
|
+
this.graphDetailProvider = this.graphDetailProvider === target.name ? null : target.name;
|
|
1175
|
+
this.requestRender();
|
|
1176
|
+
return { handled: true, render: true };
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1127
1179
|
invalidate(): void {}
|
|
1128
1180
|
dispose(): void {}
|
|
1129
1181
|
}
|
|
@@ -1239,6 +1291,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1239
1291
|
},
|
|
1240
1292
|
invalidate: () => container.invalidate(),
|
|
1241
1293
|
handleInput: (input: string) => usage.handleInput(input),
|
|
1294
|
+
handleMouse: (event: TuiMouseEvent) => usage.handleMouse({ ...event, y: event.y - 3 }),
|
|
1242
1295
|
dispose: () => {},
|
|
1243
1296
|
};
|
|
1244
1297
|
});
|