token-stats-timer 1.0.5 → 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 CHANGED
@@ -54,6 +54,16 @@ Footer 下行:cwd + git 分支 + 其他扩展状态。
54
54
 
55
55
  `/notify` —— 无参查看当前状态;`on`/`off` 开启/关闭。
56
56
 
57
+ ## 任务计时(step-timer)
58
+
59
+ - 任务执行中:工作指示器(spinner 文案)显示 `Working... 01:02`(整体已耗时,每秒刷新)
60
+ - 思考阶段:隐藏思考块文案显示 `Thinking... 00:45`(当前思考块已耗时,每秒刷新;思考结束自动恢复)
61
+ - 完成后:会话末尾插入一条汇总 `✅ 任务完成 总耗时 01:23`(失败时 ❌,中止时 ⏹),经 `appendEntry` 持久化、不进入 LLM 上下文,`/resume` 后仍在
62
+
63
+ > `Thinking...` 文案只在“隐藏思考块”模式下显示;开启思考可见时该处不显示文案,但计时不受影响。
64
+
65
+ 无独立开关,随包启用;计时口径与 run-timer 一致(一次 run = 首个 agent_start → agent_settled,含重试/压缩/排队提示)。早期版本的每 turn 摘要(timing-turn)已移除,历史条目自动隐藏。
66
+
57
67
  ## 命令
58
68
 
59
69
  - `/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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "token-stats-timer",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
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,177 @@
1
+ // step-timer 模块 —— 任务计时
2
+ // =============================================================================
3
+ // 实时显示(仅交互 TUI):
4
+ // - 任务执行中:工作指示器文案显示 "Working... 01:02"(整体已耗时,每秒刷新)
5
+ // - 思考阶段:隐藏思考块文案显示 "Thinking... 00:45"(当前思考块已耗时)
6
+ // 完成后汇总(appendEntry 持久化,不进入 LLM 上下文,resume 后可回看):
7
+ // - agent_settled appendEntry("timing-final", …):本次任务总耗时
8
+ //
9
+ // 一次 run = 空闲后的首个 agent_start → agent_settled(与 run-timer 语义一致,
10
+ // 包含重试、压缩恢复和排队的 steering/follow-up 提示)。
11
+ //
12
+ // 注意:早期版本曾用 "timing-turn" 每 turn 摘要,该渲染器已移除,
13
+ // 历史遗留的 timing-turn 条目因无渲染器而自动不再显示。
14
+
15
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
16
+ import { Box, Text } from "@earendil-works/pi-tui";
17
+
18
+ /** 一次 run 的总耗时汇总(appendEntry "timing-final") */
19
+ export interface FinalTimingData {
20
+ totalMs: number;
21
+ failed: boolean;
22
+ aborted: boolean;
23
+ }
24
+
25
+ const FINAL_TYPE = "timing-final";
26
+ const TICK_MS = 1000;
27
+
28
+ function formatDuration(ms: number): string {
29
+ const totalSeconds = Math.max(0, Math.floor(ms / 1000));
30
+ const hours = Math.floor(totalSeconds / 3600);
31
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
32
+ const seconds = totalSeconds % 60;
33
+
34
+ if (hours > 0) {
35
+ return `${hours}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
36
+ }
37
+ return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
38
+ }
39
+
40
+ export function createStepTimer(pi: ExtensionAPI): void {
41
+ let lastCtx: ExtensionContext | undefined;
42
+ let tick: ReturnType<typeof setInterval> | undefined;
43
+
44
+ let runActive = false;
45
+ let runStartMs = 0;
46
+ let runFailed = false;
47
+ let runAborted = false;
48
+
49
+ // 思考阶段状态(仅用于 Thinking... 文案计时)
50
+ let thinkingSinceMs: number | undefined;
51
+
52
+ /** 结束思考阶段并恢复默认 "Thinking..." 文案(若当前处于思考中) */
53
+ function endThinking(): void {
54
+ if (thinkingSinceMs === undefined) return;
55
+ thinkingSinceMs = undefined;
56
+ if (lastCtx?.hasUI) lastCtx.ui.setHiddenThinkingLabel();
57
+ }
58
+
59
+ function startTicker(): void {
60
+ stopTicker();
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);
70
+ }
71
+
72
+ function stopTicker(): void {
73
+ if (!tick) return;
74
+ clearInterval(tick);
75
+ tick = undefined;
76
+ }
77
+
78
+ function appendFinal(): void {
79
+ pi.appendEntry<FinalTimingData>(FINAL_TYPE, {
80
+ totalMs: Date.now() - runStartMs,
81
+ failed: runFailed,
82
+ aborted: runAborted,
83
+ });
84
+ }
85
+
86
+ pi.on("session_start", (_event, ctx) => {
87
+ lastCtx = ctx;
88
+ stopTicker();
89
+ runActive = false;
90
+ });
91
+
92
+ pi.on("agent_start", (_event, ctx) => {
93
+ lastCtx = ctx;
94
+ if (runActive) return;
95
+ runActive = true;
96
+ runStartMs = Date.now();
97
+ runFailed = false;
98
+ runAborted = false;
99
+ thinkingSinceMs = undefined;
100
+ startTicker();
101
+ if (lastCtx?.hasUI) lastCtx.ui.setWorkingMessage("Working... 00:00");
102
+ });
103
+
104
+ pi.on("message_update", (event, ctx) => {
105
+ if (!runActive) return;
106
+ lastCtx = ctx;
107
+ const ev = event.assistantMessageEvent;
108
+ if (!ev) return;
109
+ const now = Date.now();
110
+ if (ev.type === "thinking_start") {
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();
117
+ }
118
+ });
119
+
120
+ pi.on("tool_execution_start", (_event, ctx) => {
121
+ if (!runActive) return;
122
+ lastCtx = ctx;
123
+ // 工具执行时恢复正常文案,避免 Thinking 计时残留
124
+ endThinking();
125
+ });
126
+
127
+ pi.on("agent_end", (event, ctx) => {
128
+ if (!runActive) return;
129
+ 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
+
144
+ pi.on("agent_settled", (_event, ctx) => {
145
+ lastCtx = ctx;
146
+ if (!runActive) return;
147
+ endThinking();
148
+ appendFinal();
149
+ if (lastCtx?.hasUI) lastCtx.ui.setWorkingMessage(); // 恢复默认 "Working..."
150
+ runActive = false;
151
+ stopTicker();
152
+ });
153
+
154
+ pi.on("session_shutdown", (_event, _ctx) => {
155
+ // 会话提前结束(如 /exit 或崩溃):尽力补一条最终汇总
156
+ if (runActive) {
157
+ endThinking();
158
+ appendFinal();
159
+ runActive = false;
160
+ }
161
+ stopTicker();
162
+ lastCtx = undefined;
163
+ });
164
+
165
+ pi.registerEntryRenderer<FinalTimingData>(FINAL_TYPE, (entry, _opts, theme) => {
166
+ const d = entry.data;
167
+ if (!d) return undefined;
168
+ const title = d.failed
169
+ ? theme.fg("error", "❌ 任务失败 总耗时")
170
+ : d.aborted
171
+ ? theme.fg("dim", "⏹ 任务中止 总耗时")
172
+ : theme.fg("accent", "✅ 任务完成 总耗时");
173
+ const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
174
+ box.addChild(new Text(`${title} ${formatDuration(d.totalMs)}`, 0, 0));
175
+ return box;
176
+ });
177
+ }