tunacode-cli 0.0.70__py3-none-any.whl → 0.0.78.6__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 +0 -2
- tunacode/cli/commands/implementations/__init__.py +0 -3
- tunacode/cli/commands/implementations/debug.py +2 -2
- tunacode/cli/commands/implementations/development.py +10 -8
- tunacode/cli/commands/implementations/model.py +357 -29
- tunacode/cli/commands/implementations/system.py +3 -2
- tunacode/cli/commands/implementations/template.py +0 -2
- tunacode/cli/commands/registry.py +8 -7
- tunacode/cli/commands/slash/loader.py +2 -1
- tunacode/cli/commands/slash/validator.py +2 -1
- tunacode/cli/main.py +19 -1
- tunacode/cli/repl.py +90 -229
- tunacode/cli/repl_components/command_parser.py +2 -1
- tunacode/cli/repl_components/error_recovery.py +8 -5
- tunacode/cli/repl_components/output_display.py +1 -10
- tunacode/cli/repl_components/tool_executor.py +1 -13
- tunacode/configuration/defaults.py +2 -2
- tunacode/configuration/key_descriptions.py +284 -0
- tunacode/configuration/settings.py +0 -1
- tunacode/constants.py +6 -42
- tunacode/core/agents/__init__.py +43 -2
- tunacode/core/agents/agent_components/__init__.py +7 -0
- tunacode/core/agents/agent_components/agent_config.py +162 -158
- tunacode/core/agents/agent_components/agent_helpers.py +31 -2
- tunacode/core/agents/agent_components/node_processor.py +180 -146
- tunacode/core/agents/agent_components/response_state.py +123 -6
- tunacode/core/agents/agent_components/state_transition.py +116 -0
- tunacode/core/agents/agent_components/streaming.py +296 -0
- tunacode/core/agents/agent_components/task_completion.py +19 -6
- tunacode/core/agents/agent_components/tool_buffer.py +21 -1
- tunacode/core/agents/agent_components/tool_executor.py +10 -0
- tunacode/core/agents/main.py +522 -370
- tunacode/core/agents/main_legact.py +538 -0
- tunacode/core/agents/prompts.py +66 -0
- tunacode/core/agents/utils.py +29 -122
- tunacode/core/setup/__init__.py +0 -2
- tunacode/core/setup/config_setup.py +88 -227
- tunacode/core/setup/config_wizard.py +230 -0
- tunacode/core/setup/coordinator.py +2 -1
- tunacode/core/state.py +16 -64
- tunacode/core/token_usage/usage_tracker.py +3 -1
- tunacode/core/tool_authorization.py +352 -0
- tunacode/core/tool_handler.py +67 -60
- tunacode/prompts/system.xml +751 -0
- tunacode/services/mcp.py +97 -1
- tunacode/setup.py +0 -23
- tunacode/tools/base.py +54 -1
- tunacode/tools/bash.py +14 -0
- tunacode/tools/glob.py +4 -2
- tunacode/tools/grep.py +7 -17
- tunacode/tools/prompts/glob_prompt.xml +1 -1
- tunacode/tools/prompts/grep_prompt.xml +1 -0
- tunacode/tools/prompts/list_dir_prompt.xml +1 -1
- tunacode/tools/prompts/react_prompt.xml +23 -0
- tunacode/tools/prompts/read_file_prompt.xml +1 -1
- tunacode/tools/react.py +153 -0
- tunacode/tools/run_command.py +15 -0
- tunacode/types.py +14 -79
- tunacode/ui/completers.py +434 -50
- tunacode/ui/config_dashboard.py +585 -0
- tunacode/ui/console.py +63 -11
- tunacode/ui/input.py +8 -3
- tunacode/ui/keybindings.py +0 -18
- tunacode/ui/model_selector.py +395 -0
- tunacode/ui/output.py +40 -19
- tunacode/ui/panels.py +173 -49
- tunacode/ui/path_heuristics.py +91 -0
- tunacode/ui/prompt_manager.py +1 -20
- tunacode/ui/tool_ui.py +30 -8
- tunacode/utils/api_key_validation.py +93 -0
- tunacode/utils/config_comparator.py +340 -0
- tunacode/utils/models_registry.py +593 -0
- tunacode/utils/text_utils.py +18 -1
- {tunacode_cli-0.0.70.dist-info → tunacode_cli-0.0.78.6.dist-info}/METADATA +80 -12
- {tunacode_cli-0.0.70.dist-info → tunacode_cli-0.0.78.6.dist-info}/RECORD +78 -74
- tunacode/cli/commands/implementations/plan.py +0 -50
- tunacode/cli/commands/implementations/todo.py +0 -217
- tunacode/context.py +0 -71
- tunacode/core/setup/git_safety_setup.py +0 -186
- tunacode/prompts/system.md +0 -359
- tunacode/prompts/system.md.bak +0 -487
- tunacode/tools/exit_plan_mode.py +0 -273
- tunacode/tools/present_plan.py +0 -288
- tunacode/tools/prompts/exit_plan_mode_prompt.xml +0 -25
- tunacode/tools/prompts/present_plan_prompt.xml +0 -20
- tunacode/tools/prompts/todo_prompt.xml +0 -96
- tunacode/tools/todo.py +0 -456
- {tunacode_cli-0.0.70.dist-info → tunacode_cli-0.0.78.6.dist-info}/WHEEL +0 -0
- {tunacode_cli-0.0.70.dist-info → tunacode_cli-0.0.78.6.dist-info}/entry_points.txt +0 -0
- {tunacode_cli-0.0.70.dist-info → tunacode_cli-0.0.78.6.dist-info}/licenses/LICENSE +0 -0
tunacode/cli/repl.py
CHANGED
|
@@ -13,8 +13,8 @@ from pydantic_ai.exceptions import UnexpectedModelBehavior
|
|
|
13
13
|
|
|
14
14
|
from tunacode.configuration.models import ModelRegistry
|
|
15
15
|
from tunacode.constants import DEFAULT_CONTEXT_WINDOW
|
|
16
|
-
from tunacode.core
|
|
17
|
-
from tunacode.core.agents
|
|
16
|
+
from tunacode.core import agents as agent
|
|
17
|
+
from tunacode.core.agents import patch_tool_messages
|
|
18
18
|
from tunacode.core.token_usage.api_response_parser import ApiResponseParser
|
|
19
19
|
from tunacode.core.token_usage.cost_calculator import CostCalculator
|
|
20
20
|
from tunacode.core.token_usage.usage_tracker import UsageTracker
|
|
@@ -41,237 +41,69 @@ DEFAULT_SHELL = "bash"
|
|
|
41
41
|
logger = logging.getLogger(__name__)
|
|
42
42
|
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
Transform a planning request into an implementation request.
|
|
47
|
-
|
|
48
|
-
This ensures that after plan approval, the agent understands it should
|
|
49
|
-
implement rather than plan again.
|
|
50
|
-
"""
|
|
51
|
-
request = original_request.lower()
|
|
52
|
-
|
|
53
|
-
if "plan" in request:
|
|
54
|
-
request = request.replace("plan a ", "create a ")
|
|
55
|
-
request = request.replace("plan an ", "create an ")
|
|
56
|
-
request = request.replace("plan to ", "")
|
|
57
|
-
request = request.replace("plan ", "create ")
|
|
58
|
-
|
|
59
|
-
# Add clear implementation instruction
|
|
60
|
-
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."
|
|
61
|
-
|
|
62
|
-
return implementation_request
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
async def _display_plan(plan_doc) -> None:
|
|
66
|
-
"""Display the plan in a formatted way."""
|
|
67
|
-
if not plan_doc:
|
|
68
|
-
await ui.error("⚠️ Error: No plan document found to display")
|
|
69
|
-
return
|
|
70
|
-
|
|
71
|
-
output = [f"[bold cyan]🎯 {plan_doc.title}[/bold cyan]", ""]
|
|
72
|
-
|
|
73
|
-
if plan_doc.overview:
|
|
74
|
-
output.extend([f"[bold]📝 Overview:[/bold] {plan_doc.overview}", ""])
|
|
75
|
-
|
|
76
|
-
sections = [
|
|
77
|
-
("📝 Files to Modify:", plan_doc.files_to_modify, "•"),
|
|
78
|
-
("📄 Files to Create:", plan_doc.files_to_create, "•"),
|
|
79
|
-
("🧪 Testing Approach:", plan_doc.tests, "•"),
|
|
80
|
-
("✅ Success Criteria:", plan_doc.success_criteria, "•"),
|
|
81
|
-
("⚠️ Risks & Considerations:", plan_doc.risks, "•"),
|
|
82
|
-
("❓ Open Questions:", plan_doc.open_questions, "•"),
|
|
83
|
-
("📚 References:", plan_doc.references, "•"),
|
|
84
|
-
]
|
|
85
|
-
|
|
86
|
-
for title, items, prefix in sections:
|
|
87
|
-
if items:
|
|
88
|
-
output.append(f"[bold]{title}[/bold]")
|
|
89
|
-
output.extend(f" {prefix} {item}" for item in items)
|
|
90
|
-
output.append("")
|
|
91
|
-
|
|
92
|
-
output.append("[bold]🔧 Implementation Steps:[/bold]")
|
|
93
|
-
output.extend(f" {i}. {step}" for i, step in enumerate(plan_doc.steps, 1))
|
|
94
|
-
output.append("")
|
|
95
|
-
|
|
96
|
-
if plan_doc.rollback:
|
|
97
|
-
output.extend([f"[bold]🔄 Rollback Plan:[/bold] {plan_doc.rollback}", ""])
|
|
98
|
-
|
|
99
|
-
await ui.panel("📋 IMPLEMENTATION PLAN", "\n".join(output), border_style="cyan")
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
async def _detect_and_handle_text_plan(state_manager, agent_response, original_request):
|
|
103
|
-
"""Detect if agent presented a plan in text format and handle it."""
|
|
104
|
-
try:
|
|
105
|
-
# Extract response text
|
|
106
|
-
response_text = ""
|
|
107
|
-
if hasattr(agent_response, "messages") and agent_response.messages:
|
|
108
|
-
msg = agent_response.messages[-1]
|
|
109
|
-
response_text = str(getattr(msg, "content", getattr(msg, "text", msg)))
|
|
110
|
-
elif hasattr(agent_response, "result"):
|
|
111
|
-
response_text = str(getattr(agent_response.result, "output", agent_response.result))
|
|
112
|
-
else:
|
|
113
|
-
response_text = str(agent_response)
|
|
114
|
-
|
|
115
|
-
if "TUNACODE_TASK_COMPLETE" in response_text:
|
|
116
|
-
await ui.warning(
|
|
117
|
-
"⚠️ Agent failed to call present_plan tool. Please provide clearer instructions."
|
|
118
|
-
)
|
|
119
|
-
return
|
|
120
|
-
|
|
121
|
-
if "present_plan(" in response_text:
|
|
122
|
-
await ui.error(
|
|
123
|
-
"❌ Agent showed present_plan as text instead of EXECUTING it as a tool!"
|
|
124
|
-
)
|
|
125
|
-
await ui.info("Try again with: 'Execute the present_plan tool to create a plan for...'")
|
|
126
|
-
return
|
|
127
|
-
|
|
128
|
-
# Check for plan indicators
|
|
129
|
-
plan_indicators = {
|
|
130
|
-
"plan for",
|
|
131
|
-
"implementation plan",
|
|
132
|
-
"here's a plan",
|
|
133
|
-
"i'll create a plan",
|
|
134
|
-
"plan to",
|
|
135
|
-
"outline for",
|
|
136
|
-
"overview:",
|
|
137
|
-
"steps:",
|
|
138
|
-
}
|
|
139
|
-
has_plan = any(ind in response_text.lower() for ind in plan_indicators)
|
|
140
|
-
has_structure = (
|
|
141
|
-
any(x in response_text for x in ["1.", "2.", "•"]) and response_text.count("\n") > 5
|
|
142
|
-
)
|
|
143
|
-
|
|
144
|
-
if has_plan and has_structure:
|
|
145
|
-
await ui.info("📋 Plan detected in text format - extracting for review")
|
|
146
|
-
from tunacode.types import PlanDoc, PlanPhase
|
|
147
|
-
|
|
148
|
-
plan_doc = PlanDoc(
|
|
149
|
-
title="Implementation Plan",
|
|
150
|
-
overview="Automated plan extraction from text",
|
|
151
|
-
steps=["Review and implement the described functionality"],
|
|
152
|
-
files_to_modify=[],
|
|
153
|
-
files_to_create=[],
|
|
154
|
-
success_criteria=[],
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
state_manager.session.plan_phase = PlanPhase.PLAN_READY
|
|
158
|
-
state_manager.session.current_plan = plan_doc
|
|
159
|
-
await _handle_plan_approval(state_manager, original_request)
|
|
160
|
-
|
|
161
|
-
except Exception as e:
|
|
162
|
-
logger.error(f"Error detecting text plan: {e}")
|
|
44
|
+
_command_registry = CommandRegistry()
|
|
45
|
+
_command_registry.register_all_default_commands()
|
|
163
46
|
|
|
164
47
|
|
|
165
|
-
async def
|
|
166
|
-
"""
|
|
48
|
+
async def _handle_command(command: str, state_manager: StateManager) -> CommandResult:
|
|
49
|
+
"""Handles a command string using the command registry."""
|
|
50
|
+
context = CommandContext(state_manager=state_manager, process_request=execute_repl_request)
|
|
167
51
|
try:
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
state_manager.session.plan_phase = PlanPhase.REVIEW_DECISION
|
|
174
|
-
plan_doc = state_manager.session.current_plan
|
|
175
|
-
state_manager.exit_plan_mode(plan_doc)
|
|
52
|
+
_command_registry.set_process_request_callback(execute_repl_request)
|
|
53
|
+
return await _command_registry.execute(command, context)
|
|
54
|
+
except ValidationError as e:
|
|
55
|
+
await ui.error(str(e))
|
|
56
|
+
return None
|
|
176
57
|
|
|
177
|
-
await ui.info("📋 Plan has been prepared and Plan Mode exited")
|
|
178
|
-
await _display_plan(plan_doc)
|
|
179
58
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
"[yellow]Choose your action:[/yellow]\n\n"
|
|
183
|
-
" [bold green]a[/bold green] → Approve and proceed\n"
|
|
184
|
-
" [bold yellow]m[/bold yellow] → Modify the plan\n"
|
|
185
|
-
" [bold red]r[/bold red] → Reject and recreate\n"
|
|
186
|
-
)
|
|
187
|
-
await ui.panel("🎯 Plan Review", content, border_style="cyan")
|
|
59
|
+
def _extract_feedback_from_last_message(state_manager: StateManager) -> str | None:
|
|
60
|
+
"""Extract user guidance feedback from recent messages in session.messages.
|
|
188
61
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
try:
|
|
192
|
-
response = await ui.input(
|
|
193
|
-
"plan_approval", " → Your choice [a/m/r]: ", kb, state_manager
|
|
194
|
-
)
|
|
195
|
-
response = response.strip().lower()
|
|
196
|
-
state_manager.session.approval_abort_pressed = False
|
|
197
|
-
state_manager.session.approval_last_abort_time = 0.0
|
|
198
|
-
break
|
|
199
|
-
except UserAbortError:
|
|
200
|
-
current_time = time.time()
|
|
201
|
-
abort_pressed = getattr(state_manager.session, "approval_abort_pressed", False)
|
|
202
|
-
last_abort = getattr(state_manager.session, "approval_last_abort_time", 0.0)
|
|
62
|
+
When option 3 is selected with feedback, a message is added with format:
|
|
63
|
+
"Tool '...' execution cancelled before running.\nUser guidance:\n{guidance}\n..."
|
|
203
64
|
|
|
204
|
-
|
|
205
|
-
|
|
65
|
+
Note: patch_tool_messages() adds "Operation aborted by user." AFTER the feedback,
|
|
66
|
+
so we check the last few messages, not just the last one.
|
|
206
67
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
state_manager.enter_plan_mode()
|
|
210
|
-
state_manager.session.approval_abort_pressed = False
|
|
211
|
-
return
|
|
212
|
-
|
|
213
|
-
state_manager.session.approval_abort_pressed = True
|
|
214
|
-
state_manager.session.approval_last_abort_time = current_time
|
|
215
|
-
await ui.warning("Hit ESC or Ctrl+C again to return to Plan Mode")
|
|
216
|
-
|
|
217
|
-
actions = {
|
|
218
|
-
"a": (
|
|
219
|
-
"✅ Plan approved - proceeding with implementation",
|
|
220
|
-
lambda: state_manager.approve_plan(),
|
|
221
|
-
),
|
|
222
|
-
"m": (
|
|
223
|
-
"📝 Returning to Plan Mode for modifications",
|
|
224
|
-
lambda: state_manager.enter_plan_mode(),
|
|
225
|
-
),
|
|
226
|
-
"r": (
|
|
227
|
-
"🔄 Plan rejected - returning to Plan Mode",
|
|
228
|
-
lambda: state_manager.enter_plan_mode(),
|
|
229
|
-
),
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
if response in actions or response in ["approve", "modify", "reject"]:
|
|
233
|
-
key = response[0] if len(response) > 1 else response
|
|
234
|
-
msg, action = actions.get(key, (None, None))
|
|
235
|
-
if msg:
|
|
236
|
-
await ui.info(msg) if key == "a" else await ui.warning(msg)
|
|
237
|
-
action()
|
|
238
|
-
if key == "a" and original_request:
|
|
239
|
-
await ui.info("🚀 Executing implementation...")
|
|
240
|
-
await process_request(
|
|
241
|
-
_transform_to_implementation_request(original_request),
|
|
242
|
-
state_manager,
|
|
243
|
-
output=True,
|
|
244
|
-
)
|
|
245
|
-
else:
|
|
246
|
-
await ui.warning("⚠️ Invalid choice - please enter a, m, or r")
|
|
68
|
+
Args:
|
|
69
|
+
state_manager: State manager containing session messages
|
|
247
70
|
|
|
248
|
-
|
|
71
|
+
Returns:
|
|
72
|
+
The guidance text if found, None otherwise
|
|
73
|
+
"""
|
|
74
|
+
if not state_manager.session.messages:
|
|
75
|
+
return None
|
|
249
76
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
state_manager.session.plan_phase = None
|
|
77
|
+
# Check last 3 messages since patch_tool_messages() adds a message after feedback
|
|
78
|
+
messages_to_check = state_manager.session.messages[-3:]
|
|
253
79
|
|
|
80
|
+
for msg in reversed(messages_to_check):
|
|
81
|
+
# Extract content from message parts
|
|
82
|
+
if not hasattr(msg, "parts"):
|
|
83
|
+
continue
|
|
254
84
|
|
|
255
|
-
|
|
256
|
-
|
|
85
|
+
for part in msg.parts:
|
|
86
|
+
if hasattr(part, "content") and isinstance(part.content, str):
|
|
87
|
+
content = part.content
|
|
257
88
|
|
|
89
|
+
# Look for "User guidance:" pattern
|
|
90
|
+
if "User guidance:" in content:
|
|
91
|
+
lines = content.split("\n")
|
|
92
|
+
for i, line in enumerate(lines):
|
|
93
|
+
if "User guidance:" in line and i + 1 < len(lines):
|
|
94
|
+
guidance = lines[i + 1].strip()
|
|
95
|
+
# Only return non-empty guidance
|
|
96
|
+
cancelled_msg = "User cancelled without additional instructions."
|
|
97
|
+
if guidance and guidance != cancelled_msg:
|
|
98
|
+
return guidance
|
|
258
99
|
|
|
259
|
-
|
|
260
|
-
"""Handles a command string using the command registry."""
|
|
261
|
-
context = CommandContext(state_manager=state_manager, process_request=process_request)
|
|
262
|
-
try:
|
|
263
|
-
_command_registry.set_process_request_callback(process_request)
|
|
264
|
-
return await _command_registry.execute(command, context)
|
|
265
|
-
except ValidationError as e:
|
|
266
|
-
await ui.error(str(e))
|
|
267
|
-
return None
|
|
100
|
+
return None
|
|
268
101
|
|
|
269
102
|
|
|
270
|
-
async def
|
|
103
|
+
async def execute_repl_request(text: str, state_manager: StateManager, output: bool = True):
|
|
271
104
|
"""Process input using the agent, handling cancellation safely."""
|
|
272
105
|
import uuid
|
|
273
106
|
|
|
274
|
-
from tunacode.types import PlanPhase
|
|
275
107
|
from tunacode.utils.text_utils import expand_file_refs
|
|
276
108
|
|
|
277
109
|
state_manager.session.request_id = str(uuid.uuid4())
|
|
@@ -319,7 +151,9 @@ async def process_request(text: str, state_manager: StateManager, output: bool =
|
|
|
319
151
|
if enable_streaming:
|
|
320
152
|
await ui.spinner(False, state_manager.session.spinner, state_manager)
|
|
321
153
|
state_manager.session.is_streaming_active = True
|
|
322
|
-
streaming_panel = ui.StreamingAgentPanel(
|
|
154
|
+
streaming_panel = ui.StreamingAgentPanel(
|
|
155
|
+
debug=bool(state_manager.session.show_thoughts)
|
|
156
|
+
)
|
|
323
157
|
await streaming_panel.start()
|
|
324
158
|
state_manager.session.streaming_panel = streaming_panel
|
|
325
159
|
|
|
@@ -336,6 +170,20 @@ async def process_request(text: str, state_manager: StateManager, output: bool =
|
|
|
336
170
|
await streaming_panel.stop()
|
|
337
171
|
state_manager.session.streaming_panel = None
|
|
338
172
|
state_manager.session.is_streaming_active = False
|
|
173
|
+
# Emit source-side streaming diagnostics if thoughts are enabled
|
|
174
|
+
if state_manager.session.show_thoughts:
|
|
175
|
+
try:
|
|
176
|
+
raw = getattr(state_manager.session, "_debug_raw_stream_accum", "") or ""
|
|
177
|
+
events = getattr(state_manager.session, "_debug_events", []) or []
|
|
178
|
+
raw_first5 = repr(raw[:5])
|
|
179
|
+
await ui.muted(
|
|
180
|
+
f"[debug] raw_stream_first5={raw_first5} total_len={len(raw)}"
|
|
181
|
+
)
|
|
182
|
+
for line in events:
|
|
183
|
+
await ui.muted(line)
|
|
184
|
+
except Exception:
|
|
185
|
+
# Don't let diagnostics break normal flow
|
|
186
|
+
pass
|
|
339
187
|
else:
|
|
340
188
|
res = await agent.process_request(
|
|
341
189
|
text,
|
|
@@ -345,17 +193,6 @@ async def process_request(text: str, state_manager: StateManager, output: bool =
|
|
|
345
193
|
usage_tracker=usage_tracker,
|
|
346
194
|
)
|
|
347
195
|
|
|
348
|
-
# Handle plan approval or detection
|
|
349
|
-
if (
|
|
350
|
-
hasattr(state_manager.session, "plan_phase")
|
|
351
|
-
and state_manager.session.plan_phase == PlanPhase.PLAN_READY
|
|
352
|
-
):
|
|
353
|
-
await _handle_plan_approval(state_manager, text)
|
|
354
|
-
elif state_manager.is_plan_mode() and not getattr(
|
|
355
|
-
state_manager.session, "_continuing_from_plan", False
|
|
356
|
-
):
|
|
357
|
-
await _detect_and_handle_text_plan(state_manager, res, text)
|
|
358
|
-
|
|
359
196
|
if output:
|
|
360
197
|
if state_manager.session.show_thoughts:
|
|
361
198
|
for msg in state_manager.session.messages[start_idx:]:
|
|
@@ -377,7 +214,24 @@ async def process_request(text: str, state_manager: StateManager, output: bool =
|
|
|
377
214
|
except CancelledError:
|
|
378
215
|
await ui.muted(MSG_REQUEST_CANCELLED)
|
|
379
216
|
except UserAbortError:
|
|
380
|
-
|
|
217
|
+
# CLAUDE_ANCHOR[7b2c1d4e]: Guided aborts inject user instructions; skip legacy banner.
|
|
218
|
+
# Check if there's feedback to process immediately
|
|
219
|
+
feedback = _extract_feedback_from_last_message(state_manager)
|
|
220
|
+
if feedback:
|
|
221
|
+
# Process the feedback as a new request immediately
|
|
222
|
+
# Stop spinner first to clean up state before recursive call
|
|
223
|
+
await ui.spinner(False, state_manager.session.spinner, state_manager)
|
|
224
|
+
# Clear current_task so recursive call can set its own
|
|
225
|
+
state_manager.session.current_task = None
|
|
226
|
+
try:
|
|
227
|
+
await execute_repl_request(feedback, state_manager, output=output)
|
|
228
|
+
except Exception:
|
|
229
|
+
# If recursive call fails, don't let it bubble up - just continue
|
|
230
|
+
pass
|
|
231
|
+
# Return early to skip the finally block's cleanup (already done above)
|
|
232
|
+
return
|
|
233
|
+
# No feedback, just abort normally
|
|
234
|
+
pass
|
|
381
235
|
except UnexpectedModelBehavior as e:
|
|
382
236
|
await ui.muted(str(e))
|
|
383
237
|
patch_tool_messages(str(e), state_manager)
|
|
@@ -395,6 +249,10 @@ async def process_request(text: str, state_manager: StateManager, output: bool =
|
|
|
395
249
|
)
|
|
396
250
|
|
|
397
251
|
|
|
252
|
+
# Backwards compatibility: exported name expected by external integrations/tests
|
|
253
|
+
process_request = execute_repl_request
|
|
254
|
+
|
|
255
|
+
|
|
398
256
|
async def warm_code_index():
|
|
399
257
|
"""Pre-warm the code index in background for faster directory operations."""
|
|
400
258
|
try:
|
|
@@ -434,9 +292,12 @@ async def repl(state_manager: StateManager):
|
|
|
434
292
|
if state_manager.session.session_total_usage:
|
|
435
293
|
session_cost = float(state_manager.session.session_total_usage.get("cost", 0.0) or 0.0)
|
|
436
294
|
|
|
295
|
+
# Subtle, unified styling - mostly muted with minimal accent on cost
|
|
437
296
|
await ui.muted(f"• Model: {state_manager.session.current_model} • {context}")
|
|
438
297
|
if session_cost > 0:
|
|
439
|
-
await ui.
|
|
298
|
+
await ui.print(
|
|
299
|
+
f"[dim]• Session Cost:[/dim] [dim #00d7ff]${session_cost:.4f}[/dim #00d7ff]"
|
|
300
|
+
)
|
|
440
301
|
|
|
441
302
|
# Always show context
|
|
442
303
|
await show_context()
|
|
@@ -516,7 +377,7 @@ async def repl(state_manager: StateManager):
|
|
|
516
377
|
state_manager.session.operation_cancelled = False
|
|
517
378
|
|
|
518
379
|
state_manager.session.current_task = get_app().create_background_task(
|
|
519
|
-
|
|
380
|
+
execute_repl_request(line, state_manager)
|
|
520
381
|
)
|
|
521
382
|
await state_manager.session.current_task
|
|
522
383
|
|
|
@@ -31,7 +31,8 @@ def parse_args(args) -> ToolArgs:
|
|
|
31
31
|
dict: The parsed arguments.
|
|
32
32
|
|
|
33
33
|
Raises:
|
|
34
|
-
ValidationError: If 'args' is not a string or dictionary, or if the string
|
|
34
|
+
ValidationError: If 'args' is not a string or dictionary, or if the string
|
|
35
|
+
is not valid JSON.
|
|
35
36
|
"""
|
|
36
37
|
if isinstance(args, str):
|
|
37
38
|
try:
|
|
@@ -6,6 +6,7 @@ Error recovery utilities for the REPL.
|
|
|
6
6
|
|
|
7
7
|
import logging
|
|
8
8
|
|
|
9
|
+
import tunacode.core.agents as agent_api
|
|
9
10
|
from tunacode.types import StateManager
|
|
10
11
|
from tunacode.ui import console as ui
|
|
11
12
|
|
|
@@ -64,7 +65,8 @@ async def attempt_json_args_recovery(e: Exception, state_manager: StateManager)
|
|
|
64
65
|
|
|
65
66
|
await ui.warning(f"Warning: {MSG_JSON_ARGS_RECOVERY}")
|
|
66
67
|
logger.info(
|
|
67
|
-
f"Successfully recovered tool {part.tool_name} with
|
|
68
|
+
f"Successfully recovered tool {part.tool_name} with "
|
|
69
|
+
f"split JSON args",
|
|
68
70
|
extra={
|
|
69
71
|
"original_args": part.args,
|
|
70
72
|
"recovered_args": json_objects[0],
|
|
@@ -78,7 +80,8 @@ async def attempt_json_args_recovery(e: Exception, state_manager: StateManager)
|
|
|
78
80
|
|
|
79
81
|
except Exception as recovery_exc:
|
|
80
82
|
logger.error(
|
|
81
|
-
f"Error during JSON args recovery for tool
|
|
83
|
+
f"Error during JSON args recovery for tool "
|
|
84
|
+
f"{getattr(part, 'tool_name', 'unknown')}",
|
|
82
85
|
exc_info=True,
|
|
83
86
|
extra={"recovery_exception": str(recovery_exc)},
|
|
84
87
|
)
|
|
@@ -126,17 +129,17 @@ async def attempt_tool_recovery(e: Exception, state_manager: StateManager) -> bo
|
|
|
126
129
|
},
|
|
127
130
|
)
|
|
128
131
|
await ui.muted(
|
|
129
|
-
f"⚠️ Model response error. Attempting to recover by parsing tools
|
|
132
|
+
f"⚠️ Model response error. Attempting to recover by parsing tools "
|
|
133
|
+
f"from text: {str(e)[:100]}..."
|
|
130
134
|
)
|
|
131
135
|
|
|
132
136
|
try:
|
|
133
|
-
from tunacode.core.agents.main import extract_and_execute_tool_calls
|
|
134
137
|
|
|
135
138
|
def tool_callback_with_state(tool_part, _node):
|
|
136
139
|
return tool_handler(tool_part, state_manager)
|
|
137
140
|
|
|
138
141
|
# This function now returns the number of tools found
|
|
139
|
-
tools_found = await extract_and_execute_tool_calls(
|
|
142
|
+
tools_found = await agent_api.extract_and_execute_tool_calls(
|
|
140
143
|
content_to_parse, tool_callback_with_state, state_manager
|
|
141
144
|
)
|
|
142
145
|
|
|
@@ -30,19 +30,10 @@ async def display_agent_output(res, enable_streaming: bool, state_manager=None)
|
|
|
30
30
|
if '"tool_uses"' in output:
|
|
31
31
|
return
|
|
32
32
|
|
|
33
|
-
#
|
|
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
|
|
33
|
+
# Filter out system prompts and tool definitions
|
|
39
34
|
if any(
|
|
40
35
|
phrase in output
|
|
41
36
|
for phrase in [
|
|
42
|
-
"PLAN MODE - TOOL EXECUTION ONLY",
|
|
43
|
-
"🔧 PLAN MODE",
|
|
44
|
-
"TOOL EXECUTION ONLY 🔧",
|
|
45
|
-
"planning assistant that ONLY communicates",
|
|
46
37
|
"namespace functions {",
|
|
47
38
|
"namespace multi_tool_use {",
|
|
48
39
|
"You are trained on data up to",
|
|
@@ -9,7 +9,7 @@ from asyncio.exceptions import CancelledError
|
|
|
9
9
|
|
|
10
10
|
from prompt_toolkit.application import run_in_terminal
|
|
11
11
|
|
|
12
|
-
from tunacode.core.agents
|
|
12
|
+
from tunacode.core.agents import patch_tool_messages
|
|
13
13
|
from tunacode.core.tool_handler import ToolHandler
|
|
14
14
|
from tunacode.exceptions import UserAbortError
|
|
15
15
|
from tunacode.types import StateManager
|
|
@@ -59,18 +59,6 @@ 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
|
-
|
|
66
|
-
error_msg = (
|
|
67
|
-
f"🔍 Plan Mode: Tool '{part.tool_name}' is not available in Plan Mode.\n"
|
|
68
|
-
f"Only read-only tools are allowed: {', '.join(READ_ONLY_TOOLS)}\n"
|
|
69
|
-
f"Use 'exit_plan_mode' tool to present your plan and exit Plan Mode."
|
|
70
|
-
)
|
|
71
|
-
print(f"\n❌ {error_msg}\n")
|
|
72
|
-
return True # Abort the tool
|
|
73
|
-
|
|
74
62
|
if not tool_handler_instance.should_confirm(part.tool_name):
|
|
75
63
|
return False
|
|
76
64
|
request = tool_handler_instance.create_confirmation_request(part.tool_name, args)
|
|
@@ -5,7 +5,7 @@ Default configuration values for the TunaCode CLI.
|
|
|
5
5
|
Provides sensible defaults for user configuration and environment variables.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from tunacode.constants import GUIDE_FILE_NAME
|
|
8
|
+
from tunacode.constants import GUIDE_FILE_NAME
|
|
9
9
|
from tunacode.types import UserConfig
|
|
10
10
|
|
|
11
11
|
DEFAULT_USER_CONFIG: UserConfig = {
|
|
@@ -19,7 +19,7 @@ DEFAULT_USER_CONFIG: UserConfig = {
|
|
|
19
19
|
"settings": {
|
|
20
20
|
"max_retries": 10,
|
|
21
21
|
"max_iterations": 40,
|
|
22
|
-
"tool_ignore": [
|
|
22
|
+
"tool_ignore": [],
|
|
23
23
|
"guide_file": GUIDE_FILE_NAME,
|
|
24
24
|
"fallback_response": True,
|
|
25
25
|
"fallback_verbosity": "normal", # Options: minimal, normal, detailed
|