@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 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
- Codex #1 · 50% · 7d 1d 1h 22m ◷ 12m 34s
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.2 does not yet accept `ultra`, so that footer path remains unreachable until Pi adds it.
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
- 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.
41
+ Non-empty statuses from `@henryqw` extensions, currently Codex quota, occupy the right side of the first line.
43
42
 
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
+ 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`, the accent-colored checkout name is an OSC 8 `vscode://` link to the current path. Other configured commands remain plain because terminal links cannot run arbitrary shell commands.
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
 
@@ -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 finalizeActive = (): boolean => {
81
- if (activeStartedAt === undefined) return false;
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", () => stopRuntimeTimer());
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 ? hyperlink(theme.fg("accent", checkout), openUri) : theme.fg("dim", checkout);
219
- const firstLine = prStatus ? `${identity}${checkoutLink} · ${prStatus}` : `${identity}${checkoutLink}`;
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(henryStatuses.join(" "), runtime, width, ellipsis),
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.2",
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.2",
28
- "@earendil-works/pi-coding-agent": "^0.84.2",
29
- "@earendil-works/pi-tui": "^0.84.2"
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",