@henryqw/pi-footer 2.1.4 → 2.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.
- package/README.md +2 -2
- package/extensions/footer.ts +34 -19
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ pi-harness · clear-field-f8d2 [+2 ~3 ?1 ↑2] · PR #123 · approved Codex #
|
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
- The first line shows the repository, branch, Git state, and `pi-pr` pull request status beside the checkout identity. The `pi-multi-codex` quota status, when non-empty, occupies the right side. Linked-worktree branches drop the generated `worktree/` prefix.
|
|
33
|
-
- The second line shows cumulative input tokens, output tokens, latest cache-hit rate, and tokens per second for the most recent assistant response. The rate is timed from the response's first streamed content to its
|
|
33
|
+
- The second line shows cumulative input tokens, output tokens, latest cache-hit rate, and tokens per second for the most recent assistant response. The rate is timed from the response's first streamed content to its last, so request latency, time to first token, and provider work after the stream ends (such as Claude bridge shutting down Claude Code) are excluded. A response that arrives without streamed content, or all at once, shows `—`. It also shows estimated cost and context usage. Totals include reported tool usage and finished `pi-subagent` background workflows. The active model and thinking level are right-aligned. Under a virtual model, the physical model and thinking level of the latest successful response follow an arrow, for example `auto • high → gpt-5.6-luna • medium`, matching Pi's built-in footer.
|
|
34
34
|
- The third line shows a compact CodeGraph badge first when loaded: `✓ CG` indexed, `● CG` in use, `◐ CG` checking or indexing, `○ CG` missing, `! CG` setup problem, or `? CG` unknown state. Its icon uses the active theme's state color; `CG` remains plain so the badge is readable without color. When pi-codegraph is not loaded, no badge appears. A middle dot separates the CodeGraph badge from other extension statuses; cumulative agent-work time stays on the right, beneath the active model.
|
|
35
35
|
|
|
36
36
|
Git badges appear only when action is needed:
|
|
@@ -58,7 +58,7 @@ Unavailable values render as `—` without a misleading percent sign.
|
|
|
58
58
|
|
|
59
59
|
Only the non-empty `pi-multi-codex` quota status occupies the right side of the first line. The `pi-pr` pull-request status stays beside checkout identity, and CodeGraph appears separately on the third line.
|
|
60
60
|
|
|
61
|
-
Statuses from other extensions, including Ponytail and `pi-rewind`, share the left side of the third line. They are sorted by key; colors, links, glyphs, and interior spacing are preserved. Leading and trailing whitespace is trimmed, line breaks become spaces, and long statuses may be clipped to fit the footer.
|
|
61
|
+
Statuses from other extensions, including Ponytail and `pi-rewind`, share the left side of the third line. They are sorted by key; colors, links, glyphs, and interior spacing are preserved. Leading and trailing whitespace is trimmed, line breaks become spaces, and long statuses may be clipped to fit the footer. Only direct `codegraph_explore` tool calls trigger the `● CG` badge; CodeGraph queries made from inside scripted tools are not shown.
|
|
62
62
|
|
|
63
63
|
## Limits and recovery
|
|
64
64
|
|
package/extensions/footer.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { basename, dirname, join } from "node:path";
|
|
3
|
-
import type { ExtensionAPI, Theme } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { getCapabilities, hyperlink, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
5
5
|
import { configuredOpenUri } from "@henryqw/pi-open-in/open-uri";
|
|
6
6
|
|
|
@@ -15,6 +15,8 @@ const THINKING_COLORS = {
|
|
|
15
15
|
const HENRY_STATUS_KEY = "pi-multi-codex";
|
|
16
16
|
const AGENT_TIME_ENTRY = "pi-footer:agent-work";
|
|
17
17
|
const SUBAGENT_BACKGROUND_RESULT = "subagent-background-result";
|
|
18
|
+
/** Pi's VIRTUAL_MODEL_API: catalog entries that route each request to a physical model. */
|
|
19
|
+
const VIRTUAL_MODEL_API = "pi-virtual";
|
|
18
20
|
|
|
19
21
|
type CountedUsage = { input: number; output: number; cost: { total: number } };
|
|
20
22
|
type GitSummary = { badges: string; detachedOid?: string };
|
|
@@ -167,11 +169,15 @@ function rainbow(text: string): string {
|
|
|
167
169
|
return [...text].map((character, index) => color(character, colors[index % colors.length]!)).join("");
|
|
168
170
|
}
|
|
169
171
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
172
|
+
/** Physical model and thinking level of the latest successful response on the branch; failed and aborted requests are skipped. */
|
|
173
|
+
function latestResponse(ctx: ExtensionContext): { model: string; thinkingLevel?: string } | undefined {
|
|
174
|
+
const branch = ctx.sessionManager.getBranch();
|
|
175
|
+
for (let index = branch.length - 1; index >= 0; index--) {
|
|
176
|
+
const entry = branch[index]!;
|
|
177
|
+
if (entry.type !== "message" || entry.message.role !== "assistant") continue;
|
|
178
|
+
const { stopReason, model, thinkingLevel } = entry.message;
|
|
179
|
+
if (stopReason !== "error" && stopReason !== "aborted") return { model, thinkingLevel };
|
|
180
|
+
}
|
|
175
181
|
}
|
|
176
182
|
|
|
177
183
|
function codegraphBadge(status: string, inUse: boolean, theme: Theme): string {
|
|
@@ -196,7 +202,7 @@ export default function footerExtension(pi: ExtensionAPI): void {
|
|
|
196
202
|
let refreshGitStatus: (() => Promise<void>) | undefined;
|
|
197
203
|
const activeCodegraphCalls = new Set<string>();
|
|
198
204
|
pi.on("tool_execution_start", (event) => {
|
|
199
|
-
if (
|
|
205
|
+
if (event.toolName !== "codegraph_explore") return;
|
|
200
206
|
activeCodegraphCalls.add(event.toolCallId);
|
|
201
207
|
requestRuntimeRender?.();
|
|
202
208
|
});
|
|
@@ -304,21 +310,26 @@ export default function footerExtension(pi: ExtensionAPI): void {
|
|
|
304
310
|
}
|
|
305
311
|
|
|
306
312
|
let tps: number | undefined;
|
|
307
|
-
//
|
|
313
|
+
// Measure first to last streamed update: providers emit message_start before the first token,
|
|
314
|
+
// and some (Claude bridge) emit message_end only after post-stream teardown.
|
|
308
315
|
let assistantStartedAt: number | undefined;
|
|
316
|
+
let assistantLastUpdateAt: number | undefined;
|
|
309
317
|
pi.on("message_update", async (event) => {
|
|
310
|
-
if (event.message.role
|
|
318
|
+
if (event.message.role !== "assistant") return;
|
|
319
|
+
assistantLastUpdateAt = performance.now();
|
|
320
|
+
assistantStartedAt ??= assistantLastUpdateAt;
|
|
311
321
|
});
|
|
312
322
|
pi.on("message_end", async (event) => {
|
|
313
323
|
if (event.message.role !== "assistant") return;
|
|
314
324
|
const output = event.message.usage?.output ?? 0;
|
|
315
|
-
const seconds = assistantStartedAt === undefined ? 0 : (
|
|
316
|
-
assistantStartedAt = undefined;
|
|
325
|
+
const seconds = assistantStartedAt === undefined ? 0 : (assistantLastUpdateAt! - assistantStartedAt) / 1000;
|
|
326
|
+
assistantStartedAt = assistantLastUpdateAt = undefined;
|
|
317
327
|
tps = seconds > 0 ? output / seconds : undefined;
|
|
318
328
|
});
|
|
319
329
|
|
|
320
|
-
// ponytail: keyed on length + last entry (sessions are append-only); revisit if entries ever mutate in place.
|
|
330
|
+
// ponytail: keyed on length + last entry (sessions are append-only) + leaf (tree navigation); revisit if entries ever mutate in place.
|
|
321
331
|
let usageKey: string | undefined;
|
|
332
|
+
let routed: ReturnType<typeof latestResponse>;
|
|
322
333
|
let input = 0;
|
|
323
334
|
let output = 0;
|
|
324
335
|
let cost = 0;
|
|
@@ -366,10 +377,11 @@ export default function footerExtension(pi: ExtensionAPI): void {
|
|
|
366
377
|
invalidate() { },
|
|
367
378
|
render(width: number): string[] {
|
|
368
379
|
const entries = ctx.sessionManager.getEntries();
|
|
369
|
-
const key = `${entries.length}:${entries.at(-1)?.type}`;
|
|
380
|
+
const key = `${entries.length}:${entries.at(-1)?.type}:${ctx.sessionManager.getLeafId()}`;
|
|
370
381
|
if (key !== usageKey) {
|
|
371
382
|
usageKey = key;
|
|
372
383
|
computeUsage();
|
|
384
|
+
routed = latestResponse(ctx);
|
|
373
385
|
}
|
|
374
386
|
|
|
375
387
|
const reportedBranch = data.getGitBranch()?.replace(/^worktree\//, "");
|
|
@@ -388,8 +400,6 @@ export default function footerExtension(pi: ExtensionAPI): void {
|
|
|
388
400
|
if (!text) continue;
|
|
389
401
|
(key === HENRY_STATUS_KEY ? henryStatuses : externalStatuses).push(text);
|
|
390
402
|
}
|
|
391
|
-
const thinking = String(ctx.thinkingLevel ?? "off");
|
|
392
|
-
const thinkingColor = THINKING_COLORS[thinking as keyof typeof THINKING_COLORS];
|
|
393
403
|
const ellipsis = theme.fg("dim", "…");
|
|
394
404
|
const usage = theme.fg("dim", [
|
|
395
405
|
`↑ ${formatTokens(input)}`,
|
|
@@ -399,10 +409,15 @@ export default function footerExtension(pi: ExtensionAPI): void {
|
|
|
399
409
|
`$ ${cost.toFixed(3)}`,
|
|
400
410
|
`◔ ${context == null ? "—" : `${context.toFixed(1)}%`}`,
|
|
401
411
|
].join(" · "));
|
|
402
|
-
const thinkingText =
|
|
403
|
-
|
|
404
|
-
: thinkingColor === undefined ? theme.fg("dim",
|
|
405
|
-
|
|
412
|
+
const thinkingText = (level: string) => {
|
|
413
|
+
const thinkingColor = THINKING_COLORS[level as keyof typeof THINKING_COLORS];
|
|
414
|
+
return level === "ultra" ? rainbow(level) : thinkingColor === undefined ? theme.fg("dim", level) : color(level, thinkingColor);
|
|
415
|
+
};
|
|
416
|
+
const selected = theme.fg("dim", `${ctx.model?.id ?? "no-model"} • `) + thinkingText(String(ctx.thinkingLevel ?? "off"));
|
|
417
|
+
// Under a virtual selection, show where the latest request went, as Pi's own footer does.
|
|
418
|
+
const model = ctx.model?.api === VIRTUAL_MODEL_API && routed
|
|
419
|
+
? selected + theme.fg("dim", ` → ${routed.model}`) + (routed.thinkingLevel ? theme.fg("dim", " • ") + thinkingText(routed.thinkingLevel) : "")
|
|
420
|
+
: selected;
|
|
406
421
|
const elapsed = activeMilliseconds + (activeStartedAt === undefined ? 0 : performance.now() - activeStartedAt);
|
|
407
422
|
const runtime = theme.fg("dim", `◷ ${formatDuration(elapsed)}`);
|
|
408
423
|
const identity = branch ? theme.fg("dim", `${repo} · `) : "";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@henryqw/pi-footer",
|
|
3
|
-
"version": "2.1
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"description": "Show concise repository, branch, and usage details in the Pi footer.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
"pack:check": "npm pack --dry-run"
|
|
25
25
|
},
|
|
26
26
|
"peerDependencies": {
|
|
27
|
-
"@earendil-works/pi-coding-agent": ">=0.86.0 <0.
|
|
28
|
-
"@earendil-works/pi-tui": ">=0.86.0 <0.
|
|
27
|
+
"@earendil-works/pi-coding-agent": ">=0.86.0 <0.100.0",
|
|
28
|
+
"@earendil-works/pi-tui": ">=0.86.0 <0.100.0"
|
|
29
29
|
},
|
|
30
30
|
"repository": {
|
|
31
31
|
"type": "git",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"@henryqw/pi-open-in": "^3.0.0"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
52
|
-
"@earendil-works/pi-tui": "0.
|
|
51
|
+
"@earendil-works/pi-coding-agent": "0.99.1",
|
|
52
|
+
"@earendil-works/pi-tui": "0.99.1"
|
|
53
53
|
}
|
|
54
54
|
}
|