tunacode-cli 0.0.48__py3-none-any.whl → 0.0.49__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.
- api/auth.py +13 -0
- api/users.py +8 -0
- tunacode/__init__.py +4 -0
- tunacode/cli/main.py +4 -0
- tunacode/cli/repl.py +39 -6
- tunacode/configuration/defaults.py +0 -1
- tunacode/constants.py +7 -1
- tunacode/core/agents/main.py +268 -245
- tunacode/core/agents/utils.py +54 -6
- tunacode/core/logging/__init__.py +29 -0
- tunacode/core/logging/config.py +28 -0
- tunacode/core/logging/formatters.py +48 -0
- tunacode/core/logging/handlers.py +83 -0
- tunacode/core/logging/logger.py +8 -0
- tunacode/core/recursive/__init__.py +18 -0
- tunacode/core/recursive/aggregator.py +467 -0
- tunacode/core/recursive/budget.py +414 -0
- tunacode/core/recursive/decomposer.py +398 -0
- tunacode/core/recursive/executor.py +470 -0
- tunacode/core/recursive/hierarchy.py +488 -0
- tunacode/core/state.py +45 -0
- tunacode/exceptions.py +23 -0
- tunacode/tools/base.py +7 -1
- tunacode/types.py +1 -1
- tunacode/ui/completers.py +2 -2
- tunacode/ui/console.py +30 -9
- tunacode/ui/input.py +2 -1
- tunacode/ui/keybindings.py +58 -1
- tunacode/ui/logging_compat.py +44 -0
- tunacode/ui/output.py +7 -6
- tunacode/ui/panels.py +30 -5
- tunacode/ui/recursive_progress.py +380 -0
- tunacode/utils/retry.py +163 -0
- tunacode/utils/security.py +3 -2
- tunacode/utils/token_counter.py +1 -2
- {tunacode_cli-0.0.48.dist-info → tunacode_cli-0.0.49.dist-info}/METADATA +2 -2
- {tunacode_cli-0.0.48.dist-info → tunacode_cli-0.0.49.dist-info}/RECORD +41 -29
- {tunacode_cli-0.0.48.dist-info → tunacode_cli-0.0.49.dist-info}/top_level.txt +1 -0
- tunacode/core/agents/dspy_integration.py +0 -223
- tunacode/core/agents/dspy_tunacode.py +0 -458
- tunacode/prompts/dspy_task_planning.md +0 -45
- tunacode/prompts/dspy_tool_selection.md +0 -58
- {tunacode_cli-0.0.48.dist-info → tunacode_cli-0.0.49.dist-info}/WHEEL +0 -0
- {tunacode_cli-0.0.48.dist-info → tunacode_cli-0.0.49.dist-info}/entry_points.txt +0 -0
- {tunacode_cli-0.0.48.dist-info → tunacode_cli-0.0.49.dist-info}/licenses/LICENSE +0 -0
tunacode/ui/console.py
CHANGED
|
@@ -9,29 +9,26 @@ from rich.markdown import Markdown
|
|
|
9
9
|
# Import and re-export all functions from specialized modules
|
|
10
10
|
from .input import formatted_text, input, multiline_input
|
|
11
11
|
from .keybindings import create_key_bindings
|
|
12
|
+
|
|
13
|
+
# Unified UI logger compatibility layer
|
|
14
|
+
from .logging_compat import ui_logger
|
|
12
15
|
from .output import (
|
|
13
16
|
banner,
|
|
14
17
|
clear,
|
|
15
|
-
info,
|
|
16
18
|
line,
|
|
17
19
|
muted,
|
|
18
20
|
print,
|
|
19
21
|
spinner,
|
|
20
|
-
success,
|
|
21
22
|
sync_print,
|
|
22
23
|
update_available,
|
|
23
24
|
usage,
|
|
24
25
|
version,
|
|
25
|
-
warning,
|
|
26
26
|
)
|
|
27
|
-
|
|
28
|
-
# Patch banner to use sync fast version
|
|
29
27
|
from .panels import (
|
|
30
28
|
StreamingAgentPanel,
|
|
31
29
|
agent,
|
|
32
30
|
agent_streaming,
|
|
33
31
|
dump_messages,
|
|
34
|
-
error,
|
|
35
32
|
help,
|
|
36
33
|
models,
|
|
37
34
|
panel,
|
|
@@ -42,6 +39,28 @@ from .panels import (
|
|
|
42
39
|
from .prompt_manager import PromptConfig, PromptManager
|
|
43
40
|
from .validators import ModelValidator
|
|
44
41
|
|
|
42
|
+
|
|
43
|
+
# Async wrappers for UI logging
|
|
44
|
+
async def info(message: str) -> None:
|
|
45
|
+
await ui_logger.info(message)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
async def warning(message: str) -> None:
|
|
49
|
+
await ui_logger.warning(message)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
async def error(message: str) -> None:
|
|
53
|
+
await ui_logger.error(message)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async def debug(message: str) -> None:
|
|
57
|
+
await ui_logger.debug(message)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
async def success(message: str) -> None:
|
|
61
|
+
await ui_logger.success(message)
|
|
62
|
+
|
|
63
|
+
|
|
45
64
|
# Create console object for backward compatibility
|
|
46
65
|
console = RichConsole(force_terminal=True, legacy_windows=False)
|
|
47
66
|
|
|
@@ -68,22 +87,24 @@ __all__ = [
|
|
|
68
87
|
"banner",
|
|
69
88
|
"clear",
|
|
70
89
|
"console",
|
|
71
|
-
"info",
|
|
72
90
|
"line",
|
|
73
91
|
"muted",
|
|
74
92
|
"print",
|
|
75
93
|
"spinner",
|
|
76
|
-
"success",
|
|
77
94
|
"sync_print",
|
|
78
95
|
"update_available",
|
|
79
96
|
"usage",
|
|
80
97
|
"version",
|
|
98
|
+
# Unified logging wrappers
|
|
99
|
+
"info",
|
|
81
100
|
"warning",
|
|
101
|
+
"error",
|
|
102
|
+
"debug",
|
|
103
|
+
"success",
|
|
82
104
|
# From panels module
|
|
83
105
|
"agent",
|
|
84
106
|
"agent_streaming",
|
|
85
107
|
"dump_messages",
|
|
86
|
-
"error",
|
|
87
108
|
"help",
|
|
88
109
|
"models",
|
|
89
110
|
"panel",
|
tunacode/ui/input.py
CHANGED
|
@@ -75,12 +75,13 @@ async def multiline_input(
|
|
|
75
75
|
state_manager: Optional[StateManager] = None, command_registry=None
|
|
76
76
|
) -> str:
|
|
77
77
|
"""Get multiline input from the user with @file completion and highlighting."""
|
|
78
|
-
kb = create_key_bindings()
|
|
78
|
+
kb = create_key_bindings(state_manager)
|
|
79
79
|
placeholder = formatted_text(
|
|
80
80
|
(
|
|
81
81
|
"<darkgrey>"
|
|
82
82
|
"<bold>Enter</bold> to submit • "
|
|
83
83
|
"<bold>Esc + Enter</bold> for new line • "
|
|
84
|
+
"<bold>Esc twice</bold> to cancel • "
|
|
84
85
|
"<bold>/help</bold> for commands"
|
|
85
86
|
"</darkgrey>"
|
|
86
87
|
)
|
tunacode/ui/keybindings.py
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
"""Key binding handlers for TunaCode UI."""
|
|
2
2
|
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
from prompt_toolkit.application import run_in_terminal
|
|
3
7
|
from prompt_toolkit.key_binding import KeyBindings
|
|
4
8
|
|
|
9
|
+
from ..core.state import StateManager
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
5
12
|
|
|
6
|
-
|
|
13
|
+
|
|
14
|
+
def create_key_bindings(state_manager: StateManager = None) -> KeyBindings:
|
|
7
15
|
"""Create and configure key bindings for the UI."""
|
|
8
16
|
kb = KeyBindings()
|
|
9
17
|
|
|
@@ -22,4 +30,53 @@ def create_key_bindings() -> KeyBindings:
|
|
|
22
30
|
"""Insert a newline when escape then enter is pressed."""
|
|
23
31
|
event.current_buffer.insert_text("\n")
|
|
24
32
|
|
|
33
|
+
@kb.add("escape")
|
|
34
|
+
def _escape(event):
|
|
35
|
+
"""Handle ESC key with double-press logic: first press warns, second cancels."""
|
|
36
|
+
if not state_manager:
|
|
37
|
+
logger.debug("Escape key pressed without state manager")
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
current_time = time.time()
|
|
41
|
+
session = state_manager.session
|
|
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 += 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
|
|
67
|
+
if current_task and not current_task.done():
|
|
68
|
+
logger.debug(f"Cancelling current task: {current_task}")
|
|
69
|
+
try:
|
|
70
|
+
current_task.cancel()
|
|
71
|
+
logger.debug("Task cancellation initiated successfully")
|
|
72
|
+
except Exception as e:
|
|
73
|
+
logger.debug(f"Failed to cancel task: {e}")
|
|
74
|
+
else:
|
|
75
|
+
logger.debug(f"No active task to cancel: current_task={current_task}")
|
|
76
|
+
|
|
77
|
+
# Force exit the current input by raising KeyboardInterrupt
|
|
78
|
+
# This will be caught by the prompt manager and converted to UserAbortError
|
|
79
|
+
logger.debug("Raising KeyboardInterrupt to abort current operation")
|
|
80
|
+
raise KeyboardInterrupt()
|
|
81
|
+
|
|
25
82
|
return kb
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""
|
|
2
|
+
UILogger compatibility layer for unified logging.
|
|
3
|
+
|
|
4
|
+
Implements the UILogger protocol using the unified logging system,
|
|
5
|
+
preserving UI formatting and behavior for all log levels.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from tunacode.core.logging.logger import get_logger
|
|
9
|
+
from tunacode.types import UILogger
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class UnifiedUILogger(UILogger):
|
|
13
|
+
"""
|
|
14
|
+
UILogger implementation that routes all UI log calls through the unified logging system.
|
|
15
|
+
Preserves UI conventions for info, error, warning, debug, and success.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, name: str = "ui"):
|
|
19
|
+
self.logger = get_logger(name)
|
|
20
|
+
|
|
21
|
+
async def info(self, message: str) -> None:
|
|
22
|
+
# Standard info log
|
|
23
|
+
self.logger.info(message)
|
|
24
|
+
|
|
25
|
+
async def error(self, message: str) -> None:
|
|
26
|
+
# Standard error log
|
|
27
|
+
self.logger.error(message)
|
|
28
|
+
|
|
29
|
+
async def warning(self, message: str) -> None:
|
|
30
|
+
# Standard warning log
|
|
31
|
+
self.logger.warning(message)
|
|
32
|
+
|
|
33
|
+
async def debug(self, message: str) -> None:
|
|
34
|
+
# Standard debug log
|
|
35
|
+
self.logger.debug(message)
|
|
36
|
+
|
|
37
|
+
async def success(self, message: str) -> None:
|
|
38
|
+
# "Success" is a UI convention; log as info with a marker for UI formatting
|
|
39
|
+
# Add a special prefix or extra field for downstream handlers/formatters
|
|
40
|
+
self.logger.info(f"[SUCCESS] {message}", extra={"ui_success": True})
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# Singleton instance for convenience
|
|
44
|
+
ui_logger: UILogger = UnifiedUILogger()
|
tunacode/ui/output.py
CHANGED
|
@@ -18,6 +18,7 @@ from tunacode.utils.token_counter import format_token_count
|
|
|
18
18
|
|
|
19
19
|
from .constants import SPINNER_TYPE
|
|
20
20
|
from .decorators import create_sync_wrapper
|
|
21
|
+
from .logging_compat import ui_logger
|
|
21
22
|
|
|
22
23
|
# Create console with explicit settings to ensure ANSI codes work properly
|
|
23
24
|
console = Console(force_terminal=True, legacy_windows=False)
|
|
@@ -54,18 +55,18 @@ async def line() -> None:
|
|
|
54
55
|
|
|
55
56
|
|
|
56
57
|
async def info(text: str) -> None:
|
|
57
|
-
"""
|
|
58
|
-
await
|
|
58
|
+
"""Unified logging: informational message."""
|
|
59
|
+
await ui_logger.info(text)
|
|
59
60
|
|
|
60
61
|
|
|
61
62
|
async def success(message: str) -> None:
|
|
62
|
-
"""
|
|
63
|
-
await
|
|
63
|
+
"""Unified logging: success message."""
|
|
64
|
+
await ui_logger.success(message)
|
|
64
65
|
|
|
65
66
|
|
|
66
67
|
async def warning(text: str) -> None:
|
|
67
|
-
"""
|
|
68
|
-
await
|
|
68
|
+
"""Unified logging: warning message."""
|
|
69
|
+
await ui_logger.warning(text)
|
|
69
70
|
|
|
70
71
|
|
|
71
72
|
async def muted(text: str) -> None:
|
tunacode/ui/panels.py
CHANGED
|
@@ -30,7 +30,6 @@ from tunacode.constants import (
|
|
|
30
30
|
DESC_MODEL_SWITCH,
|
|
31
31
|
DESC_YOLO,
|
|
32
32
|
PANEL_AVAILABLE_COMMANDS,
|
|
33
|
-
PANEL_ERROR,
|
|
34
33
|
PANEL_MESSAGE_HISTORY,
|
|
35
34
|
PANEL_MODELS,
|
|
36
35
|
UI_COLORS,
|
|
@@ -87,11 +86,17 @@ class StreamingAgentPanel:
|
|
|
87
86
|
def _create_panel(self) -> Panel:
|
|
88
87
|
"""Create a Rich panel with current content."""
|
|
89
88
|
# Use the UI_THINKING_MESSAGE constant instead of hardcoded text
|
|
89
|
+
from rich.text import Text
|
|
90
|
+
|
|
90
91
|
from tunacode.constants import UI_THINKING_MESSAGE
|
|
91
92
|
|
|
92
|
-
|
|
93
|
+
# Handle the default thinking message with Rich markup
|
|
94
|
+
if not self.content:
|
|
95
|
+
content_renderable = Text.from_markup(UI_THINKING_MESSAGE)
|
|
96
|
+
else:
|
|
97
|
+
content_renderable = Markdown(self.content)
|
|
93
98
|
panel_obj = Panel(
|
|
94
|
-
Padding(
|
|
99
|
+
Padding(content_renderable, (0, 1, 0, 1)),
|
|
95
100
|
title=f"[bold]{self.title}[/bold]",
|
|
96
101
|
title_align="left",
|
|
97
102
|
border_style=colors.primary,
|
|
@@ -130,7 +135,25 @@ class StreamingAgentPanel:
|
|
|
130
135
|
async def stop(self):
|
|
131
136
|
"""Stop the live streaming display."""
|
|
132
137
|
if self.live:
|
|
138
|
+
# Get the console before stopping the live display
|
|
139
|
+
from .output import console
|
|
140
|
+
|
|
141
|
+
# Stop the live display
|
|
133
142
|
self.live.stop()
|
|
143
|
+
|
|
144
|
+
# Comprehensive cleanup to prevent extra lines
|
|
145
|
+
console.print("", end="") # Reset the current line without newline
|
|
146
|
+
if hasattr(console, "file") and hasattr(console.file, "flush"):
|
|
147
|
+
console.file.flush() # Ensure output is flushed
|
|
148
|
+
|
|
149
|
+
# Mark that we just finished streaming (for output control)
|
|
150
|
+
try:
|
|
151
|
+
from tunacode.core.logging.handlers import _streaming_context
|
|
152
|
+
|
|
153
|
+
_streaming_context["just_finished"] = True
|
|
154
|
+
except ImportError:
|
|
155
|
+
pass # If we can't import, continue without the optimization
|
|
156
|
+
|
|
134
157
|
self.live = None
|
|
135
158
|
|
|
136
159
|
|
|
@@ -152,8 +175,10 @@ async def agent_streaming(content_stream, bottom: int = 1):
|
|
|
152
175
|
|
|
153
176
|
|
|
154
177
|
async def error(text: str) -> None:
|
|
155
|
-
"""
|
|
156
|
-
|
|
178
|
+
"""Unified logging: error message."""
|
|
179
|
+
from .logging_compat import ui_logger
|
|
180
|
+
|
|
181
|
+
await ui_logger.error(text)
|
|
157
182
|
|
|
158
183
|
|
|
159
184
|
async def dump_messages(messages_list=None, state_manager: StateManager = None) -> None:
|