@marshal/pi-turn-stats 0.1.6 → 0.1.8

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/README.md CHANGED
@@ -7,10 +7,13 @@ A pi extension that automatically tracks per-exchange duration, token usage (inp
7
7
  - **Stats card in conversation stream** — After each reply, a stats card is appended to the stream (does not enter LLM context).
8
8
  - **Real-time status bar** — The bottom status bar shows the last exchange's duration, throughput, token count, and cost.
9
9
  - **`/turnstats` command** — Appends a session cumulative stats card (total exchanges, total LLM calls, total tokens, total cost).
10
+ - **Auto i18n** — UI labels automatically switch between Chinese and English based on your system locale (`LANG` / `LC_ALL` / `Intl`).
10
11
 
11
12
  ## Demo
12
13
 
13
- ![Turn Stats Demo](https://github.com/MarshalW/pi-turn-stats/raw/main/turn-stats.gif)
14
+ **English locale (`LANG=en`)**
15
+
16
+ ![Turn Stats Demo](https://github.com/MarshalW/pi-turn-stats/raw/main/turn-stats_en.mp4)
14
17
 
15
18
  ## Screenshots
16
19
 
@@ -28,22 +31,22 @@ A pi extension that automatically tracks per-exchange duration, token usage (inp
28
31
 
29
32
  ```bash
30
33
  # Install directly with pi
31
- pi install -l npm:@marshal/pi-turn-stats
34
+ pi install npm:@marshal/pi-turn-stats
32
35
  ```
33
36
 
34
37
  Or standard npm install:
35
38
 
36
39
  ```bash
37
- npm install @marshal/pi-turn-stats
40
+ npm install -g @marshal/pi-turn-stats
38
41
  ```
39
42
 
40
- > Note: `pi install -l npm:...` is the pi-recommended local install method that registers the extension for the current project. Plain `npm install` only downloads the package without registering it as a pi extension.
43
+ > Note: `pi install npm:...` registers the extension globally. Plain `npm install -g` only downloads the package without registering it as a pi extension.
41
44
 
42
45
  **Alternative**: Install from GitHub (for source access or custom modifications).
43
46
 
44
47
  ```bash
45
- # Install from GitHub repo, specifying tag (SSH)
46
- pi install -l git:git@github.com:MarshalW/pi-turn-stats@v0.1.1
48
+ # Install from GitHub repo (SSH)
49
+ pi install git:git@github.com:MarshalW/pi-turn-stats
47
50
  ```
48
51
 
49
52
  > Users in China are encouraged to use the npm method, as GitHub access may be unreliable.
@@ -66,6 +69,6 @@ pi install ./ # local install for testing
66
69
  git tag vX.Y.Z && git push origin main --tags
67
70
  # Publish to npm: npm version patch && npm publish --access public
68
71
  # Consumer install:
69
- # npm (recommended): pi install -l npm:@marshal/pi-turn-stats
70
- # git (fallback): pi install -l git:git@github.com:MarshalW/pi-turn-stats@vX.Y.Z
72
+ # npm (recommended): pi install npm:@marshal/pi-turn-stats
73
+ # git (fallback): pi install git:git@github.com:MarshalW/pi-turn-stats@vX.Y.Z
71
74
  ```
@@ -164,6 +164,25 @@ function fmtCost(c: number): string {
164
164
  return `$${c.toFixed(4)}`;
165
165
  }
166
166
 
167
+ /**
168
+ * After session replacement (e.g. /new) or reload, pi invalidates the old
169
+ * extension runner, but a run's `finally` can still deliver agent_settled to
170
+ * it; any ctx/`pi` access on the stale runner throws. Treat those as
171
+ * "no UI / no session" and skip side effects instead of erroring.
172
+ */
173
+ function isStaleCtxError(err: unknown): boolean {
174
+ return err instanceof Error && err.message.includes("extension ctx is stale");
175
+ }
176
+
177
+ /** Run a side effect against ctx/`pi`; silently skip when the ctx is stale. */
178
+ function withStaleGuard(fn: () => void): void {
179
+ try {
180
+ fn();
181
+ } catch (err) {
182
+ if (!isStaleCtxError(err)) throw err;
183
+ }
184
+ }
185
+
167
186
  export default function (pi: ExtensionAPI) {
168
187
  // ---- session cumulative stats ----
169
188
  const sessionTotals = {
@@ -248,8 +267,10 @@ export default function (pi: ExtensionAPI) {
248
267
 
249
268
  // ===== Session start: init status bar =====
250
269
  pi.on("session_start", (_event, ctx) => {
251
- if (!ctx.hasUI) return;
252
- ctx.ui.setStatus("turn-stats", ctx.ui.theme.fg("dim", t("statusWaiting")));
270
+ withStaleGuard(() => {
271
+ if (!ctx.hasUI) return;
272
+ ctx.ui.setStatus("turn-stats", ctx.ui.theme.fg("dim", t("statusWaiting")));
273
+ });
253
274
  });
254
275
 
255
276
  // ===== User submits: start timer =====
@@ -258,10 +279,12 @@ export default function (pi: ExtensionAPI) {
258
279
  startTime = Date.now();
259
280
  turnCount = 0;
260
281
  accum = emptyAccum();
261
- lastModel = ctx.model?.id ?? "unknown";
262
- if (ctx.hasUI) {
263
- ctx.ui.setStatus("turn-stats", ctx.ui.theme.fg("dim", t("statusRunning")));
264
- }
282
+ withStaleGuard(() => {
283
+ lastModel = ctx.model?.id ?? "unknown";
284
+ if (ctx.hasUI) {
285
+ ctx.ui.setStatus("turn-stats", ctx.ui.theme.fg("dim", t("statusRunning")));
286
+ }
287
+ });
265
288
  });
266
289
 
267
290
  // ===== Each LLM turn ends: accumulate usage =====
@@ -297,27 +320,31 @@ export default function (pi: ExtensionAPI) {
297
320
  sessionTotals.durationMs += durMs;
298
321
 
299
322
  if (SHOW_CARD && turnCount > 0) {
300
- pi.appendEntry<TurnStatsData>(ENTRY_TYPE, {
301
- kind: "exchange",
302
- startTime,
303
- endTime,
304
- turns: turnCount,
305
- exchanges: 1,
306
- ...accum,
307
- tokensPerSec: genTps,
308
- model: lastModel,
323
+ withStaleGuard(() => {
324
+ pi.appendEntry<TurnStatsData>(ENTRY_TYPE, {
325
+ kind: "exchange",
326
+ startTime,
327
+ endTime,
328
+ turns: turnCount,
329
+ exchanges: 1,
330
+ ...accum,
331
+ tokensPerSec: genTps,
332
+ model: lastModel,
333
+ });
309
334
  });
310
335
  }
311
336
 
312
- if (ctx.hasUI) {
313
- ctx.ui.setStatus(
314
- "turn-stats",
315
- ctx.ui.theme.fg(
316
- "dim",
317
- t("statusDone", fmtDuration(durMs), fmtThroughput(genTps), fmtTokens(accum.totalTokens), fmtCost(accum.cost)),
318
- ),
319
- );
320
- }
337
+ withStaleGuard(() => {
338
+ if (ctx.hasUI) {
339
+ ctx.ui.setStatus(
340
+ "turn-stats",
341
+ ctx.ui.theme.fg(
342
+ "dim",
343
+ t("statusDone", fmtDuration(durMs), fmtThroughput(genTps), fmtTokens(accum.totalTokens), fmtCost(accum.cost)),
344
+ ),
345
+ );
346
+ }
347
+ });
321
348
  });
322
349
 
323
350
  // ===== /turnstats: append session cumulative stats card =====
@@ -326,20 +353,22 @@ export default function (pi: ExtensionAPI) {
326
353
  handler: async () => {
327
354
  const sessDurMs = sessionTotals.durationMs;
328
355
  const sessGenTps = calcOutputPerSec(sessionTotals.output, sessDurMs);
329
- pi.appendEntry<TurnStatsData>(ENTRY_TYPE, {
330
- kind: "session",
331
- startTime: sessDurMs > 0 ? Date.now() - sessDurMs : Date.now(),
332
- endTime: Date.now(),
333
- turns: sessionTotals.exchanges,
334
- exchanges: sessionTotals.exchanges,
335
- input: sessionTotals.input,
336
- output: sessionTotals.output,
337
- cacheRead: sessionTotals.cacheRead,
338
- cacheWrite: sessionTotals.cacheWrite,
339
- totalTokens: sessionTotals.totalTokens,
340
- cost: sessionTotals.cost,
341
- tokensPerSec: sessGenTps,
342
- model: t("sessionModel"),
356
+ withStaleGuard(() => {
357
+ pi.appendEntry<TurnStatsData>(ENTRY_TYPE, {
358
+ kind: "session",
359
+ startTime: sessDurMs > 0 ? Date.now() - sessDurMs : Date.now(),
360
+ endTime: Date.now(),
361
+ turns: sessionTotals.exchanges,
362
+ exchanges: sessionTotals.exchanges,
363
+ input: sessionTotals.input,
364
+ output: sessionTotals.output,
365
+ cacheRead: sessionTotals.cacheRead,
366
+ cacheWrite: sessionTotals.cacheWrite,
367
+ totalTokens: sessionTotals.totalTokens,
368
+ cost: sessionTotals.cost,
369
+ tokensPerSec: sessGenTps,
370
+ model: t("sessionModel"),
371
+ });
343
372
  });
344
373
  },
345
374
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marshal/pi-turn-stats",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "pi extension: per-exchange duration, token & cost stats for conversations (stream card + status bar + /turnstats command)",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -12,11 +12,14 @@
12
12
  "README.md",
13
13
  "turn-stats.jpg",
14
14
  "turn-stats.gif",
15
- "turn-stats_en.png"
15
+ "turn-stats_en.png",
16
+ "turn-stats_en.mp4"
16
17
  ],
17
18
  "pi": {
18
19
  "extensions": [
19
20
  "./extensions/turn-stats.ts"
20
- ]
21
+ ],
22
+ "video": "https://github.com/MarshalW/pi-turn-stats/raw/main/turn-stats_en.mp4",
23
+ "image": "https://github.com/MarshalW/pi-turn-stats/raw/main/turn-stats_en.png"
21
24
  }
22
25
  }
Binary file