janito 0.4.0__py3-none-any.whl → 0.5.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 (49) hide show
  1. janito/__init__.py +48 -1
  2. janito/__main__.py +29 -334
  3. janito/agents/__init__.py +22 -0
  4. janito/agents/agent.py +21 -0
  5. janito/{claude.py → agents/claudeai.py} +10 -5
  6. janito/agents/openai.py +53 -0
  7. janito/agents/test.py +34 -0
  8. janito/analysis/__init__.py +33 -0
  9. janito/analysis/display.py +149 -0
  10. janito/analysis/options.py +112 -0
  11. janito/analysis/prompts.py +75 -0
  12. janito/change/__init__.py +19 -0
  13. janito/change/applier.py +269 -0
  14. janito/{contentchange.py → change/content.py} +5 -27
  15. janito/change/indentation.py +33 -0
  16. janito/change/position.py +169 -0
  17. janito/changehistory.py +46 -0
  18. janito/changeviewer/__init__.py +12 -0
  19. janito/changeviewer/diff.py +28 -0
  20. janito/changeviewer/panels.py +268 -0
  21. janito/changeviewer/styling.py +59 -0
  22. janito/changeviewer/themes.py +57 -0
  23. janito/cli/__init__.py +2 -0
  24. janito/cli/commands.py +53 -0
  25. janito/cli/functions.py +286 -0
  26. janito/cli/registry.py +26 -0
  27. janito/common.py +9 -9
  28. janito/console/__init__.py +3 -0
  29. janito/console/commands.py +112 -0
  30. janito/console/core.py +62 -0
  31. janito/console/display.py +157 -0
  32. janito/fileparser.py +292 -83
  33. janito/prompts.py +21 -6
  34. janito/qa.py +7 -5
  35. janito/review.py +13 -0
  36. janito/scan.py +44 -5
  37. janito/tests/test_fileparser.py +26 -0
  38. janito-0.5.0.dist-info/METADATA +146 -0
  39. janito-0.5.0.dist-info/RECORD +45 -0
  40. janito/analysis.py +0 -281
  41. janito/changeapplier.py +0 -436
  42. janito/changeviewer.py +0 -350
  43. janito/console.py +0 -330
  44. janito-0.4.0.dist-info/METADATA +0 -164
  45. janito-0.4.0.dist-info/RECORD +0 -21
  46. /janito/{contextparser.py → _contextparser.py} +0 -0
  47. {janito-0.4.0.dist-info → janito-0.5.0.dist-info}/WHEEL +0 -0
  48. {janito-0.4.0.dist-info → janito-0.5.0.dist-info}/entry_points.txt +0 -0
  49. {janito-0.4.0.dist-info → janito-0.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,157 @@
1
+ from pathlib import Path
2
+ import shutil
3
+ from prompt_toolkit.completion import WordCompleter
4
+ from prompt_toolkit.formatted_text import HTML
5
+ from rich.console import Console
6
+ from rich.panel import Panel
7
+ from rich.table import Table
8
+ from rich.layout import Layout
9
+ from importlib.metadata import version
10
+
11
+ def create_completer(workdir: Path) -> WordCompleter:
12
+ """Create command completer with common commands and paths"""
13
+ commands = [
14
+ 'ask', 'request', 'help', 'exit', 'quit',
15
+ '--raw', '--verbose', '--debug', '--test'
16
+ ]
17
+ return WordCompleter(commands, ignore_case=True)
18
+
19
+ def format_prompt(workdir: Path) -> HTML:
20
+ """Format the prompt with current directory"""
21
+ cwd = workdir.name
22
+ return HTML(f'<ansigreen>janito</ansigreen> <ansiblue>{cwd}</ansiblue>> ')
23
+
24
+ def display_help() -> None:
25
+ """Display available commands, options and their descriptions"""
26
+ console = Console()
27
+
28
+ layout = Layout()
29
+ layout.split_column(
30
+ Layout(name="header"),
31
+ Layout(name="commands"),
32
+ Layout(name="options"),
33
+ Layout(name="examples")
34
+ )
35
+
36
+ # Header
37
+ header_table = Table(box=None, show_header=False)
38
+ header_table.add_row("[bold cyan]Janito Console Help[/bold cyan]")
39
+ header_table.add_row("[dim]Your AI-powered software development buddy[/dim]")
40
+
41
+ # Commands table
42
+ commands_table = Table(title="Available Commands", box=None)
43
+ commands_table.add_column("Command", style="cyan", width=20)
44
+ commands_table.add_column("Description", style="white")
45
+
46
+ commands_table.add_row(
47
+ "/ask <text> (/a)",
48
+ "Ask a question about the codebase without making changes"
49
+ )
50
+ commands_table.add_row(
51
+ "<text> or /request <text> (/r)",
52
+ "Request code modifications or improvements"
53
+ )
54
+ commands_table.add_row(
55
+ "/help (/h)",
56
+ "Display this help message"
57
+ )
58
+ commands_table.add_row(
59
+ "/quit or /exit (/q)",
60
+ "Exit the console session"
61
+ )
62
+
63
+ # Options table
64
+ options_table = Table(title="Common Options", box=None)
65
+ options_table.add_column("Option", style="cyan", width=20)
66
+ options_table.add_column("Description", style="white")
67
+
68
+ options_table.add_row(
69
+ "--raw",
70
+ "Display raw response without formatting"
71
+ )
72
+ options_table.add_row(
73
+ "--verbose",
74
+ "Show additional information during execution"
75
+ )
76
+ options_table.add_row(
77
+ "--debug",
78
+ "Display detailed debug information"
79
+ )
80
+ options_table.add_row(
81
+ "--test <cmd>",
82
+ "Run specified test command before applying changes"
83
+ )
84
+
85
+ # Examples panel
86
+ examples = Panel(
87
+ "\n".join([
88
+ "[dim]Basic Commands:[/dim]",
89
+ " ask how does the error handling work?",
90
+ " request add input validation to user functions",
91
+ "",
92
+ "[dim]Using Options:[/dim]",
93
+ " request update tests --verbose",
94
+ " ask explain auth flow --raw",
95
+ " request optimize code --test 'pytest'",
96
+ "",
97
+ "[dim]Complex Examples:[/dim]",
98
+ " request refactor login function --verbose --test 'python -m unittest'",
99
+ " ask code structure --raw --debug"
100
+ ]),
101
+ title="Examples",
102
+ border_style="blue"
103
+ )
104
+
105
+ # Update layout
106
+ layout["header"].update(header_table)
107
+ layout["commands"].update(commands_table)
108
+ layout["options"].update(options_table)
109
+ layout["examples"].update(examples)
110
+
111
+ console.print(layout)
112
+
113
+ def display_welcome(workdir: Path) -> None:
114
+ """Display welcome message and console information"""
115
+ console = Console()
116
+ try:
117
+ ver = version("janito")
118
+ except:
119
+ ver = "dev"
120
+
121
+ term_width = shutil.get_terminal_size().columns
122
+
123
+ COLORS = {
124
+ 'primary': '#729FCF', # Soft blue for primary elements
125
+ 'secondary': '#8AE234', # Bright green for actions/success
126
+ 'accent': '#AD7FA8', # Purple for accents
127
+ 'muted': '#7F9F7F', # Muted green for less important text
128
+ }
129
+
130
+ welcome_text = (
131
+ f"[bold {COLORS['primary']}]Welcome to Janito v{ver}[/bold {COLORS['primary']}]\n"
132
+ f"[{COLORS['muted']}]Your AI-Powered Software Development Buddy[/{COLORS['muted']}]\n\n"
133
+ f"[{COLORS['accent']}]Keyboard Shortcuts:[/{COLORS['accent']}]\n"
134
+ "• ↑↓ : Navigate command history\n"
135
+ "• Tab : Complete commands and paths\n"
136
+ "• Ctrl+D : Exit console\n"
137
+ "• Ctrl+C : Cancel current operation\n\n"
138
+ f"[{COLORS['accent']}]Available Commands:[/{COLORS['accent']}]\n"
139
+ "• /ask (or /a) : Ask questions about code\n"
140
+ "• /request (or /r) : Request code changes\n"
141
+ "• /help (or /h) : Show detailed help\n"
142
+ "• /quit (or /q) : Exit console\n\n"
143
+ f"[{COLORS['secondary']}]Current Version:[/{COLORS['secondary']}] v{ver}\n"
144
+ f"[{COLORS['muted']}]Working Directory:[/{COLORS['muted']}] {workdir.absolute()}"
145
+ )
146
+
147
+ welcome_panel = Panel(
148
+ welcome_text,
149
+ width=min(80, term_width - 4),
150
+ border_style="blue",
151
+ title="Janito Console",
152
+ subtitle="Press Tab for completions"
153
+ )
154
+
155
+ console.print("\n")
156
+ console.print(welcome_panel)
157
+ console.print("\n[cyan]How can I help you with your code today?[/cyan]\n")
janito/fileparser.py CHANGED
@@ -1,20 +1,57 @@
1
1
  from pathlib import Path
2
- from typing import Dict, Tuple, List, Optional
2
+ from typing import Dict, Tuple, List, Optional, Union
3
3
  from dataclasses import dataclass
4
4
  import re
5
5
  import ast
6
- import sys # Add this import
6
+ import sys
7
+ import os # Add this import
7
8
  from rich.console import Console
8
- from rich.panel import Panel # Add this import
9
- from janito.config import config # Add this import
9
+ from rich.panel import Panel
10
+ from janito.config import config
11
+
12
+ def validate_file_path(filepath: Path) -> Tuple[bool, str]:
13
+ """
14
+ Validate that the file path exists and is readable
15
+
16
+ Args:
17
+ filepath: Path object to validate
18
+
19
+ Returns:
20
+ Tuple[bool, str]: (is_valid, error_message)
21
+ """
22
+ if not filepath.exists():
23
+ return False, f"File does not exist: {filepath}"
24
+ if not os.access(filepath, os.R_OK):
25
+ return False, f"File is not readable: {filepath}"
26
+ return True, ""
27
+
28
+ def validate_file_content(content: str) -> Tuple[bool, str]:
29
+ """
30
+ Validate that the content is not empty and contains valid text
31
+
32
+ Args:
33
+ content: String content to validate
34
+
35
+ Returns:
36
+ Tuple[bool, str]: (is_valid, error_message)
37
+ """
38
+ if not content:
39
+ return False, "Content cannot be empty"
40
+ if not isinstance(content, str):
41
+ return False, "Content must be a string"
42
+ return True, ""
10
43
 
11
44
  @dataclass
12
45
  class FileChange:
13
- """Represents a file change with search/replace, search/delete or create instructions"""
46
+ """Represents a file change with search/replace, search/delete, create or replace instructions"""
47
+ path: Path
14
48
  description: str
15
49
  is_new_file: bool
16
- content: str = "" # For new files
50
+ content: str = "" # For new files or replace operations
51
+ original_content: str = "" # Original content for file replacements
17
52
  search_blocks: List[Tuple[str, Optional[str], Optional[str]]] = None # (search, replace, description)
53
+ replace_file: bool = False # Flag for complete file replacement
54
+ remove_file: bool = False # Flag for file deletion
18
55
 
19
56
  def add_search_block(self, search: str, replace: Optional[str], description: Optional[str] = None) -> None:
20
57
  """Add a search/replace or search/delete block with optional description"""
@@ -22,104 +59,276 @@ class FileChange:
22
59
  self.search_blocks = []
23
60
  self.search_blocks.append((search, replace, description))
24
61
 
62
+ @dataclass
63
+ class FileBlock:
64
+ """Raw file block data extracted from response"""
65
+ uuid: str
66
+ filepath: str
67
+ action: str
68
+ description: str
69
+ content: str
70
+
25
71
  def validate_python_syntax(content: str, filepath: Path) -> Tuple[bool, str]:
26
72
  """Validate Python syntax and return (is_valid, error_message)"""
27
73
  try:
28
74
  ast.parse(content)
29
75
  console = Console()
30
- console.print(f"[green]✓ Python syntax validation passed:[/green] {filepath.absolute()}")
76
+ try:
77
+ rel_path = filepath.relative_to(Path.cwd())
78
+ display_path = f"./{rel_path}"
79
+ except ValueError:
80
+ display_path = str(filepath)
81
+ console.print(f"[green]✓ Python syntax validation passed:[/green] {display_path}")
31
82
  return True, ""
32
83
  except SyntaxError as e:
33
84
  error_msg = f"Line {e.lineno}: {e.msg}"
34
85
  console = Console()
35
- console.print(f"[red]✗ Python syntax validation failed:[/red] {filepath.absolute()}")
86
+ try:
87
+ rel_path = filepath.relative_to(Path.cwd())
88
+ display_path = f"./{rel_path}"
89
+ except ValueError:
90
+ display_path = str(filepath)
91
+ console.print(f"[red]✗ Python syntax validation failed:[/red] {display_path}")
36
92
  console.print(f"[red] {error_msg}[/red]")
37
93
  return False, error_msg
38
94
 
95
+ def count_lines(text: str) -> int:
96
+ """Count the number of lines in a text block."""
97
+ return len(text.splitlines())
39
98
 
40
- def parse_block_changes(response_text: str) -> Dict[Path, FileChange]:
41
- """Parse file changes from response blocks"""
42
- changes = {}
99
+ def extract_file_blocks(response_text: str) -> List[Tuple[str, str, str, str]]:
100
+ """Extract file blocks from response text and return list of (uuid, filepath, action, description, content)"""
101
+ file_blocks = []
43
102
  console = Console()
44
- # Match file blocks with UUID
45
- file_pattern = r'## ([a-f0-9]{8}) file (.*?) (modify|create) "(.*?)" ##\n?(.*?)## \1 file end ##'
46
103
 
47
- for match in re.finditer(file_pattern, response_text, re.DOTALL):
48
- uuid, filepath, action, description, content = match.groups()
49
- path = Path(filepath.strip())
104
+ # Find file blocks with improved quoted description handling
105
+ file_start_pattern = r'## ([a-f0-9]{8}) file (.*?) (modify|create|remove|replace)(?:\s+"([^"]*?)")?\s*##'
106
+ # Find the first UUID to check for duplicates
107
+ first_match = re.search(file_start_pattern, response_text)
108
+ if not first_match:
109
+ console.print("[red]FATAL ERROR: No file blocks found in response[/red]")
110
+ sys.exit(1)
111
+ fist_uuid = first_match.group(1)
112
+
113
+ # Find all file blocks
114
+ for match in re.finditer(file_start_pattern, response_text):
115
+ block_uuid, filepath, action, description = match.groups()
50
116
 
51
- if action == 'create':
52
- changes[path] = FileChange(
53
- description=description,
54
- is_new_file=True,
55
- content=content[1:] if content.startswith('\n') else content,
56
- search_blocks=[]
57
- )
58
- continue
117
+ # Show debug info for create actions
118
+ if config.debug and action == 'create':
119
+ console.print(f"[green]Found new file block:[/green] {filepath}")
59
120
 
60
- # For modifications, find all search/replace and search/delete blocks
61
- search_blocks = []
62
- block_patterns = [
63
- # Match search/replace blocks with description - updated pattern
64
- (r'## ' + re.escape(uuid) + r' search/replace "(.*?)" ##\n?(.*?)## ' +
65
- re.escape(uuid) + r' replace with ##\n?(.*?)(?=## ' + re.escape(uuid) + r'|$)', False),
66
- # Match search/delete blocks with description
67
- (r'## ' + re.escape(uuid) + r' search/delete "(.*?)" ##\n?(.*?)(?=## ' + re.escape(uuid) + r'|$)', True)
68
- ]
121
+ # Now find the complete block
122
+ full_block_pattern = (
123
+ f"## {block_uuid} file.*?##\n?"
124
+ f"(.*?)"
125
+ f"## {block_uuid} file end ##"
126
+ )
127
+
128
+ block_match = re.search(full_block_pattern, response_text[match.start():], re.DOTALL)
129
+ if not block_match:
130
+ # Show context around the incomplete block
131
+ context_start = max(0, match.start() - 100)
132
+ context_end = min(len(response_text), match.start() + 100)
133
+ context = response_text[context_start:context_end]
134
+
135
+ console.print(f"\n[red]FATAL ERROR: Found file start but no matching end for {filepath}[/red]")
136
+ console.print("[red]Context around incomplete block:[/red]")
137
+ console.print(Panel(context, title="Context", border_style="red"))
138
+ sys.exit(1)
139
+
140
+ content = block_match.group(1)
141
+ # For new files, preserve the first newline if it exists
142
+ if action == 'create' and content.startswith('\n'):
143
+ content = content[1:]
144
+
145
+ file_blocks.append((block_uuid, filepath.strip(), action, description or "", content))
69
146
 
70
147
  if config.debug:
71
- console.print("\n[blue]Updated regex patterns:[/blue]")
72
- for pattern, is_delete in block_patterns:
73
- console.print(Panel(pattern, title="Search/Replace Pattern" if not is_delete else "Search/Delete Pattern", border_style="blue"))
148
+ action_type = "Creating new file" if action == "create" else "Modifying file"
149
+ console.print(f"[green]Found valid block:[/green] {action_type} {filepath}")
150
+ console.print(f"[blue]Content length:[/blue] {len(content)} chars")
151
+
152
+ return file_blocks
153
+
154
+ def extract_modification_blocks(uuid: str, content: str) -> List[Tuple[str, str, Optional[str], str]]:
155
+ """Extract all modification blocks from content, returns list of (type, description, replace, search)"""
156
+ blocks = []
157
+ console = Console()
158
+
159
+ # Find all modification blocks in sequence
160
+ block_start = r'## ' + re.escape(uuid) + r' (search/replace|search/delete) "(.*?)" ##\n'
161
+ current_pos = 0
162
+
163
+ while True:
164
+ # Find next block start
165
+ start_match = re.search(block_start, content[current_pos:], re.DOTALL)
166
+ if not start_match:
167
+ break
168
+
169
+ block_type, description = start_match.groups()
170
+ block_start_pos = current_pos + start_match.end()
171
+
172
+ # Find block end based on type
173
+ if block_type == 'search/replace':
174
+ replace_marker = f"## {uuid} replace with ##\n"
175
+ end_marker = f"## {uuid}"
176
+
177
+ # Find replace marker
178
+ replace_pos = content.find(replace_marker, block_start_pos)
179
+ if replace_pos == -1:
180
+ # Show context around the incomplete block
181
+ context_start = max(0, current_pos + start_match.start() - 100)
182
+ context_end = min(len(content), current_pos + start_match.end() + 100)
183
+ context = content[context_start:context_end]
74
184
 
75
- for pattern, is_delete in block_patterns:
76
- if config.debug:
77
- console.print(f"\n[blue]Looking for pattern:[/blue]")
78
- console.print(Panel(pattern, title="Pattern", border_style="blue"))
79
- console.print(f"\n[blue]In content:[/blue]")
80
- console.print(Panel(content, title="Content", border_style="blue"))
185
+ console.print(f"\n[red]FATAL ERROR: Missing 'replace with' marker for block:[/red] {description}")
186
+ console.print("[red]Context around incomplete block:[/red]")
187
+ console.print(Panel(context, title="Context", border_style="red"))
188
+ sys.exit(1)
81
189
 
82
- for block_match in re.finditer(pattern, content, re.DOTALL):
83
- if is_delete:
84
- description, search = block_match.groups()
85
- search = search.rstrip('\n') + '\n' # Ensure single trailing newline
86
- replace = None
87
- else:
88
- description, search, replace = block_match.groups()
89
- search = search.rstrip('\n') + '\n' # Ensure single trailing newline
90
- replace = (replace.rstrip('\n') + '\n') if replace else None
91
-
92
- # Abort parsing if replace content is empty
93
- if not is_delete and (replace is None or replace.strip() == ''):
94
- console.print(f"\n[red]Error: Empty replace content found![/red]")
95
- console.print(f"[red]File:[/red] {filepath}")
96
- console.print(f"[red]Description:[/red] {description}")
97
- console.print("[yellow]Search block:[/yellow]")
98
- console.print(Panel(search, title="Search Content", border_style="yellow"))
99
- console.print("[red]Replace block is empty or contains only whitespace![/red]")
100
- console.print("[red]Aborting due to empty replace content.[/red]")
101
- sys.exit(1)
102
-
103
- # Enhanced debug info
104
- if config.debug or (not is_delete and (replace is None or replace.strip() == '')):
105
- console.print(f"\n[yellow]Search/Replace block analysis:[/yellow]")
106
- console.print(f"[yellow]File:[/yellow] {filepath}")
107
- console.print(f"[yellow]Description:[/yellow] {description}")
108
- console.print("[yellow]Search block:[/yellow]")
109
- console.print(Panel(search, title="Search Content", border_style="yellow"))
110
- console.print("[yellow]Replace block:[/yellow]")
111
- console.print(Panel(replace if replace else "<empty>", title="Replace Content", border_style="yellow"))
112
- console.print("\n[blue]Match groups:[/blue]")
113
- for i, group in enumerate(block_match.groups()):
114
- console.print(Panel(str(group), title=f"Group {i}", border_style="blue"))
115
-
116
- search_blocks.append((search, replace, description))
190
+ # Get search content
191
+ search = content[block_start_pos:replace_pos]
192
+
193
+ # Find end of replacement
194
+ replace_start = replace_pos + len(replace_marker)
195
+ next_block = content.find(end_marker, replace_start)
196
+ if next_block == -1:
197
+ # Use rest of content if no end marker found
198
+ replace = content[replace_start:]
199
+ else:
200
+ replace = content[replace_start:next_block]
201
+
202
+ blocks.append(('replace', description, replace, search))
203
+ current_pos = next_block if next_block != -1 else len(content)
204
+
205
+ else: # search/delete
206
+ # Find either next block start or end of content
207
+ next_block_marker = content.find(f"## {uuid}", block_start_pos)
208
+ if next_block_marker == -1:
209
+ # Use rest of content if no more blocks
210
+ search = content[block_start_pos:]
211
+ else:
212
+ search = content[block_start_pos:next_block_marker]
213
+
214
+ blocks.append(('delete', description, None, search))
215
+ current_pos = next_block_marker if next_block_marker != -1 else len(content)
216
+
217
+ if config.debug:
218
+ console.print(f"[green]Found {block_type} block:[/green] {description}")
219
+
220
+ return blocks
221
+
222
+ def handle_file_block(block: FileBlock) -> FileChange:
223
+ """Process a single file block and return a FileChange object"""
224
+ console = Console()
225
+
226
+ # Handle file removal action
227
+ if block.action == 'remove':
228
+ return FileChange(
229
+ path=Path(block.filepath),
230
+ description=block.description,
231
+ is_new_file=False,
232
+ content="",
233
+ search_blocks=[],
234
+ remove_file=True
235
+ )
236
+
237
+ # Validate file path
238
+ path = Path(block.filepath)
239
+ is_valid, error = validate_file_path(path)
240
+ if block.action == 'replace' and not is_valid:
241
+ console.print(f"[red]Invalid file path for replacement:[/red] {error}")
242
+ sys.exit(1)
243
+
244
+ # Handle file replacement action
245
+ if block.action == 'replace':
246
+ # Read original content if file exists
247
+ original_content = ""
248
+ if path.exists():
249
+ try:
250
+ original_content = path.read_text()
251
+ except Exception as e:
252
+ console.print(f"[red]Error reading original file for replacement:[/red] {e}")
253
+ sys.exit(1)
254
+
255
+ return FileChange(
256
+ path=Path(block.filepath),
257
+ description=block.description,
258
+ is_new_file=False,
259
+ content=block.content.lstrip('\n'),
260
+ original_content=original_content,
261
+ search_blocks=[],
262
+ replace_file=True
263
+ )
264
+
265
+ # Validate content for new files
266
+ if block.action == 'create':
267
+ is_valid, error = validate_file_content(block.content)
268
+ if not is_valid:
269
+ console.print(f"[red]Invalid file content for {block.filepath}:[/red] {error}")
270
+ sys.exit(1)
271
+
272
+ if block.action == 'create':
273
+ return FileChange(
274
+ path=Path(block.filepath),
275
+ description=block.description,
276
+ is_new_file=True,
277
+ content=block.content[1:] if block.content.startswith('\n') else block.content,
278
+ search_blocks=[]
279
+ )
280
+
281
+ # Extract and process modification blocks
282
+ search_blocks = []
283
+ for block_type, description, replace, search in extract_modification_blocks(block.uuid, block.content):
284
+ # Ensure consistent line endings
285
+ search = search.rstrip('\n') + '\n'
286
+ if replace is not None:
287
+ replace = replace.rstrip('\n') + '\n'
288
+
289
+ if config.debug:
290
+ console.print(f"\n[cyan]Processing {block_type} block:[/cyan] {description}")
291
+ console.print(Panel(search, title="Search Content"))
292
+ if replace:
293
+ console.print(Panel(replace, title="Replace Content"))
294
+
295
+ search_blocks.append((search, replace, description))
296
+
297
+ return FileChange(
298
+ path=Path(block.filepath),
299
+ description=block.description,
300
+ is_new_file=False,
301
+ search_blocks=search_blocks
302
+ )
303
+
304
+ def parse_block_changes(response_text: str) -> List[FileChange]:
305
+ """Parse file changes from response blocks and return list of FileChange"""
306
+ changes = []
307
+ console = Console()
308
+
309
+ # First extract all file blocks
310
+ file_blocks = extract_file_blocks(response_text)
311
+
312
+ # Process each file block independently
313
+ for block_uuid, filepath, action, description, content in file_blocks:
314
+ path = Path(filepath)
117
315
 
118
- # Add debug info if no blocks were found
119
- if config.debug and not search_blocks:
120
- console.print(f"\n[red]No search/replace blocks found for file:[/red] {filepath}")
121
- console.print("[red]Check if the content format matches the expected patterns[/red]")
316
+ file_block = FileBlock(
317
+ uuid=block_uuid,
318
+ filepath=filepath,
319
+ action=action,
320
+ description=description,
321
+ content=content
322
+ )
323
+
324
+ file_change = handle_file_block(file_block)
325
+ # For remove action, ensure remove_file flag is set
326
+ if action == 'remove':
327
+ file_change.remove_file = True
328
+ file_change.path = path
329
+ changes.append(file_change)
330
+
331
+ if config.debug:
332
+ console.print(f"\n[cyan]Processed {len(file_blocks)} file blocks[/cyan]")
122
333
 
123
- changes[path] = FileChange(description=description, is_new_file=False, search_blocks=search_blocks)
124
-
125
334
  return changes
janito/prompts.py CHANGED
@@ -20,12 +20,17 @@ Current files:
20
20
  </files>
21
21
 
22
22
  RULES:
23
- - When revmoing constants, ensure they are not used elsewhere
23
+ - When removing constants, ensure they are not used elsewhere
24
24
  - When adding new features to python files, add the necessary imports
25
25
  - Python imports should be inserted at the top of the file
26
+ - For complete file replacements, only use for existing files marked as modified
27
+ - File replacements must preserve the essential functionality
28
+ - When multiple changes affect the same code block, combine them into a single change
29
+ - if no changes are required answer only the reason in the format: <no_changes_required>reason for no changes<no_changes_required>
26
30
 
27
31
  Please provide the changes in this format:
28
32
 
33
+ For incremental changes:
29
34
  ## {uuid} file <filepath> modify "short file change description" ##
30
35
  ## {uuid} search/replace "short change description" ##
31
36
  <search_content>
@@ -33,19 +38,29 @@ Please provide the changes in this format:
33
38
  <replace_content>
34
39
  ## {uuid} file end ##
35
40
 
36
- Or to delete content:
41
+ For complete file replacement (only for existing modified files):
42
+ ## {uuid} file <filepath> replace "short file description" ##
43
+ <full_file_content>
44
+ ## {uuid} file end ##
45
+
46
+ For new files:
47
+ ## {uuid} file <filepath> create "short file description" ##
48
+ <full_file_content>
49
+ ## {uuid} file end ##
50
+
51
+ For content deletion:
37
52
  ## {uuid} file <filepath> modify ##
38
53
  ## {uuid} search/delete "short change description" ##
39
54
  <content_to_delete>
40
55
  ## {uuid} file end ##
41
56
 
42
- For new files:
43
- ## {uuid} file <filepath> create "short description" ##
44
- <full_file_content>
57
+ For file removal:
58
+ ## {uuid} file <filepath> remove "short removal reason" ##
45
59
  ## {uuid} file end ##
46
60
 
47
61
  RULES:
48
- 1. search_content MUST preserve the original identation/whitespace
62
+ 1. search_content MUST preserve the original indentation/whitespace
63
+ 2. file replacement can only be used for existing files marked as
49
64
  """
50
65
 
51
66
  def build_selected_option_prompt(option_text: str, request: str, files_content: str = "") -> str:
janito/qa.py CHANGED
@@ -4,10 +4,9 @@ from rich.panel import Panel
4
4
  from rich.syntax import Syntax
5
5
  from rich.table import Table
6
6
  from rich.rule import Rule
7
- from janito.claude import ClaudeAPIAgent
7
+ from janito.agents import AIAgent
8
8
  from janito.common import progress_send_message
9
- from typing import Dict, List
10
- import re
9
+ from janito.scan import show_content_stats
11
10
 
12
11
  QA_PROMPT = """Please provide a clear and concise answer to the following question about the codebase:
13
12
 
@@ -22,13 +21,16 @@ Focus on providing factual information and explanations. Do not suggest code cha
22
21
  Format your response using markdown with appropriate headers and code blocks.
23
22
  """
24
23
 
25
- def ask_question(question: str, files_content: str, claude: ClaudeAPIAgent) -> str:
24
+ def ask_question(question: str, files_content: str) -> str:
26
25
  """Process a question about the codebase and return the answer"""
26
+ # Show content stats before processing
27
+ show_content_stats(files_content)
28
+
27
29
  prompt = QA_PROMPT.format(
28
30
  question=question,
29
31
  files_content=files_content
30
32
  )
31
- return progress_send_message(claude, prompt)
33
+ return progress_send_message(prompt)
32
34
 
33
35
 
34
36
  def display_answer(answer: str, raw: bool = False) -> None: