@firstpick/pi-extension-git-footer-status 0.2.0 → 0.2.1

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 +76 -27
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -8,7 +8,7 @@ Enhanced Pi footer with git health and model/token telemetry.
8
8
 
9
9
  - Shows compact runtime metrics in the footer:
10
10
  - input/output/cache tokens
11
- - prompt-injection estimate (`PI: X tok`, compacted as `k` for thousands)
11
+ - export-backed initial prompt estimate (`PI: X tok`, same estimator as `/stats-pi`, compacted as `k` for thousands; falls back to live context data if Pi HTML export is unavailable)
12
12
  - live output token counter + token output speed (`tok/s`) measured from assistant streaming lifecycle events, with a session-history fallback
13
13
  - cost + context-window usage
14
14
  - current model and reasoning level
package/index.ts CHANGED
@@ -5,10 +5,10 @@ import type { AssistantMessage } from "@earendil-works/pi-ai";
5
5
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
6
6
  import {
7
7
  collectInitialPromptCalibration,
8
- estimateInitialPromptInput,
8
+ estimateInitialPromptForPiContext,
9
+ estimateInitialPromptFromPiExport,
9
10
  estimateTokensFromCharCount,
10
11
  formatTokens,
11
- type InitialPromptToolInfo,
12
12
  } from "@firstpick/pi-utils";
13
13
  import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
14
14
 
@@ -96,6 +96,11 @@ type LiveTokenSample = {
96
96
  tokens: number;
97
97
  };
98
98
 
99
+ type PromptEstimateCache = {
100
+ key: string;
101
+ tokens: number;
102
+ };
103
+
99
104
  function formatTokenSpeed(tokensPerSecond: number): string {
100
105
  if (tokensPerSecond < 100) {
101
106
  if (tokensPerSecond >= 10) return tokensPerSecond.toFixed(1);
@@ -361,6 +366,57 @@ export default function gitFooterStatus(pi: ExtensionAPI) {
361
366
  let currentAssistantLiveTokenSpeed: number | null = null;
362
367
  let currentAssistantTokenSamples: LiveTokenSample[] = [];
363
368
  let latestMeasuredTokenSpeed: number | null = null;
369
+ let promptEstimateCache: PromptEstimateCache | null = null;
370
+ let promptEstimateRequestId = 0;
371
+ let requestFooterRender: (() => void) | null = null;
372
+
373
+ const getPromptCalibration = (ctx: ExtensionContext) => collectInitialPromptCalibration(ctx.sessionManager.getSessionDir());
374
+
375
+ const getFallbackPromptEstimate = (ctx: ExtensionContext) => {
376
+ const calibration = getPromptCalibration(ctx);
377
+ const estimate = estimateInitialPromptForPiContext(pi, ctx.getSystemPrompt(), calibration);
378
+ const key = [
379
+ estimate.uncalibratedTotal,
380
+ estimate.promptText,
381
+ estimate.toolSchemas,
382
+ estimate.framing,
383
+ estimate.toolCount,
384
+ estimate.calibrationMultiplier,
385
+ estimate.calibrationSamples,
386
+ estimate.low,
387
+ estimate.high,
388
+ ].join(":");
389
+ return { calibration, estimate, key };
390
+ };
391
+
392
+ const refreshPromptInjectionEstimate = async (ctx: ExtensionContext) => {
393
+ const requestId = ++promptEstimateRequestId;
394
+ const fallback = getFallbackPromptEstimate(ctx);
395
+ promptEstimateCache = { key: fallback.key, tokens: fallback.estimate.total };
396
+ requestFooterRender?.();
397
+
398
+ try {
399
+ const result = await estimateInitialPromptFromPiExport(pi, ctx, fallback.calibration);
400
+ const latestFallback = getFallbackPromptEstimate(ctx);
401
+ if (requestId !== promptEstimateRequestId || latestFallback.key !== fallback.key) return;
402
+ promptEstimateCache = { key: fallback.key, tokens: result.estimate.total };
403
+ } catch {
404
+ if (requestId !== promptEstimateRequestId) return;
405
+ const latestFallback = getFallbackPromptEstimate(ctx);
406
+ promptEstimateCache = { key: latestFallback.key, tokens: latestFallback.estimate.total };
407
+ } finally {
408
+ if (requestId === promptEstimateRequestId) requestFooterRender?.();
409
+ }
410
+ };
411
+
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);
417
+ }
418
+ return promptEstimateCache.tokens;
419
+ };
364
420
 
