janito 0.5.0__py3-none-any.whl → 0.6.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 (106) hide show
  1. janito/__init__.py +0 -47
  2. janito/__main__.py +96 -15
  3. janito/agents/__init__.py +2 -8
  4. janito/agents/claudeai.py +3 -12
  5. janito/change/__init__.py +29 -16
  6. janito/change/__main__.py +0 -0
  7. janito/{analysis → change/analysis}/__init__.py +5 -15
  8. janito/change/analysis/__main__.py +7 -0
  9. janito/change/analysis/analyze.py +61 -0
  10. janito/change/analysis/formatting.py +78 -0
  11. janito/change/analysis/options.py +81 -0
  12. janito/{analysis → change/analysis}/prompts.py +35 -12
  13. janito/change/analysis/view/__init__.py +9 -0
  14. janito/change/analysis/view/terminal.py +171 -0
  15. janito/change/applier/__init__.py +5 -0
  16. janito/change/applier/file.py +58 -0
  17. janito/change/applier/main.py +156 -0
  18. janito/change/applier/text.py +245 -0
  19. janito/change/applier/workspace_dir.py +58 -0
  20. janito/change/core.py +131 -0
  21. janito/{changehistory.py → change/history.py} +12 -14
  22. janito/change/operations.py +7 -0
  23. janito/change/parser.py +289 -0
  24. janito/change/play.py +54 -0
  25. janito/change/preview.py +82 -0
  26. janito/change/prompts.py +126 -0
  27. janito/change/test.py +0 -0
  28. janito/change/validator.py +251 -0
  29. janito/{changeviewer → change/viewer}/__init__.py +3 -4
  30. janito/change/viewer/content.py +66 -0
  31. janito/{changeviewer → change/viewer}/diff.py +19 -4
  32. janito/change/viewer/pager.py +56 -0
  33. janito/change/viewer/panels.py +555 -0
  34. janito/change/viewer/styling.py +103 -0
  35. janito/{changeviewer → change/viewer}/themes.py +3 -5
  36. janito/clear_statement_parser/clear_statement_format.txt +328 -0
  37. janito/clear_statement_parser/examples.txt +326 -0
  38. janito/clear_statement_parser/models.py +104 -0
  39. janito/clear_statement_parser/parser.py +496 -0
  40. janito/cli/base.py +30 -0
  41. janito/cli/commands.py +30 -38
  42. janito/cli/functions.py +19 -194
  43. janito/cli/handlers/ask.py +22 -0
  44. janito/cli/handlers/demo.py +22 -0
  45. janito/cli/handlers/request.py +24 -0
  46. janito/cli/handlers/scan.py +9 -0
  47. janito/cli/history.py +61 -0
  48. janito/common.py +34 -3
  49. janito/config.py +71 -6
  50. janito/demo/__init__.py +4 -0
  51. janito/demo/data.py +13 -0
  52. janito/demo/mock_data.py +20 -0
  53. janito/demo/operations.py +45 -0
  54. janito/demo/runner.py +59 -0
  55. janito/demo/scenarios.py +32 -0
  56. janito/prompts.py +1 -80
  57. janito/qa.py +4 -3
  58. janito/search_replace/README.md +146 -0
  59. janito/search_replace/__init__.py +6 -0
  60. janito/search_replace/__main__.py +21 -0
  61. janito/search_replace/core.py +119 -0
  62. janito/search_replace/parser.py +52 -0
  63. janito/search_replace/play.py +61 -0
  64. janito/search_replace/replacer.py +36 -0
  65. janito/search_replace/searcher.py +299 -0
  66. janito/shell/__init__.py +39 -0
  67. janito/shell/bus.py +31 -0
  68. janito/shell/commands.py +195 -0
  69. janito/shell/handlers.py +122 -0
  70. janito/shell/history.py +20 -0
  71. janito/shell/processor.py +52 -0
  72. janito/tui/__init__.py +21 -0
  73. janito/tui/base.py +22 -0
  74. janito/tui/flows/__init__.py +5 -0
  75. janito/tui/flows/changes.py +65 -0
  76. janito/tui/flows/content.py +128 -0
  77. janito/tui/flows/selection.py +117 -0
  78. janito/tui/screens/__init__.py +3 -0
  79. janito/tui/screens/app.py +1 -0
  80. janito/workspace/__init__.py +7 -0
  81. janito/workspace/analysis.py +121 -0
  82. janito/workspace/manager.py +48 -0
  83. janito/workspace/scan.py +232 -0
  84. janito-0.6.0.dist-info/METADATA +185 -0
  85. janito-0.6.0.dist-info/RECORD +95 -0
  86. {janito-0.5.0.dist-info → janito-0.6.0.dist-info}/WHEEL +1 -1
  87. janito/_contextparser.py +0 -113
  88. janito/analysis/display.py +0 -149
  89. janito/analysis/options.py +0 -112
  90. janito/change/applier.py +0 -269
  91. janito/change/content.py +0 -62
  92. janito/change/indentation.py +0 -33
  93. janito/change/position.py +0 -169
  94. janito/changeviewer/panels.py +0 -268
  95. janito/changeviewer/styling.py +0 -59
  96. janito/console/__init__.py +0 -3
  97. janito/console/commands.py +0 -112
  98. janito/console/core.py +0 -62
  99. janito/console/display.py +0 -157
  100. janito/fileparser.py +0 -334
  101. janito/scan.py +0 -176
  102. janito/tests/test_fileparser.py +0 -26
  103. janito-0.5.0.dist-info/METADATA +0 -146
  104. janito-0.5.0.dist-info/RECORD +0 -45
  105. {janito-0.5.0.dist-info → janito-0.6.0.dist-info}/entry_points.txt +0 -0
  106. {janito-0.5.0.dist-info → janito-0.6.0.dist-info}/licenses/LICENSE +0 -0
