janito 0.5.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 (110) hide show
  1. janito/__init__.py +0 -47
  2. janito/__main__.py +105 -17
  3. janito/agents/__init__.py +9 -9
  4. janito/agents/agent.py +10 -3
  5. janito/agents/claudeai.py +15 -34
  6. janito/agents/openai.py +5 -1
  7. janito/change/__init__.py +29 -16
  8. janito/change/__main__.py +0 -0
  9. janito/{analysis → change/analysis}/__init__.py +5 -15
  10. janito/change/analysis/__main__.py +7 -0
  11. janito/change/analysis/analyze.py +62 -0
  12. janito/change/analysis/formatting.py +78 -0
  13. janito/change/analysis/options.py +81 -0
  14. janito/{analysis → change/analysis}/prompts.py +33 -18
  15. janito/change/analysis/view/__init__.py +9 -0
  16. janito/change/analysis/view/terminal.py +181 -0
  17. janito/change/applier/__init__.py +5 -0
  18. janito/change/applier/file.py +58 -0
  19. janito/change/applier/main.py +156 -0
  20. janito/change/applier/text.py +247 -0
  21. janito/change/applier/workspace_dir.py +58 -0
  22. janito/change/core.py +124 -0
  23. janito/{changehistory.py → change/history.py} +12 -14
  24. janito/change/operations.py +7 -0
  25. janito/change/parser.py +287 -0
  26. janito/change/play.py +54 -0
  27. janito/change/preview.py +82 -0
  28. janito/change/prompts.py +121 -0
  29. janito/change/test.py +0 -0
  30. janito/change/validator.py +269 -0
  31. janito/{changeviewer → change/viewer}/__init__.py +3 -4
  32. janito/change/viewer/content.py +66 -0
  33. janito/{changeviewer → change/viewer}/diff.py +19 -4
  34. janito/change/viewer/panels.py +533 -0
  35. janito/change/viewer/styling.py +114 -0
  36. janito/{changeviewer → change/viewer}/themes.py +3 -5
  37. janito/clear_statement_parser/clear_statement_format.txt +328 -0
  38. janito/clear_statement_parser/examples.txt +326 -0
  39. janito/clear_statement_parser/models.py +104 -0
  40. janito/clear_statement_parser/parser.py +496 -0
  41. janito/cli/base.py +30 -0
  42. janito/cli/commands.py +75 -40
  43. janito/cli/functions.py +19 -194
  44. janito/cli/history.py +61 -0
  45. janito/common.py +65 -8
  46. janito/config.py +70 -5
  47. janito/demo/__init__.py +4 -0
  48. janito/demo/data.py +13 -0
  49. janito/demo/mock_data.py +20 -0
  50. janito/demo/operations.py +45 -0
  51. janito/demo/runner.py +59 -0
  52. janito/demo/scenarios.py +32 -0
  53. janito/prompt.py +36 -0
  54. janito/qa.py +6 -14
  55. janito/search_replace/README.md +192 -0
  56. janito/search_replace/__init__.py +7 -0
  57. janito/search_replace/__main__.py +21 -0
  58. janito/search_replace/core.py +120 -0
  59. janito/search_replace/logger.py +35 -0
  60. janito/search_replace/parser.py +52 -0
  61. janito/search_replace/play.py +61 -0
  62. janito/search_replace/replacer.py +36 -0
  63. janito/search_replace/searcher.py +411 -0
  64. janito/search_replace/strategy_result.py +10 -0
  65. janito/shell/__init__.py +38 -0
  66. janito/shell/bus.py +31 -0
  67. janito/shell/commands.py +136 -0
  68. janito/shell/history.py +20 -0
  69. janito/shell/processor.py +32 -0
  70. janito/shell/prompt.py +48 -0
  71. janito/shell/registry.py +60 -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 +6 -0
  81. janito/workspace/analysis.py +121 -0
  82. janito/workspace/show.py +141 -0
  83. janito/workspace/stats.py +43 -0
  84. janito/workspace/types.py +98 -0
  85. janito/workspace/workset.py +108 -0
  86. janito/workspace/workspace.py +114 -0
  87. janito-0.7.0.dist-info/METADATA +167 -0
  88. janito-0.7.0.dist-info/RECORD +96 -0
  89. {janito-0.5.0.dist-info → janito-0.7.0.dist-info}/WHEEL +1 -1
  90. janito/_contextparser.py +0 -113
  91. janito/analysis/display.py +0 -149
  92. janito/analysis/options.py +0 -112
  93. janito/change/applier.py +0 -269
  94. janito/change/content.py +0 -62
  95. janito/change/indentation.py +0 -33
  96. janito/change/position.py +0 -169
  97. janito/changeviewer/panels.py +0 -268
  98. janito/changeviewer/styling.py +0 -59
  99. janito/console/__init__.py +0 -3
  100. janito/console/commands.py +0 -112
  101. janito/console/core.py +0 -62
  102. janito/console/display.py +0 -157
  103. janito/fileparser.py +0 -334
  104. janito/prompts.py +0 -81
  105. janito/scan.py +0 -176
  106. janito/tests/test_fileparser.py +0 -26
  107. janito-0.5.0.dist-info/METADATA +0 -146
  108. janito-0.5.0.dist-info/RECORD +0 -45
  109. {janito-0.5.0.dist-info → janito-0.7.0.dist-info}/entry_points.txt +0 -0
  110. {janito-0.5.0.dist-info → janito-0.7.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,66 @@
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
8
10
  from janito.config import config
11
+ from janito.workspace import workset
12
+ from janito.workspace.types import ScanType # Add this import
13
+ from .cli.commands import (
14
+ handle_request, handle_ask, handle_play,
15
+ handle_scan, handle_demo
16
+ )
9
17
 
10
- from .cli.commands import handle_request, handle_ask, handle_play, handle_scan
18
+ app = typer.Typer(pretty_exceptions_enable=False)
11
19
 
12
- app = typer.Typer(add_completion=False)
20
+ def validate_paths(paths: Optional[List[Path]]) -> Optional[List[Path]]:
21
+ """Validate include paths for duplicates.
22
+
23
+ Args:
24
+ paths: List of paths to validate, or None if no paths provided
25
+
26
+ Returns:
27
+ Validated list of paths or None if no paths provided
28
+ """
29
+ if not paths: # This handles both None and empty list cases
30
+ return None
31
+
32
+ # Convert paths to absolute and resolve symlinks
33
+ resolved_paths: Set[Path] = set()
34
+ unique_paths: List[Path] = []
35
+
36
+ for path in paths:
37
+ resolved = path.absolute().resolve()
38
+ if resolved in resolved_paths:
39
+ error_text = Text(f"\nError: Duplicate path provided: {path} ", style="red")
40
+ rich_print(error_text)
41
+ raise typer.Exit(1)
42
+ resolved_paths.add(resolved)
43
+ unique_paths.append(path)
44
+
45
+ return unique_paths if unique_paths else None
13
46
 
14
47
  def typer_main(
15
48
  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),
49
+ workspace_dir: Optional[Path] = typer.Option(None, "-w", "--workspace_dir", help="Working directory", file_okay=False, dir_okay=True),
17
50
  debug: bool = typer.Option(False, "--debug", help="Show debug information"),
18
51
  verbose: bool = typer.Option(False, "--verbose", help="Show verbose output"),
19
52
  include: Optional[List[Path]] = typer.Option(None, "-i", "--include", help="Additional paths to include"),
20
53
  ask: Optional[str] = typer.Option(None, "--ask", help="Ask a question about the codebase"),
21
54
  play: Optional[Path] = typer.Option(None, "--play", help="Replay a saved prompt file"),
55
+ scan: bool = typer.Option(False, "--scan", help="Preview files that would be analyzed"),
22
56
  version: bool = typer.Option(False, "--version", help="Show version information"),
57
+ test_cmd: Optional[str] = typer.Option(None, "--test", help="Command to run tests after changes"),
58
+ auto_apply: bool = typer.Option(False, "--auto-apply", help="Apply changes without confirmation"),
59
+ tui: bool = typer.Option(False, "--tui", help="Use terminal user interface"),
60
+ history: bool = typer.Option(False, "--history", help="Display history of requests"),
61
+ recursive: Optional[List[Path]] = typer.Option(None, "-r", "--recursive", help="Paths to scan recursively (directories only)"),
62
+ demo: bool = typer.Option(False, "--demo", help="Run demo scenarios"),
63
+ skip_work: bool = typer.Option(False, "--skip-work", help="Skip scanning workspace_dir when using include paths"),
23
64
  ):
