@zhushanwen/pi-canvas 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 +6 -2
- package/src/__tests__/helpers/canvas-harness.ts +2 -0
- package/src/__tests__/render.test.ts +68 -0
- package/src/__tests__/summary.test.ts +321 -0
- package/src/render.ts +23 -0
- package/src/summary.ts +118 -0
- package/src/tool-edit.ts +10 -2
- package/src/tool-enter.ts +7 -2
- package/src/tool-export.ts +10 -2
- package/src/tool-list.ts +10 -2
- package/src/tool-read.ts +10 -2
- package/src/tool-review.ts +24 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-canvas",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Problem-solving canvas toolface for pi: six deferred canvas_* tools bridging the agent to the taiji runtime CanvasService via the CANVAS_MARKER select channel (problem-solve-canvas)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"@earendil-works/pi-ai": "^1.0.0",
|
|
34
34
|
"@earendil-works/pi-coding-agent": "^1.0.0",
|
|
35
|
+
"@earendil-works/pi-tui": "^1.0.0",
|
|
35
36
|
"typebox": "1.3.27"
|
|
36
37
|
},
|
|
37
38
|
"peerDependenciesMeta": {
|
|
@@ -41,6 +42,9 @@
|
|
|
41
42
|
"@earendil-works/pi-coding-agent": {
|
|
42
43
|
"optional": true
|
|
43
44
|
},
|
|
45
|
+
"@earendil-works/pi-tui": {
|
|
46
|
+
"optional": true
|
|
47
|
+
},
|
|
44
48
|
"typebox": {
|
|
45
49
|
"optional": true
|
|
46
50
|
}
|
|
@@ -54,7 +58,7 @@
|
|
|
54
58
|
"@zhushanwen/extension-protocol": "0.21.0",
|
|
55
59
|
"@zhushanwen/pi-ext-guards": "0.5.0",
|
|
56
60
|
"@zhushanwen/pi-extension-logger": "0.6.2",
|
|
57
|
-
"@zhushanwen/subagent-core": "1.5.
|
|
61
|
+
"@zhushanwen/subagent-core": "1.5.3"
|
|
58
62
|
},
|
|
59
63
|
"scripts": {
|
|
60
64
|
"typecheck": "npx tsc --noEmit",
|
|
@@ -11,6 +11,8 @@ export interface RegisteredTool { // oe-exempt:20261015:framework:画布域类
|
|
|
11
11
|
exposure?: string;
|
|
12
12
|
parameters: unknown;
|
|
13
13
|
execute: (...args: unknown[]) => Promise<unknown>;
|
|
14
|
+
/** TUI 结果渲染 handler(renderCanvasToolResult,六工具同函数引用)。 */
|
|
15
|
+
renderResult?: (result: unknown, options: { expanded: boolean }, theme: unknown) => unknown;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
/** before_agent_start handler 的事件/ctx 最小形态(index.ts 只消费 systemPrompt 与 ctx.mode)。 */
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// render.test.ts —— canvas 六工具共用 renderResult(pi TUI 侧):折叠态一行摘要
|
|
2
|
+
// (details.summary,dim 染色)、展开态回原始 content 文本、无摘要异常形态回退。
|
|
3
|
+
|
|
4
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
|
|
7
|
+
import { renderCanvasToolResult } from "../render.js";
|
|
8
|
+
|
|
9
|
+
// mock theme(fg 直通,便于断言纯文本;todo render.test.ts 同款)
|
|
10
|
+
const mockTheme = {
|
|
11
|
+
fg: (_color: string, text: string) => text,
|
|
12
|
+
bg: (_color: string, text: string) => text,
|
|
13
|
+
bold: (text: string) => text,
|
|
14
|
+
} as unknown as Theme;
|
|
15
|
+
|
|
16
|
+
function renderedText(result: unknown, expanded: boolean): string {
|
|
17
|
+
return renderCanvasToolResult(result, { expanded }, mockTheme).render(400).join("\n");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("renderCanvasToolResult", () => {
|
|
21
|
+
it("折叠态(expanded=false):渲染 details.summary 一行摘要,不含原始 JSON", () => {
|
|
22
|
+
const result = {
|
|
23
|
+
content: [{ type: "text", text: '{"canvases":[{"canvasId":"c1"}]}' }],
|
|
24
|
+
details: { summary: "Listed 1 canvases" },
|
|
25
|
+
};
|
|
26
|
+
const out = renderedText(result, false);
|
|
27
|
+
expect(out).toContain("Listed 1 canvases");
|
|
28
|
+
expect(out).not.toContain("canvasId");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("展开态(expanded=true):渲染 content[0] 原始文本,不含摘要", () => {
|
|
32
|
+
const result = {
|
|
33
|
+
content: [{ type: "text", text: '{"canvases":[{"canvasId":"c1"}]}' }],
|
|
34
|
+
details: { summary: "Listed 1 canvases" },
|
|
35
|
+
};
|
|
36
|
+
const out = renderedText(result, true);
|
|
37
|
+
expect(out).toContain('{"canvases":[{"canvasId":"c1"}]}');
|
|
38
|
+
expect(out).not.toContain("Listed 1 canvases");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("折叠态但 details 缺失(异常形态)→ 回退 content 文本", () => {
|
|
42
|
+
const result = { content: [{ type: "text", text: "raw payload" }] };
|
|
43
|
+
expect(renderedText(result, false)).toContain("raw payload");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("折叠态但 summary 非字符串 / 空串 → 回退 content 文本", () => {
|
|
47
|
+
const base = { content: [{ type: "text", text: "raw payload" }] };
|
|
48
|
+
expect(renderedText({ ...base, details: { summary: 42 } }, false)).toContain("raw payload");
|
|
49
|
+
expect(renderedText({ ...base, details: { summary: "" } }, false)).toContain("raw payload");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("六工具注册的 renderResult 行为一致(共用 renderCanvasToolResult,经 harness 抽查两个工具)", async () => {
|
|
53
|
+
const { mountCanvas, toolOf } = await import("./helpers/canvas-harness.js");
|
|
54
|
+
const harness = mountCanvas();
|
|
55
|
+
const enter = toolOf(harness, "canvas_enter");
|
|
56
|
+
const list = toolOf(harness, "canvas_list");
|
|
57
|
+
expect(typeof enter.renderResult).toBe("function");
|
|
58
|
+
expect(typeof list.renderResult).toBe("function");
|
|
59
|
+
// 抽查经工具 handler 的折叠态渲染(handler 是各 registerTool 的方法包装,
|
|
60
|
+
// 共用同一渲染内核;类型桩:theme 直通)
|
|
61
|
+
type RenderFn = (r: unknown, o: { expanded: boolean }, t: unknown) => { render: (w: number) => string[] };
|
|
62
|
+
const render = (fn: unknown, result: unknown) =>
|
|
63
|
+
(fn as RenderFn)(result, { expanded: false }, mockTheme).render(400).join("\n");
|
|
64
|
+
const sample = { content: [{ type: "text", text: "raw" }], details: { summary: 'Entered canvas "n"' } };
|
|
65
|
+
expect(render(enter.renderResult, sample)).toContain('Entered canvas "n"');
|
|
66
|
+
expect(render(list.renderResult, sample)).toContain('Entered canvas "n"');
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
// summary.test.ts —— 六工具 details.summary 产出与 summary 纯函数文案(GUI 对话流
|
|
2
|
+
// 收起态摘要的产出侧契约):execute 组装断言走 harness(marker 往返),数据缺失
|
|
3
|
+
// 分支直接测纯函数(形状漂移退化为无后缀短文案,不抛错)。
|
|
4
|
+
|
|
5
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
canvasEditSummary,
|
|
9
|
+
canvasEnterSummary,
|
|
10
|
+
canvasExportSummary,
|
|
11
|
+
canvasListSummary,
|
|
12
|
+
canvasReadSummary,
|
|
13
|
+
canvasReviewSummary,
|
|
14
|
+
} from "../summary.js";
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
mountCanvas,
|
|
18
|
+
toolOf,
|
|
19
|
+
withExtLogEnv,
|
|
20
|
+
type ToolHarness,
|
|
21
|
+
} from "./helpers/canvas-harness.js";
|
|
22
|
+
|
|
23
|
+
describe("execute 组装 details.summary(六工具全路径)", () => {
|
|
24
|
+
let harness: ToolHarness;
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
harness = mountCanvas();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("canvas_enter 创建形态(params.name 可得)", async () => {
|
|
30
|
+
await withExtLogEnv("1", async () => {
|
|
31
|
+
harness.selectMock.mockResolvedValue(
|
|
32
|
+
JSON.stringify({ ok: true, eventId: "e1", evType: "CanvasCreated", summary: "created" }),
|
|
33
|
+
);
|
|
34
|
+
const enter = toolOf(harness, "canvas_enter");
|
|
35
|
+
const result = (await enter.execute(
|
|
36
|
+
"t1",
|
|
37
|
+
{ name: "多引擎支持", statement: "s", criterias: ["c"], scope: [] },
|
|
38
|
+
undefined,
|
|
39
|
+
undefined,
|
|
40
|
+
harness.ctx,
|
|
41
|
+
)) as { details: { summary: string } };
|
|
42
|
+
expect(result.details.summary).toBe('Entered canvas "多引擎支持"');
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("canvas_enter 打开形态(reply=CanvasState.name 优先于 params)", async () => {
|
|
47
|
+
await withExtLogEnv("1", async () => {
|
|
48
|
+
harness.selectMock.mockResolvedValue(
|
|
49
|
+
JSON.stringify({ canvasId: "c9", name: "已有画布", nodes: [] }),
|
|
50
|
+
);
|
|
51
|
+
const enter = toolOf(harness, "canvas_enter");
|
|
52
|
+
const result = (await enter.execute("t1", { canvasId: "c9" }, undefined, undefined, harness.ctx)) as {
|
|
53
|
+
details: { summary: string };
|
|
54
|
+
};
|
|
55
|
+
expect(result.details.summary).toBe('Entered canvas "已有画布"');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("canvas_edit grow 带 batch 数量;patch 无数量语义", async () => {
|
|
60
|
+
await withExtLogEnv("1", async () => {
|
|
61
|
+
harness.selectMock.mockResolvedValue(
|
|
62
|
+
JSON.stringify({ ok: true, eventId: "e2", evType: "NodeCreated", summary: "grown" }),
|
|
63
|
+
);
|
|
64
|
+
const edit = toolOf(harness, "canvas_edit");
|
|
65
|
+
const batch = [
|
|
66
|
+
{ id: "n1", type: "question", content: "q", attachTo: "root", edge: "decompose" },
|
|
67
|
+
{ id: "n2", type: "option", content: "o", attachTo: "root", edge: "resolve" },
|
|
68
|
+
];
|
|
69
|
+
const growResult = (await edit.execute(
|
|
70
|
+
"t1",
|
|
71
|
+
{ action: "grow", growParams: { canvasId: "c1", batch, reason: "r" } },
|
|
72
|
+
undefined,
|
|
73
|
+
undefined,
|
|
74
|
+
harness.ctx,
|
|
75
|
+
)) as { details: { summary: string } };
|
|
76
|
+
expect(growResult.details.summary).toBe("Canvas edited · +2 nodes");
|
|
77
|
+
|
|
78
|
+
const patchResult = (await edit.execute(
|
|
79
|
+
"t1",
|
|
80
|
+
{ action: "patch", patchParams: { canvasId: "c1", nodeId: "n1", reason: "r" } },
|
|
81
|
+
undefined,
|
|
82
|
+
undefined,
|
|
83
|
+
harness.ctx,
|
|
84
|
+
)) as { details: { summary: string } };
|
|
85
|
+
expect(patchResult.details.summary).toBe("Canvas edited");
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("canvas_read 带 name 与节点数", async () => {
|
|
90
|
+
await withExtLogEnv("1", async () => {
|
|
91
|
+
harness.selectMock.mockResolvedValue(
|
|
92
|
+
JSON.stringify({ canvasId: "c1", name: "多引擎支持", nodes: [{ id: "root" }, { id: "n1" }] }),
|
|
93
|
+
);
|
|
94
|
+
const read = toolOf(harness, "canvas_read");
|
|
95
|
+
const result = (await read.execute("t1", { canvasId: "c1" }, undefined, undefined, harness.ctx)) as {
|
|
96
|
+
details: { summary: string };
|
|
97
|
+
};
|
|
98
|
+
expect(result.details.summary).toBe('Read canvas "多引擎支持" · 2 nodes');
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("canvas_review resolve:条目 id + 处置结论", async () => {
|
|
103
|
+
await withExtLogEnv("1", async () => {
|
|
104
|
+
harness.selectMock.mockResolvedValue(
|
|
105
|
+
JSON.stringify({ ok: true, eventId: "e3", evType: "ReviewEntryResolved", summary: "r" }),
|
|
106
|
+
);
|
|
107
|
+
const review = toolOf(harness, "canvas_review");
|
|
108
|
+
const result = (await review.execute(
|
|
109
|
+
"t1",
|
|
110
|
+
{ action: "resolve", resolveParams: { canvasId: "c1", entryId: "rv-1", disposition: "accepted", note: "已修" } },
|
|
111
|
+
undefined,
|
|
112
|
+
undefined,
|
|
113
|
+
harness.ctx,
|
|
114
|
+
)) as { details: { summary: string } };
|
|
115
|
+
expect(result.details.summary).toBe("Review resolved rv-1 (accepted)");
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("canvas_review append:条目数 + must-fix 计数(params 侧统计)", async () => {
|
|
120
|
+
await withExtLogEnv("1", async () => {
|
|
121
|
+
harness.selectMock.mockResolvedValue(
|
|
122
|
+
JSON.stringify({ ok: true, eventId: "e4", evType: "ReviewEntryAppended", summary: "a" }),
|
|
123
|
+
);
|
|
124
|
+
const review = toolOf(harness, "canvas_review");
|
|
125
|
+
const result = (await review.execute(
|
|
126
|
+
"t1",
|
|
127
|
+
{
|
|
128
|
+
action: "append",
|
|
129
|
+
appendParams: {
|
|
130
|
+
canvasId: "c1",
|
|
131
|
+
round: 1,
|
|
132
|
+
entries: [
|
|
133
|
+
{ id: "rv-1", dimension: "support", severity: "must-fix", targetId: "e1", note: "n" },
|
|
134
|
+
{ id: "rv-2", dimension: "depth", severity: "suggestion", targetId: "n1", note: "n" },
|
|
135
|
+
{ id: "rv-3", dimension: "conflict", severity: "must-fix", targetId: "n2", note: "n" },
|
|
136
|
+
],
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
undefined,
|
|
140
|
+
undefined,
|
|
141
|
+
harness.ctx,
|
|
142
|
+
)) as { details: { summary: string } };
|
|
143
|
+
expect(result.details.summary).toBe("Review appended 3 entries · 2 must-fix");
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("canvas_review run:编排产物带轮次与写回条目数", async () => {
|
|
148
|
+
await withExtLogEnv("1", async () => {
|
|
149
|
+
// marker 三跳:review-request(留痕 mutate)→ read(全量,R1)→ review-append(写回 mutate)
|
|
150
|
+
harness.selectMock
|
|
151
|
+
.mockResolvedValueOnce(JSON.stringify({ ok: true, eventId: "e6", evType: "ReviewRequested", summary: "req" }))
|
|
152
|
+
.mockResolvedValueOnce(
|
|
153
|
+
JSON.stringify({
|
|
154
|
+
canvasId: "c1",
|
|
155
|
+
name: "多引擎支持",
|
|
156
|
+
root: { nodeId: "root", statement: "s", criterias: ["c"], scopeExclusions: [] },
|
|
157
|
+
direction: null,
|
|
158
|
+
nodes: [],
|
|
159
|
+
edges: [],
|
|
160
|
+
comments: [],
|
|
161
|
+
dirtyPaths: [],
|
|
162
|
+
review: null,
|
|
163
|
+
converged: false,
|
|
164
|
+
createdAt: "2026-10-17T00:00:00Z",
|
|
165
|
+
updatedAt: "2026-10-17T00:00:00Z",
|
|
166
|
+
}),
|
|
167
|
+
)
|
|
168
|
+
.mockResolvedValueOnce(JSON.stringify({ ok: true, eventId: "e7", evType: "ReviewEntryAppended", summary: "app" }));
|
|
169
|
+
const SLOT_KEY = Symbol.for("@zhushanwen/subagent-core.service");
|
|
170
|
+
// 槽值形状 = { current: service } 持有者(service-bootstrap getServiceSlot),
|
|
171
|
+
// getSubagentService() 解 .current——直接放 service 本体会取不到
|
|
172
|
+
(globalThis as Record<symbol, unknown>)[SLOT_KEY] = {
|
|
173
|
+
current: {
|
|
174
|
+
executeAndAwait: async () => ({
|
|
175
|
+
parsedOutput: {
|
|
176
|
+
entries: [{ id: "rv-1", dimension: "support", severity: "must-fix", targetId: "e1", note: "n" }],
|
|
177
|
+
summary: "总体判断",
|
|
178
|
+
},
|
|
179
|
+
}),
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
try {
|
|
183
|
+
const review = toolOf(harness, "canvas_review");
|
|
184
|
+
const result = (await review.execute(
|
|
185
|
+
"t1",
|
|
186
|
+
{ action: "run", runParams: { canvasId: "c1" } },
|
|
187
|
+
undefined,
|
|
188
|
+
undefined,
|
|
189
|
+
harness.ctx,
|
|
190
|
+
)) as { details: { summary: string } };
|
|
191
|
+
expect(result.details.summary).toBe("Review round 1 · +1 entries");
|
|
192
|
+
} finally {
|
|
193
|
+
delete (globalThis as Record<symbol, unknown>)[SLOT_KEY];
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("canvas_review run:达轮次上限停回短路带状态词", async () => {
|
|
199
|
+
await withExtLogEnv("1", async () => {
|
|
200
|
+
harness.selectMock
|
|
201
|
+
.mockResolvedValueOnce(JSON.stringify({ ok: true, eventId: "e6", evType: "ReviewRequested", summary: "已达上限" }))
|
|
202
|
+
.mockResolvedValueOnce(
|
|
203
|
+
JSON.stringify({
|
|
204
|
+
canvasId: "c1",
|
|
205
|
+
name: "多引擎支持",
|
|
206
|
+
root: { nodeId: "root", statement: "s", criterias: ["c"], scopeExclusions: [] },
|
|
207
|
+
direction: null,
|
|
208
|
+
nodes: [],
|
|
209
|
+
edges: [],
|
|
210
|
+
comments: [],
|
|
211
|
+
dirtyPaths: [],
|
|
212
|
+
review: {
|
|
213
|
+
status: "max-rounds",
|
|
214
|
+
round: 5,
|
|
215
|
+
maxRounds: 5,
|
|
216
|
+
entries: [],
|
|
217
|
+
stopNote: "停回用户裁决",
|
|
218
|
+
recoverHint: "升级裁决后重发",
|
|
219
|
+
},
|
|
220
|
+
converged: false,
|
|
221
|
+
createdAt: "2026-10-17T00:00:00Z",
|
|
222
|
+
updatedAt: "2026-10-17T00:00:00Z",
|
|
223
|
+
}),
|
|
224
|
+
);
|
|
225
|
+
const review = toolOf(harness, "canvas_review");
|
|
226
|
+
const result = (await review.execute(
|
|
227
|
+
"t1",
|
|
228
|
+
{ action: "run", runParams: { canvasId: "c1" } },
|
|
229
|
+
undefined,
|
|
230
|
+
undefined,
|
|
231
|
+
harness.ctx,
|
|
232
|
+
)) as { details: { summary: string } };
|
|
233
|
+
expect(result.details.summary).toBe("Review stopped: max-rounds");
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("canvas_export 按目标形态呈现(导出文件路径不在 mutate 回包内,不虚构)", async () => {
|
|
238
|
+
await withExtLogEnv("1", async () => {
|
|
239
|
+
harness.selectMock.mockResolvedValue(
|
|
240
|
+
JSON.stringify({ ok: true, eventId: "e5", evType: "CanvasExported", summary: "x" }),
|
|
241
|
+
);
|
|
242
|
+
const exp = toolOf(harness, "canvas_export");
|
|
243
|
+
const docResult = (await exp.execute(
|
|
244
|
+
"t1",
|
|
245
|
+
{ canvasId: "c1", target: "planning-doc" },
|
|
246
|
+
undefined,
|
|
247
|
+
undefined,
|
|
248
|
+
harness.ctx,
|
|
249
|
+
)) as { details: { summary: string } };
|
|
250
|
+
expect(docResult.details.summary).toBe("Exported planning-doc");
|
|
251
|
+
|
|
252
|
+
const cardResult = (await exp.execute(
|
|
253
|
+
"t1",
|
|
254
|
+
{ canvasId: "c1", target: "requirement-card", leafId: "leaf-9" },
|
|
255
|
+
undefined,
|
|
256
|
+
undefined,
|
|
257
|
+
harness.ctx,
|
|
258
|
+
)) as { details: { summary: string } };
|
|
259
|
+
expect(cardResult.details.summary).toBe("Exported requirement-card (leaf leaf-9)");
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("canvas_list 带清单计数", async () => {
|
|
264
|
+
await withExtLogEnv("1", async () => {
|
|
265
|
+
harness.selectMock.mockResolvedValue(
|
|
266
|
+
JSON.stringify({ canvases: [{ canvasId: "c1" }, { canvasId: "c2" }, { canvasId: "c3" }] }),
|
|
267
|
+
);
|
|
268
|
+
const list = toolOf(harness, "canvas_list");
|
|
269
|
+
const result = (await list.execute("t1", {}, undefined, undefined, harness.ctx)) as {
|
|
270
|
+
details: { summary: string };
|
|
271
|
+
};
|
|
272
|
+
expect(result.details.summary).toBe("Listed 3 canvases");
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
describe("summary 纯函数数据缺失分支(形状漂移退化为短文案,不抛错)", () => {
|
|
278
|
+
it("canvasEnterSummary:reply 与 params 均无名 → 裸文案", () => {
|
|
279
|
+
expect(canvasEnterSummary({ statement: "s", criterias: ["c"], scope: [] }, { ok: true })).toBe("Entered canvas");
|
|
280
|
+
expect(canvasEnterSummary({ statement: "s", criterias: ["c"], scope: [] }, "not-an-object")).toBe("Entered canvas");
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("canvasEditSummary:grow 缺参数块 / batch 非数组 → 无数量后缀", () => {
|
|
284
|
+
expect(canvasEditSummary({ action: "grow" })).toBe("Canvas edited");
|
|
285
|
+
expect(canvasEditSummary({ action: "select" })).toBe("Canvas edited");
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("canvasReadSummary:name 缺失 / nodes 非数组 → 逐级省略", () => {
|
|
289
|
+
expect(canvasReadSummary({ name: "n", nodes: "bad" })).toBe('Read canvas "n"');
|
|
290
|
+
expect(canvasReadSummary({})).toBe("Read canvas");
|
|
291
|
+
expect(canvasReadSummary(undefined)).toBe("Read canvas");
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("canvasReviewSummary run:编排产物缺 round/appended → Review ran;append 缺 entries → Review appended;resolve 缺参数块 → Review resolved", () => {
|
|
295
|
+
expect(canvasReviewSummary({ action: "run" }, { ok: true })).toBe("Review ran");
|
|
296
|
+
expect(canvasReviewSummary({ action: "append" }, { ok: true })).toBe("Review appended");
|
|
297
|
+
expect(canvasReviewSummary({ action: "resolve" }, { ok: true })).toBe("Review resolved");
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("canvasReviewSummary run 停回:status 缺失 → 无状态词", () => {
|
|
301
|
+
expect(canvasReviewSummary({ action: "run" }, { stopped: true })).toBe("Review stopped");
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("canvasReviewSummary append:全 suggestion(must-fix 零计数不显示零后缀)", () => {
|
|
305
|
+
expect(
|
|
306
|
+
canvasReviewSummary(
|
|
307
|
+
{ action: "append", appendParams: { canvasId: "c", round: 1, entries: [{ id: "a", severity: "suggestion" }] } },
|
|
308
|
+
{ ok: true },
|
|
309
|
+
),
|
|
310
|
+
).toBe("Review appended 1 entries");
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("canvasExportSummary:requirement-card 缺 leafId → 通用文案", () => {
|
|
314
|
+
expect(canvasExportSummary({ canvasId: "c", target: "requirement-card" })).toBe("Exported canvas");
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("canvasListSummary:canvases 缺失/非数组 → 无计数", () => {
|
|
318
|
+
expect(canvasListSummary({})).toBe("Listed canvases");
|
|
319
|
+
expect(canvasListSummary({ canvases: 3 })).toBe("Listed canvases");
|
|
320
|
+
});
|
|
321
|
+
});
|
package/src/render.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* canvas 六工具共用 renderResult(pi TUI 侧;taiji GUI 对话流不走本文件——GUI 直读
|
|
3
|
+
* details.summary)。形态对齐 todo 扩展 render.ts 先例:折叠态一行摘要(dim 染色),
|
|
4
|
+
* 展开态 / 无摘要(异常形态)回退原始 content 文本。
|
|
5
|
+
*/
|
|
6
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
8
|
+
import { firstContentText } from "@zhushanwen/extension-protocol";
|
|
9
|
+
|
|
10
|
+
/** pi toolResult 通用形状(AgentToolResult 的结构最小面,renderResult 入参收窄用)。 */
|
|
11
|
+
interface ToolResultShape { // oe-exempt:20261017:framework:details 数据形状窄 view(单实现消费,非抽象接口)
|
|
12
|
+
content: Array<{ type: string; text?: string }>;
|
|
13
|
+
details?: { summary?: unknown };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function renderCanvasToolResult(result: unknown, options: { expanded: boolean }, theme: Theme): Text {
|
|
17
|
+
const r = result as ToolResultShape;
|
|
18
|
+
const summary = r.details?.summary;
|
|
19
|
+
if (options.expanded === false && typeof summary === "string" && summary !== "") {
|
|
20
|
+
return new Text(theme.fg("dim", summary), 0, 0);
|
|
21
|
+
}
|
|
22
|
+
return new Text(firstContentText(r), 0, 0);
|
|
23
|
+
}
|
package/src/summary.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* canvas 六工具结果的一行摘要(details.summary 单一产出点)。
|
|
3
|
+
*
|
|
4
|
+
* 消费两路:各工具 execute 组装 details.summary(taiji GUI 对话流收起态展示)+
|
|
5
|
+
* render.ts 的 renderResult 折叠态渲染(pi TUI)。纯函数无 IO:reply 是 marker
|
|
6
|
+
* 通道回包(runtime 侧 JSON,不信任外部格式——逐字段守卫,形状漂移退化为无后缀
|
|
7
|
+
* 短文案而非抛错;摘要只是展示辅助,不得反过来影响工具成功路径)。
|
|
8
|
+
*
|
|
9
|
+
* 数据可得性按契约(@zhushanwen/extension-protocol canvas 契约模块)核对:
|
|
10
|
+
* - enter 打开形态 / read 的 reply = CanvasState(name/nodes);enter 创建形态与
|
|
11
|
+
* mutate 族 reply = CanvasMutateReply(ok/eventId/evType/summary,无画布名)。
|
|
12
|
+
* - review run 的 reply 是本包编排产物(review.ts runReviewRound:round/appended);
|
|
13
|
+
* max-rounds 停回短路是 stopped/status 形状(tool-review.ts)。
|
|
14
|
+
* - export 的 reply 是 CanvasMutateReply,不携带导出文件路径——target 从 params 取。
|
|
15
|
+
*/
|
|
16
|
+
import type { Static } from "typebox";
|
|
17
|
+
|
|
18
|
+
import type { CanvasEditParams } from "./tool-edit.js";
|
|
19
|
+
import type { CanvasEnterParams } from "./tool-enter.js";
|
|
20
|
+
import type { CanvasExportParams } from "./tool-export.js";
|
|
21
|
+
import type { CanvasReviewParams } from "./tool-review.js";
|
|
22
|
+
|
|
23
|
+
/** 不信任来源的对象访问统一入口(review.ts recordOf 同款)。 */
|
|
24
|
+
function recordOf(v: unknown): Record<string, unknown> | undefined {
|
|
25
|
+
return typeof v === "object" && v !== null && !Array.isArray(v)
|
|
26
|
+
? (v as Record<string, unknown>)
|
|
27
|
+
: undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function stringField(rec: Record<string, unknown> | undefined, key: string): string | undefined {
|
|
31
|
+
const v = rec?.[key];
|
|
32
|
+
return typeof v === "string" && v !== "" ? v : undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function arrayField(rec: Record<string, unknown> | undefined, key: string): unknown[] | undefined {
|
|
36
|
+
const v = rec?.[key];
|
|
37
|
+
return Array.isArray(v) ? v : undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function numberField(rec: Record<string, unknown> | undefined, key: string): number | undefined {
|
|
41
|
+
const v = rec?.[key];
|
|
42
|
+
return typeof v === "number" && Number.isFinite(v) ? v : undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** canvas_enter:打开形态 reply 带 CanvasState.name;创建形态 reply 无名(mutate
|
|
46
|
+
* 信封),退 params.name(runtime 可能按 statement 派生名,params 未传则裸文案)。 */
|
|
47
|
+
export function canvasEnterSummary(params: Static<typeof CanvasEnterParams>, reply: unknown): string {
|
|
48
|
+
const name = stringField(recordOf(reply), "name") ?? (params.name !== undefined && params.name !== "" ? params.name : undefined);
|
|
49
|
+
return name !== undefined ? `Entered canvas "${name}"` : "Entered canvas";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** canvas_edit:grow 从 params.batch 带新增节点数;其余 action 无数量语义。 */
|
|
53
|
+
export function canvasEditSummary(params: Static<typeof CanvasEditParams>): string {
|
|
54
|
+
if (params.action === "grow" && params.growParams && Array.isArray(params.growParams.batch)) {
|
|
55
|
+
return `Canvas edited · +${params.growParams.batch.length} nodes`;
|
|
56
|
+
}
|
|
57
|
+
return "Canvas edited";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** canvas_read:reply = CanvasState(name + nodes 必备字段,守卫漂移时逐级省略)。 */
|
|
61
|
+
export function canvasReadSummary(reply: unknown): string {
|
|
62
|
+
const rec = recordOf(reply);
|
|
63
|
+
const name = stringField(rec, "name");
|
|
64
|
+
const nodes = arrayField(rec, "nodes");
|
|
65
|
+
if (name !== undefined && nodes !== undefined) return `Read canvas "${name}" · ${nodes.length} nodes`;
|
|
66
|
+
if (name !== undefined) return `Read canvas "${name}"`;
|
|
67
|
+
return "Read canvas";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* canvas_review 三 action 各按可得数据呈现:
|
|
72
|
+
* - run:编排产物带 round/appended(写回条目数);达轮次上限的停回短路带
|
|
73
|
+
* stopped/status。verdict(pass/fail)不在工具结果里(报告聚合在画布状态,
|
|
74
|
+
* 经 canvas_read 读),文案不虚构该数据。
|
|
75
|
+
* - append:entries 数与 must-fix 计数从 params 数(报告条目写回量)。
|
|
76
|
+
* - resolve:处置条目与三选一结论从 params 取。
|
|
77
|
+
*/
|
|
78
|
+
export function canvasReviewSummary(params: Static<typeof CanvasReviewParams>, reply: unknown): string {
|
|
79
|
+
if (params.action === "run") {
|
|
80
|
+
const rec = recordOf(reply);
|
|
81
|
+
if (rec?.stopped === true) {
|
|
82
|
+
const status = stringField(rec, "status");
|
|
83
|
+
return status !== undefined ? `Review stopped: ${status}` : "Review stopped";
|
|
84
|
+
}
|
|
85
|
+
const round = numberField(rec, "round");
|
|
86
|
+
const appended = numberField(rec, "appended");
|
|
87
|
+
if (round !== undefined && appended !== undefined) return `Review round ${round} · +${appended} entries`;
|
|
88
|
+
return "Review ran";
|
|
89
|
+
}
|
|
90
|
+
if (params.action === "append") {
|
|
91
|
+
const entries = params.appendParams?.entries;
|
|
92
|
+
if (Array.isArray(entries)) {
|
|
93
|
+
const mustFix = entries.filter((e) => recordOf(e)?.severity === "must-fix").length;
|
|
94
|
+
return mustFix > 0
|
|
95
|
+
? `Review appended ${entries.length} entries · ${mustFix} must-fix`
|
|
96
|
+
: `Review appended ${entries.length} entries`;
|
|
97
|
+
}
|
|
98
|
+
return "Review appended";
|
|
99
|
+
}
|
|
100
|
+
const resolve = params.resolveParams;
|
|
101
|
+
if (resolve) return `Review resolved ${resolve.entryId} (${resolve.disposition})`;
|
|
102
|
+
return "Review resolved";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** canvas_export:reply 是 mutate 信封不携带产物路径——按 target 呈现导出物。 */
|
|
106
|
+
export function canvasExportSummary(params: Static<typeof CanvasExportParams>): string {
|
|
107
|
+
if (params.target === "planning-doc") return "Exported planning-doc";
|
|
108
|
+
if (params.target === "requirement-card" && params.leafId !== undefined && params.leafId !== "") {
|
|
109
|
+
return `Exported requirement-card (leaf ${params.leafId})`;
|
|
110
|
+
}
|
|
111
|
+
return "Exported canvas";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** canvas_list:reply = CanvasListReply { canvases }。 */
|
|
115
|
+
export function canvasListSummary(reply: unknown): string {
|
|
116
|
+
const canvases = arrayField(recordOf(reply), "canvases");
|
|
117
|
+
return canvases !== undefined ? `Listed ${canvases.length} canvases` : "Listed canvases";
|
|
118
|
+
}
|
package/src/tool-edit.ts
CHANGED
|
@@ -17,11 +17,13 @@
|
|
|
17
17
|
* 幂等重试一致性判定也比对它)。
|
|
18
18
|
*/
|
|
19
19
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
20
|
-
import type { ExtensionAPI, ExtensionToolContext } from "@earendil-works/pi-coding-agent";
|
|
20
|
+
import type { ExtensionAPI, ExtensionToolContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
21
21
|
import { Type, type Static } from "typebox";
|
|
22
22
|
import type { CanvasMarkerRequest } from "@zhushanwen/extension-protocol";
|
|
23
23
|
import { isCanvasMarkerRequest } from "@zhushanwen/extension-protocol";
|
|
24
24
|
|
|
25
|
+
import { renderCanvasToolResult } from "./render.js";
|
|
26
|
+
import { canvasEditSummary } from "./summary.js";
|
|
25
27
|
import { callCanvas } from "./marker-channel.js";
|
|
26
28
|
import { hostGateError, isTaijiGuiHost } from "./host.js";
|
|
27
29
|
|
|
@@ -185,6 +187,9 @@ export function registerCanvasEditTool(
|
|
|
185
187
|
description: cfg.description,
|
|
186
188
|
promptSnippet: cfg.promptSnippet,
|
|
187
189
|
parameters: cfg.parameters,
|
|
190
|
+
renderResult(result: unknown, options: { expanded: boolean }, theme: Theme) {
|
|
191
|
+
return renderCanvasToolResult(result, options, theme);
|
|
192
|
+
},
|
|
188
193
|
async execute(
|
|
189
194
|
_toolCallId: string,
|
|
190
195
|
params: Static<typeof CanvasEditParams>,
|
|
@@ -205,7 +210,10 @@ export function registerCanvasEditTool(
|
|
|
205
210
|
}
|
|
206
211
|
const markerRequest: CanvasMarkerRequest = request;
|
|
207
212
|
const reply = await callCanvas(ctx, markerRequest.action, markerRequest.params);
|
|
208
|
-
return {
|
|
213
|
+
return {
|
|
214
|
+
content: [{ type: "text" as const, text: JSON.stringify(reply) }],
|
|
215
|
+
details: { summary: canvasEditSummary(params) },
|
|
216
|
+
};
|
|
209
217
|
},
|
|
210
218
|
});
|
|
211
219
|
}
|
package/src/tool-enter.ts
CHANGED
|
@@ -10,9 +10,11 @@
|
|
|
10
10
|
* canvas_read / canvas_review / canvas_export」指引——deferred 工具须检索激活后
|
|
11
11
|
* 下一轮起可调,直接点名工具名让模型检索词精准命中。
|
|
12
12
|
*/
|
|
13
|
-
import type { ExtensionAPI, ExtensionToolContext } from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import type { ExtensionAPI, ExtensionToolContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
14
14
|
import { Type, type Static } from "typebox";
|
|
15
15
|
|
|
16
|
+
import { renderCanvasToolResult } from "./render.js";
|
|
17
|
+
import { canvasEnterSummary } from "./summary.js";
|
|
16
18
|
import { callCanvas } from "./marker-channel.js";
|
|
17
19
|
import { hostGateError, isTaijiGuiHost } from "./host.js";
|
|
18
20
|
|
|
@@ -129,6 +131,9 @@ export function registerCanvasEnterTool(pi: ExtensionAPI, cfg: EnterToolConfig):
|
|
|
129
131
|
description: cfg.description,
|
|
130
132
|
promptSnippet: cfg.promptSnippet,
|
|
131
133
|
parameters: cfg.parameters,
|
|
134
|
+
renderResult(result: unknown, options: { expanded: boolean }, theme: Theme) {
|
|
135
|
+
return renderCanvasToolResult(result, options, theme);
|
|
136
|
+
},
|
|
132
137
|
async execute(
|
|
133
138
|
_toolCallId: string,
|
|
134
139
|
params: Static<typeof CanvasEnterParams>,
|
|
@@ -141,7 +146,7 @@ export function registerCanvasEnterTool(pi: ExtensionAPI, cfg: EnterToolConfig):
|
|
|
141
146
|
if ("error" in request) throw request.error;
|
|
142
147
|
const reply = await callCanvas(ctx, request.action, request.params);
|
|
143
148
|
const text = `${JSON.stringify(reply)}\n\n${CANVAS_ENTER_GUIDANCE}`;
|
|
144
|
-
return { content: [{ type: "text" as const, text }], details:
|
|
149
|
+
return { content: [{ type: "text" as const, text }], details: { summary: canvasEnterSummary(params, reply) } };
|
|
145
150
|
},
|
|
146
151
|
});
|
|
147
152
|
}
|
package/src/tool-export.ts
CHANGED
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
* runtime 以 not_converged 拒绝(错误码 + 恢复动作文案经 marker-channel throw 透出)。
|
|
6
6
|
*/
|
|
7
7
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
8
|
-
import type { ExtensionAPI, ExtensionToolContext } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import type { ExtensionAPI, ExtensionToolContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
9
9
|
import { Type, type Static } from "typebox";
|
|
10
10
|
|
|
11
|
+
import { renderCanvasToolResult } from "./render.js";
|
|
12
|
+
import { canvasExportSummary } from "./summary.js";
|
|
11
13
|
import { callCanvas } from "./marker-channel.js";
|
|
12
14
|
import { hostGateError, isTaijiGuiHost } from "./host.js";
|
|
13
15
|
|
|
@@ -55,6 +57,9 @@ export function registerCanvasExportTool(
|
|
|
55
57
|
description: cfg.description,
|
|
56
58
|
promptSnippet: cfg.promptSnippet,
|
|
57
59
|
parameters: cfg.parameters,
|
|
60
|
+
renderResult(result: unknown, options: { expanded: boolean }, theme: Theme) {
|
|
61
|
+
return renderCanvasToolResult(result, options, theme);
|
|
62
|
+
},
|
|
58
63
|
async execute(
|
|
59
64
|
_toolCallId: string,
|
|
60
65
|
params: Static<typeof CanvasExportParams>,
|
|
@@ -66,7 +71,10 @@ export function registerCanvasExportTool(
|
|
|
66
71
|
const request = exportRequestOf(params);
|
|
67
72
|
if ("error" in request) throw request.error;
|
|
68
73
|
const reply = await callCanvas(ctx, "export", request.params);
|
|
69
|
-
return {
|
|
74
|
+
return {
|
|
75
|
+
content: [{ type: "text" as const, text: JSON.stringify(reply) }],
|
|
76
|
+
details: { summary: canvasExportSummary(params) },
|
|
77
|
+
};
|
|
70
78
|
},
|
|
71
79
|
});
|
|
72
80
|
}
|
package/src/tool-list.ts
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
* canvas_list —— 列画布清单(设计 §3.1(2) 工具面):canvas_enter 打开形态的
|
|
3
3
|
* canvasId 来源;无参数(过滤无字段——runtime 按契约 Record 结构性拒绝夹带)。
|
|
4
4
|
*/
|
|
5
|
-
import type { ExtensionAPI, ExtensionToolContext } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import type { ExtensionAPI, ExtensionToolContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { Type, type Static } from "typebox";
|
|
7
7
|
|
|
8
|
+
import { renderCanvasToolResult } from "./render.js";
|
|
9
|
+
import { canvasListSummary } from "./summary.js";
|
|
8
10
|
import { callCanvas } from "./marker-channel.js";
|
|
9
11
|
import { hostGateError, isTaijiGuiHost } from "./host.js";
|
|
10
12
|
|
|
@@ -21,6 +23,9 @@ export function registerCanvasListTool(
|
|
|
21
23
|
description: cfg.description,
|
|
22
24
|
promptSnippet: cfg.promptSnippet,
|
|
23
25
|
parameters: cfg.parameters,
|
|
26
|
+
renderResult(result: unknown, options: { expanded: boolean }, theme: Theme) {
|
|
27
|
+
return renderCanvasToolResult(result, options, theme);
|
|
28
|
+
},
|
|
24
29
|
async execute(
|
|
25
30
|
_toolCallId: string,
|
|
26
31
|
_params: Static<typeof CanvasListParams>,
|
|
@@ -30,7 +35,10 @@ export function registerCanvasListTool(
|
|
|
30
35
|
) {
|
|
31
36
|
if (!isTaijiGuiHost(ctx)) throw hostGateError();
|
|
32
37
|
const reply = await callCanvas(ctx, "list", {});
|
|
33
|
-
return {
|
|
38
|
+
return {
|
|
39
|
+
content: [{ type: "text" as const, text: JSON.stringify(reply) }],
|
|
40
|
+
details: { summary: canvasListSummary(reply) },
|
|
41
|
+
};
|
|
34
42
|
},
|
|
35
43
|
});
|
|
36
44
|
}
|
package/src/tool-read.ts
CHANGED
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
* reviewer prompt;ExecuteOptions 无 tools 通道,结构上无法授工具);
|
|
6
6
|
* 响应纪律「先 read 再动」的数据入口。
|
|
7
7
|
*/
|
|
8
|
-
import type { ExtensionAPI, ExtensionToolContext } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import type { ExtensionAPI, ExtensionToolContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
9
9
|
import { Type, type Static } from "typebox";
|
|
10
10
|
|
|
11
|
+
import { renderCanvasToolResult } from "./render.js";
|
|
12
|
+
import { canvasReadSummary } from "./summary.js";
|
|
11
13
|
import { callCanvas } from "./marker-channel.js";
|
|
12
14
|
import { hostGateError, isTaijiGuiHost } from "./host.js";
|
|
13
15
|
|
|
@@ -29,6 +31,9 @@ export function registerCanvasReadTool(
|
|
|
29
31
|
description: cfg.description,
|
|
30
32
|
promptSnippet: cfg.promptSnippet,
|
|
31
33
|
parameters: cfg.parameters,
|
|
34
|
+
renderResult(result: unknown, options: { expanded: boolean }, theme: Theme) {
|
|
35
|
+
return renderCanvasToolResult(result, options, theme);
|
|
36
|
+
},
|
|
32
37
|
async execute(
|
|
33
38
|
_toolCallId: string,
|
|
34
39
|
params: Static<typeof CanvasReadParams>,
|
|
@@ -38,7 +43,10 @@ export function registerCanvasReadTool(
|
|
|
38
43
|
) {
|
|
39
44
|
if (!isTaijiGuiHost(ctx)) throw hostGateError();
|
|
40
45
|
const reply = await callCanvas(ctx, "read", { canvasId: params.canvasId, subtree: params.subtree });
|
|
41
|
-
return {
|
|
46
|
+
return {
|
|
47
|
+
content: [{ type: "text" as const, text: JSON.stringify(reply) }],
|
|
48
|
+
details: { summary: canvasReadSummary(reply) },
|
|
49
|
+
};
|
|
42
50
|
},
|
|
43
51
|
});
|
|
44
52
|
}
|
package/src/tool-review.ts
CHANGED
|
@@ -13,13 +13,15 @@
|
|
|
13
13
|
* 分离构造保证:reviewer 无改图能力(零工具);主 agent 修复走 canvas_edit。
|
|
14
14
|
*/
|
|
15
15
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
16
|
-
import type { ExtensionAPI, ExtensionToolContext } from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import type { ExtensionAPI, ExtensionToolContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
17
17
|
import { Type, type Static } from "typebox";
|
|
18
18
|
import {
|
|
19
19
|
isCanvasReviewAppendParams,
|
|
20
20
|
isCanvasReviewResolveParams,
|
|
21
21
|
} from "@zhushanwen/extension-protocol";
|
|
22
22
|
|
|
23
|
+
import { renderCanvasToolResult } from "./render.js";
|
|
24
|
+
import { canvasReviewSummary } from "./summary.js";
|
|
23
25
|
import { callCanvas } from "./marker-channel.js";
|
|
24
26
|
import { hostGateError, isTaijiGuiHost } from "./host.js";
|
|
25
27
|
import {
|
|
@@ -97,6 +99,9 @@ export function registerCanvasReviewTool(
|
|
|
97
99
|
description: cfg.description,
|
|
98
100
|
promptSnippet: cfg.promptSnippet,
|
|
99
101
|
parameters: cfg.parameters,
|
|
102
|
+
renderResult(result: unknown, options: { expanded: boolean }, theme: Theme) {
|
|
103
|
+
return renderCanvasToolResult(result, options, theme);
|
|
104
|
+
},
|
|
100
105
|
async execute(
|
|
101
106
|
_toolCallId: string,
|
|
102
107
|
params: Static<typeof CanvasReviewParams>,
|
|
@@ -121,7 +126,10 @@ export function registerCanvasReviewTool(
|
|
|
121
126
|
);
|
|
122
127
|
}
|
|
123
128
|
const reply = await callCanvas(ctx, "review-resolve", params.resolveParams);
|
|
124
|
-
return {
|
|
129
|
+
return {
|
|
130
|
+
content: [{ type: "text" as const, text: JSON.stringify(reply) }],
|
|
131
|
+
details: { summary: canvasReviewSummary(params, reply) },
|
|
132
|
+
};
|
|
125
133
|
}
|
|
126
134
|
if (params.action === "append") {
|
|
127
135
|
const appendParams = params.appendParams;
|
|
@@ -145,7 +153,10 @@ export function registerCanvasReviewTool(
|
|
|
145
153
|
);
|
|
146
154
|
}
|
|
147
155
|
const reply = await callCanvas(ctx, "review-append", request);
|
|
148
|
-
return {
|
|
156
|
+
return {
|
|
157
|
+
content: [{ type: "text" as const, text: JSON.stringify(reply) }],
|
|
158
|
+
details: { summary: canvasReviewSummary(params, reply) },
|
|
159
|
+
};
|
|
149
160
|
}
|
|
150
161
|
// action === "run":编排链(review-request 留痕 → read 全量 → 派 reviewer → append 写回)
|
|
151
162
|
if (!params.runParams) {
|
|
@@ -169,15 +180,18 @@ export function registerCanvasReviewTool(
|
|
|
169
180
|
// 终态 + 停回说明(mutate 本身成功——合法终态转移而非校验失败),本轮没有开始,
|
|
170
181
|
// 不得再派 reviewer。停回说明结构化可读(stopNote/recoverHint),不解析 summary 文本。
|
|
171
182
|
if (readReply.review?.status === "max-rounds") {
|
|
172
|
-
const
|
|
183
|
+
const payload = {
|
|
173
184
|
ok: true,
|
|
174
185
|
canvasId: readReply.canvasId,
|
|
175
186
|
stopped: true,
|
|
176
187
|
status: readReply.review.status,
|
|
177
188
|
stopNote: readReply.review.stopNote,
|
|
178
189
|
recoverHint: readReply.review.recoverHint,
|
|
179
|
-
}
|
|
180
|
-
return {
|
|
190
|
+
};
|
|
191
|
+
return {
|
|
192
|
+
content: [{ type: "text" as const, text: JSON.stringify(payload) }],
|
|
193
|
+
details: { summary: canvasReviewSummary(params, payload) },
|
|
194
|
+
};
|
|
181
195
|
}
|
|
182
196
|
const dispatcher = resolveReviewerDispatcher();
|
|
183
197
|
const outcome = await runReviewRound({
|
|
@@ -195,7 +209,10 @@ export function registerCanvasReviewTool(
|
|
|
195
209
|
dropped: outcome.dropped,
|
|
196
210
|
summary: outcome.summary,
|
|
197
211
|
});
|
|
198
|
-
return {
|
|
212
|
+
return {
|
|
213
|
+
content: [{ type: "text" as const, text }],
|
|
214
|
+
details: { summary: canvasReviewSummary(params, outcome) },
|
|
215
|
+
};
|
|
199
216
|
},
|
|
200
217
|
});
|
|
201
218
|
}
|