janito/__init__.py CHANGED
@@ -1,49 +1,2 @@
1
1
  """Core package initialization for Janito."""
2
2
 
3
- from .analysis import (
4
- AnalysisOption,
5
- parse_analysis_options,
6
- format_analysis,
7
- get_history_file_type,
8
- get_history_path,
9
- get_timestamp,
10
- save_to_file,
11
- build_request_analysis_prompt,
12
- get_option_selection,
13
- prompt_user,
14
- validate_option_letter
15
- )
16
-
17
- from .change import (
18
- apply_single_change,
19
- parse_and_apply_changes_sequence,
20
- get_file_type,
21
- process_and_save_changes,
22
- format_parsed_changes,
23
- apply_content_changes,
24
- handle_changes_file
25
- )
26
-
27
- __all__ = [
28
- # Analysis exports
29
- 'AnalysisOption',
30
- 'parse_analysis_options',
31
- 'format_analysis',
32
- 'get_history_file_type',
33
- 'get_history_path',
34
- 'get_timestamp',
35
- 'save_to_file',
36
- 'build_request_analysis_prompt',
37
- 'get_option_selection',
38
- 'prompt_user',
39
- 'validate_option_letter',
40
-
41
- # Change exports
42
- 'apply_single_change',
43
- 'parse_and_apply_changes_sequence',
44
- 'get_file_type',
45
- 'process_and_save_changes',
46
- 'format_parsed_changes',
47
- 'apply_content_changes',
48
- 'handle_changes_file'
49
- ]
janito/__main__.py CHANGED
@@ -1,25 +1,63 @@
1
1
  import typer
2
- from typing import Optional, List
2
+ from typing import Optional, List, Set
3
3
  from pathlib import Path
4
+ from rich.text import Text
5
+ from rich import print as rich_print
4
6
  from rich.console import Console
7
+ from rich.text import Text
5
8
  from .version import get_version
6
9
 
7
- from janito.agents import AgentSingleton
10
+ from janito.agents import agent
8
11
  from janito.config import config
9
12
 
