pi-metrics 0.5.0 → 0.6.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 CHANGED
@@ -7,8 +7,8 @@ Session metrics for the [Pi coding agent](https://github.com/earendil-works/pi):
7
7
  ## Features
8
8
 
9
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`).
10
+ - When each turn ends, one line reports TPS, TTFT, token counts, generation time, stalls, and blended cost (the line already carries the turn duration, so no separate elapsed notice is emitted).
11
+ - When a run spans more than one turn and the agent fully settles (`agent_settled` — including auto-retries, compaction continuations, or Esc interruption), a final line adds the **total elapsed time from message send to stop** (`⏱ Total elapsed 18.9s`). Single-turn runs do not repeat it.
12
12
  - After each LLM turn, a notification reports TPS, TTFT, token counts, generation time, stalls, and blended cost when available.
13
13
  - Telemetry is persisted as `tps` custom session entries and restored after session resume or `/tree` navigation.
14
14
  - Metrics are exposed through session entries and notifications. Use `/config:metrics` to open the TUI settings menu, or use `/config:metrics enable|disable` to change the setting directly.
package/README.zh-CN.md CHANGED
@@ -7,8 +7,8 @@
7
7
  ## 功能
8
8
 
9
9
  - 工作期间 spinner 显示**从发出消息起的全程耗时**(如 `⏱ 47s`),跨轮次持续累加,不再每轮回零。
10
- - 每个轮次结束时插入一条灰色文本,显示该轮精确耗时(`⏱ 本轮耗时 8.2s`)。
11
- - AI 完全停止时(`agent_settled`,覆盖自动重试、compaction 续跑以及 Esc 中断)追加一行**从发出消息到停止的总耗时**(`⏱ 总耗时 18.9s`)。
10
+ - 每个轮次结束时显示 TPS、TTFT、token 数、生成耗时、stall 和可用的综合成本(一行指标里已包含本轮耗时,不再单独发一条)。
11
+ - 整段运行超过一轮时,AI 完全停止(`agent_settled`,覆盖自动重试、compaction 续跑以及 Esc 中断)再补一行**从发出消息到停止的总耗时**(`⏱ 总耗时 18.9s`);单轮运行不重复报。
12
12
  - 每轮 LLM 调用结束后显示 TPS、TTFT、token 数、生成耗时、stall 和可用的综合成本。
13
13
  - Telemetry 以 `tps` custom session entry 持久化,并在恢复 session 或 `/tree` 后恢复显示。
14
14
  - Metrics 通过 session entry 和通知提供。使用 `/config:metrics` 打开 TUI 配置菜单,也可以使用 `/config:metrics enable|disable` 直接切换。
package/SKILL.md CHANGED
@@ -15,4 +15,4 @@ description: "启用与排查 pi-metrics 的耗时、TPS、TTFT、token 和成
15
15
 
16
16
  ## 验证
17
17
 
18
- 在 TUI 中完成一次真实模型回合,观察 working spinner、轮次耗时、总耗时和可用的 TPS/TTFT/token/costRPC/print 模式不会启动 UI 定时器或通知;provider 未返回 usage 时部分指标不可用。真实模型回合属于 E2E,执行前遵守当前任务授权边界;未运行时明确报告 `NOT_RUN`。
18
+ 在 TUI 中完成一次真实模型回合,观察 working spinner、每轮的 TPS/TTFT/token 指标行(含本轮耗时)和可用的 cost;如果一次运行跑了多轮,停下时还应看到一条 `⏱ 总耗时`。RPC/print 模式不会启动 UI 定时器或通知;provider 未返回 usage 时部分指标不可用。真实模型回合属于 E2E,执行前遵守当前任务授权边界;未运行时明确报告 `NOT_RUN`。
@@ -3,10 +3,6 @@
3
3
  "zh-CN": "⏱ {value}",
4
4
  "en-US": "⏱ {value}"
5
5
  },
6
- "elapsedDone": {
7
- "zh-CN": "⏱ 本轮耗时 {value}",
8
- "en-US": "⏱ Turn elapsed {value}"
9
- },
10
6
  "elapsedTotal": {
11
7
  "zh-CN": "⏱ 总耗时 {value}",
12
8
  "en-US": "⏱ Total elapsed {value}"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-metrics",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Pi metrics extension: elapsed-time HUD and resilient TPS telemetry",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
@@ -62,7 +62,7 @@
62
62
  "@earendil-works/pi-tui": ">=0.80.0"
63
63
  },
64
64
  "dependencies": {
65
- "pi-extensions-i18n": "^0.5.0"
65
+ "pi-extensions-i18n": "^0.6.0"
66
66
  },
67
67
  "devDependencies": {
68
68
  "@earendil-works/pi-ai": "0.85.1",
package/src/index.ts CHANGED
@@ -3,7 +3,7 @@ import type {
3
3
  ExtensionCommandContext,
4
4
  ExtensionContext,
5
5
  } from "@earendil-works/pi-coding-agent";
6
- import { createTranslator, loadCatalog, notifyWithSource } from "pi-extensions-i18n";
6
+ import { createTranslator, installNoticeRenderer, loadCatalog, notifyWithSource } from "pi-extensions-i18n";
7
7
  import { NOTICE_SOURCE } from "./notice.ts";
8
8
  import turnElapsed from "./turn-elapsed.ts";
9
9
  import tps from "./tps.ts";
@@ -98,6 +98,8 @@ function registerConfigCommand(pi: ExtensionAPI): void {
98
98
 
99
99
  /** 注册耗时和 TPS 指标事件;配置关闭时不注册指标处理器。 */
100
100
  export default function piHud(pi: ExtensionAPI): void {
101
+ // 提示画成会话区里的带底色消息块;渲染器在本包这个模块实例里注册一次。
102
+ installNoticeRenderer(pi);
101
103
  registerConfigCommand(pi);
102
104
  let config: MetricsConfig;
103
105
  let configError: unknown;
@@ -3,10 +3,13 @@
3
3
  *
4
4
  * - working 期间:实时更新 spinner 文字,显示从用户发出消息起的全程耗时(如 "⏱ 47s"),
5
5
  * 跨轮不归零 —— 用户等待时最关心的是"一共等了多久"
6
- * - turn 结束时:在 chat 流插入一条 dim 灰文本,提示本轮耗时(如 "✓ 本轮耗时 12.3s")
7
- * - agent 完全停止时(agent_settled):再插入一条总耗时,覆盖从用户发出消息到 AI 停止
6
+ * - agent 完全停止时(agent_settled):插入一条总耗时,覆盖从用户发出消息到 AI 停止
8
7
  * 的整段过程(跨越多轮工具调用、自动重试和 compaction 续跑)
9
8
  *
9
+ * 关于「本轮耗时」:它已经包含在 tps 的那条指标提示里(TPS/TTFT/耗时/tokens 一行),
10
+ * 所以这里不再单独发一条,避免同一轮冒两条指标提示。
11
+ * 只有整段运行超过一轮时才发总耗时:单轮运行的总耗时只比本轮多一点收尾开销,是噪声。
12
+ *
10
13
  * 设计取舍:
11
14
  * - 用 setWorkingMessage 改 spinner 文字会覆盖 pi 默认的 "Working... (Esc to interrupt)"。
12
15
  * 为了让耗时最显眼,接受这个 trade-off —— 用户更关心"等了多久"而非"怎么中断"。
@@ -25,9 +28,75 @@ import { NOTICE_SOURCE } from "./notice.ts";
25
28
 
26
29
  const TICK_MS = 1000;
27
30
 
28
- export default function (pi: ExtensionAPI) {
29
- let turnStartTime = 0;
31
+ /** 至少跑满两轮才值得单独报一次总耗时;单轮的总耗时是噪声。 */
32
+ const MIN_TURNS_FOR_TOTAL = 2;
33
+
34
+ /** 计时状态:与 Pi 事件解耦,便于直接测试判定规则。 */
35
+ export interface ElapsedTracker {
36
+ /** 用户发出消息(或没有 input 事件时的兜底)开始记一段运行;运行中重复调用不改起点。 */
37
+ startRun(): void;
38
+ /** 本段运行的已耗时(毫秒);没有进行中的运行时返回 0。 */
39
+ runElapsed(): number;
40
+ /** 标记本轮已开始(只用计轮数,不再单独算本轮耗时)。 */
41
+ startTurn(): void;
42
+ /** 一轮结束:累计轮次并清掉本轮状态。 */
43
+ endTurn(): void;
44
+ /** AI 停下:跑满两轮时返回本段总耗时(毫秒),否则返回 undefined。 */
45
+ settle(): number | undefined;
46
+ /** 清掉本轮起点(agent_end 用)。 */
47
+ clearTurn(): void;
48
+ }
49
+
50
+ /** 造一个计时状态;now 可注入,便于测试确定性地推进时间。 */
51
+ export function createElapsedTracker(now: () => number = () => Date.now()): ElapsedTracker {
52
+ /** 是否已开始一段运行;不用时间戳做哨兵,时钟可以从 0 开始。 */
53
+ let running = false;
54
+ /** 本段运行起点(毫秒)。 */
30
55
  let runStartTime = 0;
56
+ /** 当前轮是否已开始。 */
57
+ let inTurn = false;
58
+ /** 本段运行已跑完的轮数。 */
59
+ let turnCount = 0;
60
+ return {
61
+ /** 开始记一段运行;运行中重复调用不改起点。 */
62
+ startRun(): void {
63
+ if (running) return;
64
+ running = true;
65
+ runStartTime = now();
66
+ turnCount = 0;
67
+ },
68
+ /** 本段运行的已耗时(毫秒);没有进行中的运行时返回 0。 */
69
+ runElapsed(): number {
70
+ return running ? now() - runStartTime : 0;
71
+ },
72
+ /** 标记本轮已开始(只用计轮数)。 */
73
+ startTurn(): void {
74
+ inTurn = true;
75
+ },
76
+ /** 一轮结束:累计轮次并清掉本轮状态。 */
77
+ endTurn(): void {
78
+ if (inTurn) turnCount += 1;
79
+ inTurn = false;
80
+ },
81
+ /** AI 停下:跑满两轮时返回本段总耗时(毫秒),否则返回 undefined。 */
82
+ settle(): number | undefined {
83
+ const elapsed = running ? now() - runStartTime : 0;
84
+ const turns = turnCount;
85
+ running = false;
86
+ inTurn = false;
87
+ runStartTime = 0;
88
+ turnCount = 0;
89
+ return elapsed > 0 && turns >= MIN_TURNS_FOR_TOTAL ? elapsed : undefined;
90
+ },
91
+ /** 清掉本轮状态(agent_end 用)。 */
92
+ clearTurn(): void {
93
+ inTurn = false;
94
+ },
95
+ };
96
+ }
97
+
98
+ export default function (pi: ExtensionAPI) {
99
+ const tracker = createElapsedTracker();
31
100
  let tickHandle: ReturnType<typeof setInterval> | null = null;
32
101
 
33
102
  const stopTick = () => {
@@ -39,28 +108,24 @@ export default function (pi: ExtensionAPI) {
39
108
 
40
109
  pi.on("input", async (event) => {
41
110
  // 只在空闲时收到用户消息才记总耗时起点;运行中的 steer/followUp 保留原起点
42
- if (runStartTime === 0 && (event.source === "interactive" || event.source === "rpc")) {
43
- runStartTime = Date.now();
44
- }
111
+ if (event.source === "interactive" || event.source === "rpc") tracker.startRun();
45
112
  });
46
113
 
47
114
  pi.on("agent_start", async () => {
48
115
  // 兜底:extension 注入消息触发的运行没有用户 input 事件
49
- if (runStartTime === 0) runStartTime = Date.now();
116
+ tracker.startRun();
50
117
  });
51
118
 
52
- pi.on("turn_start", async (event, ctx) => {
119
+ pi.on("turn_start", async (_event, ctx) => {
53
120
  stopTick();
54
- // 优先用事件自带的时间戳,避免 handler 调度延迟
55
- turnStartTime = event.timestamp || Date.now();
121
+ tracker.startTurn();
56
122
  if (!ctx.hasUI) return;
57
123
 
58
124
  const tick = () => {
59
- // spinner 显示全程总耗时(从用户发出消息起),跨轮不归零;
60
- // runStartTime 尚未记录时(理论上不会)退化为本轮起点
61
- const base = runStartTime || turnStartTime;
62
- if (!base) return;
63
- ctx.ui.setWorkingMessage(i18n.t("elapsedWorking", { value: formatTick(Date.now() - base) }));
125
+ // spinner 显示全程总耗时(从用户发出消息起),跨轮不归零
126
+ const elapsed = tracker.runElapsed();
127
+ if (elapsed <= 0) return;
128
+ ctx.ui.setWorkingMessage(i18n.t("elapsedWorking", { value: formatTick(elapsed) }));
64
129
  };
65
130
  tick();
66
131
  tickHandle = setInterval(tick, TICK_MS);
@@ -68,33 +133,28 @@ export default function (pi: ExtensionAPI) {
68
133
 
69
134
  pi.on("turn_end", async (_event, ctx) => {
70
135
  stopTick();
71
- if (!turnStartTime) return;
72
- const elapsed = Date.now() - turnStartTime;
73
- turnStartTime = 0;
136
+ tracker.endTurn();
74
137
  if (!ctx.hasUI) return;
75
138
 
76
- // 恢复 pi 默认 working 文字(下次 streaming 由 pi 内部重置)
139
+ // 恢复 pi 默认 working 文字(下次 streaming 由 pi 内部重置);
140
+ // 本轮耗时由 tps 那条指标提示带上,不在这里重复发。
77
141
  ctx.ui.setWorkingMessage(undefined);
78
- // 在 chat 流末尾插入一条 dim 灰文本
79
- notifyWithSource({ ctx, source: NOTICE_SOURCE, level: "info", message: i18n.t("elapsedDone", { value: formatDone(elapsed) }) });
80
142
  });
81
143
 
82
144
  pi.on("agent_end", async (_event, ctx) => {
83
145
  stopTick();
84
- turnStartTime = 0;
146
+ tracker.clearTurn();
85
147
  if (!ctx.hasUI) return;
86
148
  ctx.ui.setWorkingMessage(undefined);
87
149
  });
88
150
 
89
151
  pi.on("agent_settled", async (_event, ctx) => {
90
152
  stopTick();
91
- const runElapsed = runStartTime ? Date.now() - runStartTime : 0;
92
- runStartTime = 0;
93
- turnStartTime = 0;
153
+ const runElapsed = tracker.settle();
94
154
  if (!ctx.hasUI) return;
95
155
 
96
156
  ctx.ui.setWorkingMessage(undefined);
97
- if (runElapsed > 0) {
157
+ if (runElapsed !== undefined) {
98
158
  notifyWithSource({ ctx, source: NOTICE_SOURCE, level: "info", message: i18n.t("elapsedTotal", { value: formatDone(runElapsed) }) });
99
159
  }
100
160
  });