pi-metrics 0.2.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 ADDED
@@ -0,0 +1,26 @@
1
+ # pi-hud
2
+
3
+ Heads-up session timing for the [Pi coding agent](https://github.com/earendil-works/pi): a live elapsed timer in the working spinner plus per-turn and total run summaries in the chat flow.
4
+
5
+ [中文文档](./README.zh-CN.md)
6
+
7
+ ## Features
8
+
9
+ - While the agent is working, the spinner shows the **total elapsed time since you sent the message** (for example `⏱ 47s`). It keeps counting across turns instead of resetting per turn.
10
+ - When each turn ends, a dim line shows that turn's precise duration (`⏱ Turn elapsed 8.2s`).
11
+ - When the agent fully settles (`agent_settled` — including auto-retries, compaction continuations, or Esc interruption), a final line shows the **total elapsed time from message send to stop** (`⏱ Total elapsed 18.9s`).
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ pi install npm:pi-hud
17
+ ```
18
+
19
+ ## How it works
20
+
21
+ - The total timer starts on the `input` event (the moment you submit a message) and ends on `agent_settled`, so multi-turn tool calls, automatic retries, and queued continuations are all covered. Steer/follow-up messages sent mid-run do not reset the start point.
22
+ - In non-TUI mode (rpc/print) the timer and notifications are disabled.
23
+
24
+ ## Localization
25
+
26
+ All user-facing text is provided in `zh-CN` and `en-US` through `pi-extensions-i18n`.
@@ -0,0 +1,26 @@
1
+ # pi-hud
2
+
3
+ 面向 [Pi coding agent](https://github.com/earendil-works/pi) 的会话耗时抬头显示:working spinner 实时计时,并在对话流中给出每轮耗时与全程总耗时。
4
+
5
+ [English](./README.md)
6
+
7
+ ## 功能
8
+
9
+ - 工作期间 spinner 显示**从发出消息起的全程耗时**(如 `⏱ 47s`),跨轮次持续累加,不再每轮回零。
10
+ - 每个轮次结束时插入一条灰色文本,显示该轮精确耗时(`⏱ 本轮耗时 8.2s`)。
11
+ - AI 完全停止时(`agent_settled`,覆盖自动重试、compaction 续跑以及 Esc 中断)追加一行**从发出消息到停止的总耗时**(`⏱ 总耗时 18.9s`)。
12
+
13
+ ## 安装
14
+
15
+ ```bash
16
+ pi install npm:pi-hud
17
+ ```
18
+
19
+ ## 实现说明
20
+
21
+ - 总耗时以 `input` 事件(用户提交消息的时刻)为起点、`agent_settled` 为终点,因此多轮工具调用、自动重试和队列续跑都计入同一次总耗时;运行中发送的 steer/followUp 消息不会重置起点。
22
+ - 非 TUI 模式(rpc/print)下不启动定时器、不发送通知。
23
+
24
+ ## 国际化
25
+
26
+ 所有面向用户的文案均通过 `pi-extensions-i18n` 提供 `zh-CN` 和 `en-US` 双语。
package/index.ts ADDED
@@ -0,0 +1 @@
1
+ export { default } from "./src/index.ts";
@@ -0,0 +1,14 @@
1
+ {
2
+ "elapsedWorking": {
3
+ "zh-CN": "⏱ {value}",
4
+ "en-US": "⏱ {value}"
5
+ },
6
+ "elapsedDone": {
7
+ "zh-CN": "⏱ 本轮耗时 {value}",
8
+ "en-US": "⏱ Turn elapsed {value}"
9
+ },
10
+ "elapsedTotal": {
11
+ "zh-CN": "⏱ 总耗时 {value}",
12
+ "en-US": "⏱ Total elapsed {value}"
13
+ }
14
+ }
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "pi-metrics",
3
+ "version": "0.2.0",
4
+ "description": "Pi metrics extension: live session elapsed timer with per-turn and total run summaries",
5
+ "type": "module",
6
+ "main": "./index.ts",
7
+ "exports": {
8
+ ".": "./index.ts"
9
+ },
10
+ "files": [
11
+ "index.ts",
12
+ "src",
13
+ "locales",
14
+ "README.md",
15
+ "README.zh-CN.md"
16
+ ],
17
+ "scripts": {
18
+ "test": "tsx --test tests/pi-metrics.test.ts",
19
+ "typecheck": "tsc --noEmit --pretty false",
20
+ "build": "npm run typecheck",
21
+ "check": "npm run typecheck && npm test && npm pack --dry-run --json > /dev/null"
22
+ },
23
+ "pi": {
24
+ "extensions": [
25
+ "./index.ts"
26
+ ]
27
+ },
28
+ "engines": {
29
+ "node": ">=22"
30
+ },
31
+ "license": "MIT",
32
+ "author": "maplezzk",
33
+ "homepage": "https://github.com/maplezzk/pi-extensions/tree/main/packages/pi-metrics",
34
+ "bugs": "https://github.com/maplezzk/pi-extensions/issues",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/maplezzk/pi-extensions.git",
38
+ "directory": "packages/pi-metrics"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public",
42
+ "registry": "https://registry.npmjs.org"
43
+ },
44
+ "keywords": [
45
+ "pi-package",
46
+ "pi",
47
+ "pi-extension",
48
+ "coding-agent",
49
+ "hud",
50
+ "elapsed-time"
51
+ ],
52
+ "peerDependencies": {
53
+ "@earendil-works/pi-ai": ">=0.80.0 <0.81.0",
54
+ "@earendil-works/pi-coding-agent": ">=0.80.0 <0.81.0",
55
+ "@earendil-works/pi-tui": ">=0.80.0 <0.81.0",
56
+ "pi-extensions-i18n": "^0.3.0"
57
+ },
58
+ "devDependencies": {
59
+ "@earendil-works/pi-ai": "0.80.10",
60
+ "@earendil-works/pi-coding-agent": "0.80.10",
61
+ "@earendil-works/pi-tui": "0.80.10",
62
+ "@types/node": "24.12.4",
63
+ "tsx": "4.23.1",
64
+ "typescript": "5.9.3"
65
+ }
66
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * 耗时格式化纯函数
3
+ *
4
+ * 抽离为独立模块便于单元测试(仓库门禁要求测试确定性、不依赖 Pi 运行时)。
5
+ */
6
+
7
+ /** working 期间:紧凑格式(秒级 / 分秒) */
8
+ export function formatTick(ms: number): string {
9
+ const totalSec = Math.floor(ms / 1000);
10
+ if (totalSec < 60) return `${totalSec}s`;
11
+ const m = Math.floor(totalSec / 60);
12
+ const s = totalSec % 60;
13
+ return s === 0 ? `${m}m` : `${m}m ${s}s`;
14
+ }
15
+
16
+ /** turn_end / agent_settled:精确格式(1 位小数) */
17
+ export function formatDone(ms: number): string {
18
+ const sec = ms / 1000;
19
+ if (sec < 60) return `${sec.toFixed(1)}s`;
20
+ const m = Math.floor(sec / 60);
21
+ const s = sec - m * 60;
22
+ return `${m}m ${s.toFixed(1)}s`;
23
+ }
package/src/i18n.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { createTranslator, loadCatalog } from "pi-extensions-i18n";
2
+
3
+ export const i18n = createTranslator(loadCatalog(new URL("../locales/index.json", import.meta.url)));
package/src/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import turnElapsed from "./turn-elapsed.ts";
3
+
4
+ export default function piHud(pi: ExtensionAPI): void {
5
+ // 后续 HUD 类功能(如 token 用量、上下文水位等)在此注册
6
+ turnElapsed(pi);
7
+ }
8
+
9
+ export { default as turnElapsed } from "./turn-elapsed.ts";
10
+ export * from "./format-utils.ts";
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Pi turn 耗时显示插件
3
+ *
4
+ * - working 期间:实时更新 spinner 文字,显示从用户发出消息起的全程耗时(如 "⏱ 47s"),
5
+ * 跨轮不归零 —— 用户等待时最关心的是"一共等了多久"
6
+ * - turn 结束时:在 chat 流插入一条 dim 灰文本,提示本轮耗时(如 "✓ 本轮耗时 12.3s")
7
+ * - agent 完全停止时(agent_settled):再插入一条总耗时,覆盖从用户发出消息到 AI 停止
8
+ * 的整段过程(跨越多轮工具调用、自动重试和 compaction 续跑)
9
+ *
10
+ * 设计取舍:
11
+ * - 用 setWorkingMessage 改 spinner 文字会覆盖 pi 默认的 "Working... (Esc to interrupt)"。
12
+ * 为了让耗时最显眼,接受这个 trade-off —— 用户更关心"等了多久"而非"怎么中断"。
13
+ * - 总耗时起点用 input 事件(用户真正发出消息的时刻),而不是 agent_start(略晚)。
14
+ * 运行中收到的 steer/followUp 消息不重置起点:整段连续工作计入同一次总耗时。
15
+ * - 总耗时终点用 agent_settled 而不是 agent_end:agent_end 之后还可能发生自动重试、
16
+ * compaction 和队列续跑,agent_settled 才表示 AI 真正停下(Esc 中断也会在 finally 中触发)。
17
+ * - 非 TUI 模式(rpc / print)下 hasUI 为 false,不启动定时器、不发 notify。
18
+ */
19
+
20
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
21
+ import { formatDone, formatTick } from "./format-utils.ts";
22
+ import { i18n } from "./i18n.ts";
23
+
24
+ const TICK_MS = 1000;
25
+
26
+ export default function (pi: ExtensionAPI) {
27
+ let turnStartTime = 0;
28
+ let runStartTime = 0;
29
+ let tickHandle: ReturnType<typeof setInterval> | null = null;
30
+
31
+ const stopTick = () => {
32
+ if (tickHandle !== null) {
33
+ clearInterval(tickHandle);
34
+ tickHandle = null;
35
+ }
36
+ };
37
+
38
+ pi.on("input", async (event) => {
39
+ // 只在空闲时收到用户消息才记总耗时起点;运行中的 steer/followUp 保留原起点
40
+ if (runStartTime === 0 && (event.source === "interactive" || event.source === "rpc")) {
41
+ runStartTime = Date.now();
42
+ }
43
+ });
44
+
45
+ pi.on("agent_start", async () => {
46
+ // 兜底:extension 注入消息触发的运行没有用户 input 事件
47
+ if (runStartTime === 0) runStartTime = Date.now();
48
+ });
49
+
50
+ pi.on("turn_start", async (event, ctx) => {
51
+ stopTick();
52
+ // 优先用事件自带的时间戳,避免 handler 调度延迟
53
+ turnStartTime = event.timestamp || Date.now();
54
+ if (!ctx.hasUI) return;
55
+
56
+ const tick = () => {
57
+ // spinner 显示全程总耗时(从用户发出消息起),跨轮不归零;
58
+ // runStartTime 尚未记录时(理论上不会)退化为本轮起点
59
+ const base = runStartTime || turnStartTime;
60
+ if (!base) return;
61
+ ctx.ui.setWorkingMessage(i18n.t("elapsedWorking", { value: formatTick(Date.now() - base) }));
62
+ };
63
+ tick();
64
+ tickHandle = setInterval(tick, TICK_MS);
65
+ });
66
+
67
+ pi.on("turn_end", async (_event, ctx) => {
68
+ stopTick();
69
+ if (!turnStartTime) return;
70
+ const elapsed = Date.now() - turnStartTime;
71
+ turnStartTime = 0;
72
+ if (!ctx.hasUI) return;
73
+
74
+ // 恢复 pi 默认 working 文字(下次 streaming 由 pi 内部重置)
75
+ ctx.ui.setWorkingMessage(undefined);
76
+ // 在 chat 流末尾插入一条 dim 灰文本
77
+ ctx.ui.notify(i18n.t("elapsedDone", { value: formatDone(elapsed) }), "info");
78
+ });
79
+
80
+ pi.on("agent_end", async (_event, ctx) => {
81
+ stopTick();
82
+ turnStartTime = 0;
83
+ if (!ctx.hasUI) return;
84
+ ctx.ui.setWorkingMessage(undefined);
85
+ });
86
+
87
+ pi.on("agent_settled", async (_event, ctx) => {
88
+ stopTick();
89
+ const runElapsed = runStartTime ? Date.now() - runStartTime : 0;
90
+ runStartTime = 0;
91
+ turnStartTime = 0;
92
+ if (!ctx.hasUI) return;
93
+
94
+ ctx.ui.setWorkingMessage(undefined);
95
+ if (runElapsed > 0) {
96
+ ctx.ui.notify(i18n.t("elapsedTotal", { value: formatDone(runElapsed) }), "info");
97
+ }
98
+ });
99
+ }