pi-conversation-timer 1.0.0

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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +51 -0
  3. package/index.ts +223 -0
  4. package/package.json +37 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pi Community Contributor
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # pi-conversation-timer
2
+
3
+ > Active task timer and statusline extension for [Pi Coding Agent](https://pi.dev).
4
+
5
+ `pi-conversation-timer` tracks pure active task execution time across prompt submission to full settlement (`agent_settled`), keeping time static when idle.
6
+
7
+ ## ✨ Features
8
+
9
+ - **Zero-drift active tracking**: Only counts active work duration while Pi is processing, calling tools, and generating responses. Time stays completely static when waiting for user input.
10
+ - **Whole-task lifecycle accounting**: Accurately tracks the entire duration from prompt submission (`before_agent_start`) to final completion (`agent_settled`), across multiple tool turns and iterations.
11
+ - **Clean integer seconds**: Output format strictly uses integer seconds (e.g. `12s`, `45s`, `1m 20s`, `1h 05m`), avoiding confusing decimals.
12
+ - **Theme-aware statusline**: Native TUI integration using `ctx.ui.setStatus` with current theme coloring.
13
+ - **Detailed statistics command**: `/chat-time` displays complete session metrics including total active time, task counts, and average duration per task.
14
+
15
+ ## 📦 Install
16
+
17
+ ```bash
18
+ pi install npm:pi-conversation-timer
19
+ ```
20
+
21
+ Or install from git:
22
+
23
+ ```bash
24
+ pi install git:github.com/Agonieler/pi-conversation-timer
25
+ ```
26
+
27
+ Restart Pi or run `/reload` after installation.
28
+
29
+ ## 🚀 Usage
30
+
31
+ Once loaded, the timer appears automatically in the bottom statusline:
32
+
33
+ - **Idle state**:
34
+ ```text
35
+ ⏱ 累计 45s · 上轮 12s
36
+ ```
37
+ - **Working state**:
38
+ ```text
39
+ ⏱ 累计 52s · 工作中 7s
40
+ ```
41
+
42
+ ### Commands
43
+
44
+ | Command | Description |
45
+ | --- | --- |
46
+ | `/chat-time` | View detailed active time breakdown and task statistics in a popup |
47
+ | `/dialogue-time` | Alias for `/chat-time` |
48
+
49
+ ## 📄 License
50
+
51
+ MIT
package/index.ts ADDED
@@ -0,0 +1,223 @@
1
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+
3
+ /**
4
+ * 格式化毫秒数为人类易读的时间字符串(秒数纯整数,不带小数)
5
+ * @param ms 毫秒数
6
+ */
7
+ function formatDuration(ms: number): string {
8
+ if (ms <= 0) return "0s";
9
+ const totalSeconds = Math.round(ms / 1000);
10
+ if (totalSeconds < 60) {
11
+ return `${totalSeconds}s`;
12
+ }
13
+ const sec = totalSeconds % 60;
14
+ const totalMinutes = Math.floor(totalSeconds / 60);
15
+ if (totalMinutes < 60) {
16
+ return `${totalMinutes}m ${sec}s`;
17
+ }
18
+ const min = totalMinutes % 60;
19
+ const hours = Math.floor(totalMinutes / 60);
20
+ return `${hours}h ${min}m`;
21
+ }
22
+
23
+ export default function (pi: ExtensionAPI) {
24
+ let timerHandle: NodeJS.Timeout | null = null;
25
+ let totalActiveTimeMs = 0; // 累计工作耗时(所有任务从发出到完成的纯工作总耗时)
26
+ let currentTaskStartTime: number | null = null; // 当前任务启动时间戳(发出任务开始)
27
+ let lastTaskDuration: number | null = null; // 上轮任务完整耗时(从发出到工作结束)
28
+ let taskCount = 0; // 任务计数
29
+ let activeContext: ExtensionContext | null = null;
30
+
31
+ /**
32
+ * 生成状态栏文本
33
+ */
34
+ function getStatusText(ctx: ExtensionContext): string {
35
+ const theme = ctx.ui.theme;
36
+ const icon = theme ? theme.fg("accent", "⏱") : "⏱";
37
+
38
+ // 1. 任务正在进行中(从用户发出任务一直到工作结束)
39
+ if (currentTaskStartTime !== null) {
40
+ const taskElapsed = Math.max(0, Date.now() - currentTaskStartTime);
41
+ const currentTotal = totalActiveTimeMs + taskElapsed;
42
+ const totalText = theme ? theme.fg("text", ` 累计 ${formatDuration(currentTotal)}`) : ` 累计 ${formatDuration(currentTotal)}`;
43
+ const taskInfo = theme ? theme.fg("accent", ` · 工作中 ${formatDuration(taskElapsed)}`) : ` · 工作中 ${formatDuration(taskElapsed)}`;
44
+ return `${icon}${totalText}${taskInfo}`;
45
+ }
46
+
47
+ // 2. 空闲等待状态(没有活跃任务,时间静止不涨秒)
48
+ if (taskCount === 0 && totalActiveTimeMs === 0) {
49
+ const readyText = theme ? theme.fg("dim", " 就绪") : " 就绪";
50
+ return `${icon}${readyText}`;
51
+ }
52
+
53
+ const totalText = theme ? theme.fg("text", ` 累计 ${formatDuration(totalActiveTimeMs)}`) : ` 累计 ${formatDuration(totalActiveTimeMs)}`;
54
+ let lastTaskInfo = "";
55
+ if (lastTaskDuration !== null) {
56
+ lastTaskInfo = theme ? theme.fg("dim", ` · 上轮 ${formatDuration(lastTaskDuration)}`) : ` · 上轮 ${formatDuration(lastTaskDuration)}`;
57
+ }
58
+
59
+ return `${icon}${totalText}${lastTaskInfo}`;
60
+ }
61
+
62
+ /**
63
+ * 刷新状态栏显示
64
+ */
65
+ function updateStatus(ctx: ExtensionContext) {
66
+ if (!ctx.hasUI) return;
67
+ try {
68
+ const text = getStatusText(ctx);
69
+ ctx.ui.setStatus("conversation-timer", text);
70
+ } catch {
71
+ // 忽略折叠或界面刷新偶发异常
72
+ }
73
+ }
74
+
75
+ /**
76
+ * 停止走表定时器
77
+ */
78
+ function stopActiveTimer() {
79
+ if (timerHandle) {
80
+ clearInterval(timerHandle);
81
+ timerHandle = null;
82
+ }
83
+ }
84
+
85
+ /**
86
+ * 启动任务执行期间的走表定时器(每 500ms 刷新一次界面,展示整数秒变化)
87
+ */
88
+ function startActiveTimer(ctx: ExtensionContext) {
89
+ stopActiveTimer();
90
+ timerHandle = setInterval(() => {
91
+ if (activeContext) {
92
+ updateStatus(activeContext);
93
+ }
94
+ }, 500);
95
+ }
96
+
97
+ /**
98
+ * 当用户发出任务,AI 开始介入工作时调用
99
+ */
100
+ function handleTaskStart(ctx: ExtensionContext) {
101
+ activeContext = ctx;
102
+ if (currentTaskStartTime === null) {
103
+ currentTaskStartTime = Date.now();
104
+ taskCount++;
105
+ updateStatus(ctx);
106
+ startActiveTimer(ctx);
107
+ }
108
+ }
109
+
110
+ /**
111
+ * 当整个任务的所有轮次、工具调用、重试全部彻底完成时调用
112
+ */
113
+ function handleTaskSettled(ctx: ExtensionContext) {
114
+ activeContext = ctx;
115
+ stopActiveTimer();
116
+
117
+ if (currentTaskStartTime !== null) {
118
+ const duration = Date.now() - currentTaskStartTime;
119
+ totalActiveTimeMs += duration;
120
+ lastTaskDuration = duration;
121
+ currentTaskStartTime = null;
122
+ }
123
+
124
+ // 刷新为静止状态,等待下一次任务发出
125
+ updateStatus(ctx);
126
+ }
127
+
128
+ // 会话启动或恢复
129
+ pi.on("session_start", async (_event, ctx) => {
130
+ activeContext = ctx;
131
+ stopActiveTimer();
132
+ totalActiveTimeMs = 0;
133
+ taskCount = 0;
134
+ lastTaskDuration = null;
135
+ currentTaskStartTime = null;
136
+
137
+ // 尝试从历史消息中恢复交互耗时
138
+ try {
139
+ const entries = ctx.sessionManager?.getEntries?.() ?? [];
140
+ let lastUserTs: number | null = null;
141
+ for (const entry of entries) {
142
+ if ((entry as any).type === "message") {
143
+ const msg = (entry as any).message;
144
+ if (msg?.role === "user" && typeof msg.timestamp === "number") {
145
+ lastUserTs = msg.timestamp;
146
+ } else if (msg?.role === "assistant" && typeof msg.timestamp === "number" && lastUserTs !== null) {
147
+ const duration = Math.max(0, msg.timestamp - lastUserTs);
148
+ totalActiveTimeMs += duration;
149
+ lastTaskDuration = duration;
150
+ taskCount++;
151
+ lastUserTs = null;
152
+ }
153
+ }
154
+ }
155
+ } catch {
156
+ // 默认从 0 开始
157
+ }
158
+
159
+ updateStatus(ctx);
160
+ });
161
+
162
+ // 用户发出提示词/任务提交
163
+ pi.on("before_agent_start", async (_event, ctx) => {
164
+ handleTaskStart(ctx);
165
+ });
166
+
167
+ // 代理工作循环启动
168
+ pi.on("agent_start", async (_event, ctx) => {
169
+ handleTaskStart(ctx);
170
+ });
171
+
172
+ // 任务彻底结束(包括所有中间步骤、工具执行与重试)
173
+ pi.on("agent_settled", async (_event, ctx) => {
174
+ handleTaskSettled(ctx);
175
+ });
176
+
177
+ // 会话关闭或重置
178
+ pi.on("session_shutdown", async (_event, ctx) => {
179
+ stopActiveTimer();
180
+ if (ctx && ctx.hasUI) {
181
+ try {
182
+ ctx.ui.setStatus("conversation-timer", undefined);
183
+ } catch {
184
+ // ignore
185
+ }
186
+ }
187
+ activeContext = null;
188
+ currentTaskStartTime = null;
189
+ });
190
+
191
+ // 注册详细耗时查询命令
192
+ const showTimeHandler = async (_args: string, ctx: ExtensionContext) => {
193
+ const lines = [
194
+ `⏱ 对话任务纯工作耗时统计:`,
195
+ `• 累计工作时长: ${formatDuration(totalActiveTimeMs)}`,
196
+ `• 任务总次数: ${taskCount} 次`,
197
+ ];
198
+
199
+ if (taskCount > 0) {
200
+ const avg = Math.round(totalActiveTimeMs / taskCount);
201
+ lines.push(`• 平均每轮耗时: ${formatDuration(avg)}`);
202
+ }
203
+
204
+ if (currentTaskStartTime !== null) {
205
+ const currentElapsed = Math.max(0, Date.now() - currentTaskStartTime);
206
+ lines.push(`• 当前任务状态: 正在工作中 (${formatDuration(currentElapsed)})`);
207
+ } else if (lastTaskDuration !== null) {
208
+ lines.push(`• 上轮任务耗时: ${formatDuration(lastTaskDuration)} (从发出任务到工作全部结束)`);
209
+ }
210
+
211
+ ctx.ui.notify(lines.join("\n"), "info");
212
+ };
213
+
214
+ pi.registerCommand("chat-time", {
215
+ description: "显示发出任务到工作结束的纯工作耗时统计",
216
+ handler: showTimeHandler,
217
+ });
218
+
219
+ pi.registerCommand("dialogue-time", {
220
+ description: "显示发出任务到工作结束的纯工作耗时统计 (别名)",
221
+ handler: showTimeHandler,
222
+ });
223
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "pi-conversation-timer",
3
+ "version": "1.0.0",
4
+ "description": "Active task timer extension for Pi coding agent",
5
+ "author": "Agonieler",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "./index.ts",
9
+ "files": [
10
+ "index.ts",
11
+ "README.md",
12
+ "LICENSE"
13
+ ],
14
+ "keywords": [
15
+ "pi-package",
16
+ "pi-extension",
17
+ "pi",
18
+ "timer",
19
+ "statusline"
20
+ ],
21
+ "pi": {
22
+ "extensions": [
23
+ "./index.ts"
24
+ ]
25
+ },
26
+ "peerDependencies": {
27
+ "@earendil-works/pi-coding-agent": ">=0.80.0"
28
+ },
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/Agonieler/pi-conversation-timer.git"
32
+ },
33
+ "homepage": "https://github.com/Agonieler/pi-conversation-timer#readme",
34
+ "bugs": {
35
+ "url": "https://github.com/Agonieler/pi-conversation-timer/issues"
36
+ }
37
+ }