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.

@@ -0,0 +1,50 @@
1
+ """Plan mode commands for TunaCode."""
2
+
3
+ from typing import List
4
+
5
+ from ....types import CommandContext
6
+ from ....ui import console as ui
7
+ from ..base import Command, CommandCategory, CommandSpec, SimpleCommand
8
+
9
+
10
+ class PlanCommand(SimpleCommand):
11
+ """Enter plan mode for read-only research and planning."""
12
+
13
+ spec = CommandSpec(
14
+ name="plan",
15
+ aliases=["/plan"],
16
+ description="Enter Plan Mode - read-only research phase",
17
+ category=CommandCategory.DEVELOPMENT,
18
+ )
19
+
20
+ async def execute(self, args: List[str], context: CommandContext) -> None:
21
+ """Enter plan mode."""
22
+ context.state_manager.enter_plan_mode()
23
+
24
+ await ui.info("🔍 Entering Plan Mode")
25
+ await ui.info("• Only read-only operations available")
26
+ await ui.info("• Use tools to research and analyze the codebase")
27
+ await ui.info("• Use 'exit_plan_mode' tool to present your plan")
28
+ await ui.info("• Read-only tools: read_file, grep, list_dir, glob")
29
+ await ui.success("✅ Plan Mode active - indicator will appear above next input")
30
+
31
+
32
+ class ExitPlanCommand(SimpleCommand):
33
+ """Exit plan mode manually."""
34
+
35
+ spec = CommandSpec(
36
+ name="exit-plan",
37
+ aliases=["/exit-plan"],
38
+ description="Exit Plan Mode and return to normal mode",
39
+ category=CommandCategory.DEVELOPMENT,
40
+ )
41
+
42
+ async def execute(self, args: List[str], context: CommandContext) -> None:
43
+ """Exit plan mode manually."""
44
+ if not context.state_manager.is_plan_mode():
45
+ await ui.warning("Not currently in Plan Mode")
46
+ return
47
+
48
+ context.state_manager.exit_plan_mode()
49
+ await ui.success("🚪 Exiting Plan Mode - returning to normal mode")
50
+ await ui.info("✅ All tools are now available - '⏸ PLAN MODE ON' status removed")
@@ -23,6 +23,7 @@ from .implementations.debug import (
23
23
  )
24
24
  from .implementations.development import BranchCommand, InitCommand
25
25
  from .implementations.model import ModelCommand
26
+ from .implementations.plan import ExitPlanCommand, PlanCommand
26
27
  from .implementations.system import (
27
28
  ClearCommand,
28
29
  HelpCommand,
@@ -129,6 +130,8 @@ class CommandRegistry:
129
130
  InitCommand,
130
131
  TemplateCommand,
131
132
  TodoCommand,
133
+ PlanCommand, # Add plan command
134
+ ExitPlanCommand, # Add exit plan command
132
135
  ]
133
136
 
134
137
  # Register all discovered commands
tunacode/cli/repl.py CHANGED
@@ -54,6 +54,324 @@ logger = logging.getLogger(__name__)
54
54
  # The _tool_handler function has been moved to repl_components.tool_executor
55
55
 
56
56
 