10
13
  from .cli.commands import handle_request, handle_ask, handle_play, handle_scan
11
14
 
12
15
  app = typer.Typer(add_completion=False)
13
16
 
17
+ def validate_paths(paths: Optional[List[Path]]) -> Optional[List[Path]]:
18
+ """Validate include paths for duplicates.
19
+
20
+ Args:
21
+ paths: List of paths to validate, or None if no paths provided
22
+
23
+ Returns:
24
+ Validated list of paths or None if no paths provided
25
+ """
26
+ if not paths: # This handles both None and empty list cases
27
+ return None
28
+
29
+ # Convert paths to absolute and resolve symlinks
30
+ resolved_paths: Set[Path] = set()
31
+ unique_paths: List[Path] = []
32
+
33
+ for path in paths:
34
+ resolved = path.absolute().resolve()
35
+ if resolved in resolved_paths:
36
+ error_text = Text(f"\nError: Duplicate path provided: {path} ", style="red")
37
+ rich_print(error_text)
38
+ raise typer.Exit(1)
39
+ resolved_paths.add(resolved)
40
+ unique_paths.append(path)
41
+
42
+ return unique_paths if unique_paths else None
43
+
14
44
  def typer_main(
15
45
  change_request: str = typer.Argument(None, help="Change request or command"),
16
- workdir: Optional[Path] = typer.Option(None, "-w", "--workdir", help="Working directory", file_okay=False, dir_okay=True),
46
+ workspace_dir: Optional[Path] = typer.Option(None, "-w", "--workspace_dir", help="Working directory", file_okay=False, dir_okay=True),
17
47
  debug: bool = typer.Option(False, "--debug", help="Show debug information"),
18
48
  verbose: bool = typer.Option(False, "--verbose", help="Show verbose output"),
19
49
  include: Optional[List[Path]] = typer.Option(None, "-i", "--include", help="Additional paths to include"),
20
50
  ask: Optional[str] = typer.Option(None, "--ask", help="Ask a question about the codebase"),
21
51
  play: Optional[Path] = typer.Option(None, "--play", help="Replay a saved prompt file"),
52
+ scan: bool = typer.Option(False, "--scan", help="Preview files that would be analyzed"),
22
53
  version: bool = typer.Option(False, "--version", help="Show version information"),
54
+ test_cmd: Optional[str] = typer.Option(None, "--test", help="Command to run tests after changes"),
55
+ auto_apply: bool = typer.Option(False, "--auto-apply", help="Apply changes without confirmation"),
56
+ tui: bool = typer.Option(False, "--tui", help="Use terminal user interface"),
57
+ history: bool = typer.Option(False, "--history", help="Display history of requests"),
58
+ recursive: Optional[List[Path]] = typer.Option(None, "-r", "--recursive", help="Paths to scan recursively (directories only)"),
59
+ demo: bool = typer.Option(False, "--demo", help="Run demo scenarios"),
60
+ skipwork: bool = typer.Option(False, "--skipwork", help="Skip scanning workspace_dir when using include paths"),
23
61
  ):
24
62
  """Janito - AI-powered code modification assistant"""
25
63
  if version:
