token-stats-timer 1.0.7 → 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 +2 -4
- package/package.json +1 -1
- package/step-timer.ts +50 -44
package/README.md
CHANGED
|
@@ -57,10 +57,8 @@ Footer 下行:cwd + git 分支 + 其他扩展状态。
|
|
|
57
57
|
## 任务计时(step-timer)
|
|
58
58
|
|
|
59
59
|
- 任务执行中:工作指示器(spinner 文案)显示 `Working... 01:02`(整体已耗时,每秒刷新)
|
|
60
|
-
-
|
|
61
|
-
- 完成后:会话末尾插入一条汇总
|
|
62
|
-
|
|
63
|
-
> `Thinking...` 文案只在“隐藏思考块”模式下显示;开启思考可见时该处不显示文案,但计时不受影响。
|
|
60
|
+
- 逐条内联标注:每条 assistant 消息末尾自动追加一行 `> ⏱ 思考 00:05`——当前正在生成的消息该行实时更新,提交后永久定格;历史消息各自保留提交时的耗时、互不影响。因 pi 的 hiddenThinkingLabel 文案是全局的、无法逐条保留,故采用该内联方式(resume 回放不显示)
|
|
61
|
+
- 完成后:会话末尾插入一条汇总 `总耗时 01:23`(失败时 ❌,中止时 ⏹),经 `appendEntry` 持久化、不进入 LLM 上下文,`/resume` 后仍在
|
|
64
62
|
|
|
65
63
|
无独立开关,随包启用;计时口径与 run-timer 一致(一次 run = 首个 agent_start → agent_settled,含重试/压缩/排队提示)。早期版本的每 turn 摘要(timing-turn)已移除,历史条目自动隐藏。
|
|
66
64
|
|
package/package.json
CHANGED
package/step-timer.ts
CHANGED
|
@@ -2,15 +2,16 @@
|
|
|
2
2
|
// =============================================================================
|
|
3
3
|
// 实时显示(仅交互 TUI):
|
|
4
4
|
// - 任务执行中:工作指示器文案显示 "Working... 01:02"(整体已耗时,每秒刷新)
|
|
5
|
-
//
|
|
5
|
+
// 逐条内联标注(每条 assistant 消息末尾自动追加一行):
|
|
6
|
+
// - 例:`ai 回复内容...` 后另起一行 `> ⏱ 思考 00:05`
|
|
7
|
+
// - 当前正在生成的消息:该行随流式内容实时更新(思考计时);消息提交后永久定格
|
|
8
|
+
// - 历史消息各自保留提交时的耗时,互不影响、不再变化(标注已存在则跳过,resume 回放不显示)
|
|
9
|
+
// (pi 的 hiddenThinkingLabel 文案是全局的、无法逐条保留,故改用内联标注)
|
|
6
10
|
// 完成后汇总(appendEntry 持久化,不进入 LLM 上下文,resume 后可回看):
|
|
7
11
|
// - agent_settled appendEntry("timing-final", …):本次任务总耗时
|
|
8
12
|
//
|
|
9
13
|
// 一次 run = 空闲后的首个 agent_start → agent_settled(与 run-timer 语义一致,
|
|
10
14
|
// 包含重试、压缩恢复和排队的 steering/follow-up 提示)。
|
|
11
|
-
//
|
|
12
|
-
// 注意:早期版本曾用 "timing-turn" 每 turn 摘要,该渲染器已移除,
|
|
13
|
-
// 历史遗留的 timing-turn 条目因无渲染器而自动不再显示。
|
|
14
15
|
|
|
15
16
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
16
17
|
import { Box, Text } from "@earendil-works/pi-tui";
|
|
@@ -46,26 +47,32 @@ export function createStepTimer(pi: ExtensionAPI): void {
|
|
|
46
47
|
let runFailed = false;
|
|
47
48
|
let runAborted = false;
|
|
48
49
|
|
|
49
|
-
//
|
|
50
|
-
|
|
50
|
+
// 逐条内联标注:当前消息的思考计时(每条 assistant 消息开始时重置)
|
|
51
|
+
// curThinkingMs:已结算的思考毫秒;thinkingStart:进行中思考块的起表时间
|
|
52
|
+
let curThinkingMs = 0;
|
|
53
|
+
let thinkingStart: number | undefined;
|
|
54
|
+
|
|
55
|
+
/** 结算当前进行中的思考块(若有) */
|
|
56
|
+
function settleThinking(now: number): void {
|
|
57
|
+
if (thinkingStart !== undefined) {
|
|
58
|
+
curThinkingMs += now - thinkingStart;
|
|
59
|
+
thinkingStart = undefined;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
51
62
|
|
|
52
|
-
/**
|
|
53
|
-
function
|
|
54
|
-
if (
|
|
55
|
-
|
|
56
|
-
if (lastCtx?.hasUI) lastCtx.ui.setHiddenThinkingLabel();
|
|
63
|
+
/** 当前消息的思考总耗时(含进行中块):有思考数据才显示 */
|
|
64
|
+
function currentThinkingMs(now: number): number | undefined {
|
|
65
|
+
if (curThinkingMs === 0 && thinkingStart === undefined) return undefined;
|
|
66
|
+
return curThinkingMs + (thinkingStart !== undefined ? now - thinkingStart : 0);
|
|
57
67
|
}
|
|
58
68
|
|
|
69
|
+
const MARK_TEXT = "⏱ 思考";
|
|
70
|
+
|
|
59
71
|
function startTicker(): void {
|
|
60
72
|
stopTicker();
|
|
61
73
|
tick = setInterval(() => {
|
|
62
74
|
if (!lastCtx?.hasUI || !runActive) return;
|
|
63
|
-
|
|
64
|
-
const ui = lastCtx.ui;
|
|
65
|
-
ui.setWorkingMessage(`Working... ${formatDuration(now - runStartMs)}`);
|
|
66
|
-
if (thinkingSinceMs !== undefined) {
|
|
67
|
-
ui.setHiddenThinkingLabel(`Thinking... ${formatDuration(now - thinkingSinceMs)}`);
|
|
68
|
-
}
|
|
75
|
+
lastCtx.ui.setWorkingMessage(`Working... ${formatDuration(Date.now() - runStartMs)}`);
|
|
69
76
|
}, TICK_MS);
|
|
70
77
|
}
|
|
71
78
|
|
|
@@ -96,11 +103,20 @@ export function createStepTimer(pi: ExtensionAPI): void {
|
|
|
96
103
|
runStartMs = Date.now();
|
|
97
104
|
runFailed = false;
|
|
98
105
|
runAborted = false;
|
|
99
|
-
|
|
106
|
+
curThinkingMs = 0;
|
|
107
|
+
thinkingStart = undefined;
|
|
100
108
|
startTicker();
|
|
101
109
|
if (lastCtx?.hasUI) lastCtx.ui.setWorkingMessage("Working... 00:00");
|
|
102
110
|
});
|
|
103
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;
|
|
118
|
+
});
|
|
119
|
+
|
|
104
120
|
pi.on("message_update", (event, ctx) => {
|
|
105
121
|
if (!runActive) return;
|
|
106
122
|
lastCtx = ctx;
|
|
@@ -108,43 +124,27 @@ export function createStepTimer(pi: ExtensionAPI): void {
|
|
|
108
124
|
if (!ev) return;
|
|
109
125
|
const now = Date.now();
|
|
110
126
|
if (ev.type === "thinking_start") {
|
|
111
|
-
|
|
112
|
-
if (lastCtx?.hasUI) {
|
|
113
|
-
lastCtx.ui.setHiddenThinkingLabel("Thinking... 00:00");
|
|
114
|
-
}
|
|
127
|
+
if (thinkingStart === undefined) thinkingStart = now;
|
|
115
128
|
} else if (ev.type === "thinking_end" || ev.type === "text_start") {
|
|
116
|
-
|
|
129
|
+
settleThinking(now);
|
|
117
130
|
}
|
|
118
131
|
});
|
|
119
132
|
|
|
120
133
|
pi.on("tool_execution_start", (_event, ctx) => {
|
|
121
134
|
if (!runActive) return;
|
|
122
135
|
lastCtx = ctx;
|
|
123
|
-
//
|
|
124
|
-
|
|
136
|
+
// 工具执行通常在同一消息的 toolCall 内容之后:结算思考计时
|
|
137
|
+
settleThinking(Date.now());
|
|
125
138
|
});
|
|
126
139
|
|
|
127
140
|
pi.on("agent_end", (event, ctx) => {
|
|
128
141
|
if (!runActive) return;
|
|
129
142
|
lastCtx = ctx;
|
|
130
|
-
// 扫描消息找失败/中止信号,仅用于最终摘要素材
|
|
131
|
-
for (const msg of event.messages ?? []) {
|
|
132
|
-
if (msg.type === "assistant") {
|
|
133
|
-
if (msg.stopReason === "error" || msg.stopReason === "aborted") {
|
|
134
|
-
if (msg.stopReason === "error") runFailed = true;
|
|
135
|
-
else runAborted = true;
|
|
136
|
-
}
|
|
137
|
-
if (msg.errorMessage) runFailed = true;
|
|
138
|
-
} else if (msg.type === "toolResult" && msg.isError) {
|
|
139
|
-
runFailed = true;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
143
|
});
|
|
143
144
|
|
|
144
145
|
pi.on("agent_settled", (_event, ctx) => {
|
|
145
146
|
lastCtx = ctx;
|
|
146
147
|
if (!runActive) return;
|
|
147
|
-
endThinking();
|
|
148
148
|
appendFinal();
|
|
149
149
|
if (lastCtx?.hasUI) lastCtx.ui.setWorkingMessage(); // 恢复默认 "Working..."
|
|
150
150
|
runActive = false;
|
|
@@ -154,7 +154,6 @@ export function createStepTimer(pi: ExtensionAPI): void {
|
|
|
154
154
|
pi.on("session_shutdown", (_event, _ctx) => {
|
|
155
155
|
// 会话提前结束(如 /exit 或崩溃):尽力补一条最终汇总
|
|
156
156
|
if (runActive) {
|
|
157
|
-
endThinking();
|
|
158
157
|
appendFinal();
|
|
159
158
|
runActive = false;
|
|
160
159
|
}
|
|
@@ -162,14 +161,21 @@ export function createStepTimer(pi: ExtensionAPI): void {
|
|
|
162
161
|
lastCtx = undefined;
|
|
163
162
|
});
|
|
164
163
|
|
|
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)}`;
|
|
173
|
+
});
|
|
174
|
+
|
|
165
175
|
pi.registerEntryRenderer<FinalTimingData>(FINAL_TYPE, (entry, _opts, theme) => {
|
|
166
176
|
const d = entry.data;
|
|
167
177
|
if (!d) return undefined;
|
|
168
|
-
const title =
|
|
169
|
-
? theme.fg("error", "❌ 任务失败 总耗时")
|
|
170
|
-
: d.aborted
|
|
171
|
-
? theme.fg("dim", "⏹ 任务中止 总耗时")
|
|
172
|
-
: theme.fg("accent", "✅ 任务完成 总耗时");
|
|
178
|
+
const title = theme.fg("accent", "总耗时");
|
|
173
179
|
const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
|
|
174
180
|
box.addChild(new Text(`${title} ${formatDuration(d.totalMs)}`, 0, 0));
|
|
175
181
|
return box;
|