janito 0.3.0__py3-none-any.whl → 0.4.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 CHANGED
@@ -1,13 +1,12 @@
1
+ import sys
1
2
  import typer
2
3
  from typing import Optional, Dict, Any, List
3
4
  from pathlib import Path
4
5
  from janito.claude import ClaudeAPIAgent
5
6
  import shutil
6
7
  from janito.prompts import (
7
- build_request_analisys_prompt,
8
8
  build_selected_option_prompt,
9
- SYSTEM_PROMPT,
10
- parse_options
9
+ SYSTEM_PROMPT,
11
10
  )
12
11
  from rich.console import Console
13
12
  from rich.markdown import Markdown
@@ -34,54 +33,51 @@ from janito.scan import collect_files_content, is_dir_empty, preview_scan
34
33
  from janito.qa import ask_question, display_answer
35
34
  from rich.prompt import Prompt, Confirm
36
35
  from janito.config import config
37
- from importlib.metadata import version
36
+ from janito.version import get_version
37
+ from janito.common import progress_send_message
38
+ from janito.analysis import format_analysis, build_request_analysis_prompt, parse_analysis_options, get_history_file_type, AnalysisOption
38
39
 
39
- def get_version() -> str:
40
- try:
41
- return version("janito")
42
- except:
43
- return "dev"
44
-
45
- def format_analysis(analysis: str, raw: bool = False, claude: Optional[ClaudeAPIAgent] = None) -> None:
46
- """Format and display the analysis output"""
47
- console = Console()
48
- if raw and claude:
49
- console.print("\n=== Message History ===")
50
- for role, content in claude.messages_history:
51
- console.print(f"\n[bold cyan]{role.upper()}:[/bold cyan]")
52
- console.print(content)
53
- console.print("\n=== End Message History ===\n")
54
- else:
55
- md = Markdown(analysis)
56
- console.print(md)
57
40
 
58
41
  def prompt_user(message: str, choices: List[str] = None) -> str:
59
- """Display a prominent user prompt with optional choices"""
42
+ """Display a prominent user prompt with optional choices using consistent colors"""
60
43
  console = Console()
44
+
45
+ # Define consistent colors
46
+ COLORS = {
47
+ 'primary': '#729FCF', # Soft blue for primary elements
48
+ 'secondary': '#8AE234', # Bright green for actions/success
49
+ 'accent': '#AD7FA8', # Purple for accents
50
+ 'muted': '#7F9F7F', # Muted green for less important text
51
+ }
52
+
61
53
  console.print()
62
- console.print(Rule(" User Input Required ", style="bold cyan"))
54
+ console.print(Rule(" User Input Required ", style=f"bold {COLORS['primary']}"))
63
55
 
64
56
  if choices:
65
- choice_text = f"[cyan]Options: {', '.join(choices)}[/cyan]"
66
- console.print(Panel(choice_text, box=box.ROUNDED))
57
+ choice_text = f"[{COLORS['accent']}]Options: {', '.join(choices)}[/{COLORS['accent']}]"
58
+ console.print(Panel(choice_text, box=box.ROUNDED, border_style=COLORS['primary']))
67
59
 
68
- return Prompt.ask(f"[bold cyan]> {message}[/bold cyan]")
60
+ return Prompt.ask(f"[bold {COLORS['secondary']}]> {message}[/bold {COLORS['secondary']}]")
69
61
 
70
- def get_option_selection() -> int:
71
- """Get user input for option selection"""
62
+ def validate_option_letter(letter: str, options: dict) -> bool:
63
+ """Validate if the given letter is a valid option or 'M' for modify"""
64
+ return letter.upper() in options or letter.upper() == 'M'
65
+
66
+ def get_option_selection() -> str:
67
+ """Get user input for option selection with modify option"""
68
+ console = Console()
69
+ console.print("\n[cyan]Enter option letter or 'M' to modify request[/cyan]")
72
70
  while True:
73
- try:
74
- option = int(prompt_user("Select option number"))
75
- return option
76
- except ValueError:
77
- console = Console()
78
- console.print("[red]Please enter a valid number[/red]")
71
+ letter = prompt_user("Select option").strip().upper()
72
+ if letter == 'M' or (letter.isalpha() and len(letter) == 1):
73
+ return letter
74
+ console.print("[red]Please enter a valid letter or 'M'[/red]")
79
75
 