@@ -27,28 +65,71 @@ def typer_main(
27
65
  console.print(f"Janito version {get_version()}")
28
66
  return
29
67
 
30
- workdir = workdir or Path.cwd()
68
+ if demo:
69
+ from janito.cli.handlers.demo import DemoHandler
70
+ handler = DemoHandler()
71
+ handler.handle()
72
+ return
73
+
74
+ if history:
75
+ from janito.cli.history import display_history
76
+ display_history()
77
+ return
78
+
79
+ config.set_workspace_dir(workspace_dir)
31
80
  config.set_debug(debug)
32
81
  config.set_verbose(verbose)
82
+ config.set_auto_apply(auto_apply)
83
+ config.set_include(include)
84
+ config.set_tui(tui)
85
+ config.set_skipwork(skipwork)
86
+
87
+ # Validate skipwork usage
88
+ if skipwork and not include and not recursive:
89
+ error_text = Text("\nError: --skipwork requires at least one include path (-i or -r)", style="red")
90
+ rich_print(error_text)
91
+ raise typer.Exit(1)
92
+
93
+ if include:
94
+ resolved_paths = []
95
+ for path in include:
96
+ path = config.workspace_dir / path
97
+ resolved_paths.append(path.resolve())
98
+ config.set_include(resolved_paths)
33
99
 
34
- agent = AgentSingleton.get_agent()
100
+ # Validate recursive paths
101
+ if recursive:
102
+ resolved_paths = []
103
+ for path in recursive:
104
+ final_path = config.workspace_dir / path
105
+ if not path.is_dir():
106
+ error_text = Text(f"\nError: Recursive path must be a directory: {path} ", style="red")
107
+ rich_print(error_text)
108
+ raise typer.Exit(1)
109
+ resolved_paths.append(final_path.resolve())
110
+ config.set_recursive(resolved_paths)
111
+ include = include or []
112
+ include.extend(resolved_paths)
113
+ config.set_include(include)
114
+
115
+ if test_cmd:
116
+ config.set_test_cmd(test_cmd)
35
117
 
36
118
  if ask:
37
- handle_ask(ask, workdir, include, False, agent)
119
+ handle_ask(ask)
38
120
  elif play:
39
- handle_play(play, workdir, False)
40
- elif change_request == "scan":
41
- paths_to_scan = include if include else [workdir]
42
- handle_scan(paths_to_scan, workdir)
121
+ handle_play(play)
122
+ elif scan:
123
+ paths_to_scan = include or [config.workspace_dir]
124
+ handle_scan(paths_to_scan)
43
125
  elif change_request:
44
- handle_request(change_request, workdir, include, False, agent)
126
+ handle_request(change_request)
45
127
  else:
46
- console = Console()
47
- console.print("Error: Please provide a change request or use --ask/--play options")
48
- raise typer.Exit(1)
128
+ from janito.shell import start_shell
129
+ start_shell()
49
130
 
50
131
  def main():
51
132
  typer.run(typer_main)
52
133
 
53
134
  if __name__ == "__main__":
54
- main()
135
+ main()
janito/agents/__init__.py CHANGED
@@ -11,12 +11,6 @@ elif ai_backend == 'claudeai':
11
11
  else:
12
12
  raise ValueError(f"Unsupported AI_BACKEND: {ai_backend}")
13
13
 
14
- class AgentSingleton:
15
- _instance = None
16
-
17
- @classmethod
18
- def get_agent(cls):
19
- if cls._instance is None:
20
- cls._instance = AIAgent(SYSTEM_PROMPT)
21
- return cls._instance
14
+ # Create a singleton instance
15
+ agent = AIAgent(SYSTEM_PROMPT)
22
16
 
janito/agents/claudeai.py CHANGED
@@ -22,9 +22,7 @@ class ClaudeAIAgent(Agent):
22
22
  self.last_prompt = None
23
23
  self.last_full_message = None
24
24
  self.last_response = None
25
- self.messages_history = []
26
- if system_prompt:
27
- self.messages_history.append(("system", system_prompt))
25
+
28
26
 
29
27
  def send_message(self, message: str, stop_event: Event = None) -> str:
30
28
  """Send message to Claude API and return response"""
@@ -47,16 +45,9 @@ class ClaudeAIAgent(Agent):
47
45
  temperature=0,
48
46
  )
49
47
 
50
- # Handle response
51
- response_text = response.content[0].text
52
-
53
- # Only store and process response if not cancelled
54
- if not (stop_event and stop_event.is_set()):
55
- self.last_response = response_text
56
- self.messages_history.append(("assistant", response_text))
57
-
48
+
58
49
  # Always return the response, let caller handle cancellation
59
- return response_text
50
+ return response
60
51
 
61
52
  except KeyboardInterrupt:
62
53
  if stop_event:
janito/change/__init__.py CHANGED
@@ -1,19 +1,32 @@
1
- from .applier import apply_single_change
2
- from .position import parse_and_apply_changes_sequence
3
- from .content import (
4
- get_file_type,
5
- process_and_save_changes,
6
- format_parsed_changes,
7
- apply_content_changes,
8
- handle_changes_file
9
- )
1
+ """
2
+ This package provides the following change flow steps:
3
+
4
+ - Create a preview directory
5
+ - Build change request prompt
6
+ - Send change request to AI agent
7
+ - Parse the response into changes
8
+ - Save response to history file
9
+ - Preview the changes (applying them to the preview directory)
10
+ - Validate the changes
11
+ - Run tests if specified
12
+ - Show the change view (using janito.changeviewer)
13
+ - Prompt the user to apply the changes to the working directory
14
+ - Apply the changes
15
+ """
16
+
17
+ from typing import Tuple
18
+ from pathlib import Path
19
+ from ..agents import agent # Updated import to use singleton directly
20
+ from .parser import build_change_request_prompt, parse_response
21
+ from .preview import setup_workspace_dir_preview
22
+ from .applier.main import ChangeApplier
10
23
 
11
24
  __all__ = [
12
- 'apply_single_change',
13
- 'parse_and_apply_changes_sequence',
14
- 'get_file_type',
15
- 'process_and_save_changes',
16
- 'format_parsed_changes',
17
- 'apply_content_changes',
18
- 'handle_changes_file'
25
+ 'build_change_request_prompt',
26
+ 'get_change_response',
27
+ 'parse_response',
28
+ 'setup_workspace_dir_preview',
29
+ 'parse_change_response',
30
+ 'save_change_response',
31
+ 'ChangeApplier'
19
32
  ]
