janito 0.6.0__py3-none-any.whl → 0.8.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/__main__.py +127 -134
- janito/agents/__init__.py +22 -16
- janito/agents/agent.py +24 -20
- janito/agents/claudeai.py +41 -55
- janito/agents/deepseekai.py +47 -0
- janito/change/applied_blocks.py +34 -0
- janito/change/applier.py +167 -0
- janito/change/edit_blocks.py +148 -0
- janito/change/finder.py +72 -0
- janito/change/request.py +144 -0
- janito/change/validator.py +87 -251
- janito/change/view/content.py +63 -0
- janito/change/{viewer → view}/diff.py +44 -43
- janito/change/view/panels.py +201 -0
- janito/change/view/sections.py +69 -0
- janito/change/view/styling.py +140 -0
- janito/change/view/summary.py +37 -0
- janito/change/{viewer → view}/themes.py +62 -55
- janito/change/view/viewer.py +59 -0
- janito/cli/__init__.py +1 -1
- janito/cli/commands.py +68 -45
- janito/cli/functions.py +66 -111
- janito/common.py +132 -53
- janito/config.py +99 -101
- janito/data/change_prompt.txt +81 -0
- janito/data/system_prompt.txt +3 -0
- janito/qa.py +56 -66
- janito/version.py +22 -22
- janito/workspace/__init__.py +8 -7
- janito/workspace/analysis.py +120 -120
- janito/workspace/models.py +97 -0
- janito/workspace/show.py +115 -0
- janito/workspace/stats.py +42 -0
- janito/workspace/workset.py +135 -0
- janito/workspace/workspace.py +335 -0
- janito-0.8.0.dist-info/METADATA +106 -0
- janito-0.8.0.dist-info/RECORD +40 -0
- {janito-0.6.0.dist-info → janito-0.8.0.dist-info}/licenses/LICENSE +20 -20
- janito/__init__.py +0 -2
- janito/agents/openai.py +0 -53
- janito/agents/test.py +0 -34
- janito/change/__init__.py +0 -32
- janito/change/__main__.py +0 -0
- janito/change/analysis/__init__.py +0 -23
- janito/change/analysis/__main__.py +0 -7
- janito/change/analysis/analyze.py +0 -61
- janito/change/analysis/formatting.py +0 -78
- janito/change/analysis/options.py +0 -81
- janito/change/analysis/prompts.py +0 -98
- janito/change/analysis/view/__init__.py +0 -9
- janito/change/analysis/view/terminal.py +0 -171
- janito/change/applier/__init__.py +0 -5
- janito/change/applier/file.py +0 -58
- janito/change/applier/main.py +0 -156
- janito/change/applier/text.py +0 -245
- janito/change/applier/workspace_dir.py +0 -58
- janito/change/core.py +0 -131
- janito/change/history.py +0 -44
- janito/change/operations.py +0 -7
- janito/change/parser.py +0 -289
- janito/change/play.py +0 -54
- janito/change/preview.py +0 -82
- janito/change/prompts.py +0 -126
- janito/change/test.py +0 -0
- janito/change/viewer/__init__.py +0 -11
- janito/change/viewer/content.py +0 -66
- janito/change/viewer/pager.py +0 -56
- janito/change/viewer/panels.py +0 -555
- janito/change/viewer/styling.py +0 -103
- janito/clear_statement_parser/clear_statement_format.txt +0 -328
- janito/clear_statement_parser/examples.txt +0 -326
- janito/clear_statement_parser/models.py +0 -104
- janito/clear_statement_parser/parser.py +0 -496
- janito/cli/base.py +0 -30
- janito/cli/handlers/ask.py +0 -22
- janito/cli/handlers/demo.py +0 -22
- janito/cli/handlers/request.py +0 -24
- janito/cli/handlers/scan.py +0 -9
- janito/cli/history.py +0 -61
- janito/cli/registry.py +0 -26
- janito/demo/__init__.py +0 -4
- janito/demo/data.py +0 -13
- janito/demo/mock_data.py +0 -20
- janito/demo/operations.py +0 -45
- janito/demo/runner.py +0 -59
- janito/demo/scenarios.py +0 -32
- janito/prompts.py +0 -2
- janito/review.py +0 -13
- janito/search_replace/README.md +0 -146
- janito/search_replace/__init__.py +0 -6
- janito/search_replace/__main__.py +0 -21
- janito/search_replace/core.py +0 -119
- janito/search_replace/parser.py +0 -52
- janito/search_replace/play.py +0 -61
- janito/search_replace/replacer.py +0 -36
- janito/search_replace/searcher.py +0 -299
- janito/shell/__init__.py +0 -39
- janito/shell/bus.py +0 -31
- janito/shell/commands.py +0 -195
- janito/shell/handlers.py +0 -122
- janito/shell/history.py +0 -20
- janito/shell/processor.py +0 -52
- janito/tui/__init__.py +0 -21
- janito/tui/base.py +0 -22
- janito/tui/flows/__init__.py +0 -5
- janito/tui/flows/changes.py +0 -65
- janito/tui/flows/content.py +0 -128
- janito/tui/flows/selection.py +0 -117
- janito/tui/screens/__init__.py +0 -3
- janito/tui/screens/app.py +0 -1
- janito/workspace/manager.py +0 -48
- janito/workspace/scan.py +0 -232
- janito-0.6.0.dist-info/METADATA +0 -185
- janito-0.6.0.dist-info/RECORD +0 -95
- {janito-0.6.0.dist-info → janito-0.8.0.dist-info}/WHEEL +0 -0
- {janito-0.6.0.dist-info → janito-0.8.0.dist-info}/entry_points.txt +0 -0
janito/change/applier.py
ADDED
@@ -0,0 +1,167 @@
|
|
1
|
+
from typing import List
|
2
|
+
from pathlib import Path
|
3
|
+
from janito.config import config
|
4
|
+
from .finder import find_range, EditContentNotFoundError
|
5
|
+
from .edit_blocks import EditType, CodeChange
|
6
|
+
from .applied_blocks import AppliedBlock, AppliedBlocks
|
7
|
+
|
8
|
+
class ChangeApplier:
|
9
|
+
def __init__(self, target_dir: Path):
|
10
|
+
self.target_dir = target_dir
|
11
|
+
self.edits: List[CodeChange] = []
|
12
|
+
self._last_changed_line = 0
|
13
|
+
self.current_file = None
|
14
|
+
self.current_content: List[str] = []
|
15
|
+
self.applied_blocks = AppliedBlocks(blocks=[])
|
16
|
+
|
17
|
+
def add_edit(self, edit: CodeChange):
|
18
|
+
self.edits.append(edit)
|
19
|
+
|
20
|
+
def start_file_edit(self, filename: str, edit_type: EditType):
|
21
|
+
if self.current_file:
|
22
|
+
self.end_file_edit()
|
23
|
+
self._last_changed_line = 0
|
24
|
+
self.current_file = filename
|
25
|
+
self.current_edit_type = edit_type # Store edit type for end_file_edit
|
26
|
+
|
27
|
+
if edit_type == EditType.CREATE:
|
28
|
+
self.current_content = []
|
29
|
+
elif edit_type == EditType.DELETE:
|
30
|
+
if not (self.target_dir / filename).exists():
|
31
|
+
raise FileNotFoundError(f"Cannot delete non-existent file: {filename}")
|
32
|
+
self.current_content = []
|
33
|
+
else:
|
34
|
+
self.current_content = (self.target_dir / filename).read_text(encoding="utf-8").splitlines()
|
35
|
+
|
36
|
+
def end_file_edit(self):
|
37
|
+
if self.current_file:
|
38
|
+
target_path = self.target_dir / self.current_file
|
39
|
+
if hasattr(self, 'current_edit_type') and self.current_edit_type == EditType.DELETE:
|
40
|
+
if target_path.exists():
|
41
|
+
target_path.unlink()
|
42
|
+
else:
|
43
|
+
# Create parent directories if they don't exist
|
44
|
+
target_path.parent.mkdir(parents=True, exist_ok=True)
|
45
|
+
target_path.write_text("\n".join(self.current_content), encoding="utf-8")
|
46
|
+
self.current_file = None
|
47
|
+
|
48
|
+
def apply(self):
|
49
|
+
"""Apply all edits and show summary of changes."""
|
50
|
+
# Ensure target directory exists
|
51
|
+
self.target_dir.mkdir(parents=True, exist_ok=True)
|
52
|
+
|
53
|
+
# Track changes as we apply them
|
54
|
+
changes = []
|
55
|
+
current_file = None
|
56
|
+
|
57
|
+
# Process edits in order as they were added
|
58
|
+
for edit in self.edits:
|
59
|
+
if current_file != edit.filename:
|
60
|
+
self.end_file_edit()
|
61
|
+
self.start_file_edit(str(edit.filename), edit.edit_type)
|
62
|
+
current_file = edit.filename
|
63
|
+
self._apply_and_collect_change(edit)
|
64
|
+
|
65
|
+
self.end_file_edit()
|
66
|
+
|
67
|
+
def _apply_and_collect_change(self, edit: CodeChange) -> AppliedBlock:
|
68
|
+
"""Apply a single edit and collect its change information."""
|
69
|
+
if edit.edit_type == EditType.CREATE:
|
70
|
+
self.current_content = edit.modified
|
71
|
+
applied_block = AppliedBlock(
|
72
|
+
filename=edit.filename,
|
73
|
+
edit_type=edit.edit_type,
|
74
|
+
reason=edit.reason,
|
75
|
+
original_content=[],
|
76
|
+
modified_content=edit.modified,
|
77
|
+
range_start=1,
|
78
|
+
range_end=len(edit.modified),
|
79
|
+
block_marker=edit.block_marker
|
80
|
+
)
|
81
|
+
|
82
|
+
elif edit.edit_type == EditType.DELETE:
|
83
|
+
applied_block = AppliedBlock(
|
84
|
+
filename=edit.filename,
|
85
|
+
edit_type=edit.edit_type,
|
86
|
+
reason=edit.reason,
|
87
|
+
original_content=self.current_content,
|
88
|
+
modified_content=[],
|
89
|
+
range_start=1,
|
90
|
+
range_end=len(self.current_content),
|
91
|
+
block_marker=edit.block_marker
|
92
|
+
)
|
93
|
+
self.current_content = []
|
94
|
+
|
95
|
+
elif edit.edit_type == EditType.CLEAN:
|
96
|
+
try:
|
97
|
+
start_range = find_range(self.current_content, edit.original, self._last_changed_line)
|
98
|
+
try:
|
99
|
+
end_range = find_range(self.current_content, edit.modified, start_range[1])
|
100
|
+
except EditContentNotFoundError:
|
101
|
+
end_range = (start_range[1], start_range[1])
|
102
|
+
|
103
|
+
section = self.current_content[start_range[0]:end_range[1]]
|
104
|
+
applied_block = AppliedBlock(
|
105
|
+
filename=edit.filename,
|
106
|
+
edit_type=edit.edit_type,
|
107
|
+
reason=edit.reason,
|
108
|
+
original_content=section,
|
109
|
+
modified_content=[],
|
110
|
+
range_start=start_range[0] + 1,
|
111
|
+
range_end=end_range[1],
|
112
|
+
block_marker=edit.block_marker
|
113
|
+
)
|
114
|
+
|
115
|
+
self.current_content[start_range[0]:end_range[1]] = []
|
116
|
+
self._last_changed_line = start_range[0]
|
117
|
+
|
118
|
+
except ValueError as e:
|
119
|
+
error_msg = f"Failed to find clean section in {self.current_file}: {e}"
|
120
|
+
applied_block = AppliedBlock(
|
121
|
+
filename=edit.filename,
|
122
|
+
edit_type=edit.edit_type,
|
123
|
+
reason=edit.reason,
|
124
|
+
original_content=self.current_content,
|
125
|
+
modified_content=[],
|
126
|
+
range_start=1,
|
127
|
+
range_end=len(self.current_content),
|
128
|
+
block_marker=edit.block_marker,
|
129
|
+
error_message=error_msg,
|
130
|
+
has_error=True
|
131
|
+
)
|
132
|
+
|
133
|
+
else: # EDIT operation
|
134
|
+
try:
|
135
|
+
edit_range = find_range(self.current_content, edit.original, self._last_changed_line)
|
136
|
+
original_section = self.current_content[edit_range[0]:edit_range[1]]
|
137
|
+
|
138
|
+
applied_block = AppliedBlock(
|
139
|
+
filename=edit.filename,
|
140
|
+
edit_type=edit.edit_type,
|
141
|
+
reason=edit.reason,
|
142
|
+
original_content=original_section,
|
143
|
+
modified_content=edit.modified,
|
144
|
+
range_start=edit_range[0] + 1,
|
145
|
+
range_end=edit_range[0] + len(edit.original),
|
146
|
+
block_marker=edit.block_marker
|
147
|
+
)
|
148
|
+
|
149
|
+
self._last_changed_line = edit_range[0] + len(edit.original)
|
150
|
+
self.current_content[edit_range[0]:edit_range[1]] = edit.modified
|
151
|
+
except EditContentNotFoundError as e:
|
152
|
+
error_msg = f"Failed to find edit section in {self.current_file}: {e}"
|
153
|
+
applied_block = AppliedBlock(
|
154
|
+
filename=edit.filename,
|
155
|
+
edit_type=edit.edit_type,
|
156
|
+
reason=edit.reason,
|
157
|
+
original_content=edit.original,
|
158
|
+
modified_content=edit.modified,
|
159
|
+
range_start=self._last_changed_line + 1,
|
160
|
+
range_end=self._last_changed_line + len(edit.original),
|
161
|
+
block_marker=edit.block_marker,
|
162
|
+
error_message=error_msg,
|
163
|
+
has_error=True
|
164
|
+
)
|
165
|
+
|
166
|
+
self.applied_blocks.blocks.append(applied_block)
|
167
|
+
return applied_block
|
@@ -0,0 +1,148 @@
|
|
1
|
+
from dataclasses import dataclass
|
2
|
+
from pathlib import Path
|
3
|
+
from enum import Enum, auto
|
4
|
+
from typing import List, Tuple, Dict
|
5
|
+
import string
|
6
|
+
|
7
|
+
class EditType(Enum):
|
8
|
+
CREATE = auto()
|
9
|
+
EDIT = auto()
|
10
|
+
DELETE = auto()
|
11
|
+
CLEAN = auto()
|
12
|
+
|
13
|
+
@dataclass
|
14
|
+
class CodeChange:
|
15
|
+
filename: Path
|
16
|
+
reason: str
|
17
|
+
original: List[str] # Changed from 'before'
|
18
|
+
modified: List[str] # Changed from 'after'
|
19
|
+
edit_type: EditType = EditType.EDIT
|
20
|
+
block_marker: str = None # Track which code block this change came from
|
21
|
+
|
22
|
+
def parse_edit_command(line: str, command: str) -> tuple[Path, str]:
|
23
|
+
"""Parse an Edit or Create command line to extract filename and reason.
|
24
|
+
Expected format: Command filename "reason"
|
25
|
+
Example: Edit path/to/file.py "Add new feature"
|
26
|
+
"""
|
27
|
+
if not line or not line.startswith(command):
|
28
|
+
raise ValueError(f"Invalid command format in line:\n{line}\nExpected: {command} filename \"reason\"")
|
29
|
+
|
30
|
+
# Split by quote to separate filename from reason
|
31
|
+
parts = line.split('"')
|
32
|
+
if len(parts) < 2:
|
33
|
+
raise ValueError(f"Missing reason in quotes in line:\n{line}")
|
34
|
+
|
35
|
+
filename = Path(parts[0].replace(f"{command} ", "").strip())
|
36
|
+
reason = parts[1].strip()
|
37
|
+
|
38
|
+
return filename, reason
|
39
|
+
|
40
|
+
def get_edit_blocks(response: str) -> Tuple[List[CodeChange], str]:
|
41
|
+
"""Parse response text into a list of CodeChange objects and annotated response.
|
42
|
+
|
43
|
+
The format expected from the response follows the system prompt:
|
44
|
+
|
45
|
+
Edit file "reason"
|
46
|
+
<<<< original
|
47
|
+
{original code}
|
48
|
+
>>>> modified
|
49
|
+
{modified code}
|
50
|
+
====
|
51
|
+
|
52
|
+
Clean file "reason"
|
53
|
+
<<<< starting
|
54
|
+
{start marker lines}
|
55
|
+
>>>> ending
|
56
|
+
{end marker lines}
|
57
|
+
====
|
58
|
+
"""
|
59
|
+
edit_blocks = []
|
60
|
+
modified_response = []
|
61
|
+
current_block = []
|
62
|
+
original_content = None
|
63
|
+
current_command = None
|
64
|
+
marker_index = 0
|
65
|
+
in_block = False
|
66
|
+
|
67
|
+
for line in response.splitlines():
|
68
|
+
# Handle command lines
|
69
|
+
if line.startswith(("Edit ", "Create ", "Delete ", "Clean ")):
|
70
|
+
command = line.split(" ")[0]
|
71
|
+
filename, reason = parse_edit_command(line, command)
|
72
|
+
current_command = command
|
73
|
+
# Reset state for new command
|
74
|
+
original_content = None
|
75
|
+
current_block = []
|
76
|
+
# Add marker for this edit block
|
77
|
+
current_marker = string.ascii_uppercase[marker_index]
|
78
|
+
modified_response.append(f"[Edit Block {current_marker}]")
|
79
|
+
marker_index += 1
|
80
|
+
continue
|
81
|
+
|
82
|
+
# Add the line to modified_response unless we're in a code block or it's a block marker
|
83
|
+
if not in_block and not line.startswith(("<<<< ", ">>>> ", "====")):
|
84
|
+
modified_response.append(line)
|
85
|
+
|
86
|
+
# Handle block markers - Update to match system prompt
|
87
|
+
if line.startswith("<<<< original") or line.startswith("<<<< starting"):
|
88
|
+
current_block = []
|
89
|
+
in_block = True
|
90
|
+
if current_command == "Clean":
|
91
|
+
original_content = None
|
92
|
+
elif line.startswith(">>>> modified") or line.startswith(">>>> ending"):
|
93
|
+
if current_command == "Clean":
|
94
|
+
original_content = current_block
|
95
|
+
elif not original_content and current_block:
|
96
|
+
original_content = current_block
|
97
|
+
current_block = []
|
98
|
+
in_block = True
|
99
|
+
elif line == "====": # End of edit block
|
100
|
+
# Trim empty lines at start and end of blocks
|
101
|
+
def trim_block(block: List[str]) -> List[str]:
|
102
|
+
if not block:
|
103
|
+
return []
|
104
|
+
# Remove empty lines at start and end
|
105
|
+
while block and not block[0].strip():
|
106
|
+
block.pop(0)
|
107
|
+
while block and not block[-1].strip():
|
108
|
+
block.pop()
|
109
|
+
return block
|
110
|
+
|
111
|
+
if current_command == "Delete":
|
112
|
+
edit_blocks.append(CodeChange(filename, reason, [], [], EditType.DELETE, current_marker))
|
113
|
+
elif current_command == "Clean":
|
114
|
+
edit_blocks.append(CodeChange(
|
115
|
+
filename, reason,
|
116
|
+
trim_block(original_content or []),
|
117
|
+
trim_block(current_block),
|
118
|
+
EditType.CLEAN,
|
119
|
+
current_marker
|
120
|
+
))
|
121
|
+
elif current_command == "Create":
|
122
|
+
edit_blocks.append(CodeChange(
|
123
|
+
filename, reason,
|
124
|
+
[],
|
125
|
+
trim_block(current_block),
|
126
|
+
EditType.CREATE,
|
127
|
+
current_marker
|
128
|
+
))
|
129
|
+
elif current_command == "Edit":
|
130
|
+
original = trim_block(original_content or [])
|
131
|
+
modified = trim_block(current_block)
|
132
|
+
edit_blocks.append(CodeChange(
|
133
|
+
filename, reason,
|
134
|
+
original,
|
135
|
+
modified,
|
136
|
+
EditType.EDIT,
|
137
|
+
current_marker
|
138
|
+
))
|
139
|
+
|
140
|
+
# Reset state after block is completed
|
141
|
+
current_block = []
|
142
|
+
in_block = False
|
143
|
+
current_command = None
|
144
|
+
original_content = None
|
145
|
+
elif in_block:
|
146
|
+
current_block.append(line)
|
147
|
+
|
148
|
+
return edit_blocks, "\n".join(modified_response)
|
janito/change/finder.py
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
from typing import List, Tuple
|
2
|
+
from difflib import SequenceMatcher
|
3
|
+
|
4
|
+
class EditContentNotFoundError(ValueError):
|
5
|
+
"""Raised when edit content cannot be found in the target file."""
|
6
|
+
pass
|
7
|
+
|
8
|
+
SIMILARITY_THRESHOLD = 0.8 # Minimum similarity required for a match
|
9
|
+
|
10
|
+
def find_range(full_lines: List[str], changed_lines: List[str], start: int = 0) -> Tuple[int, int]:
|
11
|
+
"""Find the range of the first occurrence of the changed_lines in the full_lines list.
|
12
|
+
|
13
|
+
Args:
|
14
|
+
full_lines: The complete text content to search within
|
15
|
+
changed_lines: The block of lines to find
|
16
|
+
start: The line number to start searching from
|
17
|
+
|
18
|
+
Returns:
|
19
|
+
Tuple of (start_line, end_line) where the block was found
|
20
|
+
|
21
|
+
Raises:
|
22
|
+
ValueError: If no matching block is found with sufficient similarity
|
23
|
+
"""
|
24
|
+
_validate_inputs(full_lines, changed_lines, start)
|
25
|
+
|
26
|
+
if not changed_lines:
|
27
|
+
return (start, start)
|
28
|
+
|
29
|
+
best_match, best_score = _find_best_matching_block(full_lines, changed_lines, start)
|
30
|
+
|
31
|
+
if not best_match or best_score < SIMILARITY_THRESHOLD:
|
32
|
+
_raise_no_match_error(changed_lines, start, best_score)
|
33
|
+
|
34
|
+
return best_match
|
35
|
+
|
36
|
+
def _validate_inputs(full_lines: List[str], changed_lines: List[str], start: int) -> None:
|
37
|
+
if start >= len(full_lines):
|
38
|
+
raise ValueError(f"Start position {start} is beyond content length {len(full_lines)}")
|
39
|
+
|
40
|
+
def _find_best_matching_block(full_lines: List[str], changed_lines: List[str], start: int) -> Tuple[Tuple[int, int], float]:
|
41
|
+
best_match = None
|
42
|
+
best_score = 0.0
|
43
|
+
|
44
|
+
for i in range(start, len(full_lines) - len(changed_lines) + 1):
|
45
|
+
window = full_lines[i:i + len(changed_lines)]
|
46
|
+
if len(window) != len(changed_lines):
|
47
|
+
continue
|
48
|
+
|
49
|
+
similarity = _calculate_similarity(window, changed_lines)
|
50
|
+
|
51
|
+
if similarity > best_score:
|
52
|
+
best_score = similarity
|
53
|
+
best_match = (i, i + len(changed_lines))
|
54
|
+
|
55
|
+
if similarity == 1.0: # Early exit on perfect match
|
56
|
+
break
|
57
|
+
|
58
|
+
return best_match, best_score
|
59
|
+
|
60
|
+
def _calculate_similarity(window: List[str], changed_lines: List[str]) -> float:
|
61
|
+
return sum(
|
62
|
+
SequenceMatcher(None, a, b).ratio()
|
63
|
+
for a, b in zip(window, changed_lines)
|
64
|
+
) / len(changed_lines)
|
65
|
+
|
66
|
+
def _raise_no_match_error(changed_lines: List[str], start: int, best_score: float) -> None:
|
67
|
+
sample = "\n".join(changed_lines[:3]) + ("..." if len(changed_lines) > 3 else "")
|
68
|
+
raise EditContentNotFoundError(
|
69
|
+
f"Could not find matching block after line {start}. "
|
70
|
+
f"Looking for:\n{sample}\n"
|
71
|
+
f"Best match score: {best_score:.2f}"
|
72
|
+
)
|
janito/change/request.py
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
from rich.console import Console
|
2
|
+
from janito.common import progress_send_message, _get_system_prompt
|
3
|
+
from janito.workspace import workset, workspace
|
4
|
+
from janito.config import config
|
5
|
+
from pathlib import Path
|
6
|
+
import tempfile
|
7
|
+
from janito.change.applier import ChangeApplier
|
8
|
+
from janito.change.validator import Validator
|
9
|
+
from janito.change.edit_blocks import EditType, CodeChange, get_edit_blocks
|
10
|
+
from typing import List, Dict, Optional
|
11
|
+
import shutil
|
12
|
+
from rich.markdown import Markdown
|
13
|
+
import importlib.resources
|
14
|
+
from .view.viewer import ChangeViewer
|
15
|
+
|
16
|
+
def _get_change_prompt() -> str:
|
17
|
+
"""Get the change prompt from the package data or local file."""
|
18
|
+
try:
|
19
|
+
# First try to read from package data
|
20
|
+
with importlib.resources.files('janito.data').joinpath('change_prompt.txt').open('r') as f:
|
21
|
+
return f.read()
|
22
|
+
except Exception:
|
23
|
+
# Fallback to local file for development
|
24
|
+
local_path = Path(__file__).parent.parent / 'data' / 'change_prompt.txt'
|
25
|
+
if local_path.exists():
|
26
|
+
return local_path.read_text()
|
27
|
+
raise FileNotFoundError("Could not find change_prompt.txt")
|
28
|
+
|
29
|
+
def request_change(request: str) -> str:
|
30
|
+
"""Process a change request for the codebase and return the response."""
|
31
|
+
|
32
|
+
change_prompt = _get_change_prompt()
|
33
|
+
|
34
|
+
prompt = change_prompt.format(
|
35
|
+
request=request,
|
36
|
+
workset=workset.content
|
37
|
+
)
|
38
|
+
response = progress_send_message(prompt)
|
39
|
+
|
40
|
+
if response is None:
|
41
|
+
return "Sorry, the response was interrupted. Please try your request again."
|
42
|
+
|
43
|
+
|
44
|
+
# Store response in workspace directory
|
45
|
+
response_file = (config.workspace_dir or Path(".")) / '.janito_last_response.txt'
|
46
|
+
response_file.parent.mkdir(parents=True, exist_ok=True)
|
47
|
+
response_file.write_text(response, encoding='utf-8')
|
48
|
+
if config.debug:
|
49
|
+
print(f"Response saved to {response_file}")
|
50
|
+
handler = ResponseHandler(response)
|
51
|
+
handler.process()
|
52
|
+
|
53
|
+
class ResponseHandler:
|
54
|
+
|
55
|
+
def __init__(self, response: str):
|
56
|
+
self.response = response
|
57
|
+
self.console = Console()
|
58
|
+
self.edit_blocks = []
|
59
|
+
self.viewer = None
|
60
|
+
self.applied_blocks = None # Store applied blocks reference
|
61
|
+
|
62
|
+
def show_block_changes(self, block_marker: str):
|
63
|
+
"""Callback to display changes for a specific block marker"""
|
64
|
+
if block_marker and self.applied_blocks:
|
65
|
+
# Find the block with matching marker and show it
|
66
|
+
for block in self.applied_blocks.blocks:
|
67
|
+
if block.block_marker == block_marker:
|
68
|
+
self.viewer._show_block(block)
|
69
|
+
break
|
70
|
+
|
71
|
+
def process(self):
|
72
|
+
self.edit_blocks, self.annotated_response = get_edit_blocks(self.response)
|
73
|
+
|
74
|
+
# Setup preview directory and applier
|
75
|
+
preview_dir = workspace.setup_preview_directory()
|
76
|
+
applier = ChangeApplier(preview_dir)
|
77
|
+
self.viewer = ChangeViewer()
|
78
|
+
|
79
|
+
# Apply changes
|
80
|
+
for block in self.edit_blocks:
|
81
|
+
applier.add_edit(block)
|
82
|
+
applier.apply()
|
83
|
+
|
84
|
+
# Store reference to applied blocks
|
85
|
+
self.applied_blocks = applier.applied_blocks
|
86
|
+
|
87
|
+
# Split response into sections and display with changes
|
88
|
+
sections = self.annotated_response.split("[Edit Block ")
|
89
|
+
|
90
|
+
if sections:
|
91
|
+
self.console.print(Markdown(sections[0])) # Print initial text
|
92
|
+
for section in sections[1:]:
|
93
|
+
marker, text = section.split("]", 1)
|
94
|
+
# Find and show the corresponding block's changes
|
95
|
+
for block in self.applied_blocks.blocks:
|
96
|
+
if block.block_marker == marker:
|
97
|
+
self.viewer._show_block(block)
|
98
|
+
break
|
99
|
+
self.console.print(Markdown(text)) # Print text after the block
|
100
|
+
|
101
|
+
# Add horizontal ruler to separate changes from validation
|
102
|
+
self.console.rule("[bold]Validation", style="dim")
|
103
|
+
|
104
|
+
# Collect files that need validation (excluding deleted files)
|
105
|
+
files_to_validate = {edit.filename for edit in self.edit_blocks
|
106
|
+
if edit.edit_type != EditType.DELETE}
|
107
|
+
|
108
|
+
# Validate changes and run tests
|
109
|
+
validator = Validator(preview_dir)
|
110
|
+
validator.validate_files(files_to_validate)
|
111
|
+
validator.run_tests()
|
112
|
+
|
113
|
+
# Collect the list of created/modified/deleted files
|
114
|
+
created_files = [edit.filename for edit in self.edit_blocks if edit.edit_type == EditType.CREATE]
|
115
|
+
modified_files = set(edit.filename for edit in self.edit_blocks
|
116
|
+
if edit.edit_type in (EditType.EDIT, EditType.CLEAN)) # Include cleaned files
|
117
|
+
deleted_files = set(edit.filename for edit in self.edit_blocks if edit.edit_type == EditType.DELETE)
|
118
|
+
|
119
|
+
# prompt the user if we want to apply the changes
|
120
|
+
if config.auto_apply:
|
121
|
+
apply_changes = True
|
122
|
+
else:
|
123
|
+
self.console.print("\nApply changes to the workspace? [y/N] ", end="")
|
124
|
+
response = input().lower()
|
125
|
+
apply_changes = response.startswith('y')
|
126
|
+
if not apply_changes:
|
127
|
+
self.console.print("[yellow]Changes were not applied. Exiting...[/yellow]")
|
128
|
+
return
|
129
|
+
|
130
|
+
# Apply changes to workspace
|
131
|
+
workspace.apply_changes(preview_dir, created_files, modified_files, deleted_files)
|
132
|
+
|
133
|
+
def replay_saved_response():
|
134
|
+
response_file = (config.workspace_dir or Path(".")) / '.janito_last_response.txt'
|
135
|
+
print(response_file)
|
136
|
+
if not response_file.exists():
|
137
|
+
print("No saved response found")
|
138
|
+
return
|
139
|
+
|
140
|
+
with open(response_file, 'r', encoding="utf-8") as file:
|
141
|
+
response = file.read()
|
142
|
+
|
143
|
+
handler = ResponseHandler(response)
|
144
|
+
handler.process()
|