80
- def get_history_path(workdir: Path) -> Path:
81
- """Create and return the history directory path"""
82
- history_dir = workdir / '.janito' / 'history'
83
- history_dir.mkdir(parents=True, exist_ok=True)
84
- return history_dir
76
+ def get_changes_history_path(workdir: Path) -> Path:
77
+ """Create and return the changes history directory path"""
78
+ changes_history_dir = workdir / '.janito' / 'changes_history'
79
+ changes_history_dir.mkdir(parents=True, exist_ok=True)
80
+ return changes_history_dir
85
81
 
86
82
  def get_timestamp() -> str:
87
83
  """Get current UTC timestamp in YMD_HMS format with leading zeros"""
@@ -95,46 +91,148 @@ def save_prompt_to_file(prompt: str) -> Path:
95
91
  return temp_path
96
92
 
97
93
  def save_to_file(content: str, prefix: str, workdir: Path) -> Path:
98
- """Save content to a timestamped file in history directory"""
99
- history_dir = get_history_path(workdir)
94
+ """Save content to a timestamped file in changes history directory"""
95
+ changes_history_dir = get_changes_history_path(workdir)
100
96
  timestamp = get_timestamp()
101
97
  filename = f"{timestamp}_{prefix}.txt"
102
- file_path = history_dir / filename
98
+ file_path = changes_history_dir / filename
103
99
  file_path.write_text(content)
104
100
  return file_path
105
101
 
102
+ def modify_request(request: str) -> str:
103
+ """Display current request and get modified version with improved formatting"""
104
+ console = Console()
105
+
106
+ # Display current request in a panel with clear formatting
107
+ console.print("\n[bold cyan]Current Request:[/bold cyan]")
108
+ console.print(Panel(
109
+ Text(request, style="white"),
110
+ border_style="blue",
111
+ title="Previous Request",
112
+ padding=(1, 2)
113
+ ))
114
+
115
+ # Get modified request with clear prompt
116
+ console.print("\n[bold cyan]Enter modified request below:[/bold cyan]")
117
+ console.print("[dim](Press Enter to submit, Ctrl+C to cancel)[/dim]")
118
+ try:
119
+ new_request = prompt_user("Modified request")
120
+ if not new_request.strip():
121
+ console.print("[yellow]No changes made, keeping original request[/yellow]")
122
+ return request
123
+ return new_request
124
+ except KeyboardInterrupt:
125
+ console.print("\n[yellow]Modification cancelled, keeping original request[/yellow]")
126
+ return request
127
+
128
+ def format_option_text(option: AnalysisOption) -> str:
129
+ """Format an AnalysisOption into a string representation"""
130
+ option_text = f"Option {option.letter}:\n"
131
+ option_text += f"Summary: {option.summary}\n\n"
132
+ option_text += "Description:\n"
133
+ for item in option.description_items:
134
+ option_text += f"- {item}\n"
135
+ option_text += "\nAffected files:\n"
136
+ for file in option.affected_files:
137
+ option_text += f"- {file}\n"
138
+ return option_text
139
+
106
140
  def handle_option_selection(claude: ClaudeAPIAgent, initial_response: str, request: str, raw: bool = False, workdir: Optional[Path] = None, include: Optional[List[Path]] = None) -> None:
107
141
  """Handle option selection and implementation details"""
108
- option = get_option_selection()
142
+ options = parse_analysis_options(initial_response)
143
+ if not options:
144
+ console = Console()
145
+ console.print("[red]No valid options found in the response[/red]")
146
+ return
147
+
148
+ while True:
149
+ option = get_option_selection()
150
+
151
+ if option == 'M':
152
+ # Use the new modify_request function for better UX
153
+ new_request = modify_request(request)
154
+ if new_request == request:
155
+ continue
156
+
157
+ # Rerun analysis with new request
158
+ paths_to_scan = [workdir] if workdir else []
159
+ if include:
160
+ paths_to_scan.extend(include)
161
+ files_content = collect_files_content(paths_to_scan, workdir) if paths_to_scan else ""
162
+
163
+ initial_prompt = build_request_analysis_prompt(files_content, new_request)
164
+ initial_response = progress_send_message(claude, initial_prompt)
165
+ save_to_file(initial_response, 'analysis', workdir)
166
+
167
+ format_analysis(initial_response, raw, claude)
168
+ options = parse_analysis_options(initial_response)
169
+ if not options:
170
+ console = Console()
171
+ console.print("[red]No valid options found in the response[/red]")
172
+ return
173
+ continue
174
+
175
+ if not validate_option_letter(option, options):
176
+ console = Console()
177
+ console.print(f"[red]Invalid option '{option}'. Valid options are: {', '.join(options.keys())} or 'M' to modify[/red]")
178
+ continue
179
+
180
+ break
181
+
109
182
  paths_to_scan = [workdir] if workdir else []