365
421
  const recordAssistantSpeed = (message: AssistantMessage, endMs = Date.now()): boolean => {
366
422
  const outputTokens = message.usage?.output ?? 0;
@@ -419,11 +475,19 @@ export default function gitFooterStatus(pi: ExtensionAPI) {
419
475
  };
420
476
 
421
477
  pi.on("session_start", async (_event, ctx) => {
478
+ promptEstimateCache = null;
479
+ void refreshPromptInjectionEstimate(ctx);
480
+
422
481
  ctx.ui.setFooter((tui, theme, footerData) => {
423
- const unsub = footerData.onBranchChange(() => tui.requestRender());
482
+ const render = () => tui.requestRender();
483
+ requestFooterRender = render;
484
+ const unsub = footerData.onBranchChange(render);
424
485
 
425
486
  return {
426
- dispose: unsub,
487
+ dispose() {
488
+ unsub();
489
+ if (requestFooterRender === render) requestFooterRender = null;
490
+ },
427
491
  invalidate() {},
428
492
  render(width: number): string[] {
429
493
  let totalInput = 0;
@@ -489,28 +553,7 @@ export default function gitFooterStatus(pi: ExtensionAPI) {
489
553
  latestTokenSpeed = historicalTokenSpeed;
490
554
  }
491
555
 
492
- let activeTools: string[] = [];
493
- let allTools: InitialPromptToolInfo[] = [];
494
- try {
495
- activeTools = pi.getActiveTools();
496
- } catch {
497
- activeTools = [];
498
- }
499
- try {
500
- allTools = pi.getAllTools().map((tool) => ({
501
- name: tool.name,
502
- description: tool.description,
503
- parameters: tool.parameters,
504
- }));
505
- } catch {
506
- allTools = [];
507
- }
508
- const promptInjectionTokens = estimateInitialPromptInput({
509
- systemPrompt: ctx.getSystemPrompt(),
510
- activeTools,
511
- allTools,
512
- calibration: collectInitialPromptCalibration(ctx.sessionManager.getSessionDir()),
513
- }).total;
556
+ const currentPromptInjectionTokens = getFooterPromptInjectionTokens(ctx);
514
557
 
515
558
  const contextUsage = ctx.getContextUsage();
516
559
  const contextWindow = contextUsage?.contextWindow ?? ctx.model?.contextWindow ?? 0;
@@ -548,7 +591,7 @@ export default function gitFooterStatus(pi: ExtensionAPI) {
548
591
  const segments: string[] = [];
549
592
  if (ioItems.length > 0) segments.push(`${theme.fg("muted", "🪙")} ${ioItems.join(` ${itemSep} `)}`);
550
593
  if (cacheItems.length > 0) segments.push(`${theme.fg("muted", "💾")} ${cacheItems.join(` ${itemSep} `)}`);
551
- segments.push(`PI: ${formatTokens(promptInjectionTokens)} tok`);
594
+ segments.push(`PI: ${formatTokens(currentPromptInjectionTokens)} tok`);
552
595
  if (latestTokenSpeed !== null) {
553
596
  const livePrefix = liveOutputTokens > 0 ? `${formatTokens(liveOutputTokens)} tok @ ` : "";
554
597
  segments.push(`⚡ ${livePrefix}${formatTokenSpeed(latestTokenSpeed)} tok/s`);
@@ -633,6 +676,10 @@ export default function gitFooterStatus(pi: ExtensionAPI) {
633
676
  await refresh(ctx);
634
677
  });
635
678
 
679
+ pi.on("agent_start", async (_event, ctx) => {
680
+ void refreshPromptInjectionEstimate(ctx);
681
+ });
682
+
636
683
  pi.on("message_start", (event) => {
637
684
  if (event.message.role === "assistant") {
638
685
  currentAssistantStartMs = Date.now();
@@ -683,6 +730,7 @@ export default function gitFooterStatus(pi: ExtensionAPI) {
683
730
  recordAssistantSpeed(event.message as AssistantMessage);
684
731
  resetLiveAssistantState();
685
732
  }
733
+ void refreshPromptInjectionEstimate(ctx);
686
734
  await refresh(ctx);
687
735
  });
688
736
 
@@ -694,6 +742,7 @@ export default function gitFooterStatus(pi: ExtensionAPI) {
694
742
  pi.registerCommand("git-footer-refresh", {
695
743
  description: "Refresh git footer information",
696
744
  handler: async (_args, ctx) => {
745
+ await refreshPromptInjectionEstimate(ctx);
697
746
  await refresh(ctx);
698
747
  ctx.ui.notify("Git footer refreshed", "info");
699
748
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@firstpick/pi-extension-git-footer-status",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Enhanced Pi footer with git status, token usage, context usage, and model telemetry.",
5
5
  "license": "MIT",
6
6
  "keywords": [