pi-conversation-timer 1.0.0 → 1.0.1
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/index.ts +120 -18
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
1
4
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
5
|
|
|
3
6
|
/**
|
|
@@ -28,6 +31,30 @@ export default function (pi: ExtensionAPI) {
|
|
|
28
31
|
let taskCount = 0; // 任务计数
|
|
29
32
|
let activeContext: ExtensionContext | null = null;
|
|
30
33
|
|
|
34
|
+
const cachePath = join(homedir(), ".pi", "agent", "conversation-timer-cache.json");
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 保存当前会话的状态到本地缓存
|
|
38
|
+
*/
|
|
39
|
+
function saveStateToCache(ctx: ExtensionContext) {
|
|
40
|
+
try {
|
|
41
|
+
const sessionKey = ctx.sessionManager?.getSessionFile?.() ?? "default";
|
|
42
|
+
let allData: Record<string, any> = {};
|
|
43
|
+
if (existsSync(cachePath)) {
|
|
44
|
+
allData = JSON.parse(readFileSync(cachePath, "utf-8"));
|
|
45
|
+
}
|
|
46
|
+
allData[sessionKey] = {
|
|
47
|
+
totalActiveTimeMs,
|
|
48
|
+
lastTaskDuration,
|
|
49
|
+
taskCount,
|
|
50
|
+
updatedAt: Date.now(),
|
|
51
|
+
};
|
|
52
|
+
writeFileSync(cachePath, JSON.stringify(allData, null, 2), "utf-8");
|
|
53
|
+
} catch {
|
|
54
|
+
// ignore
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
31
58
|
/**
|
|
32
59
|
* 生成状态栏文本
|
|
33
60
|
*/
|
|
@@ -119,6 +146,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
119
146
|
totalActiveTimeMs += duration;
|
|
120
147
|
lastTaskDuration = duration;
|
|
121
148
|
currentTaskStartTime = null;
|
|
149
|
+
|
|
150
|
+
// 持久化当前状态到 session 记录和本地缓存,确保 reload 绝不丢失
|
|
151
|
+
try {
|
|
152
|
+
pi.appendEntry("conversation-timer-state", {
|
|
153
|
+
totalActiveTimeMs,
|
|
154
|
+
lastTaskDuration,
|
|
155
|
+
taskCount,
|
|
156
|
+
});
|
|
157
|
+
} catch {
|
|
158
|
+
// ignore
|
|
159
|
+
}
|
|
160
|
+
saveStateToCache(ctx);
|
|
122
161
|
}
|
|
123
162
|
|
|
124
163
|
// 刷新为静止状态,等待下一次任务发出
|
|
@@ -126,34 +165,94 @@ export default function (pi: ExtensionAPI) {
|
|
|
126
165
|
}
|
|
127
166
|
|
|
128
167
|
// 会话启动或恢复
|
|
129
|
-
pi.on("session_start", async (
|
|
168
|
+
pi.on("session_start", async (event, ctx) => {
|
|
130
169
|
activeContext = ctx;
|
|
131
170
|
stopActiveTimer();
|
|
132
|
-
totalActiveTimeMs = 0;
|
|
133
|
-
taskCount = 0;
|
|
134
|
-
lastTaskDuration = null;
|
|
135
|
-
currentTaskStartTime = null;
|
|
136
171
|
|
|
137
|
-
//
|
|
172
|
+
// 只有在明确新开会话(/new)时才归零;reload、resume 等情况必须保留历史时间
|
|
173
|
+
if (event.reason === "new") {
|
|
174
|
+
totalActiveTimeMs = 0;
|
|
175
|
+
taskCount = 0;
|
|
176
|
+
lastTaskDuration = null;
|
|
177
|
+
currentTaskStartTime = null;
|
|
178
|
+
updateStatus(ctx);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
let restored = false;
|
|
183
|
+
const sessionKey = ctx.sessionManager?.getSessionFile?.() ?? "default";
|
|
184
|
+
|
|
185
|
+
// 1. 优先从 session 自定义持久化记录中恢复
|
|
138
186
|
try {
|
|
139
187
|
const entries = ctx.sessionManager?.getEntries?.() ?? [];
|
|
140
|
-
let lastUserTs: number | null = null;
|
|
141
188
|
for (const entry of entries) {
|
|
142
|
-
if ((entry as any).type === "
|
|
143
|
-
const
|
|
144
|
-
if (
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
lastTaskDuration = duration;
|
|
150
|
-
taskCount++;
|
|
151
|
-
lastUserTs = null;
|
|
189
|
+
if ((entry as any).type === "custom" && (entry as any).customType === "conversation-timer-state") {
|
|
190
|
+
const data = (entry as any).data;
|
|
191
|
+
if (data && typeof data.totalActiveTimeMs === "number") {
|
|
192
|
+
totalActiveTimeMs = data.totalActiveTimeMs;
|
|
193
|
+
lastTaskDuration = data.lastTaskDuration ?? null;
|
|
194
|
+
taskCount = data.taskCount ?? 0;
|
|
195
|
+
restored = true;
|
|
152
196
|
}
|
|
153
197
|
}
|
|
154
198
|
}
|
|
155
199
|
} catch {
|
|
156
|
-
//
|
|
200
|
+
// ignore
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// 2. 如果 session 记录中没有,尝试从本地缓存文件中读取
|
|
204
|
+
if (!restored) {
|
|
205
|
+
try {
|
|
206
|
+
if (existsSync(cachePath)) {
|
|
207
|
+
const allData = JSON.parse(readFileSync(cachePath, "utf-8"));
|
|
208
|
+
const cached = allData[sessionKey];
|
|
209
|
+
if (cached && typeof cached.totalActiveTimeMs === "number") {
|
|
210
|
+
totalActiveTimeMs = cached.totalActiveTimeMs;
|
|
211
|
+
lastTaskDuration = cached.lastTaskDuration ?? null;
|
|
212
|
+
taskCount = cached.taskCount ?? 0;
|
|
213
|
+
restored = true;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
} catch {
|
|
217
|
+
// ignore
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// 3. 兜底:如果未曾记录过(旧会话),从历史消息精准计算
|
|
222
|
+
if (!restored) {
|
|
223
|
+
try {
|
|
224
|
+
const entries = ctx.sessionManager?.getEntries?.() ?? [];
|
|
225
|
+
let curStart: number | null = null;
|
|
226
|
+
let accTime = 0;
|
|
227
|
+
let count = 0;
|
|
228
|
+
let lastDur: number | null = null;
|
|
229
|
+
|
|
230
|
+
for (const entry of entries) {
|
|
231
|
+
if ((entry as any).type === "message") {
|
|
232
|
+
const msg = (entry as any).message;
|
|
233
|
+
if (msg?.role === "user" && typeof msg.timestamp === "number") {
|
|
234
|
+
curStart = msg.timestamp;
|
|
235
|
+
} else if (msg?.role === "assistant" && typeof msg.timestamp === "number" && curStart !== null) {
|
|
236
|
+
// 只有当不是 toolUse 中断时,才算整个任务真正完成
|
|
237
|
+
if (msg.stopReason !== "toolUse") {
|
|
238
|
+
const dur = Math.max(0, msg.timestamp - curStart);
|
|
239
|
+
accTime += dur;
|
|
240
|
+
lastDur = dur;
|
|
241
|
+
count++;
|
|
242
|
+
curStart = null;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (accTime > 0) {
|
|
249
|
+
totalActiveTimeMs = accTime;
|
|
250
|
+
lastTaskDuration = lastDur;
|
|
251
|
+
taskCount = count;
|
|
252
|
+
}
|
|
253
|
+
} catch {
|
|
254
|
+
// ignore
|
|
255
|
+
}
|
|
157
256
|
}
|
|
158
257
|
|
|
159
258
|
updateStatus(ctx);
|
|
@@ -177,6 +276,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
177
276
|
// 会话关闭或重置
|
|
178
277
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
179
278
|
stopActiveTimer();
|
|
279
|
+
if (activeContext) {
|
|
280
|
+
saveStateToCache(activeContext);
|
|
281
|
+
}
|
|
180
282
|
if (ctx && ctx.hasUI) {
|
|
181
283
|
try {
|
|
182
284
|
ctx.ui.setStatus("conversation-timer", undefined);
|