@zhushanwen/pi-goal 0.14.3 → 0.14.5
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 +4 -3
- package/src/__tests__/goal-control-rpc.test.ts +116 -3
- package/src/__tests__/gui.test.ts +14 -2
- package/src/__tests__/ports.test.ts +1 -1
- package/src/__tests__/stubs/pi-sdk.ts +10 -2
- package/src/adapters/goal-control-adapter.ts +46 -8
- package/src/adapters/ports.ts +2 -2
- package/src/engine/__tests__/goal.test.ts +1 -1
- package/src/index.ts +1 -1
- package/src/ports.ts +1 -1
- package/src/projection/__tests__/widget.test.ts +9 -2
- package/src/projection/gui.ts +9 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-goal",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.5",
|
|
4
4
|
"description": "Codex-style /goal command for Pi — persistent goal-driven autonomous loop with evidence-based completion, token budget, agent-reported blocking, and steering templates.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -29,10 +29,11 @@
|
|
|
29
29
|
"@earendil-works/pi-coding-agent": "^0.84.4",
|
|
30
30
|
"@earendil-works/pi-tui": "^0.84.4",
|
|
31
31
|
"typebox": "*",
|
|
32
|
-
"@zhushanwen/pi-pending-notifications": "0.7.
|
|
32
|
+
"@zhushanwen/pi-pending-notifications": "0.7.5"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@
|
|
35
|
+
"@zhushanwen/extension-protocol": "0.12.0",
|
|
36
|
+
"@zhushanwen/pi-extension-logger": "0.6.1"
|
|
36
37
|
},
|
|
37
38
|
"peerDependenciesMeta": {
|
|
38
39
|
"@earendil-works/pi-tui": {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 覆盖场景:
|
|
5
5
|
* - RPC 模式 create/complete/report_blocked → details 无 __gui__ 字段(状态展示改由
|
|
6
|
-
* handle* 内 updateWidget 经 guiSetWidget 推送
|
|
6
|
+
* handle* 内 updateWidget 经 guiSetWidget 推送 composer 任务托盘的协议 widget 区)
|
|
7
7
|
* - RPC 模式 + session.state = null → 前置 handler throw(分支不可达)
|
|
8
8
|
* - 非 RPC 模式(tui/json/print)→ details 同样无 __gui__,content 文本正常
|
|
9
9
|
*
|
|
@@ -11,12 +11,22 @@
|
|
|
11
11
|
* 不 mock handleCreate:走真实 createGoal 让 session.state 含完整字段(slug/objective/budget),
|
|
12
12
|
* 避免 hand-rolled state 与生产路径不一致。pi/ctx 用最小 fake(对齐 index.test.ts makeFactoryFixture)。
|
|
13
13
|
*/
|
|
14
|
-
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
15
|
-
import { describe, expect, it } from "vitest";
|
|
14
|
+
import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
16
16
|
|
|
17
17
|
import { registerGoalControlTool, type GoalControlDetails } from "../adapters/goal-control-adapter";
|
|
18
18
|
import { createGoalSession } from "../session";
|
|
19
19
|
|
|
20
|
+
// ── mock:logger + updateWidget(UI 通道降级用例断言面;其余用例下两者为 no-op,
|
|
21
|
+
// 与真实路径行为等价——fixture 的 setWidget/notify 本就是 no-op)──
|
|
22
|
+
const loggerWarnSpy = vi.hoisted(() => vi.fn());
|
|
23
|
+
vi.mock("@zhushanwen/pi-extension-logger", () => ({
|
|
24
|
+
getLogger: () => ({ warn: loggerWarnSpy, debug: vi.fn(), error: vi.fn() }),
|
|
25
|
+
}));
|
|
26
|
+
vi.mock("../projection/widget", () => ({ updateWidget: vi.fn() }));
|
|
27
|
+
|
|
28
|
+
import { updateWidget } from "../projection/widget";
|
|
29
|
+
|
|
20
30
|
// ── Types ─────────────────────────────────────────────
|
|
21
31
|
|
|
22
32
|
type ExecuteResult = {
|
|
@@ -33,6 +43,7 @@ interface CapturedTool {
|
|
|
33
43
|
onUpdate: unknown,
|
|
34
44
|
ctx: ExtensionContext,
|
|
35
45
|
) => Promise<ExecuteResult>;
|
|
46
|
+
renderCall: (args: Record<string, unknown>, theme: Theme) => unknown;
|
|
36
47
|
}
|
|
37
48
|
|
|
38
49
|
// ── Fake pi + ctx(最小化,对齐 index.test.ts 的 makeFactoryFixture)──
|
|
@@ -215,3 +226,105 @@ describe("goal_control execute — 非 RPC 模式无 __gui__", () => {
|
|
|
215
226
|
expect(result.content[0].text).toContain("Goal created");
|
|
216
227
|
});
|
|
217
228
|
});
|
|
229
|
+
|
|
230
|
+
// ── 辅助 UI 通道降级(create/complete/blocked 的 updateWidget/notify 在核心副作用
|
|
231
|
+
// 之后调用,UI 故障不得翻转工具结果)──────────────────
|
|
232
|
+
|
|
233
|
+
describe("goal_control execute — 辅助 UI 通道失败降级", () => {
|
|
234
|
+
beforeEach(() => {
|
|
235
|
+
vi.mocked(updateWidget).mockReset();
|
|
236
|
+
loggerWarnSpy.mockClear();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
/** 注册 tool 并同时持有 session 引用(断言持久化后的内存 state)。 */
|
|
240
|
+
function captureToolWithSession(pi: ExtensionAPI): { tool: CapturedTool; session: ReturnType<typeof createGoalSession> } {
|
|
241
|
+
let captured: CapturedTool | undefined;
|
|
242
|
+
const capturePi = {
|
|
243
|
+
...pi,
|
|
244
|
+
registerTool(tool: CapturedTool): void {
|
|
245
|
+
captured = tool;
|
|
246
|
+
},
|
|
247
|
+
} as unknown as ExtensionAPI;
|
|
248
|
+
const session = createGoalSession();
|
|
249
|
+
registerGoalControlTool(capturePi, session);
|
|
250
|
+
if (!captured) throw new Error("registerGoalControlTool did not register a tool");
|
|
251
|
+
return { tool: captured, session };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
it("create + updateWidget 抛错 → 工具调用仍成功返回、状态已创建(warn 留痕非静默)", async () => {
|
|
255
|
+
vi.mocked(updateWidget).mockImplementation(() => {
|
|
256
|
+
throw new Error("widget channel broken");
|
|
257
|
+
});
|
|
258
|
+
const { pi, ctx } = makeFixture("rpc");
|
|
259
|
+
const { tool, session } = captureToolWithSession(pi);
|
|
260
|
+
const result = await tool.execute(
|
|
261
|
+
"call-ui-fail",
|
|
262
|
+
{ action: "create", slug: "ui-fail", objective: "survive ui failure", successCriteria: ["tests pass"] },
|
|
263
|
+
undefined,
|
|
264
|
+
undefined,
|
|
265
|
+
ctx,
|
|
266
|
+
);
|
|
267
|
+
// 工具成功返回(UI 异常未上抛翻转结果)
|
|
268
|
+
expect(result.details.action).toBe("create");
|
|
269
|
+
expect(result.details.status).toBe("active");
|
|
270
|
+
expect(result.content[0].text).toContain("Goal created");
|
|
271
|
+
// 核心副作用不受影响:state 已落盘到 session(内存态可见,persist 已在 createGoal 内完成)
|
|
272
|
+
expect(session.state?.status).toBe("active");
|
|
273
|
+
expect(session.state?.objective).toBe("survive ui failure");
|
|
274
|
+
// 降级留痕:warn 而非静默
|
|
275
|
+
expect(loggerWarnSpy).toHaveBeenCalledTimes(1);
|
|
276
|
+
expect(loggerWarnSpy).toHaveBeenCalledWith(
|
|
277
|
+
expect.stringContaining("goal_control create"),
|
|
278
|
+
expect.objectContaining({ detail: expect.objectContaining({ err: "widget channel broken" }) }),
|
|
279
|
+
);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("complete + updateWidget 抛错 → 工具调用仍成功返回(终态已持久化)", async () => {
|
|
283
|
+
const { pi, ctx } = makeFixture("rpc");
|
|
284
|
+
const { tool, session } = captureToolWithSession(pi);
|
|
285
|
+
await createViaHandler(tool, pi, ctx, { slug: "ui-comp", objective: "to complete" });
|
|
286
|
+
vi.mocked(updateWidget).mockImplementation(() => {
|
|
287
|
+
throw new Error("widget channel broken");
|
|
288
|
+
});
|
|
289
|
+
const result = await tool.execute(
|
|
290
|
+
"call-ui-comp",
|
|
291
|
+
{ action: "complete", evidence: "tests green" },
|
|
292
|
+
undefined,
|
|
293
|
+
undefined,
|
|
294
|
+
ctx,
|
|
295
|
+
);
|
|
296
|
+
expect(result.details.status).toBe("complete");
|
|
297
|
+
expect(session.state?.status).toBe("complete");
|
|
298
|
+
expect(loggerWarnSpy).toHaveBeenCalledWith(
|
|
299
|
+
expect.stringContaining("goal_control complete"),
|
|
300
|
+
expect.objectContaining({ detail: expect.objectContaining({ err: "widget channel broken" }) }),
|
|
301
|
+
);
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// ── renderCall 非法 args 安全渲染(TUI 渲染先于 schema 校验)──
|
|
306
|
+
|
|
307
|
+
const stubTheme = {
|
|
308
|
+
fg: (_color: string, text: string) => text,
|
|
309
|
+
bold: (text: string) => text,
|
|
310
|
+
} as unknown as Theme;
|
|
311
|
+
|
|
312
|
+
/** 注册 → 捕获 tool → 以给定 args 调 renderCall,返回渲染文本(width 足够宽避免 wrap)。 */
|
|
313
|
+
function renderedCallText(args: unknown): string {
|
|
314
|
+
const { pi } = makeFixture("rpc");
|
|
315
|
+
const tool = captureTool(pi);
|
|
316
|
+
const node = tool.renderCall(args as Record<string, unknown>, stubTheme) as {
|
|
317
|
+
render: (width: number) => string[];
|
|
318
|
+
};
|
|
319
|
+
return node.render(400).join("\n");
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
describe("renderCall — 非法 args 安全渲染", () => {
|
|
323
|
+
it("args 为 null → 占位文本,不抛错", () => {
|
|
324
|
+
expect(renderedCallText(null)).toContain("(invalid args)");
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("action 非字符串 → 不抛错,落 report_blocked 回落分支(与原行为一致)", () => {
|
|
328
|
+
expect(renderedCallText({ action: 42 })).toContain("report_blocked");
|
|
329
|
+
});
|
|
330
|
+
});
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 覆盖:
|
|
5
5
|
* - 内容根 = group(透明组合容器):stats-line(status/turn[+tokens])+ list-tree(criteria)
|
|
6
|
-
* - meta:title=slug、状态点语义(done/failed/idle/running)、token 进度(取整 + 百分比 + 阈值 severity
|
|
6
|
+
* - meta:title=slug、状态点语义(done/failed/idle/running)、token 进度(取整 + 百分比 + 阈值 severity)、
|
|
7
|
+
* 托盘 icon(显式 'target' key)
|
|
7
8
|
* - successCriteria 逐行 list-tree(不压扁/不截断/无 icon/不编号)
|
|
8
9
|
* - token 展示走 formatTokens(无 budget 分支)
|
|
9
10
|
*/
|
|
@@ -60,12 +61,23 @@ describe("buildGoalGui(v1.1 meta head 架构)", () => {
|
|
|
60
61
|
}),
|
|
61
62
|
);
|
|
62
63
|
expect(gui.meta).toEqual({
|
|
64
|
+
icon: "target",
|
|
63
65
|
title: "fix-auth",
|
|
64
66
|
status: "running",
|
|
65
67
|
progress: { current: 4200, total: 10000, label: "42%", severity: "ok" },
|
|
66
68
|
});
|
|
67
69
|
});
|
|
68
70
|
|
|
71
|
+
it("meta.icon 推显式 'target' key(协议形状 string;badge 不推,宿主从 progress.label 派生)", () => {
|
|
72
|
+
const gui = buildGoalGui(makeState({ slug: "fix-auth", budget: { tokenBudget: 10000 } }));
|
|
73
|
+
// 协议形状:string(宿主按 lucide 名解析)或 { paths }(自定义形状)。
|
|
74
|
+
// goal 只用 key 形态——与宿主内置 widgetKey 映射('goal'→Target)同款,
|
|
75
|
+
// 宿主改映射表也不会换掉 goal 图标。
|
|
76
|
+
expect(gui.meta!.icon).toBe("target");
|
|
77
|
+
// badge 留空:托盘缺省从 progress.label 派生同值百分比,推一份会变成双真相源
|
|
78
|
+
expect(gui.meta!.badge).toBeUndefined();
|
|
79
|
+
});
|
|
80
|
+
|
|
69
81
|
it("tokensUsed 浮点 → meta.progress.current 取整(1454.84…1 不进 UI)", () => {
|
|
70
82
|
const gui = buildGoalGui(
|
|
71
83
|
makeState({ tokensUsed: 1454.8400000000001, budget: { tokenBudget: 5000 } }),
|
|
@@ -104,7 +116,7 @@ describe("buildGoalGui(v1.1 meta head 架构)", () => {
|
|
|
104
116
|
tokensUsed: 3000,
|
|
105
117
|
}),
|
|
106
118
|
);
|
|
107
|
-
expect(gui.meta).toEqual({ title: expect.any(String), status: "running" });
|
|
119
|
+
expect(gui.meta).toEqual({ icon: "target", title: expect.any(String), status: "running" });
|
|
108
120
|
const stats = findChildComp(gui, "stats-line");
|
|
109
121
|
const items = stats.props.items as { label?: string; value?: string }[];
|
|
110
122
|
expect(items.find((i) => i.label === "tokens")).toMatchObject({ value: "3k" });
|
|
@@ -7,9 +7,17 @@ export function StringEnum<T extends readonly string[]>(values: T, _options?: Re
|
|
|
7
7
|
return values[0] as T[number];
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
// Text: pi-tui 的渲染节点 stub(goal-control-adapter renderCall/renderResult
|
|
10
|
+
// Text: pi-tui 的渲染节点 stub(goal-control-adapter renderCall/renderResult 返回值)。
|
|
11
|
+
// 承载 content 并提供最小 render(原样单行返回)——renderCall 的非法 args 安全渲染
|
|
12
|
+
// 测试经它断言渲染文本。
|
|
11
13
|
export class Text {
|
|
12
|
-
|
|
14
|
+
content: unknown;
|
|
15
|
+
constructor(content: unknown, _x: number, _y: number) {
|
|
16
|
+
this.content = content;
|
|
17
|
+
}
|
|
18
|
+
render(_width: number): string[] {
|
|
19
|
+
return [String(this.content)];
|
|
20
|
+
}
|
|
13
21
|
}
|
|
14
22
|
|
|
15
23
|
// Pi SDK types used in import chains — re-export as empty
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
|
|
34
34
|
import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
35
35
|
import { Text } from "@earendil-works/pi-tui";
|
|
36
|
+
import { getLogger } from "@zhushanwen/pi-extension-logger";
|
|
36
37
|
import { type Static, Type } from "typebox";
|
|
37
38
|
|
|
38
39
|
import { SHORT_ID_LENGTH } from "../constants";
|
|
@@ -112,6 +113,26 @@ export const GoalControlParams = Type.Object(
|
|
|
112
113
|
|
|
113
114
|
export type GoalControlParamsT = Static<typeof GoalControlParams>;
|
|
114
115
|
|
|
116
|
+
const logger = getLogger("goal");
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 辅助 UI 通道降级包装:updateWidget / notify 失败只 warn 留痕不上抛。
|
|
120
|
+
*
|
|
121
|
+
* 调用点全部位于核心副作用(createGoal / finalizeAndPersist / persistState)之后——
|
|
122
|
+
* 状态已落盘,UI 通道故障不能翻转工具结果(与 base-tool-enhance notify.ts 的接入点
|
|
123
|
+
* 降级同构)。错误串格式化内联(Error message / String)而非复用
|
|
124
|
+
* @zhushanwen/pi-ext-guards 的 toErrorMessage——不为一个一行 helper 引入运行时依赖。
|
|
125
|
+
*/
|
|
126
|
+
function runUiChannelSafe(action: GoalControlDetails["action"], goalId: string, fn: () => void): void {
|
|
127
|
+
try {
|
|
128
|
+
fn();
|
|
129
|
+
} catch (err) {
|
|
130
|
+
logger.warn(`goal_control ${action}: ui channel failed; persisted state unaffected`, {
|
|
131
|
+
detail: { goalId, err: err instanceof Error ? err.message : String(err) },
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
115
136
|
// ── Details(renderResult 数据来源)──────────────────
|
|
116
137
|
|
|
117
138
|
export interface GoalControlDetails {
|
|
@@ -212,14 +233,16 @@ export function handleCreate(
|
|
|
212
233
|
// createGoal 内部 active 守卫兜底(理论上上面守卫已挡;防御性)
|
|
213
234
|
throw new Error("Goal already active. Cannot create a new one.");
|
|
214
235
|
}
|
|
215
|
-
updateWidget(session, ports.ui);
|
|
216
236
|
|
|
217
237
|
const state = session.state!;
|
|
218
238
|
// slug fallback:未提供时用 goalId 截断作标题(与 buildGoalGui 一致,避免 [undefined])
|
|
219
239
|
const slug = state.slug ?? state.goalId.slice(0, SHORT_ID_LENGTH);
|
|
220
240
|
const budgetNotice: string[] = [];
|
|
221
241
|
if (budget.tokenBudget) budgetNotice.push(`Token budget: ${budget.tokenBudget}`);
|
|
222
|
-
|
|
242
|
+
runUiChannelSafe("create", state.goalId, () => {
|
|
243
|
+
updateWidget(session, ports.ui);
|
|
244
|
+
ports.ui.notify([`Goal created [${slug}]: ${objective}`, ...budgetNotice].join("\n"), "info");
|
|
245
|
+
});
|
|
223
246
|
|
|
224
247
|
return { action: "create", goalId: state.goalId, status: state.status, slug };
|
|
225
248
|
}
|
|
@@ -251,8 +274,10 @@ export function handleComplete(
|
|
|
251
274
|
|
|
252
275
|
// FR-3.3: 唯一终态序列入口(内部:tickState → finalizeGoal(transition+history) → persist)
|
|
253
276
|
finalizeAndPersist(state, "complete", ports);
|
|
254
|
-
|
|
255
|
-
|
|
277
|
+
runUiChannelSafe("complete", state.goalId, () => {
|
|
278
|
+
updateWidget(session, ports.ui);
|
|
279
|
+
ports.ui.notify(`Goal completed: ${state.objective}`, "info");
|
|
280
|
+
});
|
|
256
281
|
|
|
257
282
|
return { action: "complete", goalId: state.goalId, status: state.status };
|
|
258
283
|
}
|
|
@@ -289,8 +314,10 @@ export function handleReportBlocked(
|
|
|
289
314
|
state.status = transitionStatus(state.status, "blocked");
|
|
290
315
|
|
|
291
316
|
persistState(session, ports);
|
|
292
|
-
|
|
293
|
-
|
|
317
|
+
runUiChannelSafe("report_blocked", state.goalId, () => {
|
|
318
|
+
updateWidget(session, ports.ui);
|
|
319
|
+
ports.ui.notify(`Goal blocked: ${reason}`, "warning");
|
|
320
|
+
});
|
|
294
321
|
|
|
295
322
|
return { action: "report_blocked", goalId: state.goalId, status: state.status };
|
|
296
323
|
}
|
|
@@ -376,12 +403,23 @@ export function registerGoalControlTool(pi: ExtensionAPI, session: GoalSession):
|
|
|
376
403
|
: `Goal reported blocked.\nGoal ID: ${details.goalId}\nReason: ${params.reason?.trim() ?? ""}`;
|
|
377
404
|
|
|
378
405
|
// 状态展示不再进 tool result(GUI 渲染字段已移除):GUI 由 handle* 内的 updateWidget
|
|
379
|
-
// 经
|
|
406
|
+
// 经 setWidgetDual 推送(GUI 臂 = guiSetWidget/marker 通道,低层原语不单独调用;
|
|
407
|
+
// 渲染终点 = composer 任务托盘的协议 widget 区)。
|
|
380
408
|
return { content: [{ type: "text", text }], details };
|
|
381
409
|
},
|
|
382
410
|
|
|
383
411
|
renderCall(args: Record<string, unknown>, theme: Theme): Text {
|
|
384
|
-
|
|
412
|
+
// TUI 渲染先于 schema 校验(args 是未经 schema 收窄的原始形态):非对象整体
|
|
413
|
+
// 走安全占位(不抛错);action/slug 逐个 typeof 守卫替代裸断言——非法 action
|
|
414
|
+
// 落 report_blocked 回落分支(与原行为一致)
|
|
415
|
+
if (typeof args !== "object" || args === null) {
|
|
416
|
+
return new Text(
|
|
417
|
+
theme.fg("toolTitle", theme.bold("goal_control ")) + theme.fg("muted", "(invalid args)"),
|
|
418
|
+
0,
|
|
419
|
+
0,
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
const action = typeof args.action === "string" ? args.action : "";
|
|
385
423
|
const slug = typeof args.slug === "string" ? args.slug : "";
|
|
386
424
|
const actionLabel =
|
|
387
425
|
action === "create"
|
package/src/adapters/ports.ts
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import type { ExtensionAPI, ExtensionContext, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
16
|
-
import { setWidgetDual } from "@
|
|
17
|
-
import type { GuiContext } from "@
|
|
16
|
+
import { setWidgetDual } from "@zhushanwen/extension-protocol";
|
|
17
|
+
import type { GuiContext } from "@zhushanwen/extension-protocol";
|
|
18
18
|
|
|
19
19
|
import { ENTRY_TYPE, HISTORY_ENTRY_TYPE } from "../persistence";
|
|
20
20
|
import type { MessagingPort, PersistencePort, SessionPort, UiPort } from "../ports";
|
|
@@ -91,7 +91,7 @@ describe("createGoalState", () => {
|
|
|
91
91
|
|
|
92
92
|
describe("E1 守卫:src 内禁止 .status 裸赋值(必须走 transitionStatus 查表)", () => {
|
|
93
93
|
it("扫描 src/**/*.ts(排除 __tests__):0 命中", () => {
|
|
94
|
-
//
|
|
94
|
+
// 正则逐字来自设计定稿,不得自行修改:
|
|
95
95
|
// (?![=>]) 排除 ==/===/=>(比较与箭头函数形态);(?!\s*transitionStatus\b) 放行白名单查表赋值。
|
|
96
96
|
// 扫描粒度 = 整文件内容(非逐行):\s* 必须能跨行吃换行,才能排除
|
|
97
97
|
// `state.status =\n transitionStatus(...)` 这类跨行白名单赋值形态,逐行实现会误报。
|
package/src/index.ts
CHANGED
|
@@ -125,7 +125,7 @@ export default function goalExtension(pi: ExtensionAPI) {
|
|
|
125
125
|
* 通道形态(C-ext-06 globalThis slot 惯例):裸函数直挂——plan 侧使用点以 typeof 守卫
|
|
126
126
|
* 读取 `globalThis[Symbol.for("@zhushanwen/pi-goal.goalInit")]`(plan/src/compact.ts)。
|
|
127
127
|
* 不挂 pi API 对象:pi 0.84.4 为每个扩展创建独立 ExtensionAPI,挂 pi 对象的字段
|
|
128
|
-
*
|
|
128
|
+
* 跨扩展不可见。
|
|
129
129
|
*
|
|
130
130
|
* @param objective 目标描述
|
|
131
131
|
* @param budget 预算配置,传 undefined 用默认值
|
package/src/ports.ts
CHANGED
|
@@ -284,12 +284,17 @@ describe("updateWidget dual payload(gui 臂)", () => {
|
|
|
284
284
|
updateWidget(session, ui);
|
|
285
285
|
const widgetCall = calls.find((c) => c.method === "setWidget" && c.args[0] === "goal");
|
|
286
286
|
const payload = widgetCall!.args[1] as {
|
|
287
|
-
gui: {
|
|
287
|
+
gui: {
|
|
288
|
+
component: { type: string };
|
|
289
|
+
meta: { title: string; progress?: { total: number }; icon?: string | { paths: string[] } };
|
|
290
|
+
};
|
|
288
291
|
text: string[];
|
|
289
292
|
};
|
|
290
293
|
// v1.1:gui 臂整个 GuiRenderResult(group 组合根 + meta 宿主元数据,head 由壳层渲染)
|
|
291
294
|
expect(payload.gui.component.type).toBe("group");
|
|
292
295
|
expect(payload.gui.meta.progress).toMatchObject({ total: 10000 });
|
|
296
|
+
// 托盘 icon 随 dual payload 的 gui 臂到达宿主(协议形状 = string key)
|
|
297
|
+
expect(payload.gui.meta.icon).toBe("target");
|
|
293
298
|
// text 臂同步构造(TUI 渲染行)
|
|
294
299
|
expect(payload.text.length).toBeGreaterThan(0);
|
|
295
300
|
});
|
|
@@ -301,9 +306,11 @@ describe("updateWidget dual payload(gui 臂)", () => {
|
|
|
301
306
|
updateWidget(session, ui);
|
|
302
307
|
const widgetCall = calls.find((c) => c.method === "setWidget" && c.args[0] === "goal");
|
|
303
308
|
const payload = widgetCall!.args[1] as {
|
|
304
|
-
gui: { component: { type: string }; meta: { title: string; progress?: unknown } };
|
|
309
|
+
gui: { component: { type: string }; meta: { title: string; progress?: unknown; icon?: unknown } };
|
|
305
310
|
};
|
|
306
311
|
expect(payload.gui.component.type).toBe("group");
|
|
307
312
|
expect(payload.gui.meta.progress).toBeUndefined();
|
|
313
|
+
// icon 与预算无关,两种形态都推(托盘不依赖宿主 widgetKey 映射兜底)
|
|
314
|
+
expect(payload.gui.meta.icon).toBe("target");
|
|
308
315
|
});
|
|
309
316
|
});
|
package/src/projection/gui.ts
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
*
|
|
7
7
|
* 与 projection/widget.ts 的分工:
|
|
8
8
|
* - widget.ts:TUI 模式渲染(ANSI 字符串,经 ctx.ui.setWidget)
|
|
9
|
-
* - gui.ts:RPC 模式渲染(结构化 GuiComponent 描述符,经
|
|
9
|
+
* - gui.ts:RPC 模式渲染(结构化 GuiComponent 描述符,经 setWidgetDual 推送——GUI 臂 =
|
|
10
|
+
* guiSetWidget/marker 通道,低层原语不单独调用;渲染终点 = composer 任务托盘的协议 widget 区)
|
|
10
11
|
*
|
|
11
12
|
* 预算阈值经 engine/budget.ts 的 getBudgetSeverity 单源化(H4):
|
|
12
13
|
* buildGoalGui(percent→severity)与 widget.getBudgetColor(percent→color)共用阈值。
|
|
@@ -19,7 +20,7 @@ import {
|
|
|
19
20
|
guiResult,
|
|
20
21
|
type TreeItem,
|
|
21
22
|
type WidgetMeta,
|
|
22
|
-
} from "@
|
|
23
|
+
} from "@zhushanwen/extension-protocol";
|
|
23
24
|
|
|
24
25
|
import { PERCENT_FACTOR, SHORT_ID_LENGTH } from "../constants";
|
|
25
26
|
import { getBudgetSeverity } from "../engine/budget";
|
|
@@ -70,7 +71,7 @@ function metaStatus(status: GoalStatus): WidgetMeta["status"] {
|
|
|
70
71
|
/**
|
|
71
72
|
* 构造 goal 的 GUI 渲染描述符(v1.1 meta head 架构)。
|
|
72
73
|
*
|
|
73
|
-
* - meta
|
|
74
|
+
* - meta(托盘 icon / 标题=slug / 状态点 / token 进度)由宿主壳层渲染成唯一 head;
|
|
74
75
|
* 有预算时进度计数用百分比 + 预算阈值 severity(70/90 warn/danger 单源)。
|
|
75
76
|
* - 内容根 = group(透明组合容器):stats-line(status/turn,无预算时补 token
|
|
76
77
|
* 绝对值)+ list-tree(criteria 逐行,无 icon——所有行同 icon 是无信息量装饰,
|
|
@@ -114,6 +115,11 @@ export function buildGoalGui(state: GoalRuntimeState): GuiRenderResult {
|
|
|
114
115
|
}
|
|
115
116
|
|
|
116
117
|
const meta: WidgetMeta = {
|
|
118
|
+
// 点名 lucide key "target"(与宿主内置 widgetKey 映射 "goal"→Target 同图标):
|
|
119
|
+
// 宿主映射只是兜底,extension 显式声明后,宿主调整自己的映射表也不会换掉 goal 的图标。
|
|
120
|
+
// 不推 badge:托盘 badge 缺省由 progress.label 派生("42%"),推一份同值字符串会把
|
|
121
|
+
// 百分比格式变成两个真相源。
|
|
122
|
+
icon: "target",
|
|
117
123
|
title: slug,
|
|
118
124
|
status: metaStatus(state.status),
|
|
119
125
|
...(hasBudget
|