@henryqw/pi-footer 0.5.2 → 0.5.4
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 +6 -7
- package/extensions/footer.ts +39 -9
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -27,25 +27,24 @@ pi install npm:@henryqw/pi-footer
|
|
|
27
27
|
| Footer | UI | Show checkout, usage, model, thinking, and extension statuses. |
|
|
28
28
|
|
|
29
29
|
```text
|
|
30
|
-
pi-packages · clear-field-f8d2 · PR #123 · approved
|
|
30
|
+
pi-packages · clear-field-f8d2 · PR #123 · approved Codex #1 · 50% · 7d 1d 1h 22m
|
|
31
31
|
↑ 12.4k · ↓ 2.1k · ↺ 84.3% · ⚡ 87.4 t/s · $ 0.127 · ◔ 36.8% gpt-5.6-luna • high
|
|
32
|
-
|
|
33
|
-
● 🐴 ponytail: ⚡ FULL
|
|
32
|
+
● 🐴 ponytail: ⚡ FULL ◷ 12m 34s
|
|
34
33
|
```
|
|
35
34
|
|
|
36
35
|
First line shows repository, branch, and pull request status (`pi-pr`) after clickable checkout link. Linked-worktree branches drop generated `worktree/` prefix.
|
|
37
36
|
|
|
38
37
|
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.
|
|
39
38
|
|
|
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.
|
|
39
|
+
`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.4 does not yet accept `ultra`, so that footer path remains unreachable until Pi adds it.
|
|
41
40
|
|
|
42
|
-
|
|
41
|
+
Non-empty statuses from `@henryqw` extensions, currently Codex quota, occupy the right side of the first line.
|
|
43
42
|
|
|
44
|
-
|
|
43
|
+
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 blocking user-prompt waits and 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. Statuses from all other extensions, including Ponytail and `pi-rewind`, share the left side, sorted by key with producer text, spacing, colors, links, and glyphs preserved.
|
|
45
44
|
|
|
46
45
|
## Clickable checkout
|
|
47
46
|
|
|
48
|
-
When `pi-open-in.json` command is exactly `code
|
|
47
|
+
When `pi-open-in.json` command is exactly `code` and Pi reports hyperlink support, the accent-colored checkout name is an OSC 8 `vscode://` link to the current path. Other configured commands and terminals with hyperlinks disabled render plain text.
|
|
49
48
|
|
|
50
49
|
Use Pi fullscreen TUI so Pi handles the custom URI:
|
|
51
50
|
|
package/extensions/footer.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { basename, dirname } from "node:path";
|
|
2
2
|
import type { Usage } from "@earendil-works/pi-ai";
|
|
3
3
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import { hyperlink, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
4
|
+
import { getCapabilities, hyperlink, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
5
5
|
import { configuredOpenUri } from "@henryqw/pi-open-in/open-uri";
|
|
6
6
|
|
|
7
7
|
const THINKING_COLORS = {
|
|
@@ -63,6 +63,7 @@ function rainbow(text: string): string {
|
|
|
63
63
|
export default function footerExtension(pi: ExtensionAPI): void {
|
|
64
64
|
let activeMilliseconds = 0;
|
|
65
65
|
let activeStartedAt: number | undefined;
|
|
66
|
+
let promptPaused = false;
|
|
66
67
|
let runtimeTimer: ReturnType<typeof setInterval> | undefined;
|
|
67
68
|
let requestRuntimeRender: (() => void) | undefined;
|
|
68
69
|
const stopRuntimeTimer = () => {
|
|
@@ -72,15 +73,31 @@ export default function footerExtension(pi: ExtensionAPI): void {
|
|
|
72
73
|
};
|
|
73
74
|
|
|
74
75
|
const startActive = () => {
|
|
75
|
-
if (activeStartedAt !== undefined) return;
|
|
76
|
+
if (activeStartedAt !== undefined || promptPaused) return;
|
|
76
77
|
activeStartedAt = performance.now();
|
|
77
78
|
if (requestRuntimeRender) runtimeTimer = setInterval(requestRuntimeRender, 1_000);
|
|
78
79
|
requestRuntimeRender?.();
|
|
79
80
|
};
|
|
80
|
-
const
|
|
81
|
-
if (activeStartedAt === undefined) return
|
|
81
|
+
const pauseActive = () => {
|
|
82
|
+
if (activeStartedAt === undefined) return;
|
|
82
83
|
activeMilliseconds += performance.now() - activeStartedAt;
|
|
83
84
|
activeStartedAt = undefined;
|
|
85
|
+
promptPaused = true;
|
|
86
|
+
stopRuntimeTimer();
|
|
87
|
+
requestRuntimeRender?.();
|
|
88
|
+
};
|
|
89
|
+
const resumeActive = () => {
|
|
90
|
+
if (!promptPaused) return;
|
|
91
|
+
promptPaused = false;
|
|
92
|
+
activeStartedAt = performance.now();
|
|
93
|
+
if (requestRuntimeRender) runtimeTimer = setInterval(requestRuntimeRender, 1_000);
|
|
94
|
+
requestRuntimeRender?.();
|
|
95
|
+
};
|
|
96
|
+
const finalizeActive = (): boolean => {
|
|
97
|
+
if (activeStartedAt === undefined && !promptPaused) return false;
|
|
98
|
+
if (activeStartedAt !== undefined) activeMilliseconds += performance.now() - activeStartedAt;
|
|
99
|
+
activeStartedAt = undefined;
|
|
100
|
+
promptPaused = false;
|
|
84
101
|
stopRuntimeTimer();
|
|
85
102
|
requestRuntimeRender?.();
|
|
86
103
|
return true;
|
|
@@ -89,14 +106,25 @@ export default function footerExtension(pi: ExtensionAPI): void {
|
|
|
89
106
|
pi.on("agent_start", (_event) => {
|
|
90
107
|
startActive();
|
|
91
108
|
});
|
|
109
|
+
pi.on("ui_prompt_start", () => {
|
|
110
|
+
pauseActive();
|
|
111
|
+
});
|
|
112
|
+
pi.on("ui_prompt_end", () => {
|
|
113
|
+
resumeActive();
|
|
114
|
+
});
|
|
92
115
|
pi.on("agent_settled", (_event, ctx) => {
|
|
93
116
|
if (ctx.isIdle() && finalizeActive()) pi.appendEntry(AGENT_TIME_ENTRY, activeMilliseconds);
|
|
94
117
|
});
|
|
95
|
-
pi.on("session_shutdown", () =>
|
|
118
|
+
pi.on("session_shutdown", () => {
|
|
119
|
+
stopRuntimeTimer();
|
|
120
|
+
activeStartedAt = undefined;
|
|
121
|
+
promptPaused = false;
|
|
122
|
+
});
|
|
96
123
|
|
|
97
124
|
pi.on("session_start", async (_event, ctx) => {
|
|
98
125
|
stopRuntimeTimer();
|
|
99
126
|
activeStartedAt = undefined;
|
|
127
|
+
promptPaused = false;
|
|
100
128
|
requestRuntimeRender = undefined;
|
|
101
129
|
// Latest valid entry wins; stored data is untrusted.
|
|
102
130
|
activeMilliseconds = 0;
|
|
@@ -215,14 +243,16 @@ export default function footerExtension(pi: ExtensionAPI): void {
|
|
|
215
243
|
const runtime = theme.fg("dim", `◷ ${formatDuration(elapsed)}`);
|
|
216
244
|
const identity = branch ? theme.fg("dim", `${repo} · `) : "";
|
|
217
245
|
const checkout = branch ?? repo;
|
|
218
|
-
const checkoutLink = openUri
|
|
219
|
-
|
|
246
|
+
const checkoutLink = openUri && getCapabilities().hyperlinks
|
|
247
|
+
? hyperlink(theme.fg("accent", checkout), openUri)
|
|
248
|
+
: theme.fg("dim", checkout);
|
|
249
|
+
const identityLine = prStatus ? `${identity}${checkoutLink} · ${prStatus}` : `${identity}${checkoutLink}`;
|
|
250
|
+
const firstLine = henryStatuses.length ? align(identityLine, henryStatuses.join(" "), width, ellipsis) : identityLine;
|
|
220
251
|
const lines = [
|
|
221
252
|
firstLine,
|
|
222
253
|
align(usage, model, width, ellipsis),
|
|
223
|
-
alignRightReserved(
|
|
254
|
+
alignRightReserved(externalStatuses.join(" "), runtime, width, ellipsis),
|
|
224
255
|
];
|
|
225
|
-
if (externalStatuses.length) lines.push(externalStatuses.join(" "));
|
|
226
256
|
return lines.map((line) => truncateToWidth(line, width, ellipsis));
|
|
227
257
|
},
|
|
228
258
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@henryqw/pi-footer",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
4
4
|
"description": "Show concise repository, branch, and usage details in the Pi footer.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
"pack:check": "npm pack --dry-run"
|
|
25
25
|
},
|
|
26
26
|
"peerDependencies": {
|
|
27
|
-
"@earendil-works/pi-ai": "^0.84.
|
|
28
|
-
"@earendil-works/pi-coding-agent": "^0.84.
|
|
29
|
-
"@earendil-works/pi-tui": "^0.84.
|
|
27
|
+
"@earendil-works/pi-ai": "^0.84.4",
|
|
28
|
+
"@earendil-works/pi-coding-agent": "^0.84.4",
|
|
29
|
+
"@earendil-works/pi-tui": "^0.84.4"
|
|
30
30
|
},
|
|
31
31
|
"repository": {
|
|
32
32
|
"type": "git",
|