57
+ def _transform_to_implementation_request(original_request: str) -> str:
58
+ """
59
+ Transform a planning request into an implementation request.
60
+
61
+ This ensures that after plan approval, the agent understands it should
62
+ implement rather than plan again.
63
+ """
64
+ # Remove plan-related language and add implementation language
65
+ request = original_request.lower()
66
+
67
+ if "plan" in request:
68
+ # Transform "plan a md file" -> "create a md file"
69
+ # Transform "plan to implement" -> "implement"
70
+ request = request.replace("plan a ", "create a ")
71
+ request = request.replace("plan an ", "create an ")
72
+ request = request.replace("plan to ", "")
73
+ request = request.replace("plan ", "create ")
74
+
75
+ # Add clear implementation instruction
76
+ implementation_request = f"{request}\n\nIMPORTANT: Actually implement and create the file(s) - do not just plan or outline. The plan has been approved, now execute the implementation."
77
+
78
+ return implementation_request
79
+
80
+
81
+ async def _display_plan(plan_doc) -> None:
82
+ """Display the plan in a formatted way."""
83
+
84
+ if not plan_doc:
85
+ await ui.error("⚠️ Error: No plan document found to display")
86
+ return
87
+
88
+ output = []
89
+ output.append(f"[bold cyan]🎯 {plan_doc.title}[/bold cyan]")
90
+ output.append("")
91
+
92
+ if plan_doc.overview:
93
+ output.append(f"[bold]📝 Overview:[/bold] {plan_doc.overview}")
94
+ output.append("")
95
+
96
+ # Files section
97
+ if plan_doc.files_to_modify:
98
+ output.append("[bold]📝 Files to Modify:[/bold]")
99
+ for f in plan_doc.files_to_modify:
100
+ output.append(f" • {f}")
101
+ output.append("")
102
+
103
+ if plan_doc.files_to_create:
104
+ output.append("[bold]📄 Files to Create:[/bold]")
105
+ for f in plan_doc.files_to_create:
106
+ output.append(f" • {f}")
107
+ output.append("")
108
+
109
+ # Implementation steps
110
+ output.append("[bold]🔧 Implementation Steps:[/bold]")
111
+ for i, step in enumerate(plan_doc.steps, 1):
112
+ output.append(f" {i}. {step}")
113
+ output.append("")
114
+
115
+ # Testing approach
116
+ if plan_doc.tests:
117
+ output.append("[bold]🧪 Testing Approach:[/bold]")
118
+ for test in plan_doc.tests:
119
+ output.append(f" • {test}")
120
+ output.append("")
121
+
122
+ # Success criteria
123
+ if plan_doc.success_criteria:
124
+ output.append("[bold]✅ Success Criteria:[/bold]")
125
+ for criteria in plan_doc.success_criteria:
126
+ output.append(f" • {criteria}")
127
+ output.append("")
128
+
129
+ # Risks and considerations
130
+ if plan_doc.risks:
131
+ output.append("[bold]⚠️ Risks & Considerations:[/bold]")
132
+ for risk in plan_doc.risks:
133
+ output.append(f" • {risk}")
134
+ output.append("")
135
+
136
+ # Open questions
137
+ if plan_doc.open_questions:
138
+ output.append("[bold]❓ Open Questions:[/bold]")
139
+ for question in plan_doc.open_questions:
140
+ output.append(f" • {question}")
141
+ output.append("")
142
+
143
+ # References
144
+ if plan_doc.references:
145
+ output.append("[bold]📚 References:[/bold]")
146
+ for ref in plan_doc.references:
147
+ output.append(f" • {ref}")
148
+ output.append("")
149
+
150
+ # Rollback plan
151
+ if plan_doc.rollback:
152
+ output.append(f"[bold]🔄 Rollback Plan:[/bold] {plan_doc.rollback}")
153
+ output.append("")
154
+
155
+ # Display the plan in a cyan panel
156
+ await ui.panel("📋 IMPLEMENTATION PLAN", "\n".join(output), border_style="cyan")
157
+
158
+
159
+ async def _detect_and_handle_text_plan(state_manager, agent_response, original_request):
160
+ """
161
+ Detect if agent presented a plan in text format and handle it.
162
+
163
+ This is a fallback for when agents ignore the present_plan tool requirement.
164
+ """
165
+ try:
166
+ # Extract response text
167
+ response_text = ""
168
+ if hasattr(agent_response, 'messages') and agent_response.messages:
169
+ latest_msg = agent_response.messages[-1]
170
+ if hasattr(latest_msg, 'content'):
171
+ response_text = str(latest_msg.content)
172
+ elif hasattr(latest_msg, 'text'):
173
+ response_text = str(latest_msg.text)
174
+ elif hasattr(agent_response, 'result') and hasattr(agent_response.result, 'output'):
175
+ response_text = str(agent_response.result.output)
176
+ else:
177
+ response_text = str(agent_response)
178
+
179
+ # Skip if agent just returned TUNACODE_TASK_COMPLETE or showed present_plan as text
180
+ if "TUNACODE_TASK_COMPLETE" in response_text:
181
+ logger.debug("Agent returned TUNACODE_TASK_COMPLETE instead of calling present_plan")
182
+ await ui.warning("⚠️ Agent failed to call present_plan tool. Please provide clearer instructions to plan the task.")
183
+ return
184
+
185
+ if "present_plan(" in response_text:
186
+ logger.debug("Agent showed present_plan as text instead of executing it")
187
+ await ui.error("❌ Agent showed present_plan as text instead of EXECUTING it as a tool!")
188
+ await ui.info("The agent must EXECUTE the present_plan tool, not show it as code.")
189
+ await ui.info("Try again with: 'Execute the present_plan tool to create a plan for...'")
190
+ return
191
+
192
+ # Check for plan indicators
193
+ plan_indicators = [
194
+ "plan for", "implementation plan", "here's a plan", "i'll create a plan",
195
+ "plan to write", "plan to create", "markdown file", "outline for the",
196
+ "plan title", "overview:", "steps:", "file title and introduction",
197
+ "main functions", "sections to cover", "structure for", "plan overview"
198
+ ]
199
+
200
+ has_plan_indicators = any(indicator in response_text.lower() for indicator in plan_indicators)
201
+
202
+ # Also check for structured content (numbered lists, bullet points, sections)
203
+ has_structure = bool(
204
+ ("1." in response_text or "2." in response_text or "3." in response_text) or
205
+ ("•" in response_text and response_text.count("•") >= 3) or
206
+ ("Title:" in response_text and "Overview:" in response_text)
207
+ )
208
+
209
+ if has_plan_indicators and has_structure:
210
+ # Agent presented a text plan - simulate the approval flow
211
+ await ui.line()
212
+ await ui.info("📋 Plan detected in text format - extracting for review")
213
+
214
+ # Create a simple plan from the text
215
+ from tunacode.types import PlanDoc, PlanPhase
216
+
217
+ # Extract title (simple heuristic)
218
+ title = "TunaCode Functions Overview Markdown File"
219
+ if "title:" in response_text.lower():
220
+ lines = response_text.split("\n")
221
+ for line in lines:
222
+ if "title:" in line.lower():
223
+ title = line.split(":", 1)[1].strip().strip('"')
224
+ break
225
+
226
+ # Create basic plan structure from detected text
227
+ plan_doc = PlanDoc(
228
+ title=title,
229
+ overview="Create a comprehensive markdown file documenting TunaCode's main functions",
230
+ steps=[
231
+ "Draft document structure with sections",
232
+ "Detail each function with descriptions and examples",
233
+ "Add usage guidelines and best practices",
234
+ "Review and finalize content"
235
+ ],
236
+ files_to_modify=[],
237
+ files_to_create=["TunaCode_Functions_Overview.md"],
238
+ success_criteria=["Clear documentation of all main TunaCode functions"]
239
+ )
240
+
241
+ # Set plan ready state and trigger approval
242
+ state_manager.session.plan_phase = PlanPhase.PLAN_READY
243
+ state_manager.session.current_plan = plan_doc
244
+
245
+ await _handle_plan_approval(state_manager, original_request)
246
+
247
+ except Exception as e:
248
+ logger.error(f"Error detecting text plan: {e}")
249
+ # If detection fails, just continue normally
250
+
251
+
252
+ async def _handle_plan_approval(state_manager, original_request=None):
253
+ """
254
+ Handle plan approval when a plan has been presented via present_plan tool.
255
+
256
+ This function:
257
+ 1. Shows the user approval options (approve/modify/reject)
258
+ 2. Handles the user's decision appropriately
259
+ 3. Continues with implementation if approved
260
+ """
261
+ try:
262
+ from tunacode.types import PlanPhase
263
+
264
+ # Exit plan mode and move to review phase
265
+ state_manager.session.plan_phase = PlanPhase.REVIEW_DECISION
266
+ plan_doc = state_manager.session.current_plan
267
+ state_manager.exit_plan_mode(plan_doc)
268
+
269
+ await ui.line()
270
+ await ui.info("📋 Plan has been prepared and Plan Mode exited")
271
+ await ui.line()
272
+
273
+ # Display the plan content now
274
+ await _display_plan(plan_doc)
275
+
276
+ # Display approval options with better styling
277
+ await ui.line()
278
+ # Create content with exactly 45 characters per line for perfect alignment
279
+ content = (
280
+ "[bold cyan]The implementation plan has been presented. [/bold cyan]\n\n"
281
+ "[yellow]Choose your action: [/yellow]\n\n"
282
+ " [bold green]a[/bold green] → Approve and proceed with implementation\n"
283
+ " [bold yellow]m[/bold yellow] → Modify the plan (return to Plan Mode) \n"
284
+ " [bold red]r[/bold red] → Reject and create different approach \n"
285
+ )
286
+ await ui.panel("🎯 Plan Review", content, border_style="cyan")
287
+ await ui.line()
288
+
289
+ # Handle double-escape pattern like main REPL
290
+ from tunacode.ui.keybindings import create_key_bindings
291
+ kb = create_key_bindings(state_manager)
292
+
293
+ while True:
294
+ try:
295
+ response = await ui.input(
296
+ session_key="plan_approval",
297
+ pretext=" → Your choice [a/m/r]: ",
298
+ key_bindings=kb,
299
+ state_manager=state_manager
300
+ )
301
+ response = response.strip().lower()
302
+
303
+ # Reset abort flags on successful input
304
+ state_manager.session.approval_abort_pressed = False
305
+ state_manager.session.approval_last_abort_time = 0.0
306
+ break
307
+
308
+ except UserAbortError:
309
+ import time
310
+ current_time = time.time()
311
+
312
+ # Get current session state
313
+ approval_abort_pressed = getattr(state_manager.session, 'approval_abort_pressed', False)
314
+ approval_last_abort_time = getattr(state_manager.session, 'approval_last_abort_time', 0.0)
315
+
316
+ # Reset if more than 3 seconds have passed
317
+ if current_time - approval_last_abort_time > 3.0:
318
+ approval_abort_pressed = False
319
+ state_manager.session.approval_abort_pressed = False
320
+
321
+ if approval_abort_pressed:
322
+ # Second escape - return to Plan Mode
323
+ await ui.line()
324
+ await ui.info("🔄 Returning to Plan Mode for further planning")
325
+ await ui.line()
326
+ state_manager.enter_plan_mode()
327
+ # Clean up approval flags
328
+ state_manager.session.approval_abort_pressed = False
329
+ state_manager.session.approval_last_abort_time = 0.0
330
+ return
331
+
332
+ # First escape - show warning and continue the loop
333
+ state_manager.session.approval_abort_pressed = True
334
+ state_manager.session.approval_last_abort_time = current_time
335
+ await ui.line()
336
+ await ui.warning("Hit ESC or Ctrl+C again to return to Plan Mode")
337
+ await ui.line()
338
+ continue
339
+
340
+ if response in ['a', 'approve']:
341
+ await ui.line()
342
+ await ui.success("✅ Plan approved - proceeding with implementation")
343
+ state_manager.approve_plan()
344
+ state_manager.session.plan_phase = None
345
+
346
+ # Continue processing the original request now that we're in normal mode
347
+ if original_request:
348
+ await ui.info("🚀 Executing implementation...")
349
+ await ui.line()
350
+ # Transform the original request to make it clear we want implementation, not more planning
351
+ implementation_request = _transform_to_implementation_request(original_request)
352
+ await process_request(implementation_request, state_manager, output=True)
353
+
354
+ elif response in ['m', 'modify']:
355
+ await ui.line()
356
+ await ui.info("📝 Returning to Plan Mode for modifications")
357
+ state_manager.enter_plan_mode()
358
+
359
+ elif response in ['r', 'reject']:
360
+ await ui.line()
361
+ await ui.warning("🔄 Plan rejected - returning to Plan Mode")
362
+ state_manager.enter_plan_mode()
363
+
364
+ else:
365
+ await ui.line()
366
+ await ui.warning("⚠️ Invalid choice - please enter a, m, or r")
367
+ state_manager.session.plan_phase = None
368
+
369
+ except Exception as e:
370
+ logger.error(f"Error in plan approval: {e}")
371
+ # If anything goes wrong, reset plan phase
372
+ state_manager.session.plan_phase = None
373
+
374
+
57
375
  # ============================================================================
