@henryqw/pi-footer 0.4.1 → 0.5.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.
- package/README.md +4 -2
- package/extensions/footer.ts +76 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ pi install npm:@henryqw/pi-footer
|
|
|
29
29
|
```text
|
|
30
30
|
pi-packages · clear-field-f8d2 · PR #123 · approved
|
|
31
31
|
↑ 12.4k · ↓ 2.1k · ↺ 84.3% · ⚡ 87.4 t/s · $ 0.127 · ◔ 36.8% gpt-5.6-luna • high
|
|
32
|
-
Codex #1 · 50% · 7d 1d 1h 22m
|
|
32
|
+
Codex #1 · 50% · 7d 1d 1h 22m ◷ 12m 34s
|
|
33
33
|
● 🐴 ponytail: ⚡ FULL
|
|
34
34
|
```
|
|
35
35
|
|
|
@@ -39,7 +39,9 @@ Second line shows cumulative input tokens, output tokens, latest cache-hit rate,
|
|
|
39
39
|
|
|
40
40
|
`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.
|
|
41
41
|
|
|
42
|
-
Third line
|
|
42
|
+
Third line shows cumulative agent-work time right-aligned beneath the model. It counts each run from `agent_start` through the final idle `agent_settled`, including automatic retries and auto-compaction inside that run, and excludes idle waits between runs. Standalone `/compact` is excluded because it runs outside the agent-run lifecycle and emits no `agent_start`. The cumulative total is persisted in the session via a `pi-footer:agent-work` custom entry after each finalized run and restored on session resume. Non-empty statuses from `@henryqw` extensions, currently Codex quota, share the left side.
|
|
43
|
+
|
|
44
|
+
Fourth line renders statuses from all other extensions, including Ponytail and `pi-rewind`. Statuses are sorted by key; producer text, spacing, colors, links, and glyphs are preserved.
|
|
43
45
|
|
|
44
46
|
## Clickable checkout
|
|
45
47
|
|
package/extensions/footer.ts
CHANGED
|
@@ -12,7 +12,12 @@ const THINKING_COLORS = {
|
|
|
12
12
|
xhigh: 208,
|
|
13
13
|
max: 196,
|
|
14
14
|
} as const;
|
|
15
|
-
const
|
|
15
|
+
const HENRY_STATUS_KEY = "pi-multi-codex";
|
|
16
|
+
const AGENT_TIME_ENTRY = "pi-footer:agent-work";
|
|
17
|
+
|
|
18
|
+
function isValidMilliseconds(value: unknown): value is number {
|
|
19
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
20
|
+
}
|
|
16
21
|
|
|
17
22
|
function formatTokens(count: number): string {
|
|
18
23
|
if (count < 1_000) return `${count}`;
|
|
@@ -20,6 +25,14 @@ function formatTokens(count: number): string {
|
|
|
20
25
|
return `${(count / 1_000_000).toFixed(1)}M`;
|
|
21
26
|
}
|
|
22
27
|
|
|
28
|
+
function formatDuration(milliseconds: number): string {
|
|
29
|
+
const totalSeconds = Math.floor(milliseconds / 1_000);
|
|
30
|
+
const hours = Math.floor(totalSeconds / 3_600);
|
|
31
|
+
const minutes = Math.floor(totalSeconds % 3_600 / 60);
|
|
32
|
+
const seconds = totalSeconds % 60;
|
|
33
|
+
return hours ? `${hours}h ${minutes}m ${seconds}s` : minutes ? `${minutes}m ${seconds}s` : `${seconds}s`;
|
|
34
|
+
}
|
|
35
|
+
|
|
23
36
|
function sanitizeStatus(text: string): string {
|
|
24
37
|
return text.replace(/[\r\n]+/g, " ").trim();
|
|
25
38
|
}
|
|
@@ -31,6 +44,13 @@ function align(left: string, right: string, width: number, ellipsis: string): st
|
|
|
31
44
|
return left + " ".repeat(width - visibleWidth(left) - visibleWidth(clippedRight)) + clippedRight;
|
|
32
45
|
}
|
|
33
46
|
|
|
47
|
+
// Mirrors align but reserves the right side (runtime) and truncates the left status first.
|
|
48
|
+
function alignRightReserved(left: string, right: string, width: number, ellipsis: string): string {
|
|
49
|
+
const available = Math.max(width - visibleWidth(right) - 2, 0);
|
|
50
|
+
const clippedLeft = truncateToWidth(left, available, ellipsis);
|
|
51
|
+
return clippedLeft + " ".repeat(Math.max(width - visibleWidth(clippedLeft) - visibleWidth(right), 0)) + truncateToWidth(right, width, "");
|
|
52
|
+
}
|
|
53
|
+
|
|
34
54
|
function color(text: string, ansi256: number): string {
|
|
35
55
|
return `\x1b[38;5;${ansi256}m${text}\x1b[39m`;
|
|
36
56
|
}
|
|
@@ -41,7 +61,50 @@ function rainbow(text: string): string {
|
|
|
41
61
|
}
|
|
42
62
|
|
|
43
63
|
export default function footerExtension(pi: ExtensionAPI): void {
|
|
64
|
+
let activeMilliseconds = 0;
|
|
65
|
+
let activeStartedAt: number | undefined;
|
|
66
|
+
let runtimeTimer: ReturnType<typeof setInterval> | undefined;
|
|
67
|
+
let requestRuntimeRender: (() => void) | undefined;
|
|
68
|
+
const stopRuntimeTimer = () => {
|
|
69
|
+
if (runtimeTimer === undefined) return;
|
|
70
|
+
clearInterval(runtimeTimer);
|
|
71
|
+
runtimeTimer = undefined;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const startActive = () => {
|
|
75
|
+
if (activeStartedAt !== undefined) return;
|
|
76
|
+
activeStartedAt = performance.now();
|
|
77
|
+
if (requestRuntimeRender) runtimeTimer = setInterval(requestRuntimeRender, 1_000);
|
|
78
|
+
requestRuntimeRender?.();
|
|
79
|
+
};
|
|
80
|
+
const finalizeActive = (): boolean => {
|
|
81
|
+
if (activeStartedAt === undefined) return false;
|
|
82
|
+
activeMilliseconds += performance.now() - activeStartedAt;
|
|
83
|
+
activeStartedAt = undefined;
|
|
84
|
+
stopRuntimeTimer();
|
|
85
|
+
requestRuntimeRender?.();
|
|
86
|
+
return true;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
pi.on("agent_start", (_event) => {
|
|
90
|
+
startActive();
|
|
91
|
+
});
|
|
92
|
+
pi.on("agent_settled", (_event, ctx) => {
|
|
93
|
+
if (ctx.isIdle() && finalizeActive()) pi.appendEntry(AGENT_TIME_ENTRY, activeMilliseconds);
|
|
94
|
+
});
|
|
95
|
+
pi.on("session_shutdown", () => stopRuntimeTimer());
|
|
96
|
+
|
|
44
97
|
pi.on("session_start", async (_event, ctx) => {
|
|
98
|
+
stopRuntimeTimer();
|
|
99
|
+
activeStartedAt = undefined;
|
|
100
|
+
requestRuntimeRender = undefined;
|
|
101
|
+
// Latest valid entry wins; stored data is untrusted.
|
|
102
|
+
activeMilliseconds = 0;
|
|
103
|
+
for (const entry of ctx.sessionManager.getEntries()) {
|
|
104
|
+
if (entry.type === "custom" && entry.customType === AGENT_TIME_ENTRY && isValidMilliseconds(entry.data)) {
|
|
105
|
+
activeMilliseconds = entry.data;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
45
108
|
if (ctx.mode !== "tui") return;
|
|
46
109
|
|
|
47
110
|
const git = await pi.exec(
|
|
@@ -100,9 +163,14 @@ export default function footerExtension(pi: ExtensionAPI): void {
|
|
|
100
163
|
};
|
|
101
164
|
|
|
102
165
|
ctx.ui.setFooter((tui, theme, data) => {
|
|
103
|
-
|
|
166
|
+
requestRuntimeRender = () => tui.requestRender();
|
|
167
|
+
const unsubscribe = data.onBranchChange(requestRuntimeRender);
|
|
104
168
|
return {
|
|
105
|
-
dispose
|
|
169
|
+
dispose() {
|
|
170
|
+
unsubscribe();
|
|
171
|
+
requestRuntimeRender = undefined;
|
|
172
|
+
stopRuntimeTimer();
|
|
173
|
+
},
|
|
106
174
|
invalidate() { },
|
|
107
175
|
render(width: number): string[] {
|
|
108
176
|
const entries = ctx.sessionManager.getEntries();
|
|
@@ -123,10 +191,10 @@ export default function footerExtension(pi: ExtensionAPI): void {
|
|
|
123
191
|
.map(([key, text]) => [key, sanitizeStatus(text)] as const)
|
|
124
192
|
.filter(([, text]) => Boolean(text));
|
|
125
193
|
const henryStatuses = statuses
|
|
126
|
-
.filter(([key]) =>
|
|
194
|
+
.filter(([key]) => key === HENRY_STATUS_KEY)
|
|
127
195
|
.map(([, text]) => text);
|
|
128
196
|
const externalStatuses = statuses
|
|
129
|
-
.filter(([key]) =>
|
|
197
|
+
.filter(([key]) => key !== HENRY_STATUS_KEY)
|
|
130
198
|
.map(([, text]) => text);
|
|
131
199
|
const thinking = String(ctx.thinkingLevel ?? "off");
|
|
132
200
|
const thinkingColor = THINKING_COLORS[thinking as keyof typeof THINKING_COLORS];
|
|
@@ -143,6 +211,8 @@ export default function footerExtension(pi: ExtensionAPI): void {
|
|
|
143
211
|
? rainbow(thinking)
|
|
144
212
|
: thinkingColor === undefined ? theme.fg("dim", thinking) : color(thinking, thinkingColor);
|
|
145
213
|
const model = theme.fg("dim", `${ctx.model?.id ?? "no-model"} • `) + thinkingText;
|
|
214
|
+
const elapsed = activeMilliseconds + (activeStartedAt === undefined ? 0 : performance.now() - activeStartedAt);
|
|
215
|
+
const runtime = theme.fg("dim", `◷ ${formatDuration(elapsed)}`);
|
|
146
216
|
const identity = branch ? theme.fg("dim", `${repo} · `) : "";
|
|
147
217
|
const checkout = branch ?? repo;
|
|
148
218
|
const checkoutLink = openUri ? hyperlink(theme.fg("accent", checkout), openUri) : theme.fg("dim", checkout);
|
|
@@ -150,8 +220,8 @@ export default function footerExtension(pi: ExtensionAPI): void {
|
|
|
150
220
|
const lines = [
|
|
151
221
|
firstLine,
|
|
152
222
|
align(usage, model, width, ellipsis),
|
|
223
|
+
alignRightReserved(henryStatuses.join(" "), runtime, width, ellipsis),
|
|
153
224
|
];
|
|
154
|
-
if (henryStatuses.length || externalStatuses.length) lines.push(henryStatuses.join(" "));
|
|
155
225
|
if (externalStatuses.length) lines.push(externalStatuses.join(" "));
|
|
156
226
|
return lines.map((line) => truncateToWidth(line, width, ellipsis));
|
|
157
227
|
},
|