token-stats-timer 1.0.4 → 1.0.6
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 +18 -0
- package/index.ts +3 -0
- package/notify.ts +1 -1
- package/package.json +2 -1
- package/step-timer.ts +400 -0
package/README.md
CHANGED
|
@@ -54,6 +54,24 @@ Footer 下行:cwd + git 分支 + 其他扩展状态。
|
|
|
54
54
|
|
|
55
55
|
`/notify` —— 无参查看当前状态;`on`/`off` 开启/关闭。
|
|
56
56
|
|
|
57
|
+
## 每步耗时(step-timer)
|
|
58
|
+
|
|
59
|
+
任务执行中实时显示每步耗时,结束后在会话里留下可回看的耗时摘要(`appendEntry` 持久化、不进入 LLM 上下文,`/resume` 后仍在)。
|
|
60
|
+
|
|
61
|
+
### 实时文案
|
|
62
|
+
|
|
63
|
+
- 任务执行中:工作指示器(spinner 文案)显示 `Working... 01:02`(整体已耗时,每秒刷新)
|
|
64
|
+
- 思考阶段:隐藏思考块文案显示 `Thinking... 00:45`(当前思考块已耗时)
|
|
65
|
+
|
|
66
|
+
> 说明:`Thinking...` 文案只在“隐藏思考块”模式下显示;开启思考可见时该处不显示文案,但计时不受影响。
|
|
67
|
+
|
|
68
|
+
### 会话摘要
|
|
69
|
+
|
|
70
|
+
- **每 turn**:每个 turn 结束后插入一条 `⏱ turn 3 · 00:45 · 思考 00:30 · bash×2 00:12 · 回复 00:03`(展开可看全部工具明细)
|
|
71
|
+
- **完成**:任务结束时插入一条汇总:`✅ 任务完成 总耗时 01:23 · 思考 00:45 (54%) · 回复 00:17 · bash×3 00:21 · 6 turns`;失败/中止时标题相应变为 ❌/⏹
|
|
72
|
+
|
|
73
|
+
无独立开关,随包启用;计时口径与 run-timer 一致(一次 run = 首个 agent_start → agent_settled,含重试/压缩/排队提示)。
|
|
74
|
+
|
|
57
75
|
## 命令
|
|
58
76
|
|
|
59
77
|
- `/stats` —— 无参进入套餐配置(为当前 provider 选择/关闭配额套餐)
|
package/index.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
|
21
21
|
import { createRunTimer } from "./run-timer.ts";
|
|
22
22
|
import { createTokenStats, formatUserPath, type SharedState } from "./token-stats.ts";
|
|
23
23
|
import { createNotifier } from "./notify.ts";
|
|
24
|
+
import { createStepTimer } from "./step-timer.ts";
|
|
24
25
|
|
|
25
26
|
const shared: SharedState = {
|
|
26
27
|
sessionActive: false,
|
|
@@ -32,6 +33,8 @@ export default function runTokenStatsExtension(pi: ExtensionAPI) {
|
|
|
32
33
|
const timer = createRunTimer(pi, shared);
|
|
33
34
|
// macOS 完成通知(成功/失败/中止),配置见 notify-config.json
|
|
34
35
|
createNotifier(pi);
|
|
36
|
+
// 每步耗时:Thinking.../Working... 实时耗时 + 每 turn/总耗时会话摘要
|
|
37
|
+
createStepTimer(pi);
|
|
35
38
|
|
|
36
39
|
pi.on("session_start", (_event, ctx) => {
|
|
37
40
|
shared.sessionActive = true;
|
package/notify.ts
CHANGED
|
@@ -140,7 +140,7 @@ export function createNotifier(pi: ExtensionAPI): void {
|
|
|
140
140
|
const durationMs = startMs !== undefined ? Date.now() - startMs : 0;
|
|
141
141
|
if (durationMs < config.minDurationSec * 1000) return;
|
|
142
142
|
|
|
143
|
-
const dur = startMs !== undefined ?
|
|
143
|
+
const dur = startMs !== undefined ? `${formatDuration(durationMs)}` : "-";
|
|
144
144
|
|
|
145
145
|
if (runFailed) {
|
|
146
146
|
if (config.onFailure) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "token-stats-timer",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "pi extension to display token usage and time spend.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"run-timer.ts",
|
|
16
16
|
"token-stats.ts",
|
|
17
17
|
"notify.ts",
|
|
18
|
+
"step-timer.ts",
|
|
18
19
|
"README.md"
|
|
19
20
|
],
|
|
20
21
|
"scripts": {
|
package/step-timer.ts
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
// step-timer 模块 —— 每步耗时(思考/工具/回复)+ 每 turn / 总耗时会话摘要
|
|
2
|
+
// =============================================================================
|
|
3
|
+
// 实时显示(仅交互 TUI):
|
|
4
|
+
// - 任务执行中:工作指示器文案改为 "Working... 01:02"(整体已耗时,每秒刷新)
|
|
5
|
+
// - 思考阶段:隐藏思考块文案改为 "Thinking... 00:45"(当前思考块已耗时)
|
|
6
|
+
// 会话摘要(appendEntry 持久化,不进入 LLM 上下文):
|
|
7
|
+
// - 每个 turn 结束 appendEntry("timing-turn", …):该 turn 思考/工具/回复分项
|
|
8
|
+
// - agent_settled appendEntry("timing-final", …):总耗时 + 思考占比 + 各工具统计
|
|
9
|
+
//
|
|
10
|
+
// 一次 run = 空闲后的首个 agent_start → agent_settled(与 run-timer 语义一致,
|
|
11
|
+
// 包含重试、压缩恢复和排队的 steering/follow-up 提示)。
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
ExtensionAPI,
|
|
15
|
+
ExtensionContext,
|
|
16
|
+
Theme,
|
|
17
|
+
} from "@earendil-works/pi-coding-agent";
|
|
18
|
+
import { Box, Text } from "@earendil-works/pi-tui";
|
|
19
|
+
|
|
20
|
+
export type StepPhase = "idle" | "thinking" | "text" | "tool";
|
|
21
|
+
|
|
22
|
+
/** 单个工具的聚合统计(当前 turn 内按工具名累计) */
|
|
23
|
+
export interface ToolStat {
|
|
24
|
+
name: string;
|
|
25
|
+
count: number;
|
|
26
|
+
totalMs: number;
|
|
27
|
+
errors: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 一个 turn 的耗时明细(appendEntry "timing-turn") */
|
|
31
|
+
export interface TurnTimingData {
|
|
32
|
+
turnIndex: number;
|
|
33
|
+
thinkingMs: number;
|
|
34
|
+
textMs: number;
|
|
35
|
+
tools: ToolStat[];
|
|
36
|
+
totalMs: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 一次 run 的总耗时汇总(appendEntry "timing-final") */
|
|
40
|
+
export interface FinalTimingData {
|
|
41
|
+
totalMs: number;
|
|
42
|
+
thinkingMs: number;
|
|
43
|
+
thinkingPct: number;
|
|
44
|
+
textMs: number;
|
|
45
|
+
tools: ToolStat[];
|
|
46
|
+
turnCount: number;
|
|
47
|
+
failed: boolean;
|
|
48
|
+
aborted: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type { ExtensionContext };
|
|
52
|
+
|
|
53
|
+
const TURN_TYPE = "timing-turn";
|
|
54
|
+
const FINAL_TYPE = "timing-final";
|
|
55
|
+
const TICK_MS = 1000;
|
|
56
|
+
const MAX_TOOLS_SHOWN = 4;
|
|
57
|
+
|
|
58
|
+
function formatDuration(ms: number): string {
|
|
59
|
+
const totalSeconds = Math.max(0, Math.floor(ms / 1000));
|
|
60
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
61
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
62
|
+
const seconds = totalSeconds % 60;
|
|
63
|
+
|
|
64
|
+
if (hours > 0) {
|
|
65
|
+
return `${hours}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
|
|
66
|
+
}
|
|
67
|
+
return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function mergeToolStat(a: Map<string, ToolStat>, b: ToolStat[]): void {
|
|
71
|
+
for (const t of b) {
|
|
72
|
+
const cur = a.get(t.name) ?? { name: t.name, count: 0, totalMs: 0, errors: 0 };
|
|
73
|
+
cur.count += t.count;
|
|
74
|
+
cur.totalMs += t.totalMs;
|
|
75
|
+
cur.errors += t.errors;
|
|
76
|
+
a.set(t.name, cur);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function toolListText(tools: ToolStat[]): string {
|
|
81
|
+
if (tools.length === 0) return "";
|
|
82
|
+
const sorted = [...tools].sort((x, y) => y.totalMs - x.totalMs).slice(0, MAX_TOOLS_SHOWN);
|
|
83
|
+
const parts = sorted.map((t) => `${t.name}×${t.count} ${formatDuration(t.totalMs)}`);
|
|
84
|
+
if (tools.length > MAX_TOOLS_SHOWN) {
|
|
85
|
+
parts.push(`…+${tools.length - MAX_TOOLS_SHOWN}个`);
|
|
86
|
+
}
|
|
87
|
+
return parts.join(" ");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** “思考 00:45 (54%)” 这类分项;为 0 时省略 */
|
|
91
|
+
function segmentText(label: string, ms: number, withPct?: number): string {
|
|
92
|
+
if (ms <= 0) return "";
|
|
93
|
+
const base = `${label} ${formatDuration(ms)}`;
|
|
94
|
+
return withPct !== undefined ? `${base} (${withPct}%)` : base;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function createStepTimer(pi: ExtensionAPI): void {
|
|
98
|
+
let lastCtx: ExtensionContext | undefined;
|
|
99
|
+
let tick: ReturnType<typeof setInterval> | undefined;
|
|
100
|
+
|
|
101
|
+
let runActive = false;
|
|
102
|
+
let runStartMs = 0;
|
|
103
|
+
let runFailed = false;
|
|
104
|
+
let runAborted = false;
|
|
105
|
+
|
|
106
|
+
// run 级聚合(供 timing-final 用)
|
|
107
|
+
let runThinkingMs = 0;
|
|
108
|
+
let runTextMs = 0;
|
|
109
|
+
const runTools = new Map<string, ToolStat>();
|
|
110
|
+
let appendedTurns = 0;
|
|
111
|
+
|
|
112
|
+
// turn 局部状态
|
|
113
|
+
let turnIndex = 0;
|
|
114
|
+
let phase: StepPhase = "idle";
|
|
115
|
+
let phaseStartMs = 0;
|
|
116
|
+
let turnThinkingMs = 0;
|
|
117
|
+
let turnTextMs = 0;
|
|
118
|
+
const turnTools = new Map<string, ToolStat>();
|
|
119
|
+
const activeTools = new Map<string, { name: string; startMs: number }>();
|
|
120
|
+
// 自上次 turn_end 后是否有计时事件(agent_settled 时兜底结算未闭合的 turn)
|
|
121
|
+
let turnDirty = false;
|
|
122
|
+
|
|
123
|
+
// ---------- 内部工具 ----------
|
|
124
|
+
|
|
125
|
+
function resetRun(): void {
|
|
126
|
+
runThinkingMs = 0;
|
|
127
|
+
runTextMs = 0;
|
|
128
|
+
runTools.clear();
|
|
129
|
+
appendedTurns = 0;
|
|
130
|
+
turnIndex = 0;
|
|
131
|
+
phase = "idle";
|
|
132
|
+
phaseStartMs = Date.now();
|
|
133
|
+
turnThinkingMs = 0;
|
|
134
|
+
turnTextMs = 0;
|
|
135
|
+
turnTools.clear();
|
|
136
|
+
activeTools.clear();
|
|
137
|
+
turnDirty = false;
|
|
138
|
+
runFailed = false;
|
|
139
|
+
runAborted = false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** 结算当前阶段的耗时(阶段切换 / turn 收口时调用) */
|
|
143
|
+
function closePhase(now: number, next: StepPhase): void {
|
|
144
|
+
if (phase === "thinking") {
|
|
145
|
+
turnThinkingMs += now - phaseStartMs;
|
|
146
|
+
if (lastCtx?.hasUI) lastCtx.ui.setHiddenThinkingLabel(); // 恢复默认 "Thinking..."
|
|
147
|
+
} else if (phase === "text") {
|
|
148
|
+
turnTextMs += now - phaseStartMs;
|
|
149
|
+
}
|
|
150
|
+
// "tool" 阶段耗时按 toolCallId 单独计时(activeTools),不走阶段时钟
|
|
151
|
+
phase = next;
|
|
152
|
+
phaseStartMs = now;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function turnToolTotal(): number {
|
|
156
|
+
let sum = 0;
|
|
157
|
+
for (const t of turnTools.values()) sum += t.totalMs;
|
|
158
|
+
return sum;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** 结算并追加一条 timing-turn 条目 */
|
|
162
|
+
function finishTurn(now: number): void {
|
|
163
|
+
const tools = [...turnTools.values()];
|
|
164
|
+
const totalMs = turnThinkingMs + turnTextMs + turnToolTotal();
|
|
165
|
+
pi.appendEntry<TurnTimingData>(TURN_TYPE, {
|
|
166
|
+
turnIndex,
|
|
167
|
+
thinkingMs: turnThinkingMs,
|
|
168
|
+
textMs: turnTextMs,
|
|
169
|
+
tools,
|
|
170
|
+
totalMs,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
runThinkingMs += turnThinkingMs;
|
|
174
|
+
runTextMs += turnTextMs;
|
|
175
|
+
mergeToolStat(runTools, tools);
|
|
176
|
+
appendedTurns++;
|
|
177
|
+
|
|
178
|
+
turnThinkingMs = 0;
|
|
179
|
+
turnTextMs = 0;
|
|
180
|
+
turnTools.clear();
|
|
181
|
+
turnDirty = false;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function updateLiveLabels(now: number): void {
|
|
185
|
+
if (!lastCtx?.hasUI || !runActive) return;
|
|
186
|
+
const ui = lastCtx.ui;
|
|
187
|
+
ui.setWorkingMessage(`Working... ${formatDuration(now - runStartMs)}`);
|
|
188
|
+
if (phase === "thinking") {
|
|
189
|
+
ui.setHiddenThinkingLabel(
|
|
190
|
+
`Thinking... ${formatDuration(turnThinkingMs + (now - phaseStartMs))}`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function startTicker(): void {
|
|
196
|
+
stopTicker();
|
|
197
|
+
tick = setInterval(() => updateLiveLabels(Date.now()), TICK_MS);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function stopTicker(): void {
|
|
201
|
+
if (!tick) return;
|
|
202
|
+
clearInterval(tick);
|
|
203
|
+
tick = undefined;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function buildFinal(now: number): FinalTimingData {
|
|
207
|
+
if (phase !== "idle") closePhase(now, "idle");
|
|
208
|
+
return {
|
|
209
|
+
totalMs: now - runStartMs,
|
|
210
|
+
thinkingMs: runThinkingMs,
|
|
211
|
+
thinkingPct: Math.round((runThinkingMs / Math.max(1, now - runStartMs)) * 100),
|
|
212
|
+
textMs: runTextMs,
|
|
213
|
+
tools: [...runTools.values()],
|
|
214
|
+
turnCount: appendedTurns,
|
|
215
|
+
failed: runFailed,
|
|
216
|
+
aborted: runAborted,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function appendFinal(now: number): void {
|
|
221
|
+
pi.appendEntry<FinalTimingData>(FINAL_TYPE, buildFinal(now));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ---------- 事件 ----------
|
|
225
|
+
|
|
226
|
+
pi.on("session_start", (_event, ctx) => {
|
|
227
|
+
lastCtx = ctx;
|
|
228
|
+
stopTicker();
|
|
229
|
+
runActive = false;
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
pi.on("agent_start", (_event, ctx) => {
|
|
233
|
+
lastCtx = ctx;
|
|
234
|
+
if (runActive) return;
|
|
235
|
+
runActive = true;
|
|
236
|
+
runStartMs = Date.now();
|
|
237
|
+
resetRun();
|
|
238
|
+
startTicker();
|
|
239
|
+
updateLiveLabels(runStartMs);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
pi.on("message_update", (event, ctx) => {
|
|
243
|
+
if (!runActive) return;
|
|
244
|
+
lastCtx = ctx;
|
|
245
|
+
turnDirty = true;
|
|
246
|
+
const ev = event.assistantMessageEvent;
|
|
247
|
+
const now = Date.now();
|
|
248
|
+
if (!ev) return;
|
|
249
|
+
if (ev.type === "thinking_start") {
|
|
250
|
+
if (phase !== "thinking") closePhase(now, "thinking");
|
|
251
|
+
} else if (ev.type === "text_start") {
|
|
252
|
+
if (phase !== "text") closePhase(now, "text");
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
pi.on("tool_execution_start", (event, ctx) => {
|
|
257
|
+
if (!runActive) return;
|
|
258
|
+
lastCtx = ctx;
|
|
259
|
+
turnDirty = true;
|
|
260
|
+
const now = Date.now();
|
|
261
|
+
if (phase !== "tool") closePhase(now, "tool");
|
|
262
|
+
activeTools.set(event.toolCallId, { name: event.toolName, startMs: now });
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
pi.on("tool_execution_end", (event, ctx) => {
|
|
266
|
+
if (!runActive) return;
|
|
267
|
+
lastCtx = ctx;
|
|
268
|
+
const now = Date.now();
|
|
269
|
+
const active = activeTools.get(event.toolCallId);
|
|
270
|
+
if (!active) return;
|
|
271
|
+
activeTools.delete(event.toolCallId);
|
|
272
|
+
const ms = now - active.startMs;
|
|
273
|
+
const stat = turnTools.get(active.name) ?? {
|
|
274
|
+
name: active.name,
|
|
275
|
+
count: 0,
|
|
276
|
+
totalMs: 0,
|
|
277
|
+
errors: 0,
|
|
278
|
+
};
|
|
279
|
+
stat.count++;
|
|
280
|
+
stat.totalMs += ms;
|
|
281
|
+
if (event.isError) stat.errors++;
|
|
282
|
+
turnTools.set(active.name, stat);
|
|
283
|
+
turnDirty = true;
|
|
284
|
+
if (activeTools.size === 0) closePhase(now, "idle");
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
pi.on("turn_start", (event, ctx) => {
|
|
288
|
+
if (!runActive) return;
|
|
289
|
+
lastCtx = ctx;
|
|
290
|
+
turnIndex = event.turnIndex;
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
pi.on("turn_end", (event, ctx) => {
|
|
294
|
+
if (!runActive) return;
|
|
295
|
+
lastCtx = ctx;
|
|
296
|
+
const now = Date.now();
|
|
297
|
+
if (turnIndex !== event.turnIndex) turnIndex = event.turnIndex;
|
|
298
|
+
closePhase(now, "idle");
|
|
299
|
+
if (turnDirty) finishTurn(now);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
pi.on("agent_end", (event, ctx) => {
|
|
303
|
+
if (!runActive) return;
|
|
304
|
+
lastCtx = ctx;
|
|
305
|
+
// 扫描消息找失败/中止信号(与 notify 一致,仅用于 final 摘要标记)
|
|
306
|
+
for (const msg of event.messages ?? []) {
|
|
307
|
+
if (msg.type === "assistant") {
|
|
308
|
+
if (msg.stopReason === "error" || msg.stopReason === "aborted") {
|
|
309
|
+
if (msg.stopReason === "error") runFailed = true;
|
|
310
|
+
else runAborted = true;
|
|
311
|
+
}
|
|
312
|
+
if (msg.errorMessage) runFailed = true;
|
|
313
|
+
} else if (msg.type === "toolResult" && msg.isError) {
|
|
314
|
+
runFailed = true;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
pi.on("agent_settled", (_event, ctx) => {
|
|
320
|
+
lastCtx = ctx;
|
|
321
|
+
if (!runActive) return;
|
|
322
|
+
const now = Date.now();
|
|
323
|
+
closePhase(now, "idle");
|
|
324
|
+
if (turnDirty) finishTurn(now);
|
|
325
|
+
appendFinal(now);
|
|
326
|
+
if (lastCtx?.hasUI) lastCtx.ui.setWorkingMessage(); // 恢复默认 "Working..."
|
|
327
|
+
runActive = false;
|
|
328
|
+
stopTicker();
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
pi.on("session_shutdown", (_event, _ctx) => {
|
|
332
|
+
// 会话提前结束(如 /exit 或崩溃):尽力补一条最终汇总
|
|
333
|
+
if (runActive) {
|
|
334
|
+
appendFinal(Date.now());
|
|
335
|
+
runActive = false;
|
|
336
|
+
}
|
|
337
|
+
stopTicker();
|
|
338
|
+
lastCtx = undefined;
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// ---------- 会话摘要渲染 ----------
|
|
342
|
+
|
|
343
|
+
pi.registerEntryRenderer<TurnTimingData>(TURN_TYPE, (entry, _opts, theme) => {
|
|
344
|
+
const d = entry.data;
|
|
345
|
+
if (!d) return undefined;
|
|
346
|
+
const segs: string[] = [`turn ${d.turnIndex}`, formatDuration(d.totalMs)];
|
|
347
|
+
const thinking = segmentText("思考", d.thinkingMs);
|
|
348
|
+
if (thinking) segs.push(thinking);
|
|
349
|
+
const text = segmentText("回复", d.textMs);
|
|
350
|
+
if (text) segs.push(text);
|
|
351
|
+
const tools = toolListText(d.tools);
|
|
352
|
+
if (tools) segs.push(tools);
|
|
353
|
+
|
|
354
|
+
const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
|
|
355
|
+
box.addChild(new Text(theme.fg("dim", `⏱ ${segs.join(" · ")}`), 0, 0));
|
|
356
|
+
if (_opts.expanded && d.tools.length > MAX_TOOLS_SHOWN) {
|
|
357
|
+
const rest = [...d.tools]
|
|
358
|
+
.sort((x, y) => y.totalMs - x.totalMs)
|
|
359
|
+
.slice(MAX_TOOLS_SHOWN);
|
|
360
|
+
for (const t of rest) {
|
|
361
|
+
box.addChild(
|
|
362
|
+
new Text(theme.fg("dim", ` ${t.name}×${t.count} ${formatDuration(t.totalMs)}`), 0, 0),
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return box;
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
pi.registerEntryRenderer<FinalTimingData>(FINAL_TYPE, (entry, _opts, theme) => {
|
|
370
|
+
const d = entry.data;
|
|
371
|
+
if (!d) return undefined;
|
|
372
|
+
const title = d.failed
|
|
373
|
+
? theme.fg("error", "❌ 任务失败 总耗时")
|
|
374
|
+
: d.aborted
|
|
375
|
+
? theme.fg("dim", "⏹ 任务中止 总耗时")
|
|
376
|
+
: theme.fg("accent", "✅ 任务完成 总耗时");
|
|
377
|
+
const segs: string[] = [formatDuration(d.totalMs)];
|
|
378
|
+
const thinking = segmentText("思考", d.thinkingMs, d.thinkingPct);
|
|
379
|
+
if (thinking) segs.push(thinking);
|
|
380
|
+
const text = segmentText("回复", d.textMs);
|
|
381
|
+
if (text) segs.push(text);
|
|
382
|
+
const tools = toolListText(d.tools);
|
|
383
|
+
if (tools) segs.push(tools);
|
|
384
|
+
segs.push(`${d.turnCount} turns`);
|
|
385
|
+
|
|
386
|
+
const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
|
|
387
|
+
box.addChild(new Text(`${title} ${theme.fg("dim", segs.join(" · "))}`, 0, 0));
|
|
388
|
+
if (_opts.expanded && d.tools.length > MAX_TOOLS_SHOWN) {
|
|
389
|
+
const rest = [...d.tools]
|
|
390
|
+
.sort((x, y) => y.totalMs - x.totalMs)
|
|
391
|
+
.slice(MAX_TOOLS_SHOWN);
|
|
392
|
+
for (const t of rest) {
|
|
393
|
+
box.addChild(
|
|
394
|
+
new Text(theme.fg("dim", ` ${t.name}×${t.count} ${formatDuration(t.totalMs)}`), 0, 0),
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return box;
|
|
399
|
+
});
|
|
400
|
+
}
|