110
183
  if include:
111
184
  paths_to_scan.extend(include)
112
185
  files_content = collect_files_content(paths_to_scan, workdir) if paths_to_scan else ""
113
186
 
114
- selected_prompt = build_selected_option_prompt(option, request, initial_response, files_content)
187
+ # Format the selected option before building prompt
188
+ selected_option = options[option]
189
+ option_text = format_option_text(selected_option)
190
+
191
+ # Remove initial_response from the arguments
192
+ selected_prompt = build_selected_option_prompt(option_text, request, files_content)
115
193
  prompt_file = save_to_file(selected_prompt, 'selected', workdir)
116
194
  if config.verbose:
117
195
  print(f"\nSelected prompt saved to: {prompt_file}")
118
196
 
119
- selected_response = claude.send_message(selected_prompt)
197
+ selected_response = progress_send_message(claude, selected_prompt)
120
198
  changes_file = save_to_file(selected_response, 'changes', workdir)
199
+
121
200
  if config.verbose:
122
- print(f"\nChanges saved to: {changes_file}")
201
+ try:
202
+ rel_path = changes_file.relative_to(workdir)
203
+ print(f"\nChanges saved to: ./{rel_path}")
204
+ except ValueError:
205
+ print(f"\nChanges saved to: {changes_file}")
123
206
 
124
207
  changes = parse_block_changes(selected_response)
125
- preview_and_apply_changes(changes, workdir)
208
+ preview_and_apply_changes(changes, workdir, config.test_cmd)
126
209
 
127
210
  def replay_saved_file(filepath: Path, claude: ClaudeAPIAgent, workdir: Path, raw: bool = False) -> None:
128
211
  """Process a saved prompt file and display the response"""
129
212
  if not filepath.exists():
130
213
  raise FileNotFoundError(f"File {filepath} not found")
131
214
 
132
- file_type = get_file_type(filepath)
133
215
  content = filepath.read_text()
134
216
 
217
+ # Add debug output of file content
218
+ if config.debug:
219
+ console = Console()
220
+ console.print("\n[bold blue]Debug: File Content[/bold blue]")
221
+ console.print(Panel(
222
+ content,
223
+ title=f"Content of {filepath.name}",
224
+ border_style="blue",
225
+ padding=(1, 2)
226
+ ))
227
+ console.print()
228
+
229
+ file_type = get_history_file_type(filepath)
230
+
135
231
  if file_type == 'changes':
136
232
  changes = parse_block_changes(content)
137
- preview_and_apply_changes(changes, workdir)
233
+ success = preview_and_apply_changes(changes, workdir, config.test_cmd)
234
+ if not success:
235
+ raise typer.Exit(1)
138
236
  elif file_type == 'analysis':
139
237
  format_analysis(content, raw, claude)
140
238
  handle_option_selection(claude, content, content, raw, workdir)
