@zhushanwen/pi-plan 0.2.3 → 0.3.1
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__/compact-handler.test.ts +4 -0
- package/src/command.ts +11 -0
- package/src/compact.ts +39 -12
- package/src/index.ts +2 -1
- package/src/state.ts +17 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-plan",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Lightweight plan mode for Pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -16,8 +16,9 @@
|
|
|
16
16
|
"license": "MIT",
|
|
17
17
|
"peerDependencies": {
|
|
18
18
|
"@earendil-works/pi-coding-agent": ">=0.73.0",
|
|
19
|
-
"
|
|
20
|
-
"@earendil-works/pi-ai": "*"
|
|
19
|
+
"typebox": "*",
|
|
20
|
+
"@earendil-works/pi-ai": "*",
|
|
21
|
+
"@zhushanwen/pi-goal": "0.7.0"
|
|
21
22
|
},
|
|
22
23
|
"peerDependenciesMeta": {
|
|
23
24
|
"@earendil-works/pi-ai": {
|
|
@@ -78,6 +78,8 @@ describe("handlePlanComplete", () => {
|
|
|
78
78
|
expect.any(String),
|
|
79
79
|
undefined,
|
|
80
80
|
ctx,
|
|
81
|
+
"plan",
|
|
82
|
+
"All 2 steps of plan.md executed and verified: Step one; Step two",
|
|
81
83
|
);
|
|
82
84
|
});
|
|
83
85
|
|
|
@@ -108,6 +110,8 @@ describe("handlePlanComplete", () => {
|
|
|
108
110
|
expect.any(String),
|
|
109
111
|
undefined,
|
|
110
112
|
ctx,
|
|
113
|
+
"plan",
|
|
114
|
+
"All 2 steps of plan.md executed and verified: Step one; Step two",
|
|
111
115
|
);
|
|
112
116
|
});
|
|
113
117
|
});
|
package/src/command.ts
CHANGED
|
@@ -18,6 +18,17 @@ export function registerPlanCommand(
|
|
|
18
18
|
"Enter plan mode: /plan [description]. " +
|
|
19
19
|
"Subcommands: /plan abort, /plan status. " +
|
|
20
20
|
"With no args, show status or detect existing plan.",
|
|
21
|
+
getArgumentCompletions(prefix: string) {
|
|
22
|
+
const parts = prefix.trimStart().split(/\s+/).filter(Boolean);
|
|
23
|
+
// More than 1 token means the user is typing a free-text requirement, no completion.
|
|
24
|
+
if (parts.length > 1) return null;
|
|
25
|
+
const trimmed = (parts[0] ?? "").toLowerCase();
|
|
26
|
+
const opts = [
|
|
27
|
+
{ label: "abort", value: "abort", description: "取消活跃的 plan mode" },
|
|
28
|
+
{ label: "status", value: "status", description: "查看 plan mode 状态" },
|
|
29
|
+
];
|
|
30
|
+
return trimmed === "" ? opts : opts.filter((o) => o.label.startsWith(trimmed));
|
|
31
|
+
},
|
|
21
32
|
handler: async (args: string, ctx: ExtensionContext) => {
|
|
22
33
|
const trimmed = args.trim();
|
|
23
34
|
const sessionId = ctx.sessionManager.getSessionId();
|
package/src/compact.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
+
import { basename } from "node:path";
|
|
2
3
|
|
|
3
4
|
import type { ExtensionAPI, ExtensionContext, SessionBeforeCompactEvent, SessionBeforeTreeEvent } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import type { GoalInitFn } from "@zhushanwen/pi-goal";
|
|
4
6
|
|
|
5
7
|
import type { PlanSessionMap, PlanState } from "./state.js";
|
|
6
8
|
import { getPlanState } from "./state.js";
|
|
@@ -67,26 +69,45 @@ function readPlanFileSafe(planFilePath: string): string {
|
|
|
67
69
|
/** Detect whether goal extension is available via its programming interface */
|
|
68
70
|
export function detectGoalCapability(pi: ExtensionAPI): boolean {
|
|
69
71
|
try {
|
|
70
|
-
|
|
72
|
+
// 交叉类型单步断言(ExtensionAPI 可赋给 ExtensionAPI & { __goalInit? })
|
|
73
|
+
const api = pi as ExtensionAPI & { __goalInit?: GoalInitFn };
|
|
71
74
|
return typeof api.__goalInit === "function";
|
|
72
75
|
} catch {
|
|
73
76
|
return false;
|
|
74
77
|
}
|
|
75
78
|
}
|
|
76
79
|
|
|
80
|
+
/**
|
|
81
|
+
* 从 plan 文件路径推导 goal slug(kebab-case;无有效字符时 fallback)。
|
|
82
|
+
* 仅 widget 标题 + history 展示用,不注入 prompt。
|
|
83
|
+
*/
|
|
84
|
+
function buildPlanSlug(planFilePath: string): string {
|
|
85
|
+
const stem = basename(planFilePath)
|
|
86
|
+
.replace(/\.md$/i, "")
|
|
87
|
+
.toLowerCase()
|
|
88
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
89
|
+
.replace(/^-+|-+$/g, "");
|
|
90
|
+
return stem || "plan-execution";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** successCriteria 步骤预览条数上限 */
|
|
94
|
+
const STEP_PREVIEW_LIMIT = 3;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 从 plan 步骤构造可检查的 successCriteria(plan 完成 = 所有步骤执行并验证)。
|
|
98
|
+
* goal 的 complete 判定会对照本字段做证据审计。
|
|
99
|
+
*/
|
|
100
|
+
function buildPlanSuccessCriteria(planFilePath: string, tasks: string[]): string {
|
|
101
|
+
const preview = tasks.slice(0, STEP_PREVIEW_LIMIT).join("; ");
|
|
102
|
+
const ellipsis = tasks.length > STEP_PREVIEW_LIMIT ? "; …" : "";
|
|
103
|
+
return `All ${tasks.length} steps of ${basename(planFilePath)} executed and verified: ${preview}${ellipsis}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
77
106
|
/** Try to initialize goal via programming interface */
|
|
78
107
|
function tryGoalInit(pi: ExtensionAPI, planFilePath: string, ctx: ExtensionContext): boolean {
|
|
79
|
-
// Inline alias mirrors @zhushanwen/pi-goal's GoalInitFn (exported from goal/src/index.ts).
|
|
80
|
-
// Kept inline due to optional duck-typed coupling (pi.__goalInit) — update both if signature changes.
|
|
81
|
-
type GoalInitFn = (
|
|
82
|
-
objective: string,
|
|
83
|
-
budget: { tokenBudget?: number; timeBudgetMinutes?: number } | undefined,
|
|
84
|
-
ctx: ExtensionContext,
|
|
85
|
-
) => boolean;
|
|
86
|
-
|
|
87
108
|
try {
|
|
88
|
-
const api = pi as
|
|
89
|
-
const goalInit = api.__goalInit
|
|
109
|
+
const api = pi as ExtensionAPI & { __goalInit?: GoalInitFn };
|
|
110
|
+
const goalInit = api.__goalInit;
|
|
90
111
|
if (typeof goalInit !== "function") return false;
|
|
91
112
|
|
|
92
113
|
const planContent = readPlanFileSafe(planFilePath);
|
|
@@ -96,7 +117,13 @@ function tryGoalInit(pi: ExtensionAPI, planFilePath: string, ctx: ExtensionConte
|
|
|
96
117
|
const tasks = extractPlanSteps(planContent);
|
|
97
118
|
if (tasks.length === 0) return false;
|
|
98
119
|
|
|
99
|
-
return goalInit(
|
|
120
|
+
return goalInit(
|
|
121
|
+
objective,
|
|
122
|
+
undefined,
|
|
123
|
+
ctx,
|
|
124
|
+
buildPlanSlug(planFilePath),
|
|
125
|
+
buildPlanSuccessCriteria(planFilePath, tasks),
|
|
126
|
+
);
|
|
100
127
|
} catch {
|
|
101
128
|
return false;
|
|
102
129
|
}
|
package/src/index.ts
CHANGED
|
@@ -14,7 +14,8 @@ export default function planExtension(pi: ExtensionAPI) {
|
|
|
14
14
|
registerPlanCommand(pi, sessions);
|
|
15
15
|
|
|
16
16
|
// External API: __planStart(allow other extensions to start plan mode programmatically, #9)
|
|
17
|
-
|
|
17
|
+
// 交叉类型单步断言(ExtensionAPI 可赋给 ExtensionAPI & { __planStart? },无需 unknown 中转)
|
|
18
|
+
const api = pi as ExtensionAPI & { __planStart?: (requirement: string, ctx: ExtensionContext) => boolean };
|
|
18
19
|
api.__planStart = (requirement: string, ctx: ExtensionContext): boolean => {
|
|
19
20
|
return startPlanMode(pi, sessions, ctx, requirement);
|
|
20
21
|
};
|
package/src/state.ts
CHANGED
|
@@ -67,8 +67,13 @@ export function resetPlanState(
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
function isPlanStateEntry(entry: SessionEntry): entry is CustomEntry<Partial<PlanState>> & { customType: "plan-state" } {
|
|
70
|
-
|
|
71
|
-
return
|
|
70
|
+
// 判别式收窄(type === "custom")后可直接访问 customType/data,无需 cast
|
|
71
|
+
return (
|
|
72
|
+
entry.type === "custom" &&
|
|
73
|
+
entry.customType === "plan-state" &&
|
|
74
|
+
typeof entry.data === "object" &&
|
|
75
|
+
entry.data !== null
|
|
76
|
+
);
|
|
72
77
|
}
|
|
73
78
|
|
|
74
79
|
export function reconstructPlanState(ctx: ExtensionContext): PlanState {
|
|
@@ -76,15 +81,16 @@ export function reconstructPlanState(ctx: ExtensionContext): PlanState {
|
|
|
76
81
|
const entries = ctx.sessionManager.getEntries();
|
|
77
82
|
|
|
78
83
|
for (let i = entries.length - 1; i >= 0; i--) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
// entries[i] 是复杂表达式(TS 不收窄),守卫移到 const 变量上
|
|
85
|
+
const entry = entries[i];
|
|
86
|
+
if (!isPlanStateEntry(entry)) continue;
|
|
87
|
+
const data = entry.data;
|
|
88
|
+
state.isActive = data?.isActive ?? false;
|
|
89
|
+
state.phase = data?.phase ?? "idle";
|
|
90
|
+
state.planFilePath = data?.planFilePath ?? "";
|
|
91
|
+
state.requirement = data?.requirement ?? "";
|
|
92
|
+
state.templateName = data?.templateName ?? "";
|
|
93
|
+
break;
|
|
88
94
|
}
|
|
89
95
|
|
|
90
96
|
return state;
|