@ia-ccun/code-agent-cli 0.0.52-beta.2 → 0.0.52-beta.3
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/config/agent/extensions/aicode-advisor.ts +13 -0
- package/config/agent/extensions/aicode-btw.ts +66 -14
- package/config/agent/extensions/aicode-pets.ts +362 -0
- package/config/agent/extensions/aicode-tips.ts +19 -10
- package/config/agent/extensions/custom.ts +0 -32
- package/config/agent/extensions/tps.ts +87 -0
- package/config/agent/extensions/working-msg.ts +160 -66
- package/config/agent/settings.json +2 -1
- package/package.json +1 -1
|
@@ -146,6 +146,19 @@ function setDisabledForModels(models: Array<string | { model: string; minEffort?
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
// ============== 工具清单序列化 ==============
|
|
149
|
+
/**
|
|
150
|
+
* stableStringify - 稳定序列化对象(键排序)
|
|
151
|
+
*
|
|
152
|
+
* 用于序列化工具参数,确保参数对象的键顺序一致,便于对比和展示。
|
|
153
|
+
*
|
|
154
|
+
* 功能说明:
|
|
155
|
+
* - 处理 null 和非对象类型:直接 JSON.stringify
|
|
156
|
+
* - 处理数组:递归序列化每个元素,undefined 替换为 "null"
|
|
157
|
+
* - 处理对象:键排序后序列化,跳过 undefined 值
|
|
158
|
+
*
|
|
159
|
+
* @param value - 要序列化的值
|
|
160
|
+
* @returns 稳定的 JSON 字符串(键排序)
|
|
161
|
+
*/
|
|
149
162
|
function stableStringify(value: unknown): string {
|
|
150
163
|
if (value === null || typeof value !== "object") {
|
|
151
164
|
return JSON.stringify(value);
|
|
@@ -40,6 +40,18 @@ function matchesBtwFocusShortcut(data: string): boolean {
|
|
|
40
40
|
return BTW_FOCUS_SHORTCUTS.some((shortcut) => matchesKey(data, shortcut));
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* 类型守卫:检查 entry 是否是有效的 Message 对象
|
|
45
|
+
* ✅ 用于安全转换,避免使用危险的 as unknown as
|
|
46
|
+
*/
|
|
47
|
+
function isMessageLike(entry: unknown): entry is Partial<Message> & { role?: string; customType?: string; content?: unknown } {
|
|
48
|
+
if (!entry || typeof entry !== "object") {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
const candidate = entry as { role?: unknown; customType?: string; content?: unknown };
|
|
52
|
+
return typeof candidate.role === "string" && Array.isArray(candidate.content);
|
|
53
|
+
}
|
|
54
|
+
|
|
43
55
|
const BTW_SYSTEM_PROMPT = [
|
|
44
56
|
"You are having an aside conversation with the user, separate from their main working session.",
|
|
45
57
|
"If main session messages are provided, they are for context only — that work is being handled by another agent.",
|
|
@@ -277,17 +289,17 @@ function buildBtwSeedState(
|
|
|
277
289
|
(message) => !isVisibleBtwMessage(message),
|
|
278
290
|
),
|
|
279
291
|
);
|
|
280
|
-
} catch {
|
|
292
|
+
} catch (error) {
|
|
293
|
+
// ✅ 改进错误处理:记录错误而不忽略
|
|
294
|
+
console.warn('[btw] buildSessionContext failed, using fallback:', error);
|
|
281
295
|
messages.push(
|
|
282
296
|
...ctx.sessionManager.getEntries().flatMap((entry) => {
|
|
283
|
-
|
|
297
|
+
// ✅ 使用类型守卫进行安全转换,避免 as unknown as
|
|
298
|
+
if (!isMessageLike(entry)) {
|
|
284
299
|
return [];
|
|
285
300
|
}
|
|
286
301
|
|
|
287
|
-
const message = entry
|
|
288
|
-
if (typeof message.role !== "string" || !Array.isArray(message.content)) {
|
|
289
|
-
return [];
|
|
290
|
-
}
|
|
302
|
+
const message = entry; // ✅ 类型安全
|
|
291
303
|
|
|
292
304
|
return isVisibleBtwMessage({ role: message.role, customType: message.customType }) ? [] : [message as Message];
|
|
293
305
|
}),
|
|
@@ -380,7 +392,9 @@ function formatToolPreview(value: unknown): string {
|
|
|
380
392
|
return "";
|
|
381
393
|
}
|
|
382
394
|
return preview.length > 120 ? `${preview.slice(0, 117)}...` : preview;
|
|
383
|
-
} catch {
|
|
395
|
+
} catch (error) {
|
|
396
|
+
// ✅ 改进错误处理:记录错误而不忽略
|
|
397
|
+
console.warn('[btw] JSON.stringify failed in formatToolPreview:', error);
|
|
384
398
|
return "";
|
|
385
399
|
}
|
|
386
400
|
}
|
|
@@ -396,6 +410,26 @@ function createEmptyTranscriptState(): BtwTranscriptState {
|
|
|
396
410
|
};
|
|
397
411
|
}
|
|
398
412
|
|
|
413
|
+
/**
|
|
414
|
+
* clearTranscriptState - 清理 transcriptState 的所有资源
|
|
415
|
+
* ✅ 清理 entries 数组
|
|
416
|
+
* ✅ 清理 toolCalls Map(防止内存泄漏)
|
|
417
|
+
* ✅ 重置所有状态
|
|
418
|
+
*/
|
|
419
|
+
function clearTranscriptState(state: BtwTranscriptState): void {
|
|
420
|
+
// ✅ 清理 toolCalls Map(防止内存泄漏)
|
|
421
|
+
state.toolCalls.clear();
|
|
422
|
+
|
|
423
|
+
// 清理 entries 数组
|
|
424
|
+
state.entries = [];
|
|
425
|
+
|
|
426
|
+
// 重置状态
|
|
427
|
+
state.nextEntryId = 1;
|
|
428
|
+
state.nextTurnId = 1;
|
|
429
|
+
state.currentTurnId = null;
|
|
430
|
+
state.lastTurnId = null;
|
|
431
|
+
}
|
|
432
|
+
|
|
399
433
|
function appendTranscriptEntry<T extends BtwTranscriptEntry>(
|
|
400
434
|
state: BtwTranscriptState,
|
|
401
435
|
entry: Omit<T, "id">,
|
|
@@ -444,6 +478,13 @@ function finishTranscriptTurn(state: BtwTranscriptState, turnId?: number | null)
|
|
|
444
478
|
if (state.currentTurnId === resolvedTurnId) {
|
|
445
479
|
state.currentTurnId = null;
|
|
446
480
|
}
|
|
481
|
+
|
|
482
|
+
// ✅ 清理当前 turn 的 toolCalls(防止内存泄漏)
|
|
483
|
+
for (const [toolCallId, toolCall] of state.toolCalls.entries()) {
|
|
484
|
+
if (toolCall.turnId === resolvedTurnId) {
|
|
485
|
+
state.toolCalls.delete(toolCallId);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
447
488
|
}
|
|
448
489
|
|
|
449
490
|
function removeTranscriptTurn(state: BtwTranscriptState, turnId: number | null): void {
|
|
@@ -574,7 +615,9 @@ function summarizeToolResult(value: unknown, maxLength = 400): { content: string
|
|
|
574
615
|
} else if (value !== undefined) {
|
|
575
616
|
try {
|
|
576
617
|
content = JSON.stringify(value, null, 2);
|
|
577
|
-
} catch {
|
|
618
|
+
} catch (error) {
|
|
619
|
+
// ✅ 改进错误处理:记录错误而不忽略
|
|
620
|
+
console.warn('[btw] JSON.stringify failed in summarizeToolResult:', error);
|
|
578
621
|
content = String(value);
|
|
579
622
|
}
|
|
580
623
|
}
|
|
@@ -1340,8 +1383,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
1340
1383
|
|
|
1341
1384
|
try {
|
|
1342
1385
|
unsubscribe();
|
|
1343
|
-
} catch {
|
|
1344
|
-
//
|
|
1386
|
+
} catch (error) {
|
|
1387
|
+
// ✅ 改进错误处理:记录错误而不忽略
|
|
1388
|
+
console.warn('[btw] Unsubscribe failed during session removal:', error);
|
|
1345
1389
|
}
|
|
1346
1390
|
}
|
|
1347
1391
|
|
|
@@ -1402,6 +1446,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
1402
1446
|
async function disposeBtwSession(): Promise<void> {
|
|
1403
1447
|
const current = activeBtwSession;
|
|
1404
1448
|
activeBtwSession = null;
|
|
1449
|
+
|
|
1450
|
+
// ✅ 清理 transcriptState(防止内存泄漏)
|
|
1451
|
+
if (transcriptState) {
|
|
1452
|
+
clearTranscriptState(transcriptState);
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1405
1455
|
if (!current) {
|
|
1406
1456
|
return;
|
|
1407
1457
|
}
|
|
@@ -1410,8 +1460,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
1410
1460
|
|
|
1411
1461
|
try {
|
|
1412
1462
|
await current.session.abort();
|
|
1413
|
-
} catch {
|
|
1414
|
-
//
|
|
1463
|
+
} catch (error) {
|
|
1464
|
+
// ✅ 改进错误处理:记录错误而不忽略
|
|
1465
|
+
console.warn('[btw] Session abort failed during disposal:', error);
|
|
1415
1466
|
}
|
|
1416
1467
|
|
|
1417
1468
|
current.session.dispose();
|
|
@@ -2144,8 +2195,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
2144
2195
|
} finally {
|
|
2145
2196
|
try {
|
|
2146
2197
|
await session.abort();
|
|
2147
|
-
} catch {
|
|
2148
|
-
//
|
|
2198
|
+
} catch (error) {
|
|
2199
|
+
// ✅ 改进错误处理:记录错误而不忽略
|
|
2200
|
+
console.warn('[btw] Session abort failed during summarize shutdown:', error);
|
|
2149
2201
|
}
|
|
2150
2202
|
session.dispose();
|
|
2151
2203
|
}
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Random Pet Widget Extension
|
|
3
|
+
*
|
|
4
|
+
* Displays a random animal sprite below the input editor.
|
|
5
|
+
* Each session randomly selects from 18 different animals.
|
|
6
|
+
* Inspired by claude-petpet's sprite system.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
10
|
+
|
|
11
|
+
// 帽子定义(参考 claude-petpet)
|
|
12
|
+
const HATS: Record<string, string> = {
|
|
13
|
+
none: "",
|
|
14
|
+
crown: " \\^^^/ ",
|
|
15
|
+
tophat: " [___] ",
|
|
16
|
+
propeller: " -+- ",
|
|
17
|
+
halo: " ( ) ",
|
|
18
|
+
wizard: " /^\\ ",
|
|
19
|
+
beanie: " (___) ",
|
|
20
|
+
tinyduck: " ,> ",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// 18 种动物精灵定义(参考 claude-petpet)
|
|
24
|
+
interface PetDefinition {
|
|
25
|
+
name: string;
|
|
26
|
+
emoji: string;
|
|
27
|
+
frames: string[][];
|
|
28
|
+
speed: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const PETS: Record<string, PetDefinition> = {
|
|
32
|
+
cat: {
|
|
33
|
+
name: "小猫",
|
|
34
|
+
emoji: "🐱",
|
|
35
|
+
frames: [
|
|
36
|
+
[" /\\_/\\ ", " ( o o) ", " ( ω ) ", ' (")_(") '],
|
|
37
|
+
[" /\\_/\\ ", " ( ^ ^) ", " ( ω ) ", ' (")_(") '],
|
|
38
|
+
[" /\\_/\\ ", " ( - -) ", " ( ω ) ", ' (")_(") '],
|
|
39
|
+
],
|
|
40
|
+
speed: 500,
|
|
41
|
+
},
|
|
42
|
+
rabbit: {
|
|
43
|
+
name: "兔子",
|
|
44
|
+
emoji: "🐰",
|
|
45
|
+
frames: [
|
|
46
|
+
[" (\\__/) ", " ( o o ) ", " =( .. )= ", ' (")_(") '],
|
|
47
|
+
[" (\\__/) ", " ( ^ ^ ) ", " =( .. )= ", ' (")_(") '],
|
|
48
|
+
[" (\\__/) ", " ( - - ) ", " =( .. )= ", ' (")_(") '],
|
|
49
|
+
],
|
|
50
|
+
speed: 400,
|
|
51
|
+
},
|
|
52
|
+
dragon: {
|
|
53
|
+
name: "龙",
|
|
54
|
+
emoji: "🐉",
|
|
55
|
+
frames: [
|
|
56
|
+
[" /^\\ /^\\ ", " < o o > ", " ( ~~ ) ", " `-vvvv-´ "],
|
|
57
|
+
[" /^\\ /^\\ ", " < ^ ^ > ", " ( ~~ ) ", " `-vvvv-´ "],
|
|
58
|
+
[" /^\\ /^\\ ", " < - - > ", " ( ~~ ) ", " `-vvvv-´ "],
|
|
59
|
+
],
|
|
60
|
+
speed: 600,
|
|
61
|
+
},
|
|
62
|
+
owl: {
|
|
63
|
+
name: "猫头鹰",
|
|
64
|
+
emoji: "🦉",
|
|
65
|
+
frames: [
|
|
66
|
+
[" /\\ /\\ ", " ((o)(o)) ", " ( >< ) ", " `----´ "],
|
|
67
|
+
[" /\\ /\\ ", " ((^)(^)) ", " ( >< ) ", " `----´ "],
|
|
68
|
+
[" /\\ /\\ ", " ((-)(-)) ", " ( >< ) ", " `----´ "],
|
|
69
|
+
],
|
|
70
|
+
speed: 700,
|
|
71
|
+
},
|
|
72
|
+
penguin: {
|
|
73
|
+
name: "企鹅",
|
|
74
|
+
emoji: "🐧",
|
|
75
|
+
frames: [
|
|
76
|
+
[" .---. ", " (o> o) ", " /( )\\ ", " `---´ "],
|
|
77
|
+
[" .---. ", " (^> ^) ", " /( )\\ ", " `---´ "],
|
|
78
|
+
[" .---. ", " (-> -) ", " /( )\\ ", " `---´ "],
|
|
79
|
+
],
|
|
80
|
+
speed: 500,
|
|
81
|
+
},
|
|
82
|
+
duck: {
|
|
83
|
+
name: "鸭子",
|
|
84
|
+
emoji: "🦆",
|
|
85
|
+
frames: [
|
|
86
|
+
[" __ ", " <(o )___ ", " ( ._> ", " `--´ "],
|
|
87
|
+
[" __ ", " <(^ )___ ", " ( ._> ", " `--´ "],
|
|
88
|
+
[" __ ", " <(- )___ ", " ( ._> ", " `--´ "],
|
|
89
|
+
],
|
|
90
|
+
speed: 450,
|
|
91
|
+
},
|
|
92
|
+
goose: {
|
|
93
|
+
name: "鹅",
|
|
94
|
+
emoji: "🦢",
|
|
95
|
+
frames: [
|
|
96
|
+
[" (o> ", " || ", " _(__)_ ", " ^^^^ "],
|
|
97
|
+
[" (^> ", " || ", " _(__)_ ", " ^^^^ "],
|
|
98
|
+
[" (- > ", " || ", " _(__)_ ", " ^^^^ "],
|
|
99
|
+
],
|
|
100
|
+
speed: 500,
|
|
101
|
+
},
|
|
102
|
+
turtle: {
|
|
103
|
+
name: "乌龟",
|
|
104
|
+
emoji: "🐢",
|
|
105
|
+
frames: [
|
|
106
|
+
[" _,--._ ", " ( o o ) ", " /[______]\\ ", " `` `` "],
|
|
107
|
+
[" _,--._ ", " ( ^ ^ ) ", " /[______]\\ ", " `` `` "],
|
|
108
|
+
[" _,--._ ", " ( - - ) ", " /[______]\\ ", " `` `` "],
|
|
109
|
+
],
|
|
110
|
+
speed: 800,
|
|
111
|
+
},
|
|
112
|
+
snail: {
|
|
113
|
+
name: "蜗牛",
|
|
114
|
+
emoji: "🐌",
|
|
115
|
+
frames: [
|
|
116
|
+
[" · .--. ", " \\ ( @ ) ", " \\_`--´ ", " ~~~~~~~ "],
|
|
117
|
+
[" ✦ .--. ", " \\ ( @ ) ", " \\_`--´ ", " ~~~~~~~ "],
|
|
118
|
+
[" ◉ .--. ", " \\ ( @ ) ", " \\_`--´ ", " ~~~~~~~ "],
|
|
119
|
+
],
|
|
120
|
+
speed: 900,
|
|
121
|
+
},
|
|
122
|
+
capybara: {
|
|
123
|
+
name: "水豚",
|
|
124
|
+
emoji: "🦫",
|
|
125
|
+
frames: [
|
|
126
|
+
[" n______n ", " ( o o ) ", " ( oo ) ", " `------´ "],
|
|
127
|
+
[" n______n ", " ( ^ ^ ) ", " ( oo ) ", " `------´ "],
|
|
128
|
+
[" n______n ", " ( - - ) ", " ( oo ) ", " `------´ "],
|
|
129
|
+
],
|
|
130
|
+
speed: 600,
|
|
131
|
+
},
|
|
132
|
+
ghost: {
|
|
133
|
+
name: "幽灵",
|
|
134
|
+
emoji: "👻",
|
|
135
|
+
frames: [
|
|
136
|
+
[" .----. ", " / o o \\ ", " | | ", " ~`~``~`~ "],
|
|
137
|
+
[" .----. ", " / ^ ^ \\ ", " | | ", " ~`~``~`~ "],
|
|
138
|
+
[" .----. ", " / - - \\ ", " | | ", " ~`~``~`~ "],
|
|
139
|
+
],
|
|
140
|
+
speed: 500,
|
|
141
|
+
},
|
|
142
|
+
robot: {
|
|
143
|
+
name: "机器人",
|
|
144
|
+
emoji: "🤖",
|
|
145
|
+
frames: [
|
|
146
|
+
[" .[||]. ", " [ o o ] ", " [ ==== ] ", " `------´ "],
|
|
147
|
+
[" .[||]. ", " [ ^ ^ ] ", " [ ==== ] ", " `------´ "],
|
|
148
|
+
[" .[||]. ", " [ - - ] ", " [ ==== ] ", " `------´ "],
|
|
149
|
+
],
|
|
150
|
+
speed: 400,
|
|
151
|
+
},
|
|
152
|
+
axolotl: {
|
|
153
|
+
name: "蝾螈",
|
|
154
|
+
emoji: "🦎",
|
|
155
|
+
frames: [
|
|
156
|
+
["}~(______)~{", "}~(o .. o)~{", " ( .--. ) ", " (_/ \\_) "],
|
|
157
|
+
["}~(______)~{", "}~(^ .. ^)~{", " ( .--. ) ", " (_/ \\_) "],
|
|
158
|
+
["}~(______)~{", "}~(- .. -)~{", " ( .--. ) ", " (_/ \\_) "],
|
|
159
|
+
],
|
|
160
|
+
speed: 600,
|
|
161
|
+
},
|
|
162
|
+
mushroom: {
|
|
163
|
+
name: "蘑菇",
|
|
164
|
+
emoji: "🍄",
|
|
165
|
+
frames: [
|
|
166
|
+
[" .-o-OO-o-. ", "(__________)", " |o o| ", " |____| "],
|
|
167
|
+
[" .-o-OO-o-. ", "(__________)", " |^ ^| ", " |____| "],
|
|
168
|
+
[" .-o-OO-o-. ", "(__________)", " |- -| ", " |____| "],
|
|
169
|
+
],
|
|
170
|
+
speed: 700,
|
|
171
|
+
},
|
|
172
|
+
blob: {
|
|
173
|
+
name: "团子",
|
|
174
|
+
emoji: "🫠",
|
|
175
|
+
frames: [
|
|
176
|
+
[" .----. ", " ( o o ) ", " ( ) ", " `----´ "],
|
|
177
|
+
[" .----. ", " ( ^ ^ ) ", " ( ) ", " `----´ "],
|
|
178
|
+
[" .----. ", " ( - - ) ", " ( ) ", " `----´ "],
|
|
179
|
+
],
|
|
180
|
+
speed: 500,
|
|
181
|
+
},
|
|
182
|
+
octopus: {
|
|
183
|
+
name: "章鱼",
|
|
184
|
+
emoji: "🐙",
|
|
185
|
+
frames: [
|
|
186
|
+
[" .----. ", " ( o o ) ", " (______) ", " /\\/\\/\\/\\ "],
|
|
187
|
+
[" .----. ", " ( ^ ^ ) ", " (______) ", " \\/\\/\\/\\/ "],
|
|
188
|
+
[" .----. ", " ( - - ) ", " (______) ", " /\\/\\/\\/\\ "],
|
|
189
|
+
],
|
|
190
|
+
speed: 400,
|
|
191
|
+
},
|
|
192
|
+
chonk: {
|
|
193
|
+
name: "胖熊",
|
|
194
|
+
emoji: "🐻",
|
|
195
|
+
frames: [
|
|
196
|
+
[" /\\ /\\ ", " ( o o ) ", " ( .. ) ", " `------´ "],
|
|
197
|
+
[" /\\ /\\ ", " ( ^ ^ ) ", " ( .. ) ", " `------´ "],
|
|
198
|
+
[" /\\ /\\ ", " ( - - ) ", " ( .. ) ", " `------´ "],
|
|
199
|
+
],
|
|
200
|
+
speed: 600,
|
|
201
|
+
},
|
|
202
|
+
cactus: {
|
|
203
|
+
name: "仙人掌",
|
|
204
|
+
emoji: "🌵",
|
|
205
|
+
frames: [
|
|
206
|
+
[" n ____ n ", " | |o o| | ", " |_| |_| ", " | | "],
|
|
207
|
+
[" n ____ n ", " | |^ ^| | ", " |_| |_| ", " | | "],
|
|
208
|
+
[" n ____ n ", " | |- -| | ", " |_| |_| ", " | | "],
|
|
209
|
+
],
|
|
210
|
+
speed: 700,
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// 随机选择动物
|
|
215
|
+
const petKeys = Object.keys(PETS);
|
|
216
|
+
let currentPet: string;
|
|
217
|
+
let currentFrame = 0;
|
|
218
|
+
let intervalId: ReturnType<typeof setInterval> | null = null;
|
|
219
|
+
let isShiny = false;
|
|
220
|
+
let currentHat = "none";
|
|
221
|
+
|
|
222
|
+
function getRandomPet(): string {
|
|
223
|
+
return petKeys[Math.floor(Math.random() * petKeys.length)]!;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function getRandomHat(): string {
|
|
227
|
+
const hatChance = Math.random();
|
|
228
|
+
if (hatChance < 0.05) return "crown";
|
|
229
|
+
if (hatChance < 0.1) return "wizard";
|
|
230
|
+
if (hatChance < 0.15) return "tophat";
|
|
231
|
+
if (hatChance < 0.2) return "halo";
|
|
232
|
+
if (hatChance < 0.25) return "beanie";
|
|
233
|
+
if (hatChance < 0.3) return "tinyduck";
|
|
234
|
+
if (hatChance < 0.35) return "propeller";
|
|
235
|
+
return "none";
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function getHatNames(): Record<string, string> {
|
|
239
|
+
return {
|
|
240
|
+
crown: "皇冠",
|
|
241
|
+
wizard: "魔法帽",
|
|
242
|
+
tophat: "礼帽",
|
|
243
|
+
halo: "光环",
|
|
244
|
+
beanie: "无檐帽",
|
|
245
|
+
tinyduck: "小鸭帽",
|
|
246
|
+
propeller: "螺旋桨帽",
|
|
247
|
+
none: "",
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function getNextFrame(): string[] {
|
|
252
|
+
const pet = PETS[currentPet]!;
|
|
253
|
+
const frames = pet.frames;
|
|
254
|
+
currentFrame = (currentFrame + 1) % frames.length;
|
|
255
|
+
const frame = [...frames[currentFrame]!];
|
|
256
|
+
|
|
257
|
+
// 添加帽子(如果有)
|
|
258
|
+
if (currentHat !== "none" && frame.length > 0 && frame[0] && !frame[0].trim()) {
|
|
259
|
+
frame[0] = HATS[currentHat] ?? frame[0]!;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// 闪亮特效
|
|
263
|
+
if (isShiny) {
|
|
264
|
+
return frame.map((line) => `\x1b[38;2;255;215;0m${line}\x1b[0m`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return frame;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function showPetNotification(ctx: ExtensionAPI["on"] extends (event: string, cb: (event: any, ctx: infer Ctx) => void) => void ? Ctx : never) {
|
|
271
|
+
const pet = PETS[currentPet]!;
|
|
272
|
+
const hatNames = getHatNames();
|
|
273
|
+
let msg = `${pet.emoji} 本次宠物:${pet.name}`;
|
|
274
|
+
if (isShiny) msg += " ✨(闪亮)";
|
|
275
|
+
if (currentHat !== "none") msg += ` + ${hatNames[currentHat]}`;
|
|
276
|
+
ctx.ui.notify(msg, "info");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function startPetAnimation(ctx: any) {
|
|
280
|
+
const pet = PETS[currentPet]!;
|
|
281
|
+
|
|
282
|
+
// 显示初始精灵
|
|
283
|
+
const initialSprite = getNextFrame();
|
|
284
|
+
ctx.ui.setWidget("random-pet", initialSprite, { placement: "belowEditor" });
|
|
285
|
+
|
|
286
|
+
// 启动动画
|
|
287
|
+
let animationCtx = ctx;
|
|
288
|
+
if (intervalId) {
|
|
289
|
+
clearInterval(intervalId);
|
|
290
|
+
}
|
|
291
|
+
intervalId = setInterval(() => {
|
|
292
|
+
try {
|
|
293
|
+
const nextSprite = getNextFrame();
|
|
294
|
+
animationCtx.ui.setWidget("random-pet", nextSprite, { placement: "belowEditor" });
|
|
295
|
+
} catch (e) {
|
|
296
|
+
if (intervalId) {
|
|
297
|
+
clearInterval(intervalId);
|
|
298
|
+
intervalId = null;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}, pet.speed);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export default function (pi: ExtensionAPI) {
|
|
305
|
+
// 注册 /pet 命令
|
|
306
|
+
pi.registerCommand("pet", {
|
|
307
|
+
description: "切换随机宠物",
|
|
308
|
+
usage: "/pet",
|
|
309
|
+
handler: async (_args, ctx) => {
|
|
310
|
+
// 清理旧动画
|
|
311
|
+
if (intervalId) {
|
|
312
|
+
clearInterval(intervalId);
|
|
313
|
+
intervalId = null;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// 随机选择新宠物
|
|
317
|
+
const oldPet = PETS[currentPet]!;
|
|
318
|
+
currentPet = getRandomPet();
|
|
319
|
+
isShiny = Math.random() < 0.1;
|
|
320
|
+
currentHat = getRandomHat();
|
|
321
|
+
currentFrame = 0;
|
|
322
|
+
|
|
323
|
+
const pet = PETS[currentPet]!;
|
|
324
|
+
|
|
325
|
+
// 启动新动画
|
|
326
|
+
startPetAnimation(ctx);
|
|
327
|
+
|
|
328
|
+
// 显示切换提示
|
|
329
|
+
const hatNames = getHatNames();
|
|
330
|
+
let msg = `${oldPet.emoji}${oldPet.name} → ${pet.emoji}${pet.name}`;
|
|
331
|
+
if (isShiny) msg += " ✨(闪亮)";
|
|
332
|
+
if (currentHat !== "none") msg += ` + ${hatNames[currentHat]}`;
|
|
333
|
+
ctx.ui.notify(msg, "info");
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
pi.on("session_start", (_event, ctx) => {
|
|
338
|
+
// 随机选择动物
|
|
339
|
+
currentPet = getRandomPet();
|
|
340
|
+
isShiny = Math.random() < 0.1;
|
|
341
|
+
currentHat = getRandomHat();
|
|
342
|
+
currentFrame = 0;
|
|
343
|
+
|
|
344
|
+
// 启动动画
|
|
345
|
+
startPetAnimation(ctx);
|
|
346
|
+
|
|
347
|
+
// 显示提示
|
|
348
|
+
const pet = PETS[currentPet]!;
|
|
349
|
+
const hatNames = getHatNames();
|
|
350
|
+
let msg = `${pet.emoji} 本次宠物:${pet.name}`;
|
|
351
|
+
if (isShiny) msg += " ✨(闪亮)";
|
|
352
|
+
if (currentHat !== "none") msg += ` + ${hatNames[currentHat]}`;
|
|
353
|
+
ctx.ui.notify(msg, "info");
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
pi.on("session_shutdown", () => {
|
|
357
|
+
if (intervalId) {
|
|
358
|
+
clearInterval(intervalId);
|
|
359
|
+
intervalId = null;
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
}
|
|
@@ -164,7 +164,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
164
164
|
let currentTipIndex = -1;
|
|
165
165
|
let autoHideTimeoutId: NodeJS.Timeout | undefined;
|
|
166
166
|
let autoShowTimeoutId: NodeJS.Timeout | undefined;
|
|
167
|
-
let
|
|
167
|
+
let currentUI: any = undefined; // 保存当前有效的 UI context
|
|
168
168
|
let isAutoPlaying = false;
|
|
169
169
|
|
|
170
170
|
// 基于 #49bccf (73, 188, 207) 的随机颜色生成
|
|
@@ -178,8 +178,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
178
178
|
};
|
|
179
179
|
|
|
180
180
|
const hideTip = () => {
|
|
181
|
-
if (!
|
|
182
|
-
|
|
181
|
+
if (!currentUI) return;
|
|
182
|
+
currentUI.setWidget("aicode-tips", [], { placement: "aboveEditor" });
|
|
183
183
|
};
|
|
184
184
|
|
|
185
185
|
const clearAllTimers = () => {
|
|
@@ -202,7 +202,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
202
202
|
};
|
|
203
203
|
|
|
204
204
|
const showRandomTip = (isInternalCall = false) => {
|
|
205
|
-
if (!
|
|
205
|
+
if (!currentUI) return;
|
|
206
206
|
|
|
207
207
|
// 清除之前的定时器(但内部轮播调用时不清除,避免中断轮播)
|
|
208
208
|
if (!isInternalCall) {
|
|
@@ -218,11 +218,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
218
218
|
// 使用随机颜色
|
|
219
219
|
const color = getRandomTipColor();
|
|
220
220
|
const coloredTip = `${color}tips: ${tip}\x1b[0m`;
|
|
221
|
-
|
|
221
|
+
currentUI.setWidget("aicode-tips", [coloredTip], { placement: "aboveEditor" });
|
|
222
222
|
|
|
223
223
|
// 自动轮播:10 秒后直接显示下一条
|
|
224
224
|
if (isAutoPlaying) {
|
|
225
|
-
autoHideTimeoutId = setTimeout(() =>
|
|
225
|
+
autoHideTimeoutId = setTimeout(() => {
|
|
226
|
+
try {
|
|
227
|
+
showRandomTip(true);
|
|
228
|
+
} catch (error) {
|
|
229
|
+
// 如果出现错误(比如 stale context),清理定时器
|
|
230
|
+
console.error('aicode-tips extension error:', error);
|
|
231
|
+
stopAutoPlay();
|
|
232
|
+
currentUI = undefined;
|
|
233
|
+
}
|
|
234
|
+
}, 10000);
|
|
226
235
|
}
|
|
227
236
|
};
|
|
228
237
|
|
|
@@ -238,22 +247,22 @@ export default function (pi: ExtensionAPI) {
|
|
|
238
247
|
|
|
239
248
|
// 会话开始时启动自动轮播
|
|
240
249
|
pi.on("session_start", (_event, ctx) => {
|
|
241
|
-
|
|
250
|
+
currentUI = ctx.ui;
|
|
242
251
|
startAutoPlay();
|
|
243
252
|
});
|
|
244
253
|
|
|
245
254
|
// 会话结束时清理
|
|
246
255
|
pi.on("session_shutdown", () => {
|
|
247
256
|
stopAutoPlay();
|
|
248
|
-
|
|
249
|
-
|
|
257
|
+
currentUI?.setWidget("aicode-tips", undefined);
|
|
258
|
+
currentUI = undefined;
|
|
250
259
|
});
|
|
251
260
|
|
|
252
261
|
// 手动切换技巧
|
|
253
262
|
pi.registerCommand("tip", {
|
|
254
263
|
description: "显示一个新的 aicode 使用技巧",
|
|
255
264
|
handler: async (_args, ctx) => {
|
|
256
|
-
|
|
265
|
+
currentUI = ctx.ui;
|
|
257
266
|
showRandomTip();
|
|
258
267
|
},
|
|
259
268
|
});
|
|
@@ -15,38 +15,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
15
15
|
// === 记录开始时间 ===
|
|
16
16
|
let agentStartTime = 0;
|
|
17
17
|
|
|
18
|
-
pi.on("agent_start", async () => {
|
|
19
|
-
agentStartTime = Date.now();
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
// === 消息完成通知(只在最后一条显示)===
|
|
23
|
-
|
|
24
|
-
pi.on("agent_end", async (event, ctx) => {
|
|
25
|
-
try {
|
|
26
|
-
// 计算耗时
|
|
27
|
-
const elapsed = Date.now() - agentStartTime;
|
|
28
|
-
let timeStr: string;
|
|
29
|
-
if (elapsed < 1000) {
|
|
30
|
-
timeStr = `${elapsed}ms`;
|
|
31
|
-
} else if (elapsed < 60000) {
|
|
32
|
-
timeStr = `${(elapsed / 1000).toFixed(1)}s`;
|
|
33
|
-
} else {
|
|
34
|
-
const mins = Math.floor(elapsed / 60000);
|
|
35
|
-
const secs = Math.floor((elapsed % 60000) / 1000);
|
|
36
|
-
timeStr = `${mins}m ${secs}s`;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// 格式化完整时间
|
|
40
|
-
const now = new Date();
|
|
41
|
-
const timeStr2 = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
|
|
42
|
-
|
|
43
|
-
// agent_end 表示一轮对话结束,显示完成提示
|
|
44
|
-
ctx.ui.notify(`${timeStr2} | ✨ 消息处理完成 (${timeStr})`, "info");
|
|
45
|
-
} catch (e) {
|
|
46
|
-
// 忽略错误(ctx 可能已过期)
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
|
-
|
|
50
18
|
// === 拦截工具调用 ===
|
|
51
19
|
|
|
52
20
|
pi.on("tool_call", async (event, ctx) => {
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @author: xujianjiang
|
|
3
|
+
* Token 性能统计扩展
|
|
4
|
+
*
|
|
5
|
+
* 功能说明:
|
|
6
|
+
* - 在每轮对话结束时显示 Token 使用统计
|
|
7
|
+
* - 显示处理速度(token/秒)
|
|
8
|
+
* - 显示输入输出 Token 数量
|
|
9
|
+
* - 显示耗时(智能格式化:ms/秒/分+秒)
|
|
10
|
+
*
|
|
11
|
+
* 输出格式示例:
|
|
12
|
+
* 2026-06-08T16:19:54 | ✨ 处理完成 | 15.2 token/秒 | ↓out 1,234 | ↑in 567 | 11.2秒
|
|
13
|
+
*
|
|
14
|
+
* 放置位置:config/agent/extensions/
|
|
15
|
+
*/
|
|
16
|
+
import type { AssistantMessage } from "@earendil-works/pi-ai";
|
|
17
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
18
|
+
|
|
19
|
+
function isAssistantMessage(message: unknown): message is AssistantMessage {
|
|
20
|
+
if (!message || typeof message !== "object") return false;
|
|
21
|
+
const role = (message as { role?: unknown }).role;
|
|
22
|
+
return role === "assistant";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function formatDateTime(date: Date): string {
|
|
26
|
+
const year = date.getFullYear();
|
|
27
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
28
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
29
|
+
const hour = String(date.getHours()).padStart(2, "0");
|
|
30
|
+
const minute = String(date.getMinutes()).padStart(2, "0");
|
|
31
|
+
const second = String(date.getSeconds()).padStart(2, "0");
|
|
32
|
+
return `${year}-${month}-${day}T${hour}:${minute}:${second}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function formatDuration(ms: number): string {
|
|
36
|
+
if (ms < 1000) {
|
|
37
|
+
return `${ms}ms`;
|
|
38
|
+
} else if (ms < 60000) {
|
|
39
|
+
return `${(ms / 1000).toFixed(1)}秒`;
|
|
40
|
+
} else {
|
|
41
|
+
const mins = Math.floor(ms / 60000);
|
|
42
|
+
const secs = Math.floor((ms % 60000) / 1000);
|
|
43
|
+
return `${mins}分${secs}秒`;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default function (pi: ExtensionAPI) {
|
|
48
|
+
let agentStartMs: number | null = null;
|
|
49
|
+
|
|
50
|
+
pi.on("agent_start", () => {
|
|
51
|
+
agentStartMs = Date.now();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
pi.on("agent_end", (event, ctx) => {
|
|
55
|
+
if (!ctx.hasUI) return;
|
|
56
|
+
if (agentStartMs === null) return;
|
|
57
|
+
|
|
58
|
+
const elapsedMs = Date.now() - agentStartMs;
|
|
59
|
+
agentStartMs = null;
|
|
60
|
+
if (elapsedMs <= 0) return;
|
|
61
|
+
|
|
62
|
+
let input = 0;
|
|
63
|
+
let output = 0;
|
|
64
|
+
let cacheRead = 0;
|
|
65
|
+
let cacheWrite = 0;
|
|
66
|
+
let totalTokens = 0;
|
|
67
|
+
|
|
68
|
+
for (const message of event.messages) {
|
|
69
|
+
if (!isAssistantMessage(message)) continue;
|
|
70
|
+
input += message.usage.input || 0;
|
|
71
|
+
output += message.usage.output || 0;
|
|
72
|
+
cacheRead += message.usage.cacheRead || 0;
|
|
73
|
+
cacheWrite += message.usage.cacheWrite || 0;
|
|
74
|
+
totalTokens += message.usage.totalTokens || 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (output <= 0) return;
|
|
78
|
+
|
|
79
|
+
const elapsedSeconds = elapsedMs / 1000;
|
|
80
|
+
const tokensPerSecond = output / elapsedSeconds;
|
|
81
|
+
const now = new Date();
|
|
82
|
+
const timeStr = formatDateTime(now);
|
|
83
|
+
const durationStr = formatDuration(elapsedMs);
|
|
84
|
+
const message = `${timeStr} | ✨ 处理完成 | ${tokensPerSecond.toFixed(2)} token/秒 | ↑in ${input.toLocaleString()} |↓out ${output.toLocaleString()} | ${durationStr}`;
|
|
85
|
+
ctx.ui.notify(message, "info");
|
|
86
|
+
});
|
|
87
|
+
}
|
|
@@ -2,11 +2,15 @@
|
|
|
2
2
|
* @author: xujianjiang
|
|
3
3
|
* working-message
|
|
4
4
|
*
|
|
5
|
-
* 自定义工作消息,格式类似 Claude Code: ✶ xxx… (时间
|
|
5
|
+
* 自定义工作消息,格式类似 Claude Code: ✶ xxx… (时间)
|
|
6
|
+
*
|
|
7
|
+
* 改进:使用 dispose 模式防止内存泄漏
|
|
8
|
+
* - 使用明确类型,避免 any
|
|
9
|
+
* - 实现可靠的定时器清理机制
|
|
10
|
+
* - 双重保障确保资源释放
|
|
6
11
|
*/
|
|
7
12
|
|
|
8
|
-
import type { ExtensionAPI, TurnEndEvent } from "@earendil-works/pi-coding-agent";
|
|
9
|
-
import type { AssistantMessage } from "@earendil-works/pi-ai";
|
|
13
|
+
import type { ExtensionAPI, ExtensionContext, TurnEndEvent } from "@earendil-works/pi-coding-agent";
|
|
10
14
|
|
|
11
15
|
// 主题色 #49bccf
|
|
12
16
|
const accentColor = "\x1b[38;2;73;188;207m";
|
|
@@ -44,11 +48,100 @@ const workingMessages = [
|
|
|
44
48
|
"Algorithm", "Architecture", "Database", "Cache", "Queue", "Microservice",
|
|
45
49
|
];
|
|
46
50
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
/**
|
|
52
|
+
* 工作消息状态管理器 - 使用 dispose 模式
|
|
53
|
+
* 确保定时器和 UI 引用的生命周期管理,防止内存泄漏
|
|
54
|
+
*/
|
|
55
|
+
class WorkingMessageStateManager {
|
|
56
|
+
// ✅ 使用明确类型,避免 any
|
|
57
|
+
private _ui: ExtensionContext['ui'] | null = null;
|
|
58
|
+
private _intervalId: ReturnType<typeof setInterval> | null = null;
|
|
59
|
+
private _startTime: number = 0;
|
|
60
|
+
private _disposed: boolean = false;
|
|
61
|
+
|
|
62
|
+
// Getter 方法
|
|
63
|
+
get ui(): ExtensionContext['ui'] | null {
|
|
64
|
+
return this._ui;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
get intervalId(): ReturnType<typeof setInterval> | null {
|
|
68
|
+
return this._intervalId;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
get startTime(): number {
|
|
72
|
+
return this._startTime;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
isDisposed(): boolean {
|
|
76
|
+
return this._disposed;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Setter 方法(检查 disposed 状态)
|
|
80
|
+
setUI(ui: ExtensionContext['ui'] | null): void {
|
|
81
|
+
if (this._disposed) {
|
|
82
|
+
console.warn('[working-msg] State already disposed, cannot set UI');
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
this._ui = ui;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
setInterval(intervalId: ReturnType<typeof setInterval> | null): void {
|
|
89
|
+
if (this._disposed) {
|
|
90
|
+
console.warn('[working-msg] State already disposed, cannot set interval');
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
this._intervalId = intervalId;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
setStartTime(startTime: number): void {
|
|
97
|
+
if (this._disposed) {
|
|
98
|
+
console.warn('[working-msg] State already disposed, cannot set start time');
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
this._startTime = startTime;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* dispose() - 清理所有资源
|
|
106
|
+
* ✅ 双重保障:清理定时器 + 清理 UI 引用
|
|
107
|
+
* ✅ disposed 标记防止重复清理
|
|
108
|
+
*/
|
|
109
|
+
dispose(): void {
|
|
110
|
+
if (this._disposed) {
|
|
111
|
+
return; // 防止重复 dispose
|
|
112
|
+
}
|
|
51
113
|
|
|
114
|
+
// ✅ 清理定时器(双重保障)
|
|
115
|
+
if (this._intervalId) {
|
|
116
|
+
clearInterval(this._intervalId);
|
|
117
|
+
this._intervalId = null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ✅ 清理 UI 引用
|
|
121
|
+
if (this._ui) {
|
|
122
|
+
try {
|
|
123
|
+
this._ui.setWorkingMessage(undefined);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
// ✅ 忽略错误,确保清理继续
|
|
126
|
+
console.warn('[working-msg] Error clearing working message:', error);
|
|
127
|
+
}
|
|
128
|
+
this._ui = null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
this._disposed = true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* reset() - 重置状态(用于新的 turn)
|
|
136
|
+
*/
|
|
137
|
+
reset(): void {
|
|
138
|
+
this.dispose(); // 先清理旧资源
|
|
139
|
+
this._disposed = false; // 重置 disposed 标记
|
|
140
|
+
this._startTime = 0;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 工具函数
|
|
52
145
|
function getNextMessage(): string {
|
|
53
146
|
const randomIndex = Math.floor(Math.random() * workingMessages.length);
|
|
54
147
|
return workingMessages[randomIndex];
|
|
@@ -61,76 +154,77 @@ function formatTime(seconds: number): string {
|
|
|
61
154
|
return `${mins} m ${secs} s`;
|
|
62
155
|
}
|
|
63
156
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
157
|
+
/**
|
|
158
|
+
* ✅ 改进的定时器逻辑
|
|
159
|
+
* - 不依赖 try-catch
|
|
160
|
+
* - 检查 disposed 状态
|
|
161
|
+
* - 使用 state.ui(明确类型)
|
|
162
|
+
*/
|
|
163
|
+
function startInterval(state: WorkingMessageStateManager): void {
|
|
164
|
+
if (state.isDisposed() || !state.ui) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const intervalId = setInterval(() => {
|
|
169
|
+
// ✅ 每次执行前检查 disposed 和 UI 状态
|
|
170
|
+
if (state.isDisposed() || !state.ui) {
|
|
171
|
+
// ✅ 自动清理
|
|
172
|
+
state.dispose();
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ✅ 安全执行(明确类型)
|
|
177
|
+
const elapsed = Math.floor((Date.now() - state.startTime) / 1000);
|
|
178
|
+
const timeStr = formatTime(elapsed);
|
|
179
|
+
const msg = `${accentColor} ✶ ${getNextMessage()}…${reset} (${timeStr})`;
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
state.ui.setWorkingMessage(msg);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
// ✅ 如果 UI 调用失败,自动清理
|
|
185
|
+
console.warn('[working-msg] UI call failed, disposing:', error);
|
|
186
|
+
state.dispose();
|
|
187
|
+
}
|
|
188
|
+
}, 3000);
|
|
189
|
+
|
|
190
|
+
state.setInterval(intervalId);
|
|
67
191
|
}
|
|
68
192
|
|
|
193
|
+
/**
|
|
194
|
+
* 扩展主函数 - 使用 dispose 模式管理状态
|
|
195
|
+
*/
|
|
69
196
|
export default function workingMessageExtension(pi: ExtensionAPI) {
|
|
70
|
-
//
|
|
197
|
+
// ✅ 单一状态管理器
|
|
198
|
+
const state = new WorkingMessageStateManager();
|
|
199
|
+
|
|
200
|
+
// ✅ session_shutdown - 清理所有资源
|
|
71
201
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
72
|
-
|
|
73
|
-
clearInterval(intervalId);
|
|
74
|
-
intervalId = null;
|
|
75
|
-
}
|
|
76
|
-
currentUI = null;
|
|
202
|
+
state.dispose();
|
|
77
203
|
});
|
|
78
204
|
|
|
79
|
-
// turn_start
|
|
205
|
+
// ✅ turn_start - 初始化状态并启动定时器
|
|
80
206
|
pi.on("turn_start", async (_event, ctx) => {
|
|
81
|
-
if (!ctx.ui)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
currentUI = ctx.ui;
|
|
207
|
+
if (!ctx.ui) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
85
210
|
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
211
|
+
// ✅ 重置状态(清理旧资源)
|
|
212
|
+
state.reset();
|
|
213
|
+
state.setUI(ctx.ui); // ✅ 明确类型:ExtensionContext['ui']
|
|
214
|
+
state.setStartTime(Date.now());
|
|
89
215
|
|
|
90
|
-
//
|
|
91
|
-
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
216
|
+
// 设置第一条消息
|
|
217
|
+
const elapsed = Math.floor((Date.now() - state.startTime) / 1000);
|
|
92
218
|
const timeStr = formatTime(elapsed);
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (!intervalId) {
|
|
99
|
-
intervalId = setInterval(() => {
|
|
100
|
-
try {
|
|
101
|
-
// 使用保存的 currentUI 而不是捕获的 ctx.ui
|
|
102
|
-
if (!currentUI) return;
|
|
103
|
-
|
|
104
|
-
// 获取当前 tokens 使用量
|
|
105
|
-
// 注意:这里不再访问 ctx.sessionManager,避免 stale context 错误
|
|
106
|
-
// tokens 数量通过其他方式获取,或者保持为 0
|
|
107
|
-
|
|
108
|
-
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
109
|
-
const timeStr = formatTime(elapsed);
|
|
110
|
-
const tokensStr = formatTokens(totalTokens);
|
|
111
|
-
const msg = `${accentColor} ✶ ${getNextMessage()}…${reset} (${timeStr} · ↓ ${tokensStr} tokens)`;
|
|
112
|
-
currentUI.setWorkingMessage(msg);
|
|
113
|
-
} catch (error) {
|
|
114
|
-
// 如果出现错误(比如 stale context),清理 interval
|
|
115
|
-
console.error('working-msg extension error:', error);
|
|
116
|
-
if (intervalId) {
|
|
117
|
-
clearInterval(intervalId);
|
|
118
|
-
intervalId = null;
|
|
119
|
-
}
|
|
120
|
-
currentUI = null;
|
|
121
|
-
}
|
|
122
|
-
}, 3000);
|
|
123
|
-
}
|
|
219
|
+
const msg = `${accentColor} ✶ ${getNextMessage()}…${reset} (${timeStr})`;
|
|
220
|
+
state.ui?.setWorkingMessage(msg);
|
|
221
|
+
|
|
222
|
+
// ✅ 启动定时器
|
|
223
|
+
startInterval(state);
|
|
124
224
|
});
|
|
125
225
|
|
|
126
|
-
//
|
|
226
|
+
// ✅ turn_end - 清理所有资源
|
|
127
227
|
pi.on("turn_end", async (_event: TurnEndEvent, ctx) => {
|
|
128
|
-
|
|
129
|
-
clearInterval(intervalId);
|
|
130
|
-
intervalId = null;
|
|
131
|
-
}
|
|
132
|
-
// 恢复默认消息
|
|
133
|
-
currentUI?.setWorkingMessage(undefined);
|
|
134
|
-
currentUI = null;
|
|
228
|
+
state.dispose();
|
|
135
229
|
});
|
|
136
|
-
}
|
|
230
|
+
}
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"~/.aicode-cli/agent/extensions/custom-footer.ts",
|
|
11
11
|
"~/.aicode-cli/agent/extensions/custom.ts",
|
|
12
12
|
"~/.aicode-cli/agent/extensions/review.ts",
|
|
13
|
-
"~/.aicode-cli/agent/extensions/working-msg.ts"
|
|
13
|
+
"~/.aicode-cli/agent/extensions/working-msg.ts",
|
|
14
|
+
"-extensions/aicode-pets.ts"
|
|
14
15
|
],
|
|
15
16
|
"packages":[
|
|
16
17
|
"npm:pi-subagents@0.17.1",
|