tunacode-cli 0.0.1__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/__init__.py +0 -0
- tunacode/cli/__init__.py +4 -0
- tunacode/cli/commands.py +632 -0
- tunacode/cli/main.py +47 -0
- tunacode/cli/repl.py +251 -0
- tunacode/configuration/__init__.py +1 -0
- tunacode/configuration/defaults.py +26 -0
- tunacode/configuration/models.py +69 -0
- tunacode/configuration/settings.py +32 -0
- tunacode/constants.py +129 -0
- tunacode/context.py +83 -0
- tunacode/core/__init__.py +0 -0
- tunacode/core/agents/__init__.py +0 -0
- tunacode/core/agents/main.py +119 -0
- tunacode/core/setup/__init__.py +17 -0
- tunacode/core/setup/agent_setup.py +41 -0
- tunacode/core/setup/base.py +37 -0
- tunacode/core/setup/config_setup.py +179 -0
- tunacode/core/setup/coordinator.py +45 -0
- tunacode/core/setup/environment_setup.py +62 -0
- tunacode/core/setup/git_safety_setup.py +188 -0
- tunacode/core/setup/undo_setup.py +32 -0
- tunacode/core/state.py +43 -0
- tunacode/core/tool_handler.py +57 -0
- tunacode/exceptions.py +105 -0
- tunacode/prompts/system.txt +71 -0
- tunacode/py.typed +0 -0
- tunacode/services/__init__.py +1 -0
- tunacode/services/mcp.py +86 -0
- tunacode/services/undo_service.py +244 -0
- tunacode/setup.py +50 -0
- tunacode/tools/__init__.py +0 -0
- tunacode/tools/base.py +244 -0
- tunacode/tools/read_file.py +89 -0
- tunacode/tools/run_command.py +107 -0
- tunacode/tools/update_file.py +117 -0
- tunacode/tools/write_file.py +82 -0
- tunacode/types.py +259 -0
- tunacode/ui/__init__.py +1 -0
- tunacode/ui/completers.py +129 -0
- tunacode/ui/console.py +74 -0
- tunacode/ui/constants.py +16 -0
- tunacode/ui/decorators.py +59 -0
- tunacode/ui/input.py +95 -0
- tunacode/ui/keybindings.py +27 -0
- tunacode/ui/lexers.py +46 -0
- tunacode/ui/output.py +109 -0
- tunacode/ui/panels.py +156 -0
- tunacode/ui/prompt_manager.py +117 -0
- tunacode/ui/tool_ui.py +187 -0
- tunacode/ui/validators.py +23 -0
- tunacode/utils/__init__.py +0 -0
- tunacode/utils/bm25.py +55 -0
- tunacode/utils/diff_utils.py +69 -0
- tunacode/utils/file_utils.py +41 -0
- tunacode/utils/ripgrep.py +17 -0
- tunacode/utils/system.py +336 -0
- tunacode/utils/text_utils.py +87 -0
- tunacode/utils/user_configuration.py +54 -0
- tunacode_cli-0.0.1.dist-info/METADATA +242 -0
- tunacode_cli-0.0.1.dist-info/RECORD +65 -0
- tunacode_cli-0.0.1.dist-info/WHEEL +5 -0
- tunacode_cli-0.0.1.dist-info/entry_points.txt +2 -0
- tunacode_cli-0.0.1.dist-info/licenses/LICENSE +21 -0
- tunacode_cli-0.0.1.dist-info/top_level.txt +1 -0
tunacode/ui/console.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Main console coordination module for Sidekick UI.
|
|
2
|
+
|
|
3
|
+
This module re-exports functions from specialized UI modules to maintain
|
|
4
|
+
backward compatibility while organizing code into focused modules.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from rich.console import Console as RichConsole
|
|
8
|
+
from rich.markdown import Markdown
|
|
9
|
+
|
|
10
|
+
# Import and re-export all functions from specialized modules
|
|
11
|
+
from .input import formatted_text, input, multiline_input
|
|
12
|
+
from .keybindings import create_key_bindings
|
|
13
|
+
from .output import (banner, clear, info, line, muted, print, spinner, success, sync_print,
|
|
14
|
+
update_available, usage, version, warning)
|
|
15
|
+
from .panels import (agent, dump_messages, error, help, models, panel, sync_panel,
|
|
16
|
+
sync_tool_confirm, tool_confirm)
|
|
17
|
+
from .prompt_manager import PromptConfig, PromptManager
|
|
18
|
+
from .validators import ModelValidator
|
|
19
|
+
|
|
20
|
+
# Create console object for backward compatibility
|
|
21
|
+
console = RichConsole()
|
|
22
|
+
|
|
23
|
+
# Create key bindings object for backward compatibility
|
|
24
|
+
kb = create_key_bindings()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Re-export markdown utility for backward compatibility
|
|
28
|
+
def markdown(text: str) -> Markdown:
|
|
29
|
+
"""Create a Markdown object."""
|
|
30
|
+
return Markdown(text)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# All functions are now available through imports above
|
|
34
|
+
__all__ = [
|
|
35
|
+
# From input module
|
|
36
|
+
"formatted_text",
|
|
37
|
+
"input",
|
|
38
|
+
"multiline_input",
|
|
39
|
+
# From keybindings module
|
|
40
|
+
"create_key_bindings",
|
|
41
|
+
"kb",
|
|
42
|
+
# From output module
|
|
43
|
+
"banner",
|
|
44
|
+
"clear",
|
|
45
|
+
"console",
|
|
46
|
+
"info",
|
|
47
|
+
"line",
|
|
48
|
+
"muted",
|
|
49
|
+
"print",
|
|
50
|
+
"spinner",
|
|
51
|
+
"success",
|
|
52
|
+
"sync_print",
|
|
53
|
+
"update_available",
|
|
54
|
+
"usage",
|
|
55
|
+
"version",
|
|
56
|
+
"warning",
|
|
57
|
+
# From panels module
|
|
58
|
+
"agent",
|
|
59
|
+
"dump_messages",
|
|
60
|
+
"error",
|
|
61
|
+
"help",
|
|
62
|
+
"models",
|
|
63
|
+
"panel",
|
|
64
|
+
"sync_panel",
|
|
65
|
+
"sync_tool_confirm",
|
|
66
|
+
"tool_confirm",
|
|
67
|
+
# From prompt_manager module
|
|
68
|
+
"PromptConfig",
|
|
69
|
+
"PromptManager",
|
|
70
|
+
# From validators module
|
|
71
|
+
"ModelValidator",
|
|
72
|
+
# Local utilities
|
|
73
|
+
"markdown",
|
|
74
|
+
]
|
tunacode/ui/constants.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""UI-specific constants for Sidekick."""
|
|
2
|
+
|
|
3
|
+
# UI Layout Constants
|
|
4
|
+
DEFAULT_PANEL_PADDING = {"top": 1, "right": 0, "bottom": 1, "left": 0}
|
|
5
|
+
|
|
6
|
+
# Spinner Configuration
|
|
7
|
+
SPINNER_TYPE = "dots12" # Modern spinner style
|
|
8
|
+
SPINNER_STYLE = "#00d7ff" # Modern cyan color
|
|
9
|
+
|
|
10
|
+
# Input Configuration
|
|
11
|
+
DEFAULT_PROMPT = "❯ "
|
|
12
|
+
MULTILINE_PROMPT = " ❯ "
|
|
13
|
+
MAX_HISTORY_SIZE = 1000
|
|
14
|
+
|
|
15
|
+
# Display Limits
|
|
16
|
+
MAX_DISPLAY_LENGTH = 50 # For truncating in UI
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module: sidekick.ui.decorators
|
|
3
|
+
|
|
4
|
+
Provides decorators for UI functions including sync/async wrapper patterns.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
from functools import wraps
|
|
9
|
+
from typing import Any, Callable, TypeVar
|
|
10
|
+
|
|
11
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def create_sync_wrapper(async_func: F) -> F:
|
|
15
|
+
"""Create a synchronous wrapper for an async function.
|
|
16
|
+
|
|
17
|
+
This decorator does NOT modify the original async function.
|
|
18
|
+
Instead, it attaches a sync version as a 'sync' attribute.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
async_func: The async function to wrap
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
The original async function with sync version attached
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
@wraps(async_func)
|
|
28
|
+
def sync_wrapper(*args, **kwargs):
|
|
29
|
+
try:
|
|
30
|
+
loop = asyncio.get_event_loop()
|
|
31
|
+
if loop.is_running():
|
|
32
|
+
# If we're already in an async context, we can't use run_until_complete
|
|
33
|
+
# This might happen when called from within an async function
|
|
34
|
+
raise RuntimeError(
|
|
35
|
+
f"Cannot call sync_{async_func.__name__} from within an async context. "
|
|
36
|
+
f"Use await {async_func.__name__}() instead."
|
|
37
|
+
)
|
|
38
|
+
except RuntimeError:
|
|
39
|
+
# No event loop exists, create one
|
|
40
|
+
loop = asyncio.new_event_loop()
|
|
41
|
+
asyncio.set_event_loop(loop)
|
|
42
|
+
|
|
43
|
+
return loop.run_until_complete(async_func(*args, **kwargs))
|
|
44
|
+
|
|
45
|
+
# Set a naming convention
|
|
46
|
+
sync_wrapper.__name__ = f"sync_{async_func.__name__}"
|
|
47
|
+
sync_wrapper.__qualname__ = f"sync_{async_func.__qualname__}"
|
|
48
|
+
|
|
49
|
+
# Update docstring to indicate this is a sync version
|
|
50
|
+
if async_func.__doc__:
|
|
51
|
+
sync_wrapper.__doc__ = (
|
|
52
|
+
f"Synchronous version of {async_func.__name__}.\n\n{async_func.__doc__}"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Attach the sync version as an attribute
|
|
56
|
+
async_func.sync = sync_wrapper
|
|
57
|
+
|
|
58
|
+
# Return the original async function
|
|
59
|
+
return async_func
|
tunacode/ui/input.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""User input handling functions for Sidekick UI."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from prompt_toolkit.formatted_text import HTML
|
|
6
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
7
|
+
from prompt_toolkit.styles import Style
|
|
8
|
+
from prompt_toolkit.validation import Validator
|
|
9
|
+
|
|
10
|
+
from tunacode.constants import UI_COLORS, UI_PROMPT_PREFIX
|
|
11
|
+
from tunacode.core.state import StateManager
|
|
12
|
+
|
|
13
|
+
from .completers import create_completer
|
|
14
|
+
from .keybindings import create_key_bindings
|
|
15
|
+
from .lexers import FileReferenceLexer
|
|
16
|
+
from .prompt_manager import PromptConfig, PromptManager
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def formatted_text(text: str) -> HTML:
|
|
20
|
+
"""Create formatted HTML text."""
|
|
21
|
+
return HTML(text)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def input(
|
|
25
|
+
session_key: str,
|
|
26
|
+
pretext: str = UI_PROMPT_PREFIX,
|
|
27
|
+
is_password: bool = False,
|
|
28
|
+
validator: Optional[Validator] = None,
|
|
29
|
+
multiline: bool = False,
|
|
30
|
+
key_bindings: Optional[KeyBindings] = None,
|
|
31
|
+
placeholder: Optional[HTML] = None,
|
|
32
|
+
completer=None,
|
|
33
|
+
lexer=None,
|
|
34
|
+
timeoutlen: float = 0.05,
|
|
35
|
+
state_manager: Optional[StateManager] = None,
|
|
36
|
+
) -> str:
|
|
37
|
+
"""
|
|
38
|
+
Prompt for user input using simplified prompt management.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
session_key: The session key for the prompt
|
|
42
|
+
pretext: The text to display before the input prompt
|
|
43
|
+
is_password: Whether to mask the input
|
|
44
|
+
validator: Optional input validator
|
|
45
|
+
multiline: Whether to allow multiline input
|
|
46
|
+
key_bindings: Optional custom key bindings
|
|
47
|
+
placeholder: Optional placeholder text
|
|
48
|
+
completer: Optional completer for tab completion
|
|
49
|
+
lexer: Optional lexer for syntax highlighting
|
|
50
|
+
timeoutlen: Timeout length for input
|
|
51
|
+
state_manager: The state manager for session storage
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
User input string
|
|
55
|
+
"""
|
|
56
|
+
# Create prompt configuration
|
|
57
|
+
config = PromptConfig(
|
|
58
|
+
multiline=multiline,
|
|
59
|
+
is_password=is_password,
|
|
60
|
+
validator=validator,
|
|
61
|
+
key_bindings=key_bindings,
|
|
62
|
+
placeholder=placeholder,
|
|
63
|
+
completer=completer,
|
|
64
|
+
lexer=lexer,
|
|
65
|
+
timeoutlen=timeoutlen,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Create prompt manager
|
|
69
|
+
manager = PromptManager(state_manager)
|
|
70
|
+
|
|
71
|
+
# Get user input
|
|
72
|
+
return await manager.get_input(session_key, pretext, config)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
async def multiline_input(state_manager: Optional[StateManager] = None, command_registry=None) -> str:
|
|
76
|
+
"""Get multiline input from the user with @file completion and highlighting."""
|
|
77
|
+
kb = create_key_bindings()
|
|
78
|
+
placeholder = formatted_text(
|
|
79
|
+
(
|
|
80
|
+
"<darkgrey>"
|
|
81
|
+
"<bold>Enter</bold> to submit, "
|
|
82
|
+
"<bold>Esc + Enter</bold> for new line, "
|
|
83
|
+
"<bold>/help</bold> for commands"
|
|
84
|
+
"</darkgrey>"
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
return await input(
|
|
88
|
+
"multiline",
|
|
89
|
+
key_bindings=kb,
|
|
90
|
+
multiline=True,
|
|
91
|
+
placeholder=placeholder,
|
|
92
|
+
completer=create_completer(command_registry),
|
|
93
|
+
lexer=FileReferenceLexer(),
|
|
94
|
+
state_manager=state_manager
|
|
95
|
+
)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Key binding handlers for Sidekick UI."""
|
|
2
|
+
|
|
3
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def create_key_bindings() -> KeyBindings:
|
|
7
|
+
"""Create and configure key bindings for the UI."""
|
|
8
|
+
kb = KeyBindings()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@kb.add("enter")
|
|
13
|
+
def _submit(event):
|
|
14
|
+
"""Submit the current buffer."""
|
|
15
|
+
event.current_buffer.validate_and_handle()
|
|
16
|
+
|
|
17
|
+
@kb.add("c-o") # ctrl+o
|
|
18
|
+
def _newline(event):
|
|
19
|
+
"""Insert a newline character."""
|
|
20
|
+
event.current_buffer.insert_text("\n")
|
|
21
|
+
|
|
22
|
+
@kb.add("escape", "enter")
|
|
23
|
+
def _escape_enter(event):
|
|
24
|
+
"""Insert a newline when escape then enter is pressed."""
|
|
25
|
+
event.current_buffer.insert_text("\n")
|
|
26
|
+
|
|
27
|
+
return kb
|
tunacode/ui/lexers.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Custom lexers for syntax highlighting in the CLI."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from prompt_toolkit.formatted_text import FormattedText
|
|
6
|
+
from prompt_toolkit.lexers import Lexer
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FileReferenceLexer(Lexer):
|
|
10
|
+
"""Lexer that highlights @file references in light blue."""
|
|
11
|
+
|
|
12
|
+
# Pattern to match @file references
|
|
13
|
+
FILE_REF_PATTERN = re.compile(r'@([\w./_-]+)')
|
|
14
|
+
|
|
15
|
+
def lex_document(self, document):
|
|
16
|
+
"""Return a formatted text list for the given document."""
|
|
17
|
+
lines = document.text.split('\n')
|
|
18
|
+
|
|
19
|
+
def get_line_tokens(line_number):
|
|
20
|
+
"""Get tokens for a specific line."""
|
|
21
|
+
if line_number >= len(lines):
|
|
22
|
+
return []
|
|
23
|
+
|
|
24
|
+
line = lines[line_number]
|
|
25
|
+
tokens = []
|
|
26
|
+
last_end = 0
|
|
27
|
+
|
|
28
|
+
# Find all @file references in the line
|
|
29
|
+
for match in self.FILE_REF_PATTERN.finditer(line):
|
|
30
|
+
start, end = match.span()
|
|
31
|
+
|
|
32
|
+
# Add text before the match
|
|
33
|
+
if start > last_end:
|
|
34
|
+
tokens.append(('', line[last_end:start]))
|
|
35
|
+
|
|
36
|
+
# Add the @file reference with styling
|
|
37
|
+
tokens.append(('class:file-reference', match.group(0)))
|
|
38
|
+
last_end = end
|
|
39
|
+
|
|
40
|
+
# Add remaining text
|
|
41
|
+
if last_end < len(line):
|
|
42
|
+
tokens.append(('', line[last_end:]))
|
|
43
|
+
|
|
44
|
+
return tokens
|
|
45
|
+
|
|
46
|
+
return get_line_tokens
|
tunacode/ui/output.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Output and display functions for Sidekick UI."""
|
|
2
|
+
|
|
3
|
+
from prompt_toolkit.application import run_in_terminal
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.padding import Padding
|
|
6
|
+
|
|
7
|
+
from tunacode.configuration.settings import ApplicationSettings
|
|
8
|
+
from tunacode.constants import (MSG_UPDATE_AVAILABLE, MSG_UPDATE_INSTRUCTION, MSG_VERSION_DISPLAY,
|
|
9
|
+
UI_COLORS, UI_THINKING_MESSAGE)
|
|
10
|
+
from tunacode.core.state import StateManager
|
|
11
|
+
from tunacode.utils.file_utils import DotDict
|
|
12
|
+
|
|
13
|
+
from .constants import SPINNER_TYPE
|
|
14
|
+
from .decorators import create_sync_wrapper
|
|
15
|
+
|
|
16
|
+
console = Console()
|
|
17
|
+
colors = DotDict(UI_COLORS)
|
|
18
|
+
|
|
19
|
+
BANNER = """[bold #00d7ff]┌─────────────────────────────────────────────────────────────────┐[/bold #00d7ff]
|
|
20
|
+
[bold #00d7ff]│[/bold #00d7ff] [bold white]T U N A C O D E[/bold white] [dim #64748b]• Agentic AI Development Environment[/dim #64748b] [bold #00d7ff]│[/bold #00d7ff]
|
|
21
|
+
[bold #00d7ff]└─────────────────────────────────────────────────────────────────┘[/bold #00d7ff]"""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@create_sync_wrapper
|
|
25
|
+
async def print(message, **kwargs) -> None:
|
|
26
|
+
"""Print a message to the console."""
|
|
27
|
+
await run_in_terminal(lambda: console.print(message, **kwargs))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async def line() -> None:
|
|
31
|
+
"""Print a line to the console."""
|
|
32
|
+
await run_in_terminal(lambda: console.line())
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async def info(text: str) -> None:
|
|
36
|
+
"""Print an informational message."""
|
|
37
|
+
await print(f"[{colors.primary}]●[/{colors.primary}] {text}", style=colors.muted)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def success(message: str) -> None:
|
|
41
|
+
"""Print a success message."""
|
|
42
|
+
await print(f"[{colors.success}]✓[/{colors.success}] {message}")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def warning(text: str) -> None:
|
|
46
|
+
"""Print a warning message."""
|
|
47
|
+
await print(f"[{colors.warning}]⚠[/{colors.warning}] {text}")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
async def muted(text: str, spaces: int = 0) -> None:
|
|
51
|
+
"""Print a muted message."""
|
|
52
|
+
await print(f"{' ' * spaces}[{colors.muted}]•[/{colors.muted}] [dim]{text}[/dim]")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
async def usage(usage: str) -> None:
|
|
56
|
+
"""Print usage information."""
|
|
57
|
+
await print(Padding(usage, (0, 0, 1, 2)), style=colors.muted)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
async def version() -> None:
|
|
61
|
+
"""Print version information."""
|
|
62
|
+
app_settings = ApplicationSettings()
|
|
63
|
+
await info(MSG_VERSION_DISPLAY.format(version=app_settings.version))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
async def banner() -> None:
|
|
67
|
+
"""Display the application banner."""
|
|
68
|
+
console.clear()
|
|
69
|
+
banner_padding = Padding(BANNER, (2, 0, 1, 0))
|
|
70
|
+
await print(banner_padding)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
async def clear() -> None:
|
|
74
|
+
"""Clear the console and display the banner."""
|
|
75
|
+
console.clear()
|
|
76
|
+
await banner()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
async def update_available(latest_version: str) -> None:
|
|
80
|
+
"""Display update available notification."""
|
|
81
|
+
await warning(MSG_UPDATE_AVAILABLE.format(latest_version=latest_version))
|
|
82
|
+
await muted(MSG_UPDATE_INSTRUCTION)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
async def spinner(show: bool = True, spinner_obj=None, state_manager: StateManager = None):
|
|
86
|
+
"""Manage a spinner display."""
|
|
87
|
+
icon = SPINNER_TYPE
|
|
88
|
+
message = UI_THINKING_MESSAGE
|
|
89
|
+
|
|
90
|
+
# Get spinner from state manager if available
|
|
91
|
+
if spinner_obj is None and state_manager:
|
|
92
|
+
spinner_obj = state_manager.session.spinner
|
|
93
|
+
|
|
94
|
+
if not spinner_obj:
|
|
95
|
+
spinner_obj = await run_in_terminal(lambda: console.status(message, spinner=icon))
|
|
96
|
+
# Store it back in state manager if available
|
|
97
|
+
if state_manager:
|
|
98
|
+
state_manager.session.spinner = spinner_obj
|
|
99
|
+
|
|
100
|
+
if show:
|
|
101
|
+
spinner_obj.start()
|
|
102
|
+
else:
|
|
103
|
+
spinner_obj.stop()
|
|
104
|
+
|
|
105
|
+
return spinner_obj
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# Auto-generated sync version
|
|
109
|
+
sync_print = print.sync # type: ignore
|
tunacode/ui/panels.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Panel display functions for Sidekick UI."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional, Union
|
|
4
|
+
|
|
5
|
+
from rich.markdown import Markdown
|
|
6
|
+
from rich.padding import Padding
|
|
7
|
+
from rich.panel import Panel
|
|
8
|
+
from rich.pretty import Pretty
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
from tunacode.configuration.models import ModelRegistry
|
|
12
|
+
from tunacode.constants import (APP_NAME, CMD_CLEAR, CMD_COMPACT, CMD_DUMP, CMD_EXIT, CMD_HELP,
|
|
13
|
+
CMD_MODEL, CMD_UNDO, CMD_YOLO, DESC_CLEAR, DESC_COMPACT, DESC_DUMP,
|
|
14
|
+
DESC_EXIT, DESC_HELP, DESC_MODEL, DESC_MODEL_DEFAULT,
|
|
15
|
+
DESC_MODEL_SWITCH, DESC_UNDO, DESC_YOLO, PANEL_AVAILABLE_COMMANDS,
|
|
16
|
+
PANEL_ERROR, PANEL_MESSAGE_HISTORY, PANEL_MODELS, UI_COLORS)
|
|
17
|
+
from tunacode.core.state import StateManager
|
|
18
|
+
from tunacode.utils.file_utils import DotDict
|
|
19
|
+
|
|
20
|
+
from .constants import DEFAULT_PANEL_PADDING
|
|
21
|
+
from .decorators import create_sync_wrapper
|
|
22
|
+
from .output import print
|
|
23
|
+
|
|
24
|
+
colors = DotDict(UI_COLORS)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@create_sync_wrapper
|
|
28
|
+
async def panel(
|
|
29
|
+
title: str,
|
|
30
|
+
text: Union[str, Markdown, Pretty],
|
|
31
|
+
top: int = DEFAULT_PANEL_PADDING["top"],
|
|
32
|
+
right: int = DEFAULT_PANEL_PADDING["right"],
|
|
33
|
+
bottom: int = DEFAULT_PANEL_PADDING["bottom"],
|
|
34
|
+
left: int = DEFAULT_PANEL_PADDING["left"],
|
|
35
|
+
border_style: Optional[str] = None,
|
|
36
|
+
**kwargs: Any,
|
|
37
|
+
) -> None:
|
|
38
|
+
"""Display a rich panel with modern styling."""
|
|
39
|
+
border_style = border_style or kwargs.get("style") or colors.border
|
|
40
|
+
panel_obj = Panel(
|
|
41
|
+
Padding(text, (0, 1, 0, 1)),
|
|
42
|
+
title=f"[bold]{title}[/bold]",
|
|
43
|
+
title_align="left",
|
|
44
|
+
border_style=border_style,
|
|
45
|
+
padding=(0, 1)
|
|
46
|
+
)
|
|
47
|
+
await print(Padding(panel_obj, (top, right, bottom, left)), **kwargs)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
async def agent(text: str, bottom: int = 1) -> None:
|
|
51
|
+
"""Display an agent panel with modern styling."""
|
|
52
|
+
title = f"[bold {colors.primary}]●[/bold {colors.primary}] {APP_NAME}"
|
|
53
|
+
await panel(title, Markdown(text), bottom=bottom, border_style=colors.primary)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async def error(text: str) -> None:
|
|
57
|
+
"""Display an error panel."""
|
|
58
|
+
await panel(PANEL_ERROR, text, style=colors.error)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
async def dump_messages(messages_list=None, state_manager: StateManager = None) -> None:
|
|
62
|
+
"""Display message history panel."""
|
|
63
|
+
if messages_list is None and state_manager:
|
|
64
|
+
# Get messages from state manager
|
|
65
|
+
messages = Pretty(state_manager.session.messages)
|
|
66
|
+
elif messages_list is not None:
|
|
67
|
+
messages = Pretty(messages_list)
|
|
68
|
+
else:
|
|
69
|
+
# No messages available
|
|
70
|
+
messages = Pretty([])
|
|
71
|
+
await panel(PANEL_MESSAGE_HISTORY, messages, style=colors.muted)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
async def models(state_manager: StateManager = None) -> None:
|
|
75
|
+
"""Display available models panel."""
|
|
76
|
+
model_registry = ModelRegistry()
|
|
77
|
+
model_ids = list(model_registry.list_models().keys())
|
|
78
|
+
model_list = "\n".join([f"{index} - {model}" for index, model in enumerate(model_ids)])
|
|
79
|
+
current_model = state_manager.session.current_model if state_manager else "unknown"
|
|
80
|
+
text = f"Current model: {current_model}\n\n{model_list}"
|
|
81
|
+
await panel(PANEL_MODELS, text, border_style=colors.muted)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
async def help(command_registry=None) -> None:
|
|
85
|
+
"""Display the available commands organized by category."""
|
|
86
|
+
table = Table(show_header=False, box=None, padding=(0, 3, 0, 0))
|
|
87
|
+
table.add_column("Command", style=f"bold {colors.primary}", justify="right", min_width=16)
|
|
88
|
+
table.add_column("Description", style=colors.muted)
|
|
89
|
+
|
|
90
|
+
if command_registry:
|
|
91
|
+
# Use the new command registry to display commands by category
|
|
92
|
+
from ..cli.commands import CommandCategory
|
|
93
|
+
|
|
94
|
+
category_order = [
|
|
95
|
+
CommandCategory.SYSTEM,
|
|
96
|
+
CommandCategory.NAVIGATION,
|
|
97
|
+
CommandCategory.DEVELOPMENT,
|
|
98
|
+
CommandCategory.MODEL,
|
|
99
|
+
CommandCategory.DEBUG,
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
for category in category_order:
|
|
103
|
+
commands = command_registry.get_commands_by_category(category)
|
|
104
|
+
if commands:
|
|
105
|
+
# Add category header
|
|
106
|
+
table.add_row("", "")
|
|
107
|
+
table.add_row(f"[bold]{category.value.title()}[/bold]", "")
|
|
108
|
+
|
|
109
|
+
# Add commands in this category
|
|
110
|
+
for command in commands:
|
|
111
|
+
# Show primary command name
|
|
112
|
+
cmd_display = f"/{command.name}"
|
|
113
|
+
table.add_row(cmd_display, command.description)
|
|
114
|
+
|
|
115
|
+
# Special handling for model command variations
|
|
116
|
+
if command.name == "model":
|
|
117
|
+
table.add_row(f"{cmd_display} <n>", DESC_MODEL_SWITCH)
|
|
118
|
+
table.add_row(f"{cmd_display} <n> default", DESC_MODEL_DEFAULT)
|
|
119
|
+
|
|
120
|
+
# Add built-in commands
|
|
121
|
+
table.add_row("", "")
|
|
122
|
+
table.add_row("[bold]Built-in[/bold]", "")
|
|
123
|
+
table.add_row(CMD_EXIT, DESC_EXIT)
|
|
124
|
+
else:
|
|
125
|
+
# Fallback to static command list
|
|
126
|
+
commands = [
|
|
127
|
+
(CMD_HELP, DESC_HELP),
|
|
128
|
+
(CMD_CLEAR, DESC_CLEAR),
|
|
129
|
+
(CMD_DUMP, DESC_DUMP),
|
|
130
|
+
(CMD_YOLO, DESC_YOLO),
|
|
131
|
+
(CMD_UNDO, DESC_UNDO),
|
|
132
|
+
(CMD_COMPACT, DESC_COMPACT),
|
|
133
|
+
(CMD_MODEL, DESC_MODEL),
|
|
134
|
+
(f"{CMD_MODEL} <n>", DESC_MODEL_SWITCH),
|
|
135
|
+
(f"{CMD_MODEL} <n> default", DESC_MODEL_DEFAULT),
|
|
136
|
+
(CMD_EXIT, DESC_EXIT),
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
for cmd, desc in commands:
|
|
140
|
+
table.add_row(cmd, desc)
|
|
141
|
+
|
|
142
|
+
await panel(PANEL_AVAILABLE_COMMANDS, table, border_style=colors.muted)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@create_sync_wrapper
|
|
146
|
+
async def tool_confirm(
|
|
147
|
+
title: str, content: Union[str, Markdown], filepath: Optional[str] = None
|
|
148
|
+
) -> None:
|
|
149
|
+
"""Display a tool confirmation panel."""
|
|
150
|
+
bottom_padding = 0 if filepath else 1
|
|
151
|
+
await panel(title, content, bottom=bottom_padding, border_style=colors.warning)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# Auto-generated sync versions
|
|
155
|
+
sync_panel = panel.sync # type: ignore
|
|
156
|
+
sync_tool_confirm = tool_confirm.sync # type: ignore
|