tunacode-cli 0.0.56__py3-none-any.whl → 0.0.60__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/implementations/plan.py +8 -8
- tunacode/cli/commands/registry.py +2 -2
- tunacode/cli/repl.py +214 -407
- tunacode/cli/repl_components/command_parser.py +37 -4
- tunacode/cli/repl_components/error_recovery.py +79 -1
- tunacode/cli/repl_components/output_display.py +14 -11
- tunacode/cli/repl_components/tool_executor.py +7 -4
- tunacode/configuration/defaults.py +8 -0
- tunacode/constants.py +8 -2
- tunacode/core/agents/agent_components/agent_config.py +128 -65
- tunacode/core/agents/agent_components/node_processor.py +6 -2
- tunacode/core/code_index.py +83 -29
- tunacode/core/state.py +1 -1
- tunacode/core/token_usage/usage_tracker.py +2 -2
- tunacode/core/tool_handler.py +3 -3
- tunacode/prompts/system.md +117 -490
- tunacode/services/mcp.py +29 -7
- tunacode/tools/base.py +110 -0
- tunacode/tools/bash.py +96 -1
- tunacode/tools/exit_plan_mode.py +114 -32
- tunacode/tools/glob.py +366 -33
- tunacode/tools/grep.py +226 -77
- tunacode/tools/grep_components/result_formatter.py +98 -4
- tunacode/tools/list_dir.py +132 -2
- tunacode/tools/present_plan.py +111 -31
- tunacode/tools/read_file.py +91 -0
- tunacode/tools/run_command.py +99 -0
- tunacode/tools/schema_assembler.py +167 -0
- tunacode/tools/todo.py +108 -1
- tunacode/tools/update_file.py +94 -0
- tunacode/tools/write_file.py +86 -0
- tunacode/types.py +10 -9
- tunacode/ui/input.py +1 -0
- tunacode/ui/keybindings.py +1 -0
- tunacode/ui/panels.py +49 -27
- tunacode/ui/prompt_manager.py +13 -7
- tunacode/utils/json_utils.py +206 -0
- tunacode/utils/ripgrep.py +332 -9
- {tunacode_cli-0.0.56.dist-info → tunacode_cli-0.0.60.dist-info}/METADATA +7 -2
- {tunacode_cli-0.0.56.dist-info → tunacode_cli-0.0.60.dist-info}/RECORD +44 -43
- tunacode/tools/read_file_async_poc.py +0 -196
- {tunacode_cli-0.0.56.dist-info → tunacode_cli-0.0.60.dist-info}/WHEEL +0 -0
- {tunacode_cli-0.0.56.dist-info → tunacode_cli-0.0.60.dist-info}/entry_points.txt +0 -0
- {tunacode_cli-0.0.56.dist-info → tunacode_cli-0.0.60.dist-info}/licenses/LICENSE +0 -0
- {tunacode_cli-0.0.56.dist-info → tunacode_cli-0.0.60.dist-info}/top_level.txt +0 -0
tunacode/cli/repl.py
CHANGED
|
@@ -1,16 +1,6 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Module: tunacode.cli.repl
|
|
3
|
-
|
|
4
|
-
Interactive REPL (Read-Eval-Print Loop) implementation for TunaCode.
|
|
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
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
# ============================================================================
|
|
11
|
-
# IMPORTS AND DEPENDENCIES
|
|
12
|
-
# ============================================================================
|
|
1
|
+
"""Interactive REPL implementation for TunaCode."""
|
|
13
2
|
|
|
3
|
+
import asyncio
|
|
14
4
|
import logging
|
|
15
5
|
import os
|
|
16
6
|
import subprocess
|
|
@@ -21,20 +11,20 @@ from prompt_toolkit.application import run_in_terminal
|
|
|
21
11
|
from prompt_toolkit.application.current import get_app
|
|
22
12
|
from pydantic_ai.exceptions import UnexpectedModelBehavior
|
|
23
13
|
|
|
14
|
+
from tunacode.configuration.models import ModelRegistry
|
|
24
15
|
from tunacode.constants import DEFAULT_CONTEXT_WINDOW
|
|
25
16
|
from tunacode.core.agents import main as agent
|
|
26
17
|
from tunacode.core.agents.main import patch_tool_messages
|
|
27
|
-
from tunacode.
|
|
18
|
+
from tunacode.core.token_usage.api_response_parser import ApiResponseParser
|
|
19
|
+
from tunacode.core.token_usage.cost_calculator import CostCalculator
|
|
20
|
+
from tunacode.core.token_usage.usage_tracker import UsageTracker
|
|
21
|
+
from tunacode.exceptions import UserAbortError, ValidationError
|
|
28
22
|
from tunacode.ui import console as ui
|
|
29
23
|
from tunacode.ui.output import get_context_window_display
|
|
30
24
|
from tunacode.utils.security import CommandSecurityError, safe_subprocess_run
|
|
31
25
|
|
|
32
26
|
from ..types import CommandContext, CommandResult, StateManager
|
|
33
27
|
from .commands import CommandRegistry
|
|
34
|
-
|
|
35
|
-
# ============================================================================
|
|
36
|
-
# MODULE-LEVEL CONSTANTS AND CONFIGURATION
|
|
37
|
-
# ============================================================================
|
|
38
28
|
from .repl_components import attempt_tool_recovery, display_agent_output, tool_handler
|
|
39
29
|
from .repl_components.output_display import MSG_REQUEST_COMPLETED
|
|
40
30
|
|
|
@@ -50,9 +40,6 @@ DEFAULT_SHELL = "bash"
|
|
|
50
40
|
# Configure logging
|
|
51
41
|
logger = logging.getLogger(__name__)
|
|
52
42
|
|
|
53
|
-
# The _parse_args function has been moved to repl_components.command_parser
|
|
54
|
-
# The _tool_handler function has been moved to repl_components.tool_executor
|
|
55
|
-
|
|
56
43
|
|
|
57
44
|
def _transform_to_implementation_request(original_request: str) -> str:
|
|
58
45
|
"""
|
|
@@ -61,12 +48,9 @@ def _transform_to_implementation_request(original_request: str) -> str:
|
|
|
61
48
|
This ensures that after plan approval, the agent understands it should
|
|
62
49
|
implement rather than plan again.
|
|
63
50
|
"""
|
|
64
|
-
# Remove plan-related language and add implementation language
|
|
65
51
|
request = original_request.lower()
|
|
66
52
|
|
|
67
53
|
if "plan" in request:
|
|
68
|
-
# Transform "plan a md file" -> "create a md file"
|
|
69
|
-
# Transform "plan to implement" -> "implement"
|
|
70
54
|
request = request.replace("plan a ", "create a ")
|
|
71
55
|
request = request.replace("plan an ", "create an ")
|
|
72
56
|
request = request.replace("plan to ", "")
|
|
@@ -80,362 +64,225 @@ def _transform_to_implementation_request(original_request: str) -> str:
|
|
|
80
64
|
|
|
81
65
|
async def _display_plan(plan_doc) -> None:
|
|
82
66
|
"""Display the plan in a formatted way."""
|
|
83
|
-
|
|
84
67
|
if not plan_doc:
|
|
85
68
|
await ui.error("⚠️ Error: No plan document found to display")
|
|
86
69
|
return
|
|
87
70
|
|
|
88
|
-
output = []
|
|
89
|
-
output.append(f"[bold cyan]🎯 {plan_doc.title}[/bold cyan]")
|
|
90
|
-
output.append("")
|
|
71
|
+
output = [f"[bold cyan]🎯 {plan_doc.title}[/bold cyan]", ""]
|
|
91
72
|
|
|
92
73
|
if plan_doc.overview:
|
|
93
|
-
output.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
+
|
|
110
92
|
output.append("[bold]🔧 Implementation Steps:[/bold]")
|
|
111
|
-
for i, step in enumerate(plan_doc.steps, 1)
|
|
112
|
-
output.append(f" {i}. {step}")
|
|
93
|
+
output.extend(f" {i}. {step}" for i, step in enumerate(plan_doc.steps, 1))
|
|
113
94
|
output.append("")
|
|
114
95
|
|
|
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
96
|
if plan_doc.rollback:
|
|
152
|
-
output.
|
|
153
|
-
output.append("")
|
|
97
|
+
output.extend([f"[bold]🔄 Rollback Plan:[/bold] {plan_doc.rollback}", ""])
|
|
154
98
|
|
|
155
|
-
# Display the plan in a cyan panel
|
|
156
99
|
await ui.panel("📋 IMPLEMENTATION PLAN", "\n".join(output), border_style="cyan")
|
|
157
100
|
|
|
158
101
|
|
|
159
102
|
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
|
-
"""
|
|
103
|
+
"""Detect if agent presented a plan in text format and handle it."""
|
|
165
104
|
try:
|
|
166
105
|
# Extract response text
|
|
167
106
|
response_text = ""
|
|
168
|
-
if hasattr(agent_response,
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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)
|
|
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))
|
|
176
112
|
else:
|
|
177
113
|
response_text = str(agent_response)
|
|
178
114
|
|
|
179
|
-
# Skip if agent just returned TUNACODE_TASK_COMPLETE or showed present_plan as text
|
|
180
115
|
if "TUNACODE_TASK_COMPLETE" in response_text:
|
|
181
|
-
|
|
182
|
-
|
|
116
|
+
await ui.warning(
|
|
117
|
+
"⚠️ Agent failed to call present_plan tool. Please provide clearer instructions."
|
|
118
|
+
)
|
|
183
119
|
return
|
|
184
120
|
|
|
185
121
|
if "present_plan(" in response_text:
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
122
|
+
await ui.error(
|
|
123
|
+
"❌ Agent showed present_plan as text instead of EXECUTING it as a tool!"
|
|
124
|
+
)
|
|
189
125
|
await ui.info("Try again with: 'Execute the present_plan tool to create a plan for...'")
|
|
190
126
|
return
|
|
191
127
|
|
|
192
128
|
# Check for plan indicators
|
|
193
|
-
plan_indicators =
|
|
194
|
-
"plan for",
|
|
195
|
-
"
|
|
196
|
-
"
|
|
197
|
-
"
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
(
|
|
206
|
-
("Title:" in response_text and "Overview:" in response_text)
|
|
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
|
|
207
142
|
)
|
|
208
143
|
|
|
209
|
-
if
|
|
210
|
-
# Agent presented a text plan - simulate the approval flow
|
|
211
|
-
await ui.line()
|
|
144
|
+
if has_plan and has_structure:
|
|
212
145
|
await ui.info("📋 Plan detected in text format - extracting for review")
|
|
213
|
-
|
|
214
|
-
# Create a simple plan from the text
|
|
215
146
|
from tunacode.types import PlanDoc, PlanPhase
|
|
216
147
|
|
|
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
148
|
plan_doc = PlanDoc(
|
|
228
|
-
title=
|
|
229
|
-
overview="
|
|
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
|
-
],
|
|
149
|
+
title="Implementation Plan",
|
|
150
|
+
overview="Automated plan extraction from text",
|
|
151
|
+
steps=["Review and implement the described functionality"],
|
|
236
152
|
files_to_modify=[],
|
|
237
|
-
files_to_create=[
|
|
238
|
-
success_criteria=[
|
|
153
|
+
files_to_create=[],
|
|
154
|
+
success_criteria=[],
|
|
239
155
|
)
|
|
240
156
|
|
|
241
|
-
# Set plan ready state and trigger approval
|
|
242
157
|
state_manager.session.plan_phase = PlanPhase.PLAN_READY
|
|
243
158
|
state_manager.session.current_plan = plan_doc
|
|
244
|
-
|
|
245
159
|
await _handle_plan_approval(state_manager, original_request)
|
|
246
160
|
|
|
247
161
|
except Exception as e:
|
|
248
162
|
logger.error(f"Error detecting text plan: {e}")
|
|
249
|
-
# If detection fails, just continue normally
|
|
250
163
|
|
|
251
164
|
|
|
252
165
|
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
|
-
"""
|
|
166
|
+
"""Handle plan approval when a plan has been presented via present_plan tool."""
|
|
261
167
|
try:
|
|
168
|
+
import time
|
|
169
|
+
|
|
262
170
|
from tunacode.types import PlanPhase
|
|
171
|
+
from tunacode.ui.keybindings import create_key_bindings
|
|
263
172
|
|
|
264
|
-
# Exit plan mode and move to review phase
|
|
265
173
|
state_manager.session.plan_phase = PlanPhase.REVIEW_DECISION
|
|
266
174
|
plan_doc = state_manager.session.current_plan
|
|
267
175
|
state_manager.exit_plan_mode(plan_doc)
|
|
268
176
|
|
|
269
|
-
await ui.line()
|
|
270
177
|
await ui.info("📋 Plan has been prepared and Plan Mode exited")
|
|
271
|
-
await ui.line()
|
|
272
|
-
|
|
273
|
-
# Display the plan content now
|
|
274
178
|
await _display_plan(plan_doc)
|
|
275
179
|
|
|
276
|
-
# Display approval options with better styling
|
|
277
|
-
await ui.line()
|
|
278
|
-
# Create content with exactly 45 characters per line for perfect alignment
|
|
279
180
|
content = (
|
|
280
|
-
"[bold cyan]The implementation plan has been presented.
|
|
281
|
-
"[yellow]Choose your action:
|
|
282
|
-
" [bold green]a[/bold green] → Approve and proceed
|
|
283
|
-
" [bold yellow]m[/bold yellow] → Modify the plan
|
|
284
|
-
" [bold red]r[/bold red] → Reject and
|
|
181
|
+
"[bold cyan]The implementation plan has been presented.[/bold cyan]\n\n"
|
|
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"
|
|
285
186
|
)
|
|
286
187
|
await ui.panel("🎯 Plan Review", content, border_style="cyan")
|
|
287
|
-
await ui.line()
|
|
288
188
|
|
|
289
|
-
# Handle double-escape pattern like main REPL
|
|
290
|
-
from tunacode.ui.keybindings import create_key_bindings
|
|
291
189
|
kb = create_key_bindings(state_manager)
|
|
292
|
-
|
|
293
190
|
while True:
|
|
294
191
|
try:
|
|
295
192
|
response = await ui.input(
|
|
296
|
-
|
|
297
|
-
pretext=" → Your choice [a/m/r]: ",
|
|
298
|
-
key_bindings=kb,
|
|
299
|
-
state_manager=state_manager
|
|
193
|
+
"plan_approval", " → Your choice [a/m/r]: ", kb, state_manager
|
|
300
194
|
)
|
|
301
195
|
response = response.strip().lower()
|
|
302
|
-
|
|
303
|
-
# Reset abort flags on successful input
|
|
304
196
|
state_manager.session.approval_abort_pressed = False
|
|
305
197
|
state_manager.session.approval_last_abort_time = 0.0
|
|
306
198
|
break
|
|
307
|
-
|
|
308
199
|
except UserAbortError:
|
|
309
|
-
import time
|
|
310
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)
|
|
311
203
|
|
|
312
|
-
|
|
313
|
-
|
|
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
|
|
204
|
+
if current_time - last_abort > 3.0:
|
|
205
|
+
abort_pressed = False
|
|
320
206
|
|
|
321
|
-
if
|
|
322
|
-
|
|
323
|
-
await ui.line()
|
|
324
|
-
await ui.info("🔄 Returning to Plan Mode for further planning")
|
|
325
|
-
await ui.line()
|
|
207
|
+
if abort_pressed:
|
|
208
|
+
await ui.info("🔄 Returning to Plan Mode")
|
|
326
209
|
state_manager.enter_plan_mode()
|
|
327
|
-
# Clean up approval flags
|
|
328
210
|
state_manager.session.approval_abort_pressed = False
|
|
329
|
-
state_manager.session.approval_last_abort_time = 0.0
|
|
330
211
|
return
|
|
331
212
|
|
|
332
|
-
# First escape - show warning and continue the loop
|
|
333
213
|
state_manager.session.approval_abort_pressed = True
|
|
334
214
|
state_manager.session.approval_last_abort_time = current_time
|
|
335
|
-
await ui.line()
|
|
336
215
|
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
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
|
+
)
|
|
364
245
|
else:
|
|
365
|
-
await ui.line()
|
|
366
246
|
await ui.warning("⚠️ Invalid choice - please enter a, m, or r")
|
|
367
|
-
|
|
247
|
+
|
|
248
|
+
state_manager.session.plan_phase = None
|
|
368
249
|
|
|
369
250
|
except Exception as e:
|
|
370
251
|
logger.error(f"Error in plan approval: {e}")
|
|
371
|
-
# If anything goes wrong, reset plan phase
|
|
372
252
|
state_manager.session.plan_phase = None
|
|
373
253
|
|
|
374
254
|
|
|
375
|
-
# ============================================================================
|
|
376
|
-
# COMMAND SYSTEM
|
|
377
|
-
# ============================================================================
|
|
378
|
-
|
|
379
255
|
_command_registry = CommandRegistry()
|
|
380
256
|
_command_registry.register_all_default_commands()
|
|
381
257
|
|
|
382
258
|
|
|
383
259
|
async def _handle_command(command: str, state_manager: StateManager) -> CommandResult:
|
|
384
|
-
"""
|
|
385
|
-
Handles a command string using the command registry.
|
|
386
|
-
|
|
387
|
-
Args:
|
|
388
|
-
command: The command string entered by the user.
|
|
389
|
-
state_manager: The state manager instance.
|
|
390
|
-
|
|
391
|
-
Returns:
|
|
392
|
-
Command result (varies by command).
|
|
393
|
-
"""
|
|
260
|
+
"""Handles a command string using the command registry."""
|
|
394
261
|
context = CommandContext(state_manager=state_manager, process_request=process_request)
|
|
395
|
-
|
|
396
262
|
try:
|
|
397
263
|
_command_registry.set_process_request_callback(process_request)
|
|
398
|
-
|
|
399
264
|
return await _command_registry.execute(command, context)
|
|
400
265
|
except ValidationError as e:
|
|
401
266
|
await ui.error(str(e))
|
|
402
267
|
return None
|
|
403
268
|
|
|
404
269
|
|
|
405
|
-
# The _attempt_tool_recovery function has been moved to repl_components.error_recovery
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
# The _display_agent_output function has been moved to repl_components.output_display
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
# ============================================================================
|
|
412
|
-
# MAIN AGENT REQUEST PROCESSING
|
|
413
|
-
# ============================================================================
|
|
414
|
-
|
|
415
|
-
|
|
416
270
|
async def process_request(text: str, state_manager: StateManager, output: bool = True):
|
|
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
|
-
"""
|
|
271
|
+
"""Process input using the agent, handling cancellation safely."""
|
|
421
272
|
import uuid
|
|
422
273
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
)
|
|
428
|
-
state_manager.session.request_id = request_id
|
|
274
|
+
from tunacode.types import PlanPhase
|
|
275
|
+
from tunacode.utils.text_utils import expand_file_refs
|
|
276
|
+
|
|
277
|
+
state_manager.session.request_id = str(uuid.uuid4())
|
|
429
278
|
|
|
430
|
-
|
|
431
|
-
operation_cancelled = getattr(state_manager.session, "operation_cancelled", False)
|
|
432
|
-
if operation_cancelled is True:
|
|
433
|
-
logger.debug("Operation cancelled before processing started")
|
|
279
|
+
if getattr(state_manager.session, "operation_cancelled", False) is True:
|
|
434
280
|
raise CancelledError("Operation was cancelled")
|
|
435
281
|
|
|
436
282
|
state_manager.session.spinner = await ui.spinner(
|
|
437
283
|
True, state_manager.session.spinner, state_manager
|
|
438
284
|
)
|
|
285
|
+
|
|
439
286
|
try:
|
|
440
287
|
patch_tool_messages(MSG_TOOL_INTERRUPTED, state_manager)
|
|
441
288
|
|
|
@@ -446,161 +293,157 @@ async def process_request(text: str, state_manager: StateManager, output: bool =
|
|
|
446
293
|
|
|
447
294
|
start_idx = len(state_manager.session.messages)
|
|
448
295
|
|
|
449
|
-
def tool_callback_with_state(part,
|
|
296
|
+
def tool_callback_with_state(part, _):
|
|
450
297
|
return tool_handler(part, state_manager)
|
|
451
298
|
|
|
452
299
|
try:
|
|
453
|
-
from tunacode.utils.text_utils import expand_file_refs
|
|
454
|
-
|
|
455
300
|
text, referenced_files = expand_file_refs(text)
|
|
456
|
-
|
|
457
|
-
state_manager.session.files_in_context.add(file_path)
|
|
301
|
+
state_manager.session.files_in_context.update(referenced_files)
|
|
458
302
|
except ValueError as e:
|
|
459
303
|
await ui.error(str(e))
|
|
460
304
|
return
|
|
461
305
|
|
|
462
|
-
|
|
463
|
-
operation_cancelled = getattr(state_manager.session, "operation_cancelled", False)
|
|
464
|
-
if operation_cancelled is True:
|
|
465
|
-
logger.debug("Operation cancelled before agent processing")
|
|
306
|
+
if getattr(state_manager.session, "operation_cancelled", False) is True:
|
|
466
307
|
raise CancelledError("Operation was cancelled")
|
|
467
308
|
|
|
468
309
|
enable_streaming = state_manager.session.user_config.get("settings", {}).get(
|
|
469
310
|
"enable_streaming", True
|
|
470
311
|
)
|
|
471
312
|
|
|
313
|
+
# Create UsageTracker to ensure session cost tracking
|
|
314
|
+
model_registry = ModelRegistry()
|
|
315
|
+
parser = ApiResponseParser()
|
|
316
|
+
calculator = CostCalculator(model_registry)
|
|
317
|
+
usage_tracker = UsageTracker(parser, calculator, state_manager)
|
|
318
|
+
|
|
472
319
|
if enable_streaming:
|
|
473
320
|
await ui.spinner(False, state_manager.session.spinner, state_manager)
|
|
474
|
-
|
|
475
321
|
state_manager.session.is_streaming_active = True
|
|
476
|
-
|
|
477
322
|
streaming_panel = ui.StreamingAgentPanel()
|
|
478
323
|
await streaming_panel.start()
|
|
479
|
-
|
|
480
324
|
state_manager.session.streaming_panel = streaming_panel
|
|
481
325
|
|
|
482
326
|
try:
|
|
483
|
-
|
|
484
|
-
async def streaming_callback(content: str):
|
|
485
|
-
await streaming_panel.update(content)
|
|
486
|
-
|
|
487
327
|
res = await agent.process_request(
|
|
488
328
|
text,
|
|
489
329
|
state_manager.session.current_model,
|
|
490
330
|
state_manager,
|
|
491
331
|
tool_callback=tool_callback_with_state,
|
|
492
|
-
streaming_callback=
|
|
332
|
+
streaming_callback=lambda content: streaming_panel.update(content),
|
|
333
|
+
usage_tracker=usage_tracker,
|
|
493
334
|
)
|
|
494
335
|
finally:
|
|
495
336
|
await streaming_panel.stop()
|
|
496
337
|
state_manager.session.streaming_panel = None
|
|
497
338
|
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)
|
|
506
339
|
else:
|
|
507
|
-
# Use normal agent processing
|
|
508
340
|
res = await agent.process_request(
|
|
509
341
|
text,
|
|
510
342
|
state_manager.session.current_model,
|
|
511
343
|
state_manager,
|
|
512
344
|
tool_callback=tool_callback_with_state,
|
|
345
|
+
usage_tracker=usage_tracker,
|
|
513
346
|
)
|
|
514
347
|
|
|
515
|
-
#
|
|
516
|
-
|
|
517
|
-
|
|
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
|
+
):
|
|
518
353
|
await _handle_plan_approval(state_manager, text)
|
|
519
|
-
elif state_manager.is_plan_mode() and not getattr(
|
|
520
|
-
|
|
354
|
+
elif state_manager.is_plan_mode() and not getattr(
|
|
355
|
+
state_manager.session, "_continuing_from_plan", False
|
|
356
|
+
):
|
|
521
357
|
await _detect_and_handle_text_plan(state_manager, res, text)
|
|
522
358
|
|
|
523
359
|
if output:
|
|
524
360
|
if state_manager.session.show_thoughts:
|
|
525
|
-
|
|
526
|
-
for msg in new_msgs:
|
|
361
|
+
for msg in state_manager.session.messages[start_idx:]:
|
|
527
362
|
if isinstance(msg, dict) and "thought" in msg:
|
|
528
363
|
await ui.muted(f"THOUGHT: {msg['thought']}")
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
# Fallback: show that the request was processed
|
|
539
|
-
await ui.muted(MSG_REQUEST_COMPLETED)
|
|
540
|
-
else:
|
|
541
|
-
# Use the dedicated function for displaying agent output
|
|
542
|
-
await display_agent_output(res, enable_streaming, state_manager)
|
|
543
|
-
|
|
544
|
-
# Always show files in context after agent response
|
|
364
|
+
if not enable_streaming:
|
|
365
|
+
if (
|
|
366
|
+
not hasattr(res, "result")
|
|
367
|
+
or res.result is None
|
|
368
|
+
or not hasattr(res.result, "output")
|
|
369
|
+
):
|
|
370
|
+
await ui.muted(MSG_REQUEST_COMPLETED)
|
|
371
|
+
else:
|
|
372
|
+
await display_agent_output(res, enable_streaming, state_manager)
|
|
545
373
|
if state_manager.session.files_in_context:
|
|
546
374
|
filenames = [Path(f).name for f in sorted(state_manager.session.files_in_context)]
|
|
547
375
|
await ui.muted(f"Files in context: {', '.join(filenames)}")
|
|
548
376
|
|
|
549
|
-
# --- ERROR HANDLING ---
|
|
550
377
|
except CancelledError:
|
|
551
378
|
await ui.muted(MSG_REQUEST_CANCELLED)
|
|
552
379
|
except UserAbortError:
|
|
553
380
|
await ui.muted(MSG_OPERATION_ABORTED)
|
|
554
381
|
except UnexpectedModelBehavior as e:
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
patch_tool_messages(error_message, state_manager)
|
|
382
|
+
await ui.muted(str(e))
|
|
383
|
+
patch_tool_messages(str(e), state_manager)
|
|
558
384
|
except Exception as e:
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
return # Successfully recovered
|
|
562
|
-
|
|
563
|
-
agent_error = AgentError(f"Agent processing failed: {str(e)}")
|
|
564
|
-
agent_error.__cause__ = e # Preserve the original exception chain
|
|
565
|
-
await ui.error(str(e))
|
|
385
|
+
if not await attempt_tool_recovery(e, state_manager):
|
|
386
|
+
await ui.error(str(e))
|
|
566
387
|
finally:
|
|
567
388
|
await ui.spinner(False, state_manager.session.spinner, state_manager)
|
|
568
389
|
state_manager.session.current_task = None
|
|
569
|
-
# Reset cancellation flag when task completes (if attribute exists)
|
|
570
390
|
if hasattr(state_manager.session, "operation_cancelled"):
|
|
571
391
|
state_manager.session.operation_cancelled = False
|
|
572
|
-
|
|
573
392
|
if "multiline" in state_manager.session.input_sessions:
|
|
574
393
|
await run_in_terminal(
|
|
575
394
|
lambda: state_manager.session.input_sessions["multiline"].app.invalidate()
|
|
576
395
|
)
|
|
577
396
|
|
|
578
397
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
398
|
+
async def warm_code_index():
|
|
399
|
+
"""Pre-warm the code index in background for faster directory operations."""
|
|
400
|
+
try:
|
|
401
|
+
from tunacode.core.code_index import CodeIndex
|
|
402
|
+
|
|
403
|
+
# Build index in thread to avoid blocking
|
|
404
|
+
index = await asyncio.to_thread(lambda: CodeIndex.get_instance())
|
|
405
|
+
await asyncio.to_thread(index.build_index)
|
|
406
|
+
|
|
407
|
+
logger.debug(f"Code index pre-warmed with {len(index._all_files)} files")
|
|
408
|
+
except Exception as e:
|
|
409
|
+
logger.debug(f"Failed to pre-warm code index: {e}")
|
|
582
410
|
|
|
583
411
|
|
|
584
412
|
async def repl(state_manager: StateManager):
|
|
585
413
|
"""Main REPL loop that handles user interaction and input processing."""
|
|
414
|
+
import time
|
|
415
|
+
|
|
416
|
+
# Start pre-warming code index in background (non-blocking)
|
|
417
|
+
asyncio.create_task(warm_code_index())
|
|
418
|
+
|
|
586
419
|
action = None
|
|
587
420
|
abort_pressed = False
|
|
588
421
|
last_abort_time = 0.0
|
|
589
422
|
|
|
590
|
-
model_name = state_manager.session.current_model
|
|
591
423
|
max_tokens = (
|
|
592
424
|
state_manager.session.user_config.get("context_window_size") or DEFAULT_CONTEXT_WINDOW
|
|
593
425
|
)
|
|
594
426
|
state_manager.session.max_tokens = max_tokens
|
|
595
|
-
|
|
596
427
|
state_manager.session.update_token_count()
|
|
597
|
-
context_display = get_context_window_display(state_manager.session.total_tokens, max_tokens)
|
|
598
428
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
429
|
+
async def show_context():
|
|
430
|
+
context = get_context_window_display(state_manager.session.total_tokens, max_tokens)
|
|
431
|
+
|
|
432
|
+
# Get session cost for display
|
|
433
|
+
session_cost = 0.0
|
|
434
|
+
if state_manager.session.session_total_usage:
|
|
435
|
+
session_cost = float(state_manager.session.session_total_usage.get("cost", 0.0) or 0.0)
|
|
436
|
+
|
|
437
|
+
await ui.muted(f"• Model: {state_manager.session.current_model} • {context}")
|
|
438
|
+
if session_cost > 0:
|
|
439
|
+
await ui.muted(f"• Session Cost: ${session_cost:.4f}")
|
|
440
|
+
|
|
441
|
+
# Always show context
|
|
442
|
+
await show_context()
|
|
443
|
+
|
|
444
|
+
# Show startup message only once
|
|
445
|
+
if not hasattr(state_manager.session, "_startup_shown"):
|
|
602
446
|
await ui.success("Ready to assist")
|
|
603
|
-
await ui.line()
|
|
604
447
|
state_manager.session._startup_shown = True
|
|
605
448
|
|
|
606
449
|
instance = agent.get_or_create_agent(state_manager.session.current_model, state_manager)
|
|
@@ -610,17 +453,11 @@ async def repl(state_manager: StateManager):
|
|
|
610
453
|
try:
|
|
611
454
|
line = await ui.multiline_input(state_manager, _command_registry)
|
|
612
455
|
except UserAbortError:
|
|
613
|
-
import time
|
|
614
|
-
|
|
615
456
|
current_time = time.time()
|
|
616
|
-
|
|
617
|
-
# Reset if more than 3 seconds have passed
|
|
618
457
|
if current_time - last_abort_time > 3.0:
|
|
619
458
|
abort_pressed = False
|
|
620
|
-
|
|
621
459
|
if abort_pressed:
|
|
622
460
|
break
|
|
623
|
-
|
|
624
461
|
abort_pressed = True
|
|
625
462
|
last_abort_time = current_time
|
|
626
463
|
await ui.warning(MSG_HIT_ABORT_KEY)
|
|
@@ -628,7 +465,6 @@ async def repl(state_manager: StateManager):
|
|
|
628
465
|
|
|
629
466
|
if not line:
|
|
630
467
|
continue
|
|
631
|
-
|
|
632
468
|
abort_pressed = False
|
|
633
469
|
|
|
634
470
|
if line.lower() in ["exit", "quit"]:
|
|
@@ -639,54 +475,40 @@ async def repl(state_manager: StateManager):
|
|
|
639
475
|
if action == "restart":
|
|
640
476
|
break
|
|
641
477
|
elif isinstance(action, str) and action:
|
|
642
|
-
# If the command returned a string (e.g., from template shortcut),
|
|
643
|
-
# process it as a prompt
|
|
644
478
|
line = action
|
|
645
|
-
# Fall through to process as normal text
|
|
646
479
|
else:
|
|
647
480
|
continue
|
|
648
481
|
|
|
649
482
|
if line.startswith("!"):
|
|
650
483
|
command = line[1:].strip()
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
484
|
+
await ui.panel(
|
|
485
|
+
"Tool(bash)",
|
|
486
|
+
f"Command: {command or 'Interactive shell'}",
|
|
487
|
+
border_style="yellow",
|
|
488
|
+
)
|
|
654
489
|
|
|
655
490
|
def run_shell():
|
|
656
491
|
try:
|
|
657
492
|
if command:
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
capture_output=False,
|
|
664
|
-
)
|
|
665
|
-
if result.returncode != 0:
|
|
666
|
-
ui.console.print(
|
|
667
|
-
f"\nCommand exited with code {result.returncode}"
|
|
668
|
-
)
|
|
669
|
-
except CommandSecurityError as e:
|
|
670
|
-
ui.console.print(f"\nSecurity validation failed: {str(e)}")
|
|
671
|
-
ui.console.print(
|
|
672
|
-
"If you need to run this command, please ensure it's safe."
|
|
673
|
-
)
|
|
493
|
+
result = safe_subprocess_run(
|
|
494
|
+
command, shell=True, validate=True, capture_output=False
|
|
495
|
+
)
|
|
496
|
+
if result.returncode != 0:
|
|
497
|
+
ui.console.print(f"\nCommand exited with code {result.returncode}")
|
|
674
498
|
else:
|
|
675
|
-
|
|
676
|
-
|
|
499
|
+
subprocess.run(os.environ.get(SHELL_ENV_VAR, DEFAULT_SHELL))
|
|
500
|
+
except CommandSecurityError as e:
|
|
501
|
+
ui.console.print(f"\nSecurity validation failed: {str(e)}")
|
|
677
502
|
except Exception as e:
|
|
678
503
|
ui.console.print(f"\nShell command failed: {str(e)}")
|
|
679
504
|
|
|
680
505
|
await run_in_terminal(run_shell)
|
|
681
|
-
await ui.line()
|
|
682
506
|
continue
|
|
683
507
|
|
|
684
|
-
# --- AGENT REQUEST PROCESSING ---
|
|
685
508
|
if state_manager.session.current_task and not state_manager.session.current_task.done():
|
|
686
509
|
await ui.muted(MSG_AGENT_BUSY)
|
|
687
510
|
continue
|
|
688
511
|
|
|
689
|
-
# Reset cancellation flag for new operations (if attribute exists)
|
|
690
512
|
if hasattr(state_manager.session, "operation_cancelled"):
|
|
691
513
|
state_manager.session.operation_cancelled = False
|
|
692
514
|
|
|
@@ -696,39 +518,24 @@ async def repl(state_manager: StateManager):
|
|
|
696
518
|
await state_manager.session.current_task
|
|
697
519
|
|
|
698
520
|
state_manager.session.update_token_count()
|
|
699
|
-
|
|
700
|
-
state_manager.session.total_tokens, state_manager.session.max_tokens
|
|
701
|
-
)
|
|
702
|
-
# Only show model/context info if thoughts are enabled
|
|
703
|
-
if state_manager.session.show_thoughts:
|
|
704
|
-
await ui.muted(
|
|
705
|
-
f"• Model: {state_manager.session.current_model} • {context_display}"
|
|
706
|
-
)
|
|
521
|
+
await show_context()
|
|
707
522
|
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
f" - [bold green]Total Session Cost: ${total_cost:.4f}[/bold green]"
|
|
728
|
-
)
|
|
729
|
-
ui.console.print(summary)
|
|
730
|
-
except (TypeError, ValueError) as e:
|
|
731
|
-
# Skip displaying summary if values can't be converted to numbers
|
|
732
|
-
logger.debug(f"Failed to display token usage summary: {e}")
|
|
733
|
-
|
|
734
|
-
await ui.info(MSG_SESSION_ENDED)
|
|
523
|
+
if action == "restart":
|
|
524
|
+
await repl(state_manager)
|
|
525
|
+
else:
|
|
526
|
+
session_total = state_manager.session.session_total_usage
|
|
527
|
+
if session_total:
|
|
528
|
+
try:
|
|
529
|
+
total_tokens = int(session_total.get("prompt_tokens", 0) or 0) + int(
|
|
530
|
+
session_total.get("completion_tokens", 0) or 0
|
|
531
|
+
)
|
|
532
|
+
total_cost = float(session_total.get("cost", 0) or 0)
|
|
533
|
+
if total_tokens > 0 or total_cost > 0:
|
|
534
|
+
ui.console.print(
|
|
535
|
+
f"\n[bold cyan]TunaCode Session Summary[/bold cyan]\n"
|
|
536
|
+
f" - Total Tokens: {total_tokens:,}\n"
|
|
537
|
+
f" - Total Cost: ${total_cost:.4f}"
|
|
538
|
+
)
|
|
539
|
+
except (TypeError, ValueError):
|
|
540
|
+
pass
|
|
541
|
+
await ui.info(MSG_SESSION_ENDED)
|