janito 0.7.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.
Files changed (112) hide show
  1. janito/__main__.py +127 -141
  2. janito/agents/__init__.py +22 -22
  3. janito/agents/agent.py +24 -27
  4. janito/agents/claudeai.py +41 -45
  5. janito/agents/deepseekai.py +47 -0
  6. janito/change/applied_blocks.py +34 -0
  7. janito/change/applier.py +167 -0
  8. janito/change/edit_blocks.py +148 -0
  9. janito/change/finder.py +72 -0
  10. janito/change/request.py +144 -0
  11. janito/change/validator.py +87 -269
  12. janito/change/view/content.py +63 -0
  13. janito/change/{viewer → view}/diff.py +44 -43
  14. janito/change/view/panels.py +201 -0
  15. janito/change/view/sections.py +69 -0
  16. janito/change/view/styling.py +140 -0
  17. janito/change/view/summary.py +37 -0
  18. janito/change/{viewer → view}/themes.py +62 -55
  19. janito/change/view/viewer.py +59 -0
  20. janito/cli/__init__.py +1 -1
  21. janito/cli/commands.py +68 -88
  22. janito/cli/functions.py +66 -111
  23. janito/common.py +132 -79
  24. janito/config.py +99 -101
  25. janito/data/change_prompt.txt +81 -0
  26. janito/data/system_prompt.txt +3 -0
  27. janito/qa.py +56 -57
  28. janito/version.py +22 -22
  29. janito/workspace/__init__.py +8 -6
  30. janito/workspace/analysis.py +120 -120
  31. janito/workspace/{types.py → models.py} +97 -98
  32. janito/workspace/show.py +115 -141
  33. janito/workspace/stats.py +42 -43
  34. janito/workspace/workset.py +135 -108
  35. janito/workspace/workspace.py +335 -114
  36. janito-0.8.0.dist-info/METADATA +106 -0
  37. janito-0.8.0.dist-info/RECORD +40 -0
  38. {janito-0.7.0.dist-info → janito-0.8.0.dist-info}/licenses/LICENSE +20 -20
  39. janito/__init__.py +0 -2
  40. janito/agents/openai.py +0 -57
  41. janito/agents/test.py +0 -34
  42. janito/change/__init__.py +0 -32
  43. janito/change/__main__.py +0 -0
  44. janito/change/analysis/__init__.py +0 -23
  45. janito/change/analysis/__main__.py +0 -7
  46. janito/change/analysis/analyze.py +0 -62
  47. janito/change/analysis/formatting.py +0 -78
  48. janito/change/analysis/options.py +0 -81
  49. janito/change/analysis/prompts.py +0 -90
  50. janito/change/analysis/view/__init__.py +0 -9
  51. janito/change/analysis/view/terminal.py +0 -181
  52. janito/change/applier/__init__.py +0 -5
  53. janito/change/applier/file.py +0 -58
  54. janito/change/applier/main.py +0 -156
  55. janito/change/applier/text.py +0 -247
  56. janito/change/applier/workspace_dir.py +0 -58
  57. janito/change/core.py +0 -124
  58. janito/change/history.py +0 -44
  59. janito/change/operations.py +0 -7
  60. janito/change/parser.py +0 -287
  61. janito/change/play.py +0 -54
  62. janito/change/preview.py +0 -82
  63. janito/change/prompts.py +0 -121
  64. janito/change/test.py +0 -0
  65. janito/change/viewer/__init__.py +0 -11
  66. janito/change/viewer/content.py +0 -66
  67. janito/change/viewer/panels.py +0 -533
  68. janito/change/viewer/styling.py +0 -114
  69. janito/clear_statement_parser/clear_statement_format.txt +0 -328
  70. janito/clear_statement_parser/examples.txt +0 -326
  71. janito/clear_statement_parser/models.py +0 -104
  72. janito/clear_statement_parser/parser.py +0 -496
  73. janito/cli/base.py +0 -30
  74. janito/cli/history.py +0 -61
  75. janito/cli/registry.py +0 -26
  76. janito/demo/__init__.py +0 -4
  77. janito/demo/data.py +0 -13
  78. janito/demo/mock_data.py +0 -20
  79. janito/demo/operations.py +0 -45
  80. janito/demo/runner.py +0 -59
  81. janito/demo/scenarios.py +0 -32
  82. janito/prompt.py +0 -36
  83. janito/review.py +0 -13
  84. janito/search_replace/README.md +0 -192
  85. janito/search_replace/__init__.py +0 -7
  86. janito/search_replace/__main__.py +0 -21
  87. janito/search_replace/core.py +0 -120
  88. janito/search_replace/logger.py +0 -35
  89. janito/search_replace/parser.py +0 -52
  90. janito/search_replace/play.py +0 -61
  91. janito/search_replace/replacer.py +0 -36
  92. janito/search_replace/searcher.py +0 -411
  93. janito/search_replace/strategy_result.py +0 -10
  94. janito/shell/__init__.py +0 -38
  95. janito/shell/bus.py +0 -31
  96. janito/shell/commands.py +0 -136
  97. janito/shell/history.py +0 -20
  98. janito/shell/processor.py +0 -32
  99. janito/shell/prompt.py +0 -48
  100. janito/shell/registry.py +0 -60
  101. janito/tui/__init__.py +0 -21
  102. janito/tui/base.py +0 -22
  103. janito/tui/flows/__init__.py +0 -5
  104. janito/tui/flows/changes.py +0 -65
  105. janito/tui/flows/content.py +0 -128
  106. janito/tui/flows/selection.py +0 -117
  107. janito/tui/screens/__init__.py +0 -3
  108. janito/tui/screens/app.py +0 -1
  109. janito-0.7.0.dist-info/METADATA +0 -167
  110. janito-0.7.0.dist-info/RECORD +0 -96
  111. {janito-0.7.0.dist-info → janito-0.8.0.dist-info}/WHEEL +0 -0
  112. {janito-0.7.0.dist-info → janito-0.8.0.dist-info}/entry_points.txt +0 -0
@@ -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)
@@ -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
+ )
@@ -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()