tunacode-cli 0.0.55__py3-none-any.whl → 0.0.57__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 +327 -186
- tunacode/cli/repl_components/command_parser.py +37 -4
- tunacode/cli/repl_components/error_recovery.py +79 -1
- tunacode/cli/repl_components/output_display.py +21 -1
- tunacode/cli/repl_components/tool_executor.py +12 -0
- tunacode/configuration/defaults.py +8 -0
- tunacode/constants.py +10 -2
- tunacode/core/agents/agent_components/agent_config.py +212 -22
- tunacode/core/agents/agent_components/node_processor.py +46 -40
- tunacode/core/code_index.py +83 -29
- tunacode/core/state.py +44 -0
- tunacode/core/token_usage/usage_tracker.py +2 -2
- tunacode/core/tool_handler.py +20 -0
- tunacode/prompts/system.md +117 -490
- tunacode/services/mcp.py +29 -7
- tunacode/tools/base.py +110 -0
- tunacode/tools/bash.py +96 -1
- tunacode/tools/exit_plan_mode.py +273 -0
- tunacode/tools/glob.py +366 -33
- tunacode/tools/grep.py +226 -77
- tunacode/tools/grep_components/result_formatter.py +98 -4
- tunacode/tools/list_dir.py +132 -2
- tunacode/tools/present_plan.py +288 -0
- tunacode/tools/read_file.py +91 -0
- tunacode/tools/run_command.py +99 -0
- tunacode/tools/schema_assembler.py +167 -0
- tunacode/tools/todo.py +108 -1
- tunacode/tools/update_file.py +94 -0
- tunacode/tools/write_file.py +86 -0
- tunacode/types.py +58 -0
- tunacode/ui/input.py +14 -2
- tunacode/ui/keybindings.py +25 -4
- tunacode/ui/panels.py +53 -8
- tunacode/ui/prompt_manager.py +25 -2
- tunacode/ui/tool_ui.py +3 -2
- tunacode/utils/json_utils.py +206 -0
- tunacode/utils/message_utils.py +14 -4
- tunacode/utils/ripgrep.py +332 -9
- {tunacode_cli-0.0.55.dist-info → tunacode_cli-0.0.57.dist-info}/METADATA +8 -3
- {tunacode_cli-0.0.55.dist-info → tunacode_cli-0.0.57.dist-info}/RECORD +46 -42
- tunacode/tools/read_file_async_poc.py +0 -196
- {tunacode_cli-0.0.55.dist-info → tunacode_cli-0.0.57.dist-info}/WHEEL +0 -0
- {tunacode_cli-0.0.55.dist-info → tunacode_cli-0.0.57.dist-info}/entry_points.txt +0 -0
- {tunacode_cli-0.0.55.dist-info → tunacode_cli-0.0.57.dist-info}/licenses/LICENSE +0 -0
- {tunacode_cli-0.0.55.dist-info → tunacode_cli-0.0.57.dist-info}/top_level.txt +0 -0
tunacode/services/mcp.py
CHANGED
|
@@ -7,7 +7,7 @@ Handles MCP server initialization, configuration validation, and client connecti
|
|
|
7
7
|
|
|
8
8
|
import os
|
|
9
9
|
from contextlib import asynccontextmanager
|
|
10
|
-
from typing import TYPE_CHECKING, AsyncIterator, List, Optional, Tuple
|
|
10
|
+
from typing import TYPE_CHECKING, AsyncIterator, Dict, List, Optional, Tuple
|
|
11
11
|
|
|
12
12
|
from pydantic_ai.mcp import MCPServerStdio
|
|
13
13
|
|
|
@@ -19,6 +19,10 @@ if TYPE_CHECKING:
|
|
|
19
19
|
|
|
20
20
|
from tunacode.core.state import StateManager
|
|
21
21
|
|
|
22
|
+
# Module-level cache for MCP server instances
|
|
23
|
+
_MCP_SERVER_CACHE: Dict[str, MCPServerStdio] = {}
|
|
24
|
+
_MCP_CONFIG_HASH: Optional[int] = None
|
|
25
|
+
|
|
22
26
|
|
|
23
27
|
class QuietMCPServer(MCPServerStdio):
|
|
24
28
|
"""A version of ``MCPServerStdio`` that suppresses *all* output coming from the
|
|
@@ -55,27 +59,45 @@ class QuietMCPServer(MCPServerStdio):
|
|
|
55
59
|
|
|
56
60
|
|
|
57
61
|
def get_mcp_servers(state_manager: "StateManager") -> List[MCPServerStdio]:
|
|
58
|
-
"""Load MCP servers from configuration.
|
|
62
|
+
"""Load MCP servers from configuration with caching.
|
|
59
63
|
|
|
60
64
|
Args:
|
|
61
65
|
state_manager: The state manager containing user configuration
|
|
62
66
|
|
|
63
67
|
Returns:
|
|
64
|
-
List of MCP server instances
|
|
68
|
+
List of MCP server instances (cached when possible)
|
|
65
69
|
|
|
66
70
|
Raises:
|
|
67
71
|
MCPError: If a server configuration is invalid
|
|
68
72
|
"""
|
|
73
|
+
global _MCP_CONFIG_HASH
|
|
74
|
+
|
|
69
75
|
mcp_servers: MCPServers = state_manager.session.user_config.get("mcpServers", {})
|
|
76
|
+
|
|
77
|
+
# Calculate hash of current config
|
|
78
|
+
current_hash = hash(str(mcp_servers))
|
|
79
|
+
|
|
80
|
+
# Check if config has changed
|
|
81
|
+
if _MCP_CONFIG_HASH == current_hash and _MCP_SERVER_CACHE:
|
|
82
|
+
# Return cached servers
|
|
83
|
+
return list(_MCP_SERVER_CACHE.values())
|
|
84
|
+
|
|
85
|
+
# Config changed or first load - clear cache and rebuild
|
|
86
|
+
_MCP_SERVER_CACHE.clear()
|
|
87
|
+
_MCP_CONFIG_HASH = current_hash
|
|
88
|
+
|
|
70
89
|
loaded_servers: List[MCPServerStdio] = []
|
|
71
90
|
MCPServerStdio.log_level = "critical"
|
|
72
91
|
|
|
73
92
|
for server_name, conf in mcp_servers.items():
|
|
74
93
|
try:
|
|
75
|
-
#
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
94
|
+
# Check if this server is already cached
|
|
95
|
+
if server_name not in _MCP_SERVER_CACHE:
|
|
96
|
+
# Create new instance
|
|
97
|
+
mcp_instance = MCPServerStdio(**conf)
|
|
98
|
+
_MCP_SERVER_CACHE[server_name] = mcp_instance
|
|
99
|
+
|
|
100
|
+
loaded_servers.append(_MCP_SERVER_CACHE[server_name])
|
|
79
101
|
except Exception as e:
|
|
80
102
|
raise MCPError(
|
|
81
103
|
server_name=server_name,
|
tunacode/tools/base.py
CHANGED
|
@@ -5,6 +5,7 @@ for all tools including error handling, UI logging, and ModelRetry support.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from abc import ABC, abstractmethod
|
|
8
|
+
from typing import Any, Dict, Optional
|
|
8
9
|
|
|
9
10
|
from pydantic_ai.exceptions import ModelRetry
|
|
10
11
|
|
|
@@ -24,6 +25,8 @@ class BaseTool(ABC):
|
|
|
24
25
|
"""
|
|
25
26
|
self.ui = ui_logger
|
|
26
27
|
self.logger = get_logger(self.__class__.__name__)
|
|
28
|
+
self._prompt_cache: Optional[str] = None
|
|
29
|
+
self._context: Dict[str, Any] = {}
|
|
27
30
|
|
|
28
31
|
async def execute(self, *args, **kwargs) -> ToolResult:
|
|
29
32
|
"""Execute the tool with error handling and logging.
|
|
@@ -138,6 +141,113 @@ class BaseTool(ABC):
|
|
|
138
141
|
"""
|
|
139
142
|
return f"in {self.tool_name}"
|
|
140
143
|
|
|
144
|
+
def prompt(self, context: Optional[Dict[str, Any]] = None) -> str:
|
|
145
|
+
"""Generate the prompt for this tool.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
context: Optional context including model, permissions, environment
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
str: The generated prompt for this tool
|
|
152
|
+
"""
|
|
153
|
+
# Update context if provided
|
|
154
|
+
if context:
|
|
155
|
+
self._context.update(context)
|
|
156
|
+
|
|
157
|
+
# Check cache if context hasn't changed
|
|
158
|
+
cache_key = str(sorted(self._context.items()))
|
|
159
|
+
if self._prompt_cache and cache_key == getattr(self, "_cache_key", None):
|
|
160
|
+
return self._prompt_cache
|
|
161
|
+
|
|
162
|
+
# Generate new prompt
|
|
163
|
+
prompt = self._generate_prompt()
|
|
164
|
+
|
|
165
|
+
# Cache the result
|
|
166
|
+
self._prompt_cache = prompt
|
|
167
|
+
self._cache_key = cache_key
|
|
168
|
+
|
|
169
|
+
return prompt
|
|
170
|
+
|
|
171
|
+
def _generate_prompt(self) -> str:
|
|
172
|
+
"""Generate the actual prompt based on current context.
|
|
173
|
+
|
|
174
|
+
Override this method in subclasses to provide tool-specific prompts.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
str: The generated prompt
|
|
178
|
+
"""
|
|
179
|
+
# Default prompt generation
|
|
180
|
+
base_prompt = self._get_base_prompt()
|
|
181
|
+
|
|
182
|
+
# Apply model-specific adjustments
|
|
183
|
+
if "model" in self._context:
|
|
184
|
+
base_prompt = self._adjust_for_model(base_prompt, self._context["model"])
|
|
185
|
+
|
|
186
|
+
# Apply permission-specific adjustments
|
|
187
|
+
if "permissions" in self._context:
|
|
188
|
+
base_prompt = self._adjust_for_permissions(base_prompt, self._context["permissions"])
|
|
189
|
+
|
|
190
|
+
return base_prompt
|
|
191
|
+
|
|
192
|
+
def _get_base_prompt(self) -> str:
|
|
193
|
+
"""Get the base prompt for this tool.
|
|
194
|
+
|
|
195
|
+
Override this in subclasses to provide tool-specific base prompts.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
str: The base prompt template
|
|
199
|
+
"""
|
|
200
|
+
return f"Execute the {self.tool_name} tool to perform its designated operation."
|
|
201
|
+
|
|
202
|
+
def _adjust_for_model(self, prompt: str, model: str) -> str:
|
|
203
|
+
"""Adjust prompt based on the model being used.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
prompt: The base prompt
|
|
207
|
+
model: The model identifier
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
str: Adjusted prompt
|
|
211
|
+
"""
|
|
212
|
+
# Default implementation - override in subclasses for specific adjustments
|
|
213
|
+
return prompt
|
|
214
|
+
|
|
215
|
+
def _adjust_for_permissions(self, prompt: str, permissions: Dict[str, Any]) -> str:
|
|
216
|
+
"""Adjust prompt based on permissions.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
prompt: The base prompt
|
|
220
|
+
permissions: Permission settings
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
str: Adjusted prompt
|
|
224
|
+
"""
|
|
225
|
+
# Default implementation - override in subclasses for specific adjustments
|
|
226
|
+
return prompt
|
|
227
|
+
|
|
228
|
+
def get_tool_schema(self) -> Dict[str, Any]:
|
|
229
|
+
"""Generate the tool schema for API integration.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
Dict containing the tool schema in OpenAI function format
|
|
233
|
+
"""
|
|
234
|
+
return {
|
|
235
|
+
"name": self.tool_name,
|
|
236
|
+
"description": self.prompt(),
|
|
237
|
+
"parameters": self._get_parameters_schema(),
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
@abstractmethod
|
|
241
|
+
def _get_parameters_schema(self) -> Dict[str, Any]:
|
|
242
|
+
"""Get the parameters schema for this tool.
|
|
243
|
+
|
|
244
|
+
Must be implemented by subclasses.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
Dict containing the JSON schema for tool parameters
|
|
248
|
+
"""
|
|
249
|
+
pass
|
|
250
|
+
|
|
141
251
|
|
|
142
252
|
class FileBasedTool(BaseTool):
|
|
143
253
|
"""Base class for tools that work with files.
|
tunacode/tools/bash.py
CHANGED
|
@@ -7,10 +7,14 @@ environment variables, timeouts, and improved output handling.
|
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
import asyncio
|
|
10
|
+
import logging
|
|
10
11
|
import os
|
|
11
12
|
import subprocess
|
|
12
|
-
from
|
|
13
|
+
from functools import lru_cache
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Dict, List, Optional
|
|
13
16
|
|
|
17
|
+
import defusedxml.ElementTree as ET
|
|
14
18
|
from pydantic_ai.exceptions import ModelRetry
|
|
15
19
|
|
|
16
20
|
from tunacode.constants import MAX_COMMAND_OUTPUT
|
|
@@ -18,6 +22,8 @@ from tunacode.exceptions import ToolExecutionError
|
|
|
18
22
|
from tunacode.tools.base import BaseTool
|
|
19
23
|
from tunacode.types import ToolResult
|
|
20
24
|
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
21
27
|
|
|
22
28
|
class BashTool(BaseTool):
|
|
23
29
|
"""Enhanced shell command execution tool with advanced features."""
|
|
@@ -26,6 +32,95 @@ class BashTool(BaseTool):
|
|
|
26
32
|
def tool_name(self) -> str:
|
|
27
33
|
return "Bash"
|
|
28
34
|
|
|
35
|
+
@lru_cache(maxsize=1)
|
|
36
|
+
def _get_base_prompt(self) -> str:
|
|
37
|
+
"""Load and return the base prompt from XML file.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
str: The loaded prompt from XML or a default prompt
|
|
41
|
+
"""
|
|
42
|
+
try:
|
|
43
|
+
# Load prompt from XML file
|
|
44
|
+
prompt_file = Path(__file__).parent / "prompts" / "bash_prompt.xml"
|
|
45
|
+
if prompt_file.exists():
|
|
46
|
+
tree = ET.parse(prompt_file)
|
|
47
|
+
root = tree.getroot()
|
|
48
|
+
description = root.find("description")
|
|
49
|
+
if description is not None:
|
|
50
|
+
return description.text.strip()
|
|
51
|
+
except Exception as e:
|
|
52
|
+
logger.warning(f"Failed to load XML prompt for bash: {e}")
|
|
53
|
+
|
|
54
|
+
# Fallback to default prompt
|
|
55
|
+
return (
|
|
56
|
+
"""Executes a given bash command in a persistent shell session with optional timeout"""
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
@lru_cache(maxsize=1)
|
|
60
|
+
def _get_parameters_schema(self) -> Dict[str, Any]:
|
|
61
|
+
"""Get the parameters schema for bash tool.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Dict containing the JSON schema for tool parameters
|
|
65
|
+
"""
|
|
66
|
+
# Try to load from XML first
|
|
67
|
+
try:
|
|
68
|
+
prompt_file = Path(__file__).parent / "prompts" / "bash_prompt.xml"
|
|
69
|
+
if prompt_file.exists():
|
|
70
|
+
tree = ET.parse(prompt_file)
|
|
71
|
+
root = tree.getroot()
|
|
72
|
+
parameters = root.find("parameters")
|
|
73
|
+
if parameters is not None:
|
|
74
|
+
schema: Dict[str, Any] = {"type": "object", "properties": {}, "required": []}
|
|
75
|
+
required_fields: List[str] = []
|
|
76
|
+
|
|
77
|
+
for param in parameters.findall("parameter"):
|
|
78
|
+
name = param.get("name")
|
|
79
|
+
required = param.get("required", "false").lower() == "true"
|
|
80
|
+
param_type = param.find("type")
|
|
81
|
+
description = param.find("description")
|
|
82
|
+
|
|
83
|
+
if name and param_type is not None:
|
|
84
|
+
prop = {
|
|
85
|
+
"type": param_type.text.strip(),
|
|
86
|
+
"description": description.text.strip()
|
|
87
|
+
if description is not None
|
|
88
|
+
else "",
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
schema["properties"][name] = prop
|
|
92
|
+
if required:
|
|
93
|
+
required_fields.append(name)
|
|
94
|
+
|
|
95
|
+
schema["required"] = required_fields
|
|
96
|
+
return schema
|
|
97
|
+
except Exception as e:
|
|
98
|
+
logger.warning(f"Failed to load parameters from XML for bash: {e}")
|
|
99
|
+
|
|
100
|
+
# Fallback to hardcoded schema
|
|
101
|
+
return {
|
|
102
|
+
"type": "object",
|
|
103
|
+
"properties": {
|
|
104
|
+
"command": {
|
|
105
|
+
"type": "string",
|
|
106
|
+
"description": "The command to execute",
|
|
107
|
+
},
|
|
108
|
+
"description": {
|
|
109
|
+
"type": "string",
|
|
110
|
+
"description": "Clear, concise description of what this command does",
|
|
111
|
+
},
|
|
112
|
+
"timeout": {
|
|
113
|
+
"type": "number",
|
|
114
|
+
"description": "Optional timeout in milliseconds",
|
|
115
|
+
},
|
|
116
|
+
"run_in_background": {
|
|
117
|
+
"type": "boolean",
|
|
118
|
+
"description": "Set to true to run this command in the background",
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
"required": ["command"],
|
|
122
|
+
}
|
|
123
|
+
|
|
29
124
|
async def _execute(
|
|
30
125
|
self,
|
|
31
126
|
command: str,
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""Tool for exiting plan mode and presenting implementation plan."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Dict, List
|
|
6
|
+
|
|
7
|
+
import defusedxml.ElementTree as ET
|
|
8
|
+
|
|
9
|
+
from tunacode.tools.base import BaseTool
|
|
10
|
+
from tunacode.types import ToolResult
|
|
11
|
+
from tunacode.ui import console as ui
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ExitPlanModeTool(BaseTool):
|
|
17
|
+
"""Present implementation plan and exit plan mode."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, state_manager, ui_logger=None):
|
|
20
|
+
"""Initialize the exit plan mode tool.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
state_manager: StateManager instance for controlling plan mode state
|
|
24
|
+
ui_logger: UI logger instance for displaying messages
|
|
25
|
+
"""
|
|
26
|
+
super().__init__(ui_logger)
|
|
27
|
+
self.state_manager = state_manager
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def tool_name(self) -> str:
|
|
31
|
+
return "exit_plan_mode"
|
|
32
|
+
|
|
33
|
+
def _get_base_prompt(self) -> str:
|
|
34
|
+
"""Load and return the base prompt from XML file.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
str: The loaded prompt from XML or a default prompt
|
|
38
|
+
"""
|
|
39
|
+
try:
|
|
40
|
+
# Load prompt from XML file
|
|
41
|
+
prompt_file = Path(__file__).parent / "prompts" / "exit_plan_mode_prompt.xml"
|
|
42
|
+
if prompt_file.exists():
|
|
43
|
+
tree = ET.parse(prompt_file)
|
|
44
|
+
root = tree.getroot()
|
|
45
|
+
description = root.find("description")
|
|
46
|
+
if description is not None:
|
|
47
|
+
return description.text.strip()
|
|
48
|
+
except Exception as e:
|
|
49
|
+
logger.warning(f"Failed to load XML prompt for exit_plan_mode: {e}")
|
|
50
|
+
|
|
51
|
+
# Fallback to default prompt
|
|
52
|
+
return """Use this tool when you have finished presenting your plan and are ready to code"""
|
|
53
|
+
|
|
54
|
+
def _get_parameters_schema(self) -> Dict[str, Any]:
|
|
55
|
+
"""Get the parameters schema for exit_plan_mode tool.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Dict containing the JSON schema for tool parameters
|
|
59
|
+
"""
|
|
60
|
+
# Try to load from XML first
|
|
61
|
+
try:
|
|
62
|
+
prompt_file = Path(__file__).parent / "prompts" / "exit_plan_mode_prompt.xml"
|
|
63
|
+
if prompt_file.exists():
|
|
64
|
+
tree = ET.parse(prompt_file)
|
|
65
|
+
root = tree.getroot()
|
|
66
|
+
parameters = root.find("parameters")
|
|
67
|
+
if parameters is not None:
|
|
68
|
+
schema: Dict[str, Any] = {"type": "object", "properties": {}, "required": []}
|
|
69
|
+
required_fields: List[str] = []
|
|
70
|
+
|
|
71
|
+
for param in parameters.findall("parameter"):
|
|
72
|
+
name = param.get("name")
|
|
73
|
+
required = param.get("required", "false").lower() == "true"
|
|
74
|
+
param_type = param.find("type")
|
|
75
|
+
description = param.find("description")
|
|
76
|
+
|
|
77
|
+
if name and param_type is not None:
|
|
78
|
+
prop = {
|
|
79
|
+
"type": param_type.text.strip(),
|
|
80
|
+
"description": description.text.strip()
|
|
81
|
+
if description is not None
|
|
82
|
+
else "",
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
schema["properties"][name] = prop
|
|
86
|
+
if required:
|
|
87
|
+
required_fields.append(name)
|
|
88
|
+
|
|
89
|
+
schema["required"] = required_fields
|
|
90
|
+
return schema
|
|
91
|
+
except Exception as e:
|
|
92
|
+
logger.warning(f"Failed to load parameters from XML for exit_plan_mode: {e}")
|
|
93
|
+
|
|
94
|
+
# Fallback to hardcoded schema
|
|
95
|
+
return {
|
|
96
|
+
"type": "object",
|
|
97
|
+
"properties": {
|
|
98
|
+
"plan": {
|
|
99
|
+
"type": "string",
|
|
100
|
+
"description": "The plan you came up with",
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
"required": ["plan"],
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async def _execute(
|
|
107
|
+
self,
|
|
108
|
+
plan_title: str,
|
|
109
|
+
overview: str,
|
|
110
|
+
implementation_steps: List[str],
|
|
111
|
+
files_to_modify: List[str] = None,
|
|
112
|
+
files_to_create: List[str] = None,
|
|
113
|
+
risks_and_considerations: List[str] = None,
|
|
114
|
+
testing_approach: str = None,
|
|
115
|
+
success_criteria: List[str] = None,
|
|
116
|
+
) -> ToolResult:
|
|
117
|
+
"""Present the implementation plan and get user approval."""
|
|
118
|
+
|
|
119
|
+
plan = {
|
|
120
|
+
"title": plan_title,
|
|
121
|
+
"overview": overview,
|
|
122
|
+
"files_to_modify": files_to_modify or [],
|
|
123
|
+
"files_to_create": files_to_create or [],
|
|
124
|
+
"implementation_steps": implementation_steps,
|
|
125
|
+
"risks_and_considerations": risks_and_considerations or [],
|
|
126
|
+
"testing_approach": testing_approach or "Manual testing of functionality",
|
|
127
|
+
"success_criteria": success_criteria or [],
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
# Present plan to user
|
|
131
|
+
await self._present_plan(plan)
|
|
132
|
+
|
|
133
|
+
# Get user approval
|
|
134
|
+
approved = await self._get_user_approval()
|
|
135
|
+
|
|
136
|
+
# Update state based on user approval
|
|
137
|
+
if approved:
|
|
138
|
+
# Store the plan and exit plan mode
|
|
139
|
+
self.state_manager.set_current_plan(plan)
|
|
140
|
+
self.state_manager.exit_plan_mode(plan)
|
|
141
|
+
await ui.success("✅ Plan approved! Exiting Plan Mode.")
|
|
142
|
+
return "Plan approved and Plan Mode exited. You can now execute the implementation using write tools (write_file, update_file, bash, run_command)."
|
|
143
|
+
else:
|
|
144
|
+
# Keep the plan but stay in plan mode
|
|
145
|
+
self.state_manager.set_current_plan(plan)
|
|
146
|
+
await ui.warning("❌ Plan rejected. Staying in Plan Mode for further research.")
|
|
147
|
+
return "Plan rejected. Continue researching and refine your approach. You remain in Plan Mode - only read-only tools are available."
|
|
148
|
+
|
|
149
|
+
async def _present_plan(self, plan: Dict[str, Any]) -> None:
|
|
150
|
+
"""Present the plan in a formatted way."""
|
|
151
|
+
# Build the entire plan output as a single string to avoid UI flooding
|
|
152
|
+
output = []
|
|
153
|
+
output.append("")
|
|
154
|
+
output.append("╭─────────────────────────────────────────────────────────╮")
|
|
155
|
+
output.append("│ 📋 IMPLEMENTATION PLAN │")
|
|
156
|
+
output.append("╰─────────────────────────────────────────────────────────╯")
|
|
157
|
+
output.append("")
|
|
158
|
+
output.append(f"🎯 {plan['title']}")
|
|
159
|
+
output.append("")
|
|
160
|
+
|
|
161
|
+
if plan["overview"]:
|
|
162
|
+
output.append(f"📝 Overview: {plan['overview']}")
|
|
163
|
+
output.append("")
|
|
164
|
+
|
|
165
|
+
# Files section
|
|
166
|
+
if plan["files_to_modify"]:
|
|
167
|
+
output.append("📝 Files to Modify:")
|
|
168
|
+
for f in plan["files_to_modify"]:
|
|
169
|
+
output.append(f" • {f}")
|
|
170
|
+
output.append("")
|
|
171
|
+
|
|
172
|
+
if plan["files_to_create"]:
|
|
173
|
+
output.append("📄 Files to Create:")
|
|
174
|
+
for f in plan["files_to_create"]:
|
|
175
|
+
output.append(f" • {f}")
|
|
176
|
+
output.append("")
|
|
177
|
+
|
|
178
|
+
# Implementation steps
|
|
179
|
+
output.append("🔧 Implementation Steps:")
|
|
180
|
+
for i, step in enumerate(plan["implementation_steps"], 1):
|
|
181
|
+
output.append(f" {i}. {step}")
|
|
182
|
+
output.append("")
|
|
183
|
+
|
|
184
|
+
# Testing approach
|
|
185
|
+
if plan["testing_approach"]:
|
|
186
|
+
output.append(f"🧪 Testing Approach: {plan['testing_approach']}")
|
|
187
|
+
output.append("")
|
|
188
|
+
|
|
189
|
+
# Success criteria
|
|
190
|
+
if plan["success_criteria"]:
|
|
191
|
+
output.append("✅ Success Criteria:")
|
|
192
|
+
for criteria in plan["success_criteria"]:
|
|
193
|
+
output.append(f" • {criteria}")
|
|
194
|
+
output.append("")
|
|
195
|
+
|
|
196
|
+
# Risks and considerations
|
|
197
|
+
if plan["risks_and_considerations"]:
|
|
198
|
+
output.append("⚠️ Risks & Considerations:")
|
|
199
|
+
for risk in plan["risks_and_considerations"]:
|
|
200
|
+
output.append(f" • {risk}")
|
|
201
|
+
output.append("")
|
|
202
|
+
|
|
203
|
+
# Print everything at once
|
|
204
|
+
await ui.info("\n".join(output))
|
|
205
|
+
|
|
206
|
+
async def _get_user_approval(self) -> bool:
|
|
207
|
+
"""Get user approval for the plan."""
|
|
208
|
+
try:
|
|
209
|
+
from prompt_toolkit import PromptSession
|
|
210
|
+
from prompt_toolkit.patch_stdout import patch_stdout
|
|
211
|
+
|
|
212
|
+
session = PromptSession()
|
|
213
|
+
|
|
214
|
+
with patch_stdout():
|
|
215
|
+
response = await session.prompt_async(
|
|
216
|
+
"\n🤔 Approve this implementation plan? (y/n): "
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
return response.strip().lower() in ["y", "yes", "approve"]
|
|
220
|
+
except (KeyboardInterrupt, EOFError):
|
|
221
|
+
return False
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def create_exit_plan_mode_tool(state_manager):
|
|
225
|
+
"""
|
|
226
|
+
Factory function to create exit_plan_mode tool with the correct state manager.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
state_manager: The StateManager instance to use
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
Callable: The exit_plan_mode function bound to the provided state manager
|
|
233
|
+
"""
|
|
234
|
+
|
|
235
|
+
async def exit_plan_mode(
|
|
236
|
+
plan_title: str,
|
|
237
|
+
overview: str,
|
|
238
|
+
implementation_steps: List[str],
|
|
239
|
+
files_to_modify: List[str] = None,
|
|
240
|
+
files_to_create: List[str] = None,
|
|
241
|
+
risks_and_considerations: List[str] = None,
|
|
242
|
+
testing_approach: str = None,
|
|
243
|
+
success_criteria: List[str] = None,
|
|
244
|
+
) -> str:
|
|
245
|
+
"""
|
|
246
|
+
Present implementation plan and exit plan mode.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
plan_title: Brief title for the implementation plan
|
|
250
|
+
overview: High-level overview of the changes needed
|
|
251
|
+
implementation_steps: Ordered list of implementation steps
|
|
252
|
+
files_to_modify: List of files that need to be modified
|
|
253
|
+
files_to_create: List of new files to be created
|
|
254
|
+
risks_and_considerations: Potential risks or important considerations
|
|
255
|
+
testing_approach: Approach for testing the implementation
|
|
256
|
+
success_criteria: Criteria for considering the implementation successful
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
str: Result message indicating plan approval status
|
|
260
|
+
"""
|
|
261
|
+
tool = ExitPlanModeTool(state_manager=state_manager)
|
|
262
|
+
return await tool._execute(
|
|
263
|
+
plan_title=plan_title,
|
|
264
|
+
overview=overview,
|
|
265
|
+
implementation_steps=implementation_steps,
|
|
266
|
+
files_to_modify=files_to_modify,
|
|
267
|
+
files_to_create=files_to_create,
|
|
268
|
+
risks_and_considerations=risks_and_considerations,
|
|
269
|
+
testing_approach=testing_approach,
|
|
270
|
+
success_criteria=success_criteria,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
return exit_plan_mode
|