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.
- janito/__init__.py +0 -47
- janito/__main__.py +105 -17
- janito/agents/__init__.py +9 -9
- janito/agents/agent.py +10 -3
- janito/agents/claudeai.py +15 -34
- janito/agents/openai.py +5 -1
- janito/change/__init__.py +29 -16
- janito/change/__main__.py +0 -0
- janito/{analysis → change/analysis}/__init__.py +5 -15
- janito/change/analysis/__main__.py +7 -0
- janito/change/analysis/analyze.py +62 -0
- janito/change/analysis/formatting.py +78 -0
- janito/change/analysis/options.py +81 -0
- janito/{analysis → change/analysis}/prompts.py +33 -18
- janito/change/analysis/view/__init__.py +9 -0
- janito/change/analysis/view/terminal.py +181 -0
- janito/change/applier/__init__.py +5 -0
- janito/change/applier/file.py +58 -0
- janito/change/applier/main.py +156 -0
- janito/change/applier/text.py +247 -0
- janito/change/applier/workspace_dir.py +58 -0
- janito/change/core.py +124 -0
- janito/{changehistory.py → change/history.py} +12 -14
- janito/change/operations.py +7 -0
- janito/change/parser.py +287 -0
- janito/change/play.py +54 -0
- janito/change/preview.py +82 -0
- janito/change/prompts.py +121 -0
- janito/change/test.py +0 -0
- janito/change/validator.py +269 -0
- janito/{changeviewer → change/viewer}/__init__.py +3 -4
- janito/change/viewer/content.py +66 -0
- janito/{changeviewer → change/viewer}/diff.py +19 -4
- janito/change/viewer/panels.py +533 -0
- janito/change/viewer/styling.py +114 -0
- janito/{changeviewer → change/viewer}/themes.py +3 -5
- janito/clear_statement_parser/clear_statement_format.txt +328 -0
- janito/clear_statement_parser/examples.txt +326 -0
- janito/clear_statement_parser/models.py +104 -0
- janito/clear_statement_parser/parser.py +496 -0
- janito/cli/base.py +30 -0
- janito/cli/commands.py +75 -40
- janito/cli/functions.py +19 -194
- janito/cli/history.py +61 -0
- janito/common.py +65 -8
- janito/config.py +70 -5
- janito/demo/__init__.py +4 -0
- janito/demo/data.py +13 -0
- janito/demo/mock_data.py +20 -0
- janito/demo/operations.py +45 -0
- janito/demo/runner.py +59 -0
- janito/demo/scenarios.py +32 -0
- janito/prompt.py +36 -0
- janito/qa.py +6 -14
- janito/search_replace/README.md +192 -0
- janito/search_replace/__init__.py +7 -0
- janito/search_replace/__main__.py +21 -0
- janito/search_replace/core.py +120 -0
- janito/search_replace/logger.py +35 -0
- janito/search_replace/parser.py +52 -0
- janito/search_replace/play.py +61 -0
- janito/search_replace/replacer.py +36 -0
- janito/search_replace/searcher.py +411 -0
- janito/search_replace/strategy_result.py +10 -0
- janito/shell/__init__.py +38 -0
- janito/shell/bus.py +31 -0
- janito/shell/commands.py +136 -0
- janito/shell/history.py +20 -0
- janito/shell/processor.py +32 -0
- janito/shell/prompt.py +48 -0
- janito/shell/registry.py +60 -0
- janito/tui/__init__.py +21 -0
- janito/tui/base.py +22 -0
- janito/tui/flows/__init__.py +5 -0
- janito/tui/flows/changes.py +65 -0
- janito/tui/flows/content.py +128 -0
- janito/tui/flows/selection.py +117 -0
- janito/tui/screens/__init__.py +3 -0
- janito/tui/screens/app.py +1 -0
- janito/workspace/__init__.py +6 -0
- janito/workspace/analysis.py +121 -0
- janito/workspace/show.py +141 -0
- janito/workspace/stats.py +43 -0
- janito/workspace/types.py +98 -0
- janito/workspace/workset.py +108 -0
- janito/workspace/workspace.py +114 -0
- janito-0.7.0.dist-info/METADATA +167 -0
- janito-0.7.0.dist-info/RECORD +96 -0
- {janito-0.5.0.dist-info → janito-0.7.0.dist-info}/WHEEL +1 -1
- janito/_contextparser.py +0 -113
- janito/analysis/display.py +0 -149
- janito/analysis/options.py +0 -112
- janito/change/applier.py +0 -269
- janito/change/content.py +0 -62
- janito/change/indentation.py +0 -33
- janito/change/position.py +0 -169
- janito/changeviewer/panels.py +0 -268
- janito/changeviewer/styling.py +0 -59
- janito/console/__init__.py +0 -3
- janito/console/commands.py +0 -112
- janito/console/core.py +0 -62
- janito/console/display.py +0 -157
- janito/fileparser.py +0 -334
- janito/prompts.py +0 -81
- janito/scan.py +0 -176
- janito/tests/test_fileparser.py +0 -26
- janito-0.5.0.dist-info/METADATA +0 -146
- janito-0.5.0.dist-info/RECORD +0 -45
- {janito-0.5.0.dist-info → janito-0.7.0.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
18
|
+
app = typer.Typer(pretty_exceptions_enable=False)
|
11
19
|
|
12
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
127
|
+
handle_ask(ask)
|
38
128
|
elif play:
|
39
|
-
handle_play(play
|
40
|
-
elif
|
41
|
-
|
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
|
133
|
+
handle_request(change_request)
|
45
134
|
else:
|
46
|
-
|
47
|
-
|
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
|
-
|
15
|
-
|
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,
|
20
|
-
"""Send message to AI
|
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
|
-
|
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
|
-
|
36
|
-
#
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
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
|
-
'
|
13
|
-
'
|
14
|
-
'
|
15
|
-
'
|
16
|
-
'
|
17
|
-
'
|
18
|
-
'
|
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 .
|
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,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
|