token-stats-timer 1.0.6 → 1.0.8
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 +4 -14
- package/package.json +1 -1
- package/step-timer.ts +65 -282
package/README.md
CHANGED
|
@@ -54,23 +54,13 @@ 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
|
-
-
|
|
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`;失败/中止时标题相应变为 ❌/⏹
|
|
60
|
+
- 逐条内联标注:每条 assistant 消息末尾自动追加一行 `> ⏱ 思考 00:05`——当前正在生成的消息该行实时更新,提交后永久定格;历史消息各自保留提交时的耗时、互不影响。因 pi 的 hiddenThinkingLabel 文案是全局的、无法逐条保留,故采用该内联方式(resume 回放不显示)
|
|
61
|
+
- 完成后:会话末尾插入一条汇总 `总耗时 01:23`(失败时 ❌,中止时 ⏹),经 `appendEntry` 持久化、不进入 LLM 上下文,`/resume` 后仍在
|
|
72
62
|
|
|
73
|
-
无独立开关,随包启用;计时口径与 run-timer 一致(一次 run = 首个 agent_start → agent_settled
|
|
63
|
+
无独立开关,随包启用;计时口径与 run-timer 一致(一次 run = 首个 agent_start → agent_settled,含重试/压缩/排队提示)。早期版本的每 turn 摘要(timing-turn)已移除,历史条目自动隐藏。
|
|
74
64
|
|
|
75
65
|
## 命令
|
|
76
66
|
|
package/package.json
CHANGED
package/step-timer.ts
CHANGED
|
@@ -1,59 +1,30 @@
|
|
|
1
|
-
// step-timer 模块 ——
|
|
1
|
+
// step-timer 模块 —— 任务计时
|
|
2
2
|
// =============================================================================
|
|
3
3
|
// 实时显示(仅交互 TUI):
|
|
4
|
-
// -
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// -
|
|
8
|
-
// -
|
|
4
|
+
// - 任务执行中:工作指示器文案显示 "Working... 01:02"(整体已耗时,每秒刷新)
|
|
5
|
+
// 逐条内联标注(每条 assistant 消息末尾自动追加一行):
|
|
6
|
+
// - 例:`ai 回复内容...` 后另起一行 `> ⏱ 思考 00:05`
|
|
7
|
+
// - 当前正在生成的消息:该行随流式内容实时更新(思考计时);消息提交后永久定格
|
|
8
|
+
// - 历史消息各自保留提交时的耗时,互不影响、不再变化(标注已存在则跳过,resume 回放不显示)
|
|
9
|
+
// (pi 的 hiddenThinkingLabel 文案是全局的、无法逐条保留,故改用内联标注)
|
|
10
|
+
// 完成后汇总(appendEntry 持久化,不进入 LLM 上下文,resume 后可回看):
|
|
11
|
+
// - agent_settled appendEntry("timing-final", …):本次任务总耗时
|
|
9
12
|
//
|
|
10
13
|
// 一次 run = 空闲后的首个 agent_start → agent_settled(与 run-timer 语义一致,
|
|
11
14
|
// 包含重试、压缩恢复和排队的 steering/follow-up 提示)。
|
|
12
15
|
|
|
13
|
-
import type {
|
|
14
|
-
ExtensionAPI,
|
|
15
|
-
ExtensionContext,
|
|
16
|
-
Theme,
|
|
17
|
-
} from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
18
17
|
import { Box, Text } from "@earendil-works/pi-tui";
|
|
19
18
|
|
|
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
19
|
/** 一次 run 的总耗时汇总(appendEntry "timing-final") */
|
|
40
20
|
export interface FinalTimingData {
|
|
41
21
|
totalMs: number;
|
|
42
|
-
thinkingMs: number;
|
|
43
|
-
thinkingPct: number;
|
|
44
|
-
textMs: number;
|
|
45
|
-
tools: ToolStat[];
|
|
46
|
-
turnCount: number;
|
|
47
22
|
failed: boolean;
|
|
48
23
|
aborted: boolean;
|
|
49
24
|
}
|
|
50
25
|
|
|
51
|
-
export type { ExtensionContext };
|
|
52
|
-
|
|
53
|
-
const TURN_TYPE = "timing-turn";
|
|
54
26
|
const FINAL_TYPE = "timing-final";
|
|
55
27
|
const TICK_MS = 1000;
|
|
56
|
-
const MAX_TOOLS_SHOWN = 4;
|
|
57
28
|
|
|
58
29
|
function formatDuration(ms: number): string {
|
|
59
30
|
const totalSeconds = Math.max(0, Math.floor(ms / 1000));
|
|
@@ -67,33 +38,6 @@ function formatDuration(ms: number): string {
|
|
|
67
38
|
return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
|
|
68
39
|
}
|
|
69
40
|
|
|
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
41
|
export function createStepTimer(pi: ExtensionAPI): void {
|
|
98
42
|
let lastCtx: ExtensionContext | undefined;
|
|
99
43
|
let tick: ReturnType<typeof setInterval> | undefined;
|
|
@@ -103,98 +47,33 @@ export function createStepTimer(pi: ExtensionAPI): void {
|
|
|
103
47
|
let runFailed = false;
|
|
104
48
|
let runAborted = false;
|
|
105
49
|
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
let
|
|
109
|
-
|
|
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;
|
|
50
|
+
// 逐条内联标注:当前消息的思考计时(每条 assistant 消息开始时重置)
|
|
51
|
+
// curThinkingMs:已结算的思考毫秒;thinkingStart:进行中思考块的起表时间
|
|
52
|
+
let curThinkingMs = 0;
|
|
53
|
+
let thinkingStart: number | undefined;
|
|
122
54
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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;
|
|
55
|
+
/** 结算当前进行中的思考块(若有) */
|
|
56
|
+
function settleThinking(now: number): void {
|
|
57
|
+
if (thinkingStart !== undefined) {
|
|
58
|
+
curThinkingMs += now - thinkingStart;
|
|
59
|
+
thinkingStart = undefined;
|
|
149
60
|
}
|
|
150
|
-
// "tool" 阶段耗时按 toolCallId 单独计时(activeTools),不走阶段时钟
|
|
151
|
-
phase = next;
|
|
152
|
-
phaseStartMs = now;
|
|
153
61
|
}
|
|
154
62
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
return
|
|
63
|
+
/** 当前消息的思考总耗时(含进行中块):有思考数据才显示 */
|
|
64
|
+
function currentThinkingMs(now: number): number | undefined {
|
|
65
|
+
if (curThinkingMs === 0 && thinkingStart === undefined) return undefined;
|
|
66
|
+
return curThinkingMs + (thinkingStart !== undefined ? now - thinkingStart : 0);
|
|
159
67
|
}
|
|
160
68
|
|
|
161
|
-
|
|
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
|
-
}
|
|
69
|
+
const MARK_TEXT = "⏱ 思考";
|
|
194
70
|
|
|
195
71
|
function startTicker(): void {
|
|
196
72
|
stopTicker();
|
|
197
|
-
tick = setInterval(() =>
|
|
73
|
+
tick = setInterval(() => {
|
|
74
|
+
if (!lastCtx?.hasUI || !runActive) return;
|
|
75
|
+
lastCtx.ui.setWorkingMessage(`Working... ${formatDuration(Date.now() - runStartMs)}`);
|
|
76
|
+
}, TICK_MS);
|
|
198
77
|
}
|
|
199
78
|
|
|
200
79
|
function stopTicker(): void {
|
|
@@ -203,26 +82,14 @@ export function createStepTimer(pi: ExtensionAPI): void {
|
|
|
203
82
|
tick = undefined;
|
|
204
83
|
}
|
|
205
84
|
|
|
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,
|
|
85
|
+
function appendFinal(): void {
|
|
86
|
+
pi.appendEntry<FinalTimingData>(FINAL_TYPE, {
|
|
87
|
+
totalMs: Date.now() - runStartMs,
|
|
215
88
|
failed: runFailed,
|
|
216
89
|
aborted: runAborted,
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function appendFinal(now: number): void {
|
|
221
|
-
pi.appendEntry<FinalTimingData>(FINAL_TYPE, buildFinal(now));
|
|
90
|
+
});
|
|
222
91
|
}
|
|
223
92
|
|
|
224
|
-
// ---------- 事件 ----------
|
|
225
|
-
|
|
226
93
|
pi.on("session_start", (_event, ctx) => {
|
|
227
94
|
lastCtx = ctx;
|
|
228
95
|
stopTicker();
|
|
@@ -234,95 +101,51 @@ export function createStepTimer(pi: ExtensionAPI): void {
|
|
|
234
101
|
if (runActive) return;
|
|
235
102
|
runActive = true;
|
|
236
103
|
runStartMs = Date.now();
|
|
237
|
-
|
|
104
|
+
runFailed = false;
|
|
105
|
+
runAborted = false;
|
|
106
|
+
curThinkingMs = 0;
|
|
107
|
+
thinkingStart = undefined;
|
|
238
108
|
startTicker();
|
|
239
|
-
|
|
109
|
+
if (lastCtx?.hasUI) lastCtx.ui.setWorkingMessage("Working... 00:00");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// 每条 assistant 消息开始时重置思考计时
|
|
113
|
+
pi.on("message_start", (event, ctx) => {
|
|
114
|
+
if (!runActive || event.message.role !== "assistant") return;
|
|
115
|
+
lastCtx = ctx;
|
|
116
|
+
curThinkingMs = 0;
|
|
117
|
+
thinkingStart = undefined;
|
|
240
118
|
});
|
|
241
119
|
|
|
242
120
|
pi.on("message_update", (event, ctx) => {
|
|
243
121
|
if (!runActive) return;
|
|
244
122
|
lastCtx = ctx;
|
|
245
|
-
turnDirty = true;
|
|
246
123
|
const ev = event.assistantMessageEvent;
|
|
247
|
-
const now = Date.now();
|
|
248
124
|
if (!ev) return;
|
|
125
|
+
const now = Date.now();
|
|
249
126
|
if (ev.type === "thinking_start") {
|
|
250
|
-
if (
|
|
251
|
-
} else if (ev.type === "text_start") {
|
|
252
|
-
|
|
127
|
+
if (thinkingStart === undefined) thinkingStart = now;
|
|
128
|
+
} else if (ev.type === "thinking_end" || ev.type === "text_start") {
|
|
129
|
+
settleThinking(now);
|
|
253
130
|
}
|
|
254
131
|
});
|
|
255
132
|
|
|
256
|
-
pi.on("tool_execution_start", (
|
|
133
|
+
pi.on("tool_execution_start", (_event, ctx) => {
|
|
257
134
|
if (!runActive) return;
|
|
258
135
|
lastCtx = ctx;
|
|
259
|
-
|
|
260
|
-
|
|
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);
|
|
136
|
+
// 工具执行通常在同一消息的 toolCall 内容之后:结算思考计时
|
|
137
|
+
settleThinking(Date.now());
|
|
300
138
|
});
|
|
301
139
|
|
|
302
140
|
pi.on("agent_end", (event, ctx) => {
|
|
303
141
|
if (!runActive) return;
|
|
304
142
|
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
143
|
});
|
|
318
144
|
|
|
319
145
|
pi.on("agent_settled", (_event, ctx) => {
|
|
320
146
|
lastCtx = ctx;
|
|
321
147
|
if (!runActive) return;
|
|
322
|
-
|
|
323
|
-
closePhase(now, "idle");
|
|
324
|
-
if (turnDirty) finishTurn(now);
|
|
325
|
-
appendFinal(now);
|
|
148
|
+
appendFinal();
|
|
326
149
|
if (lastCtx?.hasUI) lastCtx.ui.setWorkingMessage(); // 恢复默认 "Working..."
|
|
327
150
|
runActive = false;
|
|
328
151
|
stopTicker();
|
|
@@ -331,70 +154,30 @@ export function createStepTimer(pi: ExtensionAPI): void {
|
|
|
331
154
|
pi.on("session_shutdown", (_event, _ctx) => {
|
|
332
155
|
// 会话提前结束(如 /exit 或崩溃):尽力补一条最终汇总
|
|
333
156
|
if (runActive) {
|
|
334
|
-
appendFinal(
|
|
157
|
+
appendFinal();
|
|
335
158
|
runActive = false;
|
|
336
159
|
}
|
|
337
160
|
stopTicker();
|
|
338
161
|
lastCtx = undefined;
|
|
339
162
|
});
|
|
340
163
|
|
|
341
|
-
// ----------
|
|
342
|
-
|
|
343
|
-
pi.
|
|
344
|
-
|
|
345
|
-
if (
|
|
346
|
-
const
|
|
347
|
-
const
|
|
348
|
-
if (
|
|
349
|
-
|
|
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;
|
|
164
|
+
// ---------- 逐条内联标注 ----------
|
|
165
|
+
// 只在 assistant 文本上追加一行耗时;已带标注(历史消息重渲染)则跳过保持原值
|
|
166
|
+
pi.registerMarkdownTransformer((markdown, context) => {
|
|
167
|
+
if (context.messageType !== "assistant") return markdown;
|
|
168
|
+
if (markdown.includes(MARK_TEXT)) return markdown;
|
|
169
|
+
const now = Date.now();
|
|
170
|
+
const ms = currentThinkingMs(now);
|
|
171
|
+
if (ms === undefined) return markdown;
|
|
172
|
+
return `${markdown}\n\n> ${MARK_TEXT} ${formatDuration(ms)}`;
|
|
367
173
|
});
|
|
368
174
|
|
|
369
175
|
pi.registerEntryRenderer<FinalTimingData>(FINAL_TYPE, (entry, _opts, theme) => {
|
|
370
176
|
const d = entry.data;
|
|
371
177
|
if (!d) return undefined;
|
|
372
|
-
const title =
|
|
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
|
-
|
|
178
|
+
const title = theme.fg("accent", "总耗时");
|
|
386
179
|
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
|
-
}
|
|
180
|
+
box.addChild(new Text(`${title} ${formatDuration(d.totalMs)}`, 0, 0));
|
|
398
181
|
return box;
|
|
399
182
|
});
|
|
400
183
|
}
|