@zhushanwen/pi-goal 0.14.15 → 0.15.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-goal",
3
- "version": "0.14.15",
3
+ "version": "0.15.0",
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,7 +29,7 @@
29
29
  "@earendil-works/pi-coding-agent": "^1.0.0",
30
30
  "@earendil-works/pi-tui": "^1.0.0",
31
31
  "typebox": "1.3.27",
32
- "@zhushanwen/pi-pending-notifications": "0.8.6"
32
+ "@zhushanwen/pi-pending-notifications": "0.9.0"
33
33
  },
34
34
  "dependencies": {
35
35
  "@zhushanwen/extension-protocol": "0.21.0",
@@ -0,0 +1,200 @@
1
+ /**
2
+ * goal_control 收起态摘要测试:buildGoalControlSummary 三分支文案 + execute 补齐
3
+ * details.summary + renderResult 折叠/展开/旧形态回退。
4
+ *
5
+ * mock 形态对齐 goal-control-rpc.test.ts(捕获 registerTool 后直接调 execute/renderResult)。
6
+ */
7
+ import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
8
+ import { describe, expect, it, vi } from "vitest";
9
+
10
+ import {
11
+ buildGoalControlSummary,
12
+ registerGoalControlTool,
13
+ type GoalControlDetails,
14
+ } from "../adapters/goal-control-adapter";
15
+ import { createGoalSession } from "../session";
16
+
17
+ // ── mock:logger + updateWidget(与 rpc 测试同款 no-op)──
18
+ vi.mock("@zhushanwen/pi-extension-logger", () => ({
19
+ getLogger: () => ({ warn: vi.fn(), debug: vi.fn(), error: vi.fn() }),
20
+ }));
21
+ vi.mock("../projection/widget", () => ({ updateWidget: vi.fn() }));
22
+
23
+ // ── buildGoalControlSummary ──────────────────────────
24
+
25
+ describe("buildGoalControlSummary", () => {
26
+ it("create → Goal created: <slug>", () => {
27
+ const details = { action: "create", goalId: "g1", status: "active", slug: "refactor-auth" } as GoalControlDetails;
28
+ expect(buildGoalControlSummary({ action: "create" } as never, details)).toBe(
29
+ "Goal created: refactor-auth",
30
+ );
31
+ });
32
+
33
+ it("create 无 slug → 省略后缀", () => {
34
+ const details = { action: "create", goalId: "g1", status: "active" } as GoalControlDetails;
35
+ expect(buildGoalControlSummary({ action: "create" } as never, details)).toBe("Goal created");
36
+ });
37
+
38
+ it("complete → Goal completed: <goalId>(complete 的 details 无 slug)", () => {
39
+ const details = { action: "complete", goalId: "abc123", status: "complete" } as GoalControlDetails;
40
+ expect(buildGoalControlSummary({ action: "complete" } as never, details)).toBe(
41
+ "Goal completed: abc123",
42
+ );
43
+ });
44
+
45
+ it("report_blocked → Blocked: <reason 首 40 字>(超长加省略号)", () => {
46
+ const details = { action: "report_blocked", goalId: "g1", status: "blocked" } as GoalControlDetails;
47
+ const params = { action: "report_blocked", reason: "等待上游 API 修复" } as never;
48
+ expect(buildGoalControlSummary(params, details)).toBe("Blocked: 等待上游 API 修复");
49
+
50
+ const long = "x".repeat(50);
51
+ expect(buildGoalControlSummary({ action: "report_blocked", reason: long } as never, details)).toBe(
52
+ `Blocked: ${"x".repeat(40)}…`,
53
+ );
54
+ });
55
+
56
+ it("report_blocked 无 reason → 省略后缀", () => {
57
+ const details = { action: "report_blocked", goalId: "g1", status: "blocked" } as GoalControlDetails;
58
+ expect(buildGoalControlSummary({ action: "report_blocked" } as never, details)).toBe("Blocked");
59
+ });
60
+ });
61
+
62
+ // ── execute 补齐 details.summary + renderResult ──────
63
+
64
+ interface CapturedTool {
65
+ name: string;
66
+ execute: (
67
+ toolCallId: string,
68
+ params: Record<string, unknown>,
69
+ signal: AbortSignal | undefined,
70
+ onUpdate: unknown,
71
+ ctx: ExtensionContext,
72
+ ) => Promise<{ content: Array<{ type: "text"; text: string }>; details: GoalControlDetails }>;
73
+ renderResult: (result: unknown, options: { expanded: boolean }, theme: Theme) => { content: unknown };
74
+ }
75
+
76
+ function captureTool(): CapturedTool {
77
+ let captured: CapturedTool | undefined;
78
+ const pi = {
79
+ registerTool(tool: CapturedTool): void {
80
+ captured = tool;
81
+ },
82
+ on: () => {},
83
+ appendEntry: () => {},
84
+ notify: () => {},
85
+ } as unknown as ExtensionAPI;
86
+ registerGoalControlTool(pi, createGoalSession());
87
+ if (!captured) throw new Error("registerGoalControlTool did not register a tool");
88
+ return captured;
89
+ }
90
+
91
+ const ctx = {
92
+ mode: "rpc",
93
+ hasUI: true,
94
+ ui: {
95
+ theme: { fg: (_c: string, t: string) => t, bold: (t: string) => t },
96
+ setWidget: () => {},
97
+ setStatus: () => {},
98
+ notify: () => {},
99
+ },
100
+ sessionManager: { getEntries: () => [] },
101
+ } as unknown as ExtensionContext;
102
+
103
+ describe("goal_control execute details.summary", () => {
104
+ it("create → details 带现有字段 + summary", async () => {
105
+ const tool = captureTool();
106
+ const res = await tool.execute(
107
+ "id",
108
+ {
109
+ action: "create",
110
+ slug: "refactor-auth",
111
+ objective: "refactor the auth module",
112
+ successCriteria: ["tests pass"],
113
+ },
114
+ undefined,
115
+ undefined,
116
+ ctx,
117
+ );
118
+ expect(res.details).toMatchObject({
119
+ action: "create",
120
+ status: "active",
121
+ slug: "refactor-auth",
122
+ summary: "Goal created: refactor-auth",
123
+ });
124
+ });
125
+
126
+ it("report_blocked → details.summary 带 reason 首段", async () => {
127
+ const tool = captureTool();
128
+ const create = await tool.execute(
129
+ "id",
130
+ { action: "create", slug: "g", objective: "obj", successCriteria: ["c"] },
131
+ undefined,
132
+ undefined,
133
+ ctx,
134
+ );
135
+ void create;
136
+ const res = await tool.execute(
137
+ "id",
138
+ { action: "report_blocked", reason: "blocked by missing credentials" },
139
+ undefined,
140
+ undefined,
141
+ ctx,
142
+ );
143
+ expect(res.details).toMatchObject({
144
+ action: "report_blocked",
145
+ status: "blocked",
146
+ summary: "Blocked: blocked by missing credentials",
147
+ });
148
+ });
149
+ });
150
+
151
+ // ── renderResult 分态 ────────────────────────────────
152
+
153
+ const mockTheme = {
154
+ fg: (_color: string, text: string) => text,
155
+ bg: (_color: string, text: string) => text,
156
+ bold: (text: string) => text,
157
+ italic: (text: string) => text,
158
+ underline: (text: string) => text,
159
+ inverse: (text: string) => text,
160
+ strikethrough: (text: string) => text,
161
+ getFgAnsi: (_color: string) => "",
162
+ getBgAnsi: (_color: string) => "",
163
+ getColorMode: () => "truecolor" as const,
164
+ getThinkingBorderColor: () => (text: string) => text,
165
+ getBashModeBorderColor: () => (text: string) => text,
166
+ } as unknown as Theme;
167
+
168
+ describe("goal_control renderResult", () => {
169
+ const tool = captureTool();
170
+
171
+ it("折叠态 → details.summary 一行(dim)", () => {
172
+ const result = {
173
+ content: [{ type: "text", text: "Goal created.\nGoal ID: g1" }],
174
+ details: { action: "create", goalId: "g1", status: "active", slug: "refactor-auth", summary: "Goal created: refactor-auth" },
175
+ };
176
+ expect(tool.renderResult(result, { expanded: false }, mockTheme).content).toBe(
177
+ "Goal created: refactor-auth",
178
+ );
179
+ });
180
+
181
+ it("展开态 → content 纯文本(多行)", () => {
182
+ const result = {
183
+ content: [{ type: "text", text: "Goal created.\nGoal ID: g1" }],
184
+ details: { action: "create", goalId: "g1", status: "active", slug: "refactor-auth", summary: "Goal created: refactor-auth" },
185
+ };
186
+ expect(tool.renderResult(result, { expanded: true }, mockTheme).content).toBe(
187
+ "Goal created.\nGoal ID: g1",
188
+ );
189
+ });
190
+
191
+ it("旧 entry 无 summary → 折叠态回退 ◆ 状态行", () => {
192
+ const result = {
193
+ content: [{ type: "text", text: "Goal created." }],
194
+ details: { action: "complete", goalId: "g1", status: "complete" },
195
+ };
196
+ expect(tool.renderResult(result, { expanded: false }, mockTheme).content).toBe(
197
+ "◆ Goal Completed",
198
+ );
199
+ });
200
+ });
@@ -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 { firstContentText } from "@zhushanwen/extension-protocol";
36
37
  import { getLogger } from "@zhushanwen/pi-extension-logger";
37
38
  import { type Static, Type } from "typebox";
38
39
 
@@ -140,6 +141,35 @@ export interface GoalControlDetails {
140
141
  goalId: string;
141
142
  status: GoalStatus;
142
143
  slug?: string;
144
+ /** 收起态一行摘要(buildGoalControlSummary 产出;handler 返回后由 execute 统一补齐,
145
+ * 历史 entry 无此字段——GUI 收起态与 TUI 折叠态共用)。 */
146
+ summary?: string;
147
+ }
148
+
149
+ /** 收起态摘要中 reason 的保留长度(超长截断加省略号)。 */
150
+ const BLOCKED_REASON_SUMMARY_CHARS = 40;
151
+
152
+ /** 收起态一行摘要:create → slug;complete → goalId(complete 的 details 无 slug);
153
+ * report_blocked → reason 首段(超长加省略号)。 */
154
+ export function buildGoalControlSummary(
155
+ params: GoalControlParamsT,
156
+ details: GoalControlDetails,
157
+ ): string {
158
+ switch (details.action) {
159
+ case "create":
160
+ return details.slug ? `Goal created: ${details.slug}` : "Goal created";
161
+ case "complete":
162
+ return `Goal completed: ${details.goalId}`;
163
+ case "report_blocked": {
164
+ const reason = params.reason?.trim() ?? "";
165
+ if (reason === "") return "Blocked";
166
+ return reason.length > BLOCKED_REASON_SUMMARY_CHARS
167
+ ? `Blocked: ${reason.slice(0, BLOCKED_REASON_SUMMARY_CHARS)}…`
168
+ : `Blocked: ${reason}`;
169
+ }
170
+ default:
171
+ return "Done";
172
+ }
143
173
  }
144
174
 
145
175
  // ── 业务 handler(契约对齐 §3,可测:fake ports)──────
@@ -385,6 +415,8 @@ export function registerGoalControlTool(pi: ExtensionAPI, session: GoalSession):
385
415
  } else {
386
416
  details = handleReportBlocked(params, session, ports);
387
417
  }
