janito 0.6.0__py3-none-any.whl → 0.7.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 (54) hide show
  1. janito/__main__.py +37 -30
  2. janito/agents/__init__.py +8 -2
  3. janito/agents/agent.py +10 -3
  4. janito/agents/claudeai.py +13 -23
  5. janito/agents/openai.py +5 -1
  6. janito/change/analysis/analyze.py +8 -7
  7. janito/change/analysis/prompts.py +4 -12
  8. janito/change/analysis/view/terminal.py +21 -11
  9. janito/change/applier/text.py +7 -5
  10. janito/change/core.py +22 -29
  11. janito/change/parser.py +0 -2
  12. janito/change/prompts.py +16 -21
  13. janito/change/validator.py +27 -9
  14. janito/change/viewer/content.py +1 -1
  15. janito/change/viewer/panels.py +93 -115
  16. janito/change/viewer/styling.py +15 -4
  17. janito/cli/commands.py +63 -20
  18. janito/common.py +44 -18
  19. janito/config.py +44 -44
  20. janito/prompt.py +36 -0
  21. janito/qa.py +5 -14
  22. janito/search_replace/README.md +63 -17
  23. janito/search_replace/__init__.py +2 -1
  24. janito/search_replace/core.py +15 -14
  25. janito/search_replace/logger.py +35 -0
  26. janito/search_replace/searcher.py +160 -48
  27. janito/search_replace/strategy_result.py +10 -0
  28. janito/shell/__init__.py +15 -16
  29. janito/shell/commands.py +38 -97
  30. janito/shell/processor.py +7 -27
  31. janito/shell/prompt.py +48 -0
  32. janito/shell/registry.py +60 -0
  33. janito/workspace/__init__.py +4 -5
  34. janito/workspace/analysis.py +2 -2
  35. janito/workspace/show.py +141 -0
  36. janito/workspace/stats.py +43 -0
  37. janito/workspace/types.py +98 -0
  38. janito/workspace/workset.py +108 -0
  39. janito/workspace/workspace.py +114 -0
  40. janito-0.7.0.dist-info/METADATA +167 -0
  41. {janito-0.6.0.dist-info → janito-0.7.0.dist-info}/RECORD +44 -43
  42. janito/change/viewer/pager.py +0 -56
  43. janito/cli/handlers/ask.py +0 -22
  44. janito/cli/handlers/demo.py +0 -22
  45. janito/cli/handlers/request.py +0 -24
  46. janito/cli/handlers/scan.py +0 -9
  47. janito/prompts.py +0 -2
  48. janito/shell/handlers.py +0 -122
  49. janito/workspace/manager.py +0 -48
  50. janito/workspace/scan.py +0 -232
  51. janito-0.6.0.dist-info/METADATA +0 -185
  52. {janito-0.6.0.dist-info → janito-0.7.0.dist-info}/WHEEL +0 -0
  53. {janito-0.6.0.dist-info → janito-0.7.0.dist-info}/entry_points.txt +0 -0
  54. {janito-0.6.0.dist-info → janito-0.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,18 +1,47 @@
1
1
  from typing import List, Optional, Dict, Type
2
2
  from abc import ABC, abstractmethod
3
3
  import re
4
+ from .strategy_result import StrategyResult
4
5
 
5
6
  LINE_OVER_LINE_DEBUG = False
6
7
 
7
8
  class SearchStrategy(ABC):
8
9
  """Base class for search strategies."""
10
+
11
+ def __init__(self):
12
+ """Initialize strategy with name derived from class name."""
13
+ self.name = self.__class__.__name__.replace('Strategy', '')
14
+
9
15
  @abstractmethod
10
16
  def match(self, source_lines: List[str], pattern_lines: List[str], pos: int, searcher: 'Searcher') -> bool:
17
+ """Check if pattern matches source at given position.
18
+
19
+ Args:
20
+ source_lines: List of source code lines to search in
21
+ pattern_lines: List of pattern lines to match
22
+ pos: Position in source_lines to start matching
23
+ searcher: Searcher instance for utility methods
24
+
25
+ Returns:
26
+ bool: True if pattern matches at position, False otherwise
27
+ """
11
28
  pass
12
29
 
13
30
  class ExactMatchStrategy(SearchStrategy):
14
- """Exact match including indentation."""
31
+ """Strategy for exact match including indentation."""
32
+
15
33
  def match(self, source_lines: List[str], pattern_lines: List[str], pos: int, searcher: 'Searcher') -> bool:
34
+ """Match pattern exactly with indentation.
35
+
36
+ Args:
37
+ source_lines: List of source code lines to search in
38
+ pattern_lines: List of pattern lines to match
39
+ pos: Position in source_lines to start matching
40
+ searcher: Searcher instance for utility methods
41
+
42
+ Returns:
43
+ bool: True if pattern matches exactly at position, False otherwise
44
+ """
16
45
  if pos + len(pattern_lines) > len(source_lines):
17
46
  return False
18
47
  return all(source_lines[pos + i] == pattern_line
@@ -20,7 +49,19 @@ class ExactMatchStrategy(SearchStrategy):
20
49
 
21
50
  class ExactContentStrategy(SearchStrategy):
22
51
  """Exact content match ignoring all indentation."""
52
+
23
53
  def match(self, source_lines: List[str], pattern_lines: List[str], pos: int, searcher: 'Searcher') -> bool:
54
+ """Match pattern exactly ignoring indentation.
55
+
56
+ Args:
57
+ source_lines: List of source code lines to search in
58
+ pattern_lines: List of pattern lines to match
59
+ pos: Position in source_lines to start matching
60
+ searcher: Searcher instance for utility methods
61
+
62
+ Returns:
63
+ bool: True if pattern matches exactly at position, False otherwise
64
+ """
24
65
  if pos + len(pattern_lines) > len(source_lines):
25
66
  return False
26
67
  return all(source_lines[pos + i].strip() == pattern_line.strip()
@@ -29,7 +70,19 @@ class ExactContentStrategy(SearchStrategy):
29
70
 
30
71
  class IndentAwareStrategy(SearchStrategy):
31
72
  """Indentation-aware matching preserving relative indentation."""
73
+
32
74
  def match(self, source_lines: List[str], pattern_lines: List[str], pos: int, searcher: 'Searcher') -> bool:
75
+ """Match pattern preserving relative indentation.
76
+
77
+ Args:
78
+ source_lines: List of source code lines to search in
79
+ pattern_lines: List of pattern lines to match
80
+ pos: Position in source_lines to start matching
81
+ searcher: Searcher instance for utility methods
82
+
83
+ Returns:
84
+ bool: True if pattern matches preserving indentation at position, False otherwise
85
+ """
33
86
  if pos + len(pattern_lines) > len(source_lines):
34
87
  return False
35
88
  match_indent = searcher.get_indentation(source_lines[pos])
@@ -39,6 +92,7 @@ class IndentAwareStrategy(SearchStrategy):
39
92
 
40
93
  class ExactContentNoComments(SearchStrategy):
41
94
  """Exact content match ignoring indentation, comments, and empty lines."""
95
+
42
96
  def _strip_comments(self, line: str) -> str:
43
97
  """Remove comments from line."""
44
98
  if '#' in line:
@@ -48,6 +102,17 @@ class ExactContentNoComments(SearchStrategy):
48
102
  return line.strip()
49
103
 
50
104
  def match(self, source_lines: List[str], pattern_lines: List[str], pos: int, searcher: 'Searcher') -> bool:
105
+ """Match pattern ignoring comments and empty lines.
106
+
107
+ Args:
108
+ source_lines: List of source code lines to search in
109
+ pattern_lines: List of pattern lines to match
110
+ pos: Position in source_lines to start matching
111
+ searcher: Searcher instance for utility methods
112
+
113
+ Returns:
114
+ bool: True if pattern matches ignoring comments at position, False otherwise
115
+ """
51
116
  if pos + len(pattern_lines) > len(source_lines):
52
117
  return False
53
118
 
@@ -93,6 +158,7 @@ class ExactContentNoComments(SearchStrategy):
93
158
 
94
159
  class ExactContentNoCommentsFirstLinePartial(SearchStrategy):
95
160
  """Match first line partially, ignoring comments."""
161
+
96
162
  def _strip_comments(self, line: str) -> str:
97
163
  """Remove comments from line."""
98
164
  if '#' in line:
@@ -102,6 +168,17 @@ class ExactContentNoCommentsFirstLinePartial(SearchStrategy):
102
168
  return line.strip()
103
169
 
104
170
  def match(self, source_lines: List[str], pattern_lines: List[str], pos: int, searcher: 'Searcher') -> bool:
171
+ """Match first line of pattern partially ignoring comments.
172
+
173
+ Args:
174
+ source_lines: List of source code lines to search in
175
+ pattern_lines: List of pattern lines to match
176
+ pos: Position in source_lines to start matching
177
+ searcher: Searcher instance for utility methods
178
+
179
+ Returns:
180
+ bool: True if first line of pattern matches partially at position, False otherwise
181
+ """
105
182
  if pos >= len(source_lines):
106
183
  return False
107
184
 
@@ -176,7 +253,14 @@ class Searcher:
176
253
  return '', 0
177
254
 
178
255
  def _build_indent_map(self, text: str) -> dict[int, int]:
179
- """Build a map of line numbers to indentation levels."""
256
+ """Build a map of line numbers to their indentation levels.
257
+
258
+ Args:
259
+ text: Source text to analyze
260
+
261
+ Returns:
262
+ dict[int, int]: Mapping of line numbers to indentation levels
263
+ """
180
264
  indent_map = {}
181
265
  for i, line in enumerate(text.splitlines()):
182
266
  if line.strip(): # Only track non-empty lines
@@ -219,7 +303,16 @@ class Searcher:
219
303
  return '\n'.join(normalized)
220
304
 
221
305
  def _find_best_match_position(self, positions: List[int], source_lines: List[str], pattern_base_indent: int) -> Optional[int]:
222
- """Find the best matching position based on line number (earliest match)."""
306
+ """Find the best matching position among candidates.
307
+
308
+ Args:
309
+ positions: List of candidate line positions
310
+ source_lines: List of source code lines
311
+ pattern_base_indent: Base indentation level of pattern
312
+
313
+ Returns:
314
+ Optional[int]: Best matching position or None if no matches
315
+ """
223
316
  if self.debug_mode:
224
317
  print(f"[DEBUG] Finding best match among positions: {[p+1 for p in positions]}") # Show 1-based line numbers
225
318
 
@@ -231,69 +324,88 @@ class Searcher:
231
324
  print(f"[DEBUG] Selected match at line {best_pos + 1}") # Show 1-based line number
232
325
  return best_pos
233
326
 
234
- def try_match_with_strategies(self, source_lines: List[str], pattern_lines: List[str],
235
- pos: int, strategies: List[SearchStrategy]) -> bool:
236
- """Try matching using multiple strategies in order."""
327
+ def try_match_with_strategies(self, source_lines: List[str], pattern_lines: List[str],
328
+ pos: int, strategies: List[SearchStrategy]) -> StrategyResult:
329
+ """Try matching using multiple strategies in sequence.
330
+
331
+ Args:
332
+ source_lines: List of source code lines
333
+ pattern_lines: List of pattern lines to match
334
+ pos: Position to start matching
335
+ strategies: List of strategies to try
336
+
337
+ Returns:
338
+ StrategyResult: Result containing match success and strategy used
339
+ """
237
340
  if self.debug_mode and LINE_OVER_LINE_DEBUG:
238
341
  print(f"\n[DEBUG] Trying to match at line {pos + 1}")
239
-
342
+
240
343
  for strategy in strategies:
241
344
  if strategy.match(source_lines, pattern_lines, pos, self):
242
345
  if self.debug_mode:
243
346
  print(f"[DEBUG] Match found with {strategy.__class__.__name__}")
244
347
  print(f"[DEBUG] Stopping strategy chain at line {pos + 1}")
245
- return True
246
- return False
348
+ return StrategyResult(success=True, strategy_name=strategy.name, match_position=pos)
349
+ return StrategyResult(success=False)
247
350
 
248
- def _find_matches(self, source_lines: List[str], pattern_lines: List[str],
351
+ def _find_matches(self, source_lines: List[str], pattern_lines: List[str],
249
352
  file_ext: Optional[str] = None) -> List[int]:
250
- """Find all matches using configured strategies."""
251
- strategies = self.get_strategies(file_ext)
353
+ """Find all matching positions using available strategies.
252
354
 
355
+ Args:
356
+ source_lines: List of source code lines
357
+ pattern_lines: List of pattern lines to match
358
+ file_ext: Optional file extension to determine strategies
359
+
360
+ Returns:
361
+ List[int]: List of matching line positions
362
+ """
363
+ strategies = self.get_strategies(file_ext)
364
+
253
365
  if self.debug_mode:
254
366
  print("\nTrying search strategies:")
255
367
  print("-" * 50)
256
-
257
- # Try each strategy in order of preference
368
+
369
+ # Track positions already matched to avoid redundant attempts
370
+ matched_positions = set()
371
+ all_matches = []
372
+
258
373
  for strategy in strategies:
259
- matches = []
260
374
  strategy_name = strategy.__class__.__name__.replace('Strategy', '')
261
-
375
+
262
376
  if self.debug_mode:
263
377
  print(f"\n→ {strategy_name}...")
264
-
378
+
265
379
  for i in range(len(source_lines)):
380
+ if i in matched_positions:
381
+ continue
382
+
266
383
  if strategy.match(source_lines, pattern_lines, i, self):
267
- matches.append(i)
384
+ matched_positions.add(i)
385
+ all_matches.append(i)
386
+ if self.debug_mode:
387
+ print(f"✓ Match found at line {i+1} using {strategy_name}")
268
388
 
269
- if matches:
270
- if self.debug_mode:
271
- # Show 1-based line numbers consistently
272
- print(f"✓ {strategy_name}: Found {len(matches)} match(es) at line(s) {[m+1 for m in matches]}")
273
- return matches
274
- elif self.debug_mode:
275
- print(f"✗ {strategy_name}: No matches found")
276
-
277
- return []
278
-
279
- def exact_match(self, source: str, pattern: str) -> List[int]:
280
- """Perform exact line-by-line matching, preserving all whitespace except newlines."""
281
- source_lines = source.splitlines()
282
- pattern_lines = pattern.splitlines()
283
- pattern_len = len(pattern_lines)
284
- matches = []
285
-
286
- for i in range(len(source_lines) - pattern_len + 1):
287
- is_match = True
288
- for j in range(pattern_len):
289
- if source_lines[i + j] != pattern_lines[j]:
290
- is_match = False
291
- break
292
-
293
- if is_match:
294
- matches.append(i)
295
-
296
- if self.debug_mode and matches:
297
- print(f"[DEBUG] Found {len(matches)} exact matches")
389
+ if all_matches and isinstance(strategy, ExactMatchStrategy):
390
+ # If we found exact matches, no need to try other strategies
391
+ break
392
+
393
+ if self.debug_mode and all_matches:
394
+ print(f"\nFound {len(all_matches)} total match(es) at line(s) {[m+1 for m in sorted(all_matches)]}")
395
+
396
+ return sorted(all_matches)
397
+
398
+ def _check_exact_match(self, source_lines: List[str], pattern_lines: List[str], pos: int) -> bool:
399
+ """Check for exact line-by-line match at position.
400
+
401
+ Args:
402
+ source_lines: List of source code lines
403
+ pattern_lines: List of pattern lines to match
404
+ pos: Position to check for match
298
405
 
299
- return matches
406
+ Returns:
407
+ bool: True if exact match found, False otherwise
408
+ """
409
+ if pos + len(pattern_lines) > len(source_lines):
410
+ return False
411
+ return all(source_lines[pos + j] == pattern_lines[j] for j in range(len(pattern_lines)))
@@ -0,0 +1,10 @@
1
+ from dataclasses import dataclass
2
+ from typing import Optional
3
+
4
+ @dataclass
5
+ class StrategyResult:
6
+ """Encapsulates the result of a strategy match attempt."""
7
+ success: bool
8
+ strategy_name: Optional[str] = None
9
+ match_position: Optional[int] = None
10
+ file_type: Optional[str] = None
janito/shell/__init__.py CHANGED
@@ -1,29 +1,28 @@
1
1
  """Shell package initialization for Janito."""
2
2
  from typing import Optional
3
3
  from prompt_toolkit import PromptSession
4
- from prompt_toolkit.history import FileHistory
5
- from pathlib import Path
6
4
  from rich.console import Console
7
5
  from janito.config import config
8
- from janito.workspace import workspace
6
+ from janito.workspace.workset import Workset
9
7
  from .processor import CommandProcessor
8
+ from .commands import register_commands
9
+ from .registry import CommandRegistry
10
10
 
11
11
  def start_shell() -> None:
12
12
  """Start the Janito interactive shell."""
13
- history_file = Path.home() / ".janito_history"
14
- session = PromptSession(history=FileHistory(str(history_file)))
15
- processor = CommandProcessor()
13
+ # Create single registry instance
14
+ registry = CommandRegistry()
15
+ register_commands(registry)
16
+
17
+ # Create shell components with shared registry
18
+ from .prompt import create_shell_session
19
+ session = create_shell_session(registry)
20
+ processor = CommandProcessor(registry)
16
21
 
17
- # Perform workspace analysis
18
- console = Console()
19
-
20
- # Use configured paths or default to workspace_dir
21
- scan_paths = config.include if config.include else [config.workspace_dir]
22
- workspace.collect_content(scan_paths)
23
- workspace.analyze()
24
-
25
- # Store workspace content in processor for session
26
- processor.workspace_content = workspace.get_content()
22
+ # Initialize and show workset content
23
+ workset = Workset()
24
+ workset.refresh()
25
+ workset.show()
27
26
 
28
27
  while True:
29
28
  try:
janito/shell/commands.py CHANGED
@@ -1,52 +1,15 @@
1
1
  """Command system for Janito shell."""
2
- from dataclasses import dataclass
3
- from typing import Optional, Callable, Dict
4
- from pathlib import Path
5
2
  from rich.console import Console
6
3
  from rich.table import Table
7
4
  from prompt_toolkit import PromptSession
8
5
  from prompt_toolkit.shortcuts import clear as ptk_clear
6
+ from prompt_toolkit.completion import PathCompleter
7
+ from prompt_toolkit.document import Document
8
+ from pathlib import Path
9
9
  from janito.config import config
10
- from janito.workspace import collect_files_content
10
+ from janito.workspace import workset # Updated import
11
11
  from janito.workspace.analysis import analyze_workspace_content
12
-
13
- @dataclass
14
- class Command:
15
- """Command definition with handler."""
16
- name: str
17
- description: str
18
- usage: Optional[str]
19
- handler: Callable[[str], None]
20
-
21
- class CommandSystem:
22
- """Centralized command management system."""
23
- _instance = None
24
- _commands: Dict[str, Command] = {}
25
-
26
- def __new__(cls):
27
- if cls._instance is None:
28
- cls._instance = super().__new__(cls)
29
- cls._instance._commands = {}
30
- return cls._instance
31
-
32
- def register(self, command: Command) -> None:
33
- """Register a command."""
34
- self._commands[command.name] = command
35
-
36
- def get_command(self, name: str) -> Optional[Command]:
37
- """Get a command by name."""
38
- return self._commands.get(name)
39
-
40
- def get_commands(self) -> Dict[str, Command]:
41
- """Get all registered commands."""
42
- return self._commands.copy()
43
-
44
- def register_alias(self, alias: str, command_name: str) -> None:
45
- """Register an alias for a command."""
46
- if command := self.get_command(command_name):
47
- if alias in self._commands:
48
- raise ValueError(f"Alias '{alias}' already registered")
49
- self._commands[alias] = command
12
+ from .registry import CommandRegistry, Command, get_path_completer
50
13
 
51
14
  def handle_request(args: str) -> None:
52
15
  """Handle a change request."""
@@ -75,23 +38,21 @@ def handle_ask(args: str) -> None:
75
38
  def handle_help(args: str) -> None:
76
39
  """Handle help command."""
77
40
  console = Console()
78
- system = CommandSystem()
79
-
41
+ registry = CommandRegistry()
80
42
  command = args.strip() if args else None
81
- if command and (cmd := system.get_command(command)):
43
+ if command and (cmd := registry.get_command(command)):
82
44
  console.print(f"\n[bold]{command}[/bold]: {cmd.description}")
83
45
  if cmd.usage:
84
46
  console.print(f"Usage: {cmd.usage}")
85
- return
47
+ else:
48
+ table = Table(title="Available Commands")
49
+ table.add_column("Command", style="cyan")
50
+ table.add_column("Description")
86
51
 
87
- table = Table(title="Available Commands")
88
- table.add_column("Command", style="cyan")
89
- table.add_column("Description")
52
+ for name, cmd in sorted(registry.get_commands().items()):
53
+ table.add_row(name, cmd.description)
90
54
 
91
- for name, cmd in sorted(system.get_commands().items()):
92
- table.add_row(name, cmd.description)
93
-
94
- console.print(table)
55
+ console.print(table)
95
56
 
96
57
  def handle_include(args: str) -> None:
97
58
  """Handle include command."""
@@ -116,42 +77,24 @@ def handle_include(args: str) -> None:
116
77
  path = config.workspace_dir / path
117
78
  resolved_paths.append(path.resolve())
118
79
 
119
- config.set_include(resolved_paths)
120
- content = collect_files_content(resolved_paths)
121
- analyze_workspace_content(content)
80
+ workset.include(resolved_paths)
81
+ workset.show()
122
82
 
123
83
  console.print("[green]Updated include paths:[/green]")
124
84
  for path in resolved_paths:
125
85
  console.print(f" {path}")
126
86
 
127
- from prompt_toolkit.completion import PathCompleter
128
- from prompt_toolkit.document import Document
129
-
130
87
  def handle_rinclude(args: str) -> None:
131
88
  """Handle recursive include command."""
132
89
  console = Console()
133
90
  session = PromptSession()
134
- completer = PathCompleter(only_directories=True)
91
+ completer = get_path_completer(only_directories=True)
135
92
 
136
- try:
137
- if not args:
93
+ if not args:
94
+ try:
138
95
  args = session.prompt("Enter directory paths (space separated): ", completer=completer)
139
- else:
140
- # For partial paths, show completion options
141
- doc = Document(args)
142
- completions = list(completer.get_completions(doc, None))
143
- if completions:
144
- # If single completion, use it directly
145
- if len(completions) == 1:
146
- args = completions[0].text
147
- else:
148
- # Show available completions
149
- console.print("\nAvailable directories:")
150
- for comp in completions:
151
- console.print(f" {comp.text}")
152
- return
153
- except (KeyboardInterrupt, EOFError):
154
- return
96
+ except (KeyboardInterrupt, EOFError):
97
+ return
155
98
 
156
99
  paths = [p.strip() for p in args.split() if p.strip()]
157
100
  if not paths:
@@ -165,31 +108,29 @@ def handle_rinclude(args: str) -> None:
165
108
  path = config.workspace_dir / path
166
109
  resolved_paths.append(path.resolve())
167
110
 
168
- config.set_recursive(resolved_paths)
169
- config.set_include(resolved_paths)
170
- content = collect_files_content(resolved_paths)
171
- analyze_workspace_content(content)
111
+ workset.recursive(resolved_paths)
112
+ workset.include(resolved_paths) # Add recursive paths to include paths
113
+ workset.refresh()
114
+ workset.show()
172
115
 
173
116
  console.print("[green]Updated recursive include paths:[/green]")
174
117
  for path in resolved_paths:
175
118
  console.print(f" {path}")
176
119
 
177
- def register_commands() -> None:
120
+ def register_commands(registry: CommandRegistry) -> None:
178
121
  """Register all available commands."""
179
- system = CommandSystem()
180
-
181
122
  # Register main commands
182
- system.register(Command("/clear", "Clear the terminal screen", None, handle_clear))
183
- system.register(Command("/request", "Submit a change request", "/request <change request text>", handle_request))
184
- system.register(Command("/ask", "Ask a question about the codebase", "/ask <question>", handle_ask))
185
- system.register(Command("/quit", "Exit the shell", None, handle_exit))
186
- system.register(Command("/help", "Show help for commands", "/help [command]", handle_help))
187
- system.register(Command("/include", "Set paths to include in analysis", "/include <path1> [path2 ...]", handle_include))
188
- system.register(Command("/rinclude", "Set paths to include recursively", "/rinclude <path1> [path2 ...]", handle_rinclude))
123
+ registry.register(Command("/clear", "Clear the terminal screen", None, handle_clear))
124
+ registry.register(Command("/request", "Submit a change request", "/request <change request text>", handle_request))
125
+ registry.register(Command("/ask", "Ask a question about the codebase", "/ask <question>", handle_ask))
126
+ registry.register(Command("/quit", "Exit the shell", None, handle_exit))
127
+ registry.register(Command("/help", "Show help for commands", "/help [command]", handle_help))
128
+ registry.register(Command("/include", "Set paths to include in analysis", "/include <path1> [path2 ...]", handle_include, get_path_completer()))
129
+ registry.register(Command("/rinclude", "Set paths to include recursively", "/rinclude <path1> [path2 ...]", handle_rinclude, get_path_completer(True)))
189
130
 
190
131
  # Register aliases
191
- system.register_alias("clear", "/clear")
192
- system.register_alias("quit", "/quit")
193
- system.register_alias("help", "/help")
194
- system.register_alias("/inc", "/include")
195
- system.register_alias("/rinc", "/rinclude")
132
+ registry.register_alias("clear", "/clear")
133
+ registry.register_alias("quit", "/quit")
134
+ registry.register_alias("help", "/help")
135
+ registry.register_alias("/inc", "/include")
136
+ registry.register_alias("/rinc", "/rinclude")
janito/shell/processor.py CHANGED
@@ -1,15 +1,16 @@
1
1
  """Command processor for Janito shell."""
2
2
  from typing import Optional
3
3
  from rich.console import Console
4
- from .commands import CommandSystem, register_commands
4
+ from .registry import CommandRegistry
5
5
 
6
6
  class CommandProcessor:
7
7
  """Processes shell commands."""
8
8
 
9
- def __init__(self):
9
+ def __init__(self, registry: CommandRegistry) -> None:
10
+ """Initialize command processor with registry."""
11
+ super().__init__()
10
12
  self.console = Console()
11
- self.workspace_content: Optional[str] = None
12
- register_commands()
13
+ self.registry = registry
13
14
 
14
15
  def process_command(self, command_line: str) -> None:
15
16
  """Process a command line input."""
@@ -21,32 +22,11 @@ class CommandProcessor:
21
22
  cmd = parts[0].lower()
22
23
  args = parts[1] if len(parts) > 1 else ""
23
24
 
24
- system = CommandSystem()
25
- if command := system.get_command(cmd):
26
- # Special handling for /rinc command to support interactive completion
27
- if cmd in ["/rinc", "/rinclude"] and args:
28
- from prompt_toolkit.completion import PathCompleter
29
- from prompt_toolkit.document import Document
30
-
31
- completer = PathCompleter(only_directories=True)
32
- doc = Document(args)
33
- completions = list(completer.get_completions(doc, None))
34
-
35
- if completions:
36
- if len(completions) == 1:
37
- # Single completion - use it
38
- args = completions[0].text
39
- else:
40
- # Show available completions
41
- self.console.print("\nAvailable directories:")
42
- for comp in completions:
43
- self.console.print(f" {comp.text}")
44
- return
45
-
25
+ if command := self.registry.get_command(cmd):
46
26
  command.handler(args)
47
27
  else:
48
28
  # Treat as request command
49
- if request_cmd := system.get_command("/request"):
29
+ if request_cmd := self.registry.get_command("/request"):
50
30
  request_cmd.handler(command_line)
51
31
  else:
52
32
  self.console.print("[red]Error: Request command not registered[/red]")
janito/shell/prompt.py ADDED
@@ -0,0 +1,48 @@
1
+ """Prompt creation and configuration for Janito shell."""
2
+ from typing import Dict, Any
3
+ from pathlib import Path
4
+ from prompt_toolkit import PromptSession
5
+ from prompt_toolkit.history import FileHistory
6
+ from prompt_toolkit.completion import NestedCompleter
7
+ from rich import print as rich_print
8
+ from janito.config import config
9
+ from .registry import CommandRegistry
10
+
11
+ def create_shell_completer(registry: CommandRegistry):
12
+ """Create command completer for shell with nested completions."""
13
+ if config.debug:
14
+ rich_print("[yellow]Creating shell completer...[/yellow]")
15
+
16
+ commands = registry.get_commands()
17
+
18
+ if config.debug:
19
+ rich_print(f"[yellow]Found {len(commands)} commands for completion[/yellow]")
20
+
21
+ # Create nested completions for commands
22
+ completions: Dict[str, Any] = {}
23
+
24
+ for cmd_name, cmd in commands.items():
25
+ if config.debug:
26
+ rich_print(f"[yellow]Setting up completion for command: {cmd_name}[/yellow]")
27
+ completions[cmd_name] = cmd.completer
28
+
29
+ if config.debug:
30
+ rich_print("[yellow]Creating nested completer from completions dictionary[/yellow]")
31
+ return NestedCompleter.from_nested_dict(completions)
32
+
33
+ def create_shell_session(registry: CommandRegistry) -> PromptSession:
34
+ """Create and configure the shell prompt session."""
35
+ if config.debug:
36
+ rich_print("[yellow]Creating shell session...[/yellow]")
37
+
38
+ history_file = Path.home() / ".janito_history"
39
+ if config.debug:
40
+ rich_print(f"[yellow]Using history file: {history_file}[/yellow]")
41
+
42
+ completer = create_shell_completer(registry)
43
+
44
+ return PromptSession(
45
+ history=FileHistory(str(history_file)),
46
+ completer=completer,
47
+ complete_while_typing=True
48
+ )