58
376
  # COMMAND SYSTEM
59
377
  # ============================================================================
@@ -177,6 +495,14 @@ async def process_request(text: str, state_manager: StateManager, output: bool =
177
495
  await streaming_panel.stop()
178
496
  state_manager.session.streaming_panel = None
179
497
  state_manager.session.is_streaming_active = False
498
+
499
+ # Check if plan is ready for user review OR if agent presented text plan
500
+ from tunacode.types import PlanPhase
501
+ if hasattr(state_manager.session, 'plan_phase') and state_manager.session.plan_phase == PlanPhase.PLAN_READY:
502
+ await _handle_plan_approval(state_manager, text)
503
+ elif state_manager.is_plan_mode() and not getattr(state_manager.session, '_continuing_from_plan', False):
504
+ # Check if agent presented a text plan instead of using the tool
505
+ await _detect_and_handle_text_plan(state_manager, res, text)
180
506
  else:
181
507
  # Use normal agent processing
182
508
  res = await agent.process_request(
@@ -186,6 +512,14 @@ async def process_request(text: str, state_manager: StateManager, output: bool =
186
512
  tool_callback=tool_callback_with_state,
187
513
  )
188
514
 
515
+ # Check if plan is ready for user review OR if agent presented text plan
516
+ from tunacode.types import PlanPhase
517
+ if hasattr(state_manager.session, 'plan_phase') and state_manager.session.plan_phase == PlanPhase.PLAN_READY:
518
+ await _handle_plan_approval(state_manager, text)
519
+ elif state_manager.is_plan_mode() and not getattr(state_manager.session, '_continuing_from_plan', False):
520
+ # Check if agent presented a text plan instead of using the tool
521
+ await _detect_and_handle_text_plan(state_manager, res, text)
522
+
189
523
  if output:
190
524
  if state_manager.session.show_thoughts:
191
525
  new_msgs = state_manager.session.messages[start_idx:]
@@ -205,7 +539,7 @@ async def process_request(text: str, state_manager: StateManager, output: bool =
205
539
  await ui.muted(MSG_REQUEST_COMPLETED)
206
540
  else:
207
541
  # Use the dedicated function for displaying agent output
208
- await display_agent_output(res, enable_streaming)
542
+ await display_agent_output(res, enable_streaming, state_manager)
209
543
 
210
544
  # Always show files in context after agent response
211
545
  if state_manager.session.files_in_context:
@@ -10,7 +10,7 @@ from tunacode.ui import console as ui
10
10
  MSG_REQUEST_COMPLETED = "Request completed"
11
11
 
12
12
 
13
- async def display_agent_output(res, enable_streaming: bool) -> None:
13
+ async def display_agent_output(res, enable_streaming: bool, state_manager=None) -> None:
14
14
  """Display agent output using guard clauses to flatten nested conditionals."""
15
15
  if enable_streaming:
16
16
  return
@@ -29,5 +29,22 @@ async def display_agent_output(res, enable_streaming: bool) -> None:
29
29
 
30
30
  if '"tool_uses"' in output:
31
31
  return
32
+
33
+ # In plan mode, don't display any agent text output at all
34
+ # The plan will be displayed via the present_plan tool
35
+ if state_manager and state_manager.is_plan_mode():
36
+ return
37
+
38
+ # Filter out plan mode system prompts and tool definitions
39
+ if any(phrase in output for phrase in [
40
+ "PLAN MODE - TOOL EXECUTION ONLY",
41
+ "🔧 PLAN MODE",
42
+ "TOOL EXECUTION ONLY 🔧",
43
+ "planning assistant that ONLY communicates",
44
+ "namespace functions {",
45
+ "namespace multi_tool_use {",
46
+ "You are trained on data up to"
47
+ ]):
48
+ return
32
49
 
33
50
  await ui.agent(output)
@@ -59,6 +59,15 @@ async def tool_handler(part, state_manager: StateManager):
59
59
  args = parse_args(part.args)
60
60
 
61
61
  def confirm_func():
62
+ # Check if tool is blocked in plan mode first
63
+ if tool_handler_instance.is_tool_blocked_in_plan_mode(part.tool_name):
64
+ from tunacode.constants import READ_ONLY_TOOLS
65
+ error_msg = (f"🔍 Plan Mode: Tool '{part.tool_name}' is not available in Plan Mode.\n"
66
+ f"Only read-only tools are allowed: {', '.join(READ_ONLY_TOOLS)}\n"
67
+ f"Use 'exit_plan_mode' tool to present your plan and exit Plan Mode.")
68
+ print(f"\n❌ {error_msg}\n")
69
+ return True # Abort the tool
70
+
62
71
  if not tool_handler_instance.should_confirm(part.tool_name):
63
72
  return False
64
73
  request = tool_handler_instance.create_confirmation_request(part.tool_name, args)
tunacode/constants.py CHANGED
@@ -9,7 +9,7 @@ from enum import Enum
9
9
 
10
10
  # Application info
11
11
  APP_NAME = "TunaCode"
12
- APP_VERSION = "0.0.55"
12
+ APP_VERSION = "0.0.56"
13
13
 
14
14
 
15
15
  # File patterns
@@ -44,6 +44,7 @@ class ToolName(str, Enum):
44
44
  LIST_DIR = "list_dir"
45
45
  GLOB = "glob"
46
46
  TODO = "todo"
47
+ EXIT_PLAN_MODE = "exit_plan_mode"
47
48
 
48
49
 
49
50
  # Tool names (backward compatibility)
@@ -56,9 +57,10 @@ TOOL_GREP = ToolName.GREP
56
57
  TOOL_LIST_DIR = ToolName.LIST_DIR
57
58
  TOOL_GLOB = ToolName.GLOB
58
59
  TOOL_TODO = ToolName.TODO
60
+ TOOL_EXIT_PLAN_MODE = ToolName.EXIT_PLAN_MODE
59
61
 
60
62
  # Tool categorization
61
- READ_ONLY_TOOLS = [ToolName.READ_FILE, ToolName.GREP, ToolName.LIST_DIR, ToolName.GLOB]
63
+ READ_ONLY_TOOLS = [ToolName.READ_FILE, ToolName.GREP, ToolName.LIST_DIR, ToolName.GLOB, ToolName.EXIT_PLAN_MODE]
62
64
  WRITE_TOOLS = [ToolName.WRITE_FILE, ToolName.UPDATE_FILE]
63
65
  EXECUTE_TOOLS = [ToolName.BASH, ToolName.RUN_COMMAND]
64
66