janito 0.5.0__py3-none-any.whl → 0.7.0__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.
- janito/__init__.py +0 -47
- janito/__main__.py +105 -17
- janito/agents/__init__.py +9 -9
- janito/agents/agent.py +10 -3
- janito/agents/claudeai.py +15 -34
- janito/agents/openai.py +5 -1
- janito/change/__init__.py +29 -16
- janito/change/__main__.py +0 -0
- janito/{analysis → change/analysis}/__init__.py +5 -15
- janito/change/analysis/__main__.py +7 -0
- janito/change/analysis/analyze.py +62 -0
- janito/change/analysis/formatting.py +78 -0
- janito/change/analysis/options.py +81 -0
- janito/{analysis → change/analysis}/prompts.py +33 -18
- janito/change/analysis/view/__init__.py +9 -0
- janito/change/analysis/view/terminal.py +181 -0
- janito/change/applier/__init__.py +5 -0
- janito/change/applier/file.py +58 -0
- janito/change/applier/main.py +156 -0
- janito/change/applier/text.py +247 -0
- janito/change/applier/workspace_dir.py +58 -0
- janito/change/core.py +124 -0
- janito/{changehistory.py → change/history.py} +12 -14
- janito/change/operations.py +7 -0
- janito/change/parser.py +287 -0
- janito/change/play.py +54 -0
- janito/change/preview.py +82 -0
- janito/change/prompts.py +121 -0
- janito/change/test.py +0 -0
- janito/change/validator.py +269 -0
- janito/{changeviewer → change/viewer}/__init__.py +3 -4
- janito/change/viewer/content.py +66 -0
- janito/{changeviewer → change/viewer}/diff.py +19 -4
- janito/change/viewer/panels.py +533 -0
- janito/change/viewer/styling.py +114 -0
- janito/{changeviewer → change/viewer}/themes.py +3 -5
- janito/clear_statement_parser/clear_statement_format.txt +328 -0
- janito/clear_statement_parser/examples.txt +326 -0
- janito/clear_statement_parser/models.py +104 -0
- janito/clear_statement_parser/parser.py +496 -0
- janito/cli/base.py +30 -0
- janito/cli/commands.py +75 -40
- janito/cli/functions.py +19 -194
- janito/cli/history.py +61 -0
- janito/common.py +65 -8
- janito/config.py +70 -5
- janito/demo/__init__.py +4 -0
- janito/demo/data.py +13 -0
- janito/demo/mock_data.py +20 -0
- janito/demo/operations.py +45 -0
- janito/demo/runner.py +59 -0
- janito/demo/scenarios.py +32 -0
- janito/prompt.py +36 -0
- janito/qa.py +6 -14
- janito/search_replace/README.md +192 -0
- janito/search_replace/__init__.py +7 -0
- janito/search_replace/__main__.py +21 -0
- janito/search_replace/core.py +120 -0
- janito/search_replace/logger.py +35 -0
- janito/search_replace/parser.py +52 -0
- janito/search_replace/play.py +61 -0
- janito/search_replace/replacer.py +36 -0
- janito/search_replace/searcher.py +411 -0
- janito/search_replace/strategy_result.py +10 -0
- janito/shell/__init__.py +38 -0
- janito/shell/bus.py +31 -0
- janito/shell/commands.py +136 -0
- janito/shell/history.py +20 -0
- janito/shell/processor.py +32 -0
- janito/shell/prompt.py +48 -0
- janito/shell/registry.py +60 -0
- janito/tui/__init__.py +21 -0
- janito/tui/base.py +22 -0
- janito/tui/flows/__init__.py +5 -0
- janito/tui/flows/changes.py +65 -0
- janito/tui/flows/content.py +128 -0
- janito/tui/flows/selection.py +117 -0
- janito/tui/screens/__init__.py +3 -0
- janito/tui/screens/app.py +1 -0
- janito/workspace/__init__.py +6 -0
- janito/workspace/analysis.py +121 -0
- janito/workspace/show.py +141 -0
- janito/workspace/stats.py +43 -0
- janito/workspace/types.py +98 -0
- janito/workspace/workset.py +108 -0
- janito/workspace/workspace.py +114 -0
- janito-0.7.0.dist-info/METADATA +167 -0
- janito-0.7.0.dist-info/RECORD +96 -0
- {janito-0.5.0.dist-info → janito-0.7.0.dist-info}/WHEEL +1 -1
- janito/_contextparser.py +0 -113
- janito/analysis/display.py +0 -149
- janito/analysis/options.py +0 -112
- janito/change/applier.py +0 -269
- janito/change/content.py +0 -62
- janito/change/indentation.py +0 -33
- janito/change/position.py +0 -169
- janito/changeviewer/panels.py +0 -268
- janito/changeviewer/styling.py +0 -59
- janito/console/__init__.py +0 -3
- janito/console/commands.py +0 -112
- janito/console/core.py +0 -62
- janito/console/display.py +0 -157
- janito/fileparser.py +0 -334
- janito/prompts.py +0 -81
- janito/scan.py +0 -176
- janito/tests/test_fileparser.py +0 -26
- janito-0.5.0.dist-info/METADATA +0 -146
- janito-0.5.0.dist-info/RECORD +0 -45
- {janito-0.5.0.dist-info → janito-0.7.0.dist-info}/entry_points.txt +0 -0
- {janito-0.5.0.dist-info → janito-0.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,247 @@
|
|
1
|
+
from typing import Tuple, List, Optional
|
2
|
+
from rich.console import Console
|
3
|
+
from pathlib import Path
|
4
|
+
from datetime import datetime
|
5
|
+
from ..parser import TextChange
|
6
|
+
from janito.config import config
|
7
|
+
from ...clear_statement_parser.parser import StatementParser
|
8
|
+
from ...search_replace import SearchReplacer, PatternNotFoundException, Searcher
|
9
|
+
|
10
|
+
class TextFindDebugger:
|
11
|
+
def __init__(self, console: Console):
|
12
|
+
self.console = console
|
13
|
+
self.find_count = 0
|
14
|
+
|
15
|
+
def _visualize_whitespace(self, text: str) -> str:
|
16
|
+
"""Convert whitespace characters to visible markers"""
|
17
|
+
return text.replace(' ', '·').replace('\t', '→')
|
18
|
+
|
19
|
+
def debug_find(self, content: str, search: str) -> List[int]:
|
20
|
+
"""Debug find operation by showing numbered matches"""
|
21
|
+
self.find_count += 1
|
22
|
+
matches = []
|
23
|
+
|
24
|
+
# Show search pattern
|
25
|
+
self.console.print(f"\n[cyan]Find #{self.find_count} search pattern:[/cyan]")
|
26
|
+
for i, line in enumerate(search.splitlines()):
|
27
|
+
self.console.print(f"[dim]{i+1:3d} | {self._visualize_whitespace(line)}[/dim]")
|
28
|
+
|
29
|
+
# Process content line by line
|
30
|
+
lines = content.splitlines()
|
31
|
+
for i, line in enumerate(lines):
|
32
|
+
if search.strip() in line.strip():
|
33
|
+
matches.append(i + 1)
|
34
|
+
self.console.print(f"[green]Match at line {i+1}:[/green] {self._visualize_whitespace(line)}")
|
35
|
+
|
36
|
+
if not matches:
|
37
|
+
self.console.print("[yellow]No matches found[/yellow]")
|
38
|
+
|
39
|
+
return matches
|
40
|
+
|
41
|
+
class TextChangeApplier:
|
42
|
+
def __init__(self, console: Optional[Console] = None):
|
43
|
+
self.console = console or Console()
|
44
|
+
self.debugger = TextFindDebugger(self.console)
|
45
|
+
self.parser = StatementParser()
|
46
|
+
self.searcher = Searcher()
|
47
|
+
|
48
|
+
def _get_last_line_indent(self, content: str) -> str:
|
49
|
+
"""Extract indentation from the last non-empty line."""
|
50
|
+
lines = content.splitlines()
|
51
|
+
for line in reversed(lines):
|
52
|
+
if line.strip():
|
53
|
+
return self.searcher.get_indentation(line)
|
54
|
+
return ""
|
55
|
+
|
56
|
+
def _validate_operation(self, mod: TextChange) -> Tuple[bool, Optional[str]]:
|
57
|
+
"""Validate text operation type and parameters
|
58
|
+
Returns (is_valid, error_message)"""
|
59
|
+
if mod.is_append:
|
60
|
+
if not mod.replace_content:
|
61
|
+
return False, "Append operation requires content"
|
62
|
+
return True, None
|
63
|
+
|
64
|
+
# For delete operations
|
65
|
+
if mod.is_delete:
|
66
|
+
if not mod.search_content:
|
67
|
+
return False, "Delete operation requires search content"
|
68
|
+
return True, None
|
69
|
+
|
70
|
+
# For replace operations
|
71
|
+
if not mod.search_content:
|
72
|
+
return False, "Replace operation requires search content"
|
73
|
+
if mod.replace_content is None:
|
74
|
+
return False, "Replace operation requires replacement content"
|
75
|
+
|
76
|
+
return True, None
|
77
|
+
|
78
|
+
def apply_modifications(self, content: str, changes: List[TextChange], target_path: Path, debug: bool) -> Tuple[bool, str, Optional[str]]:
|
79
|
+
"""Apply text modifications to content"""
|
80
|
+
modified = content
|
81
|
+
any_changes = False
|
82
|
+
target_path = target_path.resolve()
|
83
|
+
file_ext = target_path.suffix # Get file extension including the dot
|
84
|
+
|
85
|
+
for mod in changes:
|
86
|
+
# Validate operation
|
87
|
+
is_valid, error = self._validate_operation(mod)
|
88
|
+
if not is_valid:
|
89
|
+
self.console.print(f"[yellow]Warning: Invalid text operation for {target_path}: {error}[/yellow]")
|
90
|
+
continue
|
91
|
+
|
92
|
+
try:
|
93
|
+
# Handle append operations
|
94
|
+
if not mod.search_content:
|
95
|
+
if mod.replace_content:
|
96
|
+
modified = self._append_content(modified, mod.replace_content)
|
97
|
+
any_changes = True
|
98
|
+
continue
|
99
|
+
|
100
|
+
# Handle delete operations (either explicit or via empty replacement)
|
101
|
+
if mod.is_delete or (mod.replace_content == "" and mod.search_content):
|
102
|
+
replacer = SearchReplacer(modified, mod.search_content, "", file_ext, debug=debug)
|
103
|
+
|
104
|
+
modified = replacer.replace()
|
105
|
+
any_changes = True
|
106
|
+
continue
|
107
|
+
|
108
|
+
# Handle search and replace
|
109
|
+
if debug:
|
110
|
+
print("************************** before replace")
|
111
|
+
print(modified)
|
112
|
+
print("****************************")
|
113
|
+
replacer = SearchReplacer(modified, mod.search_content, mod.replace_content, file_ext, debug=debug)
|
114
|
+
modified = replacer.replace()
|
115
|
+
if debug:
|
116
|
+
print("************************** after replace")
|
117
|
+
print(modified)
|
118
|
+
print("****************************")
|
119
|
+
any_changes = True
|
120
|
+
|
121
|
+
except PatternNotFoundException:
|
122
|
+
if config.debug:
|
123
|
+
self.debug_failed_finds(mod.search_content, modified, str(target_path))
|
124
|
+
warning_msg = self._handle_failed_search(target_path, mod.search_content, modified)
|
125
|
+
self.console.print(f"[yellow]Warning: {warning_msg}[/yellow]")
|
126
|
+
continue
|
127
|
+
|
128
|
+
return (True, modified, None) if any_changes else (False, content, "No changes were applied")
|
129
|
+
|
130
|
+
def _append_content(self, content: str, new_content: str) -> str:
|
131
|
+
"""Append content with proper indentation matching.
|
132
|
+
|
133
|
+
The indentation rules are:
|
134
|
+
1. If new content starts with empty lines, preserve original indentation
|
135
|
+
2. Otherwise, use indentation from the last non-empty line of existing content as base
|
136
|
+
3. Preserves relative indentation between lines in new content
|
137
|
+
4. Adjusts indentation if new content would go into negative space
|
138
|
+
"""
|
139
|
+
if not content.endswith('\n'):
|
140
|
+
content += '\n'
|
141
|
+
|
142
|
+
# Add empty line if the last line is not empty
|
143
|
+
if content.rstrip('\n').splitlines()[-1].strip():
|
144
|
+
content += '\n'
|
145
|
+
|
146
|
+
# If new content starts with empty lines, preserve original indentation
|
147
|
+
lines = new_content.splitlines()
|
148
|
+
if not lines or not lines[0].strip():
|
149
|
+
return content + new_content
|
150
|
+
|
151
|
+
# Get base indentation from last non-empty line
|
152
|
+
base_indent = self._get_last_line_indent(content)
|
153
|
+
|
154
|
+
# Get the first non-empty line from new content
|
155
|
+
first_line, _ = self.searcher.get_first_non_empty_line(new_content)
|
156
|
+
if first_line:
|
157
|
+
# Get the indentation of the first line of new content
|
158
|
+
new_base_indent = self.searcher.get_indentation(first_line)
|
159
|
+
|
160
|
+
# Calculate how much we need to shift if new content would go into negative space
|
161
|
+
indent_delta = len(base_indent) + (len(new_base_indent) - len(new_base_indent))
|
162
|
+
left_shift = abs(min(0, indent_delta))
|
163
|
+
|
164
|
+
result_lines = []
|
165
|
+
for line in new_content.splitlines():
|
166
|
+
if not line.strip():
|
167
|
+
result_lines.append('')
|
168
|
+
continue
|
169
|
+
|
170
|
+
# Calculate final indentation:
|
171
|
+
# 1. Get current line's indentation
|
172
|
+
line_indent = self.searcher.get_indentation(line)
|
173
|
+
# 2. Calculate relative indent compared to new content's first line
|
174
|
+
rel_indent = len(line_indent) - len(new_base_indent)
|
175
|
+
# 3. Apply base indent + relative indent, adjusting for negative space
|
176
|
+
final_indent_len = max(0, len(line_indent) - left_shift + (len(base_indent) - len(new_base_indent)))
|
177
|
+
final_indent = ' ' * final_indent_len
|
178
|
+
result_lines.append(final_indent + line.lstrip())
|
179
|
+
|
180
|
+
new_content = '\n'.join(result_lines)
|
181
|
+
|
182
|
+
return content + new_content
|
183
|
+
|
184
|
+
def _handle_failed_search(self, filepath: Path, search_text: str, content: str) -> str:
|
185
|
+
"""Handle failed search by logging debug info in a test case format"""
|
186
|
+
failed_file = config.workspace_dir / '.janito' / 'change_history' / f"{datetime.now().strftime('%Y%m%d_%H%M%S')}_changes_failed.txt"
|
187
|
+
failed_file.parent.mkdir(parents=True, exist_ok=True)
|
188
|
+
|
189
|
+
# Create test case format debug info
|
190
|
+
debug_info = f"""Test: Failed search in {filepath.name}
|
191
|
+
========================================
|
192
|
+
Original:
|
193
|
+
{content}
|
194
|
+
========================================
|
195
|
+
Search pattern:
|
196
|
+
{search_text}
|
197
|
+
========================================"""
|
198
|
+
|
199
|
+
failed_file.write_text(debug_info)
|
200
|
+
|
201
|
+
self.console.print(f"[yellow]Changes failed saved to: {failed_file}[/yellow]")
|
202
|
+
self.console.print("[yellow]Run with 'python -m janito.search_replace {failed_file}' to debug[/yellow]")
|
203
|
+
|
204
|
+
return f"Could not apply change to {filepath} - pattern not found"
|
205
|
+
|
206
|
+
def debug_failed_finds(self, search_content: str, file_content: str, filepath: str) -> None:
|
207
|
+
"""Debug find operations without applying changes"""
|
208
|
+
if not search_content or not file_content:
|
209
|
+
self.console.print("[yellow]Missing search or file content for debugging[/yellow]")
|
210
|
+
return
|
211
|
+
|
212
|
+
self.console.print(f"\n[cyan]Debugging finds for {filepath}:[/cyan]")
|
213
|
+
self.debugger.debug_find(file_content, search_content)
|
214
|
+
|
215
|
+
def extract_debug_info(self, content: str) -> tuple[Optional[str], Optional[str], Optional[str]]:
|
216
|
+
"""Extract search text and file content from failed change debug info.
|
217
|
+
|
218
|
+
Only matches section markers ("========================================")
|
219
|
+
when they appear alone on a line.
|
220
|
+
"""
|
221
|
+
try:
|
222
|
+
marker = "=" * 40
|
223
|
+
lines = content.splitlines()
|
224
|
+
section_starts = [i for i, line in enumerate(lines) if line.strip() == marker]
|
225
|
+
|
226
|
+
if len(section_starts) < 3:
|
227
|
+
raise ValueError("Missing section markers in debug file")
|
228
|
+
|
229
|
+
# Extract content between markers
|
230
|
+
original_start = section_starts[0] + 2 # +1 for section header, +1 for marker
|
231
|
+
search_start = section_starts[1] + 2
|
232
|
+
original_content = "\n".join(lines[original_start:section_starts[1]])
|
233
|
+
search_content = "\n".join(lines[search_start:section_starts[2]])
|
234
|
+
|
235
|
+
# Extract filename from first line
|
236
|
+
if not lines[0].startswith("Test: Failed search in "):
|
237
|
+
raise ValueError("Invalid debug file format")
|
238
|
+
filepath = lines[0].replace("Test: Failed search in ", "").strip()
|
239
|
+
|
240
|
+
if not all([filepath, search_content, original_content]):
|
241
|
+
raise ValueError("Missing required sections in debug file")
|
242
|
+
|
243
|
+
return filepath, search_content, original_content
|
244
|
+
|
245
|
+
except Exception as e:
|
246
|
+
self.console.print(f"[red]Error parsing debug info: {e}[/red]")
|
247
|
+
return None, None, None
|
@@ -0,0 +1,58 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
from typing import Set, List
|
3
|
+
import shutil
|
4
|
+
from rich.console import Console
|
5
|
+
from janito.config import config
|
6
|
+
from ..parser import FileChange, ChangeOperation
|
7
|
+
|
8
|
+
def verify_changes(changes: List[FileChange]) -> tuple[bool, str]:
|
9
|
+
"""Verify changes can be safely applied to workspace_dir.
|
10
|
+
Returns (is_safe, error_message)."""
|
11
|
+
for change in changes:
|
12
|
+
source_path = config.workspace_dir / change.name
|
13
|
+
|
14
|
+
if change.operation == ChangeOperation.CREATE_FILE:
|
15
|
+
if source_path.exists():
|
16
|
+
return False, f"Cannot create {change.name} - already exists"
|
17
|
+
|
18
|
+
elif change.operation in (ChangeOperation.MOVE_FILE, ChangeOperation.RENAME_FILE):
|
19
|
+
if not source_path.exists():
|
20
|
+
return False, f"Cannot {change.operation.name.lower()} non-existent file {change.name}"
|
21
|
+
target_path = config.workspace_dir / change.target
|
22
|
+
if target_path.exists():
|
23
|
+
return False, f"Cannot {change.operation.name.lower()} {change.name} to {change.target} - target already exists"
|
24
|
+
|
25
|
+
|
26
|
+
return True, ""
|
27
|
+
|
28
|
+
def apply_changes(changes: List[FileChange], preview_dir: Path, console: Console) -> bool:
|
29
|
+
"""Apply all changes from preview to workspace_dir.
|
30
|
+
Returns success status."""
|
31
|
+
is_safe, error = verify_changes(changes)
|
32
|
+
if not is_safe:
|
33
|
+
console.print(f"[red]Error: {error}[/red]")
|
34
|
+
return False
|
35
|
+
|
36
|
+
console.print("\n[blue]Applying changes to working directory...[/blue]")
|
37
|
+
|
38
|
+
for change in changes:
|
39
|
+
if change.operation == ChangeOperation.REMOVE_FILE:
|
40
|
+
remove_from_workspace_dir(change.name, console)
|
41
|
+
else:
|
42
|
+
filepath = change.target if change.operation == ChangeOperation.RENAME_FILE else change.name
|
43
|
+
target_path = config.workspace_dir / filepath
|
44
|
+
preview_path = preview_dir / filepath
|
45
|
+
|
46
|
+
target_path.parent.mkdir(parents=True, exist_ok=True)
|
47
|
+
if preview_path.exists():
|
48
|
+
shutil.copy2(preview_path, target_path)
|
49
|
+
console.print(f"[dim]Applied changes to {filepath}[/dim]")
|
50
|
+
|
51
|
+
return True
|
52
|
+
|
53
|
+
def remove_from_workspace_dir(filepath: Path, console: Console) -> None:
|
54
|
+
"""Remove file from working directory"""
|
55
|
+
target_path = config.workspace_dir / filepath
|
56
|
+
if target_path.exists():
|
57
|
+
target_path.unlink()
|
58
|
+
console.print(f"[red]Removed {filepath}[/red]")
|
janito/change/core.py
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
from typing import Optional, Tuple, List
|
3
|
+
from rich.console import Console
|
4
|
+
from rich.prompt import Confirm
|
5
|
+
from rich.panel import Panel
|
6
|
+
from rich.columns import Columns
|
7
|
+
from rich import box
|
8
|
+
|
9
|
+
from janito.common import progress_send_message
|
10
|
+
from janito.change.history import save_changes_to_history
|
11
|
+
from janito.config import config
|
12
|
+
from janito.workspace.workset import Workset # Update import to use Workset directly
|
13
|
+
from .viewer import preview_all_changes
|
14
|
+
from janito.workspace.analysis import analyze_workspace_content as show_content_stats
|
15
|
+
|
16
|
+
from . import (
|
17
|
+
build_change_request_prompt,
|
18
|
+
parse_response,
|
19
|
+
setup_workspace_dir_preview,
|
20
|
+
ChangeApplier
|
21
|
+
)
|
22
|
+
|
23
|
+
from .analysis import analyze_request
|
24
|
+
|
25
|
+
def process_change_request(
|
26
|
+
request: str,
|
27
|
+
preview_only: bool = False,
|
28
|
+
debug: bool = False
|
29
|
+
) -> Tuple[bool, Optional[Path]]:
|
30
|
+
"""
|
31
|
+
Process a change request through the main flow.
|
32
|
+
Return:
|
33
|
+
success: True if the request was processed successfully
|
34
|
+
history_file: Path to the saved history file
|
35
|
+
"""
|
36
|
+
console = Console()
|
37
|
+
workset = Workset() # Create workset instance
|
38
|
+
|
39
|
+
|
40
|
+
# Analyze workspace content
|
41
|
+
workset.show()
|
42
|
+
|
43
|
+
# Get analysis of the request using workset content
|
44
|
+
analysis = analyze_request(request)
|
45
|
+
if not analysis:
|
46
|
+
console.print("[red]Analysis failed or interrupted[/red]")
|
47
|
+
return False, None
|
48
|
+
|
49
|
+
# Build and send prompt
|
50
|
+
prompt = build_change_request_prompt(request, analysis.format_option_text())
|
51
|
+
response = progress_send_message(prompt)
|
52
|
+
if not response:
|
53
|
+
console.print("[red]Failed to get response from AI[/red]")
|
54
|
+
return False, None
|
55
|
+
|
56
|
+
# Save to history and process response
|
57
|
+
history_file = save_changes_to_history(response, request)
|
58
|
+
|
59
|
+
# Parse changes
|
60
|
+
changes = parse_response(response)
|
61
|
+
if not changes:
|
62
|
+
console.print("[yellow]No changes found in response[/yellow]")
|
63
|
+
return False, None
|
64
|
+
|
65
|
+
# Show request and response info
|
66
|
+
response_info = extract_response_info(response)
|
67
|
+
console.print("\n")
|
68
|
+
console.print(Columns([
|
69
|
+
Panel(request, title="User Request", border_style="cyan", box=box.ROUNDED),
|
70
|
+
Panel(
|
71
|
+
response_info if response_info else "No additional information provided",
|
72
|
+
title="Response Information",
|
73
|
+
border_style="green",
|
74
|
+
box=box.ROUNDED
|
75
|
+
)
|
76
|
+
], equal=True, expand=True))
|
77
|
+
console.print("\n")
|
78
|
+
|
79
|
+
if preview_only:
|
80
|
+
preview_all_changes(console, changes)
|
81
|
+
return True, history_file
|
82
|
+
|
83
|
+
# Apply changes
|
84
|
+
_, preview_dir = setup_workspace_dir_preview()
|
85
|
+
applier = ChangeApplier(preview_dir, debug=debug)
|
86
|
+
|
87
|
+
success, _ = applier.apply_changes(changes)
|
88
|
+
if success:
|
89
|
+
preview_all_changes(console, changes)
|
90
|
+
|
91
|
+
if not config.auto_apply:
|
92
|
+
apply_changes = Confirm.ask("[cyan]Apply changes to working dir?[/cyan]")
|
93
|
+
else:
|
94
|
+
apply_changes = True
|
95
|
+
console.print("[cyan]Auto-applying changes to working dir...[/cyan]")
|
96
|
+
|
97
|
+
if apply_changes:
|
98
|
+
applier.apply_to_workspace_dir(changes)
|
99
|
+
console.print("[green]Changes applied successfully[/green]")
|
100
|
+
else:
|
101
|
+
console.print("[yellow]Changes were not applied[/yellow]")
|
102
|
+
|
103
|
+
return success, history_file
|
104
|
+
|
105
|
+
|
106
|
+
def extract_response_info(response: str) -> str:
|
107
|
+
"""Extract information after END_OF_INSTRUCTIONS marker"""
|
108
|
+
if not response:
|
109
|
+
return ""
|
110
|
+
|
111
|
+
# Find the marker
|
112
|
+
marker = "END_INSTRUCTIONS"
|
113
|
+
marker_pos = response.find(marker)
|
114
|
+
|
115
|
+
if marker_pos == -1:
|
116
|
+
return ""
|
117
|
+
|
118
|
+
# Get text after marker, skipping the marker itself
|
119
|
+
info = response[marker_pos + len(marker):].strip()
|
120
|
+
|
121
|
+
# Remove any XML-style tags
|
122
|
+
info = info.replace("<Extra info about what was implemented/changed goes here>", "")
|
123
|
+
|
124
|
+
return info.strip()
|
@@ -1,20 +1,23 @@
|
|
1
1
|
from pathlib import Path
|
2
2
|
from datetime import datetime
|
3
|
-
from typing import Optional
|
3
|
+
from typing import Optional, Tuple
|
4
|
+
from janito.config import config
|
4
5
|
|
5
6
|
# Set fixed timestamp when module is loaded
|
6
7
|
APP_TIMESTAMP = datetime.now().strftime("%Y%m%d_%H%M%S")
|
7
8
|
|
8
|
-
def get_history_path(
|
9
|
+
def get_history_path() -> Path:
|
9
10
|
"""Create and return the history directory path"""
|
10
|
-
history_dir =
|
11
|
+
history_dir = config.workspace_dir / '.janito' / 'change_history'
|
11
12
|
history_dir.mkdir(parents=True, exist_ok=True)
|
12
13
|
return history_dir
|
13
14
|
|
14
15
|
def determine_history_file_type(filepath: Path) -> str:
|
15
16
|
"""Determine the type of saved file based on its name"""
|
16
17
|
name = filepath.name.lower()
|
17
|
-
if '
|
18
|
+
if '_changes_failed' in name:
|
19
|
+
return 'changes_failed'
|
20
|
+
elif 'changes' in name:
|
18
21
|
return 'changes'
|
19
22
|
elif 'selected' in name:
|
20
23
|
return 'selected'
|
@@ -24,13 +27,13 @@ def determine_history_file_type(filepath: Path) -> str:
|
|
24
27
|
return 'response'
|
25
28
|
return 'unknown'
|
26
29
|
|
27
|
-
def save_changes_to_history(content: str, request: str
|
30
|
+
def save_changes_to_history(content: str, request: str) -> Path:
|
28
31
|
"""Save change content to history folder with timestamp and request info"""
|
29
|
-
history_dir = get_history_path(
|
32
|
+
history_dir = get_history_path()
|
30
33
|
|
31
34
|
# Create history entry with request and changes
|
32
|
-
|
33
|
-
|
35
|
+
filename = f"{APP_TIMESTAMP}_changes.txt"
|
36
|
+
history_file = history_dir / filename
|
34
37
|
history_content = f"""Request: {request}
|
35
38
|
Timestamp: {APP_TIMESTAMP}
|
36
39
|
|
@@ -38,9 +41,4 @@ Changes:
|
|
38
41
|
{content}
|
39
42
|
"""
|
40
43
|
history_file.write_text(history_content)
|
41
|
-
return history_file
|
42
|
-
|
43
|
-
def get_history_file_path(workdir: Path) -> Path:
|
44
|
-
"""Get path for a history file with app timestamp"""
|
45
|
-
history_path = get_history_path(workdir)
|
46
|
-
return history_path / f"{APP_TIMESTAMP}_changes.txt"
|
44
|
+
return history_file
|