418
+ // 收起态一行摘要:details 追加 summary(现有字段保留),GUI 收起态与 TUI 折叠态共用
419
+ details = { ...details, summary: buildGoalControlSummary(params, details) };
388
420
 
389
421
  // 扁平化后 params.objective/reason 为 string|undefined(schema 不再按 action 收窄);
390
422
  // handler 已对必填字段做存在性校验,此处用 ?. 兜底,未定义时空串
@@ -423,20 +455,47 @@ export function registerGoalControlTool(pi: ExtensionAPI, session: GoalSession):
423
455
  return new Text(theme.fg("toolTitle", theme.bold("goal_control ")) + actionLabel, 0, 0);
424
456
  },
425
457
 
426
- renderResult(result: unknown, _options: { expanded: boolean }, theme: Theme): Text {
458
+ renderResult(result: unknown, options: { expanded: boolean }, theme: Theme): Text {
427
459
  const d = hasGoalDetails(result) ? result.details : undefined;
428
- if (!d) return new Text(theme.fg("dim", "goal_control"), 0, 0);
429
- const statusColor =
430
- d.status === "active"
431
- ? "accent"
432
- : d.status === "complete"
433
- ? "success"
434
- : d.status === "blocked"
435
- ? "error"
436
- : "muted";
437
- const label = d.action === "create" ? "Created" : d.action === "complete" ? "Completed" : "Blocked";
438
- const slugSuffix = d.slug ? theme.fg("dim", ` ${d.slug}`) : "";
439
- return new Text(theme.fg(statusColor, `◆ Goal ${label}`) + slugSuffix, 0, 0);
460
+ // 展开态 = content 纯文本(多行详情);content 缺失时回退折叠形态
461
+ if (options.expanded) {
462
+ const content = getContentText(result);
463
+ if (content !== "") return new Text(content, 0, 0);
464
+ }
465
+ // 折叠态 = details.summary 一行(dim);旧 entry 无 summary 时保留原 ◆ 状态行
466
+ if (d?.summary !== undefined) {
467
+ return new Text(theme.fg("dim", d.summary), 0, 0);
468
+ }
469
+ return renderLegacyGoalResult(d, theme);
440
470
  },
441
471
  });
