@zhushanwen/pi-pending-notifications 0.2.0 → 0.3.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.
- package/package.json +3 -3
- package/src/__tests__/pending-notifications.test.ts +12 -4
- package/src/index.ts +19 -10
- package/src/state.ts +6 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-pending-notifications",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Cross-extension async operation registration/query mechanism for Pi — prevents message injection during long-running operations.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"vitest": "^4.1.8"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
|
-
"@
|
|
29
|
-
"
|
|
28
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
29
|
+
"typebox": "*"
|
|
30
30
|
},
|
|
31
31
|
"scripts": {
|
|
32
32
|
"typecheck": "npx tsc --noEmit",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
/* eslint-disable taste/no-unsafe-cast */
|
|
13
13
|
|
|
14
|
-
import type { ExtensionAPI, ExtensionContext } from "@
|
|
14
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
15
15
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
16
16
|
|
|
17
17
|
import pendingNotificationsExtension from "../index";
|
|
@@ -37,6 +37,7 @@ interface MockSetup {
|
|
|
37
37
|
handlers: HandlerRegistry;
|
|
38
38
|
appendEntryMock: ReturnType<typeof vi.fn>;
|
|
39
39
|
registerToolMock: ReturnType<typeof vi.fn>;
|
|
40
|
+
sendMessageMock: ReturnType<typeof vi.fn>;
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
function createMockPi(): MockSetup {
|
|
@@ -67,7 +68,7 @@ function createMockPi(): MockSetup {
|
|
|
67
68
|
},
|
|
68
69
|
} as unknown as ExtensionAPI;
|
|
69
70
|
|
|
70
|
-
return { pi, handlers, appendEntryMock, registerToolMock };
|
|
71
|
+
return { pi, handlers, appendEntryMock, registerToolMock, sendMessageMock };
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
function createMockCtx(entries: MockSessionEntry[], sessionId = "sess-current"): ExtensionContext {
|
|
@@ -89,9 +90,15 @@ async function runTool(
|
|
|
89
90
|
params: Record<string, unknown>,
|
|
90
91
|
): Promise<{ content: Array<{ type: string; text: string }>; details: unknown }> {
|
|
91
92
|
const tool = setup.registerToolMock.mock.calls[0][0] as {
|
|
92
|
-
execute: (
|
|
93
|
+
execute: (
|
|
94
|
+
toolCallId: string,
|
|
95
|
+
p: unknown,
|
|
96
|
+
signal: AbortSignal | undefined,
|
|
97
|
+
onUpdate: unknown,
|
|
98
|
+
ctx: ExtensionContext,
|
|
99
|
+
) => Promise<{ content: Array<{ type: string; text: string }>; details: unknown }>;
|
|
93
100
|
};
|
|
94
|
-
return tool.execute(params);
|
|
101
|
+
return tool.execute("test-call-id", params, undefined, undefined, createMockCtx([]));
|
|
95
102
|
}
|
|
96
103
|
|
|
97
104
|
async function getCount(setup: MockSetup): Promise<number> {
|
|
@@ -375,6 +382,7 @@ describe("pendingNotificationsExtension factory", () => {
|
|
|
375
382
|
});
|
|
376
383
|
});
|
|
377
384
|
|
|
385
|
+
|
|
378
386
|
describe("safeAppendEntry error handling", () => {
|
|
379
387
|
it("appendEntry throwing (stale context) does not break register listener, registry still updated", async () => {
|
|
380
388
|
// listener 先 register(registry) 更新内存,再 safeAppendEntry;appendEntry 抛错被 catch,registry 仍已更新
|
package/src/index.ts
CHANGED
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
* workflow 侧通过 deps.eventBus 注入 pi.events(同一总线)。
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
-
import type { ExtensionAPI, ExtensionContext } from "@
|
|
26
|
-
import { Type } from "
|
|
25
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
26
|
+
import { Type } from "typebox";
|
|
27
27
|
|
|
28
28
|
import {
|
|
29
29
|
createRegistry,
|
|
@@ -46,9 +46,12 @@ const PendingNotificationsParams = Type.Object({
|
|
|
46
46
|
]),
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
-
/**
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
/** pending_notifications 工具的 details 形状。两个分支共用同一结构(items 缺省时 undefined),
|
|
50
|
+
* 让 registerTool 泛型对 TDetails 推断一致,避免 union 细节冲突。 */
|
|
51
|
+
interface PendingToolDetails {
|
|
52
|
+
action: string;
|
|
53
|
+
count: number;
|
|
54
|
+
items?: PendingEntry[];
|
|
52
55
|
}
|
|
53
56
|
|
|
54
57
|
/**
|
|
@@ -218,13 +221,12 @@ export default function pendingNotificationsExtension(pi: ExtensionAPI): void {
|
|
|
218
221
|
description:
|
|
219
222
|
"查询当前活跃的异步操作(workflow/subagent)。action=count 返回数量;action=list 返回列表。状态由 EventBus + session entries 维护,无需手动注册。",
|
|
220
223
|
parameters: PendingNotificationsParams,
|
|
221
|
-
execute: async (params: unknown) => {
|
|
222
|
-
const p = params as ToolParams;
|
|
224
|
+
execute: async (_toolCallId: string, params: { action: "count" | "list" }, _signal: AbortSignal | undefined, _onUpdate: unknown, _ctx: ExtensionContext): Promise<{ content: { type: "text"; text: string }[]; details: PendingToolDetails }> => {
|
|
223
225
|
const active = getActive(registry);
|
|
224
226
|
|
|
225
|
-
debugLog("debug", `tool ${
|
|
227
|
+
debugLog("debug", `tool ${params.action} requested`, { action: params.action, activeCount: active.length });
|
|
226
228
|
|
|
227
|
-
if (
|
|
229
|
+
if (params.action === "count") {
|
|
228
230
|
return {
|
|
229
231
|
content: [{ type: "text" as const, text: `${active.length} pending operation(s)` }],
|
|
230
232
|
details: { action: "count", count: active.length },
|
|
@@ -263,9 +265,13 @@ function parseRegisterEvent(data: unknown): ParsedRegister | null {
|
|
|
263
265
|
interface ParsedUnregister {
|
|
264
266
|
id: string;
|
|
265
267
|
reason: string;
|
|
268
|
+
result?: string;
|
|
269
|
+
error?: string;
|
|
270
|
+
patchFile?: string;
|
|
266
271
|
}
|
|
267
272
|
|
|
268
|
-
/** 解析 pending:unregister 事件 data
|
|
273
|
+
/** 解析 pending:unregister 事件 data(容错缺失/类型错误字段)。
|
|
274
|
+
* T2 后 subagent 完成路径携带可选的 result/error/patchFile,供消费侧 sendMessage。 */
|
|
269
275
|
function parseUnregisterEvent(data: unknown): ParsedUnregister | null {
|
|
270
276
|
if (typeof data !== "object" || data === null) return null;
|
|
271
277
|
const d = data as Record<string, unknown>;
|
|
@@ -273,6 +279,9 @@ function parseUnregisterEvent(data: unknown): ParsedUnregister | null {
|
|
|
273
279
|
return {
|
|
274
280
|
id: d.id,
|
|
275
281
|
reason: typeof d.reason === "string" ? d.reason : "completed",
|
|
282
|
+
result: typeof d.result === "string" ? d.result : undefined,
|
|
283
|
+
error: typeof d.error === "string" ? d.error : undefined,
|
|
284
|
+
patchFile: typeof d.patchFile === "string" ? d.patchFile : undefined,
|
|
276
285
|
};
|
|
277
286
|
}
|
|
278
287
|
|
package/src/state.ts
CHANGED
|
@@ -48,9 +48,14 @@ interface RegisterEntryData {
|
|
|
48
48
|
sessionId: unknown;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
/** pending:unregister entry 在 entries
|
|
51
|
+
/** pending:unregister entry 在 entries 里的最小可识别形状。
|
|
52
|
+
* result/error/patchFile 为 subagent 完成通知携带的附加字段(T2 后由 pending-notifications
|
|
53
|
+
* 消费侧读取并 sendMessage 到 LLM)。workflow 的 unregister 不携带这些字段。 */
|
|
52
54
|
interface UnregisterEntryData {
|
|
53
55
|
id: unknown;
|
|
56
|
+
result?: unknown;
|
|
57
|
+
error?: unknown;
|
|
58
|
+
patchFile?: unknown;
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
/** SessionEntry 的最小可识别形状(duck-typed,避免依赖 SDK 具体类型) */
|