File without changes
@@ -4,30 +4,20 @@ This module provides functionality for analyzing and displaying code changes.
4
4
  """
5
5
 
6
6
  from .options import AnalysisOption, parse_analysis_options
7
- from .display import (
8
- format_analysis,
9
- get_history_file_type,
10
- get_history_path,
11
- get_timestamp,
12
- save_to_file
13
- )
7
+ from .view import format_analysis, prompt_user, get_option_selection
14
8
  from .prompts import (
15
9
  build_request_analysis_prompt,
16
- get_option_selection,
17
- prompt_user,
18
10
  validate_option_letter
19
11
  )
12
+ from .analyze import analyze_request
20
13
 
21
14
  __all__ = [
22
15
  'AnalysisOption',
23
16
  'parse_analysis_options',
24
17
  'format_analysis',
25
- 'get_history_file_type',
26
- 'get_history_path',
27
- 'get_timestamp',
28
- 'save_to_file',
29
18
  'build_request_analysis_prompt',
30
19
  'get_option_selection',
31
20
  'prompt_user',
32
- 'validate_option_letter'
33
- ]
21
+ 'validate_option_letter',
22
+ 'analyze_request'
23
+ ]
@@ -0,0 +1,7 @@
1
+ """Main entry point for the analysis module."""
2
+
3
+ from .analyze import analyze_request
4
+ from janito.config import config
5
+ from janito.workspace import collect_files_content
6
+ from pathlib import Path
7
+
@@ -0,0 +1,61 @@
1
+ """Core analysis functionality."""
2
+
3
+ from typing import Optional, Dict
4
+
5
+ from janito.agents import agent
6
+ from janito.common import progress_send_message
7
+ from janito.config import config
8
+ from .view import format_analysis
9
+ from .options import AnalysisOption, parse_analysis_options
10
+ from .prompts import (
11
+ build_request_analysis_prompt,
12
+ get_option_selection,
13
+ validate_option_letter
14
+ )
15
+
16
+ def analyze_request(
17
+ request: str,
18
+ files_content_xml: str,
19
+ pre_select: str = ""
20
+ ) -> Optional[AnalysisOption]:
21
+ """
22
+ Analyze changes and get user selection.
23
+
24
+ Args:
25
+ files_content: Content of files to analyze
26
+ request: User's change request
27
+ pre_select: Optional pre-selected option letter
28
+
29
+ Returns:
30
+ Selected AnalysisOption or None if modified
31
+ """
32
+ # Build and send prompt
33
+ prompt = build_request_analysis_prompt(request, files_content_xml)
34
+ response = progress_send_message(prompt)
35
+
36
+ # Parse options
37
+ options = parse_analysis_options(response)
38
+ if not options:
39
+ return None
40
+
41
+ if pre_select:
42
+ return options.get(pre_select.upper())
43
+
44
+ if config.tui:
45
+ from janito.tui import TuiApp
46
+ app = TuiApp(options=options)
47
+ app.run()
48
+ return app.selected_option
49
+
50
+ # Display formatted analysis in terminal mode
51
+ format_analysis(response, config.raw)
52
+
53
+ # Get user selection
54
+ while True:
55
+ selection = get_option_selection()
56
+
57
+ if selection == 'M':
58
+ return None
59
+
60
+ if validate_option_letter(selection, options):
61
+ return options[selection.upper()]
@@ -0,0 +1,78 @@
1
+ """Centralized formatting utilities for analysis display."""
2
+
3
+ from typing import Dict, List, Text
4
+ from rich.text import Text
5
+ from rich.columns import Columns
6
+ from rich.padding import Padding
7
+ from pathlib import Path
8
+
9
+ # Layout constants
10
+ COLUMN_SPACING = 6 # Increased spacing between columns
11
+ MIN_PANEL_WIDTH = 45 # Wider minimum width for better readability
12
+ SECTION_PADDING = (2, 0) # More vertical padding
13
+
14
+ # Color scheme constants
15
+ STATUS_COLORS = {
16
+ 'new': 'bright_green',
17
+ 'modified': 'yellow',
18
+ 'removed': 'red',
19
+ 'default': 'white'
20
+ }
21
+
22
+ STRUCTURAL_COLORS = {
23
+ 'directory': 'dim',
24
+ 'separator': 'blue dim',
25
+ 'repeat': 'bright_magenta bold'
26
+ }
27
+
28
+ def create_header(text: str, style: str = "bold cyan") -> Text:
29
+ """Create formatted header with separator."""
30
+ content = Text()
31
+ content.append(text, style=style)
32
+ content.append("\n")
33
+ content.append("═" * len(text), style="cyan")
34
+ return content
35
+
36
+ def create_section_header(text: str, width: int = 20) -> Text:
37
+ """Create centered section header with separator."""
38
+ content = Text()
39
+ padding = (width - len(text)) // 2
40
+ content.append(" " * padding + text, style="bold cyan")
41
+ content.append("\n")
42
+ content.append("─" * width, style="cyan")
43
+ return content
44
+
45
+ def format_file_path(path: str, status: str, max_dir_length: int = 0, is_repeated: bool = False) -> Text:
46
+ """Format file path with status indicators and consistent alignment.
47
+
48
+ Args:
49
+ path: File path to format
50
+ status: File status (Modified, New, Removed)
51
+ max_dir_length: Maximum directory name length for padding
52
+ is_repeated: Whether this directory was seen before
53
+ """
54
+ content = Text()
55
+ style = STATUS_COLORS.get(status.lower(), STATUS_COLORS['default'])
56
+
57
+ parts = Path(path).parts
58
+ parent_dir = str(Path(path).parent)
59
+
60
+ if parent_dir != '.':
61
+ # Add 4 spaces for consistent base padding
62
+ base_padding = 4
63
+ dir_padding = max_dir_length + base_padding
64
+
65
+ if is_repeated:
66
+ # Add arrow with consistent spacing
67
+ content.append(" " * base_padding)
68
+ content.append("↑ ", style="magenta")
69
+ content.append(" " * (dir_padding - base_padding - 2))
70
+ else:
71
+ # Left-align directory name with consistent padding
72
+ content.append(" " * base_padding)
73
+ content.append(parent_dir, style="dim")
74
+ content.append(" " * (dir_padding - len(parent_dir) - base_padding))
75
+
76
+ # Add filename with consistent spacing
77
+ content.append(parts[-1], style=style)
78
+ return content
@@ -0,0 +1,81 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Dict, List
3
+ from pathlib import Path
4
+
5
+ @dataclass
6
+ class AnalysisOption:
7
+ """Represents an analysis option with letter identifier and details"""
8
+ letter: str
9
+ summary: str
10
+ description_items: List[str] = field(default_factory=list)
11
+ affected_files: List[str] = field(default_factory=list)
12
+
13
+ def format_option_text(self) -> str:
14
+ """Format option details as text for change core"""
15
+ text = f"Option {self.letter} - {self.summary}\n"
16
+ text += "=" * len(f"Option {self.letter} - {self.summary}") + "\n\n"
17
+
18
+ if self.description_items:
19
+ text += "Description:\n"
20
+ for item in self.description_items:
21
+ text += f"- {item}\n"
22
+ text += "\n"
23
+
24
+ if self.affected_files:
25
+ text += "Affected files:\n"
26
+ for file in self.affected_files:
27
+ text += f"- {file}\n"
28
+
29
+ return text
30
+
31
+ def is_new_directory(self, file_path: str) -> bool:
32
+ """Check if file path represents the first occurrence of a directory"""
33
+ parent = str(Path(file_path).parent)
34
+ return parent != '.' and not any(
35
+ parent in self.get_clean_path(file)
36
+ for file in self.affected_files
37
+ if self.get_clean_path(file) != file_path
38
+ )
39
+
40
+ def get_clean_path(self, file_path: str) -> str:
41
+ """Remove status markers from file path"""
42
+ return file_path.split(' (')[0].strip()
43
+
44
+ def parse_analysis_options(content: str) -> Dict[str, AnalysisOption]:
45
+ """Parse analysis options from formatted text file"""
46
+ options = {}
47
+ current_option = None
48
+ current_section = None
49
+
50
+ for line in content.splitlines():
51
+ line = line.strip()
52
+
53
+ # Skip empty lines and section separators
54
+ if not line or line.startswith('---') or line == 'END_OF_OPTIONS':
55
+ continue
56
+
57
+ # New option starts with a letter and period
58
+ if line[0].isalpha() and line[1:3] == '. ':
59
+ letter, summary = line.split('. ', 1)
60
+ current_option = AnalysisOption(letter=letter.upper(), summary=summary)
61
+ options[letter.upper()] = current_option
62
+ current_section = None
63
+ continue
64
+
65
+ # Section headers
66
+ if line.lower() == 'description:':
67
+ current_section = 'description'
68
+ continue
69
+ elif line.lower() == 'affected files:':
70
+ current_section = 'files'
71
+ continue
72
+
73
+ # Add items to current section
74
+ if current_option and line.startswith('- '):
75
+ content = line[2:].strip()
76
+ if current_section == 'description':
77
+ current_option.description_items.append(content)
78
+ elif current_section == 'files':
79
+ current_option.affected_files.append(content)
80
+
81
+ return options
@@ -1,12 +1,15 @@
1
1
  """User prompts and input handling for analysis."""
2
2
 
3
- from typing import List
3
+ from typing import List, Dict
4
4
  from rich.console import Console
5
5
  from rich.panel import Panel
6
6
  from rich.rule import Rule
7
7
  from rich.prompt import Prompt
8
8
  from rich import box
9
9
 
10
+
11
+ from .options import AnalysisOption
12
+
10
13
  # Keep only prompt-related functionality
11
14
  CHANGE_ANALYSIS_PROMPT = """
