Skip to content

Commit 864c302

Browse files
authored
feat: implement complete plan mode with 5-phase workflow (#709)
* feat: implement complete plan mode with 5-phase workflow * feat: add plan approval view with external editor support * refactor: move plan mode components to dedicated directory * feat: auto-approve plan file edits without requiring user approval * fix: hide shift+tab shortcut in plan approval view * fix: correct comment translation from Chinese to English * refactor: remove plan mode functionality
1 parent dca059d commit 864c302

30 files changed

+1589
-206
lines changed
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
# Plan Mode 完整重构
2+
3+
**Date:** 2026-01-21
4+
5+
## Context
6+
7+
Neovate Code 现有一个简化版的 Plan Mode,但缺少 Claude Code 的核心功能。参考 `plan-mode-implementation.md``planFilePath-generation-rules.md` 文档,需要对 Plan Mode 进行完整重构,实现与 Claude Code 一致的行为。
8+
9+
### 现有状态
10+
11+
- 已有简化版 Plan Mode,通过 `planSystemPrompt.ts``store.ts` 中的 `planMode` 状态实现
12+
- 缺少 `EnterPlanMode``ExitPlanMode` 工具
13+
- 常量 `AGENT_TYPE.PLAN` 已定义但未实现
14+
- 缺少 5 阶段工作流和计划文件持久化
15+
16+
### 目标
17+
18+
完整复刻 Claude Code 的 Plan Mode 功能:
19+
- 实现 EnterPlanMode/ExitPlanMode 工具
20+
- 完整 5 阶段工作流
21+
- Plan Agent 子代理
22+
- 友好的 Slug 命名 (`{形容词}-{动词}-{名词}.md`)
23+
- 全局配置目录存储 (`~/.neovate/plans/`)
24+
25+
## Discussion
26+
27+
### 关键决策
28+
29+
1. **实现范围**:选择完整复刻 Claude Code,而非简化版或渐进式实现
30+
2. **计划文件位置**:选择全局配置目录 (`~/.neovate/plans/`),遵循 Claude Code 模式
31+
3. **Plan Agent**:实现独立的 Plan Agent 子代理,支持并行设计探索
32+
4. **快捷键**:保留现有 Shift+Tab 循环切换逻辑
33+
34+
### 架构选择
35+
36+
采用**工具驱动架构**(方案 A):
37+
- AI 主动调用 `EnterPlanMode` / `ExitPlanMode` 工具控制模式切换
38+
- 与 Claude Code 行为完全一致
39+
- AI 可自主判断何时需要规划
40+
41+
### 通信模式
42+
43+
使用 **MessageBus** 进行工具与 UI 通信(参考 `task.ts``bash.ts`),而非直接操作 store:
44+
- 工具通过 `messageBus.emitEvent()` 发送事件
45+
- UI 层监听事件后更新 store 状态
46+
- 保持工具与 UI 解耦
47+
48+
## Approach
49+
50+
### 5 阶段工作流
51+
52+
1. **Phase 1: Initial Understanding** - 使用 Explore agents 理解需求和代码
53+
2. **Phase 2: Design** - 使用 Plan agents 设计实现方案
54+
3. **Phase 3: Review** - 审查计划与用户意图的一致性
55+
4. **Phase 4: Final Plan** - 写入最终计划到计划文件
56+
5. **Phase 5: Call ExitPlanMode** - 提交计划供用户审批
57+
58+
### Slug 生成规则
59+
60+
- 格式:`{形容词}-{动词}-{名词}`
61+
- 示例:`peaceful-dancing-firefly`, `async-compiling-nebula`
62+
- 词汇表:~200 形容词 × ~100 动词 × ~200 名词 = 400万种组合
63+
- 冲突处理:缓存 + 文件系统检查 + 最多 10 次重试
64+
65+
### 工具过滤
66+
67+
由 Subagent 自行处理,通过 `AgentDefinition` 中的 `tools``disallowedTools` 配置,无需在 `resolveTools()` 中做额外过滤。
68+
69+
## Architecture
70+
71+
### 文件结构
72+
73+
```
74+
src/
75+
├── tools/
76+
│ ├── enterPlanMode.ts # EnterPlanMode 工具
77+
│ └── exitPlanMode.ts # ExitPlanMode 工具
78+
├── agent/builtin/
79+
│ └── plan.ts # Plan Agent 子代理
80+
├── planFile.ts # 计划文件管理(路径、读写、缓存)
81+
└── utils/
82+
└── planSlug.ts # Slug 生成器
83+
```
84+
85+
### 需修改文件
86+
87+
```
88+
src/
89+
├── tool.ts # resolveTools() 中注册新工具
90+
├── planSystemPrompt.ts # 增强 5 阶段工作流指导
91+
├── ui/store.ts # 添加 planFilePath、planPhase 状态
92+
├── ui/ApprovalModal.tsx # 增强计划审批 UI
93+
├── agent/builtin/index.ts # 注册 Plan Agent
94+
├── constants.ts # 新增工具名和事件常量
95+
└── ui/App.tsx # MessageBus 事件监听
96+
```
97+
98+
### 常量定义
99+
100+
```typescript
101+
// src/constants.ts
102+
export const TOOL_NAMES = {
103+
// ...现有工具
104+
ENTER_PLAN_MODE: 'EnterPlanMode',
105+
EXIT_PLAN_MODE: 'ExitPlanMode',
106+
} as const;
107+
108+
export const PLAN_MODE_EVENTS = {
109+
ENTER_PLAN_MODE: 'plan.enter',
110+
EXIT_PLAN_MODE: 'plan.exit',
111+
PLAN_APPROVED: 'plan.approved',
112+
PLAN_DENIED: 'plan.denied',
113+
} as const;
114+
```
115+
116+
### EnterPlanMode 工具
117+
118+
```typescript
119+
export function createEnterPlanModeTool(opts: {
120+
context: Context;
121+
sessionId: string;
122+
messageBus?: MessageBus;
123+
planFileManager: PlanFileManager;
124+
}) {
125+
return createTool({
126+
name: TOOL_NAMES.ENTER_PLAN_MODE,
127+
parameters: z.strictObject({}),
128+
129+
execute: async (params, toolCallId) => {
130+
// 1. 检查是否在 Agent 上下文(禁止)
131+
if (context.agentId) {
132+
return { llmContent: 'Cannot use in agent contexts', isError: true };
133+
}
134+
135+
// 2. 生成/获取计划文件路径
136+
const planFilePath = planFileManager.getPlanFilePath(sessionId);
137+
138+
// 3. 通过 MessageBus 通知 UI
139+
if (messageBus) {
140+
await messageBus.emitEvent(PLAN_MODE_EVENTS.ENTER_PLAN_MODE, {
141+
sessionId, planFilePath, planExists, timestamp: Date.now(),
142+
});
143+
}
144+
145+
// 4. 返回引导信息
146+
return { llmContent: buildEnterPlanModeResponse(...) };
147+
},
148+
149+
approval: { category: 'ask', needsApproval: async () => true },
150+
});
151+
}
152+
```
153+
154+
### ExitPlanMode 工具
155+
156+
```typescript
157+
export function createExitPlanModeTool(opts: {
158+
context: Context;
159+
sessionId: string;
160+
messageBus?: MessageBus;
161+
planFileManager: PlanFileManager;
162+
}) {
163+
return createTool({
164+
name: TOOL_NAMES.EXIT_PLAN_MODE,
165+
parameters: z.strictObject({}),
166+
167+
execute: async (params, toolCallId) => {
168+
const planFilePath = planFileManager.getPlanFilePath(sessionId);
169+
const planContent = planFileManager.readPlan(sessionId);
170+
171+
if (messageBus) {
172+
await messageBus.emitEvent(PLAN_MODE_EVENTS.EXIT_PLAN_MODE, {
173+
sessionId, planFilePath, planContent, isAgent: !!context.agentId,
174+
});
175+
}
176+
177+
return { llmContent: buildExitPlanModeResponse(...) };
178+
},
179+
180+
approval: { category: 'ask', needsApproval: async () => true },
181+
});
182+
}
183+
```
184+
185+
### Plan Agent 定义
186+
187+
```typescript
188+
export function createPlanAgent(opts: { context: Context }): AgentDefinition {
189+
return {
190+
agentType: AGENT_TYPE.PLAN,
191+
source: AgentSource.BuiltIn,
192+
193+
whenToUse: `Use during plan mode Phase 2 (Design) to explore implementation approaches...`,
194+
195+
systemPrompt: `You are a Plan Agent responsible for designing implementation strategies...`,
196+
197+
model: opts.context.config.planModel || opts.context.config.model,
198+
199+
tools: ['read', 'ls', 'glob', 'grep', 'fetch', 'AskUserQuestion'],
200+
disallowedTools: ['write', 'edit', 'bash', 'EnterPlanMode', 'ExitPlanMode'],
201+
202+
forkContext: true,
203+
color: '#9333EA',
204+
};
205+
}
206+
```
207+
208+
### Store 状态增强
209+
210+
```typescript
211+
interface AppState {
212+
// 现有状态
213+
planMode: boolean;
214+
215+
// 新增状态
216+
planFilePath: string | null;
217+
planPhase: 'explore' | 'design' | 'review' | 'finalize' | 'exit' | null;
218+
planContent: string | null;
219+
}
220+
221+
interface AppActions {
222+
enterPlanMode: (opts: { planFilePath: string; planExists: boolean }) => void;
223+
exitPlanMode: (opts: { approved: boolean; approvalMode?: string; feedback?: string }) => void;
224+
updatePlanPhase: (phase: PlanPhase) => void;
225+
setPlanContent: (content: string | null) => void;
226+
}
227+
```
228+
229+
### 审批 UI
230+
231+
ExitPlanMode 触发专用审批视图:
232+
- 显示计划文件路径和内容预览
233+
- 三个选项:
234+
- "Yes, auto-accept edits" (推荐)
235+
- "Yes, manually approve edits"
236+
- "No, keep planning" (可输入反馈)
237+
238+
### 实现顺序
239+
240+
| 阶段 | 内容 | 预估时间 |
241+
|------|------|---------|
242+
| 1 | 基础设施 (planSlug.ts, planFile.ts, constants.ts) | 2-3h |
243+
| 2 | 核心工具 (enterPlanMode.ts, exitPlanMode.ts) | 3-4h |
244+
| 3 | Agent 与提示 (plan.ts, planSystemPrompt.ts) | 2-3h |
245+
| 4 | 状态与 UI (store.ts, ApprovalModal.tsx) | 3-4h |
246+
| 5 | 整合 (tool.ts, App.tsx) | 1-2h |
247+
| 6 | 测试 | 3-4h |
248+
| **总计** | | **14-20h** |
249+
250+
### 工具可用性矩阵
251+
252+
| 工具 | 正常模式 | Plan Mode | Agent 上下文 |
253+
|------|---------|-----------|-------------|
254+
| read, ls, glob, grep, fetch ||||
255+
| write, edit, bash || ❌ (仅计划文件) | 取决于配置 |
256+
| EnterPlanMode ||||
257+
| ExitPlanMode ||||
258+
| AskUserQuestion ||||
259+
| Task (Explore/Plan) ||||

src/agent/builtin/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { AgentDefinition } from '../types';
33
import { createExploreAgent } from './explore';
44
import { createGeneralPurposeAgent } from './general-purpose';
55
import { createNeovateCodeGuideAgent } from './neovate-code-guide';
6+
import { createPlanAgent } from './plan';
67

78
export function getBuiltinAgents(opts: {
89
context: Context;
@@ -11,5 +12,6 @@ export function getBuiltinAgents(opts: {
1112
createExploreAgent(opts),
1213
createGeneralPurposeAgent(opts),
1314
createNeovateCodeGuideAgent(opts),
15+
createPlanAgent(opts),
1416
];
1517
}

0 commit comments

Comments
 (0)