tunacode-cli 0.0.55__py3-none-any.whl → 0.0.56__py3-none-any.whl
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.
Potentially problematic release.
This version of tunacode-cli might be problematic. Click here for more details.
- tunacode/cli/commands/implementations/plan.py +50 -0
- tunacode/cli/commands/registry.py +3 -0
- tunacode/cli/repl.py +335 -1
- tunacode/cli/repl_components/output_display.py +18 -1
- tunacode/cli/repl_components/tool_executor.py +9 -0
- tunacode/constants.py +4 -2
- tunacode/core/agents/agent_components/agent_config.py +134 -7
- tunacode/core/agents/agent_components/node_processor.py +44 -42
- tunacode/core/state.py +44 -0
- tunacode/core/tool_handler.py +20 -0
- tunacode/tools/exit_plan_mode.py +191 -0
- tunacode/tools/present_plan.py +208 -0
- tunacode/types.py +57 -0
- tunacode/ui/input.py +13 -2
- tunacode/ui/keybindings.py +24 -4
- tunacode/ui/panels.py +23 -0
- tunacode/ui/prompt_manager.py +19 -2
- tunacode/ui/tool_ui.py +3 -2
- tunacode/utils/message_utils.py +14 -4
- {tunacode_cli-0.0.55.dist-info → tunacode_cli-0.0.56.dist-info}/METADATA +4 -3
- {tunacode_cli-0.0.55.dist-info → tunacode_cli-0.0.56.dist-info}/RECORD +25 -22
- {tunacode_cli-0.0.55.dist-info → tunacode_cli-0.0.56.dist-info}/WHEEL +0 -0
- {tunacode_cli-0.0.55.dist-info → tunacode_cli-0.0.56.dist-info}/entry_points.txt +0 -0
- {tunacode_cli-0.0.55.dist-info → tunacode_cli-0.0.56.dist-info}/licenses/LICENSE +0 -0
- {tunacode_cli-0.0.55.dist-info → tunacode_cli-0.0.56.dist-info}/top_level.txt +0 -0
|
@@ -8,6 +8,7 @@ from tunacode.core.logging.logger import get_logger
|
|
|
8
8
|
from tunacode.core.state import StateManager
|
|
9
9
|
from tunacode.services.mcp import get_mcp_servers
|
|
10
10
|
from tunacode.tools.bash import bash
|
|
11
|
+
from tunacode.tools.present_plan import create_present_plan_tool
|
|
11
12
|
from tunacode.tools.glob import glob
|
|
12
13
|
from tunacode.tools.grep import grep
|
|
13
14
|
from tunacode.tools.list_dir import list_dir
|
|
@@ -65,7 +66,11 @@ def load_tunacode_context() -> str:
|
|
|
65
66
|
|
|
66
67
|
def get_or_create_agent(model: ModelName, state_manager: StateManager) -> PydanticAgent:
|
|
67
68
|
"""Get existing agent or create new one for the specified model."""
|
|
69
|
+
import logging
|
|
70
|
+
logger = logging.getLogger(__name__)
|
|
71
|
+
|
|
68
72
|
if model not in state_manager.session.agents:
|
|
73
|
+
logger.debug(f"Creating new agent for model {model}, plan_mode={state_manager.is_plan_mode()}")
|
|
69
74
|
max_retries = state_manager.session.user_config.get("settings", {}).get("max_retries", 3)
|
|
70
75
|
|
|
71
76
|
# Lazy import Agent and Tool
|
|
@@ -78,8 +83,105 @@ def get_or_create_agent(model: ModelName, state_manager: StateManager) -> Pydant
|
|
|
78
83
|
# Load TUNACODE.md context
|
|
79
84
|
system_prompt += load_tunacode_context()
|
|
80
85
|
|
|
81
|
-
#
|
|
86
|
+
# Add plan mode context if in plan mode
|
|
87
|
+
if state_manager.is_plan_mode():
|
|
88
|
+
# REMOVE all TUNACODE_TASK_COMPLETE instructions from the system prompt
|
|
89
|
+
system_prompt = system_prompt.replace("TUNACODE_TASK_COMPLETE", "PLAN_MODE_TASK_PLACEHOLDER")
|
|
90
|
+
# Remove the completion guidance that conflicts with plan mode
|
|
91
|
+
lines_to_remove = [
|
|
92
|
+
"When a task is COMPLETE, start your response with: TUNACODE_TASK_COMPLETE",
|
|
93
|
+
"4. When a task is COMPLETE, start your response with: TUNACODE_TASK_COMPLETE",
|
|
94
|
+
"**How to signal completion:**",
|
|
95
|
+
"TUNACODE_TASK_COMPLETE",
|
|
96
|
+
"[Your summary of what was accomplished]",
|
|
97
|
+
"**IMPORTANT**: Always evaluate if you've completed the task. If yes, use TUNACODE_TASK_COMPLETE.",
|
|
98
|
+
"This prevents wasting iterations and API calls."
|
|
99
|
+
]
|
|
100
|
+
for line in lines_to_remove:
|
|
101
|
+
system_prompt = system_prompt.replace(line, "")
|
|
102
|
+
plan_mode_override = """
|
|
103
|
+
🔍 PLAN MODE - YOU MUST USE THE present_plan TOOL 🔍
|
|
104
|
+
|
|
105
|
+
CRITICAL: You are in Plan Mode. You MUST execute the present_plan TOOL, not show it as text.
|
|
106
|
+
|
|
107
|
+
❌ WRONG - DO NOT SHOW THE FUNCTION AS TEXT:
|
|
108
|
+
```
|
|
109
|
+
present_plan(title="...", ...) # THIS IS WRONG - DON'T SHOW AS CODE
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
✅ CORRECT - ACTUALLY EXECUTE THE TOOL:
|
|
113
|
+
You must EXECUTE present_plan as a tool call, just like you execute read_file or grep.
|
|
114
|
+
|
|
115
|
+
CRITICAL RULES:
|
|
116
|
+
1. DO NOT show present_plan() as code or text
|
|
117
|
+
2. DO NOT write "Here's the plan" or any text description
|
|
118
|
+
3. DO NOT use TUNACODE_TASK_COMPLETE
|
|
119
|
+
4. DO NOT use markdown code blocks for present_plan
|
|
120
|
+
|
|
121
|
+
YOU MUST EXECUTE THE TOOL:
|
|
122
|
+
When the user asks you to "plan" something, you must:
|
|
123
|
+
1. Research using read_only tools (optional)
|
|
124
|
+
2. EXECUTE present_plan tool with the plan data
|
|
125
|
+
3. The tool will handle displaying the plan
|
|
126
|
+
|
|
127
|
+
Example of CORRECT behavior:
|
|
128
|
+
User: "plan a markdown file"
|
|
129
|
+
You: [Execute read_file/grep if needed for research]
|
|
130
|
+
[Then EXECUTE present_plan tool - not as text but as an actual tool call]
|
|
131
|
+
|
|
132
|
+
Remember: present_plan is a TOOL like read_file or grep. You must EXECUTE it, not SHOW it.
|
|
133
|
+
|
|
134
|
+
Available tools:
|
|
135
|
+
- read_file, grep, list_dir, glob: For research
|
|
136
|
+
- present_plan: EXECUTE this tool to present the plan (DO NOT show as text)
|
|
137
|
+
|
|
138
|
+
"""
|
|
139
|
+
# COMPLETELY REPLACE system prompt in plan mode - nuclear option
|
|
140
|
+
system_prompt = """
|
|
141
|
+
🔧 PLAN MODE - TOOL EXECUTION ONLY 🔧
|
|
142
|
+
|
|
143
|
+
You are a planning assistant that ONLY communicates through tool execution.
|
|
144
|
+
|
|
145
|
+
CRITICAL: You cannot respond with text. You MUST use tools for everything.
|
|
146
|
+
|
|
147
|
+
AVAILABLE TOOLS:
|
|
148
|
+
- read_file(filepath): Read file contents
|
|
149
|
+
- grep(pattern): Search for text patterns
|
|
150
|
+
- list_dir(directory): List directory contents
|
|
151
|
+
- glob(pattern): Find files matching patterns
|
|
152
|
+
- present_plan(title, overview, steps, files_to_create, success_criteria): Present structured plan
|
|
153
|
+
|
|
154
|
+
MANDATORY WORKFLOW:
|
|
155
|
+
1. User asks you to plan something
|
|
156
|
+
2. You research using read-only tools (if needed)
|
|
157
|
+
3. You EXECUTE present_plan tool with structured data
|
|
158
|
+
4. DONE
|
|
159
|
+
|
|
160
|
+
FORBIDDEN:
|
|
161
|
+
- Text responses
|
|
162
|
+
- Showing function calls as code
|
|
163
|
+
- Saying "here is the plan"
|
|
164
|
+
- Any text completion
|
|
165
|
+
|
|
166
|
+
EXAMPLE:
|
|
167
|
+
User: "plan a markdown file"
|
|
168
|
+
You: [Call read_file or grep for research if needed]
|
|
169
|
+
[Call present_plan tool with actual parameters - NOT as text]
|
|
170
|
+
|
|
171
|
+
The present_plan tool takes these parameters:
|
|
172
|
+
- title: Brief title string
|
|
173
|
+
- overview: What the plan accomplishes
|
|
174
|
+
- steps: List of implementation steps
|
|
175
|
+
- files_to_create: List of files to create
|
|
176
|
+
- success_criteria: List of success criteria
|
|
177
|
+
|
|
178
|
+
YOU MUST EXECUTE present_plan TOOL TO COMPLETE ANY PLANNING TASK.
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
# Initialize tools that need state manager
|
|
82
182
|
todo_tool = TodoTool(state_manager=state_manager)
|
|
183
|
+
present_plan = create_present_plan_tool(state_manager)
|
|
184
|
+
logger.debug(f"Tools initialized, present_plan available: {present_plan is not None}")
|
|
83
185
|
|
|
84
186
|
# Add todo context if available
|
|
85
187
|
try:
|
|
@@ -89,12 +191,21 @@ def get_or_create_agent(model: ModelName, state_manager: StateManager) -> Pydant
|
|
|
89
191
|
except Exception as e:
|
|
90
192
|
logger.warning(f"Warning: Failed to load todos: {e}")
|
|
91
193
|
|
|
92
|
-
# Create
|
|
93
|
-
state_manager.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
194
|
+
# Create tool list based on mode
|
|
195
|
+
if state_manager.is_plan_mode():
|
|
196
|
+
# Plan mode: Only read-only tools + present_plan
|
|
197
|
+
tools_list = [
|
|
198
|
+
Tool(present_plan, max_retries=max_retries),
|
|
199
|
+
Tool(glob, max_retries=max_retries),
|
|
200
|
+
Tool(grep, max_retries=max_retries),
|
|
201
|
+
Tool(list_dir, max_retries=max_retries),
|
|
202
|
+
Tool(read_file, max_retries=max_retries),
|
|
203
|
+
]
|
|
204
|
+
else:
|
|
205
|
+
# Normal mode: All tools
|
|
206
|
+
tools_list = [
|
|
97
207
|
Tool(bash, max_retries=max_retries),
|
|
208
|
+
Tool(present_plan, max_retries=max_retries),
|
|
98
209
|
Tool(glob, max_retries=max_retries),
|
|
99
210
|
Tool(grep, max_retries=max_retries),
|
|
100
211
|
Tool(list_dir, max_retries=max_retries),
|
|
@@ -103,7 +214,23 @@ def get_or_create_agent(model: ModelName, state_manager: StateManager) -> Pydant
|
|
|
103
214
|
Tool(todo_tool._execute, max_retries=max_retries),
|
|
104
215
|
Tool(update_file, max_retries=max_retries),
|
|
105
216
|
Tool(write_file, max_retries=max_retries),
|
|
106
|
-
]
|
|
217
|
+
]
|
|
218
|
+
|
|
219
|
+
# Log which tools are being registered
|
|
220
|
+
logger.debug(f"Creating agent: plan_mode={state_manager.is_plan_mode()}, tools={len(tools_list)}")
|
|
221
|
+
if state_manager.is_plan_mode():
|
|
222
|
+
logger.debug(f"PLAN MODE TOOLS: {[str(tool) for tool in tools_list]}")
|
|
223
|
+
logger.debug(f"present_plan tool type: {type(present_plan)}")
|
|
224
|
+
|
|
225
|
+
if "PLAN MODE - YOU MUST USE THE present_plan TOOL" in system_prompt:
|
|
226
|
+
logger.debug("✅ Plan mode instructions ARE in system prompt")
|
|
227
|
+
else:
|
|
228
|
+
logger.debug("❌ Plan mode instructions NOT in system prompt")
|
|
229
|
+
|
|
230
|
+
state_manager.session.agents[model] = Agent(
|
|
231
|
+
model=model,
|
|
232
|
+
system_prompt=system_prompt,
|
|
233
|
+
tools=tools_list,
|
|
107
234
|
mcp_servers=get_mcp_servers(state_manager),
|
|
108
235
|
)
|
|
109
236
|
return state_manager.session.agents[model]
|
|
@@ -343,45 +343,46 @@ async def _process_tool_calls(
|
|
|
343
343
|
f"[bold #00d7ff]{batch_msg}...[/bold #00d7ff]", state_manager
|
|
344
344
|
)
|
|
345
345
|
|
|
346
|
-
# Enhanced visual feedback for parallel execution
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
buffered_part
|
|
358
|
-
|
|
359
|
-
if (
|
|
360
|
-
buffered_part.tool_name == "read_file"
|
|
361
|
-
and "file_path" in buffered_part.args
|
|
362
|
-
):
|
|
363
|
-
tool_desc += f" → {buffered_part.args['file_path']}"
|
|
364
|
-
elif (
|
|
365
|
-
buffered_part.tool_name == "grep"
|
|
366
|
-
and "pattern" in buffered_part.args
|
|
367
|
-
):
|
|
368
|
-
tool_desc += f" → pattern: '{buffered_part.args['pattern']}'"
|
|
369
|
-
if "include_files" in buffered_part.args:
|
|
370
|
-
tool_desc += (
|
|
371
|
-
f", files: '{buffered_part.args['include_files']}'"
|
|
372
|
-
)
|
|
373
|
-
elif (
|
|
374
|
-
buffered_part.tool_name == "list_dir"
|
|
375
|
-
and "directory" in buffered_part.args
|
|
376
|
-
):
|
|
377
|
-
tool_desc += f" → {buffered_part.args['directory']}"
|
|
378
|
-
elif (
|
|
379
|
-
buffered_part.tool_name == "glob"
|
|
380
|
-
and "pattern" in buffered_part.args
|
|
346
|
+
# Enhanced visual feedback for parallel execution (suppress in plan mode)
|
|
347
|
+
if not state_manager.is_plan_mode():
|
|
348
|
+
await ui.muted("\n" + "=" * 60)
|
|
349
|
+
await ui.muted(
|
|
350
|
+
f"🚀 PARALLEL BATCH #{batch_id}: Executing {len(buffered_tasks)} read-only tools concurrently"
|
|
351
|
+
)
|
|
352
|
+
await ui.muted("=" * 60)
|
|
353
|
+
|
|
354
|
+
# Display details of what's being executed
|
|
355
|
+
for idx, (buffered_part, _) in enumerate(buffered_tasks, 1):
|
|
356
|
+
tool_desc = f" [{idx}] {buffered_part.tool_name}"
|
|
357
|
+
if hasattr(buffered_part, "args") and isinstance(
|
|
358
|
+
buffered_part.args, dict
|
|
381
359
|
):
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
360
|
+
if (
|
|
361
|
+
buffered_part.tool_name == "read_file"
|
|
362
|
+
and "file_path" in buffered_part.args
|
|
363
|
+
):
|
|
364
|
+
tool_desc += f" → {buffered_part.args['file_path']}"
|
|
365
|
+
elif (
|
|
366
|
+
buffered_part.tool_name == "grep"
|
|
367
|
+
and "pattern" in buffered_part.args
|
|
368
|
+
):
|
|
369
|
+
tool_desc += f" → pattern: '{buffered_part.args['pattern']}'"
|
|
370
|
+
if "include_files" in buffered_part.args:
|
|
371
|
+
tool_desc += (
|
|
372
|
+
f", files: '{buffered_part.args['include_files']}'"
|
|
373
|
+
)
|
|
374
|
+
elif (
|
|
375
|
+
buffered_part.tool_name == "list_dir"
|
|
376
|
+
and "directory" in buffered_part.args
|
|
377
|
+
):
|
|
378
|
+
tool_desc += f" → {buffered_part.args['directory']}"
|
|
379
|
+
elif (
|
|
380
|
+
buffered_part.tool_name == "glob"
|
|
381
|
+
and "pattern" in buffered_part.args
|
|
382
|
+
):
|
|
383
|
+
tool_desc += f" → pattern: '{buffered_part.args['pattern']}'"
|
|
384
|
+
await ui.muted(tool_desc)
|
|
385
|
+
await ui.muted("=" * 60)
|
|
385
386
|
|
|
386
387
|
await execute_tools_parallel(buffered_tasks, tool_callback)
|
|
387
388
|
|
|
@@ -391,10 +392,11 @@ async def _process_tool_calls(
|
|
|
391
392
|
) # Assume 100ms per tool average
|
|
392
393
|
speedup = sequential_estimate / elapsed_time if elapsed_time > 0 else 1.0
|
|
393
394
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
395
|
+
if not state_manager.is_plan_mode():
|
|
396
|
+
await ui.muted(
|
|
397
|
+
f"✅ Parallel batch completed in {elapsed_time:.0f}ms "
|
|
398
|
+
f"(~{speedup:.1f}x faster than sequential)\n"
|
|
399
|
+
)
|
|
398
400
|
|
|
399
401
|
# Reset spinner message back to thinking
|
|
400
402
|
from tunacode.constants import UI_THINKING_MESSAGE
|
tunacode/core/state.py
CHANGED
|
@@ -15,6 +15,8 @@ from tunacode.types import (
|
|
|
15
15
|
InputSessions,
|
|
16
16
|
MessageHistory,
|
|
17
17
|
ModelName,
|
|
18
|
+
PlanDoc,
|
|
19
|
+
PlanPhase,
|
|
18
20
|
SessionId,
|
|
19
21
|
TodoItem,
|
|
20
22
|
ToolName,
|
|
@@ -84,6 +86,12 @@ class SessionState:
|
|
|
84
86
|
task_hierarchy: dict[str, Any] = field(default_factory=dict)
|
|
85
87
|
iteration_budgets: dict[str, int] = field(default_factory=dict)
|
|
86
88
|
recursive_context_stack: list[dict[str, Any]] = field(default_factory=list)
|
|
89
|
+
|
|
90
|
+
# Plan Mode state tracking
|
|
91
|
+
plan_mode: bool = False
|
|
92
|
+
plan_phase: Optional[PlanPhase] = None
|
|
93
|
+
current_plan: Optional[PlanDoc] = None
|
|
94
|
+
plan_approved: bool = False
|
|
87
95
|
|
|
88
96
|
def update_token_count(self):
|
|
89
97
|
"""Calculates the total token count from messages and files in context."""
|
|
@@ -167,3 +175,39 @@ class StateManager:
|
|
|
167
175
|
def reset_session(self) -> None:
|
|
168
176
|
"""Reset the session to a fresh state."""
|
|
169
177
|
self._session = SessionState()
|
|
178
|
+
|
|
179
|
+
# Plan Mode methods
|
|
180
|
+
def enter_plan_mode(self) -> None:
|
|
181
|
+
"""Enter plan mode - restricts to read-only operations."""
|
|
182
|
+
self._session.plan_mode = True
|
|
183
|
+
self._session.plan_phase = PlanPhase.PLANNING_RESEARCH
|
|
184
|
+
self._session.current_plan = None
|
|
185
|
+
self._session.plan_approved = False
|
|
186
|
+
# Clear agent cache to force recreation with plan mode tools
|
|
187
|
+
self._session.agents.clear()
|
|
188
|
+
|
|
189
|
+
def exit_plan_mode(self, plan: Optional[PlanDoc] = None) -> None:
|
|
190
|
+
"""Exit plan mode with optional plan data."""
|
|
191
|
+
self._session.plan_mode = False
|
|
192
|
+
self._session.plan_phase = None
|
|
193
|
+
self._session.current_plan = plan
|
|
194
|
+
self._session.plan_approved = False
|
|
195
|
+
# Clear agent cache to force recreation without plan mode tools
|
|
196
|
+
self._session.agents.clear()
|
|
197
|
+
|
|
198
|
+
def approve_plan(self) -> None:
|
|
199
|
+
"""Mark current plan as approved for execution."""
|
|
200
|
+
self._session.plan_approved = True
|
|
201
|
+
self._session.plan_mode = False
|
|
202
|
+
|
|
203
|
+
def is_plan_mode(self) -> bool:
|
|
204
|
+
"""Check if currently in plan mode."""
|
|
205
|
+
return self._session.plan_mode
|
|
206
|
+
|
|
207
|
+
def set_current_plan(self, plan: PlanDoc) -> None:
|
|
208
|
+
"""Set the current plan data."""
|
|
209
|
+
self._session.current_plan = plan
|
|
210
|
+
|
|
211
|
+
def get_current_plan(self) -> Optional[PlanDoc]:
|
|
212
|
+
"""Get the current plan data."""
|
|
213
|
+
return self._session.current_plan
|
tunacode/core/tool_handler.py
CHANGED
|
@@ -41,6 +41,14 @@ class ToolHandler:
|
|
|
41
41
|
Returns:
|
|
42
42
|
bool: True if confirmation is required, False otherwise.
|
|
43
43
|
"""
|
|
44
|
+
# Never confirm present_plan - it has its own approval flow
|
|
45
|
+
if tool_name == "present_plan":
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
# Block write tools in plan mode
|
|
49
|
+
if self.is_tool_blocked_in_plan_mode(tool_name):
|
|
50
|
+
return True # Force confirmation for blocked tools
|
|
51
|
+
|
|
44
52
|
# Skip confirmation for read-only tools
|
|
45
53
|
if is_read_only_tool(tool_name):
|
|
46
54
|
return False
|
|
@@ -52,6 +60,18 @@ class ToolHandler:
|
|
|
52
60
|
|
|
53
61
|
return not (self.state.session.yolo or tool_name in self.state.session.tool_ignore)
|
|
54
62
|
|
|
63
|
+
def is_tool_blocked_in_plan_mode(self, tool_name: ToolName) -> bool:
|
|
64
|
+
"""Check if tool is blocked in plan mode."""
|
|
65
|
+
if not self.state.is_plan_mode():
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
# Allow present_plan tool to end planning phase
|
|
69
|
+
if tool_name == "present_plan":
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
# Allow read-only tools
|
|
73
|
+
return not is_read_only_tool(tool_name)
|
|
74
|
+
|
|
55
75
|
def process_confirmation(self, response: ToolConfirmationResponse, tool_name: ToolName) -> bool:
|
|
56
76
|
"""
|
|
57
77
|
Process the confirmation response.
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Tool for exiting plan mode and presenting implementation plan."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List
|
|
4
|
+
|
|
5
|
+
from tunacode.tools.base import BaseTool
|
|
6
|
+
from tunacode.ui import console as ui
|
|
7
|
+
from tunacode.types import ToolResult
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ExitPlanModeTool(BaseTool):
|
|
11
|
+
"""Present implementation plan and exit plan mode."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, state_manager, ui_logger=None):
|
|
14
|
+
"""Initialize the exit plan mode tool.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
state_manager: StateManager instance for controlling plan mode state
|
|
18
|
+
ui_logger: UI logger instance for displaying messages
|
|
19
|
+
"""
|
|
20
|
+
super().__init__(ui_logger)
|
|
21
|
+
self.state_manager = state_manager
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def tool_name(self) -> str:
|
|
25
|
+
return "exit_plan_mode"
|
|
26
|
+
|
|
27
|
+
async def _execute(
|
|
28
|
+
self,
|
|
29
|
+
plan_title: str,
|
|
30
|
+
overview: str,
|
|
31
|
+
implementation_steps: List[str],
|
|
32
|
+
files_to_modify: List[str] = None,
|
|
33
|
+
files_to_create: List[str] = None,
|
|
34
|
+
risks_and_considerations: List[str] = None,
|
|
35
|
+
testing_approach: str = None,
|
|
36
|
+
success_criteria: List[str] = None,
|
|
37
|
+
) -> ToolResult:
|
|
38
|
+
"""Present the implementation plan and get user approval."""
|
|
39
|
+
|
|
40
|
+
plan = {
|
|
41
|
+
"title": plan_title,
|
|
42
|
+
"overview": overview,
|
|
43
|
+
"files_to_modify": files_to_modify or [],
|
|
44
|
+
"files_to_create": files_to_create or [],
|
|
45
|
+
"implementation_steps": implementation_steps,
|
|
46
|
+
"risks_and_considerations": risks_and_considerations or [],
|
|
47
|
+
"testing_approach": testing_approach or "Manual testing of functionality",
|
|
48
|
+
"success_criteria": success_criteria or []
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# Present plan to user
|
|
52
|
+
await self._present_plan(plan)
|
|
53
|
+
|
|
54
|
+
# Get user approval
|
|
55
|
+
approved = await self._get_user_approval()
|
|
56
|
+
|
|
57
|
+
# Update state based on user approval
|
|
58
|
+
if approved:
|
|
59
|
+
# Store the plan and exit plan mode
|
|
60
|
+
self.state_manager.set_current_plan(plan)
|
|
61
|
+
self.state_manager.exit_plan_mode(plan)
|
|
62
|
+
await ui.success("✅ Plan approved! Exiting Plan Mode.")
|
|
63
|
+
return "Plan approved and Plan Mode exited. You can now execute the implementation using write tools (write_file, update_file, bash, run_command)."
|
|
64
|
+
else:
|
|
65
|
+
# Keep the plan but stay in plan mode
|
|
66
|
+
self.state_manager.set_current_plan(plan)
|
|
67
|
+
await ui.warning("❌ Plan rejected. Staying in Plan Mode for further research.")
|
|
68
|
+
return "Plan rejected. Continue researching and refine your approach. You remain in Plan Mode - only read-only tools are available."
|
|
69
|
+
|
|
70
|
+
async def _present_plan(self, plan: Dict[str, Any]) -> None:
|
|
71
|
+
"""Present the plan in a formatted way."""
|
|
72
|
+
# Build the entire plan output as a single string to avoid UI flooding
|
|
73
|
+
output = []
|
|
74
|
+
output.append("")
|
|
75
|
+
output.append("╭─────────────────────────────────────────────────────────╮")
|
|
76
|
+
output.append("│ 📋 IMPLEMENTATION PLAN │")
|
|
77
|
+
output.append("╰─────────────────────────────────────────────────────────╯")
|
|
78
|
+
output.append("")
|
|
79
|
+
output.append(f"🎯 {plan['title']}")
|
|
80
|
+
output.append("")
|
|
81
|
+
|
|
82
|
+
if plan["overview"]:
|
|
83
|
+
output.append(f"📝 Overview: {plan['overview']}")
|
|
84
|
+
output.append("")
|
|
85
|
+
|
|
86
|
+
# Files section
|
|
87
|
+
if plan["files_to_modify"]:
|
|
88
|
+
output.append("📝 Files to Modify:")
|
|
89
|
+
for f in plan["files_to_modify"]:
|
|
90
|
+
output.append(f" • {f}")
|
|
91
|
+
output.append("")
|
|
92
|
+
|
|
93
|
+
if plan["files_to_create"]:
|
|
94
|
+
output.append("📄 Files to Create:")
|
|
95
|
+
for f in plan["files_to_create"]:
|
|
96
|
+
output.append(f" • {f}")
|
|
97
|
+
output.append("")
|
|
98
|
+
|
|
99
|
+
# Implementation steps
|
|
100
|
+
output.append("🔧 Implementation Steps:")
|
|
101
|
+
for i, step in enumerate(plan["implementation_steps"], 1):
|
|
102
|
+
output.append(f" {i}. {step}")
|
|
103
|
+
output.append("")
|
|
104
|
+
|
|
105
|
+
# Testing approach
|
|
106
|
+
if plan["testing_approach"]:
|
|
107
|
+
output.append(f"🧪 Testing Approach: {plan['testing_approach']}")
|
|
108
|
+
output.append("")
|
|
109
|
+
|
|
110
|
+
# Success criteria
|
|
111
|
+
if plan["success_criteria"]:
|
|
112
|
+
output.append("✅ Success Criteria:")
|
|
113
|
+
for criteria in plan["success_criteria"]:
|
|
114
|
+
output.append(f" • {criteria}")
|
|
115
|
+
output.append("")
|
|
116
|
+
|
|
117
|
+
# Risks and considerations
|
|
118
|
+
if plan["risks_and_considerations"]:
|
|
119
|
+
output.append("⚠️ Risks & Considerations:")
|
|
120
|
+
for risk in plan["risks_and_considerations"]:
|
|
121
|
+
output.append(f" • {risk}")
|
|
122
|
+
output.append("")
|
|
123
|
+
|
|
124
|
+
# Print everything at once
|
|
125
|
+
await ui.info("\n".join(output))
|
|
126
|
+
|
|
127
|
+
async def _get_user_approval(self) -> bool:
|
|
128
|
+
"""Get user approval for the plan."""
|
|
129
|
+
try:
|
|
130
|
+
from prompt_toolkit import PromptSession
|
|
131
|
+
from prompt_toolkit.patch_stdout import patch_stdout
|
|
132
|
+
|
|
133
|
+
session = PromptSession()
|
|
134
|
+
|
|
135
|
+
with patch_stdout():
|
|
136
|
+
response = await session.prompt_async("\n🤔 Approve this implementation plan? (y/n): ")
|
|
137
|
+
|
|
138
|
+
return response.strip().lower() in ['y', 'yes', 'approve']
|
|
139
|
+
except (KeyboardInterrupt, EOFError):
|
|
140
|
+
return False
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def create_exit_plan_mode_tool(state_manager):
|
|
144
|
+
"""
|
|
145
|
+
Factory function to create exit_plan_mode tool with the correct state manager.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
state_manager: The StateManager instance to use
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Callable: The exit_plan_mode function bound to the provided state manager
|
|
152
|
+
"""
|
|
153
|
+
async def exit_plan_mode(
|
|
154
|
+
plan_title: str,
|
|
155
|
+
overview: str,
|
|
156
|
+
implementation_steps: List[str],
|
|
157
|
+
files_to_modify: List[str] = None,
|
|
158
|
+
files_to_create: List[str] = None,
|
|
159
|
+
risks_and_considerations: List[str] = None,
|
|
160
|
+
testing_approach: str = None,
|
|
161
|
+
success_criteria: List[str] = None,
|
|
162
|
+
) -> str:
|
|
163
|
+
"""
|
|
164
|
+
Present implementation plan and exit plan mode.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
plan_title: Brief title for the implementation plan
|
|
168
|
+
overview: High-level overview of the changes needed
|
|
169
|
+
implementation_steps: Ordered list of implementation steps
|
|
170
|
+
files_to_modify: List of files that need to be modified
|
|
171
|
+
files_to_create: List of new files to be created
|
|
172
|
+
risks_and_considerations: Potential risks or important considerations
|
|
173
|
+
testing_approach: Approach for testing the implementation
|
|
174
|
+
success_criteria: Criteria for considering the implementation successful
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
str: Result message indicating plan approval status
|
|
178
|
+
"""
|
|
179
|
+
tool = ExitPlanModeTool(state_manager=state_manager)
|
|
180
|
+
return await tool._execute(
|
|
181
|
+
plan_title=plan_title,
|
|
182
|
+
overview=overview,
|
|
183
|
+
implementation_steps=implementation_steps,
|
|
184
|
+
files_to_modify=files_to_modify,
|
|
185
|
+
files_to_create=files_to_create,
|
|
186
|
+
risks_and_considerations=risks_and_considerations,
|
|
187
|
+
testing_approach=testing_approach,
|
|
188
|
+
success_criteria=success_criteria,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
return exit_plan_mode
|