12
15
  Current files:
@@ -16,8 +19,9 @@ Current files:
16
19
 
17
20
  Considering the above current files content, provide 3 sections, each identified by a keyword and representing an option.
18
21
  Each option should include a concise description and a list of affected files.
19
- 1st option should be minimalistic style change, 2nd organized style, 3rd exntensible style.
20
- Do not use style as keyword, instead focus on the changes summaru
22
+ 1st option should be basic style change, 2nd organized style, 3rd exntensible style.
23
+ Do not use style as keyword, instead focus on the changes summary.
24
+
21
25
  Use the following format:
22
26
 
23
27
  A. Keyword summary of the change
@@ -44,32 +48,51 @@ Request:
44
48
  def prompt_user(message: str, choices: List[str] = None) -> str:
45
49
  """Display a prominent user prompt with optional choices"""
46
50
  console = Console()
51
+ term_width = console.width or 80
47
52
  console.print()
48
- console.print(Rule(" User Input Required ", style="bold cyan"))
53
+ console.print(Rule(" User Input Required ", style="bold cyan", align="center"))
49
54
 
50
55
  if choices:
51
56
  choice_text = f"[cyan]Options: {', '.join(choices)}[/cyan]"
52
- console.print(Panel(choice_text, box=box.ROUNDED))
57
+ console.print(Panel(choice_text, box=box.ROUNDED, justify="center"))
53
58
 
