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,117 @@
|
|
|
1
|
+
"""Prompt configuration and management for Sidekick UI."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from prompt_toolkit.completion import Completer
|
|
7
|
+
from prompt_toolkit.formatted_text import FormattedText
|
|
8
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
9
|
+
from prompt_toolkit.lexers import Lexer
|
|
10
|
+
from prompt_toolkit.shortcuts import PromptSession
|
|
11
|
+
from prompt_toolkit.styles import Style
|
|
12
|
+
from prompt_toolkit.validation import Validator
|
|
13
|
+
|
|
14
|
+
from tunacode.constants import UI_COLORS
|
|
15
|
+
from tunacode.core.state import StateManager
|
|
16
|
+
from tunacode.exceptions import UserAbortError
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class PromptConfig:
|
|
21
|
+
"""Configuration for prompt sessions."""
|
|
22
|
+
|
|
23
|
+
multiline: bool = False
|
|
24
|
+
is_password: bool = False
|
|
25
|
+
validator: Optional[Validator] = None
|
|
26
|
+
key_bindings: Optional[KeyBindings] = None
|
|
27
|
+
placeholder: Optional[FormattedText] = None
|
|
28
|
+
completer: Optional[Completer] = None
|
|
29
|
+
lexer: Optional[Lexer] = None
|
|
30
|
+
timeoutlen: float = 0.05
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class PromptManager:
|
|
34
|
+
"""Manages prompt sessions and their lifecycle."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, state_manager: Optional[StateManager] = None):
|
|
37
|
+
"""Initialize the prompt manager.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
state_manager: Optional state manager for session persistence
|
|
41
|
+
"""
|
|
42
|
+
self.state_manager = state_manager
|
|
43
|
+
self._temp_sessions = {} # For when no state manager is available
|
|
44
|
+
self._style = self._create_style()
|
|
45
|
+
|
|
46
|
+
def _create_style(self) -> Style:
|
|
47
|
+
"""Create the style for the prompt with file reference highlighting."""
|
|
48
|
+
return Style.from_dict({
|
|
49
|
+
'file-reference': UI_COLORS.get('file_ref', 'light_blue'),
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
def get_session(self, session_key: str, config: PromptConfig) -> PromptSession:
|
|
53
|
+
"""Get or create a prompt session.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
session_key: Unique key for the session
|
|
57
|
+
config: Configuration for the session
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
PromptSession instance
|
|
61
|
+
"""
|
|
62
|
+
if self.state_manager:
|
|
63
|
+
# Use state manager's session storage
|
|
64
|
+
if session_key not in self.state_manager.session.input_sessions:
|
|
65
|
+
self.state_manager.session.input_sessions[session_key] = PromptSession(
|
|
66
|
+
key_bindings=config.key_bindings,
|
|
67
|
+
placeholder=config.placeholder,
|
|
68
|
+
completer=config.completer,
|
|
69
|
+
lexer=config.lexer,
|
|
70
|
+
style=self._style,
|
|
71
|
+
)
|
|
72
|
+
return self.state_manager.session.input_sessions[session_key]
|
|
73
|
+
else:
|
|
74
|
+
# Use temporary storage
|
|
75
|
+
if session_key not in self._temp_sessions:
|
|
76
|
+
self._temp_sessions[session_key] = PromptSession(
|
|
77
|
+
key_bindings=config.key_bindings,
|
|
78
|
+
placeholder=config.placeholder,
|
|
79
|
+
completer=config.completer,
|
|
80
|
+
lexer=config.lexer,
|
|
81
|
+
style=self._style,
|
|
82
|
+
)
|
|
83
|
+
return self._temp_sessions[session_key]
|
|
84
|
+
|
|
85
|
+
async def get_input(self, session_key: str, prompt: str, config: PromptConfig) -> str:
|
|
86
|
+
"""Get user input using the specified configuration.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
session_key: Unique key for the session
|
|
90
|
+
prompt: The prompt text to display
|
|
91
|
+
config: Configuration for the input
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
User input string
|
|
95
|
+
|
|
96
|
+
Raises:
|
|
97
|
+
UserAbortError: If user cancels input
|
|
98
|
+
"""
|
|
99
|
+
session = self.get_session(session_key, config)
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
# Get user input
|
|
103
|
+
response = await session.prompt_async(
|
|
104
|
+
prompt,
|
|
105
|
+
is_password=config.is_password,
|
|
106
|
+
validator=config.validator,
|
|
107
|
+
multiline=config.multiline,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Clean up response
|
|
111
|
+
if isinstance(response, str):
|
|
112
|
+
response = response.strip()
|
|
113
|
+
|
|
114
|
+
return response
|
|
115
|
+
|
|
116
|
+
except (KeyboardInterrupt, EOFError):
|
|
117
|
+
raise UserAbortError
|
tunacode/ui/tool_ui.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tool confirmation UI components, separated from business logic.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from rich.markdown import Markdown
|
|
6
|
+
from rich.padding import Padding
|
|
7
|
+
from rich.panel import Panel
|
|
8
|
+
|
|
9
|
+
from tunacode.configuration.settings import ApplicationSettings
|
|
10
|
+
from tunacode.constants import APP_NAME, TOOL_UPDATE_FILE, TOOL_WRITE_FILE, UI_COLORS
|
|
11
|
+
from tunacode.core.tool_handler import ToolConfirmationRequest, ToolConfirmationResponse
|
|
12
|
+
from tunacode.types import ToolArgs
|
|
13
|
+
from tunacode.ui import console as ui
|
|
14
|
+
from tunacode.utils.diff_utils import render_file_diff
|
|
15
|
+
from tunacode.utils.file_utils import DotDict
|
|
16
|
+
from tunacode.utils.text_utils import ext_to_lang, key_to_title
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ToolUI:
|
|
20
|
+
"""Handles tool confirmation UI presentation."""
|
|
21
|
+
|
|
22
|
+
def __init__(self):
|
|
23
|
+
self.colors = DotDict(UI_COLORS)
|
|
24
|
+
|
|
25
|
+
def _get_tool_title(self, tool_name: str) -> str:
|
|
26
|
+
"""
|
|
27
|
+
Get the display title for a tool.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
tool_name: Name of the tool.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
str: Display title.
|
|
34
|
+
"""
|
|
35
|
+
app_settings = ApplicationSettings()
|
|
36
|
+
if tool_name in app_settings.internal_tools:
|
|
37
|
+
return f"Tool({tool_name})"
|
|
38
|
+
else:
|
|
39
|
+
return f"MCP({tool_name})"
|
|
40
|
+
|
|
41
|
+
def _create_code_block(self, filepath: str, content: str) -> Markdown:
|
|
42
|
+
"""
|
|
43
|
+
Create a code block for the given file path and content.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
filepath: The path to the file.
|
|
47
|
+
content: The content of the file.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Markdown: A Markdown object representing the code block.
|
|
51
|
+
"""
|
|
52
|
+
lang = ext_to_lang(filepath)
|
|
53
|
+
code_block = f"```{lang}\n{content}\n```"
|
|
54
|
+
return ui.markdown(code_block)
|
|
55
|
+
|
|
56
|
+
def _render_args(self, tool_name: str, args: ToolArgs) -> str:
|
|
57
|
+
"""
|
|
58
|
+
Render the tool arguments for display.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
tool_name: Name of the tool.
|
|
62
|
+
args: Tool arguments.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
str: Formatted arguments for display.
|
|
66
|
+
"""
|
|
67
|
+
# Show diff between `target` and `patch` on file updates
|
|
68
|
+
if tool_name == TOOL_UPDATE_FILE:
|
|
69
|
+
return render_file_diff(args["target"], args["patch"], self.colors)
|
|
70
|
+
|
|
71
|
+
# Show file content on write_file
|
|
72
|
+
elif tool_name == TOOL_WRITE_FILE:
|
|
73
|
+
return self._create_code_block(args["filepath"], args["content"])
|
|
74
|
+
|
|
75
|
+
# Default to showing key and value on new line
|
|
76
|
+
content = ""
|
|
77
|
+
for key, value in args.items():
|
|
78
|
+
if isinstance(value, list):
|
|
79
|
+
content += f"{key_to_title(key)}:\n"
|
|
80
|
+
for item in value:
|
|
81
|
+
content += f" - {item}\n"
|
|
82
|
+
content += "\n"
|
|
83
|
+
else:
|
|
84
|
+
# If string length is over 200 characters, split to new line
|
|
85
|
+
value = str(value)
|
|
86
|
+
content += f"{key_to_title(key)}:"
|
|
87
|
+
if len(value) > 200:
|
|
88
|
+
content += f"\n{value}\n\n"
|
|
89
|
+
else:
|
|
90
|
+
content += f" {value}\n\n"
|
|
91
|
+
return content.strip()
|
|
92
|
+
|
|
93
|
+
async def show_confirmation(
|
|
94
|
+
self, request: ToolConfirmationRequest, state_manager=None
|
|
95
|
+
) -> ToolConfirmationResponse:
|
|
96
|
+
"""
|
|
97
|
+
Show tool confirmation UI and get user response.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
request: The confirmation request.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
ToolConfirmationResponse: User's response to the confirmation.
|
|
104
|
+
"""
|
|
105
|
+
title = self._get_tool_title(request.tool_name)
|
|
106
|
+
content = self._render_args(request.tool_name, request.args)
|
|
107
|
+
|
|
108
|
+
await ui.tool_confirm(title, content, filepath=request.filepath)
|
|
109
|
+
|
|
110
|
+
# If tool call has filepath, show it under panel
|
|
111
|
+
if request.filepath:
|
|
112
|
+
await ui.usage(f"File: {request.filepath}")
|
|
113
|
+
|
|
114
|
+
await ui.print(" 1. Yes (default)")
|
|
115
|
+
await ui.print(" 2. Yes, and don't ask again for commands like this")
|
|
116
|
+
await ui.print(f" 3. No, and tell {APP_NAME} what to do differently")
|
|
117
|
+
resp = (
|
|
118
|
+
await ui.input(
|
|
119
|
+
session_key="tool_confirm",
|
|
120
|
+
pretext=" Choose an option [1/2/3]: ",
|
|
121
|
+
state_manager=state_manager,
|
|
122
|
+
)
|
|
123
|
+
or "1"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
if resp == "2":
|
|
127
|
+
return ToolConfirmationResponse(approved=True, skip_future=True)
|
|
128
|
+
elif resp == "3":
|
|
129
|
+
return ToolConfirmationResponse(approved=False, abort=True)
|
|
130
|
+
else:
|
|
131
|
+
return ToolConfirmationResponse(approved=True)
|
|
132
|
+
|
|
133
|
+
def show_sync_confirmation(self, request: ToolConfirmationRequest) -> ToolConfirmationResponse:
|
|
134
|
+
"""
|
|
135
|
+
Show tool confirmation UI synchronously and get user response.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
request: The confirmation request.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
ToolConfirmationResponse: User's response to the confirmation.
|
|
142
|
+
"""
|
|
143
|
+
title = self._get_tool_title(request.tool_name)
|
|
144
|
+
content = self._render_args(request.tool_name, request.args)
|
|
145
|
+
|
|
146
|
+
# Display styled confirmation panel using direct console output
|
|
147
|
+
# Avoid using sync wrappers that might create event loop conflicts
|
|
148
|
+
panel_obj = Panel(
|
|
149
|
+
Padding(content, 1), title=title, title_align="left", border_style=self.colors.warning
|
|
150
|
+
)
|
|
151
|
+
# Add consistent spacing above panels
|
|
152
|
+
ui.console.print(Padding(panel_obj, (1, 0, 0, 0)))
|
|
153
|
+
|
|
154
|
+
if request.filepath:
|
|
155
|
+
ui.console.print(f"File: {request.filepath}", style=self.colors.muted)
|
|
156
|
+
|
|
157
|
+
ui.console.print(" 1. Yes (default)")
|
|
158
|
+
ui.console.print(" 2. Yes, and don't ask again for commands like this")
|
|
159
|
+
ui.console.print(f" 3. No, and tell {APP_NAME} what to do differently")
|
|
160
|
+
resp = input(" Choose an option [1/2/3]: ").strip() or "1"
|
|
161
|
+
|
|
162
|
+
# Add spacing after user choice for better readability
|
|
163
|
+
print()
|
|
164
|
+
|
|
165
|
+
if resp == "2":
|
|
166
|
+
return ToolConfirmationResponse(approved=True, skip_future=True)
|
|
167
|
+
elif resp == "3":
|
|
168
|
+
return ToolConfirmationResponse(approved=False, abort=True)
|
|
169
|
+
else:
|
|
170
|
+
return ToolConfirmationResponse(approved=True)
|
|
171
|
+
|
|
172
|
+
async def log_mcp(self, title: str, args: ToolArgs) -> None:
|
|
173
|
+
"""
|
|
174
|
+
Display MCP tool with its arguments.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
title: Title to display.
|
|
178
|
+
args: Arguments to display.
|
|
179
|
+
"""
|
|
180
|
+
if not args:
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
await ui.info(title)
|
|
184
|
+
for key, value in args.items():
|
|
185
|
+
if isinstance(value, list):
|
|
186
|
+
value = ", ".join(value)
|
|
187
|
+
await ui.muted(f"{key}: {value}", spaces=4)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Input validators for Sidekick UI."""
|
|
2
|
+
|
|
3
|
+
from prompt_toolkit.validation import ValidationError, Validator
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ModelValidator(Validator):
|
|
7
|
+
"""Validate default provider selection"""
|
|
8
|
+
|
|
9
|
+
def __init__(self, index):
|
|
10
|
+
self.index = index
|
|
11
|
+
|
|
12
|
+
def validate(self, document) -> None:
|
|
13
|
+
text = document.text.strip()
|
|
14
|
+
if not text:
|
|
15
|
+
raise ValidationError(message="Provider number cannot be empty")
|
|
16
|
+
elif text and not text.isdigit():
|
|
17
|
+
raise ValidationError(message="Invalid provider number")
|
|
18
|
+
elif text.isdigit():
|
|
19
|
+
number = int(text)
|
|
20
|
+
if number < 0 or number >= self.index:
|
|
21
|
+
raise ValidationError(
|
|
22
|
+
message="Invalid provider number",
|
|
23
|
+
)
|
|
File without changes
|
tunacode/utils/bm25.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import math
|
|
2
|
+
import re
|
|
3
|
+
from collections import Counter
|
|
4
|
+
from typing import Iterable, List
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def tokenize(text: str) -> List[str]:
|
|
8
|
+
"""Simple whitespace and punctuation tokenizer."""
|
|
9
|
+
return re.findall(r"\w+", text.lower())
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BM25:
|
|
13
|
+
"""Minimal BM25 implementation for small corpora."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, corpus: Iterable[str], k1: float = 1.5, b: float = 0.75):
|
|
16
|
+
self.k1 = k1
|
|
17
|
+
self.b = b
|
|
18
|
+
self.documents = [tokenize(doc) for doc in corpus]
|
|
19
|
+
self.doc_freqs = []
|
|
20
|
+
self.doc_lens = []
|
|
21
|
+
self.idf = {}
|
|
22
|
+
self.avgdl = 0.0
|
|
23
|
+
self._initialize()
|
|
24
|
+
|
|
25
|
+
def _initialize(self) -> None:
|
|
26
|
+
df = Counter()
|
|
27
|
+
for doc in self.documents:
|
|
28
|
+
freqs = Counter(doc)
|
|
29
|
+
self.doc_freqs.append(freqs)
|
|
30
|
+
self.doc_lens.append(len(doc))
|
|
31
|
+
for word in freqs:
|
|
32
|
+
df[word] += 1
|
|
33
|
+
self.avgdl = sum(self.doc_lens) / len(self.documents) if self.documents else 0.0
|
|
34
|
+
total_docs = len(self.documents)
|
|
35
|
+
for word, freq in df.items():
|
|
36
|
+
self.idf[word] = math.log(1 + (total_docs - freq + 0.5) / (freq + 0.5))
|
|
37
|
+
|
|
38
|
+
def get_scores(self, query: Iterable[str]) -> List[float]:
|
|
39
|
+
"""Calculate BM25 scores for a query."""
|
|
40
|
+
scores = [0.0] * len(self.documents)
|
|
41
|
+
query_terms = list(query)
|
|
42
|
+
for idx, doc in enumerate(self.documents):
|
|
43
|
+
freqs = self.doc_freqs[idx]
|
|
44
|
+
doc_len = self.doc_lens[idx]
|
|
45
|
+
score = 0.0
|
|
46
|
+
for term in query_terms:
|
|
47
|
+
if term not in freqs:
|
|
48
|
+
continue
|
|
49
|
+
idf = self.idf.get(term, 0.0)
|
|
50
|
+
tf = freqs[term]
|
|
51
|
+
numerator = tf * (self.k1 + 1)
|
|
52
|
+
denominator = tf + self.k1 * (1 - self.b + self.b * doc_len / self.avgdl)
|
|
53
|
+
score += idf * numerator / denominator
|
|
54
|
+
scores[idx] = score
|
|
55
|
+
return scores
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module: sidekick.utils.diff_utils
|
|
3
|
+
|
|
4
|
+
Provides diff visualization utilities for file changes.
|
|
5
|
+
Generates styled text diffs between original and modified content using the difflib library.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import difflib
|
|
9
|
+
|
|
10
|
+
from rich.text import Text
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def render_file_diff(target: str, patch: str, colors=None) -> Text:
|
|
14
|
+
"""
|
|
15
|
+
Create a formatted diff between target and patch text.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
target (str): The original text to be replaced.
|
|
19
|
+
patch (str): The new text to insert.
|
|
20
|
+
colors (dict, optional): Dictionary containing style colors.
|
|
21
|
+
If None, no styling will be applied.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
Text: A Rich Text object containing the formatted diff.
|
|
25
|
+
"""
|
|
26
|
+
# Create a clean diff with styled text
|
|
27
|
+
diff_text = Text()
|
|
28
|
+
|
|
29
|
+
# Get lines and create a diff sequence
|
|
30
|
+
target_lines = target.splitlines()
|
|
31
|
+
patch_lines = patch.splitlines()
|
|
32
|
+
|
|
33
|
+
# Use difflib to identify changes
|
|
34
|
+
matcher = difflib.SequenceMatcher(None, target_lines, patch_lines)
|
|
35
|
+
|
|
36
|
+
for op, i1, i2, j1, j2 in matcher.get_opcodes():
|
|
37
|
+
if op == "equal":
|
|
38
|
+
# Unchanged lines
|
|
39
|
+
for line in target_lines[i1:i2]:
|
|
40
|
+
diff_text.append(f" {line}\n")
|
|
41
|
+
elif op == "delete":
|
|
42
|
+
# Removed lines - show in red with (-) prefix
|
|
43
|
+
for line in target_lines[i1:i2]:
|
|
44
|
+
if colors:
|
|
45
|
+
diff_text.append(f"- {line}\n", style=colors.error)
|
|
46
|
+
else:
|
|
47
|
+
diff_text.append(f"- {line}\n")
|
|
48
|
+
elif op == "insert":
|
|
49
|
+
# Added lines - show in green with (+) prefix
|
|
50
|
+
for line in patch_lines[j1:j2]:
|
|
51
|
+
if colors:
|
|
52
|
+
diff_text.append(f"+ {line}\n", style=colors.success)
|
|
53
|
+
else:
|
|
54
|
+
diff_text.append(f"+ {line}\n")
|
|
55
|
+
elif op == "replace":
|
|
56
|
+
# Removed lines with (-) prefix
|
|
57
|
+
for line in target_lines[i1:i2]:
|
|
58
|
+
if colors:
|
|
59
|
+
diff_text.append(f"- {line}\n", style=colors.error)
|
|
60
|
+
else:
|
|
61
|
+
diff_text.append(f"- {line}\n")
|
|
62
|
+
# Added lines with (+) prefix
|
|
63
|
+
for line in patch_lines[j1:j2]:
|
|
64
|
+
if colors:
|
|
65
|
+
diff_text.append(f"+ {line}\n", style=colors.success)
|
|
66
|
+
else:
|
|
67
|
+
diff_text.append(f"+ {line}\n")
|
|
68
|
+
|
|
69
|
+
return diff_text
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module: sidekick.utils.file_utils
|
|
3
|
+
|
|
4
|
+
Provides file system utilities and helper classes.
|
|
5
|
+
Includes DotDict for dot notation access and stdout capture functionality.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import io
|
|
9
|
+
import sys
|
|
10
|
+
from contextlib import contextmanager
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DotDict(dict):
|
|
14
|
+
"""dot.notation access to dictionary attributes"""
|
|
15
|
+
|
|
16
|
+
__getattr__ = dict.get
|
|
17
|
+
__setattr__ = dict.__setitem__
|
|
18
|
+
__delattr__ = dict.__delitem__
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@contextmanager
|
|
22
|
+
def capture_stdout():
|
|
23
|
+
"""
|
|
24
|
+
Context manager to capture stdout output.
|
|
25
|
+
|
|
26
|
+
Example:
|
|
27
|
+
with capture_stdout() as stdout_capture:
|
|
28
|
+
print("This will be captured")
|
|
29
|
+
|
|
30
|
+
captured_output = stdout_capture.getvalue()
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
StringIO object containing the captured output
|
|
34
|
+
"""
|
|
35
|
+
stdout_capture = io.StringIO()
|
|
36
|
+
original_stdout = sys.stdout
|
|
37
|
+
sys.stdout = stdout_capture
|
|
38
|
+
try:
|
|
39
|
+
yield stdout_capture
|
|
40
|
+
finally:
|
|
41
|
+
sys.stdout = original_stdout
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
from typing import List
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def ripgrep(pattern: str, directory: str = ".") -> List[str]:
|
|
6
|
+
"""Return a list of file paths matching a pattern using ripgrep."""
|
|
7
|
+
try:
|
|
8
|
+
result = subprocess.run(
|
|
9
|
+
["rg", "--files", "-g", pattern, directory],
|
|
10
|
+
capture_output=True,
|
|
11
|
+
text=True,
|
|
12
|
+
check=True,
|
|
13
|
+
timeout=5,
|
|
14
|
+
)
|
|
15
|
+
return [line.strip() for line in result.stdout.splitlines() if line.strip()]
|
|
16
|
+
except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired):
|
|
17
|
+
return []
|