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
@@ -1,58 +0,0 @@
1
- from pathlib import Path
2
- from typing import Tuple, Optional
3
- from rich.console import Console
4
- from ..parser import FileChange, ChangeOperation
5
-
6
- class FileChangeApplier:
7
- def __init__(self, preview_dir: Path, console: Console = None):
8
- self.preview_dir = preview_dir
9
- self.console = console or Console()
10
-
11
- def apply_file_operation(self, change: FileChange) -> Tuple[bool, Optional[str]]:
12
- """Apply a file operation (create/replace/remove/rename/move)
13
- Returns: (success, error_message)"""
14
- path = self.preview_dir / change.name
15
- path.parent.mkdir(parents=True, exist_ok=True)
16
-
17
- # Store original content before any changes
18
- if path.exists():
19
- change.original_content = path.read_text()
20
-
21
- if change.operation == ChangeOperation.REMOVE_FILE:
22
- return self._handle_remove(path)
23
- elif change.operation in (ChangeOperation.CREATE_FILE, ChangeOperation.REPLACE_FILE):
24
- return self._handle_create_replace(path, change)
25
- elif change.operation in (ChangeOperation.RENAME_FILE, ChangeOperation.MOVE_FILE):
26
- return self._handle_move(path, change)
27
-
28
- return False, f"Unsupported operation: {change.operation}"
29
-
30
- def _handle_remove(self, path: Path) -> Tuple[bool, Optional[str]]:
31
- """Handle file removal"""
32
- if path.exists():
33
- path.unlink()
34
- return True, None
35
-
36
- def _handle_create_replace(self, path: Path, change: FileChange) -> Tuple[bool, Optional[str]]:
37
- """Handle file creation or replacement"""
38
- if change.operation == ChangeOperation.CREATE_FILE and path.exists():
39
- return False, f"Cannot create file {path} - already exists"
40
-
41
- if change.content is not None:
42
- path.write_text(change.content)
43
- return True, None
44
-
45
- return False, "No content provided for create/replace operation"
46
-
47
- def _handle_move(self, path: Path, change: FileChange) -> Tuple[bool, Optional[str]]:
48
- """Handle file move/rename operations"""
49
- if not path.exists():
50
- return False, f"Cannot move/rename non-existent file {path}"
51
-
52
- if not change.target:
53
- return False, "No target path provided for move/rename operation"
54
-
55
- new_path = self.preview_dir / change.target
56
- new_path.parent.mkdir(parents=True, exist_ok=True)
57
- path.rename(new_path)
58
- return True, None
@@ -1,156 +0,0 @@
1
- """
2
- Applies file changes to preview directory and runs tests
3
-
4
- The following situations should result in error:
5
- - Creating a file that already exists
6
- - Replace operation on a non-existent file
7
- - Rename operation on a non-existent file
8
- - Modify operation with search text not found
9
- - No changes applied to a file
10
- """
11
-
12
- from pathlib import Path
13
- from typing import Tuple, Optional, List, Set
14
- from rich.console import Console
15
- from rich.panel import Panel
16
- from rich import box
17
- import subprocess
18
- from ..validator import validate_python_syntax
19
- from .workspace_dir import apply_changes as apply_to_workspace_dir_impl
20
- from janito.config import config
21
- from .file import FileChangeApplier
22
- from .text import TextChangeApplier
23
- from ..parser import FileChange, ChangeOperation
24
- from ..validator import validate_all_changes
25
-
26
-
27
- class ChangeApplier:
28
- """Handles applying changes to files."""
29
-
30
- def __init__(self, preview_dir: Path, debug: bool = False):
31
- self.preview_dir = preview_dir
32
- self.debug = debug
33
- self.console = Console()
34
- self.file_applier = FileChangeApplier(preview_dir, self.console)
35
- self.text_applier = TextChangeApplier(self.console)
36
-
37
- def run_test_command(self, test_cmd: str) -> Tuple[bool, str, Optional[str]]:
38
- """Run test command in preview directory.
39
- Returns (success, output, error)"""
40
- try:
41
- result = subprocess.run(
42
- test_cmd,
43
- shell=True,
44
- cwd=self.preview_dir,
45
- capture_output=True,
46
- text=True,
47
- timeout=300 # 5 minute timeout
48
- )
49
- return (
50
- result.returncode == 0,
51
- result.stdout,
52
- result.stderr if result.returncode != 0 else None
53
- )
54
- except subprocess.TimeoutExpired:
55
- return False, "", "Test command timed out after 5 minutes"
56
- except Exception as e:
57
- return False, "", f"Error running test: {str(e)}"
58
-
59
- def apply_changes(self, changes: List[FileChange], debug: bool = None) -> tuple[bool, Set[Path]]:
60
- """Apply changes in preview directory, runs tests if specified.
61
- Returns (success, modified_files)"""
62
- debug = debug if debug is not None else self.debug
63
- console = Console()
64
-
65
- # Validate all changes using consolidated validator
66
- is_valid, error = validate_all_changes(changes, set(Path(c.name) for c in changes))
67
- if not is_valid:
68
- console.print(f"\n[red]{error}[/red]")
69
- return False, set()
70
-
71
- # Track modified files and apply changes
72
- modified_files: Set[Path] = set()
73
- for change in changes:
74
- if config.verbose:
75
- console.print(f"[dim]Previewing changes for {change.name}...[/dim]")
76
- success, error = self.apply_single_change(change, debug)
77
- if not success:
78
- console.print(f"\n[red]Error previewing {change.name}: {error}[/red]")
79
- return False, modified_files
80
- if not change.operation == 'remove_file':
81
- modified_files.add(change.name)
82
- elif change.operation == 'rename_file':
83
- modified_files.add(change.target)
84
-
85
- # Validate Python syntax (skip deleted and moved files)
86
- python_files = {f for f in modified_files if f.suffix == '.py'}
87
-
88
- for change in changes:
89
- if change.operation == ChangeOperation.REMOVE_FILE:
90
- python_files.discard(change.name) # Skip validation for deleted files
91
- elif change.operation in (ChangeOperation.RENAME_FILE, ChangeOperation.MOVE_FILE):
92
- python_files.discard(change.source) # Skip validation for moved/renamed sources
93
-
94
- for path in python_files:
95
- preview_path = self.preview_dir / path
96
- is_valid, error_msg = validate_python_syntax(preview_path.read_text(), preview_path)
97
- if not is_valid:
98
- console.print(f"\n[red]Python syntax validation failed for {path}:[/red]")
99
- console.print(f"[red]{error_msg}[/red]")
100
- return False, modified_files
101
-
102
- # Show success message with syntax validation status
103
- console.print("\n[cyan]Changes applied successfully.[/cyan]")
104
- if python_files:
105
- console.print(f"[green]✓ Python syntax validated for {len(python_files)} file(s)[/green]")
106
-
107
- # Run tests if specified
108
- if config.test_cmd:
109
- console.print(f"\n[cyan]Testing changes in preview directory:[/cyan] {config.test_cmd}")
110
- success, output, error = self.run_test_command(config.test_cmd)
111
- if output:
112
- console.print("\n[bold]Test Output:[/bold]")
113
- console.print(Panel(output, box=box.ROUNDED))
114
- if not success:
115
- console.print("\n[red bold]Tests failed in preview.[/red bold]")
116
- if error:
117
- console.print(Panel(error, title="Error", border_style="red"))
118
- return False, modified_files
119
-
120
- return True, modified_files
121
-
122
- def apply_single_change(self, change: FileChange, debug: bool) -> Tuple[bool, Optional[str]]:
123
- """Apply a single file change to preview directory"""
124
- path = self.preview_dir / change.name # Changed back from path to name
125
-
126
- # Handle file operations first
127
- if change.operation != ChangeOperation.MODIFY_FILE:
128
- return self.file_applier.apply_file_operation(change)
129
-
130
- # Handle text modifications
131
- if not path.exists():
132
- original_path = Path(change.name) # Changed back from path to name
133
- if not original_path.exists():
134
- return False, f"Original file not found: {original_path}"
135
- if self.console:
136
- self.console.print(f"[dim]Copying {original_path} to preview directory {path}[/dim]")
137
- path.write_text(original_path.read_text())
138
-
139
- current_content = path.read_text()
140
- success, modified_content, error = self.text_applier.apply_modifications(
141
- current_content,
142
- change.text_changes,
143
- path,
144
- debug
145
- )
146
-
147
- if not success:
148
- return False, error
149
-
150
- path.write_text(modified_content)
151
- return True, None
152
-
153
- def apply_to_workspace_dir(self, changes: List[FileChange], debug: bool = None) -> bool:
154
- """Apply changes from preview to working directory"""
155
- debug = debug if debug is not None else self.debug
156
- return apply_to_workspace_dir_impl(changes, self.preview_dir, Console())
@@ -1,247 +0,0 @@
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
@@ -1,58 +0,0 @@
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 DELETED
@@ -1,124 +0,0 @@
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()
janito/change/history.py DELETED
@@ -1,44 +0,0 @@
1
- from pathlib import Path
2
- from datetime import datetime
3
- from typing import Optional, Tuple
4
- from janito.config import config
5
-
6
- # Set fixed timestamp when module is loaded
7
- APP_TIMESTAMP = datetime.now().strftime("%Y%m%d_%H%M%S")
8
-
9
- def get_history_path() -> Path:
10
- """Create and return the history directory path"""
11
- history_dir = config.workspace_dir / '.janito' / 'change_history'
12
- history_dir.mkdir(parents=True, exist_ok=True)
13
- return history_dir
14
-
15
- def determine_history_file_type(filepath: Path) -> str:
16
- """Determine the type of saved file based on its name"""
17
- name = filepath.name.lower()
18
- if '_changes_failed' in name:
19
- return 'changes_failed'
20
- elif 'changes' in name:
21
- return 'changes'
22
- elif 'selected' in name:
23
- return 'selected'
24
- elif 'analysis' in name:
25
- return 'analysis'
26
- elif 'response' in name:
27
- return 'response'
28
- return 'unknown'
29
-
30
- def save_changes_to_history(content: str, request: str) -> Path:
31
- """Save change content to history folder with timestamp and request info"""
32
- history_dir = get_history_path()
33
-
34
- # Create history entry with request and changes
35
- filename = f"{APP_TIMESTAMP}_changes.txt"
36
- history_file = history_dir / filename
37
- history_content = f"""Request: {request}
38
- Timestamp: {APP_TIMESTAMP}
39
-
40
- Changes:
41
- {content}
42
- """
43
- history_file.write_text(history_content)
44
- return history_file