@firstpick/pi-extension-git-footer-status 0.2.3 → 0.2.5

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 (3) hide show
  1. package/README.md +1 -1
  2. package/index.ts +88 -63
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Enhanced Pi footer with git health and model/token telemetry.
4
4
 
5
- ![Status bar with metrics and git context](images/Statusbar_v0.1.5.png)
5
+ ![Status bar with metrics and git context](https://unpkg.com/@firstpick/pi-extension-git-footer-status/images/Statusbar_v0.1.5.png)
6
6
 
7
7
  ## What it does
8
8
 
package/index.ts CHANGED
@@ -101,6 +101,26 @@ type PromptEstimateCache = {
101
101
  tokens: number;
102
102
  };
103
103
 
104
+ type FooterUsageSnapshot = {
105
+ totalInput: number;
106
+ totalOutput: number;
107
+ totalCacheRead: number;
108
+ totalCacheWrite: number;
109
+ totalCost: number;
110
+ historicalTokenSpeed: number | null;
111
+ };
112
+
113
+ function emptyFooterUsageSnapshot(): FooterUsageSnapshot {
114
+ return {
115
+ totalInput: 0,
116
+ totalOutput: 0,
117
+ totalCacheRead: 0,
118
+ totalCacheWrite: 0,
119
+ totalCost: 0,
120
+ historicalTokenSpeed: null,
121
+ };
122
+ }
123
+
104
124
  function formatTokenSpeed(tokensPerSecond: number): string {
105
125
  if (tokensPerSecond < 100) {
106
126
  if (tokensPerSecond >= 10) return tokensPerSecond.toFixed(1);
@@ -368,6 +388,7 @@ export default function gitFooterStatus(pi: ExtensionAPI) {
368
388
  let latestMeasuredTokenSpeed: number | null = null;
369
389
  let promptEstimateCache: PromptEstimateCache | null = null;
370
390
  let promptEstimateRequestId = 0;
391
+ let footerUsageSnapshot: FooterUsageSnapshot = emptyFooterUsageSnapshot();
371
392
  let requestFooterRender: (() => void) | null = null;
372
393
 
373
394
  const getPromptCalibration = (ctx: ExtensionContext) => collectInitialPromptCalibration(ctx.sessionManager.getSessionDir());
@@ -409,13 +430,62 @@ export default function gitFooterStatus(pi: ExtensionAPI) {
409
430
  }
410
431
  };
411
432
 
412
- const getFooterPromptInjectionTokens = (ctx: ExtensionContext): number => {
413
- const fallback = getFallbackPromptEstimate(ctx);
414
- if (!promptEstimateCache || promptEstimateCache.key !== fallback.key) {
415
- promptEstimateCache = { key: fallback.key, tokens: fallback.estimate.total };
416
- void refreshPromptInjectionEstimate(ctx);
433
+ const getFooterPromptInjectionTokens = (): number => promptEstimateCache?.tokens ?? 0;
434
+
435
+ const recomputeFooterUsageSnapshot = (ctx: ExtensionContext): FooterUsageSnapshot => {
436
+ const snapshot = emptyFooterUsageSnapshot();
437
+ const entries = ctx.sessionManager.getEntries();
438
+
439
+ for (let i = 0; i < entries.length; i++) {
440
+ const entry = entries[i];
441
+ if (entry.type !== "message" || entry.message.role !== "assistant") continue;
442
+
443
+ const message = entry.message as AssistantMessage;
444
+ snapshot.totalInput += message.usage?.input ?? 0;
445
+ snapshot.totalOutput += message.usage?.output ?? 0;
446
+ snapshot.totalCacheRead += message.usage?.cacheRead ?? 0;
447
+ snapshot.totalCacheWrite += message.usage?.cacheWrite ?? 0;
448
+ snapshot.totalCost += message.usage?.cost?.total ?? 0;
449
+
450
+ const outputTokens = message.usage?.output ?? 0;
451
+ if (outputTokens <= 0) continue;
452
+
453
+ const endMs = getEntryTimestampMs(entry);
454
+ if (endMs === null) continue;
455
+
456
+ let fallbackSpeed: number | null = null;
457
+ for (let j = i - 1; j >= 0; j--) {
458
+ const previous = entries[j];
459
+ if (previous.type !== "message") continue;
460
+
461
+ // Skip assistant-to-assistant deltas (too noisy for speed).
462
+ if (previous.message.role === "assistant") continue;
463
+
464
+ const startMs = getEntryTimestampMs(previous);
465
+ if (startMs === null || endMs <= startMs) continue;
466
+
467
+ const elapsedSeconds = (endMs - startMs) / 1000;
468
+ if (elapsedSeconds <= 0) continue;
469
+
470
+ const speed = outputTokens / elapsedSeconds;
471
+ if (!isReasonableTokenSpeed(speed)) continue;
472
+
473
+ // Prefer user-anchored speed (best approximation of full turn latency).
474
+ if (previous.message.role === "user") {
475
+ snapshot.historicalTokenSpeed = speed;
476
+ break;
477
+ }
478
+
479
+ // Keep first non-assistant speed as fallback if no user message is found.
480
+ if (fallbackSpeed === null) fallbackSpeed = speed;
481
+ }
482
+
483
+ if (fallbackSpeed !== null && snapshot.historicalTokenSpeed === null) {
484
+ snapshot.historicalTokenSpeed = fallbackSpeed;
485
+ }
417
486
  }
418
- return promptEstimateCache.tokens;
487
+
488
+ return snapshot;
419
489
  };
420
490
 
421
491
  const recordAssistantSpeed = (message: AssistantMessage, endMs = Date.now()): boolean => {
@@ -476,6 +546,7 @@ export default function gitFooterStatus(pi: ExtensionAPI) {
476
546
 
477
547
  pi.on("session_start", async (_event, ctx) => {
478
548
  promptEstimateCache = null;
549
+ footerUsageSnapshot = recomputeFooterUsageSnapshot(ctx);
479
550
  void refreshPromptInjectionEstimate(ctx);
480
551
 
481
552
  ctx.ui.setFooter((tui, theme, footerData) => {
@@ -490,70 +561,22 @@ export default function gitFooterStatus(pi: ExtensionAPI) {
490
561
  },
491
562
  invalidate() {},
492
563
  render(width: number): string[] {
493
- let totalInput = 0;
494
- let totalOutput = 0;
495
- let totalCacheRead = 0;
496
- let totalCacheWrite = 0;
497
- let totalCost = 0;
564
+ const {
565
+ totalInput,
566
+ totalOutput,
567
+ totalCacheRead,
568
+ totalCacheWrite,
569
+ totalCost,
570
+ historicalTokenSpeed,
571
+ } = footerUsageSnapshot;
498
572
  const liveOutputTokens = currentAssistantStartMs !== null ? currentAssistantEstimatedOutputTokens : 0;
499
573
  let latestTokenSpeed: number | null = currentAssistantStartMs !== null ? currentAssistantLiveTokenSpeed : latestMeasuredTokenSpeed;
500
- let historicalTokenSpeed: number | null = null;
501
-
502
- const entries = ctx.sessionManager.getEntries();
503
- for (let i = 0; i < entries.length; i++) {
504
- const entry = entries[i];
505
- if (entry.type === "message" && entry.message.role === "assistant") {
506
- const message = entry.message as AssistantMessage;
507
- totalInput += message.usage?.input ?? 0;
508
- totalOutput += message.usage?.output ?? 0;
509
- totalCacheRead += message.usage?.cacheRead ?? 0;
510
- totalCacheWrite += message.usage?.cacheWrite ?? 0;
511
- totalCost += message.usage?.cost?.total ?? 0;
512
-
513
- if (latestMeasuredTokenSpeed === null && (message.usage?.output ?? 0) > 0) {
514
- const endMs = getEntryTimestampMs(entry);
515
- if (endMs !== null) {
516
- let fallbackSpeed: number | null = null;
517
-
518
- for (let j = i - 1; j >= 0; j--) {
519
- const previous = entries[j];
520
- if (previous.type !== "message") continue;
521
-
522
- // Skip assistant-to-assistant deltas (too noisy for speed).
523
- if (previous.message.role === "assistant") continue;
524
-
525
- const startMs = getEntryTimestampMs(previous);
526
- if (startMs === null || endMs <= startMs) continue;
527
-
528
- const elapsedSeconds = (endMs - startMs) / 1000;
529
- if (elapsedSeconds <= 0) continue;
530
-
531
- const speed = (message.usage?.output ?? 0) / elapsedSeconds;
532
- if (!isReasonableTokenSpeed(speed)) continue;
533
-
534
- // Prefer user-anchored speed (best approximation of full turn latency).
535
- if (previous.message.role === "user") {
536
- historicalTokenSpeed = speed;
537
- break;
538
- }
539
-
540
- // Keep first non-assistant speed as fallback if no user message is found.
541
- if (fallbackSpeed === null) fallbackSpeed = speed;
542
- }
543
-
544
- if (fallbackSpeed !== null && historicalTokenSpeed === null) {
545
- historicalTokenSpeed = fallbackSpeed;
546
- }
547
- }
548
- }
549
- }
550
- }
551
574
 
552
575
  if (latestTokenSpeed === null && historicalTokenSpeed !== null) {
553
576
  latestTokenSpeed = historicalTokenSpeed;
554
577
  }
555
578
 
556
- const currentPromptInjectionTokens = getFooterPromptInjectionTokens(ctx);
579
+ const currentPromptInjectionTokens = getFooterPromptInjectionTokens();
557
580
 
558
581
  const contextUsage = ctx.getContextUsage();
559
582
  const contextWindow = contextUsage?.contextWindow ?? ctx.model?.contextWindow ?? 0;
@@ -730,6 +753,8 @@ export default function gitFooterStatus(pi: ExtensionAPI) {
730
753
  recordAssistantSpeed(event.message as AssistantMessage);
731
754
  resetLiveAssistantState();
732
755
  }
756
+ footerUsageSnapshot = recomputeFooterUsageSnapshot(ctx);
757
+ requestFooterRender?.();
733
758
  void refreshPromptInjectionEstimate(ctx);
734
759
  await refresh(ctx);
735
760
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@firstpick/pi-extension-git-footer-status",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "Enhanced Pi footer with git status, token usage, context usage, and model telemetry.",
5
5
  "license": "MIT",
6
6
  "keywords": [