442
472
  }
473
+
474
+ /** unknown 值按字段名安全取值(index-signature 收窄,替代全可选结构断言)。 */
475
+ function fieldValue(v: unknown, key: string): unknown {
476
+ if (typeof v !== "object" || v === null) return undefined;
477
+ return (v as Record<string, unknown>)[key];
478
+ }
479
+
480
+ /** content[0] 的 text 纯文本(content 缺失或非 text 类型时兜底空串)。 */
481
+ function getContentText(result: unknown): string {
482
+ const content = fieldValue(result, "content");
483
+ if (!Array.isArray(content)) return "";
484
+ return firstContentText({ content });
485
+ }
486
+
487
+ /** 旧形态回退(details 无 summary 的历史 entry):原 ◆ 状态行,视觉零变化。 */
488
+ function renderLegacyGoalResult(d: GoalControlDetails | undefined, theme: Theme): Text {
489
+ if (!d) return new Text(theme.fg("dim", "goal_control"), 0, 0);
490
+ const statusColor =
491
+ d.status === "active"
492
+ ? "accent"
493
+ : d.status === "complete"
494
+ ? "success"
495
+ : d.status === "blocked"
496
+ ? "error"
497
+ : "muted";
498
+ const label = d.action === "create" ? "Created" : d.action === "complete" ? "Completed" : "Blocked";
499
+ const slugSuffix = d.slug ? theme.fg("dim", ` ${d.slug}`) : "";
500
+ return new Text(theme.fg(statusColor, `◆ Goal ${label}`) + slugSuffix, 0, 0);
501
+ }