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.

Files changed (35) hide show
  1. tunacode/cli/commands/__init__.py +2 -0
  2. tunacode/cli/commands/implementations/plan.py +50 -0
  3. tunacode/cli/commands/registry.py +7 -1
  4. tunacode/cli/repl.py +358 -8
  5. tunacode/cli/repl_components/output_display.py +18 -1
  6. tunacode/cli/repl_components/tool_executor.py +15 -4
  7. tunacode/constants.py +4 -2
  8. tunacode/core/agents/agent_components/__init__.py +20 -0
  9. tunacode/core/agents/agent_components/agent_config.py +134 -7
  10. tunacode/core/agents/agent_components/agent_helpers.py +219 -0
  11. tunacode/core/agents/agent_components/node_processor.py +82 -115
  12. tunacode/core/agents/agent_components/truncation_checker.py +81 -0
  13. tunacode/core/agents/main.py +86 -312
  14. tunacode/core/state.py +51 -3
  15. tunacode/core/tool_handler.py +20 -0
  16. tunacode/prompts/system.md +5 -4
  17. tunacode/tools/exit_plan_mode.py +191 -0
  18. tunacode/tools/grep.py +12 -1
  19. tunacode/tools/present_plan.py +208 -0
  20. tunacode/types.py +57 -0
  21. tunacode/ui/console.py +2 -0
  22. tunacode/ui/input.py +13 -2
  23. tunacode/ui/keybindings.py +26 -38
  24. tunacode/ui/output.py +39 -4
  25. tunacode/ui/panels.py +79 -2
  26. tunacode/ui/prompt_manager.py +19 -2
  27. tunacode/ui/tool_descriptions.py +115 -0
  28. tunacode/ui/tool_ui.py +3 -2
  29. tunacode/utils/message_utils.py +14 -4
  30. {tunacode_cli-0.0.54.dist-info → tunacode_cli-0.0.56.dist-info}/METADATA +4 -3
  31. {tunacode_cli-0.0.54.dist-info → tunacode_cli-0.0.56.dist-info}/RECORD +35 -29
  32. {tunacode_cli-0.0.54.dist-info → tunacode_cli-0.0.56.dist-info}/WHEEL +0 -0
  33. {tunacode_cli-0.0.54.dist-info → tunacode_cli-0.0.56.dist-info}/entry_points.txt +0 -0
  34. {tunacode_cli-0.0.54.dist-info → tunacode_cli-0.0.56.dist-info}/licenses/LICENSE +0 -0
  35. {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
- # ESC key tracking for double-press functionality
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
@@ -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.
@@ -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. When you say "Let me..." or "I will..." you MUST execute the corresponding tool in THE SAME RESPONSE
11
- 2. Never describe what you'll do without doing it - ALWAYS execute tools when discussing actions
12
- 3. When a task is COMPLETE, start your response with: TUNACODE_TASK_COMPLETE
13
- 4. If your response is cut off or truncated, you'll be prompted to continue - complete your action
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