24
65
  """Janito - AI-powered code modification assistant"""
25
66
  if version:
@@ -27,28 +68,75 @@ def typer_main(
27
68
  console.print(f"Janito version {get_version()}")
28
69
  return
29
70
 
30
- workdir = workdir or Path.cwd()
71
+ if demo:
72
+ handle_demo()
73
+ return
74
+
75
+ if history:
76
+ from janito.cli.history import display_history
77
+ display_history()
78
+ return
79
+
80
+ # Configure workspace
81
+ config.set_workspace_dir(workspace_dir)
31
82
  config.set_debug(debug)
32
83
  config.set_verbose(verbose)
84
+ config.set_auto_apply(auto_apply)
85
+ config.set_tui(tui)
33
86
 
34
- agent = AgentSingleton.get_agent()
87
+ # Configure workset with scan paths
88
+ if include:
89
+ if config.debug:
90
+ Console(stderr=True).print("[cyan]Debug: Processing include paths...[/cyan]")
91
+ for path in include:
92
+ full_path = config.workspace_dir / path
93
+ if not full_path.resolve().is_relative_to(config.workspace_dir):
94
+ error_text = Text(f"\nError: Path must be within workspace: {path}", style="red")
95
+ rich_print(error_text)
96
+ raise typer.Exit(1)
97
+ workset.add_scan_path(path, ScanType.PLAIN)
98
+
99
+ if recursive:
100
+ if config.debug:
101
+ Console(stderr=True).print("[cyan]Debug: Processing recursive paths...[/cyan]")
102
+ for path in recursive:
103
+ full_path = config.workspace_dir / path
104
+ if not path.is_dir():
105
+ error_text = Text(f"\nError: Recursive path must be a directory: {path} ", style="red")
106
+ rich_print(error_text)
107
+ raise typer.Exit(1)
108
+ if not full_path.resolve().is_relative_to(config.workspace_dir):
109
+ error_text = Text(f"\nError: Path must be within workspace: {path}", style="red")
110
+ rich_print(error_text)
111
+ raise typer.Exit(1)
112
+ workset.add_scan_path(path, ScanType.RECURSIVE)
113
+
114
+ # Validate skip_work usage
115
+ if skip_work and not workset.paths:
116
+ error_text = Text("\nError: --skip-work requires at least one include path (-i or -r)", style="red")
117
+ rich_print(error_text)
118
+ raise typer.Exit(1)
119
+
120
+ if test_cmd:
121
+ config.set_test_cmd(test_cmd)
122
+
123
+ # Refresh workset content before handling commands
124
+ workset.refresh()
35
125
 
36
126
  if ask:
37
- handle_ask(ask, workdir, include, False, agent)
127
+ handle_ask(ask)
38
128
  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)
