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
tunacode/ui/input.py
CHANGED
|
@@ -76,19 +76,28 @@ async def multiline_input(
|
|
|
76
76
|
) -> str:
|
|
77
77
|
"""Get multiline input from the user with @file completion and highlighting."""
|
|
78
78
|
kb = create_key_bindings(state_manager)
|
|
79
|
+
|
|
80
|
+
# Clear any residual terminal output
|
|
81
|
+
import sys
|
|
82
|
+
sys.stdout.flush()
|
|
83
|
+
|
|
84
|
+
# Full placeholder with all keyboard shortcuts
|
|
79
85
|
placeholder = formatted_text(
|
|
80
86
|
(
|
|
81
87
|
"<darkgrey>"
|
|
82
88
|
"<bold>Enter</bold> to submit • "
|
|
83
89
|
"<bold>Esc + Enter</bold> for new line • "
|
|
84
90
|
"<bold>Esc twice</bold> to cancel • "
|
|
91
|
+
"<bold>Shift + Tab</bold> toggle plan mode • "
|
|
85
92
|
"<bold>/help</bold> for commands"
|
|
86
93
|
"</darkgrey>"
|
|
87
94
|
)
|
|
88
95
|
)
|
|
89
|
-
|
|
96
|
+
|
|
97
|
+
# Display input area (Plan Mode indicator is handled dynamically in prompt manager)
|
|
98
|
+
result = await input(
|
|
90
99
|
"multiline",
|
|
91
|
-
pretext="> ",
|
|
100
|
+
pretext="> ",
|
|
92
101
|
key_bindings=kb,
|
|
93
102
|
multiline=True,
|
|
94
103
|
placeholder=placeholder,
|
|
@@ -96,3 +105,5 @@ async def multiline_input(
|
|
|
96
105
|
lexer=FileReferenceLexer(),
|
|
97
106
|
state_manager=state_manager,
|
|
98
107
|
)
|
|
108
|
+
|
|
109
|
+
return result
|
tunacode/ui/keybindings.py
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
"""Key binding handlers for TunaCode UI."""
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
-
import time
|
|
5
4
|
|
|
6
|
-
from prompt_toolkit.application import run_in_terminal
|
|
7
5
|
from prompt_toolkit.key_binding import KeyBindings
|
|
8
6
|
|
|
9
7
|
from ..core.state import StateManager
|
|
@@ -32,38 +30,12 @@ def create_key_bindings(state_manager: StateManager = None) -> KeyBindings:
|
|
|
32
30
|
|
|
33
31
|
@kb.add("escape")
|
|
34
32
|
def _escape(event):
|
|
35
|
-
"""Handle ESC key
|
|
36
|
-
|
|
37
|
-
logger.debug("Escape key pressed without state manager")
|
|
38
|
-
return
|
|
33
|
+
"""Handle ESC key - trigger Ctrl+C behavior."""
|
|
34
|
+
logger.debug("ESC key pressed - simulating Ctrl+C")
|
|
39
35
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
# Reset counter if too much time has passed (3 seconds timeout)
|
|
44
|
-
if session.last_esc_time and (current_time - session.last_esc_time) > 3.0:
|
|
45
|
-
session.esc_press_count = 0
|
|
46
|
-
|
|
47
|
-
session.esc_press_count = (session.esc_press_count or 0) + 1
|
|
48
|
-
session.last_esc_time = current_time
|
|
49
|
-
|
|
50
|
-
logger.debug(f"ESC key pressed: count={session.esc_press_count}, time={current_time}")
|
|
51
|
-
|
|
52
|
-
if session.esc_press_count == 1:
|
|
53
|
-
# First ESC press - show warning message
|
|
54
|
-
from ..ui.output import warning
|
|
55
|
-
|
|
56
|
-
run_in_terminal(lambda: warning("Hit ESC again within 3 seconds to cancel operation"))
|
|
57
|
-
logger.debug("First ESC press - showing warning")
|
|
58
|
-
else:
|
|
59
|
-
# Second ESC press - cancel operation
|
|
60
|
-
session.esc_press_count = 0 # Reset counter
|
|
61
|
-
logger.debug("Second ESC press - initiating cancellation")
|
|
62
|
-
|
|
63
|
-
# Mark the session as being cancelled to prevent new operations
|
|
64
|
-
session.operation_cancelled = True
|
|
65
|
-
|
|
66
|
-
current_task = session.current_task
|
|
36
|
+
# Cancel any active task if present
|
|
37
|
+
if state_manager and hasattr(state_manager.session, "current_task"):
|
|
38
|
+
current_task = state_manager.session.current_task
|
|
67
39
|
if current_task and not current_task.done():
|
|
68
40
|
logger.debug(f"Cancelling current task: {current_task}")
|
|
69
41
|
try:
|
|
@@ -71,12 +43,28 @@ def create_key_bindings(state_manager: StateManager = None) -> KeyBindings:
|
|
|
71
43
|
logger.debug("Task cancellation initiated successfully")
|
|
72
44
|
except Exception as e:
|
|
73
45
|
logger.debug(f"Failed to cancel task: {e}")
|
|
46
|
+
|
|
47
|
+
# Trigger the same behavior as Ctrl+C by sending the signal
|
|
48
|
+
import os
|
|
49
|
+
import signal
|
|
50
|
+
os.kill(os.getpid(), signal.SIGINT)
|
|
51
|
+
|
|
52
|
+
@kb.add("s-tab") # shift+tab
|
|
53
|
+
def _toggle_plan_mode(event):
|
|
54
|
+
"""Toggle between Plan Mode and normal mode."""
|
|
55
|
+
if state_manager:
|
|
56
|
+
# Toggle the state
|
|
57
|
+
if state_manager.is_plan_mode():
|
|
58
|
+
state_manager.exit_plan_mode()
|
|
59
|
+
logger.debug("Toggled to normal mode via Shift+Tab")
|
|
74
60
|
else:
|
|
75
|
-
|
|
61
|
+
state_manager.enter_plan_mode()
|
|
62
|
+
logger.debug("Toggled to Plan Mode via Shift+Tab")
|
|
63
|
+
|
|
64
|
+
# Clear the current buffer and refresh the display
|
|
65
|
+
event.current_buffer.reset()
|
|
76
66
|
|
|
77
|
-
# Force
|
|
78
|
-
|
|
79
|
-
logger.debug("Raising KeyboardInterrupt to abort current operation")
|
|
80
|
-
raise KeyboardInterrupt()
|
|
67
|
+
# Force a refresh of the application without exiting
|
|
68
|
+
event.app.invalidate()
|
|
81
69
|
|
|
82
70
|
return kb
|
tunacode/ui/output.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""Output and display functions for TunaCode UI."""
|
|
2
2
|
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
3
5
|
from prompt_toolkit.application import run_in_terminal
|
|
4
6
|
from rich.console import Console
|
|
5
7
|
from rich.padding import Padding
|
|
@@ -109,20 +111,39 @@ async def show_update_message(latest_version: str) -> None:
|
|
|
109
111
|
await update_available(latest_version)
|
|
110
112
|
|
|
111
113
|
|
|
112
|
-
async def spinner(
|
|
113
|
-
|
|
114
|
+
async def spinner(
|
|
115
|
+
show: bool = True,
|
|
116
|
+
spinner_obj=None,
|
|
117
|
+
state_manager: StateManager = None,
|
|
118
|
+
message: Optional[str] = None,
|
|
119
|
+
):
|
|
120
|
+
"""Manage a spinner display with dynamic message support.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
show: Whether to show (True) or hide (False) the spinner
|
|
124
|
+
spinner_obj: Existing spinner object to reuse
|
|
125
|
+
state_manager: State manager instance for storing spinner
|
|
126
|
+
message: Optional custom message to display (uses UI_THINKING_MESSAGE if None)
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
The spinner object for further manipulation
|
|
130
|
+
"""
|
|
114
131
|
icon = SPINNER_TYPE
|
|
115
|
-
|
|
132
|
+
display_message = message or UI_THINKING_MESSAGE
|
|
116
133
|
|
|
117
134
|
# Get spinner from state manager if available
|
|
118
135
|
if spinner_obj is None and state_manager:
|
|
119
136
|
spinner_obj = state_manager.session.spinner
|
|
120
137
|
|
|
121
138
|
if not spinner_obj:
|
|
122
|
-
spinner_obj = await run_in_terminal(lambda: console.status(
|
|
139
|
+
spinner_obj = await run_in_terminal(lambda: console.status(display_message, spinner=icon))
|
|
123
140
|
# Store it back in state manager if available
|
|
124
141
|
if state_manager:
|
|
125
142
|
state_manager.session.spinner = spinner_obj
|
|
143
|
+
else:
|
|
144
|
+
# Update existing spinner message if provided
|
|
145
|
+
if message and hasattr(spinner_obj, "update"):
|
|
146
|
+
spinner_obj.update(display_message)
|
|
126
147
|
|
|
127
148
|
if show:
|
|
128
149
|
spinner_obj.start()
|
|
@@ -132,6 +153,20 @@ async def spinner(show: bool = True, spinner_obj=None, state_manager: StateManag
|
|
|
132
153
|
return spinner_obj
|
|
133
154
|
|
|
134
155
|
|
|
156
|
+
async def update_spinner_message(message: str, state_manager: StateManager = None):
|
|
157
|
+
"""Update the spinner message if a spinner is active.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
message: New message to display
|
|
161
|
+
state_manager: State manager instance containing spinner
|
|
162
|
+
"""
|
|
163
|
+
if state_manager and state_manager.session.spinner:
|
|
164
|
+
spinner_obj = state_manager.session.spinner
|
|
165
|
+
if hasattr(spinner_obj, "update"):
|
|
166
|
+
# Rich's Status object supports update method
|
|
167
|
+
await run_in_terminal(lambda: spinner_obj.update(message))
|
|
168
|
+
|
|
169
|
+
|
|
135
170
|
def get_context_window_display(total_tokens: int, max_tokens: int) -> str:
|
|
136
171
|
"""
|
|
137
172
|
Create a color-coded display for the context window status.
|
tunacode/ui/panels.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""Panel display functions for TunaCode UI."""
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
4
|
+
import time
|
|
3
5
|
from typing import Any, Optional, Union
|
|
4
6
|
|
|
5
7
|
from rich.box import ROUNDED
|
|
@@ -81,11 +83,24 @@ async def agent(text: str, bottom: int = 1) -> None:
|
|
|
81
83
|
class StreamingAgentPanel:
|
|
82
84
|
"""Streaming agent panel using Rich.Live for progressive display."""
|
|
83
85
|
|
|
86
|
+
bottom: int
|
|
87
|
+
title: str
|
|
88
|
+
content: str
|
|
89
|
+
live: Optional[Live]
|
|
90
|
+
_last_update_time: float
|
|
91
|
+
_dots_task: Optional[asyncio.Task]
|
|
92
|
+
_dots_count: int
|
|
93
|
+
_show_dots: bool
|
|
94
|
+
|
|
84
95
|
def __init__(self, bottom: int = 1):
|
|
85
96
|
self.bottom = bottom
|
|
86
97
|
self.title = f"[bold {colors.primary}]●[/bold {colors.primary}] {APP_NAME}"
|
|
87
98
|
self.content = ""
|
|
88
99
|
self.live = None
|
|
100
|
+
self._last_update_time = 0.0
|
|
101
|
+
self._dots_task = None
|
|
102
|
+
self._dots_count = 0
|
|
103
|
+
self._show_dots = False
|
|
89
104
|
|
|
90
105
|
def _create_panel(self) -> Padding:
|
|
91
106
|
"""Create a Rich panel with current content."""
|
|
@@ -94,11 +109,19 @@ class StreamingAgentPanel:
|
|
|
94
109
|
|
|
95
110
|
from tunacode.constants import UI_THINKING_MESSAGE
|
|
96
111
|
|
|
97
|
-
#
|
|
112
|
+
# Show "Thinking..." only when no content has arrived yet
|
|
98
113
|
if not self.content:
|
|
99
114
|
content_renderable: Union[Text, Markdown] = Text.from_markup(UI_THINKING_MESSAGE)
|
|
100
115
|
else:
|
|
101
|
-
|
|
116
|
+
# Once we have content, show it with optional dots animation
|
|
117
|
+
display_content = self.content
|
|
118
|
+
# Add animated dots if we're waiting for more content
|
|
119
|
+
if self._show_dots:
|
|
120
|
+
# Cycle through: "", ".", "..", "..."
|
|
121
|
+
dots_patterns = ["", ".", "..", "..."]
|
|
122
|
+
dots = dots_patterns[self._dots_count % len(dots_patterns)]
|
|
123
|
+
display_content = self.content.rstrip() + dots
|
|
124
|
+
content_renderable = Markdown(display_content)
|
|
102
125
|
panel_obj = Panel(
|
|
103
126
|
Padding(content_renderable, (0, 1, 0, 1)),
|
|
104
127
|
title=f"[bold]{self.title}[/bold]",
|
|
@@ -117,31 +140,85 @@ class StreamingAgentPanel:
|
|
|
117
140
|
),
|
|
118
141
|
)
|
|
119
142
|
|
|
143
|
+
async def _animate_dots(self):
|
|
144
|
+
"""Animate dots after a pause in streaming."""
|
|
145
|
+
while True:
|
|
146
|
+
await asyncio.sleep(0.5)
|
|
147
|
+
current_time = time.time()
|
|
148
|
+
# Only show dots after 1 second of no updates
|
|
149
|
+
if current_time - self._last_update_time > 1.0:
|
|
150
|
+
self._show_dots = True
|
|
151
|
+
self._dots_count += 1
|
|
152
|
+
if self.live:
|
|
153
|
+
self.live.update(self._create_panel())
|
|
154
|
+
else:
|
|
155
|
+
self._show_dots = False
|
|
156
|
+
self._dots_count = 0
|
|
157
|
+
|
|
120
158
|
async def start(self):
|
|
121
159
|
"""Start the live streaming display."""
|
|
122
160
|
from .output import console
|
|
123
161
|
|
|
124
162
|
self.live = Live(self._create_panel(), console=console, refresh_per_second=4)
|
|
125
163
|
self.live.start()
|
|
164
|
+
self._last_update_time = time.time()
|
|
165
|
+
# Start the dots animation task
|
|
166
|
+
self._dots_task = asyncio.create_task(self._animate_dots())
|
|
126
167
|
|
|
127
168
|
async def update(self, content_chunk: str):
|
|
128
169
|
"""Update the streaming display with new content."""
|
|
129
170
|
# Defensive: some providers may yield None chunks intermittently
|
|
130
171
|
if content_chunk is None:
|
|
131
172
|
content_chunk = ""
|
|
173
|
+
|
|
174
|
+
# Filter out plan mode system prompts and tool definitions from streaming
|
|
175
|
+
if any(phrase in str(content_chunk) for phrase in [
|
|
176
|
+
"🔧 PLAN MODE",
|
|
177
|
+
"TOOL EXECUTION ONLY",
|
|
178
|
+
"planning assistant that ONLY communicates",
|
|
179
|
+
"namespace functions {",
|
|
180
|
+
"namespace multi_tool_use {",
|
|
181
|
+
"You are trained on data up to"
|
|
182
|
+
]):
|
|
183
|
+
return
|
|
184
|
+
|
|
132
185
|
# Ensure type safety for concatenation
|
|
133
186
|
self.content = (self.content or "") + str(content_chunk)
|
|
187
|
+
|
|
188
|
+
# Reset the update timer when we get new content
|
|
189
|
+
self._last_update_time = time.time()
|
|
190
|
+
self._show_dots = False # Hide dots immediately when new content arrives
|
|
191
|
+
|
|
134
192
|
if self.live:
|
|
135
193
|
self.live.update(self._create_panel())
|
|
136
194
|
|
|
137
195
|
async def set_content(self, content: str):
|
|
138
196
|
"""Set the complete content (overwrites previous)."""
|
|
197
|
+
# Filter out plan mode system prompts and tool definitions
|
|
198
|
+
if any(phrase in str(content) for phrase in [
|
|
199
|
+
"🔧 PLAN MODE",
|
|
200
|
+
"TOOL EXECUTION ONLY",
|
|
201
|
+
"planning assistant that ONLY communicates",
|
|
202
|
+
"namespace functions {",
|
|
203
|
+
"namespace multi_tool_use {",
|
|
204
|
+
"You are trained on data up to"
|
|
205
|
+
]):
|
|
206
|
+
return
|
|
207
|
+
|
|
139
208
|
self.content = content
|
|
140
209
|
if self.live:
|
|
141
210
|
self.live.update(self._create_panel())
|
|
142
211
|
|
|
143
212
|
async def stop(self):
|
|
144
213
|
"""Stop the live streaming display."""
|
|
214
|
+
# Cancel the dots animation task
|
|
215
|
+
if self._dots_task:
|
|
216
|
+
self._dots_task.cancel()
|
|
217
|
+
try:
|
|
218
|
+
await self._dots_task
|
|
219
|
+
except asyncio.CancelledError:
|
|
220
|
+
pass
|
|
221
|
+
|
|
145
222
|
if self.live:
|
|
146
223
|
# Get the console before stopping the live display
|
|
147
224
|
from .output import console
|
tunacode/ui/prompt_manager.py
CHANGED
|
@@ -100,15 +100,32 @@ class PromptManager:
|
|
|
100
100
|
"""
|
|
101
101
|
session = self.get_session(session_key, config)
|
|
102
102
|
|
|
103
|
-
# Create a custom prompt that changes based on input
|
|
103
|
+
# Create a custom prompt that changes based on input and plan mode
|
|
104
104
|
def get_prompt():
|
|
105
|
+
# Start with the base prompt
|
|
106
|
+
base_prompt = prompt
|
|
107
|
+
|
|
108
|
+
# Add Plan Mode indicator if active
|
|
109
|
+
if (self.state_manager and
|
|
110
|
+
self.state_manager.is_plan_mode() and
|
|
111
|
+
"PLAN MODE ON" not in base_prompt):
|
|
112
|
+
base_prompt = '<style fg="#40E0D0"><bold>⏸ PLAN MODE ON</bold></style>\n' + base_prompt
|
|
113
|
+
elif (self.state_manager and
|
|
114
|
+
not self.state_manager.is_plan_mode() and
|
|
115
|
+
("⏸" in base_prompt or "PLAN MODE ON" in base_prompt)):
|
|
116
|
+
# Remove plan mode indicator if no longer in plan mode
|
|
117
|
+
lines = base_prompt.split("\n")
|
|
118
|
+
if len(lines) > 1 and ("⏸" in lines[0] or "PLAN MODE ON" in lines[0]):
|
|
119
|
+
base_prompt = "\n".join(lines[1:])
|
|
120
|
+
|
|
105
121
|
# Check if current buffer starts with "!"
|
|
106
122
|
if hasattr(session.app, "current_buffer") and session.app.current_buffer:
|
|
107
123
|
text = session.app.current_buffer.text
|
|
108
124
|
if text.startswith("!"):
|
|
109
125
|
# Use bright yellow background with black text for high visibility
|
|
110
126
|
return HTML('<style bg="#ffcc00" fg="black"><b> ◆ BASH MODE ◆ </b></style> ')
|
|
111
|
-
|
|
127
|
+
|
|
128
|
+
return HTML(base_prompt) if isinstance(base_prompt, str) else base_prompt
|
|
112
129
|
|
|
113
130
|
try:
|
|
114
131
|
# Get user input with dynamic prompt
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Tool description mappings for user-friendly spinner messages."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_tool_description(tool_name: str, args: Optional[Dict] = None) -> str:
|
|
7
|
+
"""
|
|
8
|
+
Get a human-readable description for a tool execution.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
tool_name: Name of the tool being executed
|
|
12
|
+
args: Optional tool arguments for more specific descriptions
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
User-friendly description of the tool operation
|
|
16
|
+
"""
|
|
17
|
+
# Base descriptions for each tool
|
|
18
|
+
base_descriptions = {
|
|
19
|
+
# File operations
|
|
20
|
+
"read_file": "Reading file",
|
|
21
|
+
"write_file": "Writing file",
|
|
22
|
+
"update_file": "Updating file",
|
|
23
|
+
"create_file": "Creating file",
|
|
24
|
+
"delete_file": "Deleting file",
|
|
25
|
+
# Directory operations
|
|
26
|
+
"list_dir": "Listing directory",
|
|
27
|
+
"create_dir": "Creating directory",
|
|
28
|
+
"delete_dir": "Deleting directory",
|
|
29
|
+
# Search operations
|
|
30
|
+
"grep": "Searching files",
|
|
31
|
+
"glob": "Finding files",
|
|
32
|
+
"find_files": "Searching for files",
|
|
33
|
+
# Code operations
|
|
34
|
+
"run_command": "Executing command",
|
|
35
|
+
"bash": "Running shell command",
|
|
36
|
+
"python": "Executing Python code",
|
|
37
|
+
# Analysis operations
|
|
38
|
+
"analyze_code": "Analyzing code",
|
|
39
|
+
"lint": "Running linter",
|
|
40
|
+
"format_code": "Formatting code",
|
|
41
|
+
# Version control
|
|
42
|
+
"git_status": "Checking git status",
|
|
43
|
+
"git_diff": "Getting git diff",
|
|
44
|
+
"git_commit": "Creating git commit",
|
|
45
|
+
# Testing
|
|
46
|
+
"run_tests": "Running tests",
|
|
47
|
+
"test": "Executing tests",
|
|
48
|
+
# Documentation
|
|
49
|
+
"generate_docs": "Generating documentation",
|
|
50
|
+
"update_docs": "Updating documentation",
|
|
51
|
+
# Default
|
|
52
|
+
"unknown": "Processing",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# Get base description
|
|
56
|
+
base_desc = base_descriptions.get(tool_name, f"Executing {tool_name}")
|
|
57
|
+
|
|
58
|
+
# Add specific details from args if available
|
|
59
|
+
if args:
|
|
60
|
+
if tool_name == "read_file" and "file_path" in args:
|
|
61
|
+
return f"{base_desc}: {args['file_path']}"
|
|
62
|
+
elif tool_name == "write_file" and "file_path" in args:
|
|
63
|
+
return f"{base_desc}: {args['file_path']}"
|
|
64
|
+
elif tool_name == "update_file" and "file_path" in args:
|
|
65
|
+
return f"{base_desc}: {args['file_path']}"
|
|
66
|
+
elif tool_name == "list_dir" and "directory" in args:
|
|
67
|
+
return f"{base_desc}: {args['directory']}"
|
|
68
|
+
elif tool_name == "grep" and "pattern" in args:
|
|
69
|
+
pattern = args["pattern"]
|
|
70
|
+
# Truncate long patterns
|
|
71
|
+
if len(pattern) > 30:
|
|
72
|
+
pattern = pattern[:27] + "..."
|
|
73
|
+
return f"{base_desc} for: {pattern}"
|
|
74
|
+
elif tool_name == "glob" and "pattern" in args:
|
|
75
|
+
return f"{base_desc}: {args['pattern']}"
|
|
76
|
+
elif tool_name == "run_command" and "command" in args:
|
|
77
|
+
cmd = args["command"]
|
|
78
|
+
# Truncate long commands
|
|
79
|
+
if len(cmd) > 40:
|
|
80
|
+
cmd = cmd[:37] + "..."
|
|
81
|
+
return f"{base_desc}: {cmd}"
|
|
82
|
+
elif tool_name == "bash" and "command" in args:
|
|
83
|
+
cmd = args["command"]
|
|
84
|
+
if len(cmd) > 40:
|
|
85
|
+
cmd = cmd[:37] + "..."
|
|
86
|
+
return f"{base_desc}: {cmd}"
|
|
87
|
+
|
|
88
|
+
return base_desc
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def get_batch_description(tool_count: int, tool_names: Optional[list] = None) -> str:
|
|
92
|
+
"""
|
|
93
|
+
Get a description for batch tool execution.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
tool_count: Number of tools being executed
|
|
97
|
+
tool_names: Optional list of tool names for more detail
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Description of the batch operation
|
|
101
|
+
"""
|
|
102
|
+
if tool_count == 1:
|
|
103
|
+
return "Executing 1 tool"
|
|
104
|
+
|
|
105
|
+
if tool_names and len(set(tool_names)) == 1:
|
|
106
|
+
# All tools are the same type
|
|
107
|
+
tool_type = tool_names[0]
|
|
108
|
+
if tool_type == "read_file":
|
|
109
|
+
return f"Reading {tool_count} files in parallel"
|
|
110
|
+
elif tool_type == "grep":
|
|
111
|
+
return f"Searching {tool_count} patterns in parallel"
|
|
112
|
+
elif tool_type == "list_dir":
|
|
113
|
+
return f"Listing {tool_count} directories in parallel"
|
|
114
|
+
|
|
115
|
+
return f"Executing {tool_count} tools in parallel"
|
tunacode/ui/tool_ui.py
CHANGED
|
@@ -76,8 +76,9 @@ class ToolUI:
|
|
|
76
76
|
|
|
77
77
|
# Show file content on write_file
|
|
78
78
|
elif tool_name == TOOL_WRITE_FILE:
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
lang = ext_to_lang(args["filepath"])
|
|
80
|
+
code_block = f"```{lang}\n{args['content']}\n```"
|
|
81
|
+
return code_block
|
|
81
82
|
|
|
82
83
|
# Default to showing key and value on new line
|
|
83
84
|
content = ""
|
tunacode/utils/message_utils.py
CHANGED
|
@@ -9,11 +9,21 @@ def get_message_content(message: Any) -> str:
|
|
|
9
9
|
return message
|
|
10
10
|
if isinstance(message, dict):
|
|
11
11
|
if "content" in message:
|
|
12
|
-
|
|
12
|
+
content = message["content"]
|
|
13
|
+
# Handle nested content structures
|
|
14
|
+
if isinstance(content, list):
|
|
15
|
+
return " ".join(get_message_content(item) for item in content)
|
|
16
|
+
return str(content)
|
|
13
17
|
if "thought" in message:
|
|
14
|
-
return message["thought"]
|
|
18
|
+
return str(message["thought"])
|
|
15
19
|
if hasattr(message, "content"):
|
|
16
|
-
|
|
20
|
+
content = message.content
|
|
21
|
+
if isinstance(content, list):
|
|
22
|
+
return " ".join(get_message_content(item) for item in content)
|
|
23
|
+
return str(content)
|
|
17
24
|
if hasattr(message, "parts"):
|
|
18
|
-
|
|
25
|
+
parts = message.parts
|
|
26
|
+
if isinstance(parts, list):
|
|
27
|
+
return " ".join(get_message_content(part) for part in parts)
|
|
28
|
+
return str(parts)
|
|
19
29
|
return ""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tunacode-cli
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.56
|
|
4
4
|
Summary: Your agentic CLI developer.
|
|
5
5
|
Author-email: larock22 <noreply@github.com>
|
|
6
6
|
License: MIT
|
|
@@ -39,6 +39,7 @@ Requires-Dist: vulture>=2.7; extra == "dev"
|
|
|
39
39
|
Requires-Dist: unimport>=1.0.0; extra == "dev"
|
|
40
40
|
Requires-Dist: autoflake>=2.0.0; extra == "dev"
|
|
41
41
|
Requires-Dist: dead>=1.5.0; extra == "dev"
|
|
42
|
+
Requires-Dist: hatch>=1.6.0; extra == "dev"
|
|
42
43
|
Dynamic: license-file
|
|
43
44
|
|
|
44
45
|
# TunaCode CLI
|
|
@@ -106,7 +107,7 @@ tunacode --model "anthropic:claude-3.5-sonnet" --key "sk-ant-your-anthropic-key"
|
|
|
106
107
|
tunacode --model "openrouter:openai/gpt-4o" --key "sk-or-your-openrouter-key"
|
|
107
108
|
```
|
|
108
109
|
|
|
109
|
-
Your config is saved to `~/.config/tunacode.json` (edit directly with `nvim ~/.config/tunacode.json`)
|
|
110
|
+
Your config is saved to `~/.config/tunacode.json`. This file stores your API keys, model preferences, and runtime settings like `max_iterations` (default: 40) and `context_window_size`. You can edit it directly with `nvim ~/.config/tunacode.json` or see [the complete configuration example](documentation/configuration/config-file-example.md) for all available options.
|
|
110
111
|
|
|
111
112
|
### Recommended Models
|
|
112
113
|
|
|
@@ -243,7 +244,7 @@ tunacode --model "anthropic:claude-3.5-sonnet" --key "sk-ant-your-anthropic-key"
|
|
|
243
244
|
tunacode --model "openrouter:openai/gpt-4o" --key "sk-or-your-openrouter-key"
|
|
244
245
|
```
|
|
245
246
|
|
|
246
|
-
Your config is saved to `~/.config/tunacode.json` (edit directly with `nvim ~/.config/tunacode.json`)
|
|
247
|
+
Your config is saved to `~/.config/tunacode.json`. This file stores your API keys, model preferences, and runtime settings like `max_iterations` (default: 40) and `context_window_size`. You can edit it directly with `nvim ~/.config/tunacode.json` or see [the complete configuration example](documentation/configuration/config-file-example.md) for all available options.
|
|
247
248
|
|
|
248
249
|
### Recommended Models
|
|
249
250
|
|