tunacode-cli 0.0.54__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/__init__.py +2 -0
- tunacode/cli/commands/implementations/plan.py +50 -0
- tunacode/cli/commands/registry.py +7 -1
- tunacode/cli/repl.py +358 -8
- tunacode/cli/repl_components/output_display.py +18 -1
- tunacode/cli/repl_components/tool_executor.py +15 -4
- tunacode/constants.py +4 -2
- tunacode/core/agents/agent_components/__init__.py +20 -0
- tunacode/core/agents/agent_components/agent_config.py +134 -7
- tunacode/core/agents/agent_components/agent_helpers.py +219 -0
- tunacode/core/agents/agent_components/node_processor.py +82 -115
- tunacode/core/agents/agent_components/truncation_checker.py +81 -0
- tunacode/core/agents/main.py +86 -312
- tunacode/core/state.py +51 -3
- tunacode/core/tool_handler.py +20 -0
- tunacode/prompts/system.md +5 -4
- tunacode/tools/exit_plan_mode.py +191 -0
- tunacode/tools/grep.py +12 -1
- tunacode/tools/present_plan.py +208 -0
- tunacode/types.py +57 -0
- tunacode/ui/console.py +2 -0
- tunacode/ui/input.py +13 -2
- tunacode/ui/keybindings.py +26 -38
- tunacode/ui/output.py +39 -4
- tunacode/ui/panels.py +79 -2
- tunacode/ui/prompt_manager.py +19 -2
- tunacode/ui/tool_descriptions.py +115 -0
- tunacode/ui/tool_ui.py +3 -2
- tunacode/utils/message_utils.py +14 -4
- {tunacode_cli-0.0.54.dist-info → tunacode_cli-0.0.56.dist-info}/METADATA +4 -3
- {tunacode_cli-0.0.54.dist-info → tunacode_cli-0.0.56.dist-info}/RECORD +35 -29
- {tunacode_cli-0.0.54.dist-info → tunacode_cli-0.0.56.dist-info}/WHEEL +0 -0
- {tunacode_cli-0.0.54.dist-info → tunacode_cli-0.0.56.dist-info}/entry_points.txt +0 -0
- {tunacode_cli-0.0.54.dist-info → tunacode_cli-0.0.56.dist-info}/licenses/LICENSE +0 -0
- {tunacode_cli-0.0.54.dist-info → tunacode_cli-0.0.56.dist-info}/top_level.txt +0 -0
tunacode/core/state.py
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
State management system for session data in TunaCode CLI.
|
|
4
4
|
Handles user preferences, conversation history, and runtime state.
|
|
5
|
+
|
|
6
|
+
CLAUDE_ANCHOR[state-module]: Central state management and session tracking
|
|
5
7
|
"""
|
|
6
8
|
|
|
7
9
|
import uuid
|
|
@@ -13,6 +15,8 @@ from tunacode.types import (
|
|
|
13
15
|
InputSessions,
|
|
14
16
|
MessageHistory,
|
|
15
17
|
ModelName,
|
|
18
|
+
PlanDoc,
|
|
19
|
+
PlanPhase,
|
|
16
20
|
SessionId,
|
|
17
21
|
TodoItem,
|
|
18
22
|
ToolName,
|
|
@@ -27,6 +31,8 @@ if TYPE_CHECKING:
|
|
|
27
31
|
|
|
28
32
|
@dataclass
|
|
29
33
|
class SessionState:
|
|
34
|
+
"""CLAUDE_ANCHOR[session-state]: Core session state container"""
|
|
35
|
+
|
|
30
36
|
user_config: UserConfig = field(default_factory=dict)
|
|
31
37
|
agents: dict[str, Any] = field(
|
|
32
38
|
default_factory=dict
|
|
@@ -44,9 +50,7 @@ class SessionState:
|
|
|
44
50
|
input_sessions: InputSessions = field(default_factory=dict)
|
|
45
51
|
current_task: Optional[Any] = None
|
|
46
52
|
todos: list[TodoItem] = field(default_factory=list)
|
|
47
|
-
#
|
|
48
|
-
esc_press_count: int = 0
|
|
49
|
-
last_esc_time: Optional[float] = None
|
|
53
|
+
# Operation state tracking
|
|
50
54
|
operation_cancelled: bool = False
|
|
51
55
|
# Enhanced tracking for thoughts display
|
|
52
56
|
files_in_context: set[str] = field(default_factory=set)
|
|
@@ -82,6 +86,12 @@ class SessionState:
|
|
|
82
86
|
task_hierarchy: dict[str, Any] = field(default_factory=dict)
|
|
83
87
|
iteration_budgets: dict[str, int] = field(default_factory=dict)
|
|
84
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
|
|
85
95
|
|
|
86
96
|
def update_token_count(self):
|
|
87
97
|
"""Calculates the total token count from messages and files in context."""
|
|
@@ -92,6 +102,8 @@ class SessionState:
|
|
|
92
102
|
|
|
93
103
|
|
|
94
104
|
class StateManager:
|
|
105
|
+
"""CLAUDE_ANCHOR[state-manager]: Main state manager singleton"""
|
|
106
|
+
|
|
95
107
|
def __init__(self):
|
|
96
108
|
self._session = SessionState()
|
|
97
109
|
self._tool_handler: Optional["ToolHandler"] = None
|
|
@@ -163,3 +175,39 @@ class StateManager:
|
|
|
163
175
|
def reset_session(self) -> None:
|
|
164
176
|
"""Reset the session to a fresh state."""
|
|
165
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.
|
tunacode/prompts/system.md
CHANGED
|
@@ -7,10 +7,11 @@ You are **"TunaCode"**, a **senior software developer AI assistant operating ins
|
|
|
7
7
|
Your task is to **execute real actions** via tools and **report observations** after every tool use.
|
|
8
8
|
|
|
9
9
|
**CRITICAL BEHAVIOR RULES:**
|
|
10
|
-
1.
|
|
11
|
-
2.
|
|
12
|
-
3.
|
|
13
|
-
4.
|
|
10
|
+
1. **ALWAYS ANNOUNCE YOUR INTENTIONS FIRST**: Before executing any tools, briefly state what you're about to do (e.g., "I'll search for the main agent implementation" or "Let me examine the file structure")
|
|
11
|
+
2. When you say "Let me..." or "I will..." you MUST execute the corresponding tool in THE SAME RESPONSE
|
|
12
|
+
3. Never describe what you'll do without doing it - ALWAYS execute tools when discussing actions
|
|
13
|
+
4. When a task is COMPLETE, start your response with: TUNACODE_TASK_COMPLETE
|
|
14
|
+
5. If your response is cut off or truncated, you'll be prompted to continue - complete your action
|
|
14
15
|
|
|
15
16
|
You MUST follow these rules:
|
|
16
17
|
|
|
@@ -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
|
tunacode/tools/grep.py
CHANGED
|
@@ -7,6 +7,8 @@ This tool provides sophisticated grep-like functionality with:
|
|
|
7
7
|
- Smart result ranking and deduplication
|
|
8
8
|
- Context-aware output formatting
|
|
9
9
|
- Timeout handling for overly broad patterns (3 second deadline for first match)
|
|
10
|
+
|
|
11
|
+
CLAUDE_ANCHOR[grep-module]: Fast parallel file search with 3-second deadline
|
|
10
12
|
"""
|
|
11
13
|
|
|
12
14
|
import asyncio
|
|
@@ -29,7 +31,10 @@ from tunacode.tools.grep_components.result_formatter import ResultFormatter
|
|
|
29
31
|
|
|
30
32
|
|
|
31
33
|
class ParallelGrep(BaseTool):
|
|
32
|
-
"""Advanced parallel grep tool with multiple search strategies.
|
|
34
|
+
"""Advanced parallel grep tool with multiple search strategies.
|
|
35
|
+
|
|
36
|
+
CLAUDE_ANCHOR[parallel-grep-class]: Main grep implementation with timeout handling
|
|
37
|
+
"""
|
|
33
38
|
|
|
34
39
|
def __init__(self, ui_logger=None):
|
|
35
40
|
super().__init__(ui_logger)
|
|
@@ -123,7 +128,13 @@ class ParallelGrep(BaseTool):
|
|
|
123
128
|
# 4️⃣ Execute chosen strategy with pre-filtered candidates
|
|
124
129
|
# Execute search with pre-filtered candidates
|
|
125
130
|
if search_type == "ripgrep":
|
|
131
|
+
# Try ripgrep first for performance. If ripgrep is unavailable or
|
|
132
|
+
# returns no results (e.g., binary missing), gracefully fallback to
|
|
133
|
+
# the Python implementation so the tool still returns matches.
|
|
126
134
|
results = await self._ripgrep_search_filtered(pattern, candidates, config)
|
|
135
|
+
if not results:
|
|
136
|
+
# Fallback to python search when ripgrep produced no output
|
|
137
|
+
results = await self._python_search_filtered(pattern, candidates, config)
|
|
127
138
|
elif search_type == "python":
|
|
128
139
|
results = await self._python_search_filtered(pattern, candidates, config)
|
|
129
140
|
elif search_type == "hybrid":
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""Tool for presenting a structured plan and exiting plan mode."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
|
|
5
|
+
from tunacode.tools.base import BaseTool
|
|
6
|
+
from tunacode.ui import console as ui
|
|
7
|
+
from tunacode.types import ToolResult, PlanDoc, PlanPhase
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PresentPlanTool(BaseTool):
|
|
11
|
+
"""Present a structured implementation plan and request user approval."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, state_manager, ui_logger=None):
|
|
14
|
+
"""Initialize the present plan 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 "present_plan"
|
|
26
|
+
|
|
27
|
+
async def _execute(
|
|
28
|
+
self,
|
|
29
|
+
title: str,
|
|
30
|
+
overview: str,
|
|
31
|
+
steps: List[str],
|
|
32
|
+
files_to_modify: List[str] = None,
|
|
33
|
+
files_to_create: List[str] = None,
|
|
34
|
+
risks: List[str] = None,
|
|
35
|
+
tests: List[str] = None,
|
|
36
|
+
rollback: Optional[str] = None,
|
|
37
|
+
open_questions: List[str] = None,
|
|
38
|
+
success_criteria: List[str] = None,
|
|
39
|
+
references: List[str] = None,
|
|
40
|
+
) -> ToolResult:
|
|
41
|
+
"""Present the implementation plan for user approval."""
|
|
42
|
+
|
|
43
|
+
# Create PlanDoc from parameters
|
|
44
|
+
plan_doc = PlanDoc(
|
|
45
|
+
title=title,
|
|
46
|
+
overview=overview,
|
|
47
|
+
steps=steps,
|
|
48
|
+
files_to_modify=files_to_modify or [],
|
|
49
|
+
files_to_create=files_to_create or [],
|
|
50
|
+
risks=risks or [],
|
|
51
|
+
tests=tests or [],
|
|
52
|
+
rollback=rollback,
|
|
53
|
+
open_questions=open_questions or [],
|
|
54
|
+
success_criteria=success_criteria or [],
|
|
55
|
+
references=references or []
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Validate the plan
|
|
59
|
+
is_valid, missing_sections = plan_doc.validate()
|
|
60
|
+
if not is_valid:
|
|
61
|
+
return f"❌ Plan incomplete. Missing sections: {', '.join(missing_sections)}. Continue researching and refining your plan."
|
|
62
|
+
|
|
63
|
+
# Set plan phase to PLAN_READY and store the plan
|
|
64
|
+
# The REPL will handle displaying the plan when it detects PLAN_READY phase
|
|
65
|
+
self.state_manager.session.plan_phase = PlanPhase.PLAN_READY
|
|
66
|
+
self.state_manager.session.current_plan = plan_doc
|
|
67
|
+
|
|
68
|
+
return "Plan ready for review. The system will now present it to the user for approval."
|
|
69
|
+
|
|
70
|
+
async def _present_plan(self, plan_doc: PlanDoc) -> None:
|
|
71
|
+
"""Present the plan in a formatted way."""
|
|
72
|
+
output = []
|
|
73
|
+
output.append("")
|
|
74
|
+
output.append("╭─────────────────────────────────────────────────────────╮")
|
|
75
|
+
output.append("│ 📋 IMPLEMENTATION PLAN │")
|
|
76
|
+
output.append("╰─────────────────────────────────────────────────────────╯")
|
|
77
|
+
output.append("")
|
|
78
|
+
output.append(f"🎯 **{plan_doc.title}**")
|
|
79
|
+
output.append("")
|
|
80
|
+
|
|
81
|
+
if plan_doc.overview:
|
|
82
|
+
output.append(f"📝 **Overview:** {plan_doc.overview}")
|
|
83
|
+
output.append("")
|
|
84
|
+
|
|
85
|
+
# Files section
|
|
86
|
+
if plan_doc.files_to_modify:
|
|
87
|
+
output.append("📝 **Files to Modify:**")
|
|
88
|
+
for f in plan_doc.files_to_modify:
|
|
89
|
+
output.append(f" • {f}")
|
|
90
|
+
output.append("")
|
|
91
|
+
|
|
92
|
+
if plan_doc.files_to_create:
|
|
93
|
+
output.append("📄 **Files to Create:**")
|
|
94
|
+
for f in plan_doc.files_to_create:
|
|
95
|
+
output.append(f" • {f}")
|
|
96
|
+
output.append("")
|
|
97
|
+
|
|
98
|
+
# Implementation steps
|
|
99
|
+
output.append("🔧 **Implementation Steps:**")
|
|
100
|
+
for i, step in enumerate(plan_doc.steps, 1):
|
|
101
|
+
output.append(f" {i}. {step}")
|
|
102
|
+
output.append("")
|
|
103
|
+
|
|
104
|
+
# Testing approach
|
|
105
|
+
if plan_doc.tests:
|
|
106
|
+
output.append("🧪 **Testing Approach:**")
|
|
107
|
+
for test in plan_doc.tests:
|
|
108
|
+
output.append(f" • {test}")
|
|
109
|
+
output.append("")
|
|
110
|
+
|
|
111
|
+
# Success criteria
|
|
112
|
+
if plan_doc.success_criteria:
|
|
113
|
+
output.append("✅ **Success Criteria:**")
|
|
114
|
+
for criteria in plan_doc.success_criteria:
|
|
115
|
+
output.append(f" • {criteria}")
|
|
116
|
+
output.append("")
|
|
117
|
+
|
|
118
|
+
# Risks and considerations
|
|
119
|
+
if plan_doc.risks:
|
|
120
|
+
output.append("⚠️ **Risks & Considerations:**")
|
|
121
|
+
for risk in plan_doc.risks:
|
|
122
|
+
output.append(f" • {risk}")
|
|
123
|
+
output.append("")
|
|
124
|
+
|
|
125
|
+
# Open questions
|
|
126
|
+
if plan_doc.open_questions:
|
|
127
|
+
output.append("❓ **Open Questions:**")
|
|
128
|
+
for question in plan_doc.open_questions:
|
|
129
|
+
output.append(f" • {question}")
|
|
130
|
+
output.append("")
|
|
131
|
+
|
|
132
|
+
# References
|
|
133
|
+
if plan_doc.references:
|
|
134
|
+
output.append("📚 **References:**")
|
|
135
|
+
for ref in plan_doc.references:
|
|
136
|
+
output.append(f" • {ref}")
|
|
137
|
+
output.append("")
|
|
138
|
+
|
|
139
|
+
# Rollback plan
|
|
140
|
+
if plan_doc.rollback:
|
|
141
|
+
output.append(f"🔄 **Rollback Plan:** {plan_doc.rollback}")
|
|
142
|
+
output.append("")
|
|
143
|
+
|
|
144
|
+
# Print everything at once
|
|
145
|
+
await ui.info("\n".join(output))
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def create_present_plan_tool(state_manager):
|
|
149
|
+
"""
|
|
150
|
+
Factory function to create present_plan tool with the correct state manager.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
state_manager: The StateManager instance to use
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
Callable: The present_plan function bound to the provided state manager
|
|
157
|
+
"""
|
|
158
|
+
async def present_plan(
|
|
159
|
+
title: str,
|
|
160
|
+
overview: str,
|
|
161
|
+
steps: List[str],
|
|
162
|
+
files_to_modify: List[str] = None,
|
|
163
|
+
files_to_create: List[str] = None,
|
|
164
|
+
risks: List[str] = None,
|
|
165
|
+
tests: List[str] = None,
|
|
166
|
+
rollback: Optional[str] = None,
|
|
167
|
+
open_questions: List[str] = None,
|
|
168
|
+
success_criteria: List[str] = None,
|
|
169
|
+
references: List[str] = None,
|
|
170
|
+
) -> str:
|
|
171
|
+
"""
|
|
172
|
+
Present a structured implementation plan for user approval.
|
|
173
|
+
|
|
174
|
+
This tool should ONLY be called when you have a complete, well-researched plan.
|
|
175
|
+
All required sections must be filled out before calling this tool.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
title: Brief, descriptive title for the implementation plan
|
|
179
|
+
overview: High-level summary of what needs to be implemented and why
|
|
180
|
+
steps: Ordered list of specific implementation steps (required)
|
|
181
|
+
files_to_modify: List of existing files that need to be modified
|
|
182
|
+
files_to_create: List of new files that need to be created
|
|
183
|
+
risks: Potential risks, challenges, or considerations
|
|
184
|
+
tests: Testing approach and test cases to validate implementation
|
|
185
|
+
rollback: Plan for reverting changes if needed
|
|
186
|
+
open_questions: Any remaining questions or uncertainties
|
|
187
|
+
success_criteria: Specific criteria for considering the task complete
|
|
188
|
+
references: External resources, documentation, or research sources
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
str: Status message about plan presentation
|
|
192
|
+
"""
|
|
193
|
+
tool = PresentPlanTool(state_manager=state_manager)
|
|
194
|
+
return await tool._execute(
|
|
195
|
+
title=title,
|
|
196
|
+
overview=overview,
|
|
197
|
+
steps=steps,
|
|
198
|
+
files_to_modify=files_to_modify,
|
|
199
|
+
files_to_create=files_to_create,
|
|
200
|
+
risks=risks,
|
|
201
|
+
tests=tests,
|
|
202
|
+
rollback=rollback,
|
|
203
|
+
open_questions=open_questions,
|
|
204
|
+
success_criteria=success_criteria,
|
|
205
|
+
references=references,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
return present_plan
|
tunacode/types.py
CHANGED
|
@@ -21,6 +21,9 @@ from typing import (
|
|
|
21
21
|
Union,
|
|
22
22
|
)
|
|
23
23
|
|
|
24
|
+
# Plan types will be defined below
|
|
25
|
+
from enum import Enum
|
|
26
|
+
|
|
24
27
|
# Try to import pydantic-ai types if available
|
|
25
28
|
try:
|
|
26
29
|
from pydantic_ai import Agent
|
|
@@ -192,6 +195,60 @@ class SimpleResult:
|
|
|
192
195
|
output: str
|
|
193
196
|
|
|
194
197
|
|
|
198
|
+
# =============================================================================
|
|
199
|
+
# Plan Types
|
|
200
|
+
# =============================================================================
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class PlanPhase(Enum):
|
|
204
|
+
"""Plan Mode phases."""
|
|
205
|
+
PLANNING_RESEARCH = "research"
|
|
206
|
+
PLANNING_DRAFT = "draft"
|
|
207
|
+
PLAN_READY = "ready"
|
|
208
|
+
REVIEW_DECISION = "review"
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@dataclass
|
|
212
|
+
class PlanDoc:
|
|
213
|
+
"""Structured plan document with all required sections."""
|
|
214
|
+
|
|
215
|
+
# Required sections
|
|
216
|
+
title: str
|
|
217
|
+
overview: str
|
|
218
|
+
steps: List[str]
|
|
219
|
+
files_to_modify: List[str]
|
|
220
|
+
files_to_create: List[str]
|
|
221
|
+
|
|
222
|
+
# Optional but recommended sections
|
|
223
|
+
risks: List[str] = field(default_factory=list)
|
|
224
|
+
tests: List[str] = field(default_factory=list)
|
|
225
|
+
rollback: Optional[str] = None
|
|
226
|
+
open_questions: List[str] = field(default_factory=list)
|
|
227
|
+
success_criteria: List[str] = field(default_factory=list)
|
|
228
|
+
references: List[str] = field(default_factory=list)
|
|
229
|
+
|
|
230
|
+
def validate(self) -> Tuple[bool, List[str]]:
|
|
231
|
+
"""
|
|
232
|
+
Validate the plan document.
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
tuple: (is_valid, list_of_missing_sections)
|
|
236
|
+
"""
|
|
237
|
+
missing = []
|
|
238
|
+
|
|
239
|
+
# Check required fields
|
|
240
|
+
if not self.title or not self.title.strip():
|
|
241
|
+
missing.append("title")
|
|
242
|
+
if not self.overview or not self.overview.strip():
|
|
243
|
+
missing.append("overview")
|
|
244
|
+
if not self.steps:
|
|
245
|
+
missing.append("steps")
|
|
246
|
+
if not self.files_to_modify and not self.files_to_create:
|
|
247
|
+
missing.append("files_to_modify or files_to_create")
|
|
248
|
+
|
|
249
|
+
return len(missing) == 0, missing
|
|
250
|
+
|
|
251
|
+
|
|
195
252
|
# =============================================================================
|
|
196
253
|
# Session and State Types
|
|
197
254
|
# =============================================================================
|
tunacode/ui/console.py
CHANGED
|
@@ -21,6 +21,7 @@ from .output import (
|
|
|
21
21
|
spinner,
|
|
22
22
|
sync_print,
|
|
23
23
|
update_available,
|
|
24
|
+
update_spinner_message,
|
|
24
25
|
usage,
|
|
25
26
|
version,
|
|
26
27
|
)
|
|
@@ -93,6 +94,7 @@ __all__ = [
|
|
|
93
94
|
"spinner",
|
|
94
95
|
"sync_print",
|
|
95
96
|
"update_available",
|
|
97
|
+
"update_spinner_message",
|
|
96
98
|
"usage",
|
|
97
99
|
"version",
|
|
98
100
|
# Unified logging wrappers
|