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 +158 -59
- janito/analysis.py +281 -0
- janito/changeapplier.py +436 -0
- janito/changeviewer.py +337 -51
- janito/claude.py +31 -46
- janito/common.py +23 -0
- janito/config.py +8 -3
- janito/console.py +300 -30
- janito/contentchange.py +8 -89
- janito/contextparser.py +113 -0
- janito/fileparser.py +125 -0
- janito/prompts.py +43 -74
- janito/qa.py +36 -5
- janito/scan.py +24 -9
- janito/version.py +23 -0
- {janito-0.3.0.dist-info → janito-0.4.0.dist-info}/METADATA +34 -8
- janito-0.4.0.dist-info/RECORD +21 -0
- janito-0.3.0.dist-info/RECORD +0 -15
- {janito-0.3.0.dist-info → janito-0.4.0.dist-info}/WHEEL +0 -0
- {janito-0.3.0.dist-info → janito-0.4.0.dist-info}/entry_points.txt +0 -0
- {janito-0.3.0.dist-info → janito-0.4.0.dist-info}/licenses/LICENSE +0 -0
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
|
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
|
54
|
+
console.print(Rule(" User Input Required ", style=f"bold {COLORS['primary']}"))
|
63
55
|
|
64
56
|
if choices:
|
65
|
-
choice_text = f"[
|
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
|
60
|
+
return Prompt.ask(f"[bold {COLORS['secondary']}]> {message}[/bold {COLORS['secondary']}]")
|
69
61
|
|
70
|
-
def
|
71
|
-
"""
|
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
|
-
|
74
|
-
|
75
|
-
return
|
76
|
-
|
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
|
81
|
-
"""Create and return the history directory path"""
|
82
|
-
|
83
|
-
|
84
|
-
return
|
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
|
-
|
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 =
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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(
|
251
|
+
preview_and_apply_changes(changes, workdir, config.test_cmd)
|
153
252
|
else:
|
154
|
-
response = claude
|
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.
|
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 =
|
249
|
-
initial_response = claude
|
250
|
-
|
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
|
+
)
|