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
|
@@ -7,6 +7,8 @@ This package provides a modular command system with:
|
|
|
7
7
|
|
|
8
8
|
The main public API provides backward compatibility with the original
|
|
9
9
|
commands.py module while enabling better organization and maintainability.
|
|
10
|
+
|
|
11
|
+
CLAUDE_ANCHOR[commands-module]: Command registry and dispatch system
|
|
10
12
|
"""
|
|
11
13
|
|
|
12
14
|
# Import base classes and infrastructure
|
|
@@ -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")
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
"""Command registry and factory for TunaCode CLI commands.
|
|
1
|
+
"""Command registry and factory for TunaCode CLI commands.
|
|
2
|
+
|
|
3
|
+
CLAUDE_ANCHOR[command-registry]: Central command registration and execution
|
|
4
|
+
"""
|
|
2
5
|
|
|
3
6
|
from dataclasses import dataclass
|
|
4
7
|
from typing import Any, Dict, List, Optional, Type
|
|
@@ -20,6 +23,7 @@ from .implementations.debug import (
|
|
|
20
23
|
)
|
|
21
24
|
from .implementations.development import BranchCommand, InitCommand
|
|
22
25
|
from .implementations.model import ModelCommand
|
|
26
|
+
from .implementations.plan import ExitPlanCommand, PlanCommand
|
|
23
27
|
from .implementations.system import (
|
|
24
28
|
ClearCommand,
|
|
25
29
|
HelpCommand,
|
|
@@ -126,6 +130,8 @@ class CommandRegistry:
|
|
|
126
130
|
InitCommand,
|
|
127
131
|
TemplateCommand,
|
|
128
132
|
TodoCommand,
|
|
133
|
+
PlanCommand, # Add plan command
|
|
134
|
+
ExitPlanCommand, # Add exit plan command
|
|
129
135
|
]
|
|
130
136
|
|
|
131
137
|
# Register all discovered commands
|
tunacode/cli/repl.py
CHANGED
|
@@ -3,6 +3,8 @@ Module: tunacode.cli.repl
|
|
|
3
3
|
|
|
4
4
|
Interactive REPL (Read-Eval-Print Loop) implementation for TunaCode.
|
|
5
5
|
Handles user input, command processing, and agent interaction in an interactive shell.
|
|
6
|
+
|
|
7
|
+
CLAUDE_ANCHOR[repl-module]: Core REPL loop and user interaction handling
|
|
6
8
|
"""
|
|
7
9
|
|
|
8
10
|
# ============================================================================
|
|
@@ -41,7 +43,7 @@ MSG_TOOL_INTERRUPTED = "Tool execution was interrupted"
|
|
|
41
43
|
MSG_REQUEST_CANCELLED = "Request cancelled"
|
|
42
44
|
MSG_SESSION_ENDED = "Session ended. Happy coding!"
|
|
43
45
|
MSG_AGENT_BUSY = "Agent is busy, press Ctrl+C to interrupt."
|
|
44
|
-
|
|
46
|
+
MSG_HIT_ABORT_KEY = "Hit ESC or Ctrl+C again to exit"
|
|
45
47
|
SHELL_ENV_VAR = "SHELL"
|
|
46
48
|
DEFAULT_SHELL = "bash"
|
|
47
49
|
|
|
@@ -52,6 +54,324 @@ logger = logging.getLogger(__name__)
|
|
|
52
54
|
# The _tool_handler function has been moved to repl_components.tool_executor
|
|
53
55
|
|
|
54
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
|
+
|
|
55
375
|
# ============================================================================
|
|
56
376
|
# COMMAND SYSTEM
|
|
57
377
|
# ============================================================================
|
|
@@ -94,7 +414,10 @@ async def _handle_command(command: str, state_manager: StateManager) -> CommandR
|
|
|
94
414
|
|
|
95
415
|
|
|
96
416
|
async def process_request(text: str, state_manager: StateManager, output: bool = True):
|
|
97
|
-
"""Process input using the agent, handling cancellation safely.
|
|
417
|
+
"""Process input using the agent, handling cancellation safely.
|
|
418
|
+
|
|
419
|
+
CLAUDE_ANCHOR[process-request-repl]: REPL's main request processor with error handling
|
|
420
|
+
"""
|
|
98
421
|
import uuid
|
|
99
422
|
|
|
100
423
|
# Generate a unique ID for this request for correlated logging
|
|
@@ -172,6 +495,14 @@ async def process_request(text: str, state_manager: StateManager, output: bool =
|
|
|
172
495
|
await streaming_panel.stop()
|
|
173
496
|
state_manager.session.streaming_panel = None
|
|
174
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)
|
|
175
506
|
else:
|
|
176
507
|
# Use normal agent processing
|
|
177
508
|
res = await agent.process_request(
|
|
@@ -181,6 +512,14 @@ async def process_request(text: str, state_manager: StateManager, output: bool =
|
|
|
181
512
|
tool_callback=tool_callback_with_state,
|
|
182
513
|
)
|
|
183
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
|
+
|
|
184
523
|
if output:
|
|
185
524
|
if state_manager.session.show_thoughts:
|
|
186
525
|
new_msgs = state_manager.session.messages[start_idx:]
|
|
@@ -200,7 +539,7 @@ async def process_request(text: str, state_manager: StateManager, output: bool =
|
|
|
200
539
|
await ui.muted(MSG_REQUEST_COMPLETED)
|
|
201
540
|
else:
|
|
202
541
|
# Use the dedicated function for displaying agent output
|
|
203
|
-
await display_agent_output(res, enable_streaming)
|
|
542
|
+
await display_agent_output(res, enable_streaming, state_manager)
|
|
204
543
|
|
|
205
544
|
# Always show files in context after agent response
|
|
206
545
|
if state_manager.session.files_in_context:
|
|
@@ -245,7 +584,8 @@ async def process_request(text: str, state_manager: StateManager, output: bool =
|
|
|
245
584
|
async def repl(state_manager: StateManager):
|
|
246
585
|
"""Main REPL loop that handles user interaction and input processing."""
|
|
247
586
|
action = None
|
|
248
|
-
|
|
587
|
+
abort_pressed = False
|
|
588
|
+
last_abort_time = 0.0
|
|
249
589
|
|
|
250
590
|
model_name = state_manager.session.current_model
|
|
251
591
|
max_tokens = (
|
|
@@ -270,16 +610,26 @@ async def repl(state_manager: StateManager):
|
|
|
270
610
|
try:
|
|
271
611
|
line = await ui.multiline_input(state_manager, _command_registry)
|
|
272
612
|
except UserAbortError:
|
|
273
|
-
|
|
613
|
+
import time
|
|
614
|
+
|
|
615
|
+
current_time = time.time()
|
|
616
|
+
|
|
617
|
+
# Reset if more than 3 seconds have passed
|
|
618
|
+
if current_time - last_abort_time > 3.0:
|
|
619
|
+
abort_pressed = False
|
|
620
|
+
|
|
621
|
+
if abort_pressed:
|
|
274
622
|
break
|
|
275
|
-
|
|
276
|
-
|
|
623
|
+
|
|
624
|
+
abort_pressed = True
|
|
625
|
+
last_abort_time = current_time
|
|
626
|
+
await ui.warning(MSG_HIT_ABORT_KEY)
|
|
277
627
|
continue
|
|
278
628
|
|
|
279
629
|
if not line:
|
|
280
630
|
continue
|
|
281
631
|
|
|
282
|
-
|
|
632
|
+
abort_pressed = False
|
|
283
633
|
|
|
284
634
|
if line.lower() in ["exit", "quit"]:
|
|
285
635
|
break
|
|
@@ -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)
|
|
@@ -43,8 +43,9 @@ async def tool_handler(part, state_manager: StateManager):
|
|
|
43
43
|
if tool_handler_instance.should_confirm(part.tool_name):
|
|
44
44
|
await ui.info(f"Tool({part.tool_name})")
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
# Keep spinner running during tool execution - it will be updated with tool status
|
|
47
|
+
# if not state_manager.session.is_streaming_active and state_manager.session.spinner:
|
|
48
|
+
# state_manager.session.spinner.stop()
|
|
48
49
|
|
|
49
50
|
streaming_panel = None
|
|
50
51
|
if state_manager.session.is_streaming_active and hasattr(
|
|
@@ -58,6 +59,15 @@ async def tool_handler(part, state_manager: StateManager):
|
|
|
58
59
|
args = parse_args(part.args)
|
|
59
60
|
|
|
60
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
|
+
|
|
61
71
|
if not tool_handler_instance.should_confirm(part.tool_name):
|
|
62
72
|
return False
|
|
63
73
|
request = tool_handler_instance.create_confirmation_request(part.tool_name, args)
|
|
@@ -80,5 +90,6 @@ async def tool_handler(part, state_manager: StateManager):
|
|
|
80
90
|
if streaming_panel and tool_handler_instance.should_confirm(part.tool_name):
|
|
81
91
|
await streaming_panel.start()
|
|
82
92
|
|
|
83
|
-
|
|
84
|
-
|
|
93
|
+
# Spinner continues running - no need to restart
|
|
94
|
+
# if not state_manager.session.is_streaming_active and state_manager.session.spinner:
|
|
95
|
+
# state_manager.session.spinner.start()
|
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.
|
|
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
|
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
"""Agent components package for modular agent functionality."""
|
|
2
2
|
|
|
3
3
|
from .agent_config import get_or_create_agent
|
|
4
|
+
from .agent_helpers import (
|
|
5
|
+
create_empty_response_message,
|
|
6
|
+
create_fallback_response,
|
|
7
|
+
create_progress_summary,
|
|
8
|
+
create_user_message,
|
|
9
|
+
format_fallback_output,
|
|
10
|
+
get_recent_tools_context,
|
|
11
|
+
get_tool_description,
|
|
12
|
+
get_tool_summary,
|
|
13
|
+
get_user_prompt_part_class,
|
|
14
|
+
)
|
|
4
15
|
from .json_tool_parser import extract_and_execute_tool_calls, parse_json_tool_calls
|
|
5
16
|
from .message_handler import get_model_messages, patch_tool_messages
|
|
6
17
|
from .node_processor import _process_node
|
|
@@ -24,4 +35,13 @@ __all__ = [
|
|
|
24
35
|
"check_task_completion",
|
|
25
36
|
"ToolBuffer",
|
|
26
37
|
"execute_tools_parallel",
|
|
38
|
+
"create_empty_response_message",
|
|
39
|
+
"create_fallback_response",
|
|
40
|
+
"create_progress_summary",
|
|
41
|
+
"create_user_message",
|
|
42
|
+
"format_fallback_output",
|
|
43
|
+
"get_recent_tools_context",
|
|
44
|
+
"get_tool_description",
|
|
45
|
+
"get_tool_summary",
|
|
46
|
+
"get_user_prompt_part_class",
|
|
27
47
|
]
|