@qmxme/pi-stats 0.2.0 → 0.3.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/stats.ts +51 -50
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qmxme/pi-stats",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Stats widget extension for pi - shows token throughput, usage, and duration",
5
5
  "repository": {
6
6
  "type": "git",
package/stats.ts CHANGED
@@ -1,65 +1,66 @@
1
1
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
2
 
3
3
  export default function (pi: ExtensionAPI) {
4
- let streamingStart: number | undefined;
5
- let initialOutputTokens: number = 0;
6
- let widgetKey = "streaming-stats";
4
+ let agentStartMs: number | null = null;
7
5
 
8
- // Format numbers with K/M suffix
9
- function formatNum(n: number): string {
10
- if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
11
- if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`;
12
- return n.toString();
13
- }
6
+ pi.on("agent_start", () => {
7
+ agentStartMs = Date.now();
8
+ });
14
9
 
15
- // Format duration
16
- function formatDuration(ms: number): string {
17
- const s = ms / 1000;
18
- if (s < 60) return `${s.toFixed(1)}s`;
19
- const m = Math.floor(s / 60);
20
- const rem = (s % 60).toFixed(1);
21
- return `${m}m${rem}s`;
22
- }
10
+ pi.on("agent_end", async (event, ctx) => {
11
+ if (!ctx.hasUI) return;
12
+ if (agentStartMs === null) return;
23
13
 
24
- pi.on("message_start", async (event, ctx) => {
25
- if (event.message.role === "assistant") {
26
- streamingStart = Date.now();
27
- initialOutputTokens = event.message.usage.output;
28
- ctx.ui.setWidget(widgetKey, undefined);
29
- }
30
- });
14
+ const elapsedMs = Date.now() - agentStartMs;
15
+ agentStartMs = null;
16
+ if (elapsedMs <= 0) return;
31
17
 
32
- pi.on("message_end", async (event, ctx) => {
33
- if (event.message.role === "assistant" && streamingStart) {
34
- const elapsed = Date.now() - streamingStart;
35
- const usage = event.message.usage;
18
+ let input = 0;
19
+ let output = 0;
20
+ let cacheRead = 0;
21
+ let cacheWrite = 0;
22
+ let totalTokens = 0;
36
23
 
37
- // Calculate total output tokens generated in this turn
38
- const outputTokens = usage.output - initialOutputTokens;
39
- const tps = elapsed > 0 ? (outputTokens / elapsed) * 1000 : 0;
24
+ for (const message of event.messages) {
25
+ if (message.role !== "assistant") continue;
26
+ input += message.usage?.input ?? 0;
27
+ output += message.usage?.output ?? 0;
28
+ cacheRead += message.usage?.cacheRead ?? 0;
29
+ cacheWrite += message.usage?.cacheWrite ?? 0;
30
+ totalTokens += message.usage?.totalTokens ?? 0;
31
+ }
40
32
 
41
- const total = usage.output + usage.input + usage.cacheRead + usage.cacheWrite;
33
+ if (output <= 0) return;
42
34
 
43
- // Build cache string, only showing non-zero values
44
- let cacheStr = "";
45
- const cacheParts: string[] = [];
46
- if (usage.cacheRead > 0) cacheParts.push(`${formatNum(usage.cacheRead)}↓`);
47
- if (usage.cacheWrite > 0) cacheParts.push(`${formatNum(usage.cacheWrite)}↑`);
48
- if (cacheParts.length > 0) {
49
- cacheStr = ` | cache ${cacheParts.join(" ")}`;
50
- }
35
+ // Format numbers with K/M suffix
36
+ function formatNum(n: number): string {
37
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
38
+ if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`;
39
+ return n.toString();
40
+ }
51
41
 
52
- const stats = `${tps.toFixed(1)} tok/s | ${formatNum(usage.input)} → ${formatNum(usage.output)}${cacheStr} | total ${formatNum(total)} | ${formatDuration(elapsed)}`;
42
+ // Format duration
43
+ function formatDuration(ms: number): string {
44
+ const s = ms / 1000;
45
+ if (s < 60) return `${s.toFixed(1)}s`;
46
+ const m = Math.floor(s / 60);
47
+ const rem = (s % 60).toFixed(1);
48
+ return `${m}m${rem}s`;
49
+ }
53
50
 
54
- ctx.ui.setWidget(widgetKey, (tui, theme) => {
55
- const lines = [theme.fg("dim", stats)];
56
- return {
57
- render: () => lines,
58
- invalidate: () => {},
59
- };
60
- });
51
+ const elapsedSeconds = elapsedMs / 1000;
52
+ const tps = output / elapsedSeconds;
61
53
 
62
- streamingStart = undefined;
54
+ // Build cache string, only showing non-zero values
55
+ let cacheStr = "";
56
+ const cacheParts: string[] = [];
57
+ if (cacheRead > 0) cacheParts.push(`${formatNum(cacheRead)}↓`);
58
+ if (cacheWrite > 0) cacheParts.push(`${formatNum(cacheWrite)}↑`);
59
+ if (cacheParts.length > 0) {
60
+ cacheStr = ` | cache ${cacheParts.join(" ")}`;
63
61
  }
62
+
63
+ const stats = `${tps.toFixed(1)} tok/s | ${formatNum(input)} → ${formatNum(output)}${cacheStr} | total ${formatNum(totalTokens)} | ${formatDuration(elapsedMs)}`;
64
+ ctx.ui.notify(stats, "info");
64
65
  });
65
- }
66
+ }