@po.dev/pi-usage-dashboard 0.1.2 → 0.1.3
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 +82 -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,31 @@ 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("");
|
|
808
836
|
for (let i = 0; i < model.providers.length; i++) {
|
|
809
837
|
const p = model.providers[i]!;
|
|
810
838
|
const cursor = i === this.graphLegendIndex ? th.fg("accent", "▸ ") : " ";
|
|
811
839
|
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(
|
|
840
|
+
lines.push(`${cursor}${marker} ${padRight(this.graphHidden.has(p.name) ? th.fg("dim", p.name) : p.name, 24)} ${padLeft(formatValue(p.total), 8)}`);
|
|
841
|
+
}
|
|
842
|
+
const detail = model.providers.find((p) => p.name === this.graphDetailProvider);
|
|
843
|
+
if (detail) {
|
|
844
|
+
const rows = detail.models.slice(0, 8);
|
|
845
|
+
const prefix = " · ";
|
|
846
|
+
const costWidth = Math.max(visibleWidth("Cost ($)"), ...rows.map((m) => visibleWidth(formatAxisCost(m.cost))));
|
|
847
|
+
const tokenWidth = Math.max(visibleWidth("Token usage"), ...rows.map((m) => visibleWidth(formatTokens(m.tokens))));
|
|
848
|
+
const modelWidth = Math.max(visibleWidth("Model"), Math.min(Math.max(...rows.map((m) => visibleWidth(m.name))), width - visibleWidth(prefix) - costWidth - tokenWidth - 3));
|
|
849
|
+
lines.push("", th.fg("accent", `${detail.name} by model`));
|
|
850
|
+
lines.push(this.theme.fg("muted", `${prefix}${padRight("Model", modelWidth)} ${padLeft("Cost ($)", costWidth)} ${padLeft("Token usage", tokenWidth)}`));
|
|
851
|
+
for (const m of rows) {
|
|
852
|
+
lines.push(`${prefix}${padRight(truncateToWidth(m.name, modelWidth), modelWidth)} ${padLeft(formatAxisCost(m.cost), costWidth)} ${padLeft(formatTokens(m.tokens), tokenWidth)}`);
|
|
853
|
+
}
|
|
813
854
|
}
|
|
814
855
|
lines.push("");
|
|
815
856
|
return lines;
|
|
@@ -1100,23 +1141,23 @@ class UsageComponent {
|
|
|
1100
1141
|
const variants =
|
|
1101
1142
|
this.viewMode === "graph"
|
|
1102
1143
|
? [
|
|
1103
|
-
"[Tab
|
|
1104
|
-
"[
|
|
1105
|
-
"[
|
|
1144
|
+
"[Tab] view [←→] period [↑↓] select [Enter/click] models [Space] hide [e] export [q] close",
|
|
1145
|
+
"[←→] period [↑↓] select [Enter] models [q] close",
|
|
1146
|
+
"[Enter] models [q] close",
|
|
1106
1147
|
"[q] close",
|
|
1107
1148
|
]
|
|
1108
1149
|
: this.viewMode === "insights"
|
|
1109
1150
|
? [
|
|
1110
|
-
"[Tab
|
|
1111
|
-
"[
|
|
1112
|
-
"[↑↓] select [
|
|
1151
|
+
"[Tab] view [←→] period [↑↓] select [e] export [q] close",
|
|
1152
|
+
"[←→] period [↑↓] select [e] export [q] close",
|
|
1153
|
+
"[↑↓] select [q] close",
|
|
1113
1154
|
"[q] close",
|
|
1114
1155
|
]
|
|
1115
1156
|
: [
|
|
1116
|
-
"[Tab
|
|
1117
|
-
"[
|
|
1118
|
-
"[↑↓] select [Enter] expand [/] filter [x] hide [
|
|
1119
|
-
"[↑↓] select [/] [x] [
|
|
1157
|
+
"[Tab] view [←→] period [↑↓] select [Enter] expand [/] filter [x] hide [a] all [e] export [q] close",
|
|
1158
|
+
"[←→] period [↑↓] select [Enter] expand [/] filter [x] hide [e] export [q] close",
|
|
1159
|
+
"[↑↓] select [Enter] expand [/] filter [x] hide [q] close",
|
|
1160
|
+
"[↑↓] select [/] [x] [q]",
|
|
1120
1161
|
"[↑↓] select [q] close",
|
|
1121
1162
|
"[q] close",
|
|
1122
1163
|
];
|
|
@@ -1124,6 +1165,19 @@ class UsageComponent {
|
|
|
1124
1165
|
return [...noteLines, this.theme.fg("dim", line)];
|
|
1125
1166
|
}
|
|
1126
1167
|
|
|
1168
|
+
handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined {
|
|
1169
|
+
if (this.viewMode !== "graph" || event.type !== "click" || event.button !== "left") return undefined;
|
|
1170
|
+
const legendStart = 20;
|
|
1171
|
+
const idx = event.y - legendStart;
|
|
1172
|
+
const providers = this.buildDailyProviderModel().providers;
|
|
1173
|
+
if (idx < 0 || idx >= providers.length) return undefined;
|
|
1174
|
+
const target = providers[idx]!;
|
|
1175
|
+
this.graphLegendIndex = idx;
|
|
1176
|
+
this.graphDetailProvider = this.graphDetailProvider === target.name ? null : target.name;
|
|
1177
|
+
this.requestRender();
|
|
1178
|
+
return { handled: true, render: true };
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1127
1181
|
invalidate(): void {}
|
|
1128
1182
|
dispose(): void {}
|
|
1129
1183
|
}
|
|
@@ -1239,6 +1293,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1239
1293
|
},
|
|
1240
1294
|
invalidate: () => container.invalidate(),
|
|
1241
1295
|
handleInput: (input: string) => usage.handleInput(input),
|
|
1296
|
+
handleMouse: (event: TuiMouseEvent) => usage.handleMouse({ ...event, y: event.y - 3 }),
|
|
1242
1297
|
dispose: () => {},
|
|
1243
1298
|
};
|
|
1244
1299
|
});
|