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
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module: sidekick.tools.run_command
|
|
3
|
+
|
|
4
|
+
Command execution tool for agent operations in the Sidekick application.
|
|
5
|
+
Provides controlled shell command execution with output capture and truncation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import subprocess
|
|
9
|
+
|
|
10
|
+
from tunacode.constants import (CMD_OUTPUT_FORMAT, CMD_OUTPUT_NO_ERRORS, CMD_OUTPUT_NO_OUTPUT,
|
|
11
|
+
CMD_OUTPUT_TRUNCATED, COMMAND_OUTPUT_END_SIZE,
|
|
12
|
+
COMMAND_OUTPUT_START_INDEX, COMMAND_OUTPUT_THRESHOLD,
|
|
13
|
+
ERROR_COMMAND_EXECUTION, MAX_COMMAND_OUTPUT)
|
|
14
|
+
from tunacode.exceptions import ToolExecutionError
|
|
15
|
+
from tunacode.tools.base import BaseTool
|
|
16
|
+
from tunacode.types import ToolResult
|
|
17
|
+
from tunacode.ui import console as default_ui
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class RunCommandTool(BaseTool):
|
|
21
|
+
"""Tool for running shell commands."""
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def tool_name(self) -> str:
|
|
25
|
+
return "Shell"
|
|
26
|
+
|
|
27
|
+
async def _execute(self, command: str) -> ToolResult:
|
|
28
|
+
"""Run a shell command and return the output.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
command: The command to run.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
ToolResult: The output of the command (stdout and stderr).
|
|
35
|
+
|
|
36
|
+
Raises:
|
|
37
|
+
FileNotFoundError: If command not found
|
|
38
|
+
Exception: Any command execution errors
|
|
39
|
+
"""
|
|
40
|
+
process = subprocess.Popen(
|
|
41
|
+
command,
|
|
42
|
+
shell=True,
|
|
43
|
+
stdout=subprocess.PIPE,
|
|
44
|
+
stderr=subprocess.PIPE,
|
|
45
|
+
text=True,
|
|
46
|
+
)
|
|
47
|
+
stdout, stderr = process.communicate()
|
|
48
|
+
output = stdout.strip() or CMD_OUTPUT_NO_OUTPUT
|
|
49
|
+
error = stderr.strip() or CMD_OUTPUT_NO_ERRORS
|
|
50
|
+
resp = CMD_OUTPUT_FORMAT.format(output=output, error=error).strip()
|
|
51
|
+
|
|
52
|
+
# Truncate if the output is too long to prevent issues
|
|
53
|
+
if len(resp) > MAX_COMMAND_OUTPUT:
|
|
54
|
+
# Include both the beginning and end of the output
|
|
55
|
+
start_part = resp[:COMMAND_OUTPUT_START_INDEX]
|
|
56
|
+
end_part = (
|
|
57
|
+
resp[-COMMAND_OUTPUT_END_SIZE:]
|
|
58
|
+
if len(resp) > COMMAND_OUTPUT_THRESHOLD
|
|
59
|
+
else resp[COMMAND_OUTPUT_START_INDEX:]
|
|
60
|
+
)
|
|
61
|
+
truncated_resp = start_part + CMD_OUTPUT_TRUNCATED + end_part
|
|
62
|
+
return truncated_resp
|
|
63
|
+
|
|
64
|
+
return resp
|
|
65
|
+
|
|
66
|
+
async def _handle_error(self, error: Exception, command: str = None) -> ToolResult:
|
|
67
|
+
"""Handle errors with specific messages for common cases.
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
ToolExecutionError: Always raised with structured error information
|
|
71
|
+
"""
|
|
72
|
+
if isinstance(error, FileNotFoundError):
|
|
73
|
+
err_msg = ERROR_COMMAND_EXECUTION.format(command=command, error=error)
|
|
74
|
+
else:
|
|
75
|
+
# Use parent class handling for other errors
|
|
76
|
+
await super()._handle_error(error, command)
|
|
77
|
+
return # super() will raise, this is unreachable
|
|
78
|
+
|
|
79
|
+
if self.ui:
|
|
80
|
+
await self.ui.error(err_msg)
|
|
81
|
+
|
|
82
|
+
raise ToolExecutionError(tool_name=self.tool_name, message=err_msg, original_error=error)
|
|
83
|
+
|
|
84
|
+
def _get_error_context(self, command: str = None) -> str:
|
|
85
|
+
"""Get error context for command execution."""
|
|
86
|
+
if command:
|
|
87
|
+
return f"running command '{command}'"
|
|
88
|
+
return super()._get_error_context()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# Create the function that maintains the existing interface
|
|
92
|
+
async def run_command(command: str) -> ToolResult:
|
|
93
|
+
"""
|
|
94
|
+
Run a shell command and return the output. User must confirm risky commands.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
command (str): The command to run.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
ToolResult: The output of the command (stdout and stderr) or an error message.
|
|
101
|
+
"""
|
|
102
|
+
tool = RunCommandTool(default_ui)
|
|
103
|
+
try:
|
|
104
|
+
return await tool.execute(command)
|
|
105
|
+
except ToolExecutionError as e:
|
|
106
|
+
# Return error message for pydantic-ai compatibility
|
|
107
|
+
return str(e)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module: sidekick.tools.update_file
|
|
3
|
+
|
|
4
|
+
File update tool for agent operations in the Sidekick application.
|
|
5
|
+
Enables safe text replacement in existing files with target/patch semantics.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
from pydantic_ai.exceptions import ModelRetry
|
|
11
|
+
|
|
12
|
+
from tunacode.exceptions import ToolExecutionError
|
|
13
|
+
from tunacode.tools.base import FileBasedTool
|
|
14
|
+
from tunacode.types import FileContent, FilePath, ToolResult
|
|
15
|
+
from tunacode.ui import console as default_ui
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class UpdateFileTool(FileBasedTool):
|
|
19
|
+
"""Tool for updating existing files by replacing text blocks."""
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def tool_name(self) -> str:
|
|
23
|
+
return "Update"
|
|
24
|
+
|
|
25
|
+
async def _execute(
|
|
26
|
+
self, filepath: FilePath, target: FileContent, patch: FileContent
|
|
27
|
+
) -> ToolResult:
|
|
28
|
+
"""Update an existing file by replacing a target text block with a patch.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
filepath: The path to the file to update.
|
|
32
|
+
target: The entire, exact block of text to be replaced.
|
|
33
|
+
patch: The new block of text to insert.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
ToolResult: A message indicating success.
|
|
37
|
+
|
|
38
|
+
Raises:
|
|
39
|
+
ModelRetry: If file not found or target not found
|
|
40
|
+
Exception: Any file operation errors
|
|
41
|
+
"""
|
|
42
|
+
if not os.path.exists(filepath):
|
|
43
|
+
raise ModelRetry(
|
|
44
|
+
f"File '{filepath}' not found. Cannot update. "
|
|
45
|
+
"Verify the filepath or use `write_file` if it's a new file."
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
with open(filepath, "r", encoding="utf-8") as f:
|
|
49
|
+
original = f.read()
|
|
50
|
+
|
|
51
|
+
if target not in original:
|
|
52
|
+
# Provide context to help the LLM find the target
|
|
53
|
+
context_lines = 10
|
|
54
|
+
lines = original.splitlines()
|
|
55
|
+
snippet = "\n".join(lines[:context_lines])
|
|
56
|
+
# Use ModelRetry to guide the LLM
|
|
57
|
+
raise ModelRetry(
|
|
58
|
+
f"Target block not found in '{filepath}'. "
|
|
59
|
+
"Ensure the `target` argument exactly matches the content you want to replace. "
|
|
60
|
+
f"File starts with:\n---\n{snippet}\n---"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
new_content = original.replace(target, patch, 1) # Replace only the first occurrence
|
|
64
|
+
|
|
65
|
+
if original == new_content:
|
|
66
|
+
# This could happen if target and patch are identical
|
|
67
|
+
raise ModelRetry(
|
|
68
|
+
f"Update target found, but replacement resulted in no changes to '{filepath}'. "
|
|
69
|
+
"Was the `target` identical to the `patch`? Please check the file content."
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
with open(filepath, "w", encoding="utf-8") as f:
|
|
73
|
+
f.write(new_content)
|
|
74
|
+
|
|
75
|
+
return f"File '{filepath}' updated successfully."
|
|
76
|
+
|
|
77
|
+
def _format_args(
|
|
78
|
+
self, filepath: FilePath, target: FileContent = None, patch: FileContent = None
|
|
79
|
+
) -> str:
|
|
80
|
+
"""Format arguments, truncating target and patch for display."""
|
|
81
|
+
args = [repr(filepath)]
|
|
82
|
+
|
|
83
|
+
if target is not None:
|
|
84
|
+
if len(target) > 50:
|
|
85
|
+
args.append(f"target='{target[:47]}...'")
|
|
86
|
+
else:
|
|
87
|
+
args.append(f"target={repr(target)}")
|
|
88
|
+
|
|
89
|
+
if patch is not None:
|
|
90
|
+
if len(patch) > 50:
|
|
91
|
+
args.append(f"patch='{patch[:47]}...'")
|
|
92
|
+
else:
|
|
93
|
+
args.append(f"patch={repr(patch)}")
|
|
94
|
+
|
|
95
|
+
return ", ".join(args)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# Create the function that maintains the existing interface
|
|
99
|
+
async def update_file(filepath: FilePath, target: FileContent, patch: FileContent) -> ToolResult:
|
|
100
|
+
"""
|
|
101
|
+
Update an existing file by replacing a target text block with a patch.
|
|
102
|
+
Requires confirmation with diff before applying.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
filepath (FilePath): The path to the file to update.
|
|
106
|
+
target (FileContent): The entire, exact block of text to be replaced.
|
|
107
|
+
patch (FileContent): The new block of text to insert.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
ToolResult: A message indicating the success or failure of the operation.
|
|
111
|
+
"""
|
|
112
|
+
tool = UpdateFileTool(default_ui)
|
|
113
|
+
try:
|
|
114
|
+
return await tool.execute(filepath, target, patch)
|
|
115
|
+
except ToolExecutionError as e:
|
|
116
|
+
# Return error message for pydantic-ai compatibility
|
|
117
|
+
return str(e)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module: sidekick.tools.write_file
|
|
3
|
+
|
|
4
|
+
File writing tool for agent operations in the Sidekick application.
|
|
5
|
+
Creates new files with automatic directory creation and overwrite protection.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
from pydantic_ai.exceptions import ModelRetry
|
|
11
|
+
|
|
12
|
+
from tunacode.exceptions import ToolExecutionError
|
|
13
|
+
from tunacode.tools.base import FileBasedTool
|
|
14
|
+
from tunacode.types import FileContent, FilePath, ToolResult
|
|
15
|
+
from tunacode.ui import console as default_ui
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class WriteFileTool(FileBasedTool):
|
|
19
|
+
"""Tool for writing content to new files."""
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def tool_name(self) -> str:
|
|
23
|
+
return "Write"
|
|
24
|
+
|
|
25
|
+
async def _execute(self, filepath: FilePath, content: FileContent) -> ToolResult:
|
|
26
|
+
"""Write content to a new file. Fails if the file already exists.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
filepath: The path to the file to write to.
|
|
30
|
+
content: The content to write to the file.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
ToolResult: A message indicating success.
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
ModelRetry: If the file already exists
|
|
37
|
+
Exception: Any file writing errors
|
|
38
|
+
"""
|
|
39
|
+
# Prevent overwriting existing files with this tool.
|
|
40
|
+
if os.path.exists(filepath):
|
|
41
|
+
# Use ModelRetry to guide the LLM
|
|
42
|
+
raise ModelRetry(
|
|
43
|
+
f"File '{filepath}' already exists. "
|
|
44
|
+
"Use the `update_file` tool to modify it, or choose a different filepath."
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Create directories if they don't exist
|
|
48
|
+
dirpath = os.path.dirname(filepath)
|
|
49
|
+
if dirpath and not os.path.exists(dirpath):
|
|
50
|
+
os.makedirs(dirpath, exist_ok=True)
|
|
51
|
+
|
|
52
|
+
with open(filepath, "w", encoding="utf-8") as file:
|
|
53
|
+
file.write(content)
|
|
54
|
+
|
|
55
|
+
return f"Successfully wrote to new file: {filepath}"
|
|
56
|
+
|
|
57
|
+
def _format_args(self, filepath: FilePath, content: FileContent = None) -> str:
|
|
58
|
+
"""Format arguments, truncating content for display."""
|
|
59
|
+
if content is not None and len(content) > 50:
|
|
60
|
+
return f"{repr(filepath)}, content='{content[:47]}...'"
|
|
61
|
+
return super()._format_args(filepath, content)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# Create the function that maintains the existing interface
|
|
65
|
+
async def write_file(filepath: FilePath, content: FileContent) -> ToolResult:
|
|
66
|
+
"""
|
|
67
|
+
Write content to a new file. Fails if the file already exists.
|
|
68
|
+
Requires confirmation before writing.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
filepath (FilePath): The path to the file to write to.
|
|
72
|
+
content (FileContent): The content to write to the file.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
ToolResult: A message indicating the success or failure of the operation.
|
|
76
|
+
"""
|
|
77
|
+
tool = WriteFileTool(default_ui)
|
|
78
|
+
try:
|
|
79
|
+
return await tool.execute(filepath, content)
|
|
80
|
+
except ToolExecutionError as e:
|
|
81
|
+
# Return error message for pydantic-ai compatibility
|
|
82
|
+
return str(e)
|
tunacode/types.py
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Centralized type definitions for Sidekick CLI.
|
|
3
|
+
|
|
4
|
+
This module contains all type aliases, protocols, and type definitions
|
|
5
|
+
used throughout the Sidekick codebase.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Awaitable, Callable, Dict, List, Optional, Protocol, Tuple, Union
|
|
11
|
+
|
|
12
|
+
# Try to import pydantic-ai types if available
|
|
13
|
+
try:
|
|
14
|
+
from pydantic_ai import Agent
|
|
15
|
+
from pydantic_ai.messages import ModelRequest, ModelResponse, ToolReturnPart
|
|
16
|
+
|
|
17
|
+
PydanticAgent = Agent
|
|
18
|
+
MessagePart = Union[ToolReturnPart, Any]
|
|
19
|
+
except ImportError:
|
|
20
|
+
# Fallback if pydantic-ai is not available
|
|
21
|
+
PydanticAgent = Any
|
|
22
|
+
MessagePart = Any
|
|
23
|
+
ModelRequest = Any
|
|
24
|
+
ModelResponse = Any
|
|
25
|
+
|
|
26
|
+
# =============================================================================
|
|
27
|
+
# Core Types
|
|
28
|
+
# =============================================================================
|
|
29
|
+
|
|
30
|
+
# Basic type aliases
|
|
31
|
+
UserConfig = Dict[str, Any]
|
|
32
|
+
EnvConfig = Dict[str, str]
|
|
33
|
+
ModelName = str
|
|
34
|
+
ToolName = str
|
|
35
|
+
SessionId = str
|
|
36
|
+
DeviceId = str
|
|
37
|
+
InputSessions = Dict[str, Any]
|
|
38
|
+
|
|
39
|
+
# =============================================================================
|
|
40
|
+
# Configuration Types
|
|
41
|
+
# =============================================================================
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class ModelPricing:
|
|
46
|
+
"""Pricing information for a model."""
|
|
47
|
+
|
|
48
|
+
input: float
|
|
49
|
+
cached_input: float
|
|
50
|
+
output: float
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class ModelConfig:
|
|
55
|
+
"""Configuration for a model including pricing."""
|
|
56
|
+
|
|
57
|
+
pricing: ModelPricing
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
ModelRegistry = Dict[str, ModelConfig]
|
|
61
|
+
|
|
62
|
+
# Path configuration
|
|
63
|
+
ConfigPath = Path
|
|
64
|
+
ConfigFile = Path
|
|
65
|
+
|
|
66
|
+
# =============================================================================
|
|
67
|
+
# Tool Types
|
|
68
|
+
# =============================================================================
|
|
69
|
+
|
|
70
|
+
# Tool execution types
|
|
71
|
+
ToolArgs = Dict[str, Any]
|
|
72
|
+
ToolResult = str
|
|
73
|
+
ToolCallback = Callable[[Any, Any], Awaitable[None]]
|
|
74
|
+
ToolCallId = str
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class ToolFunction(Protocol):
|
|
78
|
+
"""Protocol for tool functions."""
|
|
79
|
+
|
|
80
|
+
async def __call__(self, *args, **kwargs) -> str: ...
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class ToolConfirmationRequest:
|
|
85
|
+
"""Request for tool execution confirmation."""
|
|
86
|
+
|
|
87
|
+
tool_name: str
|
|
88
|
+
args: Dict[str, Any]
|
|
89
|
+
filepath: Optional[str] = None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass
|
|
93
|
+
class ToolConfirmationResponse:
|
|
94
|
+
"""Response from tool confirmation dialog."""
|
|
95
|
+
|
|
96
|
+
approved: bool
|
|
97
|
+
skip_future: bool = False
|
|
98
|
+
abort: bool = False
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# =============================================================================
|
|
102
|
+
# UI Types
|
|
103
|
+
# =============================================================================
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class UILogger(Protocol):
|
|
107
|
+
"""Protocol for UI logging operations."""
|
|
108
|
+
|
|
109
|
+
async def info(self, message: str) -> None: ...
|
|
110
|
+
async def error(self, message: str) -> None: ...
|
|
111
|
+
async def warning(self, message: str) -> None: ...
|
|
112
|
+
async def debug(self, message: str) -> None: ...
|
|
113
|
+
async def success(self, message: str) -> None: ...
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# UI callback types
|
|
117
|
+
UICallback = Callable[[str], Awaitable[None]]
|
|
118
|
+
UIInputCallback = Callable[[str, str], Awaitable[str]]
|
|
119
|
+
|
|
120
|
+
# =============================================================================
|
|
121
|
+
# Agent Types
|
|
122
|
+
# =============================================================================
|
|
123
|
+
|
|
124
|
+
# Agent response types
|
|
125
|
+
AgentResponse = Any # Replace with proper pydantic-ai types when available
|
|
126
|
+
MessageHistory = List[Any]
|
|
127
|
+
AgentRun = Any # pydantic_ai.RunContext or similar
|
|
128
|
+
|
|
129
|
+
# Agent configuration
|
|
130
|
+
AgentConfig = Dict[str, Any]
|
|
131
|
+
AgentName = str
|
|
132
|
+
|
|
133
|
+
# =============================================================================
|
|
134
|
+
# Session and State Types
|
|
135
|
+
# =============================================================================
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@dataclass
|
|
139
|
+
class SessionState:
|
|
140
|
+
"""Complete session state for the application."""
|
|
141
|
+
|
|
142
|
+
user_config: Dict[str, Any]
|
|
143
|
+
agents: Dict[str, Any]
|
|
144
|
+
messages: List[Any]
|
|
145
|
+
total_cost: float
|
|
146
|
+
current_model: str
|
|
147
|
+
spinner: Optional[Any]
|
|
148
|
+
tool_ignore: List[str]
|
|
149
|
+
yolo: bool
|
|
150
|
+
undo_initialized: bool
|
|
151
|
+
session_id: str
|
|
152
|
+
device_id: Optional[str]
|
|
153
|
+
input_sessions: Dict[str, Any]
|
|
154
|
+
current_task: Optional[Any]
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# Forward reference for StateManager to avoid circular imports
|
|
158
|
+
StateManager = Any # Will be replaced with actual StateManager type
|
|
159
|
+
|
|
160
|
+
# =============================================================================
|
|
161
|
+
# Command Types
|
|
162
|
+
# =============================================================================
|
|
163
|
+
|
|
164
|
+
# Command execution types
|
|
165
|
+
CommandArgs = List[str]
|
|
166
|
+
CommandResult = Optional[Any]
|
|
167
|
+
ProcessRequestCallback = Callable[[str, StateManager, bool], Awaitable[Any]]
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@dataclass
|
|
171
|
+
class CommandContext:
|
|
172
|
+
"""Context passed to command handlers."""
|
|
173
|
+
|
|
174
|
+
state_manager: StateManager
|
|
175
|
+
process_request: Optional[ProcessRequestCallback] = None
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# =============================================================================
|
|
179
|
+
# Service Types
|
|
180
|
+
# =============================================================================
|
|
181
|
+
|
|
182
|
+
# MCP (Model Context Protocol) types
|
|
183
|
+
MCPServerConfig = Dict[str, Any]
|
|
184
|
+
MCPServers = Dict[str, MCPServerConfig]
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# =============================================================================
|
|
188
|
+
# File Operation Types
|
|
189
|
+
# =============================================================================
|
|
190
|
+
|
|
191
|
+
# File-related types
|
|
192
|
+
FilePath = Union[str, Path]
|
|
193
|
+
FileContent = str
|
|
194
|
+
FileEncoding = str
|
|
195
|
+
FileDiff = Tuple[str, str] # (original, modified)
|
|
196
|
+
FileSize = int
|
|
197
|
+
LineNumber = int
|
|
198
|
+
|
|
199
|
+
# =============================================================================
|
|
200
|
+
# Error Handling Types
|
|
201
|
+
# =============================================================================
|
|
202
|
+
|
|
203
|
+
# Error context types
|
|
204
|
+
ErrorContext = Dict[str, Any]
|
|
205
|
+
OriginalError = Optional[Exception]
|
|
206
|
+
ErrorMessage = str
|
|
207
|
+
|
|
208
|
+
# =============================================================================
|
|
209
|
+
# Async Types
|
|
210
|
+
# =============================================================================
|
|
211
|
+
|
|
212
|
+
# Async function types
|
|
213
|
+
AsyncFunc = Callable[..., Awaitable[Any]]
|
|
214
|
+
AsyncToolFunc = Callable[..., Awaitable[str]]
|
|
215
|
+
AsyncVoidFunc = Callable[..., Awaitable[None]]
|
|
216
|
+
|
|
217
|
+
# =============================================================================
|
|
218
|
+
# Diff and Update Types
|
|
219
|
+
# =============================================================================
|
|
220
|
+
|
|
221
|
+
# Types for file updates and diffs
|
|
222
|
+
UpdateOperation = Dict[str, Any]
|
|
223
|
+
DiffLine = str
|
|
224
|
+
DiffHunk = List[DiffLine]
|
|
225
|
+
|
|
226
|
+
# =============================================================================
|
|
227
|
+
# Validation Types
|
|
228
|
+
# =============================================================================
|
|
229
|
+
|
|
230
|
+
# Input validation types
|
|
231
|
+
ValidationResult = Union[bool, str] # True for valid, error message for invalid
|
|
232
|
+
Validator = Callable[[Any], ValidationResult]
|
|
233
|
+
|
|
234
|
+
# =============================================================================
|
|
235
|
+
# Cost Tracking Types
|
|
236
|
+
# =============================================================================
|
|
237
|
+
|
|
238
|
+
# Cost calculation types
|
|
239
|
+
TokenCount = int
|
|
240
|
+
CostAmount = float
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@dataclass
|
|
244
|
+
class TokenUsage:
|
|
245
|
+
"""Token usage for a request."""
|
|
246
|
+
|
|
247
|
+
input_tokens: int
|
|
248
|
+
cached_tokens: int
|
|
249
|
+
output_tokens: int
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
@dataclass
|
|
253
|
+
class CostBreakdown:
|
|
254
|
+
"""Breakdown of costs for a request."""
|
|
255
|
+
|
|
256
|
+
input_cost: float
|
|
257
|
+
cached_cost: float
|
|
258
|
+
output_cost: float
|
|
259
|
+
total_cost: float
|
tunacode/ui/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# UI package
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Completers for file references and commands."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Iterable, Optional
|
|
5
|
+
|
|
6
|
+
from prompt_toolkit.completion import CompleteEvent, Completer, Completion, merge_completers
|
|
7
|
+
from prompt_toolkit.document import Document
|
|
8
|
+
|
|
9
|
+
from ..cli.commands import CommandRegistry
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CommandCompleter(Completer):
|
|
13
|
+
"""Completer for slash commands."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, command_registry: Optional[CommandRegistry] = None):
|
|
16
|
+
self.command_registry = command_registry
|
|
17
|
+
|
|
18
|
+
def get_completions(
|
|
19
|
+
self, document: Document, complete_event: CompleteEvent
|
|
20
|
+
) -> Iterable[Completion]:
|
|
21
|
+
"""Get completions for slash commands."""
|
|
22
|
+
# Get the text before cursor
|
|
23
|
+
text = document.text_before_cursor
|
|
24
|
+
|
|
25
|
+
# Check if we're at the start of a line or after whitespace
|
|
26
|
+
if text and not text.isspace() and text[-1] != '\n':
|
|
27
|
+
# Only complete commands at the start of input or after a newline
|
|
28
|
+
last_newline = text.rfind('\n')
|
|
29
|
+
line_start = text[last_newline + 1:] if last_newline >= 0 else text
|
|
30
|
+
|
|
31
|
+
# Skip if not at the beginning of a line
|
|
32
|
+
if line_start and not line_start.startswith('/'):
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
# Get the word before cursor
|
|
36
|
+
word_before_cursor = document.get_word_before_cursor(WORD=True)
|
|
37
|
+
|
|
38
|
+
# Only complete if word starts with /
|
|
39
|
+
if not word_before_cursor.startswith('/'):
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
# Get command names from registry
|
|
43
|
+
if self.command_registry:
|
|
44
|
+
command_names = self.command_registry.get_command_names()
|
|
45
|
+
else:
|
|
46
|
+
# Fallback list of commands
|
|
47
|
+
command_names = ['/help', '/clear', '/dump', '/yolo', '/undo',
|
|
48
|
+
'/branch', '/compact', '/model', '/init']
|
|
49
|
+
|
|
50
|
+
# Get the partial command (without /)
|
|
51
|
+
partial = word_before_cursor[1:].lower()
|
|
52
|
+
|
|
53
|
+
# Yield completions for matching commands
|
|
54
|
+
for cmd in command_names:
|
|
55
|
+
if cmd.startswith('/') and cmd[1:].lower().startswith(partial):
|
|
56
|
+
yield Completion(
|
|
57
|
+
text=cmd,
|
|
58
|
+
start_position=-len(word_before_cursor),
|
|
59
|
+
display=cmd,
|
|
60
|
+
display_meta='command'
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class FileReferenceCompleter(Completer):
|
|
65
|
+
"""Completer for @file references that provides file path suggestions."""
|
|
66
|
+
|
|
67
|
+
def get_completions(
|
|
68
|
+
self, document: Document, complete_event: CompleteEvent
|
|
69
|
+
) -> Iterable[Completion]:
|
|
70
|
+
"""Get completions for @file references."""
|
|
71
|
+
# Get the word before cursor
|
|
72
|
+
word_before_cursor = document.get_word_before_cursor(WORD=True)
|
|
73
|
+
|
|
74
|
+
# Check if we're in an @file reference
|
|
75
|
+
if not word_before_cursor.startswith("@"):
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
# Get the path part after @
|
|
79
|
+
path_part = word_before_cursor[1:] # Remove @
|
|
80
|
+
|
|
81
|
+
# Determine directory and prefix
|
|
82
|
+
if "/" in path_part:
|
|
83
|
+
# Path includes directory
|
|
84
|
+
dir_path = os.path.dirname(path_part)
|
|
85
|
+
prefix = os.path.basename(path_part)
|
|
86
|
+
else:
|
|
87
|
+
# Just filename, search in current directory
|
|
88
|
+
dir_path = "."
|
|
89
|
+
prefix = path_part
|
|
90
|
+
|
|
91
|
+
# Get matching files
|
|
92
|
+
try:
|
|
93
|
+
if os.path.exists(dir_path) and os.path.isdir(dir_path):
|
|
94
|
+
for item in sorted(os.listdir(dir_path)):
|
|
95
|
+
if item.startswith(prefix):
|
|
96
|
+
full_path = os.path.join(dir_path, item) if dir_path != "." else item
|
|
97
|
+
|
|
98
|
+
# Skip hidden files unless explicitly requested
|
|
99
|
+
if item.startswith(".") and not prefix.startswith("."):
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
# Add / for directories
|
|
103
|
+
if os.path.isdir(full_path):
|
|
104
|
+
display = item + "/"
|
|
105
|
+
completion = full_path + "/"
|
|
106
|
+
else:
|
|
107
|
+
display = item
|
|
108
|
+
completion = full_path
|
|
109
|
+
|
|
110
|
+
# Calculate how much to replace
|
|
111
|
+
start_position = -len(path_part)
|
|
112
|
+
|
|
113
|
+
yield Completion(
|
|
114
|
+
text=completion,
|
|
115
|
+
start_position=start_position,
|
|
116
|
+
display=display,
|
|
117
|
+
display_meta="dir" if os.path.isdir(full_path) else "file"
|
|
118
|
+
)
|
|
119
|
+
except (OSError, PermissionError):
|
|
120
|
+
# Silently ignore inaccessible directories
|
|
121
|
+
pass
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def create_completer(command_registry: Optional[CommandRegistry] = None) -> Completer:
|
|
125
|
+
"""Create a merged completer for both commands and file references."""
|
|
126
|
+
return merge_completers([
|
|
127
|
+
CommandCompleter(command_registry),
|
|
128
|
+
FileReferenceCompleter(),
|
|
129
|
+
])
|