@henryqw/pi-footer 0.2.1 → 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.
package/README.md CHANGED
@@ -23,13 +23,13 @@ pi install npm:@henryqw/pi-footer
23
23
 
24
24
  ```text
25
25
  pi-packages · clear-field-f8d2 · PR #123 · approved
26
- ↑ 12.4k · ↓ 2.1k · ↺ 84.3% · $ 0.127 · ◔ 36.8% gpt-5.6-luna • high
26
+ ↑ 12.4k · ↓ 2.1k · ↺ 84.3% · ⚡ 87.4 t/s · $ 0.127 · ◔ 36.8% gpt-5.6-luna • high
27
27
  Codex #1 · 50% · 7d 1d 1h 22m
28
28
  ```
29
29
 
30
30
  First line shows repository, branch, and pull request status (`pi-pr`) after clickable checkout link. Linked-worktree branches drop generated `worktree/` prefix.
31
31
 
32
- Second line shows cumulative input tokens, output tokens, latest cache-hit rate, estimated cost, and context usage. Unavailable values render as `—` without a misleading percent sign. Active model and thinking level are right-aligned.
32
+ Second line shows cumulative input tokens, output tokens, latest cache-hit rate, tokens per second of the most recent assistant response, estimated cost, and context usage. Unavailable values render as `—` without a misleading percent sign. Active model and thinking level are right-aligned.
33
33
 
34
34
  `off` uses the same dim grey as the model name. Active levels use an ANSI-256 gradient: green `minimal`, yellow-green `low`, lime `medium`, yellow `high`, orange `xhigh`, and red `max`. `ultra` renders as a rainbow when the runtime supplies it. Pi 0.84.2 does not yet accept `ultra`, so that footer path remains unreachable until Pi adds it.
35
35
 
@@ -53,34 +53,62 @@ export default function footerExtension(pi: ExtensionAPI): void {
53
53
  const commonName = commonDir && basename(commonDir) === ".git" ? basename(dirname(commonDir)) : undefined;
54
54
  const repo = git.code === 0 ? commonName && commonName !== rootName ? commonName : rootName : basename(ctx.cwd);
55
55
 
56
+ let tps: number | undefined;
57
+ let assistantStartedAt: number | undefined;
58
+ pi.on("message_start", async (event) => {
59
+ if (event.message.role === "assistant") assistantStartedAt = performance.now();
60
+ });
61
+ pi.on("message_end", async (event) => {
62
+ if (event.message.role !== "assistant") return;
63
+ const output = event.message.usage?.output ?? 0;
64
+ const seconds = assistantStartedAt === undefined ? 0 : (performance.now() - assistantStartedAt) / 1000;
65
+ assistantStartedAt = undefined;
66
+ tps = seconds > 0 ? output / seconds : undefined;
67
+ });
68
+
69
+ // ponytail: keyed on length + last entry (sessions are append-only); revisit if entries ever mutate in place.
70
+ let usageKey: string | undefined;
71
+ let input = 0;
72
+ let output = 0;
73
+ let cost = 0;
74
+ let cacheRate: number | undefined;
75
+ const computeUsage = () => {
76
+ input = 0;
77
+ output = 0;
78
+ cost = 0;
79
+ cacheRate = undefined;
80
+ const add = (usage: Usage | undefined) => {
81
+ if (!usage) return;
82
+ input += usage.input;
83
+ output += usage.output;
84
+ cost += usage.cost.total;
85
+ };
86
+
87
+ for (const entry of ctx.sessionManager.getEntries()) {
88
+ if (entry.type === "message" && entry.message.role === "assistant") {
89
+ const usage = entry.message.usage;
90
+ const prompt = usage.input + usage.cacheRead + usage.cacheWrite;
91
+ cacheRate = prompt ? usage.cacheRead / prompt * 100 : 0;
92
+ add(usage);
93
+ } else if (entry.type === "message" && entry.message.role === "toolResult") {
94
+ add(entry.message.usage);
95
+ } else if (entry.type === "branch_summary" || entry.type === "compaction") {
96
+ add(entry.usage);
97
+ }
98
+ }
99
+ };
100
+
56
101
  ctx.ui.setFooter((tui, theme, data) => {
57
102
  const unsubscribe = data.onBranchChange(() => tui.requestRender());
58
103
  return {
59
104
  dispose: unsubscribe,
60
105
  invalidate() { },
61
106
  render(width: number): string[] {
62
- let input = 0;
63
- let output = 0;
64
- let cost = 0;
65
- let cacheRate: number | undefined;
66
- const add = (usage: Usage | undefined) => {
67
- if (!usage) return;
68
- input += usage.input;
69
- output += usage.output;
70
- cost += usage.cost.total;
71
- };
72
-
73
- for (const entry of ctx.sessionManager.getEntries()) {
74
- if (entry.type === "message" && entry.message.role === "assistant") {
75
- const usage = entry.message.usage;
76
- const prompt = usage.input + usage.cacheRead + usage.cacheWrite;
77
- cacheRate = prompt ? usage.cacheRead / prompt * 100 : 0;
78
- add(usage);
79
- } else if (entry.type === "message" && entry.message.role === "toolResult") {
80
- add(entry.message.usage);
81
- } else if (entry.type === "branch_summary" || entry.type === "compaction") {
82
- add(entry.usage);
83
- }
107
+ const entries = ctx.sessionManager.getEntries();
108
+ const key = `${entries.length}:${entries.at(-1)?.type}`;
109
+ if (key !== usageKey) {
110
+ usageKey = key;
111
+ computeUsage();
84
112
  }
85
113
 
86
114
  const branch = data.getGitBranch()?.replace(/^worktree\//, "");
@@ -100,7 +128,8 @@ export default function footerExtension(pi: ExtensionAPI): void {
100
128
  `↑ ${formatTokens(input)}`,
101
129
  `↓ ${formatTokens(output)}`,
102
130
  `↺ ${cacheRate === undefined ? "—" : `${cacheRate.toFixed(1)}%`}`,
103
- `$ ${cost.toFixed(3)}`,
131
+ `⚡ ${tps === undefined ? "—" : `${tps.toFixed(1)} t/s`}`,
132
+ `$ ${cost.toFixed(3)}`,
104
133
  `◔ ${context == null ? "—" : `${context.toFixed(1)}%`}`,
105
134
  ].join(" · "));
106
135
  const thinkingText = thinking === "ultra"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henryqw/pi-footer",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Show concise repository, branch, and usage details in the Pi footer.",
5
5
  "keywords": [
6
6
  "pi-package",