@@ -144,14 +242,15 @@ def replay_saved_file(filepath: Path, claude: ClaudeAPIAgent, workdir: Path, raw
144
242
  console.print("\n=== Prompt Content ===")
145
243
  console.print(content)
146
244
  console.print("=== End Prompt Content ===\n")
147
- response = claude.send_message(content)
245
+
246
+ response = progress_send_message(claude, content)
148
247
  changes_file = save_to_file(response, 'changes_', workdir)
149
248
  print(f"\nChanges saved to: {changes_file}")
150
249
 
151
250
  changes = parse_block_changes(response)
152
- preview_and_apply_changes(preview_changes, workdir)
251
+ preview_and_apply_changes(changes, workdir, config.test_cmd)
153
252
  else:
154
- response = claude.send_message(content)
253
+ response = progress_send_message(claude, content)
155
254
  format_analysis(response, raw)
156
255
 
157
256
  def process_question(question: str, workdir: Path, include: List[Path], raw: bool, claude: ClaudeAPIAgent) -> None:
@@ -160,7 +259,6 @@ def process_question(question: str, workdir: Path, include: List[Path], raw: boo
160
259
  if include:
161
260
  paths_to_scan.extend(include)
162
261
  files_content = collect_files_content(paths_to_scan, workdir)
163
-
164
262
  answer = ask_question(question, files_content, claude)
165
263
  display_answer(answer, raw)
166
264
 
@@ -187,14 +285,15 @@ def typer_main(
187
285
  play: Optional[Path] = typer.Option(None, "--play", help="Replay a saved prompt file"),
188
286
  include: Optional[List[Path]] = typer.Option(None, "-i", "--include", help="Additional paths to include in analysis", exists=True),
189
287
  debug: bool = typer.Option(False, "--debug", help="Show debug information"),
190
- debug_line: Optional[int] = typer.Option(None, "--debug-line", help="Show debug information only for specific line number"),
191
288
  verbose: bool = typer.Option(False, "-v", "--verbose", help="Show verbose output"),
192
289
  scan: bool = typer.Option(False, "--scan", help="Preview files that would be analyzed"),
193
290
  version: bool = typer.Option(False, "--version", help="Show version and exit"),
291
+ test: Optional[str] = typer.Option(None, "-t", "--test", help="Test command to run before applying changes"),
194
292
  ) -> None:
195
293
  """
196
294
  Analyze files and provide modification instructions.
197
295
  """
296
+
198
297
  if version:
199
298
  console = Console()
200
299
  console.print(f"Janito v{get_version()}")
@@ -202,7 +301,7 @@ def typer_main(
202
301
 
203
302
  config.set_debug(debug)
204
303
  config.set_verbose(verbose)
205
- config.set_debug_line(debug_line)
304
+ config.set_test_cmd(test)
206
305
 
207
306
  claude = ClaudeAPIAgent(system_prompt=SYSTEM_PROMPT)
208
307
 
@@ -245,9 +344,9 @@ def typer_main(
245
344
  else:
246
345
  files_content = collect_files_content(paths_to_scan, workdir)
247
346
 
248
- initial_prompt = build_request_analisys_prompt(files_content, request)
249
- initial_response = claude.send_message(initial_prompt)
250
- analysis_file = save_to_file(initial_response, 'analysis', workdir)
347
+ initial_prompt = build_request_analysis_prompt(files_content, request)
348
+ initial_response = progress_send_message(claude, initial_prompt)
349
+ save_to_file(initial_response, 'analysis', workdir)
251
350
 
252
351
  format_analysis(initial_response, raw, claude)
253
352
 
janito/analysis.py ADDED
@@ -0,0 +1,281 @@
1
+ """Analysis display module for Janito.
2
+
3
+ This module handles the formatting and display of analysis results, option selection,
4
+ and related functionality for the Janito application.
5
+ """
6
+
7
+ from typing import Optional, Dict, List, Tuple
8
+ from pathlib import Path
9
+ from rich.console import Console
10
+ from rich.markdown import Markdown
11
+ from rich.panel import Panel
12
+ from rich.text import Text
13
+ from rich import box
14
+ from rich.columns import Columns
15
+ from rich.rule import Rule
16
+ from rich.prompt import Prompt
17
+ from janito.claude import ClaudeAPIAgent
18
+ from janito.scan import collect_files_content
19
+ from janito.common import progress_send_message
20
+ from janito.config import config
21
+ from dataclasses import dataclass
22
+ import re
23
+
24
+ MIN_PANEL_WIDTH = 40 # Minimum width for each panel
25
+
26
+ def get_history_file_type(filepath: Path) -> str:
27
+ """Determine the type of saved file based on its name"""
28
+ name = filepath.name.lower()
29
+ if 'changes' in name:
30
+ return 'changes'
31
+ elif 'selected' in name:
32
+ return 'selected'
33
+ elif 'analysis' in name:
34
+ return 'analysis'
35
+ elif 'response' in name:
36
+ return 'response'
37
+ return 'unknown'
38
+
39
+ @dataclass
40
+ class AnalysisOption:
41
+ letter: str
42
+ summary: str
43
+ affected_files: List[str]
44
+ description_items: List[str] # Changed from description to description_items
45
+
46
+ CHANGE_ANALYSIS_PROMPT = """
47
+ Current files:
48
+ <files>
49
+ {files_content}
50
+ </files>
51
+
52
+ Considering the above current files content, provide options for the requested change in the following format:
53
+
54
+ A. Keyword summary of the change
55
+ -----------------
56
+ Description:
57
+ - Detailed description of the change
58
+
59
+ Affected files:
60
+ - file1.py
61
+ - file2.py (new)
62
+ -----------------
63
+ END_OF_OPTIONS (mandatory marker)
64
+
65
+ RULES:
66
+ - do NOT provide the content of the files
67
+ - do NOT offer to implement the changes
68
+
69
+ Request:
70
+ {request}
71
+ """
72
+
73
+
74
+
75
+
76
+ def prompt_user(message: str, choices: List[str] = None) -> str:
77
+ """Display a prominent user prompt with optional choices"""
78
+ console = Console()
79
+ console.print()
80
+ console.print(Rule(" User Input Required ", style="bold cyan"))
81
+
82
+ if choices:
83
+ choice_text = f"[cyan]Options: {', '.join(choices)}[/cyan]"
84
+ console.print(Panel(choice_text, box=box.ROUNDED))
85
+
86
+ return Prompt.ask(f"[bold cyan]> {message}[/bold cyan]")
87
+
88
+ def validate_option_letter(letter: str, options: dict) -> bool:
89
+ """Validate if the given letter is a valid option or 'M' for modify"""
90
+ return letter.upper() in options or letter.upper() == 'M'
91
+
92
+ def get_option_selection() -> str:
93
+ """Get user input for option selection with modify option"""
94
+ console = Console()
95
+ console.print("\n[cyan]Enter option letter or 'M' to modify request[/cyan]")
96
+ while True:
97
+ letter = prompt_user("Select option").strip().upper()
98
+ if letter == 'M' or (letter.isalpha() and len(letter) == 1):
99
+ return letter
100
+ console.print("[red]Please enter a valid letter or 'M'[/red]")
101
+
102
+ def _display_options(options: Dict[str, AnalysisOption]) -> None:
103
+ """Display available options with left-aligned content and horizontally centered panels."""
104
+ console = Console()
105
+
106
+ # Display centered title using Rule
107
+ console.print()
108
+ console.print(Rule(" Available Options ", style="bold cyan", align="center"))
109
+ console.print()
110
+
111
+ # Calculate optimal width based on terminal
112
+ term_width = console.width or 100
113
+ panel_width = max(MIN_PANEL_WIDTH, (term_width // 2) - 10) # Width for two columns
114
+
115
+ # Create panels for each option
116
+ panels = []
117
+ for letter, option in options.items():
118
+ content = Text()
119
+
120
+ # Display description as bullet points
121
+ content.append("Description:\n", style="bold cyan")
122
+ for item in option.description_items:
123
+ content.append(f"• {item}\n", style="white")
124
+ content.append("\n")
125
+
126
+ # Display affected files
127
+ if option.affected_files:
128
+ content.append("Affected files:\n", style="bold cyan")
129
+ for file in option.affected_files:
130
+ content.append(f"• {file}\n", style="yellow")
131
+
132
+ # Create panel with consistent styling
133
+ panel = Panel(
134
+ content,
135
+ box=box.ROUNDED,
136
+ border_style="cyan",
137
+ title=f"Option {letter}: {option.summary}",
138
+ title_align="center",
139
+ padding=(1, 2),
140
+ width=panel_width
141
+ )
142
+ panels.append(panel)
143
+
144
+ # Display panels in columns with center alignment
145
+ if panels:
146
+ # Group panels into pairs for two columns
147
+ for i in range(0, len(panels), 2):
148
+ pair = panels[i:i+2]
149
+ columns = Columns(
150
+ pair,
151
+ align="center",
152
+ expand=True,
153
+ equal=True,
154
+ padding=(0, 2)
155
+ )
156
+ console.print(columns)
157
+ console.print() # Add spacing between rows
158
+
159
+ def _display_markdown(content: str) -> None:
160
+ """Display content in markdown format."""
161
+ console = Console()
162
+ md = Markdown(content)
163
+ console.print(md)
164
+
165
+ def _display_raw_history(claude: ClaudeAPIAgent) -> None:
166
+ """Display raw message history from Claude agent."""
167
+ console = Console()
168
+ console.print("\n=== Message History ===")
169
+ for role, content in claude.messages_history:
170
+ console.print(f"\n[bold cyan]{role.upper()}:[/bold cyan]")
171
+ console.print(content)
172
+ console.print("\n=== End Message History ===\n")
173
+
174
+
175
+ def format_analysis(analysis: str, raw: bool = False, claude: Optional[ClaudeAPIAgent] = None, workdir: Optional[Path] = None) -> None:
176
+ """Format and display the analysis output with enhanced capabilities."""
177
+ console = Console()
178
+
179
+ if raw and claude:
180
+ _display_raw_history(claude)
181
+ else:
182
+ options = parse_analysis_options(analysis)
183
+ if options:
184
+ _display_options(options)
185
+ else:
186
+ console.print("\n[yellow]Warning: No valid options found in response. Displaying as markdown.[/yellow]\n")
187
+ _display_markdown(analysis)
188
+
189
+ def get_history_path(workdir: Path) -> Path:
190
+ """Create and return the history directory path"""
191
+ history_dir = workdir / '.janito' / 'history'
192
+ history_dir.mkdir(parents=True, exist_ok=True)
193
+ return history_dir
194
+
195
+ def get_timestamp() -> str:
196
+ """Get current UTC timestamp in YMD_HMS format with leading zeros"""
197
+ from datetime import datetime, timezone
198
+ return datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')
199
+
200
+ def save_to_file(content: str, prefix: str, workdir: Path) -> Path:
201
+ """Save content to a timestamped file in history directory"""
202
+ history_dir = get_history_path(workdir)
203
+ timestamp = get_timestamp()
204
+ filename = f"{timestamp}_{prefix}.txt"
205
+ file_path = history_dir / filename
206
+ file_path.write_text(content)
207
+ return file_path
208
+
209
+
210
+
211
+ def parse_analysis_options(response: str) -> dict[str, AnalysisOption]:
212
+ """Parse options from the response text using a line-based approach."""
213
+ options = {}
214
+
215
+ # Extract content up to END_OF_OPTIONS
216
+ if 'END_OF_OPTIONS' in response:
217
+ response = response.split('END_OF_OPTIONS')[0]
218
+
219
+ lines = response.splitlines()
220
+ current_option = None
221
+ current_section = None
222
+
223
+ for line in lines:
224
+ line = line.strip()
225
+ if not line:
226
+ continue
227
+
228
+ # Check for new option starting with letter
229
+ if len(line) >= 2 and line[0].isalpha() and line[1] == '.' and line[0].isupper():
230
+ if current_option:
231
+ options[current_option.letter] = current_option
232
+
233
+ letter = line[0]
234
+ summary = line[2:].strip()
235
+ current_option = AnalysisOption(
236
+ letter=letter,
237
+ summary=summary,
238
+ affected_files=[],
239
+ description_items=[]
240
+ )
241
+ current_section = None
242
+ continue
243
+
244
+ # Skip separator lines
245
+ if line.startswith('---'):
246
+ continue
247
+
248
+ # Check for section headers
249
+ if line.startswith('Description:'):
250
+ current_section = 'description'
251
+ continue
252
+ elif line.startswith('Affected files:'):
253
+ current_section = 'files'
254
+ continue
255
+
256
+ # Process content based on current section
257
+ if current_option and current_section and line:
258
+ if current_section == 'description':
259
+ # Strip bullet points and whitespace
260
+ item = line.lstrip(' -•').strip()
261
+ if item:
262
+ current_option.description_items.append(item)
263
+ elif current_section == 'files':
264
+ # Strip bullet points and (modified)/(new) annotations
265
+ file_path = line.lstrip(' -')
266
+ file_path = re.sub(r'\s*\([^)]+\)\s*$', '', file_path)
267
+ if file_path:
268
+ current_option.affected_files.append(file_path)
269
+
270
+ # Add the last option if exists
271
+ if current_option:
272
+ options[current_option.letter] = current_option
273
+
274
+ return options
275
+
276
+ def build_request_analysis_prompt(files_content: str, request: str) -> str:
277
+ """Build prompt for information requests"""
278
+ return CHANGE_ANALYSIS_PROMPT.format(
279
+ files_content=files_content,
280
+ request=request
281
+ )