tunacode-cli 0.0.35__py3-none-any.whl → 0.0.37__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 +62 -0
- tunacode/cli/commands/base.py +99 -0
- tunacode/cli/commands/implementations/__init__.py +38 -0
- tunacode/cli/commands/implementations/conversation.py +115 -0
- tunacode/cli/commands/implementations/debug.py +189 -0
- tunacode/cli/commands/implementations/development.py +77 -0
- tunacode/cli/commands/implementations/model.py +61 -0
- tunacode/cli/commands/implementations/system.py +216 -0
- tunacode/cli/commands/registry.py +236 -0
- tunacode/cli/repl.py +91 -30
- tunacode/configuration/settings.py +9 -2
- tunacode/constants.py +1 -1
- tunacode/core/agents/main.py +53 -3
- tunacode/core/agents/utils.py +304 -0
- tunacode/core/setup/config_setup.py +0 -1
- tunacode/core/state.py +13 -2
- tunacode/setup.py +7 -2
- tunacode/tools/read_file.py +8 -2
- tunacode/tools/read_file_async_poc.py +18 -10
- tunacode/tools/run_command.py +11 -4
- tunacode/ui/console.py +31 -4
- tunacode/ui/output.py +7 -2
- tunacode/ui/panels.py +98 -5
- tunacode/ui/utils.py +3 -0
- tunacode/utils/text_utils.py +6 -2
- {tunacode_cli-0.0.35.dist-info → tunacode_cli-0.0.37.dist-info}/METADATA +17 -17
- {tunacode_cli-0.0.35.dist-info → tunacode_cli-0.0.37.dist-info}/RECORD +31 -21
- tunacode/cli/commands.py +0 -893
- {tunacode_cli-0.0.35.dist-info → tunacode_cli-0.0.37.dist-info}/WHEEL +0 -0
- {tunacode_cli-0.0.35.dist-info → tunacode_cli-0.0.37.dist-info}/entry_points.txt +0 -0
- {tunacode_cli-0.0.35.dist-info → tunacode_cli-0.0.37.dist-info}/licenses/LICENSE +0 -0
- {tunacode_cli-0.0.35.dist-info → tunacode_cli-0.0.37.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""System-level commands for TunaCode CLI."""
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
from typing import List
|
|
7
|
+
|
|
8
|
+
from ....types import CommandContext
|
|
9
|
+
from ....ui import console as ui
|
|
10
|
+
from ..base import CommandCategory, CommandSpec, SimpleCommand
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class HelpCommand(SimpleCommand):
|
|
14
|
+
"""Show help information."""
|
|
15
|
+
|
|
16
|
+
spec = CommandSpec(
|
|
17
|
+
name="help",
|
|
18
|
+
aliases=["/help"],
|
|
19
|
+
description="Show help information",
|
|
20
|
+
category=CommandCategory.SYSTEM,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
def __init__(self, command_registry=None):
|
|
24
|
+
self._command_registry = command_registry
|
|
25
|
+
|
|
26
|
+
async def execute(self, args: List[str], context: CommandContext) -> None:
|
|
27
|
+
await ui.help(self._command_registry)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ClearCommand(SimpleCommand):
|
|
31
|
+
"""Clear screen and message history."""
|
|
32
|
+
|
|
33
|
+
spec = CommandSpec(
|
|
34
|
+
name="clear",
|
|
35
|
+
aliases=["/clear"],
|
|
36
|
+
description="Clear the screen and message history",
|
|
37
|
+
category=CommandCategory.NAVIGATION,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
async def execute(self, args: List[str], context: CommandContext) -> None:
|
|
41
|
+
# Patch any orphaned tool calls before clearing
|
|
42
|
+
from tunacode.core.agents.main import patch_tool_messages
|
|
43
|
+
|
|
44
|
+
patch_tool_messages("Conversation cleared", context.state_manager)
|
|
45
|
+
|
|
46
|
+
await ui.clear()
|
|
47
|
+
context.state_manager.session.messages = []
|
|
48
|
+
context.state_manager.session.files_in_context.clear()
|
|
49
|
+
await ui.success("Message history and file context cleared")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class RefreshConfigCommand(SimpleCommand):
|
|
53
|
+
"""Refresh configuration from defaults."""
|
|
54
|
+
|
|
55
|
+
spec = CommandSpec(
|
|
56
|
+
name="refresh",
|
|
57
|
+
aliases=["/refresh"],
|
|
58
|
+
description="Refresh configuration from defaults (useful after updates)",
|
|
59
|
+
category=CommandCategory.SYSTEM,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
async def execute(self, args: List[str], context: CommandContext) -> None:
|
|
63
|
+
from tunacode.configuration.defaults import DEFAULT_USER_CONFIG
|
|
64
|
+
|
|
65
|
+
# Update current session config with latest defaults
|
|
66
|
+
for key, value in DEFAULT_USER_CONFIG.items():
|
|
67
|
+
if key not in context.state_manager.session.user_config:
|
|
68
|
+
context.state_manager.session.user_config[key] = value
|
|
69
|
+
elif isinstance(value, dict):
|
|
70
|
+
# Merge dict values, preserving user overrides
|
|
71
|
+
for subkey, subvalue in value.items():
|
|
72
|
+
if subkey not in context.state_manager.session.user_config[key]:
|
|
73
|
+
context.state_manager.session.user_config[key][subkey] = subvalue
|
|
74
|
+
|
|
75
|
+
# Show updated max_iterations
|
|
76
|
+
max_iterations = context.state_manager.session.user_config.get("settings", {}).get(
|
|
77
|
+
"max_iterations", 20
|
|
78
|
+
)
|
|
79
|
+
await ui.success(f"Configuration refreshed - max iterations: {max_iterations}")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class UpdateCommand(SimpleCommand):
|
|
83
|
+
"""Update TunaCode to the latest version."""
|
|
84
|
+
|
|
85
|
+
spec = CommandSpec(
|
|
86
|
+
name="update",
|
|
87
|
+
aliases=["/update"],
|
|
88
|
+
description="Update TunaCode to the latest version",
|
|
89
|
+
category=CommandCategory.SYSTEM,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
async def execute(self, args: List[str], context: CommandContext) -> None:
|
|
93
|
+
await ui.info("Checking for TunaCode updates...")
|
|
94
|
+
|
|
95
|
+
# Detect installation method
|
|
96
|
+
installation_method = None
|
|
97
|
+
|
|
98
|
+
# Check if installed via pipx
|
|
99
|
+
if shutil.which("pipx"):
|
|
100
|
+
try:
|
|
101
|
+
result = subprocess.run(
|
|
102
|
+
["pipx", "list"], capture_output=True, text=True, timeout=10
|
|
103
|
+
)
|
|
104
|
+
pipx_installed = "tunacode" in result.stdout.lower()
|
|
105
|
+
if pipx_installed:
|
|
106
|
+
installation_method = "pipx"
|
|
107
|
+
except (subprocess.TimeoutExpired, subprocess.CalledProcessError):
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
# Check if installed via pip
|
|
111
|
+
if not installation_method:
|
|
112
|
+
try:
|
|
113
|
+
result = subprocess.run(
|
|
114
|
+
[sys.executable, "-m", "pip", "show", "tunacode-cli"],
|
|
115
|
+
capture_output=True,
|
|
116
|
+
text=True,
|
|
117
|
+
timeout=10,
|
|
118
|
+
)
|
|
119
|
+
if result.returncode == 0:
|
|
120
|
+
installation_method = "pip"
|
|
121
|
+
except (subprocess.TimeoutExpired, subprocess.CalledProcessError):
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
if not installation_method:
|
|
125
|
+
await ui.error("Could not detect TunaCode installation method")
|
|
126
|
+
await ui.muted("Manual update options:")
|
|
127
|
+
await ui.muted(" pipx: pipx upgrade tunacode")
|
|
128
|
+
await ui.muted(" pip: pip install --upgrade tunacode-cli")
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
# Perform update based on detected method
|
|
132
|
+
try:
|
|
133
|
+
if installation_method == "pipx":
|
|
134
|
+
await ui.info("Updating via pipx...")
|
|
135
|
+
result = subprocess.run(
|
|
136
|
+
["pipx", "upgrade", "tunacode"],
|
|
137
|
+
capture_output=True,
|
|
138
|
+
text=True,
|
|
139
|
+
timeout=60,
|
|
140
|
+
)
|
|
141
|
+
else: # pip
|
|
142
|
+
await ui.info("Updating via pip...")
|
|
143
|
+
result = subprocess.run(
|
|
144
|
+
[
|
|
145
|
+
sys.executable,
|
|
146
|
+
"-m",
|
|
147
|
+
"pip",
|
|
148
|
+
"install",
|
|
149
|
+
"--upgrade",
|
|
150
|
+
"tunacode-cli",
|
|
151
|
+
],
|
|
152
|
+
capture_output=True,
|
|
153
|
+
text=True,
|
|
154
|
+
timeout=60,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
if result.returncode == 0:
|
|
158
|
+
await ui.success("TunaCode updated successfully!")
|
|
159
|
+
await ui.muted("Restart TunaCode to use the new version")
|
|
160
|
+
|
|
161
|
+
# Show update output if available
|
|
162
|
+
if result.stdout.strip():
|
|
163
|
+
output_lines = result.stdout.strip().split("\n")
|
|
164
|
+
for line in output_lines[-5:]: # Show last 5 lines
|
|
165
|
+
if line.strip():
|
|
166
|
+
await ui.muted(f" {line}")
|
|
167
|
+
else:
|
|
168
|
+
await ui.error("Update failed")
|
|
169
|
+
if result.stderr:
|
|
170
|
+
await ui.muted(f"Error: {result.stderr.strip()}")
|
|
171
|
+
|
|
172
|
+
except subprocess.TimeoutExpired:
|
|
173
|
+
await ui.error("Update timed out")
|
|
174
|
+
except subprocess.CalledProcessError as e:
|
|
175
|
+
await ui.error(f"Update failed: {e}")
|
|
176
|
+
except FileNotFoundError:
|
|
177
|
+
await ui.error(f"Could not find {installation_method} executable")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class StreamingCommand(SimpleCommand):
|
|
181
|
+
"""Toggle streaming display on/off."""
|
|
182
|
+
|
|
183
|
+
spec = CommandSpec(
|
|
184
|
+
name="streaming",
|
|
185
|
+
aliases=["/streaming"],
|
|
186
|
+
description="Toggle streaming display on/off",
|
|
187
|
+
category=CommandCategory.SYSTEM,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
async def execute(self, args: List[str], context: CommandContext) -> None:
|
|
191
|
+
current_setting = context.state_manager.session.user_config.get("settings", {}).get(
|
|
192
|
+
"enable_streaming", True
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
if args and args[0].lower() in ["on", "true", "1", "enable", "enabled"]:
|
|
196
|
+
new_setting = True
|
|
197
|
+
elif args and args[0].lower() in ["off", "false", "0", "disable", "disabled"]:
|
|
198
|
+
new_setting = False
|
|
199
|
+
else:
|
|
200
|
+
# Toggle current setting
|
|
201
|
+
new_setting = not current_setting
|
|
202
|
+
|
|
203
|
+
# Update the configuration
|
|
204
|
+
if "settings" not in context.state_manager.session.user_config:
|
|
205
|
+
context.state_manager.session.user_config["settings"] = {}
|
|
206
|
+
context.state_manager.session.user_config["settings"]["enable_streaming"] = new_setting
|
|
207
|
+
|
|
208
|
+
status = "enabled" if new_setting else "disabled"
|
|
209
|
+
await ui.success(f"Streaming display {status}")
|
|
210
|
+
|
|
211
|
+
if new_setting:
|
|
212
|
+
await ui.muted(
|
|
213
|
+
"Responses will be displayed progressively as they are generated (default)"
|
|
214
|
+
)
|
|
215
|
+
else:
|
|
216
|
+
await ui.muted("Responses will be displayed all at once after completion")
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""Command registry and factory for TunaCode CLI commands."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Dict, List, Optional, Type
|
|
5
|
+
|
|
6
|
+
from ...exceptions import ValidationError
|
|
7
|
+
from ...types import CommandArgs, CommandContext, ProcessRequestCallback
|
|
8
|
+
from .base import Command, CommandCategory
|
|
9
|
+
|
|
10
|
+
# Import all command implementations
|
|
11
|
+
from .implementations.conversation import CompactCommand
|
|
12
|
+
from .implementations.debug import (
|
|
13
|
+
DumpCommand,
|
|
14
|
+
FixCommand,
|
|
15
|
+
IterationsCommand,
|
|
16
|
+
ParseToolsCommand,
|
|
17
|
+
ThoughtsCommand,
|
|
18
|
+
YoloCommand,
|
|
19
|
+
)
|
|
20
|
+
from .implementations.development import BranchCommand, InitCommand
|
|
21
|
+
from .implementations.model import ModelCommand
|
|
22
|
+
from .implementations.system import (
|
|
23
|
+
ClearCommand,
|
|
24
|
+
HelpCommand,
|
|
25
|
+
RefreshConfigCommand,
|
|
26
|
+
StreamingCommand,
|
|
27
|
+
UpdateCommand,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class CommandDependencies:
|
|
33
|
+
"""Container for command dependencies."""
|
|
34
|
+
|
|
35
|
+
process_request_callback: Optional[ProcessRequestCallback] = None
|
|
36
|
+
command_registry: Optional[Any] = None # Reference to the registry itself
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class CommandFactory:
|
|
40
|
+
"""Factory for creating commands with proper dependency injection."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, dependencies: Optional[CommandDependencies] = None):
|
|
43
|
+
self.dependencies = dependencies or CommandDependencies()
|
|
44
|
+
|
|
45
|
+
def create_command(self, command_class: Type[Command]) -> Command:
|
|
46
|
+
"""Create a command instance with proper dependencies."""
|
|
47
|
+
# Special handling for commands that need dependencies
|
|
48
|
+
if command_class == CompactCommand:
|
|
49
|
+
return CompactCommand(self.dependencies.process_request_callback)
|
|
50
|
+
elif command_class == HelpCommand:
|
|
51
|
+
return HelpCommand(self.dependencies.command_registry)
|
|
52
|
+
|
|
53
|
+
# Default creation for commands without dependencies
|
|
54
|
+
return command_class()
|
|
55
|
+
|
|
56
|
+
def update_dependencies(self, **kwargs) -> None:
|
|
57
|
+
"""Update factory dependencies."""
|
|
58
|
+
for key, value in kwargs.items():
|
|
59
|
+
if hasattr(self.dependencies, key):
|
|
60
|
+
setattr(self.dependencies, key, value)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class CommandRegistry:
|
|
64
|
+
"""Registry for managing commands with auto-discovery and categories."""
|
|
65
|
+
|
|
66
|
+
def __init__(self, factory: Optional[CommandFactory] = None):
|
|
67
|
+
self._commands: Dict[str, Command] = {}
|
|
68
|
+
self._categories: Dict[CommandCategory, List[Command]] = {
|
|
69
|
+
category: [] for category in CommandCategory
|
|
70
|
+
}
|
|
71
|
+
self._factory = factory or CommandFactory()
|
|
72
|
+
self._discovered = False
|
|
73
|
+
|
|
74
|
+
# Set registry reference in factory dependencies
|
|
75
|
+
self._factory.update_dependencies(command_registry=self)
|
|
76
|
+
|
|
77
|
+
def register(self, command: Command) -> None:
|
|
78
|
+
"""Register a command and its aliases."""
|
|
79
|
+
# Register by primary name
|
|
80
|
+
self._commands[command.name] = command
|
|
81
|
+
|
|
82
|
+
# Register all aliases
|
|
83
|
+
for alias in command.aliases:
|
|
84
|
+
self._commands[alias.lower()] = command
|
|
85
|
+
|
|
86
|
+
# Add to category (remove existing instance first to prevent duplicates)
|
|
87
|
+
category_commands = self._categories[command.category]
|
|
88
|
+
# Remove any existing instance of this command class
|
|
89
|
+
self._categories[command.category] = [
|
|
90
|
+
cmd for cmd in category_commands if cmd.__class__ != command.__class__
|
|
91
|
+
]
|
|
92
|
+
# Add the new instance
|
|
93
|
+
self._categories[command.category].append(command)
|
|
94
|
+
|
|
95
|
+
def register_command_class(self, command_class: Type[Command]) -> None:
|
|
96
|
+
"""Register a command class using the factory."""
|
|
97
|
+
command = self._factory.create_command(command_class)
|
|
98
|
+
self.register(command)
|
|
99
|
+
|
|
100
|
+
def discover_commands(self) -> None:
|
|
101
|
+
"""Auto-discover and register all command classes."""
|
|
102
|
+
if self._discovered:
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
# List of all command classes to register
|
|
106
|
+
command_classes = [
|
|
107
|
+
YoloCommand,
|
|
108
|
+
DumpCommand,
|
|
109
|
+
ThoughtsCommand,
|
|
110
|
+
IterationsCommand,
|
|
111
|
+
ClearCommand,
|
|
112
|
+
FixCommand,
|
|
113
|
+
ParseToolsCommand,
|
|
114
|
+
RefreshConfigCommand,
|
|
115
|
+
StreamingCommand,
|
|
116
|
+
UpdateCommand,
|
|
117
|
+
HelpCommand,
|
|
118
|
+
BranchCommand,
|
|
119
|
+
CompactCommand,
|
|
120
|
+
ModelCommand,
|
|
121
|
+
InitCommand,
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
# Register all discovered commands
|
|
125
|
+
for command_class in command_classes:
|
|
126
|
+
self.register_command_class(command_class)
|
|
127
|
+
|
|
128
|
+
self._discovered = True
|
|
129
|
+
|
|
130
|
+
def register_all_default_commands(self) -> None:
|
|
131
|
+
"""Register all default commands (backward compatibility)."""
|
|
132
|
+
self.discover_commands()
|
|
133
|
+
|
|
134
|
+
def set_process_request_callback(self, callback: ProcessRequestCallback) -> None:
|
|
135
|
+
"""Set the process_request callback for commands that need it."""
|
|
136
|
+
# Only update if callback has changed
|
|
137
|
+
if self._factory.dependencies.process_request_callback == callback:
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
self._factory.update_dependencies(process_request_callback=callback)
|
|
141
|
+
|
|
142
|
+
# Re-register CompactCommand with new dependency if already registered
|
|
143
|
+
if "compact" in self._commands:
|
|
144
|
+
self.register_command_class(CompactCommand)
|
|
145
|
+
|
|
146
|
+
async def execute(self, command_text: str, context: CommandContext) -> Any:
|
|
147
|
+
"""
|
|
148
|
+
Execute a command.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
command_text: The full command text
|
|
152
|
+
context: Execution context
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Command-specific return value, or None if command not found
|
|
156
|
+
|
|
157
|
+
Raises:
|
|
158
|
+
ValidationError: If command is not found or empty
|
|
159
|
+
"""
|
|
160
|
+
# Ensure commands are discovered
|
|
161
|
+
self.discover_commands()
|
|
162
|
+
|
|
163
|
+
parts = command_text.split()
|
|
164
|
+
if not parts:
|
|
165
|
+
raise ValidationError("Empty command")
|
|
166
|
+
|
|
167
|
+
command_name = parts[0].lower()
|
|
168
|
+
args = parts[1:]
|
|
169
|
+
|
|
170
|
+
# First try exact match
|
|
171
|
+
if command_name in self._commands:
|
|
172
|
+
command = self._commands[command_name]
|
|
173
|
+
return await command.execute(args, context)
|
|
174
|
+
|
|
175
|
+
# Try partial matching
|
|
176
|
+
matches = self.find_matching_commands(command_name)
|
|
177
|
+
|
|
178
|
+
if not matches:
|
|
179
|
+
raise ValidationError(f"Unknown command: {command_name}")
|
|
180
|
+
elif len(matches) == 1:
|
|
181
|
+
# Unambiguous match
|
|
182
|
+
command = self._commands[matches[0]]
|
|
183
|
+
return await command.execute(args, context)
|
|
184
|
+
else:
|
|
185
|
+
# Ambiguous - show possibilities
|
|
186
|
+
matches_str = ", ".join(sorted(set(matches)))
|
|
187
|
+
raise ValidationError(
|
|
188
|
+
f"Ambiguous command '{command_name}'. Did you mean: {matches_str}?"
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
def find_matching_commands(self, partial_command: str) -> List[str]:
|
|
192
|
+
"""
|
|
193
|
+
Find all commands that start with the given partial command.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
partial_command: The partial command to match
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
List of matching command names
|
|
200
|
+
"""
|
|
201
|
+
self.discover_commands()
|
|
202
|
+
partial = partial_command.lower()
|
|
203
|
+
return [cmd for cmd in self._commands.keys() if cmd.startswith(partial)]
|
|
204
|
+
|
|
205
|
+
def is_command(self, text: str) -> bool:
|
|
206
|
+
"""Check if text starts with a registered command (supports partial matching)."""
|
|
207
|
+
if not text:
|
|
208
|
+
return False
|
|
209
|
+
|
|
210
|
+
parts = text.split()
|
|
211
|
+
if not parts:
|
|
212
|
+
return False
|
|
213
|
+
|
|
214
|
+
command_name = parts[0].lower()
|
|
215
|
+
|
|
216
|
+
# Check exact match first
|
|
217
|
+
if command_name in self._commands:
|
|
218
|
+
return True
|
|
219
|
+
|
|
220
|
+
# Check partial match
|
|
221
|
+
return len(self.find_matching_commands(command_name)) > 0
|
|
222
|
+
|
|
223
|
+
def get_command_names(self) -> CommandArgs:
|
|
224
|
+
"""Get all registered command names (including aliases)."""
|
|
225
|
+
self.discover_commands()
|
|
226
|
+
return sorted(self._commands.keys())
|
|
227
|
+
|
|
228
|
+
def get_commands_by_category(self, category: CommandCategory) -> List[Command]:
|
|
229
|
+
"""Get all commands in a specific category."""
|
|
230
|
+
self.discover_commands()
|
|
231
|
+
return self._categories.get(category, [])
|
|
232
|
+
|
|
233
|
+
def get_all_categories(self) -> Dict[CommandCategory, List[Command]]:
|
|
234
|
+
"""Get all commands organized by category."""
|
|
235
|
+
self.discover_commands()
|
|
236
|
+
return self._categories.copy()
|
tunacode/cli/repl.py
CHANGED
|
@@ -70,8 +70,9 @@ async def _tool_confirm(tool_call, node, state_manager: StateManager):
|
|
|
70
70
|
await _tool_ui.log_mcp(title, args)
|
|
71
71
|
return
|
|
72
72
|
|
|
73
|
-
# Stop spinner during user interaction
|
|
74
|
-
state_manager.session.spinner
|
|
73
|
+
# Stop spinner during user interaction (only if not streaming)
|
|
74
|
+
if not state_manager.session.is_streaming_active and state_manager.session.spinner:
|
|
75
|
+
state_manager.session.spinner.stop()
|
|
75
76
|
|
|
76
77
|
# Create confirmation request
|
|
77
78
|
request = tool_handler.create_confirmation_request(tool_call.tool_name, args)
|
|
@@ -84,7 +85,10 @@ async def _tool_confirm(tool_call, node, state_manager: StateManager):
|
|
|
84
85
|
raise UserAbortError("User aborted.")
|
|
85
86
|
|
|
86
87
|
await ui.line() # Add line after user input
|
|
87
|
-
|
|
88
|
+
|
|
89
|
+
# Restart spinner (only if not streaming)
|
|
90
|
+
if not state_manager.session.is_streaming_active and state_manager.session.spinner:
|
|
91
|
+
state_manager.session.spinner.start()
|
|
88
92
|
|
|
89
93
|
|
|
90
94
|
async def _tool_handler(part, node, state_manager: StateManager):
|
|
@@ -96,7 +100,19 @@ async def _tool_handler(part, node, state_manager: StateManager):
|
|
|
96
100
|
if tool_handler.should_confirm(part.tool_name):
|
|
97
101
|
await ui.info(f"Tool({part.tool_name})")
|
|
98
102
|
|
|
99
|
-
|
|
103
|
+
# Stop spinner only if not streaming
|
|
104
|
+
if not state_manager.session.is_streaming_active and state_manager.session.spinner:
|
|
105
|
+
state_manager.session.spinner.stop()
|
|
106
|
+
|
|
107
|
+
# Track if we need to stop/restart streaming panel
|
|
108
|
+
streaming_panel = None
|
|
109
|
+
if state_manager.session.is_streaming_active and hasattr(
|
|
110
|
+
state_manager.session, "streaming_panel"
|
|
111
|
+
):
|
|
112
|
+
streaming_panel = state_manager.session.streaming_panel
|
|
113
|
+
# Stop the streaming panel to prevent UI interference during confirmation
|
|
114
|
+
if streaming_panel and tool_handler.should_confirm(part.tool_name):
|
|
115
|
+
await streaming_panel.stop()
|
|
100
116
|
|
|
101
117
|
try:
|
|
102
118
|
args = _parse_args(part.args)
|
|
@@ -128,7 +144,13 @@ async def _tool_handler(part, node, state_manager: StateManager):
|
|
|
128
144
|
patch_tool_messages("Operation aborted by user.", state_manager)
|
|
129
145
|
raise
|
|
130
146
|
finally:
|
|
131
|
-
|
|
147
|
+
# Restart streaming panel if it was stopped
|
|
148
|
+
if streaming_panel and tool_handler.should_confirm(part.tool_name):
|
|
149
|
+
await streaming_panel.start()
|
|
150
|
+
|
|
151
|
+
# Restart spinner only if not streaming
|
|
152
|
+
if not state_manager.session.is_streaming_active and state_manager.session.spinner:
|
|
153
|
+
state_manager.session.spinner.start()
|
|
132
154
|
|
|
133
155
|
|
|
134
156
|
# Initialize command registry
|
|
@@ -195,38 +217,77 @@ async def process_request(text: str, state_manager: StateManager, output: bool =
|
|
|
195
217
|
await ui.error(str(e))
|
|
196
218
|
return
|
|
197
219
|
|
|
198
|
-
#
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
text,
|
|
202
|
-
state_manager,
|
|
203
|
-
tool_callback=tool_callback_with_state,
|
|
220
|
+
# Check if streaming is enabled (default: True for better UX)
|
|
221
|
+
enable_streaming = state_manager.session.user_config.get("settings", {}).get(
|
|
222
|
+
"enable_streaming", True
|
|
204
223
|
)
|
|
224
|
+
|
|
225
|
+
if enable_streaming:
|
|
226
|
+
# Stop spinner before starting streaming display (Rich.Live conflict)
|
|
227
|
+
await ui.spinner(False, state_manager.session.spinner, state_manager)
|
|
228
|
+
|
|
229
|
+
# Mark that streaming is active to prevent spinner conflicts
|
|
230
|
+
state_manager.session.is_streaming_active = True
|
|
231
|
+
|
|
232
|
+
# Use streaming agent processing
|
|
233
|
+
streaming_panel = ui.StreamingAgentPanel()
|
|
234
|
+
await streaming_panel.start()
|
|
235
|
+
|
|
236
|
+
# Store streaming panel reference in session for tool handler access
|
|
237
|
+
state_manager.session.streaming_panel = streaming_panel
|
|
238
|
+
|
|
239
|
+
try:
|
|
240
|
+
|
|
241
|
+
async def streaming_callback(content: str):
|
|
242
|
+
await streaming_panel.update(content)
|
|
243
|
+
|
|
244
|
+
res = await agent.process_request(
|
|
245
|
+
state_manager.session.current_model,
|
|
246
|
+
text,
|
|
247
|
+
state_manager,
|
|
248
|
+
tool_callback=tool_callback_with_state,
|
|
249
|
+
streaming_callback=streaming_callback,
|
|
250
|
+
)
|
|
251
|
+
finally:
|
|
252
|
+
await streaming_panel.stop()
|
|
253
|
+
# Clear streaming panel reference
|
|
254
|
+
state_manager.session.streaming_panel = None
|
|
255
|
+
# Mark streaming as inactive
|
|
256
|
+
state_manager.session.is_streaming_active = False
|
|
257
|
+
# Don't restart spinner - it will be stopped in the outer finally block anyway
|
|
258
|
+
else:
|
|
259
|
+
# Use normal agent processing
|
|
260
|
+
res = await agent.process_request(
|
|
261
|
+
state_manager.session.current_model,
|
|
262
|
+
text,
|
|
263
|
+
state_manager,
|
|
264
|
+
tool_callback=tool_callback_with_state,
|
|
265
|
+
)
|
|
205
266
|
if output:
|
|
206
267
|
if state_manager.session.show_thoughts:
|
|
207
268
|
new_msgs = state_manager.session.messages[start_idx:]
|
|
208
269
|
for msg in new_msgs:
|
|
209
270
|
if isinstance(msg, dict) and "thought" in msg:
|
|
210
271
|
await ui.muted(f"THOUGHT: {msg['thought']}")
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
#
|
|
215
|
-
if
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
await ui.
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
272
|
+
|
|
273
|
+
# Only display result if not streaming (streaming already showed content)
|
|
274
|
+
if not enable_streaming:
|
|
275
|
+
# Check if result exists and has output
|
|
276
|
+
if (
|
|
277
|
+
hasattr(res, "result")
|
|
278
|
+
and res.result is not None
|
|
279
|
+
and hasattr(res.result, "output")
|
|
280
|
+
):
|
|
281
|
+
await ui.agent(res.result.output)
|
|
282
|
+
else:
|
|
283
|
+
# Fallback: show that the request was processed
|
|
284
|
+
await ui.muted("Request completed")
|
|
285
|
+
|
|
286
|
+
# Always show files in context after agent response
|
|
287
|
+
if state_manager.session.files_in_context:
|
|
288
|
+
# Extract just filenames from full paths for readability
|
|
289
|
+
filenames = [Path(f).name for f in sorted(state_manager.session.files_in_context)]
|
|
290
|
+
await ui.muted(f"\nFiles in context: {', '.join(filenames)}")
|
|
230
291
|
except CancelledError:
|
|
231
292
|
await ui.muted("Request cancelled")
|
|
232
293
|
except UserAbortError:
|
|
@@ -7,8 +7,15 @@ Handles configuration paths, model registries, and application metadata.
|
|
|
7
7
|
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
|
|
10
|
-
from tunacode.constants import (
|
|
11
|
-
|
|
10
|
+
from tunacode.constants import (
|
|
11
|
+
APP_NAME,
|
|
12
|
+
APP_VERSION,
|
|
13
|
+
CONFIG_FILE_NAME,
|
|
14
|
+
TOOL_READ_FILE,
|
|
15
|
+
TOOL_RUN_COMMAND,
|
|
16
|
+
TOOL_UPDATE_FILE,
|
|
17
|
+
TOOL_WRITE_FILE,
|
|
18
|
+
)
|
|
12
19
|
from tunacode.types import ConfigFile, ConfigPath, ToolName
|
|
13
20
|
|
|
14
21
|
|
tunacode/constants.py
CHANGED