@zhushanwen/pi-plan 0.2.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/index.ts +1 -0
- package/package.json +39 -0
- package/src/__tests__/command.test.ts +166 -0
- package/src/__tests__/compact-handler.test.ts +179 -0
- package/src/__tests__/compact.test.ts +102 -0
- package/src/__tests__/state.test.ts +117 -0
- package/src/__tests__/templates.test.ts +34 -0
- package/src/__tests__/tool.test.ts +181 -0
- package/src/command.ts +177 -0
- package/src/compact.ts +196 -0
- package/src/index.ts +40 -0
- package/src/state.ts +91 -0
- package/src/templates.ts +61 -0
- package/src/tool.ts +353 -0
- package/src/widget.ts +15 -0
- package/templates/bugfix-plan.md +22 -0
- package/templates/feature-plan.md +25 -0
- package/templates/implementation-plan.md +19 -0
- package/templates/refactor-plan.md +22 -0
- package/templates/research-plan.md +22 -0
package/src/templates.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
|
|
8
|
+
export interface TemplateInfo {
|
|
9
|
+
name: string;
|
|
10
|
+
source: "builtin" | "global" | "project";
|
|
11
|
+
path: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getBuiltinTemplateDir(): string {
|
|
15
|
+
return path.resolve(__dirname, "..", "templates");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function scanTemplateDir(dir: string, source: TemplateInfo["source"], seen: Set<string>): TemplateInfo[] {
|
|
19
|
+
const results: TemplateInfo[] = [];
|
|
20
|
+
if (!fs.existsSync(dir)) return results;
|
|
21
|
+
for (const file of fs.readdirSync(dir)) {
|
|
22
|
+
if (file.endsWith(".md")) {
|
|
23
|
+
const name = file.replace(/\.md$/, "");
|
|
24
|
+
if (!seen.has(name)) {
|
|
25
|
+
results.push({ name, source, path: path.join(dir, file) });
|
|
26
|
+
seen.add(name);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return results;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function listTemplates(projectDir?: string): TemplateInfo[] {
|
|
34
|
+
const seen = new Set<string>();
|
|
35
|
+
const templates: TemplateInfo[] = [];
|
|
36
|
+
|
|
37
|
+
// 1. Project-level templates (highest priority)
|
|
38
|
+
if (projectDir) {
|
|
39
|
+
templates.push(...scanTemplateDir(path.join(projectDir, ".pi", "plan-templates"), "project", seen));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 2. Global templates
|
|
43
|
+
templates.push(...scanTemplateDir(path.join(os.homedir(), ".pi", "agent", "plan-templates"), "global", seen));
|
|
44
|
+
|
|
45
|
+
// 3. Builtin templates (lowest priority)
|
|
46
|
+
templates.push(...scanTemplateDir(getBuiltinTemplateDir(), "builtin", seen));
|
|
47
|
+
|
|
48
|
+
return templates;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function loadTemplate(name: string, projectDir?: string): string | null {
|
|
52
|
+
const templates = listTemplates(projectDir);
|
|
53
|
+
const template = templates.find((t) => t.name === name);
|
|
54
|
+
if (!template) return null;
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
return fs.readFileSync(template.path, "utf-8");
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
package/src/tool.ts
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { StringEnum } from "@mariozechner/pi-ai";
|
|
5
|
+
import type { ExtensionAPI, ExtensionContext, Theme, ThemeColor } from "@mariozechner/pi-coding-agent";
|
|
6
|
+
import { Text } from "@mariozechner/pi-tui";
|
|
7
|
+
import { Type } from "typebox";
|
|
8
|
+
|
|
9
|
+
import type { PlanSessionMap } from "./state.js";
|
|
10
|
+
import { getPlanState, persistPlanState, resetPlanState } from "./state.js";
|
|
11
|
+
import { listTemplates, loadTemplate } from "./templates.js";
|
|
12
|
+
import { updatePlanWidget } from "./widget.js";
|
|
13
|
+
|
|
14
|
+
// ── Action types ───────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
export const PLAN_ACTIONS = [
|
|
17
|
+
"list-template",
|
|
18
|
+
"select-template",
|
|
19
|
+
"create-template",
|
|
20
|
+
"complete",
|
|
21
|
+
"abort",
|
|
22
|
+
] as const;
|
|
23
|
+
|
|
24
|
+
export type PlanAction = (typeof PLAN_ACTIONS)[number];
|
|
25
|
+
|
|
26
|
+
export function validateAction(action: string): action is PlanAction {
|
|
27
|
+
return (PLAN_ACTIONS as readonly string[]).includes(action);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ── Details types ──────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
interface ListTemplateDetails {
|
|
33
|
+
action: "list-template";
|
|
34
|
+
templates: Array<{ name: string; source: string; path: string }>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface SelectTemplateDetails {
|
|
38
|
+
action: "select-template";
|
|
39
|
+
templateName: string;
|
|
40
|
+
content: string;
|
|
41
|
+
phase: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface CreateTemplateDetails {
|
|
45
|
+
action: "create-template";
|
|
46
|
+
templateName: string;
|
|
47
|
+
templateDir: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface CompleteDetails {
|
|
51
|
+
action: "complete";
|
|
52
|
+
planFilePath: string;
|
|
53
|
+
isolation: string;
|
|
54
|
+
execMode: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface CompleteCancelledDetails {
|
|
58
|
+
action: "complete-cancelled";
|
|
59
|
+
reason: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface AbortDetails {
|
|
63
|
+
action: "abort";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
type PlanDetails =
|
|
67
|
+
| ListTemplateDetails
|
|
68
|
+
| SelectTemplateDetails
|
|
69
|
+
| CreateTemplateDetails
|
|
70
|
+
| CompleteDetails
|
|
71
|
+
| CompleteCancelledDetails
|
|
72
|
+
| AbortDetails;
|
|
73
|
+
|
|
74
|
+
// ── Helpers ────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
/** Restore the default full tool set after exiting plan mode. */
|
|
77
|
+
function restoreFullToolSet(pi: ExtensionAPI): void {
|
|
78
|
+
const allToolNames = pi.getAllTools().map((t: { name: string }) => t.name);
|
|
79
|
+
pi.setActiveTools(allToolNames);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Compact template list for TUI display. Two-column, max 5 lines. */
|
|
83
|
+
function formatTemplateList(
|
|
84
|
+
templates: Array<{ name: string; source: string }>,
|
|
85
|
+
): string {
|
|
86
|
+
const names = templates.map((t) => `${t.name} (${t.source})`);
|
|
87
|
+
if (names.length === 0) return "No templates available.";
|
|
88
|
+
|
|
89
|
+
const MAX_DISPLAY = 8;
|
|
90
|
+
const HALF = 2;
|
|
91
|
+
const truncated = names.length > MAX_DISPLAY;
|
|
92
|
+
const display = names.slice(0, MAX_DISPLAY);
|
|
93
|
+
|
|
94
|
+
// Two-column layout
|
|
95
|
+
const half = Math.ceil(display.length / HALF);
|
|
96
|
+
const col1 = display.slice(0, half);
|
|
97
|
+
const col2 = display.slice(half);
|
|
98
|
+
const lines: string[] = [];
|
|
99
|
+
for (let i = 0; i < half; i++) {
|
|
100
|
+
const right = col2[i] ? ` ${half + i + 1} ${col2[i]}` : "";
|
|
101
|
+
lines.push(` ${i + 1} ${col1[i] ?? ""}` + right);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (truncated) lines.push(` ... ${names.length - MAX_DISPLAY} more`);
|
|
105
|
+
return lines.join("\n");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Relative path from project dir */
|
|
109
|
+
function relativePath(fullPath: string, projectDir: string): string {
|
|
110
|
+
if (fullPath.startsWith(projectDir)) {
|
|
111
|
+
return fullPath.slice(projectDir.length + 1);
|
|
112
|
+
}
|
|
113
|
+
return fullPath;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── renderResult ───────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
function renderPlanResult(
|
|
119
|
+
result: { content: Array<{ type: string; text?: string }>; details?: PlanDetails },
|
|
120
|
+
_options: unknown,
|
|
121
|
+
theme: Theme,
|
|
122
|
+
): Text {
|
|
123
|
+
const details = result.details;
|
|
124
|
+
if (!details) {
|
|
125
|
+
const text = result.content[0];
|
|
126
|
+
return new Text(text?.type === "text" ? (text.text ?? "") : "", 0, 0);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const fg = (token: ThemeColor, text: string) => theme.fg(token, text);
|
|
130
|
+
const NL = "\n";
|
|
131
|
+
|
|
132
|
+
switch (details.action) {
|
|
133
|
+
case "list-template": {
|
|
134
|
+
const header = fg("accent", `${details.templates.length} 个模板可用`) + NL;
|
|
135
|
+
const body = formatTemplateList(details.templates) + NL;
|
|
136
|
+
const hint = fg("dim", "→ plan(select-template, templateName='xxx')");
|
|
137
|
+
return new Text(header + body + hint, 0, 0);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
case "select-template": {
|
|
141
|
+
const header = fg("success", `✓ ${details.templateName}`) + NL;
|
|
142
|
+
const body = fg("dim", ` brainstorming → ${details.phase}`) + NL;
|
|
143
|
+
const hint = fg("dim", "→ 按模板章节顺序写 plan.md");
|
|
144
|
+
return new Text(header + body + hint, 0, 0);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
case "create-template": {
|
|
148
|
+
const header = fg("success", `✓ 已创建: ${details.templateName}`) + NL;
|
|
149
|
+
const body = fg("dim", ` ${details.templateDir}`);
|
|
150
|
+
return new Text(header + body, 0, 0);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
case "complete": {
|
|
154
|
+
const header = fg("success", `✓ Plan 已批准 → ${details.execMode}`) + NL;
|
|
155
|
+
const body = fg("dim", ` ${details.planFilePath}`) + NL;
|
|
156
|
+
const info = fg("dim", ` isolation: ${details.isolation} · 工具集已恢复`);
|
|
157
|
+
return new Text(header + body + info, 0, 0);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
case "complete-cancelled": {
|
|
161
|
+
const header = fg("warning", `✗ 用户选择: ${details.reason}`) + NL;
|
|
162
|
+
const body = fg("dim", " 继续在 plan mode 中");
|
|
163
|
+
return new Text(header + body, 0, 0);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
case "abort": {
|
|
167
|
+
const header = fg("error", "✗ Plan mode 已退出") + NL;
|
|
168
|
+
const body = fg("dim", " 工具集已恢复");
|
|
169
|
+
return new Text(header + body, 0, 0);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ── Register tool ──────────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
export function registerPlanTool(
|
|
177
|
+
pi: ExtensionAPI,
|
|
178
|
+
sessions: PlanSessionMap,
|
|
179
|
+
): void {
|
|
180
|
+
pi.registerTool({
|
|
181
|
+
name: "plan",
|
|
182
|
+
label: "Plan Mode",
|
|
183
|
+
description:
|
|
184
|
+
"Manages plan mode lifecycle (template selection, state transitions, completion). " +
|
|
185
|
+
"NOT for writing plan content — use the 'write' tool to write plan.md. " +
|
|
186
|
+
"Actions: list-template, select-template, create-template, complete, abort.",
|
|
187
|
+
parameters: Type.Object({
|
|
188
|
+
action: StringEnum(PLAN_ACTIONS, { description: "Action to perform" }),
|
|
189
|
+
templateName: Type.Optional(Type.String({ description: "Template name (for select-template)" })),
|
|
190
|
+
templateContent: Type.Optional(Type.String({ description: "Template content (for create-template)" })),
|
|
191
|
+
isolation: Type.Optional(
|
|
192
|
+
StringEnum(["compact", "tree", "direct"], {
|
|
193
|
+
description: "Isolation mode for plan execution (for complete action)",
|
|
194
|
+
}),
|
|
195
|
+
),
|
|
196
|
+
}),
|
|
197
|
+
promptSnippet:
|
|
198
|
+
"## When to use this tool vs 'write'\n" +
|
|
199
|
+
"Use 'plan' tool ONLY for plan mode state management:\n" +
|
|
200
|
+
"- list-template / select-template / create-template — template operations\n" +
|
|
201
|
+
"- complete — user approved plan, exit plan mode\n" +
|
|
202
|
+
"- abort — cancel plan mode\n" +
|
|
203
|
+
"\n" +
|
|
204
|
+
"Use 'write' tool for ALL plan content: writing plan.md, updating plan chapters.\n" +
|
|
205
|
+
"\n" +
|
|
206
|
+
"## End-to-end workflow example\n" +
|
|
207
|
+
"1. /plan 'add dark mode' — user enters plan mode\n" +
|
|
208
|
+
"2. AI explores codebase (read, grep, bash) — brainstorming\n" +
|
|
209
|
+
"3. plan(action='list-template') — show available templates\n" +
|
|
210
|
+
"4. User picks template → plan(action='select-template', templateName='feature-plan')\n" +
|
|
211
|
+
"5. write({path: planFilePath, content: '...filled template...'}) — write plan content\n" +
|
|
212
|
+
"6. User reviews → plan(action='complete', isolation='compact') — exit plan mode\n" +
|
|
213
|
+
"\n" +
|
|
214
|
+
"## Common mistakes\n" +
|
|
215
|
+
"❌ plan(action='complete') to 'write the plan' — WRONG, use write tool\n" +
|
|
216
|
+
"❌ Calling plan tool when user says 'write plan to file' — use write tool\n" +
|
|
217
|
+
"✅ plan(action='list-template') to discover templates\n" +
|
|
218
|
+
"✅ plan(action='complete') AFTER plan.md is written AND user approves",
|
|
219
|
+
renderResult(
|
|
220
|
+
result: { content: Array<{ type: string; text?: string }>; details?: PlanDetails },
|
|
221
|
+
options: unknown,
|
|
222
|
+
theme: Theme,
|
|
223
|
+
): Text {
|
|
224
|
+
return renderPlanResult(result, options, theme);
|
|
225
|
+
},
|
|
226
|
+
async execute(
|
|
227
|
+
_toolCallId: string,
|
|
228
|
+
params: Record<string, unknown>,
|
|
229
|
+
_signal: AbortSignal | undefined,
|
|
230
|
+
_onUpdate: unknown,
|
|
231
|
+
ctx: ExtensionContext,
|
|
232
|
+
): Promise<{ content: Array<{ type: "text"; text: string }>; details: PlanDetails }> {
|
|
233
|
+
const action = params.action as string;
|
|
234
|
+
if (!validateAction(action)) {
|
|
235
|
+
throw new Error(`Unknown plan action: ${action}. Valid actions: ${PLAN_ACTIONS.join(", ")}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
239
|
+
const state = getPlanState(sessions, sessionId, ctx);
|
|
240
|
+
const projectDir = ctx.cwd;
|
|
241
|
+
|
|
242
|
+
switch (action) {
|
|
243
|
+
case "list-template": {
|
|
244
|
+
const templates = listTemplates(projectDir);
|
|
245
|
+
return {
|
|
246
|
+
content: [{ type: "text" as const, text: `${templates.length} templates available` }],
|
|
247
|
+
details: { action: "list-template", templates },
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
case "select-template": {
|
|
252
|
+
const templateName = params.templateName as string;
|
|
253
|
+
if (!templateName) {
|
|
254
|
+
throw new Error("templateName is required for select-template");
|
|
255
|
+
}
|
|
256
|
+
const content = loadTemplate(templateName, projectDir);
|
|
257
|
+
if (!content) {
|
|
258
|
+
throw new Error(`Template not found: ${templateName}`);
|
|
259
|
+
}
|
|
260
|
+
state.templateName = templateName;
|
|
261
|
+
state.phase = "writing";
|
|
262
|
+
persistPlanState(pi, state);
|
|
263
|
+
return {
|
|
264
|
+
content: [{ type: "text" as const, text: `Template selected: ${templateName}` }],
|
|
265
|
+
details: { action: "select-template", templateName, content, phase: state.phase },
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
case "create-template": {
|
|
270
|
+
const templateName = params.templateName as string;
|
|
271
|
+
const templateContent = params.templateContent as string;
|
|
272
|
+
if (!templateName || !templateContent) {
|
|
273
|
+
throw new Error("templateName and templateContent are required for create-template");
|
|
274
|
+
}
|
|
275
|
+
const sanitizedName = templateName.replace(/[^a-zA-Z0-9_-]/g, "");
|
|
276
|
+
if (!sanitizedName) {
|
|
277
|
+
throw new Error("Invalid template name: must contain alphanumeric characters");
|
|
278
|
+
}
|
|
279
|
+
const templateDir = path.join(projectDir, ".pi", "plan-templates");
|
|
280
|
+
fs.mkdirSync(templateDir, { recursive: true });
|
|
281
|
+
const filePath = path.join(templateDir, `${sanitizedName}.md`);
|
|
282
|
+
fs.writeFileSync(filePath, templateContent);
|
|
283
|
+
return {
|
|
284
|
+
content: [{ type: "text" as const, text: `Template created: ${sanitizedName}` }],
|
|
285
|
+
details: {
|
|
286
|
+
action: "create-template",
|
|
287
|
+
templateName: sanitizedName,
|
|
288
|
+
templateDir: relativePath(filePath, projectDir),
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
case "complete": {
|
|
294
|
+
// Build execution options filtered by available capabilities
|
|
295
|
+
const execOptions = ["Subagent-driven execution"];
|
|
296
|
+
const hasGoal = (await import("./compact.js")).detectGoalCapability(pi);
|
|
297
|
+
if (hasGoal) execOptions.push("Goal-driven execution (/goal)");
|
|
298
|
+
execOptions.push("Single-agent (current session)");
|
|
299
|
+
execOptions.push("Modify the plan first", "Save for later");
|
|
300
|
+
|
|
301
|
+
let chosenMode = "single-agent";
|
|
302
|
+
if (typeof ctx.ui.select === "function") {
|
|
303
|
+
const choice = await ctx.ui.select("Plan is ready. Choose execution method:", execOptions);
|
|
304
|
+
if (!choice || choice === "Modify the plan first" || choice === "Save for later") {
|
|
305
|
+
return {
|
|
306
|
+
content: [
|
|
307
|
+
{ type: "text" as const, text: `User chose: ${choice ?? "cancelled"}. Staying in plan mode.` },
|
|
308
|
+
],
|
|
309
|
+
details: { action: "complete-cancelled", reason: choice ?? "cancelled" },
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
if (choice === "Subagent-driven execution") chosenMode = "subagent";
|
|
313
|
+
else if (choice === "Goal-driven execution (/goal)") chosenMode = "goal";
|
|
314
|
+
else chosenMode = "single-agent";
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Persist final phase before cleanup
|
|
318
|
+
const planFilePath = state.planFilePath;
|
|
319
|
+
const isolation = (params.isolation as string) ?? "direct";
|
|
320
|
+
state.phase = "complete";
|
|
321
|
+
persistPlanState(pi, state);
|
|
322
|
+
|
|
323
|
+
// Restore full tool set
|
|
324
|
+
restoreFullToolSet(pi);
|
|
325
|
+
|
|
326
|
+
// Execute completion handler (compact/tree setup)
|
|
327
|
+
const { handlePlanComplete } = await import("./compact.js");
|
|
328
|
+
handlePlanComplete(pi, ctx, state, isolation, chosenMode);
|
|
329
|
+
|
|
330
|
+
// Reset state and clear widget — same as abort
|
|
331
|
+
const updatedState = resetPlanState(pi, sessions, sessionId, ctx);
|
|
332
|
+
updatePlanWidget(ctx, updatedState);
|
|
333
|
+
|
|
334
|
+
const displayPath = relativePath(planFilePath, projectDir);
|
|
335
|
+
return {
|
|
336
|
+
content: [{ type: "text" as const, text: `Plan approved. File: ${displayPath}` }],
|
|
337
|
+
details: { action: "complete", planFilePath: displayPath, isolation, execMode: chosenMode },
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
case "abort": {
|
|
342
|
+
const updatedState = resetPlanState(pi, sessions, sessionId, ctx);
|
|
343
|
+
updatePlanWidget(ctx, updatedState);
|
|
344
|
+
restoreFullToolSet(pi);
|
|
345
|
+
return {
|
|
346
|
+
content: [{ type: "text" as const, text: "Plan mode aborted. Full tool access restored." }],
|
|
347
|
+
details: { action: "abort" },
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
}
|
package/src/widget.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
import type { PlanState } from "./state.js";
|
|
4
|
+
|
|
5
|
+
export function updatePlanWidget(ctx: ExtensionContext, state: PlanState): void {
|
|
6
|
+
if (!state.isActive) {
|
|
7
|
+
ctx.ui.setWidget("plan-mode", undefined);
|
|
8
|
+
ctx.ui.setStatus("plan-mode", undefined);
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const th = ctx.ui.theme;
|
|
13
|
+
ctx.ui.setWidget("plan-mode", [th.fg("accent", "[Plan Mode]")]);
|
|
14
|
+
ctx.ui.setStatus("plan-mode", th.fg("accent", "Plan Mode"));
|
|
15
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
template: bugfix-plan
|
|
3
|
+
created: ""
|
|
4
|
+
status: draft
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Bugfix Plan: [Bug Name]
|
|
8
|
+
|
|
9
|
+
## 现象
|
|
10
|
+
<!-- Bug 的具体表现 -->
|
|
11
|
+
|
|
12
|
+
## 根因分析
|
|
13
|
+
<!-- 通过代码探索和日志分析得出的根因 -->
|
|
14
|
+
|
|
15
|
+
## 修复策略
|
|
16
|
+
<!-- 修复方案和替代方案 -->
|
|
17
|
+
|
|
18
|
+
## 受影响文件
|
|
19
|
+
<!-- 需要修改的文件列表 -->
|
|
20
|
+
|
|
21
|
+
## 回归测试
|
|
22
|
+
<!-- 如何验证修复不会引入新问题 -->
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
template: feature-plan
|
|
3
|
+
created: ""
|
|
4
|
+
status: draft
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Feature Plan: [Feature Name]
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
<!-- 简述功能目标和价值 -->
|
|
11
|
+
|
|
12
|
+
## Requirements
|
|
13
|
+
<!-- 用户需求、业务需求 -->
|
|
14
|
+
|
|
15
|
+
## Design Decisions
|
|
16
|
+
<!-- 技术选型、架构决策 -->
|
|
17
|
+
|
|
18
|
+
## Implementation Steps
|
|
19
|
+
<!-- 分步骤的实现计划 -->
|
|
20
|
+
|
|
21
|
+
## Testing Strategy
|
|
22
|
+
<!-- 测试策略 -->
|
|
23
|
+
|
|
24
|
+
## Risks & Mitigations
|
|
25
|
+
<!-- 风险和缓解措施 -->
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
template: implementation-plan
|
|
3
|
+
created: ""
|
|
4
|
+
status: draft
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Implementation Plan: [Feature Name]
|
|
8
|
+
|
|
9
|
+
## Spec 摘要
|
|
10
|
+
<!-- 对应 spec 的关键要求 -->
|
|
11
|
+
|
|
12
|
+
## 任务分解
|
|
13
|
+
<!-- 分解为可执行的任务 -->
|
|
14
|
+
|
|
15
|
+
## 实现顺序
|
|
16
|
+
<!-- 任务的依赖关系和执行顺序 -->
|
|
17
|
+
|
|
18
|
+
## 验证
|
|
19
|
+
<!-- 如何验证实现正确性 -->
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
template: refactor-plan
|
|
3
|
+
created: ""
|
|
4
|
+
status: draft
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Refactor Plan: [Refactor Name]
|
|
8
|
+
|
|
9
|
+
## 现状
|
|
10
|
+
<!-- 当前代码的问题 -->
|
|
11
|
+
|
|
12
|
+
## 目标结构
|
|
13
|
+
<!-- 重构后的目标架构 -->
|
|
14
|
+
|
|
15
|
+
## 分步骤计划
|
|
16
|
+
<!-- 重构的分步执行计划 -->
|
|
17
|
+
|
|
18
|
+
## 风险与缓解
|
|
19
|
+
<!-- 重构风险和缓解措施 -->
|
|
20
|
+
|
|
21
|
+
## 验证
|
|
22
|
+
<!-- 如何验证重构正确性 -->
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
template: research-plan
|
|
3
|
+
created: ""
|
|
4
|
+
status: draft
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Research Plan: [Topic]
|
|
8
|
+
|
|
9
|
+
## 问题
|
|
10
|
+
<!-- 需要调研的问题 -->
|
|
11
|
+
|
|
12
|
+
## 候选方案
|
|
13
|
+
<!-- 候选方案列表 -->
|
|
14
|
+
|
|
15
|
+
## 对比分析
|
|
16
|
+
<!-- 方案的优劣对比 -->
|
|
17
|
+
|
|
18
|
+
## 推荐
|
|
19
|
+
<!-- 推荐方案和理由 -->
|
|
20
|
+
|
|
21
|
+
## 后续步骤
|
|
22
|
+
<!-- 调研结论后的下一步 -->
|