pi-metrics 0.4.2 → 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 +2 -2
- package/README.zh-CN.md +2 -2
- package/SKILL.md +1 -1
- package/locales/index.json +0 -4
- package/package.json +2 -2
- package/src/index.ts +41 -18
- package/src/notice.ts +13 -0
- package/src/tps.ts +6 -4
- package/src/turn-elapsed.ts +90 -28
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,
|
|
11
|
-
- When the agent fully settles (`agent_settled` — including auto-retries, compaction continuations, or Esc interruption), a final line
|
|
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
|
-
-
|
|
11
|
-
- AI
|
|
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
|
|
18
|
+
在 TUI 中完成一次真实模型回合,观察 working spinner、每轮的 TPS/TTFT/token 指标行(含本轮耗时)和可用的 cost;如果一次运行跑了多轮,停下时还应看到一条 `⏱ 总耗时`。RPC/print 模式不会启动 UI 定时器或通知;provider 未返回 usage 时部分指标不可用。真实模型回合属于 E2E,执行前遵守当前任务授权边界;未运行时明确报告 `NOT_RUN`。
|
package/locales/index.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-metrics",
|
|
3
|
-
"version": "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.
|
|
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,8 @@ import type {
|
|
|
3
3
|
ExtensionCommandContext,
|
|
4
4
|
ExtensionContext,
|
|
5
5
|
} from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import { createTranslator, loadCatalog } from "pi-extensions-i18n";
|
|
6
|
+
import { createTranslator, installNoticeRenderer, loadCatalog, notifyWithSource } from "pi-extensions-i18n";
|
|
7
|
+
import { NOTICE_SOURCE } from "./notice.ts";
|
|
7
8
|
import turnElapsed from "./turn-elapsed.ts";
|
|
8
9
|
import tps from "./tps.ts";
|
|
9
10
|
import { configPath, loadConfig, parseConfig, saveConfig, type MetricsConfig } from "./config.ts";
|
|
@@ -29,32 +30,42 @@ function registerConfigCommand(pi: ExtensionAPI): void {
|
|
|
29
30
|
handler: async (args: string, ctx: ExtensionCommandContext): Promise<void> => {
|
|
30
31
|
const value = args.trim();
|
|
31
32
|
if (value && value !== CONFIG_RESET_COMMAND && value !== ENABLE_COMMAND && value !== DISABLE_COMMAND) {
|
|
32
|
-
ctx
|
|
33
|
+
notifyWithSource({ ctx, source: NOTICE_SOURCE, level: NOTICE_WARNING, message: i18n.t("configCommandUsage") });
|
|
33
34
|
return;
|
|
34
35
|
}
|
|
35
36
|
if (value === CONFIG_RESET_COMMAND || value === ENABLE_COMMAND || value === DISABLE_COMMAND) {
|
|
36
37
|
try {
|
|
37
38
|
const config = parseConfig(value === CONFIG_RESET_COMMAND ? {} : { enabled: value === ENABLE_COMMAND });
|
|
38
39
|
const path = saveConfig(config);
|
|
39
|
-
ctx
|
|
40
|
+
notifyWithSource({ ctx, source: NOTICE_SOURCE, level: NOTICE_INFO, message: i18n.t("configCommandSaved", { path }) });
|
|
40
41
|
} catch (error) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
notifyWithSource({
|
|
43
|
+
ctx,
|
|
44
|
+
source: NOTICE_SOURCE,
|
|
45
|
+
level: NOTICE_ERROR,
|
|
46
|
+
message: i18n.t("configCommandInvalid", {
|
|
47
|
+
error: error instanceof Error ? error.message : String(error),
|
|
48
|
+
}),
|
|
49
|
+
});
|
|
44
50
|
}
|
|
45
51
|
return;
|
|
46
52
|
}
|
|
47
53
|
if (!ctx.hasUI) {
|
|
48
|
-
ctx
|
|
54
|
+
notifyWithSource({ ctx, source: NOTICE_SOURCE, level: NOTICE_WARNING, message: i18n.t("configCommandInteractiveOnly") });
|
|
49
55
|
return;
|
|
50
56
|
}
|
|
51
57
|
let current: MetricsConfig;
|
|
52
58
|
try {
|
|
53
59
|
current = loadConfig();
|
|
54
60
|
} catch (error) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
61
|
+
notifyWithSource({
|
|
62
|
+
ctx,
|
|
63
|
+
source: NOTICE_SOURCE,
|
|
64
|
+
level: NOTICE_ERROR,
|
|
65
|
+
message: i18n.t("configCommandInvalid", {
|
|
66
|
+
error: error instanceof Error ? error.message : String(error),
|
|
67
|
+
}),
|
|
68
|
+
});
|
|
58
69
|
return;
|
|
59
70
|
}
|
|
60
71
|
const enabledChoice = i18n.t("configEnabled", { value: i18n.t(current.enabled ? "configOn" : "configOff") });
|
|
@@ -69,11 +80,16 @@ function registerConfigCommand(pi: ExtensionAPI): void {
|
|
|
69
80
|
const choice = selected === enabledChoice ? CONFIG_CHOICE.enabled : CONFIG_CHOICE.disabled;
|
|
70
81
|
try {
|
|
71
82
|
const path = saveConfig({ enabled: choice === CONFIG_CHOICE.enabled });
|
|
72
|
-
ctx
|
|
83
|
+
notifyWithSource({ ctx, source: NOTICE_SOURCE, level: NOTICE_INFO, message: i18n.t("configCommandSaved", { path }) });
|
|
73
84
|
} catch (error) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
85
|
+
notifyWithSource({
|
|
86
|
+
ctx,
|
|
87
|
+
source: NOTICE_SOURCE,
|
|
88
|
+
level: NOTICE_ERROR,
|
|
89
|
+
message: i18n.t("configCommandInvalid", {
|
|
90
|
+
error: error instanceof Error ? error.message : String(error),
|
|
91
|
+
}),
|
|
92
|
+
});
|
|
77
93
|
}
|
|
78
94
|
},
|
|
79
95
|
};
|
|
@@ -82,6 +98,8 @@ function registerConfigCommand(pi: ExtensionAPI): void {
|
|
|
82
98
|
|
|
83
99
|
/** 注册耗时和 TPS 指标事件;配置关闭时不注册指标处理器。 */
|
|
84
100
|
export default function piHud(pi: ExtensionAPI): void {
|
|
101
|
+
// 提示画成会话区里的带底色消息块;渲染器在本包这个模块实例里注册一次。
|
|
102
|
+
installNoticeRenderer(pi);
|
|
85
103
|
registerConfigCommand(pi);
|
|
86
104
|
let config: MetricsConfig;
|
|
87
105
|
let configError: unknown;
|
|
@@ -93,10 +111,15 @@ export default function piHud(pi: ExtensionAPI): void {
|
|
|
93
111
|
}
|
|
94
112
|
if (configError !== undefined) {
|
|
95
113
|
pi.on("session_start", (_event, ctx: ExtensionContext) => {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
114
|
+
notifyWithSource({
|
|
115
|
+
ctx,
|
|
116
|
+
source: NOTICE_SOURCE,
|
|
117
|
+
level: NOTICE_WARNING,
|
|
118
|
+
message: i18n.t("configLoadFailed", {
|
|
119
|
+
path: configPath(),
|
|
120
|
+
error: configError instanceof Error ? configError.message : String(configError),
|
|
121
|
+
}),
|
|
122
|
+
});
|
|
100
123
|
});
|
|
101
124
|
}
|
|
102
125
|
if (!config.enabled) return;
|
package/src/notice.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 本扩展统一的提示来源定义。
|
|
3
|
+
* 所有 notifyWithSource 调用共用同一份标签与颜色,避免两个入口不一致。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { NoticeColor, NoticeSource } from "pi-extensions-i18n";
|
|
7
|
+
|
|
8
|
+
/** 本扩展的提示标签;短且唯一,便于在会话里定位来源。 */
|
|
9
|
+
export const NOTICE_TAG = "metrics";
|
|
10
|
+
/** 提示标签颜色;与其它扩展错开,避免看起来像同一条消息。 */
|
|
11
|
+
export const NOTICE_COLOR: NoticeColor = "dim";
|
|
12
|
+
/** 本扩展的提示来源。 */
|
|
13
|
+
export const NOTICE_SOURCE: NoticeSource = { tag: NOTICE_TAG, color: NOTICE_COLOR };
|
package/src/tps.ts
CHANGED
|
@@ -11,7 +11,9 @@ import type {
|
|
|
11
11
|
ExtensionAPI,
|
|
12
12
|
ExtensionContext,
|
|
13
13
|
} from "@earendil-works/pi-coding-agent";
|
|
14
|
+
import { notifyWithSource } from "pi-extensions-i18n";
|
|
14
15
|
import { i18n } from "./i18n.ts";
|
|
16
|
+
import { NOTICE_SOURCE } from "./notice.ts";
|
|
15
17
|
|
|
16
18
|
interface TurnStartEvent {
|
|
17
19
|
type: "turn_start";
|
|
@@ -330,11 +332,11 @@ function restoreTPSNotification(
|
|
|
330
332
|
const data = entry.data as Record<string, unknown> | null | undefined;
|
|
331
333
|
if (!data) continue;
|
|
332
334
|
if (typeof data.model === "object" && data.model !== null) {
|
|
333
|
-
schedule(() => ctx
|
|
335
|
+
schedule(() => notifyWithSource({ ctx, source: NOTICE_SOURCE, level: "info", message: composeDisplayString(data as unknown as TurnTelemetry) }));
|
|
334
336
|
return;
|
|
335
337
|
}
|
|
336
338
|
if (typeof data.message === "string") {
|
|
337
|
-
schedule(() => ctx
|
|
339
|
+
schedule(() => notifyWithSource({ ctx, source: NOTICE_SOURCE, level: "info", message: data.message as string }));
|
|
338
340
|
return;
|
|
339
341
|
}
|
|
340
342
|
}
|
|
@@ -392,7 +394,7 @@ export default function tpsExtension(pi: ExtensionAPI): void {
|
|
|
392
394
|
committed.telemetry = corrected;
|
|
393
395
|
pi.appendEntry("tps", corrected);
|
|
394
396
|
pi.events?.emit("tps:telemetry", corrected);
|
|
395
|
-
if (committed.ctx.hasUI) committed.ctx
|
|
397
|
+
if (committed.ctx.hasUI) notifyWithSource({ ctx: committed.ctx, source: NOTICE_SOURCE, level: "info", message: composeDisplayString(corrected) });
|
|
396
398
|
});
|
|
397
399
|
|
|
398
400
|
pi.on("session_shutdown", () => {
|
|
@@ -513,7 +515,7 @@ export default function tpsExtension(pi: ExtensionAPI): void {
|
|
|
513
515
|
};
|
|
514
516
|
pi.appendEntry("tps", telemetry);
|
|
515
517
|
pi.events?.emit("tps:telemetry", telemetry);
|
|
516
|
-
if (ctx.hasUI) ctx
|
|
518
|
+
if (ctx.hasUI) notifyWithSource({ ctx, source: NOTICE_SOURCE, level: "info", message: composeDisplayString(telemetry) });
|
|
517
519
|
});
|
|
518
520
|
|
|
519
521
|
}
|
package/src/turn-elapsed.ts
CHANGED
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
*
|
|
4
4
|
* - working 期间:实时更新 spinner 文字,显示从用户发出消息起的全程耗时(如 "⏱ 47s"),
|
|
5
5
|
* 跨轮不归零 —— 用户等待时最关心的是"一共等了多久"
|
|
6
|
-
* -
|
|
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 —— 用户更关心"等了多久"而非"怎么中断"。
|
|
@@ -18,14 +21,82 @@
|
|
|
18
21
|
*/
|
|
19
22
|
|
|
20
23
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
24
|
+
import { notifyWithSource } from "pi-extensions-i18n";
|
|
21
25
|
import { formatDone, formatTick } from "./format-utils.ts";
|
|
22
26
|
import { i18n } from "./i18n.ts";
|
|
27
|
+
import { NOTICE_SOURCE } from "./notice.ts";
|
|
23
28
|
|
|
24
29
|
const TICK_MS = 1000;
|
|
25
30
|
|
|
26
|
-
|
|
27
|
-
|
|
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
|
+
/** 本段运行起点(毫秒)。 */
|
|
28
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();
|
|
29
100
|
let tickHandle: ReturnType<typeof setInterval> | null = null;
|
|
30
101
|
|
|
31
102
|
const stopTick = () => {
|
|
@@ -37,28 +108,24 @@ export default function (pi: ExtensionAPI) {
|
|
|
37
108
|
|
|
38
109
|
pi.on("input", async (event) => {
|
|
39
110
|
// 只在空闲时收到用户消息才记总耗时起点;运行中的 steer/followUp 保留原起点
|
|
40
|
-
if (
|
|
41
|
-
runStartTime = Date.now();
|
|
42
|
-
}
|
|
111
|
+
if (event.source === "interactive" || event.source === "rpc") tracker.startRun();
|
|
43
112
|
});
|
|
44
113
|
|
|
45
114
|
pi.on("agent_start", async () => {
|
|
46
115
|
// 兜底:extension 注入消息触发的运行没有用户 input 事件
|
|
47
|
-
|
|
116
|
+
tracker.startRun();
|
|
48
117
|
});
|
|
49
118
|
|
|
50
|
-
pi.on("turn_start", async (
|
|
119
|
+
pi.on("turn_start", async (_event, ctx) => {
|
|
51
120
|
stopTick();
|
|
52
|
-
|
|
53
|
-
turnStartTime = event.timestamp || Date.now();
|
|
121
|
+
tracker.startTurn();
|
|
54
122
|
if (!ctx.hasUI) return;
|
|
55
123
|
|
|
56
124
|
const tick = () => {
|
|
57
|
-
// spinner
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
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) }));
|
|
62
129
|
};
|
|
63
130
|
tick();
|
|
64
131
|
tickHandle = setInterval(tick, TICK_MS);
|
|
@@ -66,34 +133,29 @@ export default function (pi: ExtensionAPI) {
|
|
|
66
133
|
|
|
67
134
|
pi.on("turn_end", async (_event, ctx) => {
|
|
68
135
|
stopTick();
|
|
69
|
-
|
|
70
|
-
const elapsed = Date.now() - turnStartTime;
|
|
71
|
-
turnStartTime = 0;
|
|
136
|
+
tracker.endTurn();
|
|
72
137
|
if (!ctx.hasUI) return;
|
|
73
138
|
|
|
74
|
-
// 恢复 pi 默认 working 文字(下次 streaming 由 pi
|
|
139
|
+
// 恢复 pi 默认 working 文字(下次 streaming 由 pi 内部重置);
|
|
140
|
+
// 本轮耗时由 tps 那条指标提示带上,不在这里重复发。
|
|
75
141
|
ctx.ui.setWorkingMessage(undefined);
|
|
76
|
-
// 在 chat 流末尾插入一条 dim 灰文本
|
|
77
|
-
ctx.ui.notify(i18n.t("elapsedDone", { value: formatDone(elapsed) }), "info");
|
|
78
142
|
});
|
|
79
143
|
|
|
80
144
|
pi.on("agent_end", async (_event, ctx) => {
|
|
81
145
|
stopTick();
|
|
82
|
-
|
|
146
|
+
tracker.clearTurn();
|
|
83
147
|
if (!ctx.hasUI) return;
|
|
84
148
|
ctx.ui.setWorkingMessage(undefined);
|
|
85
149
|
});
|
|
86
150
|
|
|
87
151
|
pi.on("agent_settled", async (_event, ctx) => {
|
|
88
152
|
stopTick();
|
|
89
|
-
const runElapsed =
|
|
90
|
-
runStartTime = 0;
|
|
91
|
-
turnStartTime = 0;
|
|
153
|
+
const runElapsed = tracker.settle();
|
|
92
154
|
if (!ctx.hasUI) return;
|
|
93
155
|
|
|
94
156
|
ctx.ui.setWorkingMessage(undefined);
|
|
95
|
-
if (runElapsed
|
|
96
|
-
ctx
|
|
157
|
+
if (runElapsed !== undefined) {
|
|
158
|
+
notifyWithSource({ ctx, source: NOTICE_SOURCE, level: "info", message: i18n.t("elapsedTotal", { value: formatDone(runElapsed) }) });
|
|
97
159
|
}
|
|
98
160
|
});
|
|
99
161
|
}
|