@po.dev/pi-usage-dashboard 0.1.1 → 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 +91 -30
- package/package.json +17 -4
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,70 @@ 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);
|
|
815
|
+
const axisW = Math.max(0, days.length * slotW - 1);
|
|
787
816
|
const height = 8;
|
|
788
817
|
for (let row = height; row >= 1; row--) {
|
|
789
818
|
const threshold = (model.max * row) / height;
|
|
790
|
-
let line = th.fg("dim", `${row === height ?
|
|
791
|
-
for (
|
|
819
|
+
let line = th.fg("dim", `${row === height ? formatValue(model.max) : row === 1 ? formatValue(0) : ""}`.padStart(labelW) + " │");
|
|
820
|
+
for (let d = 0; d < days.length; d++) {
|
|
821
|
+
const day = days[d]!;
|
|
792
822
|
let acc = 0, owner = -1;
|
|
793
823
|
for (let i = 0; i < model.providers.length; i++) {
|
|
794
824
|
const p = model.providers[i]!;
|
|
@@ -796,18 +826,31 @@ class UsageComponent {
|
|
|
796
826
|
acc += day.providers.get(p.name) ?? 0;
|
|
797
827
|
if (acc >= threshold) { owner = i; break; }
|
|
798
828
|
}
|
|
799
|
-
line += owner < 0 ? " " : seriesColor(owner) + "█" + COLOR_RESET;
|
|
829
|
+
line += (owner < 0 ? " " : seriesColor(owner) + "█" + COLOR_RESET) + " ".repeat(d === days.length - 1 ? 0 : slotW - 1);
|
|
800
830
|
}
|
|
801
831
|
lines.push(line);
|
|
802
832
|
}
|
|
803
|
-
lines.push(th.fg("dim", " ".repeat(labelW) + " └" + "─".
|
|
804
|
-
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()));
|
|
805
835
|
lines.push("");
|
|
806
836
|
for (let i = 0; i < model.providers.length; i++) {
|
|
807
837
|
const p = model.providers[i]!;
|
|
808
838
|
const cursor = i === this.graphLegendIndex ? th.fg("accent", "▸ ") : " ";
|
|
809
839
|
const marker = this.graphHidden.has(p.name) ? th.fg("dim", "·") : seriesColor(i) + "•" + COLOR_RESET;
|
|
810
|
-
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
|
+
}
|
|
811
854
|
}
|
|
812
855
|
lines.push("");
|
|
813
856
|
return lines;
|
|
@@ -824,11 +867,14 @@ class UsageComponent {
|
|
|
824
867
|
const total = detail ? detail.chars : this.promptSections.reduce((sum, section) => sum + section.chars, 0);
|
|
825
868
|
const source = this.historySelected ? `History ${this.historySelected.slice(0, 8)}` : "Current session";
|
|
826
869
|
const lines = [th.bold(detail ? `${detail.label} sources` : `${source} prompt sources`), th.fg("dim", detail ? "Esc back · source sizes" : "Assembled system prompt · source sizes · Enter details"), ""];
|
|
870
|
+
const colors: ("accent" | "success" | "warning" | "thinkingHigh")[] = ["accent", "success", "warning", "thinkingHigh"];
|
|
827
871
|
if (!detail) {
|
|
828
872
|
const widthBar = Math.max(20, Math.min(width - 4, 60));
|
|
829
|
-
const colors: ("accent" | "success" | "warning" | "thinkingHigh")[] = ["accent", "success", "warning", "thinkingHigh"];
|
|
830
873
|
let bar = "";
|
|
831
|
-
for (let i = 0; i < rows.length; i++)
|
|
874
|
+
for (let i = 0; i < rows.length; i++) {
|
|
875
|
+
if (i > 0) bar += " ";
|
|
876
|
+
bar += th.fg(colors[i % colors.length]!, "▬".repeat(Math.round(rows[i]!.chars / Math.max(total, 1) * widthBar)));
|
|
877
|
+
}
|
|
832
878
|
lines.push(bar, "");
|
|
833
879
|
}
|
|
834
880
|
for (let i = 0; i < rows.length; i++) {
|
|
@@ -837,8 +883,9 @@ class UsageComponent {
|
|
|
837
883
|
const pct = total ? `${(chars / total * 100).toFixed(1)}%` : "0.0%";
|
|
838
884
|
const selected = i === this.insightIndex;
|
|
839
885
|
const marker = selected ? th.fg("accent", "▸ ") : th.fg("dim", "· ");
|
|
886
|
+
const indicator = detail ? "" : th.fg(colors[i % colors.length]!, "■ ");
|
|
840
887
|
const suffix = th.fg("dim", `${formatNumber(chars)} chars ${pct}`);
|
|
841
|
-
lines.push(`${marker}${selected ? th.fg("accent", row.label) : row.label}${" ".repeat(Math.max(1, width - visibleWidth(marker + row.label + suffix)))}${suffix}`);
|
|
888
|
+
lines.push(`${marker}${indicator}${selected ? th.fg("accent", row.label) : row.label}${" ".repeat(Math.max(1, width - visibleWidth(marker + indicator + row.label + suffix)))}${suffix}`);
|
|
842
889
|
}
|
|
843
890
|
lines.push("", th.fg("dim", "[↑↓] select [Enter] open [Esc] back"));
|
|
844
891
|
return lines;
|
|
@@ -1094,23 +1141,23 @@ class UsageComponent {
|
|
|
1094
1141
|
const variants =
|
|
1095
1142
|
this.viewMode === "graph"
|
|
1096
1143
|
? [
|
|
1097
|
-
"[Tab
|
|
1098
|
-
"[
|
|
1099
|
-
"[
|
|
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",
|
|
1100
1147
|
"[q] close",
|
|
1101
1148
|
]
|
|
1102
1149
|
: this.viewMode === "insights"
|
|
1103
1150
|
? [
|
|
1104
|
-
"[Tab
|
|
1105
|
-
"[
|
|
1106
|
-
"[↑↓] select [
|
|
1151
|
+
"[Tab] view [←→] period [↑↓] select [e] export [q] close",
|
|
1152
|
+
"[←→] period [↑↓] select [e] export [q] close",
|
|
1153
|
+
"[↑↓] select [q] close",
|
|
1107
1154
|
"[q] close",
|
|
1108
1155
|
]
|
|
1109
1156
|
: [
|
|
1110
|
-
"[Tab
|
|
1111
|
-
"[
|
|
1112
|
-
"[↑↓] select [Enter] expand [/] filter [x] hide [
|
|
1113
|
-
"[↑↓] 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]",
|
|
1114
1161
|
"[↑↓] select [q] close",
|
|
1115
1162
|
"[q] close",
|
|
1116
1163
|
];
|
|
@@ -1118,6 +1165,19 @@ class UsageComponent {
|
|
|
1118
1165
|
return [...noteLines, this.theme.fg("dim", line)];
|
|
1119
1166
|
}
|
|
1120
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
|
+
|
|
1121
1181
|
invalidate(): void {}
|
|
1122
1182
|
dispose(): void {}
|
|
1123
1183
|
}
|
|
@@ -1233,6 +1293,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1233
1293
|
},
|
|
1234
1294
|
invalidate: () => container.invalidate(),
|
|
1235
1295
|
handleInput: (input: string) => usage.handleInput(input),
|
|
1296
|
+
handleMouse: (event: TuiMouseEvent) => usage.handleMouse({ ...event, y: event.y - 3 }),
|
|
1236
1297
|
dispose: () => {},
|
|
1237
1298
|
};
|
|
1238
1299
|
});
|
package/package.json
CHANGED
|
@@ -1,13 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@po.dev/pi-usage-dashboard",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Local token and cost dashboard for pi sessions",
|
|
5
|
-
"keywords": [
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package",
|
|
7
|
+
"pi",
|
|
8
|
+
"usage",
|
|
9
|
+
"tokens",
|
|
10
|
+
"dashboard"
|
|
11
|
+
],
|
|
6
12
|
"license": "MIT",
|
|
7
|
-
"files": [
|
|
13
|
+
"files": [
|
|
14
|
+
"*.ts",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
8
17
|
"peerDependencies": {
|
|
9
18
|
"@earendil-works/pi-coding-agent": "*",
|
|
10
19
|
"@earendil-works/pi-tui": "*"
|
|
11
20
|
},
|
|
12
|
-
"pi": {
|
|
21
|
+
"pi": {
|
|
22
|
+
"extensions": [
|
|
23
|
+
"./index.ts"
|
|
24
|
+
]
|
|
25
|
+
}
|
|
13
26
|
}
|