129
+ handle_play(play)
130
+ elif scan:
131
+ handle_scan()
43
132
  elif change_request:
44
- handle_request(change_request, workdir, include, False, agent)
133
+ handle_request(change_request)
45
134
  else:
46
- console = Console()
47
- console.print("Error: Please provide a change request or use --ask/--play options")
48
- raise typer.Exit(1)
135
+ from janito.shell import start_shell
136
+ start_shell()
49
137
 
50
138
  def main():
51
139
  typer.run(typer_main)
52
140
 
53
141
  if __name__ == "__main__":
54
- main()
142
+ main()
janito/agents/__init__.py CHANGED
@@ -5,18 +5,18 @@ SYSTEM_PROMPT = """I am Janito, your friendly software development buddy. I help
5
5
  ai_backend = os.getenv('AI_BACKEND', 'claudeai').lower()
6
6
 
7
7
  if ai_backend == 'openai':
8
+ import warnings
9
+ warnings.warn(
10
+ "Using deprecated OpenAI backend. Please switch to Claude AI backend by removing AI_BACKEND=openai "
11
+ "from your environment variables.",
12
+ DeprecationWarning,
13
+ stacklevel=2
14
+ )
8
15
  from .openai import OpenAIAgent as AIAgent
9
16
  elif ai_backend == 'claudeai':
10
17
  from .claudeai import ClaudeAIAgent as AIAgent
11
18
  else:
12
19
  raise ValueError(f"Unsupported AI_BACKEND: {ai_backend}")
13
20
 
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
22
-
21
+ # Create a singleton instance
22
+ agent = AIAgent(SYSTEM_PROMPT)
janito/agents/agent.py CHANGED
@@ -1,4 +1,3 @@
1
-
2
1
  from abc import ABC, abstractmethod
3
2
  from threading import Event
4
3
  from typing import Optional, List, Tuple
@@ -16,6 +15,14 @@ class Agent(ABC):
16
15
  self.messages_history.append(("system", system_prompt))
17
16
 
18
17
  @abstractmethod
19
- def send_message(self, message: str, stop_event: Event = None) -> str:
20
- """Send message to AI service and return response"""
18
+ def send_message(self, message: str, system: str) -> str:
19
+ """Send message to the AI agent
20
+
21
+ Args:
22
+ message: The message to send
23
+ stop_event: Optional event to signal cancellation
24
+
25
+ Returns:
26
+ The response from the AI agent
27
+ """
21
28
  pass
janito/agents/claudeai.py CHANGED
@@ -22,43 +22,24 @@ 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))
28
25
 
29
- def send_message(self, message: str, stop_event: Event = None) -> str:
26
+
27
+ def send_message(self, message: str, system_message: str = None) -> str:
30
28
  """Send message to Claude API and return response"""
31
29
  self.messages_history.append(("user", message))
32
30
  # Store the full message
33
31
  self.last_full_message = message
34
32
 
35
- try:
36
- # Check if already cancelled
37
- if stop_event and stop_event.is_set():
38
- return ""
39
-
40
- response = self.client.messages.create(
41
- model=self.model, # Use discovered model
42
- system=self.system_message,
43
- max_tokens=8192,
44
- messages=[
45
- {"role": "user", "content": message}
46
- ],
47
- temperature=0,
48
- )
49
-
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
-
58
- # Always return the response, let caller handle cancellation
59
- return response_text
60
-
61
- except KeyboardInterrupt:
62
- if stop_event:
63
- stop_event.set()
64
- return ""
33
+ response = self.client.messages.create(
34
+ model=self.model, # Use discovered model
35
+ system=system_message or self.system_message,
36
+ max_tokens=8192,
37
+ messages=[
38
+ {"role": "user", "content": message}
39
+ ],
40
+ temperature=0,
41
+ )
42
+
43
+
44
+ # Always return the response, let caller handle cancellation
45
+ return response
janito/agents/openai.py CHANGED
@@ -5,7 +5,11 @@ from threading import Event
5
5
  from .agent import Agent
6
6
 
7
7
  class OpenAIAgent(Agent):
8
- """Handles interaction with OpenAI API, including message handling"""
8
+ """[DEPRECATED] Handles interaction with OpenAI API, including message handling.
9
+
10
+ This backend is no longer actively maintained. Please use the Claude AI backend instead.
11
+ The code is kept for backward compatibility but may be removed in future versions.
12
+ """
9
13
  DEFAULT_MODEL = "o1-mini-2024-09-12"
10
14
 
11
15
  def __init__(self, api_key: Optional[str] = None, system_prompt: str = None):
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,62 @@
1
+ """Core analysis functionality."""
2
+
3
+ from typing import Optional
4
+ from janito.agents import agent
5
+ from janito.common import progress_send_message
6
+ from janito.config import config
7
+ from janito.workspace.workset import Workset
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
+ pre_select: str = ""
19
+ ) -> Optional[AnalysisOption]:
20
+ """
21
+ Analyze changes and get user selection.
22
+
23
+ Args:
24
+ request: User's change request
25
+ files_content_xml: Optional content of files to analyze
26
+ pre_select: Optional pre-selected option letter
27
+
28
+ Returns:
29
+ Selected AnalysisOption or None if modified
30
+ """
31
+ workset = Workset() # Create workset instance
32
+
33
+ # Build and send prompt using workset content directly
34
+ prompt = build_request_analysis_prompt(request)
35
+ response = progress_send_message(prompt)
36
+
37
+ # Parse and handle options
38
+ options = parse_analysis_options(response)
39
+ if not options:
40
+ return None
41
+
42
+ if pre_select:
43
+ return options.get(pre_select.upper())
44
+
45
+ if config.tui:
46
+ from janito.tui import TuiApp
47
+ app = TuiApp(options=options)
48
+ app.run()
49
+ return app.selected_option
50
+
51
+ # Display formatted analysis in terminal mode
52
+ format_analysis(response, config.raw)
53
+
54
+ # Get user selection
55
+ while True:
56
+ selection = get_option_selection()
57
+
58
+ if selection == 'M':
59
+ return None
60
+
61
+ if validate_option_letter(selection, options):
62
+ 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