token-stats-timer 1.0.6 → 1.0.7
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 +5 -13
- package/package.json +1 -1
- package/step-timer.ts +49 -272
package/README.md
CHANGED
|
@@ -54,23 +54,15 @@ Footer 下行:cwd + git 分支 + 其他扩展状态。
|
|
|
54
54
|
|
|
55
55
|
`/notify` —— 无参查看当前状态;`on`/`off` 开启/关闭。
|
|
56
56
|
|
|
57
|
-
##
|
|
58
|
-
|
|
59
|
-
任务执行中实时显示每步耗时,结束后在会话里留下可回看的耗时摘要(`appendEntry` 持久化、不进入 LLM 上下文,`/resume` 后仍在)。
|
|
60
|
-
|
|
61
|
-
### 实时文案
|
|
57
|
+
## 任务计时(step-timer)
|
|
62
58
|
|
|
63
59
|
- 任务执行中:工作指示器(spinner 文案)显示 `Working... 01:02`(整体已耗时,每秒刷新)
|
|
64
|
-
- 思考阶段:隐藏思考块文案显示 `Thinking... 00:45
|
|
65
|
-
|
|
66
|
-
> 说明:`Thinking...` 文案只在“隐藏思考块”模式下显示;开启思考可见时该处不显示文案,但计时不受影响。
|
|
67
|
-
|
|
68
|
-
### 会话摘要
|
|
60
|
+
- 思考阶段:隐藏思考块文案显示 `Thinking... 00:45`(当前思考块已耗时,每秒刷新;思考结束自动恢复)
|
|
61
|
+
- 完成后:会话末尾插入一条汇总 `✅ 任务完成 总耗时 01:23`(失败时 ❌,中止时 ⏹),经 `appendEntry` 持久化、不进入 LLM 上下文,`/resume` 后仍在
|
|
69
62
|
|
|
70
|
-
|
|
71
|
-
- **完成**:任务结束时插入一条汇总:`✅ 任务完成 总耗时 01:23 · 思考 00:45 (54%) · 回复 00:17 · bash×3 00:21 · 6 turns`;失败/中止时标题相应变为 ❌/⏹
|
|
63
|
+
> `Thinking...` 文案只在“隐藏思考块”模式下显示;开启思考可见时该处不显示文案,但计时不受影响。
|
|
72
64
|
|
|
73
|
-
无独立开关,随包启用;计时口径与 run-timer 一致(一次 run = 首个 agent_start → agent_settled
|
|
65
|
+
无独立开关,随包启用;计时口径与 run-timer 一致(一次 run = 首个 agent_start → agent_settled,含重试/压缩/排队提示)。早期版本的每 turn 摘要(timing-turn)已移除,历史条目自动隐藏。
|
|
74
66
|
|
|
75
67
|
## 命令
|
|
76
68
|
|
package/package.json
CHANGED
package/step-timer.ts
CHANGED
|
@@ -1,59 +1,29 @@
|
|
|
1
|
-
// step-timer 模块 ——
|
|
1
|
+
// step-timer 模块 —— 任务计时
|
|
2
2
|
// =============================================================================
|
|
3
3
|
// 实时显示(仅交互 TUI):
|
|
4
|
-
// -
|
|
5
|
-
// -
|
|
6
|
-
//
|
|
7
|
-
// -
|
|
8
|
-
// - agent_settled appendEntry("timing-final", …):总耗时 + 思考占比 + 各工具统计
|
|
4
|
+
// - 任务执行中:工作指示器文案显示 "Working... 01:02"(整体已耗时,每秒刷新)
|
|
5
|
+
// - 思考阶段:隐藏思考块文案显示 "Thinking... 00:45"(当前思考块已耗时)
|
|
6
|
+
// 完成后汇总(appendEntry 持久化,不进入 LLM 上下文,resume 后可回看):
|
|
7
|
+
// - agent_settled appendEntry("timing-final", …):本次任务总耗时
|
|
9
8
|
//
|
|
10
9
|
// 一次 run = 空闲后的首个 agent_start → agent_settled(与 run-timer 语义一致,
|
|
11
10
|
// 包含重试、压缩恢复和排队的 steering/follow-up 提示)。
|
|
11
|
+
//
|
|
12
|
+
// 注意:早期版本曾用 "timing-turn" 每 turn 摘要,该渲染器已移除,
|
|
13
|
+
// 历史遗留的 timing-turn 条目因无渲染器而自动不再显示。
|
|
12
14
|
|
|
13
|
-
import type {
|
|
14
|
-
ExtensionAPI,
|
|
15
|
-
ExtensionContext,
|
|
16
|
-
Theme,
|
|
17
|
-
} from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
18
16
|
import { Box, Text } from "@earendil-works/pi-tui";
|
|
19
17
|
|
|
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
18
|
/** 一次 run 的总耗时汇总(appendEntry "timing-final") */
|
|
40
19
|
export interface FinalTimingData {
|
|
41
20
|
totalMs: number;
|
|
42
|
-
thinkingMs: number;
|
|
43
|
-
thinkingPct: number;
|
|
44
|
-
textMs: number;
|
|
45
|
-
tools: ToolStat[];
|
|
46
|
-
turnCount: number;
|
|
47
21
|
failed: boolean;
|
|
48
22
|
aborted: boolean;
|
|
49
23
|
}
|
|
50
24
|
|
|
51
|
-
export type { ExtensionContext };
|
|
52
|
-
|
|
53
|
-
const TURN_TYPE = "timing-turn";
|
|
54
25
|
const FINAL_TYPE = "timing-final";
|
|
55
26
|
const TICK_MS = 1000;
|
|
56
|
-
const MAX_TOOLS_SHOWN = 4;
|
|
57
27
|
|
|
58
28
|
function formatDuration(ms: number): string {
|
|
59
29
|
const totalSeconds = Math.max(0, Math.floor(ms / 1000));
|
|
@@ -67,33 +37,6 @@ function formatDuration(ms: number): string {
|
|
|
67
37
|
return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
|
|
68
38
|
}
|
|
69
39
|
|
|
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
40
|
export function createStepTimer(pi: ExtensionAPI): void {
|
|
98
41
|
let lastCtx: ExtensionContext | undefined;
|
|
99
42
|
let tick: ReturnType<typeof setInterval> | undefined;
|
|
@@ -103,98 +46,27 @@ export function createStepTimer(pi: ExtensionAPI): void {
|
|
|
103
46
|
let runFailed = false;
|
|
104
47
|
let runAborted = false;
|
|
105
48
|
|
|
106
|
-
//
|
|
107
|
-
let
|
|
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
|
-
// ---------- 内部工具 ----------
|
|
49
|
+
// 思考阶段状态(仅用于 Thinking... 文案计时)
|
|
50
|
+
let thinkingSinceMs: number | undefined;
|
|
124
51
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
}
|
|
52
|
+
/** 结束思考阶段并恢复默认 "Thinking..." 文案(若当前处于思考中) */
|
|
53
|
+
function endThinking(): void {
|
|
54
|
+
if (thinkingSinceMs === undefined) return;
|
|
55
|
+
thinkingSinceMs = undefined;
|
|
56
|
+
if (lastCtx?.hasUI) lastCtx.ui.setHiddenThinkingLabel();
|
|
193
57
|
}
|
|
194
58
|
|
|
195
59
|
function startTicker(): void {
|
|
196
60
|
stopTicker();
|
|
197
|
-
tick = setInterval(() =>
|
|
61
|
+
tick = setInterval(() => {
|
|
62
|
+
if (!lastCtx?.hasUI || !runActive) return;
|
|
63
|
+
const now = Date.now();
|
|
64
|
+
const ui = lastCtx.ui;
|
|
65
|
+
ui.setWorkingMessage(`Working... ${formatDuration(now - runStartMs)}`);
|
|
66
|
+
if (thinkingSinceMs !== undefined) {
|
|
67
|
+
ui.setHiddenThinkingLabel(`Thinking... ${formatDuration(now - thinkingSinceMs)}`);
|
|
68
|
+
}
|
|
69
|
+
}, TICK_MS);
|
|
198
70
|
}
|
|
199
71
|
|
|
200
72
|
function stopTicker(): void {
|
|
@@ -203,26 +75,14 @@ export function createStepTimer(pi: ExtensionAPI): void {
|
|
|
203
75
|
tick = undefined;
|
|
204
76
|
}
|
|
205
77
|
|
|
206
|
-
function
|
|
207
|
-
|
|
208
|
-
|
|
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,
|
|
78
|
+
function appendFinal(): void {
|
|
79
|
+
pi.appendEntry<FinalTimingData>(FINAL_TYPE, {
|
|
80
|
+
totalMs: Date.now() - runStartMs,
|
|
215
81
|
failed: runFailed,
|
|
216
82
|
aborted: runAborted,
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function appendFinal(now: number): void {
|
|
221
|
-
pi.appendEntry<FinalTimingData>(FINAL_TYPE, buildFinal(now));
|
|
83
|
+
});
|
|
222
84
|
}
|
|
223
85
|
|
|
224
|
-
// ---------- 事件 ----------
|
|
225
|
-
|
|
226
86
|
pi.on("session_start", (_event, ctx) => {
|
|
227
87
|
lastCtx = ctx;
|
|
228
88
|
stopTicker();
|
|
@@ -234,75 +94,40 @@ export function createStepTimer(pi: ExtensionAPI): void {
|
|
|
234
94
|
if (runActive) return;
|
|
235
95
|
runActive = true;
|
|
236
96
|
runStartMs = Date.now();
|
|
237
|
-
|
|
97
|
+
runFailed = false;
|
|
98
|
+
runAborted = false;
|
|
99
|
+
thinkingSinceMs = undefined;
|
|
238
100
|
startTicker();
|
|
239
|
-
|
|
101
|
+
if (lastCtx?.hasUI) lastCtx.ui.setWorkingMessage("Working... 00:00");
|
|
240
102
|
});
|
|
241
103
|
|
|
242
104
|
pi.on("message_update", (event, ctx) => {
|
|
243
105
|
if (!runActive) return;
|
|
244
106
|
lastCtx = ctx;
|
|
245
|
-
turnDirty = true;
|
|
246
107
|
const ev = event.assistantMessageEvent;
|
|
247
|
-
const now = Date.now();
|
|
248
108
|
if (!ev) return;
|
|
109
|
+
const now = Date.now();
|
|
249
110
|
if (ev.type === "thinking_start") {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
111
|
+
thinkingSinceMs = now;
|
|
112
|
+
if (lastCtx?.hasUI) {
|
|
113
|
+
lastCtx.ui.setHiddenThinkingLabel("Thinking... 00:00");
|
|
114
|
+
}
|
|
115
|
+
} else if (ev.type === "thinking_end" || ev.type === "text_start") {
|
|
116
|
+
endThinking();
|
|
253
117
|
}
|
|
254
118
|
});
|
|
255
119
|
|
|
256
|
-
pi.on("tool_execution_start", (
|
|
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) => {
|
|
120
|
+
pi.on("tool_execution_start", (_event, ctx) => {
|
|
294
121
|
if (!runActive) return;
|
|
295
122
|
lastCtx = ctx;
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
closePhase(now, "idle");
|
|
299
|
-
if (turnDirty) finishTurn(now);
|
|
123
|
+
// 工具执行时恢复正常文案,避免 Thinking 计时残留
|
|
124
|
+
endThinking();
|
|
300
125
|
});
|
|
301
126
|
|
|
302
127
|
pi.on("agent_end", (event, ctx) => {
|
|
303
128
|
if (!runActive) return;
|
|
304
129
|
lastCtx = ctx;
|
|
305
|
-
//
|
|
130
|
+
// 扫描消息找失败/中止信号,仅用于最终摘要素材
|
|
306
131
|
for (const msg of event.messages ?? []) {
|
|
307
132
|
if (msg.type === "assistant") {
|
|
308
133
|
if (msg.stopReason === "error" || msg.stopReason === "aborted") {
|
|
@@ -319,10 +144,8 @@ export function createStepTimer(pi: ExtensionAPI): void {
|
|
|
319
144
|
pi.on("agent_settled", (_event, ctx) => {
|
|
320
145
|
lastCtx = ctx;
|
|
321
146
|
if (!runActive) return;
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
if (turnDirty) finishTurn(now);
|
|
325
|
-
appendFinal(now);
|
|
147
|
+
endThinking();
|
|
148
|
+
appendFinal();
|
|
326
149
|
if (lastCtx?.hasUI) lastCtx.ui.setWorkingMessage(); // 恢复默认 "Working..."
|
|
327
150
|
runActive = false;
|
|
328
151
|
stopTicker();
|
|
@@ -331,41 +154,14 @@ export function createStepTimer(pi: ExtensionAPI): void {
|
|
|
331
154
|
pi.on("session_shutdown", (_event, _ctx) => {
|
|
332
155
|
// 会话提前结束(如 /exit 或崩溃):尽力补一条最终汇总
|
|
333
156
|
if (runActive) {
|
|
334
|
-
|
|
157
|
+
endThinking();
|
|
158
|
+
appendFinal();
|
|
335
159
|
runActive = false;
|
|
336
160
|
}
|
|
337
161
|
stopTicker();
|
|
338
162
|
lastCtx = undefined;
|
|
339
163
|
});
|
|
340
164
|
|
|
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
165
|
pi.registerEntryRenderer<FinalTimingData>(FINAL_TYPE, (entry, _opts, theme) => {
|
|
370
166
|
const d = entry.data;
|
|
371
167
|
if (!d) return undefined;
|
|
@@ -374,27 +170,8 @@ export function createStepTimer(pi: ExtensionAPI): void {
|
|
|
374
170
|
: d.aborted
|
|
375
171
|
? theme.fg("dim", "⏹ 任务中止 总耗时")
|
|
376
172
|
: 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
173
|
const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
|
|
387
|
-
box.addChild(new Text(`${title} ${
|
|
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
|
-
}
|
|
174
|
+
box.addChild(new Text(`${title} ${formatDuration(d.totalMs)}`, 0, 0));
|
|
398
175
|
return box;
|
|
399
176
|
});
|
|
400
177
|
}
|