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.
Files changed (51) hide show
  1. janito/__init__.py +48 -1
  2. janito/__main__.py +29 -235
  3. janito/_contextparser.py +113 -0
  4. janito/agents/__init__.py +22 -0
  5. janito/agents/agent.py +21 -0
  6. janito/agents/claudeai.py +64 -0
  7. janito/agents/openai.py +53 -0
  8. janito/agents/test.py +34 -0
  9. janito/analysis/__init__.py +33 -0
  10. janito/analysis/display.py +149 -0
  11. janito/analysis/options.py +112 -0
  12. janito/analysis/prompts.py +75 -0
  13. janito/change/__init__.py +19 -0
  14. janito/change/applier.py +269 -0
  15. janito/change/content.py +62 -0
  16. janito/change/indentation.py +33 -0
  17. janito/change/position.py +169 -0
  18. janito/changehistory.py +46 -0
  19. janito/changeviewer/__init__.py +12 -0
  20. janito/changeviewer/diff.py +28 -0
  21. janito/changeviewer/panels.py +268 -0
  22. janito/changeviewer/styling.py +59 -0
  23. janito/changeviewer/themes.py +57 -0
  24. janito/cli/__init__.py +2 -0
  25. janito/cli/commands.py +53 -0
  26. janito/cli/functions.py +286 -0
  27. janito/cli/registry.py +26 -0
  28. janito/common.py +23 -0
  29. janito/config.py +8 -3
  30. janito/console/__init__.py +3 -0
  31. janito/console/commands.py +112 -0
  32. janito/console/core.py +62 -0
  33. janito/console/display.py +157 -0
  34. janito/fileparser.py +334 -0
  35. janito/prompts.py +58 -74
  36. janito/qa.py +40 -7
  37. janito/review.py +13 -0
  38. janito/scan.py +68 -14
  39. janito/tests/test_fileparser.py +26 -0
  40. janito/version.py +23 -0
  41. janito-0.5.0.dist-info/METADATA +146 -0
  42. janito-0.5.0.dist-info/RECORD +45 -0
  43. janito/changeviewer.py +0 -64
  44. janito/claude.py +0 -74
  45. janito/console.py +0 -60
  46. janito/contentchange.py +0 -165
  47. janito-0.3.0.dist-info/METADATA +0 -138
  48. janito-0.3.0.dist-info/RECORD +0 -15
  49. {janito-0.3.0.dist-info → janito-0.5.0.dist-info}/WHEEL +0 -0
  50. {janito-0.3.0.dist-info → janito-0.5.0.dist-info}/entry_points.txt +0 -0
  51. {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
@@ -0,0 +1,2 @@
1
+
2
+ # This file is intentionally left empty to make the cli directory a package.
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)
@@ -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 # Add this line
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: # Add this method
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: # Add this method
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,3 @@
1
+ from .core import start_console_session
2
+
3
+ __all__ = ['start_console_session']
@@ -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