54
- return Prompt.ask(f"[bold cyan]> {message}[/bold cyan]")
59
+ # Center the prompt with padding
60
+ padding = (term_width - len(message)) // 2
61
+ padded_message = " " * padding + message
62
+ return Prompt.ask(f"[bold cyan]{padded_message}[/bold cyan]")
55
63
 
56
- def validate_option_letter(letter: str, options: dict) -> bool:
64
+ def validate_option_letter(letter: str, options: Dict[str, AnalysisOption]) -> bool:
57
65
  """Validate if the given letter is a valid option or 'M' for modify"""
58
- return letter.upper() in options or letter.upper() == 'M'
66
+ if letter.upper() == 'M':
67
+ return True
68
+ return letter.upper() in options
59
69
 
60
70
  def get_option_selection() -> str:
61
71
  """Get user input for option selection with modify option"""
62
72
  console = Console()
63
- console.print("\n[cyan]Enter option letter or 'M' to modify request[/cyan]")
73
+ term_width = console.width or 80
74
+ message = "Enter option letter or 'M' to modify request"
75
+ padding = (term_width - len(message)) // 2
76
+ padded_message = " " * padding + message
77
+
78
+ console.print(f"\n[cyan]{padded_message}[/cyan]")
64
79
  while True:
65
80
  letter = prompt_user("Select option").strip().upper()
66
81
  if letter == 'M' or (letter.isalpha() and len(letter) == 1):
67
82
  return letter
68
- console.print("[red]Please enter a valid letter or 'M'[/red]")
83
+
84
+ error_msg = "Please enter a valid letter or 'M'"
85
+ error_padding = (term_width - len(error_msg)) // 2
86
+ padded_error = " " * error_padding + error_msg
87
+ console.print(f"[red]{padded_error}[/red]")
69
88
 
70
- def build_request_analysis_prompt(files_content: str, request: str) -> str:
89
+ def build_request_analysis_prompt(request: str, files_content_xml: str) -> str:
71
90
  """Build prompt for information requests"""
72
91
  return CHANGE_ANALYSIS_PROMPT.format(
73
- files_content=files_content,
92
+ files_content=files_content_xml,
74
93
  request=request
75
94
  )
95
+
96
+ def build_request_analysis_prompt(request: str, files_content_xml: str) -> str:
97
+ """Build analysis prompt with minimal formatting."""
98
+ return f"Current files:\n{files_content_xml}\n\nRequest:\n{request}"
@@ -0,0 +1,9 @@
1
+ """View package for analysis UI components."""
2
+
3
+ from .terminal import format_analysis, prompt_user, get_option_selection
4
+
5
+ __all__ = [
6
+ 'format_analysis',
7
+ 'prompt_user',
8
+ 'get_option_selection'
9
+ ]