janito 0.3.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.
- janito/__init__.py +48 -1
- janito/__main__.py +29 -235
- janito/_contextparser.py +113 -0
- janito/agents/__init__.py +22 -0
- janito/agents/agent.py +21 -0
- janito/agents/claudeai.py +64 -0
- janito/agents/openai.py +53 -0
- janito/agents/test.py +34 -0
- janito/analysis/__init__.py +33 -0
- janito/analysis/display.py +149 -0
- janito/analysis/options.py +112 -0
- janito/analysis/prompts.py +75 -0
- janito/change/__init__.py +19 -0
- janito/change/applier.py +269 -0
- janito/change/content.py +62 -0
- janito/change/indentation.py +33 -0
- janito/change/position.py +169 -0
- janito/changehistory.py +46 -0
- janito/changeviewer/__init__.py +12 -0
- janito/changeviewer/diff.py +28 -0
- janito/changeviewer/panels.py +268 -0
- janito/changeviewer/styling.py +59 -0
- janito/changeviewer/themes.py +57 -0
- janito/cli/__init__.py +2 -0
- janito/cli/commands.py +53 -0
- janito/cli/functions.py +286 -0
- janito/cli/registry.py +26 -0
- janito/common.py +23 -0
- janito/config.py +8 -3
- janito/console/__init__.py +3 -0
- janito/console/commands.py +112 -0
- janito/console/core.py +62 -0
- janito/console/display.py +157 -0
- janito/fileparser.py +334 -0
- janito/prompts.py +58 -74
- janito/qa.py +40 -7
- janito/review.py +13 -0
- janito/scan.py +68 -14
- janito/tests/test_fileparser.py +26 -0
- janito/version.py +23 -0
- janito-0.5.0.dist-info/METADATA +146 -0
- janito-0.5.0.dist-info/RECORD +45 -0
- janito/changeviewer.py +0 -64
- janito/claude.py +0 -74
- janito/console.py +0 -60
- janito/contentchange.py +0 -165
- janito-0.3.0.dist-info/METADATA +0 -138
- janito-0.3.0.dist-info/RECORD +0 -15
- {janito-0.3.0.dist-info → janito-0.5.0.dist-info}/WHEEL +0 -0
- {janito-0.3.0.dist-info → janito-0.5.0.dist-info}/entry_points.txt +0 -0
- {janito-0.3.0.dist-info → janito-0.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,57 @@
|
|
1
|
+
from dataclasses import dataclass
|
2
|
+
from typing import Dict
|
3
|
+
from enum import Enum
|
4
|
+
|
5
|
+
class ThemeType(Enum):
|
6
|
+
DARK = "dark"
|
7
|
+
LIGHT = "light"
|
8
|
+
|
9
|
+
# Dark theme colors
|
10
|
+
DARK_LINE_BACKGROUNDS = {
|
11
|
+
'unchanged': '#1A1A1A',
|
12
|
+
'added': '#003300',
|
13
|
+
'deleted': '#660000',
|
14
|
+
'modified': '#000030'
|
15
|
+
}
|
16
|
+
|
17
|
+
# Light theme colors
|
18
|
+
LIGHT_LINE_BACKGROUNDS = {
|
19
|
+
'unchanged': 'grey93', # Light gray for unchanged
|
20
|
+
'added': 'light_green', # Semantic light green for additions
|
21
|
+
'deleted': 'light_red', # Semantic light red for deletions
|
22
|
+
'modified': 'light_blue' # Semantic light blue for modifications
|
23
|
+
}
|
24
|
+
|
25
|
+
@dataclass
|
26
|
+
class ColorTheme:
|
27
|
+
text_color: str
|
28
|
+
line_backgrounds: Dict[str, str]
|
29
|
+
theme_type: ThemeType
|
30
|
+
|
31
|
+
# Predefined themes
|
32
|
+
DARK_THEME = ColorTheme(
|
33
|
+
text_color="white",
|
34
|
+
line_backgrounds=DARK_LINE_BACKGROUNDS,
|
35
|
+
theme_type=ThemeType.DARK
|
36
|
+
)
|
37
|
+
|
38
|
+
LIGHT_THEME = ColorTheme(
|
39
|
+
text_color="black",
|
40
|
+
line_backgrounds=LIGHT_LINE_BACKGROUNDS,
|
41
|
+
theme_type=ThemeType.LIGHT
|
42
|
+
)
|
43
|
+
|
44
|
+
DEFAULT_THEME = DARK_THEME
|
45
|
+
|
46
|
+
def validate_theme(theme: ColorTheme) -> bool:
|
47
|
+
"""Validate that a theme contains all required colors"""
|
48
|
+
required_line_backgrounds = {'unchanged', 'added', 'deleted', 'modified'}
|
49
|
+
|
50
|
+
if not isinstance(theme, ColorTheme):
|
51
|
+
return False
|
52
|
+
|
53
|
+
return all(bg in theme.line_backgrounds for bg in required_line_backgrounds)
|
54
|
+
|
55
|
+
def get_theme_by_type(theme_type: ThemeType) -> ColorTheme:
|
56
|
+
"""Get a predefined theme by type"""
|
57
|
+
return LIGHT_THEME if theme_type == ThemeType.LIGHT else DARK_THEME
|
janito/cli/__init__.py
ADDED
janito/cli/commands.py
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
from typing import Optional, List
|
3
|
+
from rich.console import Console
|
4
|
+
|
5
|
+
from janito.agents import AIAgent
|
6
|
+
from janito.scan import preview_scan, is_dir_empty
|
7
|
+
from janito.config import config
|
8
|
+
|
9
|
+
from .functions import (
|
10
|
+
process_question, replay_saved_file, ensure_workdir,
|
11
|
+
review_text, save_to_file, collect_files_content,
|
12
|
+
build_request_analysis_prompt, progress_send_message,
|
13
|
+
format_analysis, handle_option_selection, read_stdin
|
14
|
+
)
|
15
|
+
|
16
|
+
|
17
|
+
def handle_ask(question: str, workdir: Path, include: List[Path], raw: bool, agent: AIAgent):
|
18
|
+
"""Ask a question about the codebase"""
|
19
|
+
workdir = ensure_workdir(workdir)
|
20
|
+
if question == ".":
|
21
|
+
question = read_stdin()
|
22
|
+
process_question(question, workdir, include, raw, agent)
|
23
|
+
|
24
|
+
def handle_scan(paths_to_scan: List[Path], workdir: Path):
|
25
|
+
"""Preview files that would be analyzed"""
|
26
|
+
workdir = ensure_workdir(workdir)
|
27
|
+
preview_scan(paths_to_scan, workdir)
|
28
|
+
|
29
|
+
def handle_play(filepath: Path, workdir: Path, raw: bool):
|
30
|
+
"""Replay a saved prompt file"""
|
31
|
+
workdir = ensure_workdir(workdir)
|
32
|
+
replay_saved_file(filepath, workdir, raw)
|
33
|
+
|
34
|
+
def handle_request(request: str, workdir: Path, include: List[Path], raw: bool, agent: AIAgent):
|
35
|
+
"""Process modification request"""
|
36
|
+
workdir = ensure_workdir(workdir)
|
37
|
+
paths_to_scan = include if include else [workdir]
|
38
|
+
|
39
|
+
is_empty = is_dir_empty(workdir)
|
40
|
+
if is_empty and not include:
|
41
|
+
console = Console()
|
42
|
+
console.print("\n[bold blue]Empty directory - will create new files as needed[/bold blue]")
|
43
|
+
files_content = ""
|
44
|
+
else:
|
45
|
+
files_content = collect_files_content(paths_to_scan, workdir)
|
46
|
+
|
47
|
+
initial_prompt = build_request_analysis_prompt(files_content, request)
|
48
|
+
initial_response = progress_send_message(initial_prompt)
|
49
|
+
save_to_file(initial_response, 'analysis', workdir)
|
50
|
+
|
51
|
+
format_analysis(initial_response, raw, agent)
|
52
|
+
|
53
|
+
handle_option_selection(initial_response, request, raw, workdir, include)
|
janito/cli/functions.py
ADDED
@@ -0,0 +1,286 @@
|
|
1
|
+
from typing import Optional, List
|
2
|
+
from pathlib import Path
|
3
|
+
from rich.console import Console
|
4
|
+
from rich.prompt import Prompt, Confirm
|
5
|
+
from rich.panel import Panel
|
6
|
+
from rich.text import Text
|
7
|
+
from rich.markdown import Markdown
|
8
|
+
from datetime import datetime, timezone
|
9
|
+
import tempfile
|
10
|
+
import typer
|
11
|
+
import sys
|
12
|
+
|
13
|
+
from janito.agents import AIAgent
|
14
|
+
from janito.config import config
|
15
|
+
from janito.scan import collect_files_content, show_content_stats
|
16
|
+
from janito.analysis import (
|
17
|
+
format_analysis, build_request_analysis_prompt,
|
18
|
+
parse_analysis_options, get_history_file_type, AnalysisOption
|
19
|
+
)
|
20
|
+
from janito.qa import ask_question, display_answer
|
21
|
+
from janito.common import progress_send_message
|
22
|
+
from janito.prompts import build_selected_option_prompt
|
23
|
+
from janito.fileparser import parse_block_changes
|
24
|
+
from janito.change.applier import preview_and_apply_changes
|
25
|
+
|
26
|
+
def prompt_user(message: str, choices: List[str] = None) -> str:
|
27
|
+
"""Display a simple user prompt with optional choices"""
|
28
|
+
console = Console()
|
29
|
+
|
30
|
+
if choices:
|
31
|
+
console.print(f"\n[cyan]Options: {', '.join(choices)}[/cyan]")
|
32
|
+
|
33
|
+
return Prompt.ask(f"[bold cyan]> {message}[/bold cyan]")
|
34
|
+
|
35
|
+
def validate_option_letter(letter: str, options: dict) -> bool:
|
36
|
+
"""Validate if the given letter is a valid option or 'M' for modify"""
|
37
|
+
return letter.upper() in options or letter.upper() == 'M'
|
38
|
+
|
39
|
+
def get_option_selection() -> str:
|
40
|
+
"""Get user input for option selection"""
|
41
|
+
console = Console()
|
42
|
+
console.print("\n[cyan]Enter option letter or 'M' to modify request[/cyan]")
|
43
|
+
|
44
|
+
while True:
|
45
|
+
letter = Prompt.ask("[bold cyan]Select option[/bold cyan]").strip().upper()
|
46
|
+
if letter == 'M' or (letter.isalpha() and len(letter) == 1):
|
47
|
+
return letter
|
48
|
+
|
49
|
+
console.print("[red]Please enter a valid letter or 'M'[/red]")
|
50
|
+
|
51
|
+
def get_change_history_path(workdir: Path) -> Path:
|
52
|
+
"""Create and return the changes history directory path"""
|
53
|
+
changes_history_dir = workdir / '.janito' / 'change_history'
|
54
|
+
changes_history_dir.mkdir(parents=True, exist_ok=True)
|
55
|
+
return changes_history_dir
|
56
|
+
|
57
|
+
def get_timestamp() -> str:
|
58
|
+
"""Get current UTC timestamp in YMD_HMS format with leading zeros"""
|
59
|
+
return datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')
|
60
|
+
|
61
|
+
def save_prompt_to_file(prompt: str) -> Path:
|
62
|
+
"""Save prompt to a named temporary file that won't be deleted"""
|
63
|
+
temp_file = tempfile.NamedTemporaryFile(prefix='selected_', suffix='.txt', delete=False)
|
64
|
+
temp_path = Path(temp_file.name)
|
65
|
+
temp_path.write_text(prompt)
|
66
|
+
return temp_path
|
67
|
+
|
68
|
+
def save_to_file(content: str, prefix: str, workdir: Path) -> Path:
|
69
|
+
"""Save content to a timestamped file in changes history directory"""
|
70
|
+
changes_history_dir = get_change_history_path(workdir)
|
71
|
+
timestamp = get_timestamp()
|
72
|
+
filename = f"{timestamp}_{prefix}.txt"
|
73
|
+
file_path = changes_history_dir / filename
|
74
|
+
file_path.write_text(content)
|
75
|
+
return file_path
|
76
|
+
|
77
|
+
def modify_request(request: str) -> str:
|
78
|
+
"""Display current request and get modified version with improved formatting"""
|
79
|
+
console = Console()
|
80
|
+
|
81
|
+
# Display current request in a panel with clear formatting
|
82
|
+
console.print("\n[bold cyan]Current Request:[/bold cyan]")
|
83
|
+
console.print(Panel(
|
84
|
+
Text(request, style="white"),
|
85
|
+
border_style="blue",
|
86
|
+
title="Previous Request",
|
87
|
+
padding=(1, 2)
|
88
|
+
))
|
89
|
+
|
90
|
+
# Get modified request with clear prompt
|
91
|
+
console.print("\n[bold cyan]Enter modified request below:[/bold cyan]")
|
92
|
+
console.print("[dim](Press Enter to submit, Ctrl+C to cancel)[/dim]")
|
93
|
+
try:
|
94
|
+
new_request = prompt_user("Modified request")
|
95
|
+
if not new_request.strip():
|
96
|
+
console.print("[yellow]No changes made, keeping original request[/yellow]")
|
97
|
+
return request
|
98
|
+
return new_request
|
99
|
+
except KeyboardInterrupt:
|
100
|
+
console.print("\n[yellow]Modification cancelled, keeping original request[/yellow]")
|
101
|
+
return request
|
102
|
+
|
103
|
+
def format_option_text(option: AnalysisOption) -> str:
|
104
|
+
"""Format an AnalysisOption into a string representation"""
|
105
|
+
option_text = f"Option {option.letter}:\n"
|
106
|
+
option_text += f"Summary: {option.summary}\n\n"
|
107
|
+
option_text += "Description:\n"
|
108
|
+
for item in option.description_items:
|
109
|
+
option_text += f"- {item}\n"
|
110
|
+
option_text += "\nAffected files:\n"
|
111
|
+
for file in option.affected_files:
|
112
|
+
option_text += f"- {file}\n"
|
113
|
+
return option_text
|
114
|
+
|
115
|
+
def handle_option_selection(initial_response: str, request: str, raw: bool = False, workdir: Optional[Path] = None, include: Optional[List[Path]] = None) -> None:
|
116
|
+
"""Handle option selection and implementation details"""
|
117
|
+
options = parse_analysis_options(initial_response)
|
118
|
+
if not options:
|
119
|
+
console = Console()
|
120
|
+
console.print("[red]No valid options found in the response[/red]")
|
121
|
+
return
|
122
|
+
|
123
|
+
while True:
|
124
|
+
option = get_option_selection()
|
125
|
+
|
126
|
+
if option == 'M':
|
127
|
+
# Use the new modify_request function for better UX
|
128
|
+
new_request = modify_request(request)
|
129
|
+
if new_request == request:
|
130
|
+
continue
|
131
|
+
|
132
|
+
# Rerun analysis with new request
|
133
|
+
# Only scan files listed in the selected option's affected_files
|
134
|
+
selected_option = options[option]
|
135
|
+
files_to_scan = selected_option.affected_files
|
136
|
+
|
137
|
+
# Convert relative paths to absolute paths
|
138
|
+
absolute_paths = []
|
139
|
+
for file_path in files_to_scan:
|
140
|
+
# Remove (new) suffix if present
|
141
|
+
clean_path = file_path.split(' (')[0].strip()
|
142
|
+
path = workdir / clean_path if workdir else Path(clean_path)
|
143
|
+
if path.exists():
|
144
|
+
absolute_paths.append(path)
|
145
|
+
|
146
|
+
files_content = collect_files_content(absolute_paths, workdir) if absolute_paths else ""
|
147
|
+
show_content_stats(files_content)
|
148
|
+
initial_prompt = build_request_analysis_prompt(files_content, new_request)
|
149
|
+
initial_response = progress_send_message(initial_prompt)
|
150
|
+
save_to_file(initial_response, 'analysis', workdir)
|
151
|
+
|
152
|
+
format_analysis(initial_response, raw)
|
153
|
+
options = parse_analysis_options(initial_response)
|
154
|
+
if not options:
|
155
|
+
console = Console()
|
156
|
+
console.print("[red]No valid options found in the response[/red]")
|
157
|
+
return
|
158
|
+
continue
|
159
|
+
|
160
|
+
if not validate_option_letter(option, options):
|
161
|
+
console = Console()
|
162
|
+
console.print(f"[red]Invalid option '{option}'. Valid options are: {', '.join(options.keys())} or 'M' to modify[/red]")
|
163
|
+
continue
|
164
|
+
|
165
|
+
break
|
166
|
+
|
167
|
+
# Only scan files listed in the selected option's affected_files
|
168
|
+
selected_option = options[option]
|
169
|
+
files_to_scan = selected_option.affected_files
|
170
|
+
|
171
|
+
# Convert relative paths to absolute paths
|
172
|
+
absolute_paths = []
|
173
|
+
for file_path in files_to_scan:
|
174
|
+
# Remove (new) suffix if present
|
175
|
+
clean_path = file_path.split(' (')[0].strip()
|
176
|
+
path = workdir / clean_path if workdir else Path(clean_path)
|
177
|
+
if path.exists():
|
178
|
+
absolute_paths.append(path)
|
179
|
+
|
180
|
+
files_content = collect_files_content(absolute_paths, workdir) if absolute_paths else ""
|
181
|
+
show_content_stats(files_content)
|
182
|
+
|
183
|
+
# Format the selected option before building prompt
|
184
|
+
selected_option = options[option]
|
185
|
+
option_text = format_option_text(selected_option)
|
186
|
+
|
187
|
+
selected_prompt = build_selected_option_prompt(option_text, request, files_content)
|
188
|
+
prompt_file = save_to_file(selected_prompt, 'selected', workdir)
|
189
|
+
if config.verbose:
|
190
|
+
print(f"\nSelected prompt saved to: {prompt_file}")
|
191
|
+
|
192
|
+
selected_response = progress_send_message(selected_prompt)
|
193
|
+
changes_file = save_to_file(selected_response, 'changes', workdir)
|
194
|
+
|
195
|
+
if config.verbose:
|
196
|
+
try:
|
197
|
+
rel_path = changes_file.relative_to(workdir)
|
198
|
+
print(f"\nChanges saved to: ./{rel_path}")
|
199
|
+
except ValueError:
|
200
|
+
print(f"\nChanges saved to: {changes_file}")
|
201
|
+
|
202
|
+
changes = parse_block_changes(selected_response)
|
203
|
+
preview_and_apply_changes(changes, workdir, config.test_cmd)
|
204
|
+
|
205
|
+
def read_stdin() -> str:
|
206
|
+
"""Read input from stdin until EOF"""
|
207
|
+
console = Console()
|
208
|
+
console.print("[dim]Enter your input (press Ctrl+D when finished):[/dim]")
|
209
|
+
return sys.stdin.read().strip()
|
210
|
+
|
211
|
+
def replay_saved_file(filepath: Path, workdir: Path, raw: bool = False) -> None:
|
212
|
+
"""Process a saved prompt file and display the response"""
|
213
|
+
if not filepath.exists():
|
214
|
+
raise FileNotFoundError(f"File {filepath} not found")
|
215
|
+
|
216
|
+
content = filepath.read_text()
|
217
|
+
|
218
|
+
# Add debug output of file content
|
219
|
+
if config.debug:
|
220
|
+
console = Console()
|
221
|
+
console.print("\n[bold blue]Debug: File Content[/bold blue]")
|
222
|
+
console.print(Panel(
|
223
|
+
content,
|
224
|
+
title=f"Content of {filepath.name}",
|
225
|
+
border_style="blue",
|
226
|
+
padding=(1, 2)
|
227
|
+
))
|
228
|
+
console.print()
|
229
|
+
|
230
|
+
file_type = get_history_file_type(filepath)
|
231
|
+
|
232
|
+
if file_type == 'changes':
|
233
|
+
changes = parse_block_changes(content)
|
234
|
+
success = preview_and_apply_changes(changes, workdir, config.test_cmd)
|
235
|
+
if not success:
|
236
|
+
raise typer.Exit(1)
|
237
|
+
elif file_type == 'analysis':
|
238
|
+
format_analysis(content, raw)
|
239
|
+
handle_option_selection(content, content, raw, workdir)
|
240
|
+
elif file_type == 'selected':
|
241
|
+
if raw:
|
242
|
+
console = Console()
|
243
|
+
console.print("\n=== Prompt Content ===")
|
244
|
+
console.print(content)
|
245
|
+
console.print("=== End Prompt Content ===\n")
|
246
|
+
|
247
|
+
response = progress_send_message(content)
|
248
|
+
changes_file = save_to_file(response, 'changes_', workdir)
|
249
|
+
print(f"\nChanges saved to: {changes_file}")
|
250
|
+
|
251
|
+
changes = parse_block_changes(response)
|
252
|
+
preview_and_apply_changes(changes, workdir, config.test_cmd)
|
253
|
+
else:
|
254
|
+
response = progress_send_message(content)
|
255
|
+
format_analysis(response, raw)
|
256
|
+
|
257
|
+
def process_question(question: str, workdir: Path, include: List[Path], raw: bool, agent: AIAgent) -> None:
|
258
|
+
"""Process a question about the codebase"""
|
259
|
+
paths_to_scan = [workdir] if workdir else []
|
260
|
+
if include:
|
261
|
+
paths_to_scan.extend(include)
|
262
|
+
files_content = collect_files_content(paths_to_scan, workdir)
|
263
|
+
answer = ask_question(question, files_content)
|
264
|
+
display_answer(answer, raw)
|
265
|
+
|
266
|
+
def ensure_workdir(workdir: Path) -> Path:
|
267
|
+
"""Ensure working directory exists, prompt for creation if it doesn't"""
|
268
|
+
if workdir.exists():
|
269
|
+
return workdir.absolute()
|
270
|
+
|
271
|
+
console = Console()
|
272
|
+
console.print(f"\n[yellow]Directory does not exist:[/yellow] {workdir}")
|
273
|
+
if Confirm.ask("Create directory?"):
|
274
|
+
workdir.mkdir(parents=True)
|
275
|
+
console.print(f"[green]Created directory:[/green] {workdir}")
|
276
|
+
return workdir.absolute()
|
277
|
+
raise typer.Exit(1)
|
278
|
+
|
279
|
+
def review_text(text: str, raw: bool = False) -> None:
|
280
|
+
"""Review the provided text using Claude"""
|
281
|
+
console = Console()
|
282
|
+
response = progress_send_message(f"Please review this text and provide feedback:\n\n{text}")
|
283
|
+
if raw:
|
284
|
+
console.print(response)
|
285
|
+
else:
|
286
|
+
console.print(Markdown(response))
|
janito/cli/registry.py
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
from typing import Callable, Dict
|
2
|
+
from dataclasses import dataclass
|
3
|
+
|
4
|
+
@dataclass
|
5
|
+
class Command:
|
6
|
+
name: str
|
7
|
+
handler: Callable
|
8
|
+
help_text: str
|
9
|
+
|
10
|
+
class CommandRegistry:
|
11
|
+
def __init__(self):
|
12
|
+
self._commands: Dict[str, Command] = {}
|
13
|
+
|
14
|
+
def register(self, name: str, help_text: str):
|
15
|
+
def decorator(handler: Callable):
|
16
|
+
self._commands[name] = Command(name, handler, help_text)
|
17
|
+
return handler
|
18
|
+
return decorator
|
19
|
+
|
20
|
+
def get_command(self, name: str) -> Command:
|
21
|
+
return self._commands.get(name)
|
22
|
+
|
23
|
+
def get_all_commands(self) -> Dict[str, Command]:
|
24
|
+
return self._commands
|
25
|
+
|
26
|
+
registry = CommandRegistry()
|
janito/common.py
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
|
2
|
+
from janito.agents import AgentSingleton
|
3
|
+
|
4
|
+
def progress_send_message(message: str) -> str:
|
5
|
+
"""
|
6
|
+
Send a message to the AI agent with a progress indicator and elapsed time.
|
7
|
+
|
8
|
+
Args:
|
9
|
+
message: The message to send
|
10
|
+
|
11
|
+
Returns:
|
12
|
+
The response from the AI agent
|
13
|
+
"""
|
14
|
+
agent = AgentSingleton.get_agent()
|
15
|
+
with Progress(
|
16
|
+
SpinnerColumn(),
|
17
|
+
TextColumn("[progress.description]{task.description}", justify="center"),
|
18
|
+
TimeElapsedColumn(),
|
19
|
+
) as progress:
|
20
|
+
task = progress.add_task("Waiting for response from AI agent...", total=None)
|
21
|
+
response = agent.send_message(message)
|
22
|
+
progress.update(task, completed=True)
|
23
|
+
return response
|
janito/config.py
CHANGED
@@ -7,7 +7,8 @@ class ConfigManager:
|
|
7
7
|
def __init__(self):
|
8
8
|
self.debug = False
|
9
9
|
self.verbose = False
|
10
|
-
self.debug_line = None
|
10
|
+
self.debug_line = None
|
11
|
+
self.test_cmd = os.getenv('JANITO_TEST_CMD')
|
11
12
|
|
12
13
|
@classmethod
|
13
14
|
def get_instance(cls) -> "ConfigManager":
|
@@ -21,12 +22,16 @@ class ConfigManager:
|
|
21
22
|
def set_verbose(self, enabled: bool) -> None:
|
22
23
|
self.verbose = enabled
|
23
24
|
|
24
|
-
def set_debug_line(self, line: Optional[int]) -> None:
|
25
|
+
def set_debug_line(self, line: Optional[int]) -> None:
|
25
26
|
self.debug_line = line
|
26
27
|
|
27
|
-
def should_debug_line(self, line: int) -> bool:
|
28
|
+
def should_debug_line(self, line: int) -> bool:
|
28
29
|
"""Return True if we should show debug for this line number"""
|
29
30
|
return self.debug and (self.debug_line is None or self.debug_line == line)
|
30
31
|
|
32
|
+
def set_test_cmd(self, cmd: Optional[str]) -> None:
|
33
|
+
"""Set the test command, overriding environment variable"""
|
34
|
+
self.test_cmd = cmd if cmd is not None else os.getenv('JANITO_TEST_CMD')
|
35
|
+
|
31
36
|
# Create a singleton instance
|
32
37
|
config = ConfigManager.get_instance()
|
@@ -0,0 +1,112 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
from typing import List
|
3
|
+
from rich.console import Console
|
4
|
+
from rich.panel import Panel
|
5
|
+
from janito.agents import AIAgent
|
6
|
+
from janito.analysis import build_request_analysis_prompt
|
7
|
+
from janito.scan import collect_files_content
|
8
|
+
from janito.common import progress_send_message
|
9
|
+
from janito.__main__ import handle_option_selection
|
10
|
+
from .display import display_help
|
11
|
+
|
12
|
+
def process_command(command: str, args: str, workdir: Path, include: List[Path], agent: AIAgent) -> None:
|
13
|
+
"""Process console commands using CLI functions for consistent behavior"""
|
14
|
+
console = Console()
|
15
|
+
|
16
|
+
# Parse command options
|
17
|
+
raw = False
|
18
|
+
verbose = False
|
19
|
+
debug = False
|
20
|
+
test_cmd = None
|
21
|
+
|
22
|
+
# Extract options from args
|
23
|
+
words = args.split()
|
24
|
+
filtered_args = []
|
25
|
+
i = 0
|
26
|
+
while i < len(words):
|
27
|
+
if words[i] == '--raw':
|
28
|
+
raw = True
|
29
|
+
elif words[i] == '--verbose':
|
30
|
+
verbose = True
|
31
|
+
elif words[i] == '--debug':
|
32
|
+
debug = True
|
33
|
+
elif words[i] == '--test' and i + 1 < len(words):
|
34
|
+
test_cmd = words[i + 1]
|
35
|
+
i += 1
|
36
|
+
else:
|
37
|
+
filtered_args.append(words[i])
|
38
|
+
i += 1
|
39
|
+
|
40
|
+
args = ' '.join(filtered_args)
|
41
|
+
|
42
|
+
# Update config with command options
|
43
|
+
from janito.config import config
|
44
|
+
config.set_debug(debug)
|
45
|
+
config.set_verbose(verbose)
|
46
|
+
config.set_test_cmd(test_cmd)
|
47
|
+
|
48
|
+
# Remove leading slash if present
|
49
|
+
command = command.lstrip('/')
|
50
|
+
|
51
|
+
# Handle command aliases
|
52
|
+
command_aliases = {
|
53
|
+
'h': 'help',
|
54
|
+
'a': 'ask',
|
55
|
+
'r': 'request',
|
56
|
+
'q': 'quit',
|
57
|
+
'exit': 'quit'
|
58
|
+
}
|
59
|
+
command = command_aliases.get(command, command)
|
60
|
+
|
61
|
+
if command == "help":
|
62
|
+
display_help()
|
63
|
+
return
|
64
|
+
|
65
|
+
if command == "quit":
|
66
|
+
raise EOFError()
|
67
|
+
|
68
|
+
if command == "ask":
|
69
|
+
if not args:
|
70
|
+
console.print(Panel(
|
71
|
+
"[red]Ask command requires a question[/red]",
|
72
|
+
title="Error",
|
73
|
+
border_style="red"
|
74
|
+
))
|
75
|
+
return
|
76
|
+
|
77
|
+
# Use CLI question processing function
|
78
|
+
from janito.__main__ import process_question
|
79
|
+
process_question(args, workdir, include, raw, claude)
|
80
|
+
return
|
81
|
+
|
82
|
+
if command == "request":
|
83
|
+
if not args:
|
84
|
+
console.print(Panel(
|
85
|
+
"[red]Request command requires a description[/red]",
|
86
|
+
title="Error",
|
87
|
+
border_style="red"
|
88
|
+
))
|
89
|
+
return
|
90
|
+
|
91
|
+
paths_to_scan = [workdir] if workdir else []
|
92
|
+
if include:
|
93
|
+
paths_to_scan.extend(include)
|
94
|
+
files_content = collect_files_content(paths_to_scan, workdir)
|
95
|
+
|
96
|
+
# Use CLI request processing functions
|
97
|
+
initial_prompt = build_request_analysis_prompt(files_content, args)
|
98
|
+
initial_response = progress_send_message(initial_prompt)
|
99
|
+
|
100
|
+
from janito.__main__ import save_to_file
|
101
|
+
save_to_file(initial_response, 'analysis', workdir)
|
102
|
+
|
103
|
+
from janito.analysis import format_analysis
|
104
|
+
format_analysis(initial_response, raw)
|
105
|
+
handle_option_selection(initial_response, args, raw, workdir, include)
|
106
|
+
return
|
107
|
+
|
108
|
+
console.print(Panel(
|
109
|
+
f"[red]Unknown command: /{command}[/red]\nType '/help' for available commands",
|
110
|
+
title="Error",
|
111
|
+
border_style="red"
|
112
|
+
))
|
janito/console/core.py
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
from typing import List, Optional
|
3
|
+
from prompt_toolkit import PromptSession
|
4
|
+
from prompt_toolkit.history import FileHistory
|
5
|
+
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
6
|
+
from rich.console import Console
|
7
|
+
from janito.agents import AgentSingleton
|
8
|
+
from janito.prompts import SYSTEM_PROMPT
|
9
|
+
from .display import create_completer, format_prompt, display_welcome
|
10
|
+
from .commands import process_command
|
11
|
+
|
12
|
+
def start_console_session(workdir: Path, include: Optional[List[Path]] = None) -> None:
|
13
|
+
"""Start an enhanced interactive console session"""
|
14
|
+
console = Console()
|
15
|
+
agent = AgentSingleton.get_agent()
|
16
|
+
|
17
|
+
# Setup history with persistence
|
18
|
+
history_file = workdir / '.janito' / 'console_history'
|
19
|
+
history_file.parent.mkdir(parents=True, exist_ok=True)
|
20
|
+
|
21
|
+
# Create session with history and completions
|
22
|
+
session = PromptSession(
|
23
|
+
history=FileHistory(str(history_file)),
|
24
|
+
completer=create_completer(workdir),
|
25
|
+
auto_suggest=AutoSuggestFromHistory(),
|
26
|
+
complete_while_typing=True
|
27
|
+
)
|
28
|
+
|
29
|
+
display_welcome(workdir)
|
30
|
+
|
31
|
+
while True:
|
32
|
+
try:
|
33
|
+
# Get input with formatted prompt
|
34
|
+
user_input = session.prompt(
|
35
|
+
lambda: format_prompt(workdir),
|
36
|
+
complete_while_typing=True
|
37
|
+
).strip()
|
38
|
+
|
39
|
+
if not user_input:
|
40
|
+
continue
|
41
|
+
|
42
|
+
if user_input.lower() in ('exit', 'quit'):
|
43
|
+
console.print("\n[cyan]Goodbye! Have a great day![/cyan]\n")
|
44
|
+
break
|
45
|
+
|
46
|
+
# Split input into command and args
|
47
|
+
parts = user_input.split(maxsplit=1)
|
48
|
+
if parts[0].startswith('/'): # Handle /command format
|
49
|
+
command = parts[0][1:] # Remove the / prefix
|
50
|
+
else:
|
51
|
+
command = "request" # Default to request if no command specified
|
52
|
+
|
53
|
+
args = parts[1] if len(parts) > 1 else ""
|
54
|
+
|
55
|
+
# Process command with separated args
|
56
|
+
process_command(command, args, workdir, include, claude)
|
57
|
+
|
58
|
+
except KeyboardInterrupt:
|
59
|
+
continue
|
60
|
+
except EOFError:
|
61
|
+
console.print("\n[cyan]Goodbye! Have a great day![/cyan]\n")
|
62
|
+
break
|